Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
trixi-framework
GitHub Repository: trixi-framework/Trixi.jl
Path: blob/main/src/semidiscretization/semidiscretization.jl
2055 views
1
# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
2
# Since these FMAs can increase the performance of many numerical algorithms,
3
# we need to opt-in explicitly.
4
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
5
@muladd begin
6
#! format: noindent
7
8
"""
9
ndofs(semi::AbstractSemidiscretization)
10
11
Return the number of degrees of freedom associated with each scalar variable.
12
"""
13
@inline function ndofs(semi::AbstractSemidiscretization)
14
mesh, _, solver, cache = mesh_equations_solver_cache(semi)
15
ndofs(mesh, solver, cache)
16
end
17
18
"""
19
ndofsglobal(semi::AbstractSemidiscretization)
20
21
Return the global number of degrees of freedom associated with each scalar variable across all MPI ranks.
22
This is the same as [`ndofs`](@ref) for simulations running in serial or
23
parallelized via threads. It will in general be different for simulations
24
running in parallel with MPI.
25
"""
26
@inline function ndofsglobal(semi::AbstractSemidiscretization)
27
mesh, _, solver, cache = mesh_equations_solver_cache(semi)
28
ndofsglobal(mesh, solver, cache)
29
end
30
31
"""
32
integrate_via_indices(func, u_ode, semi::AbstractSemidiscretization, args...; normalize=true)
33
34
Call `func(u, i..., element, equations, solver, args...)` for all nodal indices `i..., element`
35
and integrate the result using a quadrature associated with the semidiscretization `semi`.
36
37
If `normalize` is true, the result is divided by the total volume of the computational domain.
38
"""
39
function integrate_via_indices(func::Func, u_ode, semi::AbstractSemidiscretization,
40
args...; normalize = true) where {Func}
41
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
42
43
u = wrap_array(u_ode, mesh, equations, solver, cache)
44
integrate_via_indices(func, u, mesh, equations, solver, cache, args...,
45
normalize = normalize)
46
end
47
48
"""
49
integrate([func=(u_node,equations)->u_node,] u_ode, semi::AbstractSemidiscretization; normalize=true)
50
51
Call `func(u_node, equations)` for each vector of nodal variables `u_node` in `u_ode`
52
and integrate the result using a quadrature associated with the semidiscretization `semi`.
53
54
If `normalize` is true, the result is divided by the total volume of the computational domain.
55
"""
56
function integrate(func::Func, u_ode, semi::AbstractSemidiscretization;
57
normalize = true) where {Func}
58
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
59
60
u = wrap_array(u_ode, mesh, equations, solver, cache)
61
integrate(func, u, mesh, equations, solver, cache, normalize = normalize)
62
end
63
64
function integrate(u_ode, semi::AbstractSemidiscretization; normalize = true)
65
integrate(cons2cons, u_ode, semi; normalize = normalize)
66
end
67
68
"""
69
calc_error_norms([func=(u_node,equations)->u_node,] u_ode, t, analyzer, semi::AbstractSemidiscretization, cache_analysis)
70
71
Calculate discrete L2 and L∞ error norms of `func` applied to each nodal variable `u_node` in `u_ode`.
72
If no exact solution is available, "errors" are calculated using some reference state and can be useful
73
for regression tests.
74
"""
75
function calc_error_norms(u_ode, t, analyzer, semi::AbstractSemidiscretization,
76
cache_analysis)
77
calc_error_norms(cons2cons, u_ode, t, analyzer, semi, cache_analysis)
78
end
79
80
"""
81
semidiscretize(semi::AbstractSemidiscretization, tspan;
82
jac_prototype::Union{AbstractMatrix, Nothing} = nothing,
83
colorvec::Union{AbstractVector, Nothing} = nothing,
84
storage_type = nothing,
85
real_type = nothing)
86
87
Wrap the semidiscretization `semi` as an ODE problem in the time interval `tspan`
88
that can be passed to `solve` from the [SciML ecosystem](https://diffeq.sciml.ai/latest/).
89
90
Optional keyword arguments:
91
- `jac_prototype`: Expected to come from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl).
92
Specifies the sparsity structure of the Jacobian to enable e.g. efficient implicit time stepping.
93
- `colorvec`: Expected to come from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl).
94
Allows for even faster Jacobian computation if a sparse `jac_prototype` is given (optional).
95
- `storage_type` and `real_type`: Configure the underlying computational datastructures.
96
`storage_type` changes the fundamental array type being used, allowing the experimental use of `CuArray`
97
or other GPU array types. `real_type` changes the computational data type being used.
98
"""
99
function semidiscretize(semi::AbstractSemidiscretization, tspan;
100
jac_prototype::Union{AbstractMatrix, Nothing} = nothing,
101
colorvec::Union{AbstractVector, Nothing} = nothing,
102
reset_threads = true,
103
storage_type = nothing,
104
real_type = nothing)
105
# Optionally reset Polyester.jl threads. See
106
# https://github.com/trixi-framework/Trixi.jl/issues/1583
107
# https://github.com/JuliaSIMD/Polyester.jl/issues/30
108
if reset_threads
109
Polyester.reset_threads!()
110
end
111
112
if !(storage_type === nothing && real_type === nothing)
113
if storage_type === nothing
114
storage_type = Array
115
end
116
if real_type === nothing
117
real_type = real(semi)
118
end
119
semi = trixi_adapt(storage_type, real_type, semi)
120
if eltype(tspan) !== real_type
121
tspan = convert.(real_type, tspan)
122
end
123
end
124
125
u0_ode = compute_coefficients(first(tspan), semi) # Invoke initial condition
126
127
# TODO: MPI, do we want to synchronize loading and print debug statements, e.g. using
128
# mpi_isparallel() && MPI.Barrier(mpi_comm())
129
# See https://github.com/trixi-framework/Trixi.jl/issues/328
130
iip = true # is-inplace, i.e., we modify a vector when calling rhs!
131
specialize = SciMLBase.FullSpecialize # specialize on rhs! and parameters (semi)
132
133
# Check if Jacobian prototype is provided for sparse Jacobian
134
if jac_prototype !== nothing
135
# Convert `jac_prototype` to real type, as seen here:
136
# https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Declaring-a-Sparse-Jacobian-with-Automatic-Sparsity-Detection
137
ode = SciMLBase.ODEFunction(rhs!,
138
jac_prototype = convert.(eltype(u0_ode),
139
jac_prototype),
140
colorvec = colorvec) # coloring vector is optional
141
142
return ODEProblem{iip, specialize}(ode, u0_ode, tspan, semi)
143
else
144
# We could also construct an `ODEFunction` without the Jacobian here,
145
# but we stick to the more light-weight direct in-place function `rhs!`.
146
return ODEProblem{iip, specialize}(rhs!, u0_ode, tspan, semi)
147
end
148
end
149
150
"""
151
semidiscretize(semi::AbstractSemidiscretization, tspan,
152
restart_file::AbstractString;
153
jac_prototype::Union{AbstractMatrix, Nothing} = nothing,
154
colorvec::Union{AbstractVector, Nothing} = nothing)
155
156
Wrap the semidiscretization `semi` as an ODE problem in the time interval `tspan`
157
that can be passed to `solve` from the [SciML ecosystem](https://diffeq.sciml.ai/latest/).
158
The initial condition etc. is taken from the `restart_file`.
159
160
Optional keyword arguments:
161
- `jac_prototype`: Expected to come from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl).
162
Specifies the sparsity structure of the Jacobian to enable e.g. efficient implicit time stepping.
163
- `colorvec`: Expected to come from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl).
164
Allows for even faster Jacobian computation. Not necessarily required when `jac_prototype` is given.
165
"""
166
function semidiscretize(semi::AbstractSemidiscretization, tspan,
167
restart_file::AbstractString;
168
jac_prototype::Union{AbstractMatrix, Nothing} = nothing,
169
colorvec::Union{AbstractVector, Nothing} = nothing,
170
reset_threads = true)
171
# Optionally reset Polyester.jl threads. See
172
# https://github.com/trixi-framework/Trixi.jl/issues/1583
173
# https://github.com/JuliaSIMD/Polyester.jl/issues/30
174
if reset_threads
175
Polyester.reset_threads!()
176
end
177
178
u0_ode = load_restart_file(semi, restart_file) # Load initial condition from restart file
179
180
# TODO: MPI, do we want to synchronize loading and print debug statements, e.g. using
181
# mpi_isparallel() && MPI.Barrier(mpi_comm())
182
# See https://github.com/trixi-framework/Trixi.jl/issues/328
183
iip = true # is-inplace, i.e., we modify a vector when calling rhs!
184
specialize = SciMLBase.FullSpecialize # specialize on rhs! and parameters (semi)
185
186
# Check if Jacobian prototype is provided for sparse Jacobian
187
if jac_prototype !== nothing
188
# Convert `jac_prototype` to real type, as seen here:
189
# https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Declaring-a-Sparse-Jacobian-with-Automatic-Sparsity-Detection
190
ode = SciMLBase.ODEFunction(rhs!,
191
jac_prototype = convert.(eltype(u0_ode),
192
jac_prototype),
193
colorvec = colorvec) # coloring vector is optional
194
195
return ODEProblem{iip, specialize}(ode, u0_ode, tspan, semi)
196
else
197
# We could also construct an `ODEFunction` without the Jacobian here,
198
# but we stick to the more light-weight direct in-place function `rhs!`.
199
return ODEProblem{iip, specialize}(rhs!, u0_ode, tspan, semi)
200
end
201
end
202
203
"""
204
compute_coefficients(func, t, semi::AbstractSemidiscretization)
205
206
Compute the discrete coefficients of the continuous function `func` at time `t`
207
associated with the semidiscretization `semi`.
208
For example, the discrete coefficients of `func` for a discontinuous Galerkin
209
spectral element method ([`DGSEM`](@ref)) are the values of `func` at the
210
Lobatto-Legendre nodes. Similarly, a classical finite difference method will use
211
the values of `func` at the nodes of the grid assoociated with the semidiscretization
212
`semi`.
213
214
For semidiscretizations `semi` associated with an initial condition, `func` can be omitted
215
to use the given initial condition at time `t`.
216
"""
217
function compute_coefficients(func, t, semi::AbstractSemidiscretization)
218
u_ode = allocate_coefficients(mesh_equations_solver_cache(semi)...)
219
# Call `compute_coefficients` defined below
220
compute_coefficients!(u_ode, func, t, semi)
221
return u_ode
222
end
223
224
"""
225
compute_coefficients!(u_ode, func, t, semi::AbstractSemidiscretization)
226
227
Same as [`compute_coefficients`](@ref) but stores the result in `u_ode`.
228
"""
229
function compute_coefficients!(u_ode, func, t, semi::AbstractSemidiscretization)
230
backend = trixi_backend(u_ode)
231
u = wrap_array(u_ode, semi)
232
# Call `compute_coefficients` defined by the solver
233
compute_coefficients!(backend, u, func, t, mesh_equations_solver_cache(semi)...)
234
end
235
236
"""
237
linear_structure(semi::AbstractSemidiscretization;
238
t0=zero(real(semi)))
239
240
Wraps the right-hand side operator of the semidiscretization `semi`
241
at time `t0` as an affine-linear operator given by a linear operator `A`
242
and a vector `b`.
243
"""
244
function linear_structure(semi::AbstractSemidiscretization;
245
t0 = zero(real(semi)))
246
# allocate memory
247
u_ode = allocate_coefficients(mesh_equations_solver_cache(semi)...)
248
du_ode = similar(u_ode)
249
250
# get the right hand side from possible source terms
251
u_ode .= zero(eltype(u_ode))
252
rhs!(du_ode, u_ode, semi, t0)
253
# Create a copy of `b` used internally to extract the linear part of `semi`.
254
# This is necessary to get everything correct when the users updates the
255
# returned vector `b`.
256
b = -du_ode
257
b_tmp = copy(b)
258
259
# wrap the linear operator
260
A = LinearMap(length(u_ode), ismutating = true) do dest, src
261
rhs!(dest, src, semi, t0)
262
@. dest += b_tmp
263
dest
264
end
265
266
return A, b
267
end
268
269
"""
270
jacobian_fd(semi::AbstractSemidiscretization;
271
t0=zero(real(semi)),
272
u0_ode=compute_coefficients(t0, semi))
273
274
Uses the right-hand side operator of the semidiscretization `semi`
275
and simple second order finite difference to compute the Jacobian `J`
276
of the semidiscretization `semi` at time `t0` and state `u0_ode`.
277
"""
278
function jacobian_fd(semi::AbstractSemidiscretization;
279
t0 = zero(real(semi)),
280
u0_ode = compute_coefficients(t0, semi))
281
# copy the initial state since it will be modified in the following
282
u_ode = copy(u0_ode)
283
du0_ode = similar(u_ode)
284
dup_ode = similar(u_ode)
285
dum_ode = similar(u_ode)
286
287
# compute residual of linearization state
288
rhs!(du0_ode, u_ode, semi, t0)
289
290
# initialize Jacobian matrix
291
J = zeros(eltype(u_ode), length(u_ode), length(u_ode))
292
293
# use second order finite difference to estimate Jacobian matrix
294
for idx in eachindex(u0_ode)
295
# determine size of fluctuation
296
# This is the approach used by FiniteDiff.jl to compute the
297
# step size, which assures that the finite difference is accurate
298
# for very small and very large absolute values `u0_ode[idx]`.
299
# See https://github.com/trixi-framework/Trixi.jl/pull/2514#issuecomment-3190534904.
300
absstep = sqrt(eps(typeof(u0_ode[idx])))
301
relstep = absstep
302
epsilon = max(relstep * abs(u0_ode[idx]), absstep)
303
304
# plus fluctuation
305
u_ode[idx] = u0_ode[idx] + epsilon
306
rhs!(dup_ode, u_ode, semi, t0)
307
308
# minus fluctuation
309
u_ode[idx] = u0_ode[idx] - epsilon
310
rhs!(dum_ode, u_ode, semi, t0)
311
312
# restore linearization state
313
u_ode[idx] = u0_ode[idx]
314
315
# central second order finite difference
316
@. J[:, idx] = (dup_ode - dum_ode) / (2 * epsilon)
317
end
318
319
return J
320
end
321
322
"""
323
jacobian_ad_forward(semi::AbstractSemidiscretization;
324
t0=zero(real(semi)),
325
u0_ode=compute_coefficients(t0, semi))
326
327
Uses the right-hand side operator of the semidiscretization `semi`
328
and forward mode automatic differentiation to compute the Jacobian `J`
329
of the semidiscretization `semi` at time `t0` and state `u0_ode`.
330
"""
331
function jacobian_ad_forward(semi::AbstractSemidiscretization;
332
t0 = zero(real(semi)),
333
u0_ode = compute_coefficients(t0, semi))
334
jacobian_ad_forward(semi, t0, u0_ode)
335
end
336
337
# The following version is for plain arrays
338
function jacobian_ad_forward(semi::AbstractSemidiscretization, t0, u0_ode)
339
du_ode = similar(u0_ode)
340
config = ForwardDiff.JacobianConfig(nothing, du_ode, u0_ode)
341
342
# Use a function barrier since the generation of the `config` we use above
343
# is not type-stable
344
_jacobian_ad_forward(semi, t0, u0_ode, du_ode, config)
345
end
346
347
function _jacobian_ad_forward(semi, t0, u0_ode, du_ode, config)
348
new_semi = remake(semi, uEltype = eltype(config))
349
# Create anonymous function passed as first argument to `ForwardDiff.jacobian` to match
350
# `ForwardDiff.jacobian(f!, y::AbstractArray, x::AbstractArray,
351
# cfg::JacobianConfig = JacobianConfig(f!, y, x), check=Val{true}())`
352
J = ForwardDiff.jacobian(du_ode, u0_ode, config) do du_ode, u_ode
353
Trixi.rhs!(du_ode, u_ode, new_semi, t0)
354
end
355
356
return J
357
end
358
359
# unpack u if it is wrapped in VectorOfArray (mainly for DGMulti solvers)
360
jacobian_ad_forward(semi::AbstractSemidiscretization, t0, u0_ode::VectorOfArray) = jacobian_ad_forward(semi,
361
t0,
362
parent(u0_ode))
363
364
# This version is specialized to `StructArray`s used by some `DGMulti` solvers.
365
# We need to convert the numerical solution vectors since ForwardDiff cannot
366
# handle arrays of `SVector`s.
367
function jacobian_ad_forward(semi::AbstractSemidiscretization, t0, _u0_ode::StructArray)
368
u0_ode_plain = similar(_u0_ode, eltype(eltype(_u0_ode)),
369
(size(_u0_ode)..., nvariables(semi)))
370
for (v, u_v) in enumerate(StructArrays.components(_u0_ode))
371
u0_ode_plain[.., v] = u_v
372
end
373
du_ode_plain = similar(u0_ode_plain)
374
config = ForwardDiff.JacobianConfig(nothing, du_ode_plain, u0_ode_plain)
375
376
# Use a function barrier since the generation of the `config` we use above
377
# is not type-stable
378
_jacobian_ad_forward_structarrays(semi, t0, u0_ode_plain, du_ode_plain, config)
379
end
380
381
function _jacobian_ad_forward_structarrays(semi, t0, u0_ode_plain, du_ode_plain, config)
382
new_semi = remake(semi, uEltype = eltype(config))
383
# Create anonymous function passed as first argument to `ForwardDiff.jacobian` to match
384
# `ForwardDiff.jacobian(f!, y::AbstractArray, x::AbstractArray,
385
# cfg::JacobianConfig = JacobianConfig(f!, y, x), check=Val{true}())`
386
J = ForwardDiff.jacobian(du_ode_plain, u0_ode_plain,
387
config) do du_ode_plain, u_ode_plain
388
u_ode = StructArray{SVector{nvariables(semi), eltype(config)}}(ntuple(v -> view(u_ode_plain,
389
:,
390
:,
391
v),
392
nvariables(semi)))
393
du_ode = StructArray{SVector{nvariables(semi), eltype(config)}}(ntuple(v -> view(du_ode_plain,
394
:,
395
:,
396
v),
397
nvariables(semi)))
398
Trixi.rhs!(du_ode, u_ode, new_semi, t0)
399
end
400
401
return J
402
end
403
404
# This version is specialized to arrays of `StaticArray`s used by some `DGMulti` solvers.
405
# We need to convert the numerical solution vectors since ForwardDiff cannot
406
# handle arrays of `SVector`s.
407
function jacobian_ad_forward(semi::AbstractSemidiscretization, t0,
408
_u0_ode::AbstractArray{<:SVector})
409
u0_ode_plain = reinterpret(eltype(eltype(_u0_ode)), _u0_ode)
410
du_ode_plain = similar(u0_ode_plain)
411
config = ForwardDiff.JacobianConfig(nothing, du_ode_plain, u0_ode_plain)
412
413
# Use a function barrier since the generation of the `config` we use above
414
# is not type-stable
415
_jacobian_ad_forward_staticarrays(semi, t0, u0_ode_plain, du_ode_plain, config)
416
end
417
418
function _jacobian_ad_forward_staticarrays(semi, t0, u0_ode_plain, du_ode_plain, config)
419
new_semi = remake(semi, uEltype = eltype(config))
420
J = ForwardDiff.jacobian(du_ode_plain, u0_ode_plain,
421
config) do du_ode_plain, u_ode_plain
422
u_ode = reinterpret(SVector{nvariables(semi), eltype(config)}, u_ode_plain)
423
du_ode = reinterpret(SVector{nvariables(semi), eltype(config)}, du_ode_plain)
424
Trixi.rhs!(du_ode, u_ode, new_semi, t0)
425
end
426
427
return J
428
end
429
430
# Sometimes, it can be useful to save some (scalar) variables associated with each element,
431
# e.g. AMR indicators or shock indicators. Since these usually have to be re-computed
432
# directly before IO and do not necessarily need to be stored in memory before,
433
# get_element_variables!(element_variables, ..)
434
# is used to retrieve such up to date element variables, modifying
435
# `element_variables::Dict{Symbol,Any}` in place.
436
function get_element_variables!(element_variables, u_ode,
437
semi::AbstractSemidiscretization)
438
u = wrap_array(u_ode, semi)
439
get_element_variables!(element_variables, u, mesh_equations_solver_cache(semi)...)
440
end
441
442
# Required for storing `extra_node_variables` in the `SaveSolutionCallback`.
443
# Not to be confused with `get_node_vars` which returns the variables of the simulated equation.
444
function get_node_variables!(node_variables, u_ode, semi::AbstractSemidiscretization)
445
get_node_variables!(node_variables, u_ode, mesh_equations_solver_cache(semi)...)
446
end
447
448
# To implement AMR and use OrdinaryDiffEq.jl etc., we have to be a bit creative.
449
# Since the caches of the SciML ecosystem are immutable structs, we cannot simply
450
# change the underlying arrays therein. Hence, to support changing the number of
451
# DOFs, we need to use `resize!`. In some sense, this will force us to write more
452
# efficient code, since `resize!` will make use of previously allocated memory
453
# instead of allocating memory from scratch every time.
454
#
455
# However, multidimensional `Array`s don't support `resize!`. One option might be
456
# to use ElasticArrays.jl. But I don't really like that approach. Needing to use
457
# ElasticArray doesn't feel completely good to me, since we also want to experiment
458
# with other array types such as PaddedMatrices.jl, see trixi-framework/Trixi.jl#166.
459
# Then, we would need to wrap an Array inside something from PaddedMatrices.jl inside
460
# something from ElasticArrays.jl - or the other way round? Is that possible at all?
461
# If we go further, this looks like it could easily explode.
462
#
463
# Currently, the best option seems to be to let OrdinaryDiffEq.jl use `Vector`s,
464
# which can be `resize!`ed for AMR. Then, we have to wrap these `Vector`s inside
465
# Trixi.jl as our favorite multidimensional array type. We need to do this wrapping
466
# in every method exposed to OrdinaryDiffEq, i.e. in the first levels of things like
467
# rhs!, AMRCallback, StepsizeCallback, AnalysisCallback, SaveSolutionCallback
468
#
469
# This wrapping will also allow us to experiment more easily with additional
470
# kinds of wrapping, e.g. HybridArrays.jl or PaddedMatrices.jl to inform the
471
# compiler about the sizes of the first few dimensions in DG methods, i.e.
472
# nvariables(equations) and nnodes(dg).
473
#
474
# In some sense, having plain multidimensional `Array`s not support `resize!`
475
# isn't necessarily a bug (although it would be nice to add this possibility to
476
# base Julia) but can turn out to be a feature for us, because it will allow us
477
# more specializations.
478
# Since we can use multiple dispatch, these kinds of specializations can be
479
# tailored specifically to each combinations of mesh/solver etc.
480
#
481
# Under the hood, `wrap_array(u_ode, mesh, equations, solver, cache)` might
482
# (and probably will) use `unsafe_wrap`. Hence, you have to remember to
483
# `GC.@preserve` temporaries that are only used indirectly via `wrap_array`
484
# to avoid stochastic memory errors.
485
#
486
# Xref https://github.com/SciML/OrdinaryDiffEq.jl/pull/1275
487
function wrap_array(u_ode, semi::AbstractSemidiscretization)
488
wrap_array(u_ode, mesh_equations_solver_cache(semi)...)
489
end
490
491
# Like `wrap_array`, but guarantees to return a plain `Array`, which can be better
492
# for writing solution files etc.
493
function wrap_array_native(u_ode, semi::AbstractSemidiscretization)
494
wrap_array_native(u_ode, mesh_equations_solver_cache(semi)...)
495
end
496
497
# TODO: Taal, document interface?
498
# New mesh/solver combinations have to implement
499
# - ndofs(mesh, solver, cache)
500
# - ndofsgloabal(mesh, solver, cache)
501
# - ndims(mesh)
502
# - nnodes(solver)
503
# - real(solver)
504
# - create_cache(mesh, equations, solver, RealT)
505
# - wrap_array(u_ode, mesh, equations, solver, cache)
506
# - integrate(func, u, mesh, equations, solver, cache; normalize=true)
507
# - integrate_via_indices(func, u, mesh, equations, solver, cache, args...; normalize=true)
508
# - calc_error_norms(func, u, t, analyzer, mesh, equations, initial_condition, solver, cache, cache_analysis)
509
# - allocate_coefficients(mesh, equations, solver, cache)
510
# - compute_coefficients!(u, func, mesh, equations, solver, cache)
511
# - rhs!(du, u, t, mesh, equations, boundary_conditions, source_terms, solver, cache)
512
#
513
514
end # @muladd
515
516