Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
trixi-framework
GitHub Repository: trixi-framework/Trixi.jl
Path: blob/main/src/callbacks_step/analysis.jl
5586 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
# TODO: Taal refactor
9
# - analysis_interval part as PeriodicCallback called after a certain amount of simulation time
10
"""
11
AnalysisCallback(semi; interval=0,
12
save_analysis=false,
13
output_directory="out",
14
analysis_filename="analysis.dat",
15
extra_analysis_errors=Symbol[],
16
extra_analysis_integrals=())
17
18
Analyze a numerical solution every `interval` time steps and print the
19
results to the screen. If `save_analysis`, the results are also saved in
20
`joinpath(output_directory, analysis_filename)`.
21
22
Additional errors can be computed, e.g. by passing
23
`extra_analysis_errors = (:l2_error_primitive, :linf_error_primitive)`
24
or `extra_analysis_errors = (:conservation_error,)`.
25
26
If you want to omit the computation (to safe compute-time) of the [`default_analysis_errors`](@ref), specify
27
`analysis_errors = Symbol[]`.
28
Note: `default_analysis_errors` are `:l2_error` and `:linf_error` for all equations.
29
If you want to compute `extra_analysis_errors` such as `:conservation_error` solely, i.e.,
30
without `:l2_error, :linf_error` you need to specify
31
`analysis_errors = [:conservation_error]` instead of `extra_analysis_errors = [:conservation_error]`.
32
33
Further scalar functions `func` in `extra_analysis_integrals` are applied to the numerical
34
solution and integrated over the computational domain. Some examples for this are
35
[`entropy`](@ref), [`energy_kinetic`](@ref), [`energy_internal`](@ref), and [`energy_total`](@ref).
36
You can also write your own function with the same signature as the examples listed above and
37
pass it via `extra_analysis_integrals`.
38
See the developer comments about `Trixi.analyze`, `Trixi.pretty_form_utf`, and
39
`Trixi.pretty_form_ascii` for further information on how to create custom analysis quantities.
40
41
In addition, the analysis callback records and outputs a number of quantities that are useful for
42
evaluating the computational performance, such as the total runtime, the performance index
43
(time/DOF/rhs!), the time spent in garbage collection (GC), or the current memory usage (alloc'd
44
memory).
45
"""
46
mutable struct AnalysisCallback{Analyzer, AnalysisIntegrals, InitialStateIntegrals,
47
Cache}
48
start_time::Float64
49
start_time_last_analysis::Float64
50
ncalls_rhs_last_analysis::Int
51
start_gc_time::Float64
52
const interval::Int
53
const save_analysis::Bool
54
const output_directory::String
55
const analysis_filename::String
56
const analyzer::Analyzer
57
const analysis_errors::Vector{Symbol}
58
const analysis_integrals::AnalysisIntegrals
59
initial_state_integrals::InitialStateIntegrals
60
const cache::Cache
61
end
62
63
# TODO: Taal bikeshedding, implement a method with less information and the signature
64
# function Base.show(io::IO, analysis_callback::AnalysisCallback)
65
# end
66
function Base.show(io::IO, ::MIME"text/plain",
67
cb::DiscreteCallback{<:Any, <:AnalysisCallback})
68
@nospecialize cb # reduce precompilation time
69
70
if get(io, :compact, false)
71
show(io, cb)
72
else
73
analysis_callback = cb.affect!
74
75
setup = Pair{String, Any}["interval" => analysis_callback.interval,
76
"analyzer" => analysis_callback.analyzer]
77
for (idx, error) in enumerate(analysis_callback.analysis_errors)
78
push!(setup, "│ error " * string(idx) => error)
79
end
80
for (idx, integral) in enumerate(analysis_callback.analysis_integrals)
81
push!(setup, "│ integral " * string(idx) => integral)
82
end
83
push!(setup,
84
"save analysis to file" => analysis_callback.save_analysis ? "yes" : "no")
85
if analysis_callback.save_analysis
86
push!(setup, "│ filename" => analysis_callback.analysis_filename)
87
push!(setup,
88
"│ output directory" => abspath(normpath(analysis_callback.output_directory)))
89
end
90
summary_box(io, "AnalysisCallback", setup)
91
end
92
end
93
94
# This is the convenience constructor that gets called from the elixirs
95
function AnalysisCallback(semi::AbstractSemidiscretization; kwargs...)
96
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
97
return AnalysisCallback(mesh, equations, solver, cache; kwargs...)
98
end
99
100
# This is the actual constructor
101
function AnalysisCallback(mesh, equations::AbstractEquations, solver, cache;
102
interval = 0,
103
save_analysis = false,
104
output_directory = "out",
105
analysis_filename = "analysis.dat",
106
extra_analysis_errors = Symbol[],
107
analysis_errors = union(default_analysis_errors(equations),
108
extra_analysis_errors),
109
extra_analysis_integrals = (),
110
analysis_integrals = union(default_analysis_integrals(equations),
111
extra_analysis_integrals),
112
RealT = real(solver),
113
uEltype = eltype(cache.elements),
114
kwargs...)
115
# Decide when the callback is activated.
116
# With error-based step size control, some steps can be rejected. Thus,
117
# `integrator.iter >= integrator.stats.naccept`
118
# (total #steps) (#accepted steps)
119
# We need to check the number of accepted steps since callbacks are not
120
# activated after a rejected step.
121
condition = (u, t, integrator) -> interval > 0 &&
122
(integrator.stats.naccept % interval == 0 || isfinished(integrator))
123
124
analyzer = SolutionAnalyzer(solver; kwargs...)
125
cache_analysis = create_cache_analysis(analyzer, mesh, equations, solver, cache,
126
RealT, uEltype)
127
128
analysis_callback = AnalysisCallback(0.0, 0.0, 0, 0.0,
129
interval, save_analysis, output_directory,
130
analysis_filename,
131
analyzer,
132
analysis_errors, Tuple(analysis_integrals),
133
SVector(ntuple(_ -> zero(uEltype),
134
Val(nvariables(equations)))),
135
cache_analysis)
136
137
return DiscreteCallback(condition, analysis_callback,
138
save_positions = (false, false),
139
initialize = initialize!)
140
end
141
142
# This method gets called from OrdinaryDiffEq's `solve(...)`
143
function initialize!(cb::DiscreteCallback{Condition, Affect!}, u_ode, t,
144
integrator) where {Condition, Affect! <: AnalysisCallback}
145
semi = integrator.p
146
du_ode = first(get_tmp_cache(integrator))
147
return initialize!(cb, u_ode, du_ode, t, integrator, semi)
148
end
149
150
# This is the actual initialization method
151
# Note: we have this indirection to allow initializing a callback from the AnalysisCallbackCoupled
152
function initialize!(cb::DiscreteCallback{Condition, Affect!}, u_ode, du_ode, t,
153
integrator, semi) where {Condition, Affect! <: AnalysisCallback}
154
initial_state_integrals = integrate(u_ode, semi)
155
_, equations, _, _ = mesh_equations_solver_cache(semi)
156
157
analysis_callback = cb.affect!
158
analysis_callback.initial_state_integrals = initial_state_integrals
159
@unpack save_analysis, output_directory, analysis_filename, analysis_errors, analysis_integrals = analysis_callback
160
161
if save_analysis && mpi_isroot()
162
mkpath(output_directory)
163
164
# write header of output file
165
open(joinpath(output_directory, analysis_filename), "w") do io
166
print(io, "#timestep ")
167
print(io, "time ")
168
print(io, "dt ")
169
if :l2_error in analysis_errors
170
for v in varnames(cons2cons, equations)
171
print(io, "l2_" * v * " ")
172
end
173
end
174
if :linf_error in analysis_errors
175
for v in varnames(cons2cons, equations)
176
print(io, "linf_" * v * " ")
177
end
178
end
179
if :conservation_error in analysis_errors
180
for v in varnames(cons2cons, equations)
181
print(io, "cons_" * v * " ")
182
end
183
end
184
if :residual in analysis_errors
185
for v in varnames(cons2cons, equations)
186
print(io, "res_" * v * " ")
187
end
188
end
189
if :l2_error_primitive in analysis_errors
190
for v in varnames(cons2prim, equations)
191
print(io, "l2_" * v * " ")
192
end
193
end
194
if :linf_error_primitive in analysis_errors
195
for v in varnames(cons2prim, equations)
196
print(io, "linf_" * v * " ")
197
end
198
end
199
200
for quantity in analysis_integrals
201
print(io, pretty_form_ascii(quantity), " ")
202
end
203
204
println(io)
205
return nothing
206
end
207
end
208
209
# Record current time using a high-resolution clock
210
analysis_callback.start_time = time_ns()
211
212
# Record current time for performance index computation
213
analysis_callback.start_time_last_analysis = time_ns()
214
215
# Record current number of `rhs!` calls for performance index computation
216
analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)
217
218
# Record total time spent in garbage collection so far using a high-resolution clock
219
# Note: For details see the actual callback function below
220
analysis_callback.start_gc_time = Base.gc_time_ns()
221
222
analysis_callback(u_ode, du_ode, integrator, semi)
223
return nothing
224
end
225
226
# This method gets called from OrdinaryDiffEq's `solve(...)`
227
function (analysis_callback::AnalysisCallback)(integrator)
228
semi = integrator.p
229
du_ode = first(get_tmp_cache(integrator))
230
u_ode = integrator.u
231
return analysis_callback(u_ode, du_ode, integrator, semi)
232
end
233
234
# This method gets called internally as the main entry point to the AnalysiCallback
235
# TODO: Taal refactor, allow passing an IO object (which could be devnull to avoid cluttering the console)
236
function (analysis_callback::AnalysisCallback)(u_ode, du_ode, integrator, semi)
237
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
238
@unpack dt, t = integrator
239
iter = integrator.stats.naccept
240
241
# Compute the percentage of the simulation that is done
242
t = integrator.t
243
t_initial = first(integrator.sol.prob.tspan)
244
t_final = last(integrator.sol.prob.tspan)
245
sim_time_percentage = (t - t_initial) / (t_final - t_initial) * 100
246
247
# Record performance measurements and compute performance index (PID)
248
runtime_since_last_analysis = 1.0e-9 * (time_ns() -
249
analysis_callback.start_time_last_analysis)
250
# PID is an MPI-aware measure of how much time per global degree of freedom (i.e., over all ranks
251
# and threads) and per `rhs!` evaluation is required. MPI-aware means that it essentially adds up
252
# the time spent on each computing unit. Thus, in an ideally parallelized program, the PID should be constant
253
# independent of the number of MPI ranks or threads used, since, e.g., using 4x the number of ranks should
254
# divide the runtime on each rank by 4. See also the Trixi.jl docs ("Performance" section) for
255
# more information.
256
ncalls_rhs_since_last_analysis = (ncalls(semi.performance_counter)
257
-
258
analysis_callback.ncalls_rhs_last_analysis)
259
# This assumes that the same number of threads is used on each MPI rank.
260
performance_index = runtime_since_last_analysis * mpi_nranks() *
261
Threads.nthreads() /
262
(ndofsglobal(mesh, solver, cache)
263
*
264
ncalls_rhs_since_last_analysis)
265
266
# Compute the total runtime since the analysis callback has been initialized, in seconds
267
runtime_absolute = 1.0e-9 * (time_ns() - analysis_callback.start_time)
268
269
# Compute the relative runtime per thread as time spent in `rhs!` divided by the number of calls
270
# to `rhs!` and the number of local degrees of freedom
271
# OBS! This computation must happen *after* the PID computation above, since `take!(...)`
272
# will reset the number of calls to `rhs!`
273
runtime_relative = 1.0e-9 * take!(semi.performance_counter) * Threads.nthreads() /
274
ndofs(semi)
275
276
# Compute the total time spent in garbage collection since the analysis callback has been
277
# initialized, in seconds
278
# Note: `Base.gc_time_ns()` is not part of the public Julia API but has been available at least
279
# since Julia 1.6. Should this function be removed without replacement in a future Julia
280
# release, just delete this analysis quantity from the callback.
281
# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L83-L84
282
gc_time_absolute = 1.0e-9 * (Base.gc_time_ns() - analysis_callback.start_gc_time)
283
284
# Compute the percentage of total time that was spent in garbage collection
285
gc_time_percentage = gc_time_absolute / runtime_absolute * 100
286
287
# Obtain the current memory usage of the Julia garbage collector, in MiB, i.e., the total size of
288
# objects in memory that have been allocated by the JIT compiler or the user code.
289
# Note: `Base.gc_live_bytes()` is not part of the public Julia API but has been available at least
290
# since Julia 1.6. Should this function be removed without replacement in a future Julia
291
# release, just delete this analysis quantity from the callback.
292
# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L86-L97
293
memory_use = Base.gc_live_bytes() / 2^20 # bytes -> MiB
294
295
@trixi_timeit timer() "analyze solution" begin
296
# General information
297
mpi_println()
298
mpi_println("─"^100)
299
mpi_println(" Simulation running '", get_name(equations), "' with ",
300
summary(solver))
301
mpi_println("─"^100)
302
mpi_println(" #timesteps: " * @sprintf("% 14d", iter) *
303
" " *
304
" run time: " * @sprintf("%10.8e s", runtime_absolute))
305
mpi_println(" Δt: " * @sprintf("%10.8e", dt) *
306
" " *
307
" └── GC time: " *
308
@sprintf("%10.8e s (%5.3f%%)", gc_time_absolute, gc_time_percentage))
309
mpi_println(rpad(" sim. time: " *
310
@sprintf("%10.8e (%5.3f%%)", t, sim_time_percentage), 46) *
311
" time/DOF/rhs!: " * @sprintf("%10.8e s", runtime_relative))
312
mpi_println(" " * " " *
313
" " *
314
" PID: " * @sprintf("%10.8e s", performance_index))
315
mpi_println(" #DOFs per field:" * @sprintf("% 14d", ndofsglobal(semi)) *
316
" " *
317
" alloc'd memory: " * @sprintf("%14.3f MiB", memory_use))
318
mpi_println(" #elements: " *
319
@sprintf("% 14d", nelementsglobal(mesh, solver, cache)))
320
321
# Level information (only for AMR and/or non-uniform `TreeMesh`es)
322
print_level_information(integrator.opts.callback, mesh, solver, cache)
323
mpi_println()
324
325
# Open file for appending and store time step and time information
326
if mpi_isroot() && analysis_callback.save_analysis
327
io = open(joinpath(analysis_callback.output_directory,
328
analysis_callback.analysis_filename), "a")
329
print(io, iter)
330
print(io, " ", t)
331
print(io, " ", dt)
332
else
333
io = devnull
334
end
335
336
# Calculate current time derivative (needed for semidiscrete entropy time derivative, residual, etc.)
337
# `integrator.f` is usually just a call to `rhs!`
338
# However, we want to allow users to modify the ODE RHS outside of Trixi.jl
339
# and allow us to pass a combined ODE RHS to OrdinaryDiffEq, e.g., for
340
# hyperbolic-parabolic systems.
341
@notimeit timer() integrator.f(du_ode, u_ode, semi, t)
342
u = wrap_array(u_ode, mesh, equations, solver, cache)
343
du = wrap_array(du_ode, mesh, equations, solver, cache)
344
# Compute l2_error, linf_error
345
analysis_callback(io, du, u, u_ode, t, semi)
346
347
mpi_println("─"^100)
348
mpi_println()
349
350
# Add line break and close analysis file if it was opened
351
if mpi_isroot() && analysis_callback.save_analysis
352
# This resolves a possible type instability introduced above, since `io`
353
# can either be an `IOStream` or `devnull`, but we know that it must be
354
# an `IOStream here`.
355
println(io::IOStream)
356
close(io::IOStream)
357
end
358
end
359
360
# avoid re-evaluating possible FSAL stages
361
u_modified!(integrator, false)
362
363
# Reset performance measurements
364
analysis_callback.start_time_last_analysis = time_ns()
365
analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)
366
367
return nothing
368
end
369
370
# This method is just called internally from `(analysis_callback::AnalysisCallback)(integrator)`
371
# and serves as a function barrier. Additionally, it makes the code easier to profile and optimize.
372
function (analysis_callback::AnalysisCallback)(io, du, u, u_ode, t, semi)
373
@unpack analyzer, analysis_errors, analysis_integrals = analysis_callback
374
cache_analysis = analysis_callback.cache
375
_, equations, _, _ = mesh_equations_solver_cache(semi)
376
377
# Calculate and print derived quantities (error norms, entropy etc.)
378
# Variable names required for L2 error, Linf error, and conservation error
379
if any(q in analysis_errors
380
for q in (:l2_error, :linf_error, :conservation_error, :residual)) &&
381
mpi_isroot()
382
print(" Variable: ")
383
for v in eachvariable(equations)
384
@printf(" %-14s", varnames(cons2cons, equations)[v])
385
end
386
println()
387
end
388
389
if :l2_error in analysis_errors || :linf_error in analysis_errors
390
# Calculate L2/Linf errors
391
l2_error, linf_error = calc_error_norms(u_ode, t, analyzer, semi,
392
cache_analysis)
393
394
if mpi_isroot()
395
# L2 error
396
if :l2_error in analysis_errors
397
print(" L2 error: ")
398
for v in eachvariable(equations)
399
@printf(" % 10.8e", l2_error[v])
400
print(io, " ", l2_error[v])
401
end
402
println()
403
end
404
405
# Linf error
406
if :linf_error in analysis_errors
407
print(" Linf error: ")
408
for v in eachvariable(equations)
409
@printf(" % 10.8e", linf_error[v])
410
print(io, " ", linf_error[v])
411
end
412
println()
413
end
414
end
415
end
416
417
# Conservation error
418
if :conservation_error in analysis_errors
419
@unpack initial_state_integrals = analysis_callback
420
state_integrals = integrate(u_ode, semi)
421
422
if mpi_isroot()
423
print(" |∑U - ∑U₀|: ")
424
for v in eachvariable(equations)
425
err = abs(state_integrals[v] - initial_state_integrals[v])
426
@printf(" % 10.8e", err)
427
print(io, " ", err)
428
end
429
println()
430
end
431
end
432
433
# Residual (defined here as the vector maximum of the absolute values of the time derivatives)
434
if :residual in analysis_errors
435
mpi_print(" max(|Uₜ|): ")
436
for v in eachvariable(equations)
437
# Calculate maximum absolute value of Uₜ
438
res = maximum(abs, view(du, v, ..))
439
if mpi_isparallel()
440
# TODO: Debugging, here is a type instability
441
# Base.max instead of max needed, see comment in src/auxiliary/math.jl
442
global_res = MPI.Reduce!(Ref(res), Base.max, mpi_root(), mpi_comm())
443
if mpi_isroot()
444
res::eltype(du) = global_res[]
445
end
446
end
447
if mpi_isroot()
448
@printf(" % 10.8e", res)
449
print(io, " ", res)
450
end
451
end
452
mpi_println()
453
end
454
455
# L2/L∞ errors of the primitive variables
456
if :l2_error_primitive in analysis_errors ||
457
:linf_error_primitive in analysis_errors
458
l2_error_prim, linf_error_prim = calc_error_norms(cons2prim, u_ode, t, analyzer,
459
semi, cache_analysis)
460
461
if mpi_isroot()
462
print(" Variable: ")
463
for v in eachvariable(equations)
464
@printf(" %-14s", varnames(cons2prim, equations)[v])
465
end
466
println()
467
468
# L2 error
469
if :l2_error_primitive in analysis_errors
470
print(" L2 error prim.: ")
471
for v in eachvariable(equations)
472
@printf("%10.8e ", l2_error_prim[v])
473
print(io, " ", l2_error_prim[v])
474
end
475
println()
476
end
477
478
# L∞ error
479
if :linf_error_primitive in analysis_errors
480
print(" Linf error pri.:")
481
for v in eachvariable(equations)
482
@printf("%10.8e ", linf_error_prim[v])
483
print(io, " ", linf_error_prim[v])
484
end
485
println()
486
end
487
end
488
end
489
490
# additional integrals
491
analyze_integrals(analysis_integrals, io, du, u, t, semi)
492
493
return nothing
494
end
495
496
function print_level_information(mesh, solver, cache, min_level, max_level)
497
# Get local element count per level
498
elements_per_level = get_elements_per_level(min_level, max_level, mesh, solver,
499
cache)
500
501
# Sum up across all ranks
502
MPI.Reduce!(elements_per_level, +, mpi_root(), mpi_comm())
503
504
# Print
505
for level in max_level:-1:(min_level + 1)
506
mpi_println(" ├── level $level: " *
507
@sprintf("% 14d", elements_per_level[level + 1 - min_level]))
508
end
509
mpi_println(" └── level $min_level: " *
510
@sprintf("% 14d", elements_per_level[1]))
511
512
return nothing
513
end
514
515
function print_level_information(callbacks, mesh::TreeMesh, solver, cache)
516
if uses_amr(callbacks)
517
# Get global minimum and maximum level from the AMRController
518
min_level = max_level = 0
519
for cb in callbacks.discrete_callbacks
520
if cb.affect! isa AMRCallback
521
min_level = cb.affect!.controller.base_level
522
max_level = cb.affect!.controller.max_level
523
end
524
end
525
print_level_information(mesh, solver, cache, min_level, max_level)
526
# Special check for `TreeMesh`es without AMR.
527
# These meshes may still be non-uniform due to `refinement_patches`, see
528
# `refine_box!`, `coarsen_box!`, and `refine_sphere!`.
529
elseif minimum_level(mesh.tree) != maximum_level(mesh.tree)
530
min_level = minimum_level(mesh.tree)
531
max_level = maximum_level(mesh.tree)
532
print_level_information(mesh, solver, cache, min_level, max_level)
533
else # Uniform mesh
534
return nothing
535
end
536
end
537
538
function print_level_information(callbacks, mesh, solver, cache)
539
if uses_amr(callbacks)
540
# Get global minimum and maximum level from the AMRController
541
min_level = max_level = 0
542
for cb in callbacks.discrete_callbacks
543
if cb.affect! isa AMRCallback
544
min_level = cb.affect!.controller.base_level
545
max_level = cb.affect!.controller.max_level
546
end
547
end
548
print_level_information(mesh, solver, cache, min_level, max_level)
549
else # Uniform mesh
550
return nothing
551
end
552
end
553
554
function get_elements_per_level(min_level, max_level, mesh::P4estMesh, solver, cache)
555
elements_per_level = zeros(P4EST_MAXLEVEL + 1)
556
557
for tree in unsafe_wrap_sc(p4est_tree_t, mesh.p4est.trees)
558
elements_per_level .+= tree.quadrants_per_level
559
end
560
561
return @view(elements_per_level[(min_level + 1):(max_level + 1)])
562
end
563
564
function get_elements_per_level(min_level, max_level, mesh::T8codeMesh, solver, cache)
565
levels = trixi_t8_get_local_element_levels(mesh.forest)
566
567
return [count(==(l), levels) for l in min_level:max_level]
568
end
569
570
function get_elements_per_level(min_level, max_level, mesh::TreeMesh, solver, cache)
571
levels = [mesh.tree.levels[cache.elements.cell_ids[element]]
572
for element in eachelement(solver, cache)]
573
return [count(==(l), levels) for l in min_level:max_level]
574
end
575
576
# Iterate over tuples of analysis integrals in a type-stable way using "lispy tuple programming".
577
function analyze_integrals(analysis_integrals::NTuple{N, Any}, io, du, u, t,
578
semi) where {N}
579
580
# Extract the first analysis integral and process it; keep the remaining to be processed later
581
quantity = first(analysis_integrals)
582
remaining_quantities = Base.tail(analysis_integrals)
583
584
res = analyze(quantity, du, u, t, semi)
585
if mpi_isroot()
586
@printf(" %-12s:", pretty_form_utf(quantity))
587
@printf(" % 10.8e", res)
588
print(io, " ", res)
589
end
590
mpi_println()
591
592
# Recursively call this method with the unprocessed integrals
593
analyze_integrals(remaining_quantities, io, du, u, t, semi)
594
return nothing
595
end
596
597
# terminate the type-stable iteration over tuples
598
function analyze_integrals(analysis_integrals::Tuple{}, io, du, u, t, semi)
599
return nothing
600
end
601
602
# used for error checks and EOC analysis
603
function (cb::DiscreteCallback{Condition, Affect!})(sol) where {Condition,
604
Affect! <:
605
AnalysisCallback}
606
analysis_callback = cb.affect!
607
semi = sol.prob.p
608
@unpack analyzer = analysis_callback
609
cache_analysis = analysis_callback.cache
610
611
l2_error, linf_error = calc_error_norms(sol.u[end], sol.t[end], analyzer, semi,
612
cache_analysis)
613
return (; l2 = l2_error, linf = linf_error)
614
end
615
616
# some common analysis_integrals
617
# to support another analysis integral, you can overload
618
# Trixi.analyze, Trixi.pretty_form_utf, Trixi.pretty_form_ascii
619
function analyze(quantity, du, u, t, semi::AbstractSemidiscretization)
620
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
621
return analyze(quantity, du, u, t, mesh, equations, solver, cache)
622
end
623
function analyze(quantity, du, u, t, mesh, equations, solver, cache)
624
return integrate(quantity, u, mesh, equations, solver, cache, normalize = true)
625
end
626
pretty_form_utf(quantity) = get_name(quantity)
627
pretty_form_ascii(quantity) = get_name(quantity)
628
629
# Special analyze for `SemidiscretizationHyperbolicParabolic` such that
630
# precomputed gradients are available.
631
function analyze(quantity::typeof(enstrophy), du, u, t,
632
semi::SemidiscretizationHyperbolicParabolic)
633
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
634
equations_parabolic = semi.equations_parabolic
635
cache_parabolic = semi.cache_parabolic
636
# We do not apply `enstrophy` directly here because we might later have different `quantity`s
637
# that we wish to integrate, which can share this routine.
638
return analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver,
639
cache, cache_parabolic)
640
end
641
function analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver,
642
cache, cache_parabolic)
643
return integrate(quantity, u, mesh, equations, equations_parabolic, solver, cache,
644
cache_parabolic, normalize = true)
645
end
646
647
function entropy_timederivative end
648
pretty_form_utf(::typeof(entropy_timederivative)) = "∑∂S/∂U ⋅ Uₜ"
649
pretty_form_ascii(::typeof(entropy_timederivative)) = "dsdu_ut"
650
651
pretty_form_utf(::typeof(entropy)) = "∑S"
652
653
pretty_form_utf(::typeof(energy_total)) = "∑e_total"
654
pretty_form_ascii(::typeof(energy_total)) = "e_total"
655
656
pretty_form_utf(::typeof(energy_kinetic)) = "∑e_kinetic"
657
pretty_form_ascii(::typeof(energy_kinetic)) = "e_kinetic"
658
659
pretty_form_utf(::typeof(energy_kinetic_nondimensional)) = "∑e_kinetic*"
660
pretty_form_ascii(::typeof(energy_kinetic_nondimensional)) = "e_kinetic*"
661
662
pretty_form_utf(::typeof(energy_internal)) = "∑e_internal"
663
pretty_form_ascii(::typeof(energy_internal)) = "e_internal"
664
665
pretty_form_utf(::typeof(energy_magnetic)) = "∑e_magnetic"
666
pretty_form_ascii(::typeof(energy_magnetic)) = "e_magnetic"
667
668
pretty_form_utf(::typeof(cross_helicity)) = "∑v⋅B"
669
pretty_form_ascii(::typeof(cross_helicity)) = "v_dot_B"
670
671
pretty_form_utf(::typeof(enstrophy)) = "∑enstrophy"
672
pretty_form_ascii(::typeof(enstrophy)) = "enstrophy"
673
674
pretty_form_utf(::Val{:l2_divb}) = "L2 ∇⋅B"
675
pretty_form_ascii(::Val{:l2_divb}) = "l2_divb"
676
677
pretty_form_utf(::Val{:linf_divb}) = "L∞ ∇⋅B"
678
pretty_form_ascii(::Val{:linf_divb}) = "linf_divb"
679
680
pretty_form_utf(::typeof(lake_at_rest_error)) = "∑|H₀-(h+b)|"
681
pretty_form_ascii(::typeof(lake_at_rest_error)) = "|H0-(h+b)|"
682
end # @muladd
683
684
# specialized implementations specific to some solvers
685
include("analysis_dg1d.jl")
686
include("analysis_dg2d.jl")
687
include("analysis_surface_integral.jl")
688
include("analysis_dg2d_parallel.jl")
689
include("analysis_dg3d.jl")
690
include("analysis_dg3d_parallel.jl")
691
692
# This version of `analyze` is used for [`AnalysisSurfaceIntegral`](@ref) which requires
693
# `semi` to be passed along to retrieve the current boundary indices, which are non-static
694
# in the case of AMR.
695
function analyze(quantity::AnalysisSurfaceIntegral, du, u, t,
696
semi::AbstractSemidiscretization)
697
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
698
return analyze(quantity, du, u, t, mesh, equations, solver, cache, semi)
699
end
700
701
# Special analyze for `SemidiscretizationHyperbolicParabolic` such that
702
# precomputed gradients are available. Required for `enstrophy` (see above) and viscous forces.
703
# Note that this needs to be included after `analysis_surface_integral_2d.jl` to
704
# have `VariableParabolic` available.
705
function analyze(quantity::AnalysisSurfaceIntegral{Variable},
706
du, u, t,
707
semi::SemidiscretizationHyperbolicParabolic) where {Variable <:
708
VariableParabolic}
709
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
710
equations_parabolic = semi.equations_parabolic
711
cache_parabolic = semi.cache_parabolic
712
return analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver, cache,
713
semi, cache_parabolic)
714
end
715
716