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
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
# 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
interval::Int
53
save_analysis::Bool
54
output_directory::String
55
analysis_filename::String
56
analyzer::Analyzer
57
analysis_errors::Vector{Symbol}
58
analysis_integrals::AnalysisIntegrals
59
initial_state_integrals::InitialStateIntegrals
60
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
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
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
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
end
206
end
207
208
# Record current time using a high-resolution clock
209
analysis_callback.start_time = time_ns()
210
211
# Record current time for performance index computation
212
analysis_callback.start_time_last_analysis = time_ns()
213
214
# Record current number of `rhs!` calls for performance index computation
215
analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)
216
217
# Record total time spent in garbage collection so far using a high-resolution clock
218
# Note: For details see the actual callback function below
219
analysis_callback.start_gc_time = Base.gc_time_ns()
220
221
analysis_callback(u_ode, du_ode, integrator, semi)
222
return nothing
223
end
224
225
# This method gets called from OrdinaryDiffEq's `solve(...)`
226
function (analysis_callback::AnalysisCallback)(integrator)
227
semi = integrator.p
228
du_ode = first(get_tmp_cache(integrator))
229
u_ode = integrator.u
230
analysis_callback(u_ode, du_ode, integrator, semi)
231
end
232
233
# This method gets called internally as the main entry point to the AnalysiCallback
234
# TODO: Taal refactor, allow passing an IO object (which could be devnull to avoid cluttering the console)
235
function (analysis_callback::AnalysisCallback)(u_ode, du_ode, integrator, semi)
236
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
237
@unpack dt, t = integrator
238
iter = integrator.stats.naccept
239
240
# Compute the percentage of the simulation that is done
241
t = integrator.t
242
t_initial = first(integrator.sol.prob.tspan)
243
t_final = last(integrator.sol.prob.tspan)
244
sim_time_percentage = (t - t_initial) / (t_final - t_initial) * 100
245
246
# Record performance measurements and compute performance index (PID)
247
runtime_since_last_analysis = 1.0e-9 * (time_ns() -
248
analysis_callback.start_time_last_analysis)
249
# PID is an MPI-aware measure of how much time per global degree of freedom (i.e., over all ranks
250
# and threads) and per `rhs!` evaluation is required. MPI-aware means that it essentially adds up
251
# the time spent on each computing unit. Thus, in an ideally parallelized program, the PID should be constant
252
# independent of the number of MPI ranks or threads used, since, e.g., using 4x the number of ranks should
253
# divide the runtime on each rank by 4. See also the Trixi.jl docs ("Performance" section) for
254
# more information.
255
ncalls_rhs_since_last_analysis = (ncalls(semi.performance_counter)
256
-
257
analysis_callback.ncalls_rhs_last_analysis)
258
# This assumes that the same number of threads is used on each MPI rank.
259
performance_index = runtime_since_last_analysis * mpi_nranks() *
260
Threads.nthreads() /
261
(ndofsglobal(mesh, solver, cache)
262
*
263
ncalls_rhs_since_last_analysis)
264
265
# Compute the total runtime since the analysis callback has been initialized, in seconds
266
runtime_absolute = 1.0e-9 * (time_ns() - analysis_callback.start_time)
267
268
# Compute the relative runtime per thread as time spent in `rhs!` divided by the number of calls
269
# to `rhs!` and the number of local degrees of freedom
270
# OBS! This computation must happen *after* the PID computation above, since `take!(...)`
271
# will reset the number of calls to `rhs!`
272
runtime_relative = 1.0e-9 * take!(semi.performance_counter) * Threads.nthreads() /
273
ndofs(semi)
274
275
# Compute the total time spent in garbage collection since the analysis callback has been
276
# initialized, in seconds
277
# Note: `Base.gc_time_ns()` is not part of the public Julia API but has been available at least
278
# since Julia 1.6. Should this function be removed without replacement in a future Julia
279
# release, just delete this analysis quantity from the callback.
280
# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L83-L84
281
gc_time_absolute = 1.0e-9 * (Base.gc_time_ns() - analysis_callback.start_gc_time)
282
283
# Compute the percentage of total time that was spent in garbage collection
284
gc_time_percentage = gc_time_absolute / runtime_absolute * 100
285
286
# Obtain the current memory usage of the Julia garbage collector, in MiB, i.e., the total size of
287
# objects in memory that have been allocated by the JIT compiler or the user code.
288
# Note: `Base.gc_live_bytes()` is not part of the public Julia API but has been available at least
289
# since Julia 1.6. Should this function be removed without replacement in a future Julia
290
# release, just delete this analysis quantity from the callback.
291
# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L86-L97
292
memory_use = Base.gc_live_bytes() / 2^20 # bytes -> MiB
293
294
@trixi_timeit timer() "analyze solution" begin
295
# General information
296
mpi_println()
297
mpi_println("─"^100)
298
mpi_println(" Simulation running '", get_name(equations), "' with ",
299
summary(solver))
300
mpi_println("─"^100)
301
mpi_println(" #timesteps: " * @sprintf("% 14d", iter) *
302
" " *
303
" run time: " * @sprintf("%10.8e s", runtime_absolute))
304
mpi_println(" Δt: " * @sprintf("%10.8e", dt) *
305
" " *
306
" └── GC time: " *
307
@sprintf("%10.8e s (%5.3f%%)", gc_time_absolute, gc_time_percentage))
308
mpi_println(rpad(" sim. time: " *
309
@sprintf("%10.8e (%5.3f%%)", t, sim_time_percentage), 46) *
310
" time/DOF/rhs!: " * @sprintf("%10.8e s", runtime_relative))
311
mpi_println(" " * " " *
312
" " *
313
" PID: " * @sprintf("%10.8e s", performance_index))
314
mpi_println(" #DOFs per field:" * @sprintf("% 14d", ndofsglobal(semi)) *
315
" " *
316
" alloc'd memory: " * @sprintf("%14.3f MiB", memory_use))
317
mpi_println(" #elements: " *
318
@sprintf("% 14d", nelementsglobal(mesh, solver, cache)))
319
320
# Level information (only for AMR and/or non-uniform `TreeMesh`es)
321
print_level_information(integrator.opts.callback, mesh, solver, cache)
322
mpi_println()
323
324
# Open file for appending and store time step and time information
325
if mpi_isroot() && analysis_callback.save_analysis
326
io = open(joinpath(analysis_callback.output_directory,
327
analysis_callback.analysis_filename), "a")
328
print(io, iter)
329
print(io, " ", t)
330
print(io, " ", dt)
331
else
332
io = devnull
333
end
334
335
# Calculate current time derivative (needed for semidiscrete entropy time derivative, residual, etc.)
336
# `integrator.f` is usually just a call to `rhs!`
337
# However, we want to allow users to modify the ODE RHS outside of Trixi.jl
338
# and allow us to pass a combined ODE RHS to OrdinaryDiffEq, e.g., for
339
# hyperbolic-parabolic systems.
340
@notimeit timer() integrator.f(du_ode, u_ode, semi, t)
341
u = wrap_array(u_ode, mesh, equations, solver, cache)
342
du = wrap_array(du_ode, mesh, equations, solver, cache)
343
# Compute l2_error, linf_error
344
analysis_callback(io, du, u, u_ode, t, semi)
345
346
mpi_println("─"^100)
347
mpi_println()
348
349
# Add line break and close analysis file if it was opened
350
if mpi_isroot() && analysis_callback.save_analysis
351
# This resolves a possible type instability introduced above, since `io`
352
# can either be an `IOStream` or `devnull`, but we know that it must be
353
# an `IOStream here`.
354
println(io::IOStream)
355
close(io::IOStream)
356
end
357
end
358
359
# avoid re-evaluating possible FSAL stages
360
u_modified!(integrator, false)
361
362
# Reset performance measurements
363
analysis_callback.start_time_last_analysis = time_ns()
364
analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)
365
366
return nothing
367
end
368
369
# This method is just called internally from `(analysis_callback::AnalysisCallback)(integrator)`
370
# and serves as a function barrier. Additionally, it makes the code easier to profile and optimize.
371
function (analysis_callback::AnalysisCallback)(io, du, u, u_ode, t, semi)
372
@unpack analyzer, analysis_errors, analysis_integrals = analysis_callback
373
cache_analysis = analysis_callback.cache
374
_, equations, _, _ = mesh_equations_solver_cache(semi)
375
376
# Calculate and print derived quantities (error norms, entropy etc.)
377
# Variable names required for L2 error, Linf error, and conservation error
378
if any(q in analysis_errors
379
for q in (:l2_error, :linf_error, :conservation_error, :residual)) &&
380
mpi_isroot()
381
print(" Variable: ")
382
for v in eachvariable(equations)
383
@printf(" %-14s", varnames(cons2cons, equations)[v])
384
end
385
println()
386
end
387
388
if :l2_error in analysis_errors || :linf_error in analysis_errors
389
# Calculate L2/Linf errors
390
l2_error, linf_error = calc_error_norms(u_ode, t, analyzer, semi,
391
cache_analysis)
392
393
if mpi_isroot()
394
# L2 error
395
if :l2_error in analysis_errors
396
print(" L2 error: ")
397
for v in eachvariable(equations)
398
@printf(" % 10.8e", l2_error[v])
399
print(io, " ", l2_error[v])
400
end
401
println()
402
end
403
404
# Linf error
405
if :linf_error in analysis_errors
406
print(" Linf error: ")
407
for v in eachvariable(equations)
408
@printf(" % 10.8e", linf_error[v])
409
print(io, " ", linf_error[v])
410
end
411
println()
412
end
413
end
414
end
415
416
# Conservation error
417
if :conservation_error in analysis_errors
418
@unpack initial_state_integrals = analysis_callback
419
state_integrals = integrate(u_ode, semi)
420
421
if mpi_isroot()
422
print(" |∑U - ∑U₀|: ")
423
for v in eachvariable(equations)
424
err = abs(state_integrals[v] - initial_state_integrals[v])
425
@printf(" % 10.8e", err)
426
print(io, " ", err)
427
end
428
println()
429
end
430
end
431
432
# Residual (defined here as the vector maximum of the absolute values of the time derivatives)
433
if :residual in analysis_errors
434
mpi_print(" max(|Uₜ|): ")
435
for v in eachvariable(equations)
436
# Calculate maximum absolute value of Uₜ
437
res = maximum(abs, view(du, v, ..))
438
if mpi_isparallel()
439
# TODO: Debugging, here is a type instability
440
# Base.max instead of max needed, see comment in src/auxiliary/math.jl
441
global_res = MPI.Reduce!(Ref(res), Base.max, mpi_root(), mpi_comm())
442
if mpi_isroot()
443
res::eltype(du) = global_res[]
444
end
445
end
446
if mpi_isroot()
447
@printf(" % 10.8e", res)
448
print(io, " ", res)
449
end
450
end
451
mpi_println()
452
end
453
454
# L2/L∞ errors of the primitive variables
455
if :l2_error_primitive in analysis_errors ||
456
:linf_error_primitive in analysis_errors
457
l2_error_prim, linf_error_prim = calc_error_norms(cons2prim, u_ode, t, analyzer,
458
semi, cache_analysis)
459
460
if mpi_isroot()
461
print(" Variable: ")
462
for v in eachvariable(equations)
463
@printf(" %-14s", varnames(cons2prim, equations)[v])
464
end
465
println()
466
467
# L2 error
468
if :l2_error_primitive in analysis_errors
469
print(" L2 error prim.: ")
470
for v in eachvariable(equations)
471
@printf("%10.8e ", l2_error_prim[v])
472
print(io, " ", l2_error_prim[v])
473
end
474
println()
475
end
476
477
# L∞ error
478
if :linf_error_primitive in analysis_errors
479
print(" Linf error pri.:")
480
for v in eachvariable(equations)
481
@printf("%10.8e ", linf_error_prim[v])
482
print(io, " ", linf_error_prim[v])
483
end
484
println()
485
end
486
end
487
end
488
489
# additional integrals
490
analyze_integrals(analysis_integrals, io, du, u, t, semi)
491
492
return nothing
493
end
494
495
function print_level_information(mesh, solver, cache, min_level, max_level)
496
# Get local element count per level
497
elements_per_level = get_elements_per_level(min_level, max_level, mesh, solver,
498
cache)
499
500
# Sum up across all ranks
501
MPI.Reduce!(elements_per_level, +, mpi_root(), mpi_comm())
502
503
# Print
504
for level in max_level:-1:(min_level + 1)
505
mpi_println(" ├── level $level: " *
506
@sprintf("% 14d", elements_per_level[level + 1 - min_level]))
507
end
508
mpi_println(" └── level $min_level: " *
509
@sprintf("% 14d", elements_per_level[1]))
510
511
return nothing
512
end
513
514
function print_level_information(callbacks, mesh::TreeMesh, solver, cache)
515
if uses_amr(callbacks)
516
# Get global minimum and maximum level from the AMRController
517
min_level = max_level = 0
518
for cb in callbacks.discrete_callbacks
519
if cb.affect! isa AMRCallback
520
min_level = cb.affect!.controller.base_level
521
max_level = cb.affect!.controller.max_level
522
end
523
end
524
print_level_information(mesh, solver, cache, min_level, max_level)
525
# Special check for `TreeMesh`es without AMR.
526
# These meshes may still be non-uniform due to `refinement_patches`, see
527
# `refine_box!`, `coarsen_box!`, and `refine_sphere!`.
528
elseif minimum_level(mesh.tree) != maximum_level(mesh.tree)
529
min_level = minimum_level(mesh.tree)
530
max_level = maximum_level(mesh.tree)
531
print_level_information(mesh, solver, cache, min_level, max_level)
532
else # Uniform mesh
533
return nothing
534
end
535
end
536
537
function print_level_information(callbacks, mesh, solver, cache)
538
if uses_amr(callbacks)
539
# Get global minimum and maximum level from the AMRController
540
min_level = max_level = 0
541
for cb in callbacks.discrete_callbacks
542
if cb.affect! isa AMRCallback
543
min_level = cb.affect!.controller.base_level
544
max_level = cb.affect!.controller.max_level
545
end
546
end
547
print_level_information(mesh, solver, cache, min_level, max_level)
548
else # Uniform mesh
549
return nothing
550
end
551
end
552
553
function get_elements_per_level(min_level, max_level, mesh::P4estMesh, solver, cache)
554
elements_per_level = zeros(P4EST_MAXLEVEL + 1)
555
556
for tree in unsafe_wrap_sc(p4est_tree_t, mesh.p4est.trees)
557
elements_per_level .+= tree.quadrants_per_level
558
end
559
560
return @view(elements_per_level[(min_level + 1):(max_level + 1)])
561
end
562
563
function get_elements_per_level(min_level, max_level, mesh::T8codeMesh, solver, cache)
564
levels = trixi_t8_get_local_element_levels(mesh.forest)
565
566
return [count(==(l), levels) for l in min_level:max_level]
567
end
568
569
function get_elements_per_level(min_level, max_level, mesh::TreeMesh, solver, cache)
570
levels = [mesh.tree.levels[cache.elements.cell_ids[element]]
571
for element in eachelement(solver, cache)]
572
return [count(==(l), levels) for l in min_level:max_level]
573
end
574
575
# Iterate over tuples of analysis integrals in a type-stable way using "lispy tuple programming".
576
function analyze_integrals(analysis_integrals::NTuple{N, Any}, io, du, u, t,
577
semi) where {N}
578
579
# Extract the first analysis integral and process it; keep the remaining to be processed later
580
quantity = first(analysis_integrals)
581
remaining_quantities = Base.tail(analysis_integrals)
582
583
res = analyze(quantity, du, u, t, semi)
584
if mpi_isroot()
585
@printf(" %-12s:", pretty_form_utf(quantity))
586
@printf(" % 10.8e", res)
587
print(io, " ", res)
588
end
589
mpi_println()
590
591
# Recursively call this method with the unprocessed integrals
592
analyze_integrals(remaining_quantities, io, du, u, t, semi)
593
return nothing
594
end
595
596
# terminate the type-stable iteration over tuples
597
function analyze_integrals(analysis_integrals::Tuple{}, io, du, u, t, semi)
598
nothing
599
end
600
601
# used for error checks and EOC analysis
602
function (cb::DiscreteCallback{Condition, Affect!})(sol) where {Condition,
603
Affect! <:
604
AnalysisCallback}
605
analysis_callback = cb.affect!
606
semi = sol.prob.p
607
@unpack analyzer = analysis_callback
608
cache_analysis = analysis_callback.cache
609
610
l2_error, linf_error = calc_error_norms(sol.u[end], sol.t[end], analyzer, semi,
611
cache_analysis)
612
(; l2 = l2_error, linf = linf_error)
613
end
614
615
# some common analysis_integrals
616
# to support another analysis integral, you can overload
617
# Trixi.analyze, Trixi.pretty_form_utf, Trixi.pretty_form_ascii
618
function analyze(quantity, du, u, t, semi::AbstractSemidiscretization)
619
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
620
analyze(quantity, du, u, t, mesh, equations, solver, cache)
621
end
622
function analyze(quantity, du, u, t, mesh, equations, solver, cache)
623
integrate(quantity, u, mesh, equations, solver, cache, normalize = true)
624
end
625
pretty_form_utf(quantity) = get_name(quantity)
626
pretty_form_ascii(quantity) = get_name(quantity)
627
628
# Special analyze for `SemidiscretizationHyperbolicParabolic` such that
629
# precomputed gradients are available.
630
function analyze(quantity::typeof(enstrophy), du, u, t,
631
semi::SemidiscretizationHyperbolicParabolic)
632
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
633
equations_parabolic = semi.equations_parabolic
634
cache_parabolic = semi.cache_parabolic
635
# We do not apply `enstrophy` directly here because we might later have different `quantity`s
636
# that we wish to integrate, which can share this routine.
637
analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver, cache,
638
cache_parabolic)
639
end
640
function analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver,
641
cache, cache_parabolic)
642
integrate(quantity, u, mesh, equations, equations_parabolic, solver, cache,
643
cache_parabolic, normalize = true)
644
end
645
646
function entropy_timederivative end
647
pretty_form_utf(::typeof(entropy_timederivative)) = "∑∂S/∂U ⋅ Uₜ"
648
pretty_form_ascii(::typeof(entropy_timederivative)) = "dsdu_ut"
649
650
pretty_form_utf(::typeof(entropy)) = "∑S"
651
652
pretty_form_utf(::typeof(energy_total)) = "∑e_total"
653
pretty_form_ascii(::typeof(energy_total)) = "e_total"
654
655
pretty_form_utf(::typeof(energy_kinetic)) = "∑e_kinetic"
656
pretty_form_ascii(::typeof(energy_kinetic)) = "e_kinetic"
657
658
pretty_form_utf(::typeof(energy_kinetic_nondimensional)) = "∑e_kinetic*"
659
pretty_form_ascii(::typeof(energy_kinetic_nondimensional)) = "e_kinetic*"
660
661
pretty_form_utf(::typeof(energy_internal)) = "∑e_internal"
662
pretty_form_ascii(::typeof(energy_internal)) = "e_internal"
663
664
pretty_form_utf(::typeof(energy_magnetic)) = "∑e_magnetic"
665
pretty_form_ascii(::typeof(energy_magnetic)) = "e_magnetic"
666
667
pretty_form_utf(::typeof(cross_helicity)) = "∑v⋅B"
668
pretty_form_ascii(::typeof(cross_helicity)) = "v_dot_B"
669
670
pretty_form_utf(::typeof(enstrophy)) = "∑enstrophy"
671
pretty_form_ascii(::typeof(enstrophy)) = "enstrophy"
672
673
pretty_form_utf(::Val{:l2_divb}) = "L2 ∇⋅B"
674
pretty_form_ascii(::Val{:l2_divb}) = "l2_divb"
675
676
pretty_form_utf(::Val{:linf_divb}) = "L∞ ∇⋅B"
677
pretty_form_ascii(::Val{:linf_divb}) = "linf_divb"
678
679
pretty_form_utf(::typeof(lake_at_rest_error)) = "∑|H₀-(h+b)|"
680
pretty_form_ascii(::typeof(lake_at_rest_error)) = "|H0-(h+b)|"
681
end # @muladd
682
683
# specialized implementations specific to some solvers
684
include("analysis_dg1d.jl")
685
include("analysis_dg2d.jl")
686
include("analysis_surface_integral.jl")
687
include("analysis_dg2d_parallel.jl")
688
include("analysis_dg3d.jl")
689
include("analysis_dg3d_parallel.jl")
690
691
# This version of `analyze` is used for [`AnalysisSurfaceIntegral`](@ref) which requires
692
# `semi` to be passed along to retrieve the current boundary indices, which are non-static
693
# in the case of AMR.
694
function analyze(quantity::AnalysisSurfaceIntegral, du, u, t,
695
semi::AbstractSemidiscretization)
696
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
697
analyze(quantity, du, u, t, mesh, equations, solver, cache, semi)
698
end
699
700
# Special analyze for `SemidiscretizationHyperbolicParabolic` such that
701
# precomputed gradients are available. Required for `enstrophy` (see above) and viscous forces.
702
# Note that this needs to be included after `analysis_surface_integral_2d.jl` to
703
# have `VariableViscous` available.
704
function analyze(quantity::AnalysisSurfaceIntegral{Variable},
705
du, u, t,
706
semi::SemidiscretizationHyperbolicParabolic) where {
707
Variable <:
708
VariableViscous}
709
mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
710
equations_parabolic = semi.equations_parabolic
711
cache_parabolic = semi.cache_parabolic
712
analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver, cache, semi,
713
cache_parabolic)
714
end
715
716