Path: blob/main/src/solvers/dgmulti/flux_differencing.jl
5590 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# Return the contravariant basis vector corresponding to the Cartesian8# coordinate direction `orientation` in a given `element` of the `mesh`.9# The contravariant basis vectors have entries `dx_i / dxhat_j` where10# j ∈ {1, ..., NDIMS}. Here, `x_i` and `xhat_j` are the ith physical coordinate11# and jth reference coordinate, respectively. These are geometric terms which12# appear when using the chain rule to compute physical derivatives as a linear13# combination of reference derivatives.14@inline function get_contravariant_vector(element, orientation,15mesh::DGMultiMesh{NDIMS}, cache) where {NDIMS}16# note that rstxyzJ = [rxJ, sxJ, txJ; ryJ syJ tyJ; rzJ szJ tzJ], so that this will return17# SVector{2}(rxJ[1, element], ryJ[1, element]) in 2D.1819# assumes geometric terms are constant on each element20dxidxhatj = mesh.md.rstxyzJ21return SVector{NDIMS}(getindex.(dxidxhatj[:, orientation], 1, element))22end2324@inline function get_contravariant_vector(element, orientation,25mesh::DGMultiMesh{NDIMS, NonAffine},26cache) where {NDIMS}27# note that rstxyzJ = [rxJ, sxJ, txJ; ryJ syJ tyJ; rzJ szJ tzJ]2829# assumes geometric terms vary spatially over each element30(; dxidxhatj) = cache31return SVector{NDIMS}(view.(dxidxhatj[:, orientation], :, element))32end3334# For Affine meshes, `get_contravariant_vector` returns an SVector of scalars (constant over the35# element). The normal direction is the same for all node pairs.36@inline get_normal_direction(normal_directions::AbstractVector, i, j) = normal_directions3738# For NonAffine meshes, `get_contravariant_vector` returns an SVector of per-node arrays.39# We average the normals at nodes i and j for provably entropy-stable de-aliasing of geometric terms.40@inline function get_normal_direction(normal_directions::AbstractVector{<:AbstractVector},41i, j)42return 0.5f0 * (getindex.(normal_directions, i) + getindex.(normal_directions, j))43end4445# use hybridized SBP operators for general flux differencing schemes.46function compute_flux_differencing_SBP_matrices(dg::DGMulti)47return compute_flux_differencing_SBP_matrices(dg, has_sparse_operators(dg))48end4950function compute_flux_differencing_SBP_matrices(dg::DGMulti, sparse_operators)51rd = dg.basis52Qrst_hybridized, VhP, Ph = StartUpDG.hybridized_SBP_operators(rd)53Qrst_skew = map(A -> 0.5 * (A - A'), Qrst_hybridized)54if sparse_operators == true55Qrst_skew = map(Qi -> droptol!(sparse(Qi'), 100 * eps(eltype(Qi)))', Qrst_skew)56end57return Qrst_skew, VhP, Ph58end5960# use traditional multidimensional SBP operators for SBP approximation types.61function compute_flux_differencing_SBP_matrices(dg::DGMultiFluxDiffSBP,62sparse_operators)63rd = dg.basis64@unpack M, Drst, Pq = rd65Qrst = map(D -> M * D, Drst)66Qrst_skew = map(A -> 0.5 * (A - A'), Qrst)67if sparse_operators == true68Qrst_skew = map(Qi -> droptol!(sparse(Qi'), 100 * eps(eltype(Qi)))', Qrst_skew)69end70return Qrst_skew71end7273# Build element-to-element connectivity from face-to-face connectivity.74# Used for smoothing of shock capturing blending parameters (see `apply_smoothing!`).75#76# Here, `mesh.md.FToF` is a `(num_faces_per_element × num_elements)` array where77# `FToF[f, e]` stores the global face index of the neighbor of local face `f` on78# element `e`.79#80# Global face indices are laid out as81#82# global_face_index = (element_index - 1) * num_faces + local_face_index,83#84# so that the element index can be recovered by integer division:85#86# element_index = (global_face_index - 1) ÷ num_faces + 1.87#88# For a non-periodic boundary face, `FToF[f, e]` points back to face `f` of element89# `e` itself, so boundary elements are listed as their own neighbor.90function build_element_to_element_connectivity(mesh::DGMultiMesh, dg::DGMulti)91face_to_face_connectivity = mesh.md.FToF92element_to_element_connectivity = similar(face_to_face_connectivity)93for e in axes(face_to_face_connectivity, 2)94for f in axes(face_to_face_connectivity, 1)95neighbor_face_index = face_to_face_connectivity[f, e]9697# Reverse-engineer element index from face index. Assumes all elements98# have the same number of faces.99neighbor_element_index = ((neighbor_face_index - 1) ÷ dg.basis.num_faces) +1001101element_to_element_connectivity[f, e] = neighbor_element_index102end103end104return element_to_element_connectivity105end106107# For flux differencing SBP-type approximations, store solutions in Matrix{SVector{nvars}}.108# This results in a slight speedup for `calc_volume_integral!`.109function allocate_nested_array(uEltype, nvars, array_dimensions, dg::DGMultiFluxDiffSBP)110return zeros(SVector{nvars, uEltype}, array_dimensions...)111end112113function create_cache(mesh::DGMultiMesh, equations, dg::DGMultiFluxDiffSBP,114RealT, uEltype)115rd = dg.basis116md = mesh.md117118# for use with flux differencing schemes119Qrst_skew = compute_flux_differencing_SBP_matrices(dg)120121lift_scalings = rd.wf ./ rd.wq[rd.Fmask] # lift scalings for diag-norm SBP operators122123nvars = nvariables(equations)124# Use an array of SVectors (chunks of `nvars` are contiguous in memory) to speed up flux differencing125du_local_threaded = [zeros(SVector{nvars, uEltype}, rd.Nq)126for _ in 1:Threads.maxthreadid()]127128solution_container = initialize_dgmulti_solution_container(mesh, equations, dg,129uEltype)130131return (; md, Qrst_skew, dxidxhatj = md.rstxyzJ,132invJ = inv.(md.J), lift_scalings, inv_wq = inv.(rd.wq),133solution_container, du_local_threaded)134end135136# most general create_cache: works for `DGMultiFluxDiff{<:Polynomial}`137function create_cache(mesh::DGMultiMesh, equations, dg::DGMultiFluxDiff, RealT, uEltype)138rd = dg.basis139@unpack md = mesh140141Qrst_skew, VhP, Ph = compute_flux_differencing_SBP_matrices(dg)142143# temp storage for entropy variables at volume quad points144nvars = nvariables(equations)145entropy_var_values = allocate_nested_array(uEltype, nvars, (rd.Nq, md.num_elements),146dg)147148# storage for all quadrature points (concatenated volume / face quadrature points)149num_quad_points_total = rd.Nq + rd.Nfq150entropy_projected_u_values = allocate_nested_array(uEltype, nvars,151(num_quad_points_total,152md.num_elements), dg)153projected_entropy_var_values = allocate_nested_array(uEltype, nvars,154(num_quad_points_total,155md.num_elements), dg)156157# For this specific solver, `prolong2interfaces` will not be used anymore.158# Instead, this step is also performed in `entropy_projection!`. Thus, we set159# `u_face_values` as a `view` into `entropy_projected_u_values`. We do not do160# the same for `u_values` since we will use that with LoopVectorization, which161# cannot handle such views as of v0.12.66, the latest version at the time of writing.162u_values = allocate_nested_array(uEltype, nvars, size(md.xq), dg)163u_face_values = view(entropy_projected_u_values, (rd.Nq + 1):num_quad_points_total,164:)165flux_face_values = similar(u_face_values)166167# local storage for interface fluxes, rhs, and source168local_values_threaded = [allocate_nested_array(uEltype, nvars, (rd.Nq,), dg)169for _ in 1:Threads.maxthreadid()]170171# Use an array of SVectors (chunks of `nvars` are contiguous in memory) to speed up flux differencing172# The result is then transferred to `rhs_local`, a thread-local element of173# `rhs_local_threaded::StructArray{<:SVector}` before projecting it and storing it into `du`.174du_local_threaded = [zeros(SVector{nvars, uEltype}, num_quad_points_total)175for _ in 1:Threads.maxthreadid()]176rhs_local_threaded = [allocate_nested_array(uEltype, nvars,177(num_quad_points_total,), dg)178for _ in 1:Threads.maxthreadid()]179180# interpolate geometric terms to both quadrature and face values for curved meshes181(; Vq, Vf) = dg.basis182interpolated_geometric_terms = map(x -> [Vq; Vf] * x, mesh.md.rstxyzJ)183J = Vq * md.J184185solution_container = DGMultiSolutionContainer(u_values, u_face_values,186flux_face_values,187local_values_threaded)188189return (; md, Qrst_skew, VhP, Ph,190invJ = inv.(J), dxidxhatj = interpolated_geometric_terms,191entropy_var_values, projected_entropy_var_values,192entropy_projected_u_values,193solution_container, du_local_threaded, rhs_local_threaded)194end195196# TODO: DGMulti. Address hard-coding of `entropy2cons!` and `cons2entropy!` for this function.197function entropy_projection!(cache, u, mesh::DGMultiMesh, equations, dg::DGMulti)198rd = dg.basis199@unpack Vq = rd200@unpack VhP, entropy_var_values = cache201@unpack projected_entropy_var_values, entropy_projected_u_values = cache202(; u_values) = cache.solution_container203204apply_to_each_field(mul_by!(Vq), u_values, u)205206cons2entropy!(entropy_var_values, u_values, equations)207208# "VhP" fuses the projection "P" with interpolation to volume and face quadrature "Vh"209apply_to_each_field(mul_by!(VhP), projected_entropy_var_values, entropy_var_values)210211entropy2cons!(entropy_projected_u_values, projected_entropy_var_values, equations)212return nothing213end214215@inline function cons2entropy!(entropy_var_values::StructArray,216u_values::StructArray,217equations)218@threaded for i in eachindex(u_values)219entropy_var_values[i] = cons2entropy(u_values[i], equations)220end221end222223@inline function entropy2cons!(entropy_projected_u_values::StructArray,224projected_entropy_var_values::StructArray,225equations)226@threaded for i in eachindex(projected_entropy_var_values)227entropy_projected_u_values[i] = entropy2cons(projected_entropy_var_values[i],228equations)229end230end231232# Trait-like system to dispatch based on whether or not the SBP operators are sparse.233# Designed to be extendable to include specialized `approximation_types` too.234@inline function has_sparse_operators(dg::DGMultiFluxDiff)235rd = dg.basis236return has_sparse_operators(rd.element_type, rd.approximation_type)237end238239# General fallback for DGMulti solvers:240# Polynomial-based solvers use hybridized SBP operators, which have blocks scaled by outward241# normal components. This implies that operators for different coordinate directions have242# different sparsity patterns. We default to using sum factorization (which is faster when243# operators are sparse) for all `DGMulti` / `StartUpDG.jl` approximation types.244@inline has_sparse_operators(element_type, approx_type) = True()245246# For traditional SBP operators on triangles, the operators are fully dense. We avoid using247# sum factorization here, which is slower for fully dense matrices.248@inline function has_sparse_operators(::Union{Line, Tri, Tet},249approx_type::AT) where {AT <: SBP}250return False()251end252253# SBP/GaussSBP operators on quads/hexes use tensor-product operators. Thus, sum factorization is254# more efficient and we use the sparsity structure.255@inline function has_sparse_operators(::Union{Quad, Hex},256approx_type::AT) where {AT <: SBP}257return True()258end259@inline has_sparse_operators(::Union{Quad, Hex}, approx_type::GaussSBP) = True()260261# FD SBP methods have sparse operators262@inline function has_sparse_operators(::Union{Line, Quad, Hex},263approx_type::AbstractDerivativeOperator)264return True()265end266267function calc_volume_integral!(du, u, mesh::DGMultiMesh,268have_nonconservative_terms, equations,269volume_integral, dg::DGMultiFluxDiff, cache,270alpha = true)271# No interpolation performed for general volume integral.272# Instead, an element-wise entropy projection (`entropy_projection!`) is performed before, see273# `rhs!` for `DGMultiFluxDiff`, which populates `entropy_projected_u_values`274@threaded for element in eachelement(mesh, dg, cache)275volume_integral_kernel!(du, u, element, mesh,276have_nonconservative_terms, equations,277volume_integral, dg, cache, alpha)278end279280return nothing281end282283# Computes flux differencing contribution over a single element by looping over node pairs (i, j).284# The physical normal direction for each pair is n_ij = geometric_matrix * ref_entries,285# where ref_entries[d] = Qrst_skew[d][i,j].286# This fuses the NDIMS per-dimension flux287# evaluations of the old dimension-by-dimension loop into a single evaluation per pair.288# Essentially, instead of calculating289# volume_flux(u_i, u_j, 1, equations) * Qx[i, j] + volume_flux(u_i, u_j, 2, equations) * Qy[i, j] + ...290# where Qx[i, j] = dr/dx * Qr[i, j] + ds/dx * Qs[i, j], we can expand out and evaluate291# volume_flux(u_i, u_j, [dr/dx, dr/dy] * Qr[i, j], equations) +292# volume_flux(u_i, u_j, [ds/dx, ds/dy] * Qs[i, j], equations)293# which is slightly faster.294#295# For dense operators (SBP on Line/Tri/Tet), we do not use this sum factorization trick.296@inline function local_flux_differencing!(du_local, u_local, element_index,297have_nonconservative_terms::False,298volume_flux,299has_sparse_operators::False, mesh,300equations, dg, cache)301@unpack Qrst_skew = cache302NDIMS = ndims(mesh)303row_ids = axes(first(Qrst_skew), 1)304geometric_matrix = get_contravariant_matrix(element_index, mesh, cache)305for i in row_ids306u_i = u_local[i]307for j in row_ids308# We use the symmetry of the volume flux and the anti-symmetry309# of the derivative operator to save half of the volume flux310# computations.311if j > i312u_j = u_local[j]313ref_entries = SVector(ntuple(d -> Qrst_skew[d][i, j], Val(NDIMS)))314normal_direction = geometric_matrix * ref_entries315AF_ij = 2 * volume_flux(u_i, u_j, normal_direction, equations)316du_local[i] = du_local[i] + AF_ij317du_local[j] = du_local[j] - AF_ij # Due to skew-symmetry318end319end320end321end322323@inline function local_flux_differencing!(du_local, u_local, element_index,324have_nonconservative_terms::True, volume_flux,325has_sparse_operators::False, mesh,326equations, dg, cache)327@unpack Qrst_skew = cache328NDIMS = ndims(mesh)329flux_conservative, flux_nonconservative = volume_flux330row_ids = axes(first(Qrst_skew), 1)331geometric_matrix = get_contravariant_matrix(element_index, mesh, cache)332for i in row_ids333u_i = u_local[i]334for j in row_ids335ref_entries = SVector(ntuple(d -> Qrst_skew[d][i, j], Val(NDIMS)))336normal_direction = geometric_matrix * ref_entries337# We use the symmetry of the volume flux and the anti-symmetry338# of the derivative operator to save half of the volume flux339# computations.340if j > i341u_j = u_local[j]342AF_ij = 2 * flux_conservative(u_i, u_j, normal_direction, equations)343du_local[i] = du_local[i] + AF_ij344du_local[j] = du_local[j] - AF_ij # Due to skew-symmetry345end346# Non-conservative terms use the full (non-symmetric) loop.347# The 0.5f0 factor on the normal direction is necessary for the nonconservative348# fluxes based on the interpretation of global SBP operators.349# See also `calc_interface_flux!` with `have_nonconservative_terms::True`350# in src/solvers/dgsem_tree/dg_1d.jl351f_nc = flux_nonconservative(u_i, u_local[j], 0.5f0 * normal_direction,352equations)353du_local[i] = du_local[i] + 2 * f_nc354end355end356end357358# When the operators are sparse, we use the sum-factorization approach to359# computing flux differencing. Each dimension has its own sparse operator with360# its own sparsity pattern (e.g., tensor-product structure on Quad/Hex elements),361# so we loop per-dimension. For each nonzero entry A[i,j] we evaluate the flux once362# and exploit skew-symmetry to accumulate both the (i,j) and (j,i) contributions.363@inline function local_flux_differencing!(du_local, u_local, element_index,364have_nonconservative_terms::False,365volume_flux,366has_sparse_operators::True, mesh,367equations, dg, cache)368@unpack Qrst_skew = cache369for dim in eachdim(mesh)370normal_directions = get_contravariant_vector(element_index, dim, mesh, cache)371Q_skew = Qrst_skew[dim]372A_base, row_ids, rows, vals = sparse_operator_data(Q_skew)373for i in row_ids374u_i = u_local[i]375du_i = du_local[i]376for id in nzrange(A_base, i)377j = rows[id]378# This routine computes only the upper-triangular part of the hadamard sum (A .* F).379# We avoid computing the lower-triangular part, and instead accumulate those contributions380# while computing the upper-triangular part (using the fact that A is skew-symmetric and F381# is symmetric).382if j > i383u_j = u_local[j]384A_ij = vals[id]385normal_direction_ij = get_normal_direction(normal_directions, i, j)386AF_ij = 2 * A_ij *387volume_flux(u_i, u_j, normal_direction_ij, equations)388du_i = du_i + AF_ij389du_local[j] = du_local[j] - AF_ij # Due to skew-symmetry390end391end392du_local[i] = du_i393end394end395end396397@inline function local_flux_differencing!(du_local, u_local, element_index,398have_nonconservative_terms::True, volume_flux,399has_sparse_operators::True, mesh,400equations, dg, cache)401@unpack Qrst_skew = cache402flux_conservative, flux_nonconservative = volume_flux403for dim in eachdim(mesh)404normal_directions = get_contravariant_vector(element_index, dim, mesh, cache)405Q_skew = Qrst_skew[dim]406A_base, row_ids, rows, vals = sparse_operator_data(Q_skew)407for i in row_ids408u_i = u_local[i]409du_i = du_local[i]410for id in nzrange(A_base, i)411j = rows[id]412A_ij = vals[id]413u_j = u_local[j]414normal_direction_ij = get_normal_direction(normal_directions, i, j)415# Conservative part: exploit skew-symmetry (calculate upper triangular part only).416if j > i417AF_ij = 2 * A_ij *418flux_conservative(u_i, u_j, normal_direction_ij, equations)419du_i = du_i + AF_ij420du_local[j] = du_local[j] - AF_ij # Due to skew-symmetry421end422# Non-conservative terms use the full (non-symmetric) loop.423# The 0.5f0 factor on the normal direction is necessary for the nonconservative424# fluxes based on the interpretation of global SBP operators.425# See also `calc_interface_flux!` with `have_nonconservative_terms::True`426# in src/solvers/dgsem_tree/dg_1d.jl427f_nc = flux_nonconservative(u_i, u_j, 0.5f0 * normal_direction_ij,428equations)429du_i = du_i + 2 * A_ij * f_nc430end431du_local[i] = du_i432end433end434end435436# calculates volume integral for <:Polynomial approximation types. We437# do not assume any additional structure (such as collocated volume or438# face nodes, tensor product structure, etc) in `DGMulti`.439@inline function volume_integral_kernel!(du, u, element, mesh::DGMultiMesh,440have_nonconservative_terms, equations,441volume_integral::VolumeIntegralFluxDifferencing,442dg::DGMultiFluxDiff, cache, alpha = true)443@unpack entropy_projected_u_values, Ph = cache444@unpack du_local_threaded, rhs_local_threaded = cache445446du_local = du_local_threaded[Threads.threadid()]447fill!(du_local, zero(eltype(du_local)))448u_local = view(entropy_projected_u_values, :, element)449450local_flux_differencing!(du_local, u_local, element,451have_nonconservative_terms,452volume_integral.volume_flux,453has_sparse_operators(dg),454mesh, equations, dg, cache)455456# convert du_local::Vector{<:SVector} to StructArray{<:SVector} for faster457# apply_to_each_field performance.458rhs_local = rhs_local_threaded[Threads.threadid()]459for i in Base.OneTo(length(du_local))460rhs_local[i] = du_local[i]461end462apply_to_each_field(mul_by_accum!(Ph, alpha), view(du, :, element), rhs_local)463464return nothing465end466467@inline function volume_integral_kernel!(du, u, element, mesh::DGMultiMesh,468have_nonconservative_terms, equations,469volume_integral::VolumeIntegralFluxDifferencing,470dg::DGMultiFluxDiffSBP, cache,471alpha = true)472@unpack du_local_threaded, inv_wq = cache473474du_local = du_local_threaded[Threads.threadid()]475fill!(du_local, zero(eltype(du_local)))476u_local = view(u, :, element)477478local_flux_differencing!(du_local, u_local, element,479have_nonconservative_terms,480volume_integral.volume_flux,481has_sparse_operators(dg),482mesh, equations, dg, cache)483484for i in each_quad_node(mesh, dg, cache)485du[i, element] = du[i, element] + alpha * du_local[i] * inv_wq[i]486end487488return nothing489end490491# Specialize since `u_values` isn't computed for DGMultiFluxDiffSBP solvers.492function calc_sources!(du, u, t, source_terms,493mesh, equations, dg::DGMultiFluxDiffSBP, cache)494md = mesh.md495496@threaded for e in eachelement(mesh, dg, cache)497for i in each_quad_node(mesh, dg, cache)498du[i, e] += source_terms(u[i, e], SVector(getindex.(md.xyzq, i, e)), t,499equations)500end501end502end503504# Specializes on Polynomial (e.g., modal) DG methods with a flux differencing volume integral, e.g.,505# an entropy conservative/stable discretization. For modal DG schemes, an extra `entropy_projection!`506# is required (see https://doi.org/10.1016/j.jcp.2018.02.033, Section 4.3).507# Also called by DGMultiFluxDiff{<:GaussSBP} solvers.508function rhs!(du, u, t, mesh, equations, boundary_conditions::BC,509source_terms::Source, dg::DGMultiFluxDiff, cache) where {Source, BC}510@trixi_timeit timer() "reset ∂u/∂t" set_zero!(du, dg, cache)511512# this function evaluates the solution at volume and face quadrature points (which was previously513# done in `prolong2interfaces` and `calc_volume_integral`)514@trixi_timeit timer() "entropy_projection!" begin515entropy_projection!(cache, u, mesh, equations, dg)516end517518@trixi_timeit timer() "volume integral" begin519calc_volume_integral!(du, u, mesh, have_nonconservative_terms(equations),520equations,521dg.volume_integral, dg, cache)522end523524# the following functions are the same as in VolumeIntegralWeakForm, and can be reused from dg.jl525@trixi_timeit timer() "interface flux" begin526calc_interface_flux!(cache, dg.surface_integral, mesh,527have_nonconservative_terms(equations), equations, dg)528end529530@trixi_timeit timer() "boundary flux" begin531calc_boundary_flux!(cache, t, boundary_conditions, mesh,532have_nonconservative_terms(equations), equations, dg)533end534535@trixi_timeit timer() "surface integral" begin536calc_surface_integral!(du, u, mesh, equations,537dg.surface_integral, dg, cache)538end539540@trixi_timeit timer() "Jacobian" invert_jacobian!(du, mesh, equations, dg, cache)541542@trixi_timeit timer() "source terms" begin543calc_sources!(du, u, t, source_terms, mesh, equations, dg, cache)544end545546return nothing547end548549# Specializes on SBP (e.g., nodal/collocation) DG methods with a flux differencing volume550# integral, e.g., an entropy conservative/stable discretization. The implementation of `rhs!`551# for such schemes is very similar to the implementation of `rhs!` for standard DG methods,552# but specializes `calc_volume_integral`.553function rhs!(du, u, t, mesh, equations,554boundary_conditions::BC, source_terms::Source,555dg::DGMultiFluxDiffSBP, cache) where {BC, Source}556@trixi_timeit timer() "reset ∂u/∂t" set_zero!(du, dg, cache)557558@trixi_timeit timer() "volume integral" calc_volume_integral!(du, u, mesh,559have_nonconservative_terms(equations),560equations,561dg.volume_integral,562dg, cache)563564@trixi_timeit timer() "prolong2interfaces" prolong2interfaces!(cache, u, mesh,565equations, dg)566567@trixi_timeit timer() "interface flux" calc_interface_flux!(cache,568dg.surface_integral,569mesh,570have_nonconservative_terms(equations),571equations, dg)572573@trixi_timeit timer() "boundary flux" calc_boundary_flux!(cache, t,574boundary_conditions, mesh,575have_nonconservative_terms(equations),576equations, dg)577578@trixi_timeit timer() "surface integral" calc_surface_integral!(du, u, mesh,579equations,580dg.surface_integral,581dg, cache)582583@trixi_timeit timer() "Jacobian" invert_jacobian!(du, mesh, equations, dg, cache)584585@trixi_timeit timer() "source terms" calc_sources!(du, u, t, source_terms, mesh,586equations, dg, cache)587588return nothing589end590end # @muladd591592593