Path: blob/main/src/callbacks_step/analysis.jl
2055 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::Float6451interval::Int52save_analysis::Bool53output_directory::String54analysis_filename::String55analyzer::Analyzer56analysis_errors::Vector{Symbol}57analysis_integrals::AnalysisIntegrals58initial_state_integrals::InitialStateIntegrals59cache::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)96AnalysisCallback(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)135136DiscreteCallback(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))146initialize!(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)204end205end206207# Record current time using a high-resolution clock208analysis_callback.start_time = time_ns()209210# Record current time for performance index computation211analysis_callback.start_time_last_analysis = time_ns()212213# Record current number of `rhs!` calls for performance index computation214analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)215216# Record total time spent in garbage collection so far using a high-resolution clock217# Note: For details see the actual callback function below218analysis_callback.start_gc_time = Base.gc_time_ns()219220analysis_callback(u_ode, du_ode, integrator, semi)221return nothing222end223224# This method gets called from OrdinaryDiffEq's `solve(...)`225function (analysis_callback::AnalysisCallback)(integrator)226semi = integrator.p227du_ode = first(get_tmp_cache(integrator))228u_ode = integrator.u229analysis_callback(u_ode, du_ode, integrator, semi)230end231232# This method gets called internally as the main entry point to the AnalysiCallback233# TODO: Taal refactor, allow passing an IO object (which could be devnull to avoid cluttering the console)234function (analysis_callback::AnalysisCallback)(u_ode, du_ode, integrator, semi)235mesh, equations, solver, cache = mesh_equations_solver_cache(semi)236@unpack dt, t = integrator237iter = integrator.stats.naccept238239# Compute the percentage of the simulation that is done240t = integrator.t241t_initial = first(integrator.sol.prob.tspan)242t_final = last(integrator.sol.prob.tspan)243sim_time_percentage = (t - t_initial) / (t_final - t_initial) * 100244245# Record performance measurements and compute performance index (PID)246runtime_since_last_analysis = 1.0e-9 * (time_ns() -247analysis_callback.start_time_last_analysis)248# PID is an MPI-aware measure of how much time per global degree of freedom (i.e., over all ranks249# and threads) and per `rhs!` evaluation is required. MPI-aware means that it essentially adds up250# the time spent on each computing unit. Thus, in an ideally parallelized program, the PID should be constant251# independent of the number of MPI ranks or threads used, since, e.g., using 4x the number of ranks should252# divide the runtime on each rank by 4. See also the Trixi.jl docs ("Performance" section) for253# more information.254ncalls_rhs_since_last_analysis = (ncalls(semi.performance_counter)255-256analysis_callback.ncalls_rhs_last_analysis)257# This assumes that the same number of threads is used on each MPI rank.258performance_index = runtime_since_last_analysis * mpi_nranks() *259Threads.nthreads() /260(ndofsglobal(mesh, solver, cache)261*262ncalls_rhs_since_last_analysis)263264# Compute the total runtime since the analysis callback has been initialized, in seconds265runtime_absolute = 1.0e-9 * (time_ns() - analysis_callback.start_time)266267# Compute the relative runtime per thread as time spent in `rhs!` divided by the number of calls268# to `rhs!` and the number of local degrees of freedom269# OBS! This computation must happen *after* the PID computation above, since `take!(...)`270# will reset the number of calls to `rhs!`271runtime_relative = 1.0e-9 * take!(semi.performance_counter) * Threads.nthreads() /272ndofs(semi)273274# Compute the total time spent in garbage collection since the analysis callback has been275# initialized, in seconds276# Note: `Base.gc_time_ns()` is not part of the public Julia API but has been available at least277# since Julia 1.6. Should this function be removed without replacement in a future Julia278# release, just delete this analysis quantity from the callback.279# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L83-L84280gc_time_absolute = 1.0e-9 * (Base.gc_time_ns() - analysis_callback.start_gc_time)281282# Compute the percentage of total time that was spent in garbage collection283gc_time_percentage = gc_time_absolute / runtime_absolute * 100284285# Obtain the current memory usage of the Julia garbage collector, in MiB, i.e., the total size of286# objects in memory that have been allocated by the JIT compiler or the user code.287# Note: `Base.gc_live_bytes()` is not part of the public Julia API but has been available at least288# since Julia 1.6. Should this function be removed without replacement in a future Julia289# release, just delete this analysis quantity from the callback.290# Source: https://github.com/JuliaLang/julia/blob/b540315cb4bd91e6f3a3e4ab8129a58556947628/base/timing.jl#L86-L97291memory_use = Base.gc_live_bytes() / 2^20 # bytes -> MiB292293@trixi_timeit timer() "analyze solution" begin294# General information295mpi_println()296mpi_println("─"^100)297mpi_println(" Simulation running '", get_name(equations), "' with ",298summary(solver))299mpi_println("─"^100)300mpi_println(" #timesteps: " * @sprintf("% 14d", iter) *301" " *302" run time: " * @sprintf("%10.8e s", runtime_absolute))303mpi_println(" Δt: " * @sprintf("%10.8e", dt) *304" " *305" └── GC time: " *306@sprintf("%10.8e s (%5.3f%%)", gc_time_absolute, gc_time_percentage))307mpi_println(rpad(" sim. time: " *308@sprintf("%10.8e (%5.3f%%)", t, sim_time_percentage), 46) *309" time/DOF/rhs!: " * @sprintf("%10.8e s", runtime_relative))310mpi_println(" " * " " *311" " *312" PID: " * @sprintf("%10.8e s", performance_index))313mpi_println(" #DOFs per field:" * @sprintf("% 14d", ndofsglobal(semi)) *314" " *315" alloc'd memory: " * @sprintf("%14.3f MiB", memory_use))316mpi_println(" #elements: " *317@sprintf("% 14d", nelementsglobal(mesh, solver, cache)))318319# Level information (only for AMR and/or non-uniform `TreeMesh`es)320print_level_information(integrator.opts.callback, mesh, solver, cache)321mpi_println()322323# Open file for appending and store time step and time information324if mpi_isroot() && analysis_callback.save_analysis325io = open(joinpath(analysis_callback.output_directory,326analysis_callback.analysis_filename), "a")327print(io, iter)328print(io, " ", t)329print(io, " ", dt)330else331io = devnull332end333334# Calculate current time derivative (needed for semidiscrete entropy time derivative, residual, etc.)335# `integrator.f` is usually just a call to `rhs!`336# However, we want to allow users to modify the ODE RHS outside of Trixi.jl337# and allow us to pass a combined ODE RHS to OrdinaryDiffEq, e.g., for338# hyperbolic-parabolic systems.339@notimeit timer() integrator.f(du_ode, u_ode, semi, t)340u = wrap_array(u_ode, mesh, equations, solver, cache)341du = wrap_array(du_ode, mesh, equations, solver, cache)342# Compute l2_error, linf_error343analysis_callback(io, du, u, u_ode, t, semi)344345mpi_println("─"^100)346mpi_println()347348# Add line break and close analysis file if it was opened349if mpi_isroot() && analysis_callback.save_analysis350# This resolves a possible type instability introduced above, since `io`351# can either be an `IOStream` or `devnull`, but we know that it must be352# an `IOStream here`.353println(io::IOStream)354close(io::IOStream)355end356end357358# avoid re-evaluating possible FSAL stages359u_modified!(integrator, false)360361# Reset performance measurements362analysis_callback.start_time_last_analysis = time_ns()363analysis_callback.ncalls_rhs_last_analysis = ncalls(semi.performance_counter)364365return nothing366end367368# This method is just called internally from `(analysis_callback::AnalysisCallback)(integrator)`369# and serves as a function barrier. Additionally, it makes the code easier to profile and optimize.370function (analysis_callback::AnalysisCallback)(io, du, u, u_ode, t, semi)371@unpack analyzer, analysis_errors, analysis_integrals = analysis_callback372cache_analysis = analysis_callback.cache373_, equations, _, _ = mesh_equations_solver_cache(semi)374375# Calculate and print derived quantities (error norms, entropy etc.)376# Variable names required for L2 error, Linf error, and conservation error377if any(q in analysis_errors378for q in (:l2_error, :linf_error, :conservation_error, :residual)) &&379mpi_isroot()380print(" Variable: ")381for v in eachvariable(equations)382@printf(" %-14s", varnames(cons2cons, equations)[v])383end384println()385end386387if :l2_error in analysis_errors || :linf_error in analysis_errors388# Calculate L2/Linf errors389l2_error, linf_error = calc_error_norms(u_ode, t, analyzer, semi,390cache_analysis)391392if mpi_isroot()393# L2 error394if :l2_error in analysis_errors395print(" L2 error: ")396for v in eachvariable(equations)397@printf(" % 10.8e", l2_error[v])398print(io, " ", l2_error[v])399end400println()401end402403# Linf error404if :linf_error in analysis_errors405print(" Linf error: ")406for v in eachvariable(equations)407@printf(" % 10.8e", linf_error[v])408print(io, " ", linf_error[v])409end410println()411end412end413end414415# Conservation error416if :conservation_error in analysis_errors417@unpack initial_state_integrals = analysis_callback418state_integrals = integrate(u_ode, semi)419420if mpi_isroot()421print(" |∑U - ∑U₀|: ")422for v in eachvariable(equations)423err = abs(state_integrals[v] - initial_state_integrals[v])424@printf(" % 10.8e", err)425print(io, " ", err)426end427println()428end429end430431# Residual (defined here as the vector maximum of the absolute values of the time derivatives)432if :residual in analysis_errors433mpi_print(" max(|Uₜ|): ")434for v in eachvariable(equations)435# Calculate maximum absolute value of Uₜ436res = maximum(abs, view(du, v, ..))437if mpi_isparallel()438# TODO: Debugging, here is a type instability439# Base.max instead of max needed, see comment in src/auxiliary/math.jl440global_res = MPI.Reduce!(Ref(res), Base.max, mpi_root(), mpi_comm())441if mpi_isroot()442res::eltype(du) = global_res[]443end444end445if mpi_isroot()446@printf(" % 10.8e", res)447print(io, " ", res)448end449end450mpi_println()451end452453# L2/L∞ errors of the primitive variables454if :l2_error_primitive in analysis_errors ||455:linf_error_primitive in analysis_errors456l2_error_prim, linf_error_prim = calc_error_norms(cons2prim, u_ode, t, analyzer,457semi, cache_analysis)458459if mpi_isroot()460print(" Variable: ")461for v in eachvariable(equations)462@printf(" %-14s", varnames(cons2prim, equations)[v])463end464println()465466# L2 error467if :l2_error_primitive in analysis_errors468print(" L2 error prim.: ")469for v in eachvariable(equations)470@printf("%10.8e ", l2_error_prim[v])471print(io, " ", l2_error_prim[v])472end473println()474end475476# L∞ error477if :linf_error_primitive in analysis_errors478print(" Linf error pri.:")479for v in eachvariable(equations)480@printf("%10.8e ", linf_error_prim[v])481print(io, " ", linf_error_prim[v])482end483println()484end485end486end487488# additional integrals489analyze_integrals(analysis_integrals, io, du, u, t, semi)490491return nothing492end493494function print_level_information(mesh, solver, cache, min_level, max_level)495# Get local element count per level496elements_per_level = get_elements_per_level(min_level, max_level, mesh, solver,497cache)498499# Sum up across all ranks500MPI.Reduce!(elements_per_level, +, mpi_root(), mpi_comm())501502503for level in max_level:-1:(min_level + 1)504mpi_println(" ├── level $level: " *505@sprintf("% 14d", elements_per_level[level + 1 - min_level]))506end507mpi_println(" └── level $min_level: " *508@sprintf("% 14d", elements_per_level[1]))509510return nothing511end512513function print_level_information(callbacks, mesh::TreeMesh, solver, cache)514if uses_amr(callbacks)515# Get global minimum and maximum level from the AMRController516min_level = max_level = 0517for cb in callbacks.discrete_callbacks518if cb.affect! isa AMRCallback519min_level = cb.affect!.controller.base_level520max_level = cb.affect!.controller.max_level521end522end523print_level_information(mesh, solver, cache, min_level, max_level)524# Special check for `TreeMesh`es without AMR.525# These meshes may still be non-uniform due to `refinement_patches`, see526# `refine_box!`, `coarsen_box!`, and `refine_sphere!`.527elseif minimum_level(mesh.tree) != maximum_level(mesh.tree)528min_level = minimum_level(mesh.tree)529max_level = maximum_level(mesh.tree)530print_level_information(mesh, solver, cache, min_level, max_level)531else # Uniform mesh532return nothing533end534end535536function print_level_information(callbacks, mesh, solver, cache)537if uses_amr(callbacks)538# Get global minimum and maximum level from the AMRController539min_level = max_level = 0540for cb in callbacks.discrete_callbacks541if cb.affect! isa AMRCallback542min_level = cb.affect!.controller.base_level543max_level = cb.affect!.controller.max_level544end545end546print_level_information(mesh, solver, cache, min_level, max_level)547else # Uniform mesh548return nothing549end550end551552function get_elements_per_level(min_level, max_level, mesh::P4estMesh, solver, cache)553elements_per_level = zeros(P4EST_MAXLEVEL + 1)554555for tree in unsafe_wrap_sc(p4est_tree_t, mesh.p4est.trees)556elements_per_level .+= tree.quadrants_per_level557end558559return @view(elements_per_level[(min_level + 1):(max_level + 1)])560end561562function get_elements_per_level(min_level, max_level, mesh::T8codeMesh, solver, cache)563levels = trixi_t8_get_local_element_levels(mesh.forest)564565return [count(==(l), levels) for l in min_level:max_level]566end567568function get_elements_per_level(min_level, max_level, mesh::TreeMesh, solver, cache)569levels = [mesh.tree.levels[cache.elements.cell_ids[element]]570for element in eachelement(solver, cache)]571return [count(==(l), levels) for l in min_level:max_level]572end573574# Iterate over tuples of analysis integrals in a type-stable way using "lispy tuple programming".575function analyze_integrals(analysis_integrals::NTuple{N, Any}, io, du, u, t,576semi) where {N}577578# Extract the first analysis integral and process it; keep the remaining to be processed later579quantity = first(analysis_integrals)580remaining_quantities = Base.tail(analysis_integrals)581582res = analyze(quantity, du, u, t, semi)583if mpi_isroot()584@printf(" %-12s:", pretty_form_utf(quantity))585@printf(" % 10.8e", res)586print(io, " ", res)587end588mpi_println()589590# Recursively call this method with the unprocessed integrals591analyze_integrals(remaining_quantities, io, du, u, t, semi)592return nothing593end594595# terminate the type-stable iteration over tuples596function analyze_integrals(analysis_integrals::Tuple{}, io, du, u, t, semi)597nothing598end599600# used for error checks and EOC analysis601function (cb::DiscreteCallback{Condition, Affect!})(sol) where {Condition,602Affect! <:603AnalysisCallback}604analysis_callback = cb.affect!605semi = sol.prob.p606@unpack analyzer = analysis_callback607cache_analysis = analysis_callback.cache608609l2_error, linf_error = calc_error_norms(sol.u[end], sol.t[end], analyzer, semi,610cache_analysis)611(; l2 = l2_error, linf = linf_error)612end613614# some common analysis_integrals615# to support another analysis integral, you can overload616# Trixi.analyze, Trixi.pretty_form_utf, Trixi.pretty_form_ascii617function analyze(quantity, du, u, t, semi::AbstractSemidiscretization)618mesh, equations, solver, cache = mesh_equations_solver_cache(semi)619analyze(quantity, du, u, t, mesh, equations, solver, cache)620end621function analyze(quantity, du, u, t, mesh, equations, solver, cache)622integrate(quantity, u, mesh, equations, solver, cache, normalize = true)623end624pretty_form_utf(quantity) = get_name(quantity)625pretty_form_ascii(quantity) = get_name(quantity)626627# Special analyze for `SemidiscretizationHyperbolicParabolic` such that628# precomputed gradients are available.629function analyze(quantity::typeof(enstrophy), du, u, t,630semi::SemidiscretizationHyperbolicParabolic)631mesh, equations, solver, cache = mesh_equations_solver_cache(semi)632equations_parabolic = semi.equations_parabolic633cache_parabolic = semi.cache_parabolic634# We do not apply `enstrophy` directly here because we might later have different `quantity`s635# that we wish to integrate, which can share this routine.636analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver, cache,637cache_parabolic)638end639function analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver,640cache, cache_parabolic)641integrate(quantity, u, mesh, equations, equations_parabolic, solver, cache,642cache_parabolic, normalize = true)643end644645function entropy_timederivative end646pretty_form_utf(::typeof(entropy_timederivative)) = "∑∂S/∂U ⋅ Uₜ"647pretty_form_ascii(::typeof(entropy_timederivative)) = "dsdu_ut"648649pretty_form_utf(::typeof(entropy)) = "∑S"650651pretty_form_utf(::typeof(energy_total)) = "∑e_total"652pretty_form_ascii(::typeof(energy_total)) = "e_total"653654pretty_form_utf(::typeof(energy_kinetic)) = "∑e_kinetic"655pretty_form_ascii(::typeof(energy_kinetic)) = "e_kinetic"656657pretty_form_utf(::typeof(energy_kinetic_nondimensional)) = "∑e_kinetic*"658pretty_form_ascii(::typeof(energy_kinetic_nondimensional)) = "e_kinetic*"659660pretty_form_utf(::typeof(energy_internal)) = "∑e_internal"661pretty_form_ascii(::typeof(energy_internal)) = "e_internal"662663pretty_form_utf(::typeof(energy_magnetic)) = "∑e_magnetic"664pretty_form_ascii(::typeof(energy_magnetic)) = "e_magnetic"665666pretty_form_utf(::typeof(cross_helicity)) = "∑v⋅B"667pretty_form_ascii(::typeof(cross_helicity)) = "v_dot_B"668669pretty_form_utf(::typeof(enstrophy)) = "∑enstrophy"670pretty_form_ascii(::typeof(enstrophy)) = "enstrophy"671672pretty_form_utf(::Val{:l2_divb}) = "L2 ∇⋅B"673pretty_form_ascii(::Val{:l2_divb}) = "l2_divb"674675pretty_form_utf(::Val{:linf_divb}) = "L∞ ∇⋅B"676pretty_form_ascii(::Val{:linf_divb}) = "linf_divb"677678pretty_form_utf(::typeof(lake_at_rest_error)) = "∑|H₀-(h+b)|"679pretty_form_ascii(::typeof(lake_at_rest_error)) = "|H0-(h+b)|"680end # @muladd681682# specialized implementations specific to some solvers683include("analysis_dg1d.jl")684include("analysis_dg2d.jl")685include("analysis_surface_integral.jl")686include("analysis_dg2d_parallel.jl")687include("analysis_dg3d.jl")688include("analysis_dg3d_parallel.jl")689690# This version of `analyze` is used for [`AnalysisSurfaceIntegral`](@ref) which requires691# `semi` to be passed along to retrieve the current boundary indices, which are non-static692# in the case of AMR.693function analyze(quantity::AnalysisSurfaceIntegral, du, u, t,694semi::AbstractSemidiscretization)695mesh, equations, solver, cache = mesh_equations_solver_cache(semi)696analyze(quantity, du, u, t, mesh, equations, solver, cache, semi)697end698699# Special analyze for `SemidiscretizationHyperbolicParabolic` such that700# precomputed gradients are available. Required for `enstrophy` (see above) and viscous forces.701# Note that this needs to be included after `analysis_surface_integral_2d.jl` to702# have `VariableViscous` available.703function analyze(quantity::AnalysisSurfaceIntegral{Variable},704du, u, t,705semi::SemidiscretizationHyperbolicParabolic) where {706Variable <:707VariableViscous}708mesh, equations, solver, cache = mesh_equations_solver_cache(semi)709equations_parabolic = semi.equations_parabolic710cache_parabolic = semi.cache_parabolic711analyze(quantity, du, u, t, mesh, equations, equations_parabolic, solver, cache, semi,712cache_parabolic)713end714715716