Path: blob/main/src/callbacks_step/analysis.jl
5586 views
# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).1# Since these FMAs can increase the performance of many numerical algorithms,2# we need to opt-in explicitly.3# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.4@muladd begin5#! format: noindent67# TODO: Taal refactor8# - analysis_interval part as PeriodicCallback called after a certain amount of simulation time9"""10AnalysisCallback(semi; interval=0,11save_analysis=false,12output_directory="out",13analysis_filename="analysis.dat",14extra_analysis_errors=Symbol[],15extra_analysis_integrals=())1617Analyze a numerical solution every `interval` time steps and print the18results to the screen. If `save_analysis`, the results are also saved in19`joinpath(output_directory, analysis_filename)`.2021Additional errors can be computed, e.g. by passing22`extra_analysis_errors = (:l2_error_primitive, :linf_error_primitive)`23or `extra_analysis_errors = (:conservation_error,)`.2425If you want to omit the computation (to safe compute-time) of the [`default_analysis_errors`](@ref), specify26`analysis_errors = Symbol[]`.27Note: `default_analysis_errors` are `:l2_error` and `:linf_error` for all equations.28If you want to compute `extra_analysis_errors` such as `:conservation_error` solely, i.e.,29without `:l2_error, :linf_error` you need to specify30`analysis_errors = [:conservation_error]` instead of `extra_analysis_errors = [:conservation_error]`.3132Further scalar functions `func` in `extra_analysis_integrals` are applied to the numerical33solution and integrated over the computational domain. Some examples for this are34[`entropy`](@ref), [`energy_kinetic`](@ref), [`energy_internal`](@ref), and [`energy_total`](@ref).35You can also write your own function with the same signature as the examples listed above and36pass it via `extra_analysis_integrals`.37See the developer comments about `Trixi.analyze`, `Trixi.pretty_form_utf`, and38`Trixi.pretty_form_ascii` for further information on how to create custom analysis quantities.3940In addition, the analysis callback records and outputs a number of quantities that are useful for41evaluating the computational performance, such as the total runtime, the performance index42(time/DOF/rhs!), the time spent in garbage collection (GC), or the current memory usage (alloc'd43memory).44"""45mutable struct AnalysisCallback{Analyzer, AnalysisIntegrals, InitialStateIntegrals,46Cache}47start_time::Float6448start_time_last_analysis::Float6449ncalls_rhs_last_analysis::Int50start_gc_time::Float6451const interval::Int52const save_analysis::Bool53const output_directory::String54const analysis_filename::String55const analyzer::Analyzer56const analysis_errors::Vector{Symbol}57const analysis_integrals::AnalysisIntegrals58initial_state_integrals::InitialStateIntegrals59const cache::Cache60end6162# TODO: Taal bikeshedding, implement a method with less information and the signature63# function Base.show(io::IO, analysis_callback::AnalysisCallback)64# end65function Base.show(io::IO, ::MIME"text/plain",66cb::DiscreteCallback{<:Any, <:AnalysisCallback})67@nospecialize cb # reduce precompilation time6869if get(io, :compact, false)70show(io, cb)71else72analysis_callback = cb.affect!7374setup = Pair{String, Any}["interval" => analysis_callback.interval,75"analyzer" => analysis_callback.analyzer]76for (idx, error) in enumerate(analysis_callback.analysis_errors)77push!(setup, "│ error " * string(idx) => error)78end79for (idx, integral) in enumerate(analysis_callback.analysis_integrals)80push!(setup, "│ integral " * string(idx) => integral)81end82push!(setup,83"save analysis to file" => analysis_callback.save_analysis ? "yes" : "no")84if analysis_callback.save_analysis85push!(setup, "│ filename" => analysis_callback.analysis_filename)86push!(setup,87"│ output directory" => abspath(normpath(analysis_callback.output_directory)))88end89summary_box(io, "AnalysisCallback", setup)90end91end9293# This is the convenience constructor that gets called from the elixirs94function AnalysisCallback(semi::AbstractSemidiscretization; kwargs...)95mesh, equations, solver, cache = mesh_equations_solver_cache(semi)96return AnalysisCallback(mesh, equations, solver, cache; kwargs...)97end9899# This is the actual constructor100function AnalysisCallback(mesh, equations::AbstractEquations, solver, cache;101interval = 0,102save_analysis = false,103output_directory = "out",104analysis_filename = "analysis.dat",105extra_analysis_errors = Symbol[],106analysis_errors = union(default_analysis_errors(equations),107extra_analysis_errors),108extra_analysis_integrals = (),109analysis_integrals = union(default_analysis_integrals(equations),110extra_analysis_integrals),111RealT = real(solver),112uEltype = eltype(cache.elements),113kwargs...)114# Decide when the callback is activated.115# With error-based step size control, some steps can be rejected. Thus,116# `integrator.iter >= integrator.stats.naccept`117# (total #steps) (#accepted steps)118# We need to check the number of accepted steps since callbacks are not119# activated after a rejected step.120condition = (u, t, integrator) -> interval > 0 &&121(integrator.stats.naccept % interval == 0 || isfinished(integrator))122123analyzer = SolutionAnalyzer(solver; kwargs...)124cache_analysis = create_cache_analysis(analyzer, mesh, equations, solver, cache,125RealT, uEltype)126127analysis_callback = AnalysisCallback(0.0, 0.0, 0, 0.0,128interval, save_analysis, output_directory,129analysis_filename,130analyzer,131analysis_errors, Tuple(analysis_integrals),132SVector(ntuple(_ -> zero(uEltype),133Val(nvariables(equations)))),134cache_analysis)135136return DiscreteCallback(condition, analysis_callback,137save_positions = (false, false),138initialize = initialize!)139end140141# This method gets called from OrdinaryDiffEq's `solve(...)`142function initialize!(cb::DiscreteCallback{Condition, Affect!}, u_ode, t,143integrator) where {Condition, Affect! <: AnalysisCallback}144semi = integrator.p145du_ode = first(get_tmp_cache(integrator))146return initialize!(cb, u_ode, du_ode, t, integrator, semi)147end148149# This is the actual initialization method150# Note: we have this indirection to allow initializing a callback from the AnalysisCallbackCoupled151function initialize!(cb::DiscreteCallback{Condition, Affect!}, u_ode, du_ode, t,152integrator, semi) where {Condition, Affect! <: AnalysisCallback}153initial_state_integrals = integrate(u_ode, semi)154_, equations, _, _ = mesh_equations_solver_cache(semi)155156analysis_callback = cb.affect!157analysis_callback.initial_state_integrals = initial_state_integrals158@unpack save_analysis, output_directory, analysis_filename, analysis_errors, analysis_integrals = analysis_callback159160if save_analysis && mpi_isroot()161mkpath(output_directory)162163# write header of output file164open(joinpath(output_directory, analysis_filename), "w") do io165print(io, "#timestep ")166print(io, "time ")167print(io, "dt ")168if :l2_error in analysis_errors169for v in varnames(cons2cons, equations)170print(io, "l2_" * v * " ")171end172end173if :linf_error in analysis_errors174for v in varnames(cons2cons, equations)175print(io, "linf_" * v * " ")176end177end178if :conservation_error in analysis_errors179for v in varnames(cons2cons, equations)180print(io, "cons_" * v * " ")181end182end183if :residual in analysis_errors184for v in varnames(cons2cons, equations)185print(io, "res_" * v * " ")186end187end188if :l2_error_primitive in analysis_errors189for v in varnames(cons2prim, equations)190print(io, "l2_" * v * " ")191end192end193if :linf_error_primitive in analysis_errors194for v in varnames(cons2prim, equations)195print(io, "linf_" * v * " ")196end197end198199for quantity in analysis_integrals200print(io, pretty_form_ascii(quantity), " ")201end202203println(io)204return nothing205end206end207208# Record current time using a high-resolution clock209analysis_callback.start_time = time_ns()210211# Record current time for performance index computation212analysis_callback.start_time_last_analysis = time_ns()213214# Record current number of `rhs!` calls for performance index computation215analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)216217# Record total time spent in garbage collection so far using a high-resolution clock218# Note: For details see the actual callback function below219analysis_callback.start_gc_time = Base.gc_time_ns()220221analysis_callback(u_ode, du_ode, integrator, semi)222return nothing223end224225# This method gets called from OrdinaryDiffEq's `solve(...)`226function (analysis_callback::AnalysisCallback)(integrator)227semi = integrator.p228du_ode = first(get_tmp_cache(integrator))229u_ode = integrator.u230return analysis_callback(u_ode, du_ode, integrator, semi)231end232233# This method gets called internally as the main entry point to the AnalysiCallback234# TODO: Taal refactor, allow passing an IO object (which could be devnull to avoid cluttering the console)235function (analysis_callback::AnalysisCallback)(u_ode, du_ode, integrator, semi)236mesh, equations, solver, cache = mesh_equations_solver_cache(semi)237@unpack dt, t = integrator238iter = integrator.stats.naccept239240# Compute the percentage of the simulation that is done241t = integrator.t242t_initial = first(integrator.sol.prob.tspan)243t_final = last(integrator.sol.prob.tspan)244sim_time_percentage = (t - t_initial) / (t_final - t_initial) * 100245246# Record performance measurements and compute performance index (PID)247runtime_since_last_analysis = 1.0e-9 * (time_ns() -248analysis_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 ranks250# and threads) and per `rhs!` evaluation is required. MPI-aware means that it essentially adds up251# the time spent on each computing unit. Thus, in an ideally parallelized program, the PID should be constant252# independent of the number of MPI ranks or threads used, since, e.g., using 4x the number of ranks should253# divide the runtime on each rank by 4. See also the Trixi.jl docs ("Performance" section) for254# more information.255ncalls_rhs_since_last_analysis = (ncalls(semi.performance_counter)256-257analysis_callback.ncalls_rhs_last_analysis)258# This assumes that the same number of threads is used on each MPI rank.259performance_index = runtime_since_last_analysis * mpi_nranks() *260Threads.nthreads() /261(ndofsglobal(mesh, solver, cache)262*263ncalls_rhs_since_last_analysis)264265# Compute the total runtime since the analysis callback has been initialized, in seconds266runtime_absolute = 1.0e-9 * (time_ns() - analysis_callback.start_time)267268# Compute the relative runtime per thread as time spent in `rhs!` divided by the number of calls269# to `rhs!` and the number of local degrees of freedom270# OBS! This computation must happen *after* the PID computation above, since `take!(...)`271# will reset the number of calls to `rhs!`272runtime_relative = 1.0e-9 * take!(semi.performance_counter) * Threads.nthreads() /273ndofs(semi)274275# Compute the total time spent in garbage collection since the analysis callback has been276# initialized, in seconds277# Note: `Base.gc_time_ns()` is not part of the public Julia API but has been available at least278# since Julia 1.6. Should this function be removed without replacement in a future Julia279# release, just delete this analysis quantity from the callback.280# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L83-L84281gc_time_absolute = 1.0e-9 * (Base.gc_time_ns() - analysis_callback.start_gc_time)282283# Compute the percentage of total time that was spent in garbage collection284gc_time_percentage = gc_time_absolute / runtime_absolute * 100285286# Obtain the current memory usage of the Julia garbage collector, in MiB, i.e., the total size of287# 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 least289# since Julia 1.6. Should this function be removed without replacement in a future Julia290# release, just delete this analysis quantity from the callback.291# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L86-L97292memory_use = Base.gc_live_bytes() / 2^20 # bytes -> MiB293294@trixi_timeit timer() "analyze solution" begin295# General information296mpi_println()297mpi_println("─"^100)298mpi_println(" Simulation running '", get_name(equations), "' with ",299summary(solver))300mpi_println("─"^100)301mpi_println(" #timesteps: " * @sprintf("% 14d", iter) *302" " *303" run time: " * @sprintf("%10.8e s", runtime_absolute))304mpi_println(" Δt: " * @sprintf("%10.8e", dt) *305" " *306" └── GC time: " *307@sprintf("%10.8e s (%5.3f%%)", gc_time_absolute, gc_time_percentage))308mpi_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))311mpi_println(" " * " " *312" " *313" PID: " * @sprintf("%10.8e s", performance_index))314mpi_println(" #DOFs per field:" * @sprintf("% 14d", ndofsglobal(semi)) *315" " *316" alloc'd memory: " * @sprintf("%14.3f MiB", memory_use))317mpi_println(" #elements: " *318@sprintf("% 14d", nelementsglobal(mesh, solver, cache)))319320# Level information (only for AMR and/or non-uniform `TreeMesh`es)321print_level_information(integrator.opts.callback, mesh, solver, cache)322mpi_println()323324# Open file for appending and store time step and time information325if mpi_isroot() && analysis_callback.save_analysis326io = open(joinpath(analysis_callback.output_directory,327analysis_callback.analysis_filename), "a")328print(io, iter)329print(io, " ", t)330print(io, " ", dt)331else332io = devnull333end334335# 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.jl338# and allow us to pass a combined ODE RHS to OrdinaryDiffEq, e.g., for339# hyperbolic-parabolic systems.340@notimeit timer() integrator.f(du_ode, u_ode, semi, t)341u = wrap_array(u_ode, mesh, equations, solver, cache)342du = wrap_array(du_ode, mesh, equations, solver, cache)343# Compute l2_error, linf_error344analysis_callback(io, du, u, u_ode, t, semi)345346mpi_println("─"^100)347mpi_println()348349# Add line break and close analysis file if it was opened350if mpi_isroot() && analysis_callback.save_analysis351# This resolves a possible type instability introduced above, since `io`352# can either be an `IOStream` or `devnull`, but we know that it must be353# an `IOStream here`.354println(io::IOStream)355close(io::IOStream)356end357end358359# avoid re-evaluating possible FSAL stages360u_modified!(integrator, false)361362# Reset performance measurements363analysis_callback.start_time_last_analysis = time_ns()364analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)365366return nothing367end368369# 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.371function (analysis_callback::AnalysisCallback)(io, du, u, u_ode, t, semi)372@unpack analyzer, analysis_errors, analysis_integrals = analysis_callback373cache_analysis = analysis_callback.cache374_, equations, _, _ = mesh_equations_solver_cache(semi)375376# Calculate and print derived quantities (error norms, entropy etc.)377# Variable names required for L2 error, Linf error, and conservation error378if any(q in analysis_errors379for q in (:l2_error, :linf_error, :conservation_error, :residual)) &&380mpi_isroot()381print(" Variable: ")382for v in eachvariable(equations)383@printf(" %-14s", varnames(cons2cons, equations)[v])384end385println()386end387388if :l2_error in analysis_errors || :linf_error in analysis_errors389# Calculate L2/Linf errors390l2_error, linf_error = calc_error_norms(u_ode, t, analyzer, semi,391cache_analysis)392393if mpi_isroot()394# L2 error395if :l2_error in analysis_errors396print(" L2 error: ")397for v in eachvariable(equations)398@printf(" % 10.8e", l2_error[v])399print(io, " ", l2_error[v])400end401println()402end403404# Linf error405if :linf_error in analysis_errors406print(" Linf error: ")407for v in eachvariable(equations)408@printf(" % 10.8e", linf_error[v])409print(io, " ", linf_error[v])410end411println()412end413end414end415416# Conservation error417if :conservation_error in analysis_errors418@unpack initial_state_integrals = analysis_callback419state_integrals = integrate(u_ode, semi)420421if mpi_isroot()422print(" |∑U - ∑U₀|: ")423for v in eachvariable(equations)424err = abs(state_integrals[v] - initial_state_integrals[v])425@printf(" % 10.8e", err)426print(io, " ", err)427end428println()429end430end431432# Residual (defined here as the vector maximum of the absolute values of the time derivatives)433if :residual in analysis_errors434mpi_print(" max(|Uₜ|): ")435for v in eachvariable(equations)436# Calculate maximum absolute value of Uₜ437res = maximum(abs, view(du, v, ..))438if mpi_isparallel()439# TODO: Debugging, here is a type instability440# Base.max instead of max needed, see comment in src/auxiliary/math.jl441global_res = MPI.Reduce!(Ref(res), Base.max, mpi_root(), mpi_comm())442if mpi_isroot()443res::eltype(du) = global_res[]444end445end446if mpi_isroot()447@printf(" % 10.8e", res)448print(io, " ", res)449end450end451mpi_println()452end453454# L2/L∞ errors of the primitive variables455if :l2_error_primitive in analysis_errors ||456:linf_error_primitive in analysis_errors457l2_error_prim, linf_error_prim = calc_error_norms(cons2prim, u_ode, t, analyzer,458semi, cache_analysis)459460if mpi_isroot()461print(" Variable: ")462for v in eachvariable(equations)463@printf(" %-14s", varnames(cons2prim, equations)[v])464end465println()466467# L2 error468if :l2_error_primitive in analysis_errors469print(" L2 error prim.: ")470for v in eachvariable(equations)471@printf("%10.8e ", l2_error_prim[v])472print(io, " ", l2_error_prim[v])473end474println()475end476477# L∞ error478if :linf_error_primitive in analysis_errors479print(" Linf error pri.:")480for v in eachvariable(equations)481@printf("%10.8e ", linf_error_prim[v])482print(io, " ", linf_error_prim[v])483end484println()485end486end487end488489# additional integrals490analyze_integrals(analysis_integrals, io, du, u, t, semi)491492return nothing493end494495function print_level_information(mesh, solver, cache, min_level, max_level)496# Get local element count per level497elements_per_level = get_elements_per_level(min_level, max_level, mesh, solver,498cache)499500# Sum up across all ranks501MPI.Reduce!(elements_per_level, +, mpi_root(), mpi_comm())502503504for level in max_level:-1:(min_level + 1)505mpi_println(" ├── level $level: " *506@sprintf("% 14d", elements_per_level[level + 1 - min_level]))507end508mpi_println(" └── level $min_level: " *509@sprintf("% 14d", elements_per_level[1]))510511return nothing512end513514function print_level_information(callbacks, mesh::TreeMesh, solver, cache)515if uses_amr(callbacks)516# Get global minimum and maximum level from the AMRController517min_level = max_level = 0518for cb in callbacks.discrete_callbacks519if cb.affect! isa AMRCallback520min_level = cb.affect!.controller.base_level521max_level = cb.affect!.controller.max_level522end523end524print_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`, see527# `refine_box!`, `coarsen_box!`, and `refine_sphere!`.528elseif minimum_level(mesh.tree) != maximum_level(mesh.tree)529min_level = minimum_level(mesh.tree)530max_level = maximum_level(mesh.tree)531print_level_information(mesh, solver, cache, min_level, max_level)532else # Uniform mesh533return nothing534end535end536537function print_level_information(callbacks, mesh, solver, cache)538if uses_amr(callbacks)539# Get global minimum and maximum level from the AMRController540min_level = max_level = 0541for cb in callbacks.discrete_callbacks542if cb.affect! isa AMRCallback543min_level = cb.affect!.controller.base_level544max_level = cb.affect!.controller.max_level545end546end547print_level_information(mesh, solver, cache, min_level, max_level)548else # Uniform mesh549return nothing550end551end552553function get_elements_per_level(min_level, max_level, mesh::P4estMesh, solver, cache)554elements_per_level = zeros(P4EST_MAXLEVEL + 1)555556for tree in unsafe_wrap_sc(p4est_tree_t, mesh.p4est.trees)557elements_per_level .+= tree.quadrants_per_level558end559560return @view(elements_per_level[(min_level + 1):(max_level + 1)])561end562563function get_elements_per_level(min_level, max_level, mesh::T8codeMesh, solver, cache)564levels = trixi_t8_get_local_element_levels(mesh.forest)565566return [count(==(l), levels) for l in min_level:max_level]567end568569function get_elements_per_level(min_level, max_level, mesh::TreeMesh, solver, cache)570levels = [mesh.tree.levels[cache.elements.cell_ids[element]]571for element in eachelement(solver, cache)]572return [count(==(l), levels) for l in min_level:max_level]573end574575# Iterate over tuples of analysis integrals in a type-stable way using "lispy tuple programming".576function analyze_integrals(analysis_integrals::NTuple{N, Any}, io, du, u, t,577semi) where {N}578579# Extract the first analysis integral and process it; keep the remaining to be processed later580quantity = first(analysis_integrals)581remaining_quantities = Base.tail(analysis_integrals)582583res = analyze(quantity, du, u, t, semi)584if mpi_isroot()585@printf(" %-12s:", pretty_form_utf(quantity))586@printf(" % 10.8e", res)587print(io, " ", res)588end589mpi_println()590591# Recursively call this method with the unprocessed integrals592analyze_integrals(remaining_quantities, io, du, u, t, semi)593return nothing594end595596# terminate the type-stable iteration over tuples597function analyze_integrals(analysis_integrals::Tuple{}, io, du, u, t, semi)598return nothing599end600601# used for error checks and EOC analysis602function (cb::DiscreteCallback{Condition, Affect!})(sol) where {Condition,603Affect! <:604AnalysisCallback}605analysis_callback = cb.affect!606semi = sol.prob.p607@unpack analyzer = analysis_callback608cache_analysis = analysis_callback.cache609610l2_error, linf_error = calc_error_norms(sol.u[end], sol.t[end], analyzer, semi,611cache_analysis)612return (; l2 = l2_error, linf = linf_error)613end614615# some common analysis_integrals616# to support another analysis integral, you can overload617# Trixi.analyze, Trixi.pretty_form_utf, Trixi.pretty_form_ascii618function analyze(quantity, du, u, t, semi::AbstractSemidiscretization)619mesh, equations, solver, cache = mesh_equations_solver_cache(semi)620return analyze(quantity, du, u, t, mesh, equations, solver, cache)621end622function analyze(quantity, du, u, t, mesh, equations, solver, cache)623return integrate(quantity, u, mesh, equations, solver, cache, normalize = true)624end625pretty_form_utf(quantity) = get_name(quantity)626pretty_form_ascii(quantity) = get_name(quantity)627628# Special analyze for `SemidiscretizationHyperbolicParabolic` such that629# precomputed gradients are available.630function analyze(quantity::typeof(enstrophy), du, u, t,631semi::SemidiscretizationHyperbolicParabolic)632mesh, equations, solver, cache = mesh_equations_solver_cache(semi)633equations_parabolic = semi.equations_parabolic634cache_parabolic = semi.cache_parabolic635# We do not apply `enstrophy` directly here because we might later have different `quantity`s636# that we wish to integrate, which can share this routine.637return analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver,638cache, cache_parabolic)639end640function analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver,641cache, cache_parabolic)642return integrate(quantity, u, mesh, equations, equations_parabolic, solver, cache,643cache_parabolic, normalize = true)644end645646function entropy_timederivative end647pretty_form_utf(::typeof(entropy_timederivative)) = "∑∂S/∂U ⋅ Uₜ"648pretty_form_ascii(::typeof(entropy_timederivative)) = "dsdu_ut"649650pretty_form_utf(::typeof(entropy)) = "∑S"651652pretty_form_utf(::typeof(energy_total)) = "∑e_total"653pretty_form_ascii(::typeof(energy_total)) = "e_total"654655pretty_form_utf(::typeof(energy_kinetic)) = "∑e_kinetic"656pretty_form_ascii(::typeof(energy_kinetic)) = "e_kinetic"657658pretty_form_utf(::typeof(energy_kinetic_nondimensional)) = "∑e_kinetic*"659pretty_form_ascii(::typeof(energy_kinetic_nondimensional)) = "e_kinetic*"660661pretty_form_utf(::typeof(energy_internal)) = "∑e_internal"662pretty_form_ascii(::typeof(energy_internal)) = "e_internal"663664pretty_form_utf(::typeof(energy_magnetic)) = "∑e_magnetic"665pretty_form_ascii(::typeof(energy_magnetic)) = "e_magnetic"666667pretty_form_utf(::typeof(cross_helicity)) = "∑v⋅B"668pretty_form_ascii(::typeof(cross_helicity)) = "v_dot_B"669670pretty_form_utf(::typeof(enstrophy)) = "∑enstrophy"671pretty_form_ascii(::typeof(enstrophy)) = "enstrophy"672673pretty_form_utf(::Val{:l2_divb}) = "L2 ∇⋅B"674pretty_form_ascii(::Val{:l2_divb}) = "l2_divb"675676pretty_form_utf(::Val{:linf_divb}) = "L∞ ∇⋅B"677pretty_form_ascii(::Val{:linf_divb}) = "linf_divb"678679pretty_form_utf(::typeof(lake_at_rest_error)) = "∑|H₀-(h+b)|"680pretty_form_ascii(::typeof(lake_at_rest_error)) = "|H0-(h+b)|"681end # @muladd682683# specialized implementations specific to some solvers684include("analysis_dg1d.jl")685include("analysis_dg2d.jl")686include("analysis_surface_integral.jl")687include("analysis_dg2d_parallel.jl")688include("analysis_dg3d.jl")689include("analysis_dg3d_parallel.jl")690691# This version of `analyze` is used for [`AnalysisSurfaceIntegral`](@ref) which requires692# `semi` to be passed along to retrieve the current boundary indices, which are non-static693# in the case of AMR.694function analyze(quantity::AnalysisSurfaceIntegral, du, u, t,695semi::AbstractSemidiscretization)696mesh, equations, solver, cache = mesh_equations_solver_cache(semi)697return analyze(quantity, du, u, t, mesh, equations, solver, cache, semi)698end699700# Special analyze for `SemidiscretizationHyperbolicParabolic` such that701# 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` to703# have `VariableParabolic` available.704function analyze(quantity::AnalysisSurfaceIntegral{Variable},705du, u, t,706semi::SemidiscretizationHyperbolicParabolic) where {Variable <:707VariableParabolic}708mesh, equations, solver, cache = mesh_equations_solver_cache(semi)709equations_parabolic = semi.equations_parabolic710cache_parabolic = semi.cache_parabolic711return analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver, cache,712semi, cache_parabolic)713end714715716