Path: blob/main/src/semidiscretization/semidiscretization.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"""8ndofs(semi::AbstractSemidiscretization)910Return the number of degrees of freedom associated with each scalar variable.11"""12@inline function ndofs(semi::AbstractSemidiscretization)13mesh, _, solver, cache = mesh_equations_solver_cache(semi)14ndofs(mesh, solver, cache)15end1617"""18ndofsglobal(semi::AbstractSemidiscretization)1920Return the global number of degrees of freedom associated with each scalar variable across all MPI ranks.21This is the same as [`ndofs`](@ref) for simulations running in serial or22parallelized via threads. It will in general be different for simulations23running in parallel with MPI.24"""25@inline function ndofsglobal(semi::AbstractSemidiscretization)26mesh, _, solver, cache = mesh_equations_solver_cache(semi)27ndofsglobal(mesh, solver, cache)28end2930"""31integrate_via_indices(func, u_ode, semi::AbstractSemidiscretization, args...; normalize=true)3233Call `func(u, i..., element, equations, solver, args...)` for all nodal indices `i..., element`34and integrate the result using a quadrature associated with the semidiscretization `semi`.3536If `normalize` is true, the result is divided by the total volume of the computational domain.37"""38function integrate_via_indices(func::Func, u_ode, semi::AbstractSemidiscretization,39args...; normalize = true) where {Func}40mesh, equations, solver, cache = mesh_equations_solver_cache(semi)4142u = wrap_array(u_ode, mesh, equations, solver, cache)43integrate_via_indices(func, u, mesh, equations, solver, cache, args...,44normalize = normalize)45end4647"""48integrate([func=(u_node,equations)->u_node,] u_ode, semi::AbstractSemidiscretization; normalize=true)4950Call `func(u_node, equations)` for each vector of nodal variables `u_node` in `u_ode`51and integrate the result using a quadrature associated with the semidiscretization `semi`.5253If `normalize` is true, the result is divided by the total volume of the computational domain.54"""55function integrate(func::Func, u_ode, semi::AbstractSemidiscretization;56normalize = true) where {Func}57mesh, equations, solver, cache = mesh_equations_solver_cache(semi)5859u = wrap_array(u_ode, mesh, equations, solver, cache)60integrate(func, u, mesh, equations, solver, cache, normalize = normalize)61end6263function integrate(u_ode, semi::AbstractSemidiscretization; normalize = true)64integrate(cons2cons, u_ode, semi; normalize = normalize)65end6667"""68calc_error_norms([func=(u_node,equations)->u_node,] u_ode, t, analyzer, semi::AbstractSemidiscretization, cache_analysis)6970Calculate discrete L2 and L∞ error norms of `func` applied to each nodal variable `u_node` in `u_ode`.71If no exact solution is available, "errors" are calculated using some reference state and can be useful72for regression tests.73"""74function calc_error_norms(u_ode, t, analyzer, semi::AbstractSemidiscretization,75cache_analysis)76calc_error_norms(cons2cons, u_ode, t, analyzer, semi, cache_analysis)77end7879"""80semidiscretize(semi::AbstractSemidiscretization, tspan;81jac_prototype::Union{AbstractMatrix, Nothing} = nothing,82colorvec::Union{AbstractVector, Nothing} = nothing,83storage_type = nothing,84real_type = nothing)8586Wrap the semidiscretization `semi` as an ODE problem in the time interval `tspan`87that can be passed to `solve` from the [SciML ecosystem](https://diffeq.sciml.ai/latest/).8889Optional keyword arguments:90- `jac_prototype`: Expected to come from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl).91Specifies the sparsity structure of the Jacobian to enable e.g. efficient implicit time stepping.92- `colorvec`: Expected to come from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl).93Allows for even faster Jacobian computation if a sparse `jac_prototype` is given (optional).94- `storage_type` and `real_type`: Configure the underlying computational datastructures.95`storage_type` changes the fundamental array type being used, allowing the experimental use of `CuArray`96or other GPU array types. `real_type` changes the computational data type being used.97"""98function semidiscretize(semi::AbstractSemidiscretization, tspan;99jac_prototype::Union{AbstractMatrix, Nothing} = nothing,100colorvec::Union{AbstractVector, Nothing} = nothing,101reset_threads = true,102storage_type = nothing,103real_type = nothing)104# Optionally reset Polyester.jl threads. See105# https://github.com/trixi-framework/Trixi.jl/issues/1583106# https://github.com/JuliaSIMD/Polyester.jl/issues/30107if reset_threads108Polyester.reset_threads!()109end110111if !(storage_type === nothing && real_type === nothing)112if storage_type === nothing113storage_type = Array114end115if real_type === nothing116real_type = real(semi)117end118semi = trixi_adapt(storage_type, real_type, semi)119if eltype(tspan) !== real_type120tspan = convert.(real_type, tspan)121end122end123124u0_ode = compute_coefficients(first(tspan), semi) # Invoke initial condition125126# TODO: MPI, do we want to synchronize loading and print debug statements, e.g. using127# mpi_isparallel() && MPI.Barrier(mpi_comm())128# See https://github.com/trixi-framework/Trixi.jl/issues/328129iip = true # is-inplace, i.e., we modify a vector when calling rhs!130specialize = SciMLBase.FullSpecialize # specialize on rhs! and parameters (semi)131132# Check if Jacobian prototype is provided for sparse Jacobian133if jac_prototype !== nothing134# Convert `jac_prototype` to real type, as seen here:135# https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Declaring-a-Sparse-Jacobian-with-Automatic-Sparsity-Detection136ode = SciMLBase.ODEFunction(rhs!,137jac_prototype = convert.(eltype(u0_ode),138jac_prototype),139colorvec = colorvec) # coloring vector is optional140141return ODEProblem{iip, specialize}(ode, u0_ode, tspan, semi)142else143# We could also construct an `ODEFunction` without the Jacobian here,144# but we stick to the more light-weight direct in-place function `rhs!`.145return ODEProblem{iip, specialize}(rhs!, u0_ode, tspan, semi)146end147end148149"""150semidiscretize(semi::AbstractSemidiscretization, tspan,151restart_file::AbstractString;152jac_prototype::Union{AbstractMatrix, Nothing} = nothing,153colorvec::Union{AbstractVector, Nothing} = nothing)154155Wrap the semidiscretization `semi` as an ODE problem in the time interval `tspan`156that can be passed to `solve` from the [SciML ecosystem](https://diffeq.sciml.ai/latest/).157The initial condition etc. is taken from the `restart_file`.158159Optional keyword arguments:160- `jac_prototype`: Expected to come from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl).161Specifies the sparsity structure of the Jacobian to enable e.g. efficient implicit time stepping.162- `colorvec`: Expected to come from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl).163Allows for even faster Jacobian computation. Not necessarily required when `jac_prototype` is given.164"""165function semidiscretize(semi::AbstractSemidiscretization, tspan,166restart_file::AbstractString;167jac_prototype::Union{AbstractMatrix, Nothing} = nothing,168colorvec::Union{AbstractVector, Nothing} = nothing,169reset_threads = true)170# Optionally reset Polyester.jl threads. See171# https://github.com/trixi-framework/Trixi.jl/issues/1583172# https://github.com/JuliaSIMD/Polyester.jl/issues/30173if reset_threads174Polyester.reset_threads!()175end176177u0_ode = load_restart_file(semi, restart_file) # Load initial condition from restart file178179# TODO: MPI, do we want to synchronize loading and print debug statements, e.g. using180# mpi_isparallel() && MPI.Barrier(mpi_comm())181# See https://github.com/trixi-framework/Trixi.jl/issues/328182iip = true # is-inplace, i.e., we modify a vector when calling rhs!183specialize = SciMLBase.FullSpecialize # specialize on rhs! and parameters (semi)184185# Check if Jacobian prototype is provided for sparse Jacobian186if jac_prototype !== nothing187# Convert `jac_prototype` to real type, as seen here:188# https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Declaring-a-Sparse-Jacobian-with-Automatic-Sparsity-Detection189ode = SciMLBase.ODEFunction(rhs!,190jac_prototype = convert.(eltype(u0_ode),191jac_prototype),192colorvec = colorvec) # coloring vector is optional193194return ODEProblem{iip, specialize}(ode, u0_ode, tspan, semi)195else196# We could also construct an `ODEFunction` without the Jacobian here,197# but we stick to the more light-weight direct in-place function `rhs!`.198return ODEProblem{iip, specialize}(rhs!, u0_ode, tspan, semi)199end200end201202"""203compute_coefficients(func, t, semi::AbstractSemidiscretization)204205Compute the discrete coefficients of the continuous function `func` at time `t`206associated with the semidiscretization `semi`.207For example, the discrete coefficients of `func` for a discontinuous Galerkin208spectral element method ([`DGSEM`](@ref)) are the values of `func` at the209Lobatto-Legendre nodes. Similarly, a classical finite difference method will use210the values of `func` at the nodes of the grid assoociated with the semidiscretization211`semi`.212213For semidiscretizations `semi` associated with an initial condition, `func` can be omitted214to use the given initial condition at time `t`.215"""216function compute_coefficients(func, t, semi::AbstractSemidiscretization)217u_ode = allocate_coefficients(mesh_equations_solver_cache(semi)...)218# Call `compute_coefficients` defined below219compute_coefficients!(u_ode, func, t, semi)220return u_ode221end222223"""224compute_coefficients!(u_ode, func, t, semi::AbstractSemidiscretization)225226Same as [`compute_coefficients`](@ref) but stores the result in `u_ode`.227"""228function compute_coefficients!(u_ode, func, t, semi::AbstractSemidiscretization)229backend = trixi_backend(u_ode)230u = wrap_array(u_ode, semi)231# Call `compute_coefficients` defined by the solver232compute_coefficients!(backend, u, func, t, mesh_equations_solver_cache(semi)...)233end234235"""236linear_structure(semi::AbstractSemidiscretization;237t0=zero(real(semi)))238239Wraps the right-hand side operator of the semidiscretization `semi`240at time `t0` as an affine-linear operator given by a linear operator `A`241and a vector `b`.242"""243function linear_structure(semi::AbstractSemidiscretization;244t0 = zero(real(semi)))245# allocate memory246u_ode = allocate_coefficients(mesh_equations_solver_cache(semi)...)247du_ode = similar(u_ode)248249# get the right hand side from possible source terms250u_ode .= zero(eltype(u_ode))251rhs!(du_ode, u_ode, semi, t0)252# Create a copy of `b` used internally to extract the linear part of `semi`.253# This is necessary to get everything correct when the users updates the254# returned vector `b`.255b = -du_ode256b_tmp = copy(b)257258# wrap the linear operator259A = LinearMap(length(u_ode), ismutating = true) do dest, src260rhs!(dest, src, semi, t0)261@. dest += b_tmp262dest263end264265return A, b266end267268"""269jacobian_fd(semi::AbstractSemidiscretization;270t0=zero(real(semi)),271u0_ode=compute_coefficients(t0, semi))272273Uses the right-hand side operator of the semidiscretization `semi`274and simple second order finite difference to compute the Jacobian `J`275of the semidiscretization `semi` at time `t0` and state `u0_ode`.276"""277function jacobian_fd(semi::AbstractSemidiscretization;278t0 = zero(real(semi)),279u0_ode = compute_coefficients(t0, semi))280# copy the initial state since it will be modified in the following281u_ode = copy(u0_ode)282du0_ode = similar(u_ode)283dup_ode = similar(u_ode)284dum_ode = similar(u_ode)285286# compute residual of linearization state287rhs!(du0_ode, u_ode, semi, t0)288289# initialize Jacobian matrix290J = zeros(eltype(u_ode), length(u_ode), length(u_ode))291292# use second order finite difference to estimate Jacobian matrix293for idx in eachindex(u0_ode)294# determine size of fluctuation295# This is the approach used by FiniteDiff.jl to compute the296# step size, which assures that the finite difference is accurate297# for very small and very large absolute values `u0_ode[idx]`.298# See https://github.com/trixi-framework/Trixi.jl/pull/2514#issuecomment-3190534904.299absstep = sqrt(eps(typeof(u0_ode[idx])))300relstep = absstep301epsilon = max(relstep * abs(u0_ode[idx]), absstep)302303# plus fluctuation304u_ode[idx] = u0_ode[idx] + epsilon305rhs!(dup_ode, u_ode, semi, t0)306307# minus fluctuation308u_ode[idx] = u0_ode[idx] - epsilon309rhs!(dum_ode, u_ode, semi, t0)310311# restore linearization state312u_ode[idx] = u0_ode[idx]313314# central second order finite difference315@. J[:, idx] = (dup_ode - dum_ode) / (2 * epsilon)316end317318return J319end320321"""322jacobian_ad_forward(semi::AbstractSemidiscretization;323t0=zero(real(semi)),324u0_ode=compute_coefficients(t0, semi))325326Uses the right-hand side operator of the semidiscretization `semi`327and forward mode automatic differentiation to compute the Jacobian `J`328of the semidiscretization `semi` at time `t0` and state `u0_ode`.329"""330function jacobian_ad_forward(semi::AbstractSemidiscretization;331t0 = zero(real(semi)),332u0_ode = compute_coefficients(t0, semi))333jacobian_ad_forward(semi, t0, u0_ode)334end335336# The following version is for plain arrays337function jacobian_ad_forward(semi::AbstractSemidiscretization, t0, u0_ode)338du_ode = similar(u0_ode)339config = ForwardDiff.JacobianConfig(nothing, du_ode, u0_ode)340341# Use a function barrier since the generation of the `config` we use above342# is not type-stable343_jacobian_ad_forward(semi, t0, u0_ode, du_ode, config)344end345346function _jacobian_ad_forward(semi, t0, u0_ode, du_ode, config)347new_semi = remake(semi, uEltype = eltype(config))348# Create anonymous function passed as first argument to `ForwardDiff.jacobian` to match349# `ForwardDiff.jacobian(f!, y::AbstractArray, x::AbstractArray,350# cfg::JacobianConfig = JacobianConfig(f!, y, x), check=Val{true}())`351J = ForwardDiff.jacobian(du_ode, u0_ode, config) do du_ode, u_ode352Trixi.rhs!(du_ode, u_ode, new_semi, t0)353end354355return J356end357358# unpack u if it is wrapped in VectorOfArray (mainly for DGMulti solvers)359jacobian_ad_forward(semi::AbstractSemidiscretization, t0, u0_ode::VectorOfArray) = jacobian_ad_forward(semi,360t0,361parent(u0_ode))362363# This version is specialized to `StructArray`s used by some `DGMulti` solvers.364# We need to convert the numerical solution vectors since ForwardDiff cannot365# handle arrays of `SVector`s.366function jacobian_ad_forward(semi::AbstractSemidiscretization, t0, _u0_ode::StructArray)367u0_ode_plain = similar(_u0_ode, eltype(eltype(_u0_ode)),368(size(_u0_ode)..., nvariables(semi)))369for (v, u_v) in enumerate(StructArrays.components(_u0_ode))370u0_ode_plain[.., v] = u_v371end372du_ode_plain = similar(u0_ode_plain)373config = ForwardDiff.JacobianConfig(nothing, du_ode_plain, u0_ode_plain)374375# Use a function barrier since the generation of the `config` we use above376# is not type-stable377_jacobian_ad_forward_structarrays(semi, t0, u0_ode_plain, du_ode_plain, config)378end379380function _jacobian_ad_forward_structarrays(semi, t0, u0_ode_plain, du_ode_plain, config)381new_semi = remake(semi, uEltype = eltype(config))382# Create anonymous function passed as first argument to `ForwardDiff.jacobian` to match383# `ForwardDiff.jacobian(f!, y::AbstractArray, x::AbstractArray,384# cfg::JacobianConfig = JacobianConfig(f!, y, x), check=Val{true}())`385J = ForwardDiff.jacobian(du_ode_plain, u0_ode_plain,386config) do du_ode_plain, u_ode_plain387u_ode = StructArray{SVector{nvariables(semi), eltype(config)}}(ntuple(v -> view(u_ode_plain,388:,389:,390v),391nvariables(semi)))392du_ode = StructArray{SVector{nvariables(semi), eltype(config)}}(ntuple(v -> view(du_ode_plain,393:,394:,395v),396nvariables(semi)))397Trixi.rhs!(du_ode, u_ode, new_semi, t0)398end399400return J401end402403# This version is specialized to arrays of `StaticArray`s used by some `DGMulti` solvers.404# We need to convert the numerical solution vectors since ForwardDiff cannot405# handle arrays of `SVector`s.406function jacobian_ad_forward(semi::AbstractSemidiscretization, t0,407_u0_ode::AbstractArray{<:SVector})408u0_ode_plain = reinterpret(eltype(eltype(_u0_ode)), _u0_ode)409du_ode_plain = similar(u0_ode_plain)410config = ForwardDiff.JacobianConfig(nothing, du_ode_plain, u0_ode_plain)411412# Use a function barrier since the generation of the `config` we use above413# is not type-stable414_jacobian_ad_forward_staticarrays(semi, t0, u0_ode_plain, du_ode_plain, config)415end416417function _jacobian_ad_forward_staticarrays(semi, t0, u0_ode_plain, du_ode_plain, config)418new_semi = remake(semi, uEltype = eltype(config))419J = ForwardDiff.jacobian(du_ode_plain, u0_ode_plain,420config) do du_ode_plain, u_ode_plain421u_ode = reinterpret(SVector{nvariables(semi), eltype(config)}, u_ode_plain)422du_ode = reinterpret(SVector{nvariables(semi), eltype(config)}, du_ode_plain)423Trixi.rhs!(du_ode, u_ode, new_semi, t0)424end425426return J427end428429# Sometimes, it can be useful to save some (scalar) variables associated with each element,430# e.g. AMR indicators or shock indicators. Since these usually have to be re-computed431# directly before IO and do not necessarily need to be stored in memory before,432# get_element_variables!(element_variables, ..)433# is used to retrieve such up to date element variables, modifying434# `element_variables::Dict{Symbol,Any}` in place.435function get_element_variables!(element_variables, u_ode,436semi::AbstractSemidiscretization)437u = wrap_array(u_ode, semi)438get_element_variables!(element_variables, u, mesh_equations_solver_cache(semi)...)439end440441# Required for storing `extra_node_variables` in the `SaveSolutionCallback`.442# Not to be confused with `get_node_vars` which returns the variables of the simulated equation.443function get_node_variables!(node_variables, u_ode, semi::AbstractSemidiscretization)444get_node_variables!(node_variables, u_ode, mesh_equations_solver_cache(semi)...)445end446447# To implement AMR and use OrdinaryDiffEq.jl etc., we have to be a bit creative.448# Since the caches of the SciML ecosystem are immutable structs, we cannot simply449# change the underlying arrays therein. Hence, to support changing the number of450# DOFs, we need to use `resize!`. In some sense, this will force us to write more451# efficient code, since `resize!` will make use of previously allocated memory452# instead of allocating memory from scratch every time.453#454# However, multidimensional `Array`s don't support `resize!`. One option might be455# to use ElasticArrays.jl. But I don't really like that approach. Needing to use456# ElasticArray doesn't feel completely good to me, since we also want to experiment457# with other array types such as PaddedMatrices.jl, see trixi-framework/Trixi.jl#166.458# Then, we would need to wrap an Array inside something from PaddedMatrices.jl inside459# something from ElasticArrays.jl - or the other way round? Is that possible at all?460# If we go further, this looks like it could easily explode.461#462# Currently, the best option seems to be to let OrdinaryDiffEq.jl use `Vector`s,463# which can be `resize!`ed for AMR. Then, we have to wrap these `Vector`s inside464# Trixi.jl as our favorite multidimensional array type. We need to do this wrapping465# in every method exposed to OrdinaryDiffEq, i.e. in the first levels of things like466# rhs!, AMRCallback, StepsizeCallback, AnalysisCallback, SaveSolutionCallback467#468# This wrapping will also allow us to experiment more easily with additional469# kinds of wrapping, e.g. HybridArrays.jl or PaddedMatrices.jl to inform the470# compiler about the sizes of the first few dimensions in DG methods, i.e.471# nvariables(equations) and nnodes(dg).472#473# In some sense, having plain multidimensional `Array`s not support `resize!`474# isn't necessarily a bug (although it would be nice to add this possibility to475# base Julia) but can turn out to be a feature for us, because it will allow us476# more specializations.477# Since we can use multiple dispatch, these kinds of specializations can be478# tailored specifically to each combinations of mesh/solver etc.479#480# Under the hood, `wrap_array(u_ode, mesh, equations, solver, cache)` might481# (and probably will) use `unsafe_wrap`. Hence, you have to remember to482# `GC.@preserve` temporaries that are only used indirectly via `wrap_array`483# to avoid stochastic memory errors.484#485# Xref https://github.com/SciML/OrdinaryDiffEq.jl/pull/1275486function wrap_array(u_ode, semi::AbstractSemidiscretization)487wrap_array(u_ode, mesh_equations_solver_cache(semi)...)488end489490# Like `wrap_array`, but guarantees to return a plain `Array`, which can be better491# for writing solution files etc.492function wrap_array_native(u_ode, semi::AbstractSemidiscretization)493wrap_array_native(u_ode, mesh_equations_solver_cache(semi)...)494end495496# TODO: Taal, document interface?497# New mesh/solver combinations have to implement498# - ndofs(mesh, solver, cache)499# - ndofsgloabal(mesh, solver, cache)500# - ndims(mesh)501# - nnodes(solver)502# - real(solver)503# - create_cache(mesh, equations, solver, RealT)504# - wrap_array(u_ode, mesh, equations, solver, cache)505# - integrate(func, u, mesh, equations, solver, cache; normalize=true)506# - integrate_via_indices(func, u, mesh, equations, solver, cache, args...; normalize=true)507# - calc_error_norms(func, u, t, analyzer, mesh, equations, initial_condition, solver, cache, cache_analysis)508# - allocate_coefficients(mesh, equations, solver, cache)509# - compute_coefficients!(u, func, mesh, equations, solver, cache)510# - rhs!(du, u, t, mesh, equations, boundary_conditions, source_terms, solver, cache)511#512513end # @muladd514515516