Path: blob/main/src/semidiscretization/semidiscretization_hyperbolic_parabolic.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"""8SemidiscretizationHyperbolicParabolic910A struct containing everything needed to describe a spatial semidiscretization11of a mixed hyperbolic-parabolic conservation law.12"""13struct SemidiscretizationHyperbolicParabolic{Mesh, Equations, EquationsParabolic,14InitialCondition,15BoundaryConditions,16BoundaryConditionsParabolic,17SourceTerms, SourceTermsParabolic,18Solver, SolverParabolic,19Cache, CacheParabolic} <:20AbstractSemidiscretization21mesh::Mesh2223equations::Equations24equations_parabolic::EquationsParabolic2526# This guy is a bit messy since we abuse it as some kind of "exact solution"27# although this doesn't really exist...28initial_condition::InitialCondition2930boundary_conditions::BoundaryConditions31boundary_conditions_parabolic::BoundaryConditionsParabolic3233source_terms::SourceTerms34source_terms_parabolic::SourceTermsParabolic3536solver::Solver37solver_parabolic::SolverParabolic3839cache::Cache40cache_parabolic::CacheParabolic4142performance_counter::PerformanceCounterList{2}43end44# We assume some properties of the fields of the semidiscretization, e.g.,45# the `equations` and the `mesh` should have the same dimension. We check these46# properties in the outer constructor defined below. While we could ensure47# them even better in an inner constructor, we do not use this approach to48# simplify the integration with Adapt.jl for GPU usage, see49# https://github.com/trixi-framework/Trixi.jl/pull/2677#issuecomment-35917899215051"""52SemidiscretizationHyperbolicParabolic(mesh, both_equations, initial_condition, solver;53solver_parabolic=default_parabolic_solver(),54source_terms=nothing,55source_terms_parabolic=nothing,56bboundary_conditions,57RealT=real(solver),58uEltype=RealT)5960Construct a semidiscretization of a hyperbolic-parabolic PDE.6162Boundary conditions must be provided explicitly as a tuple of two boundary conditions, where the first entry corresponds to the63hyperbolic part and the second to the parabolic part. The boundary conditions for the hyperbolic and parabolic part can be64either passed as `NamedTuple` or as a single boundary condition that is applied to all boundaries.65"""66function SemidiscretizationHyperbolicParabolic(mesh, equations::Tuple,67initial_condition, solver;68solver_parabolic = default_parabolic_solver(),69source_terms = nothing,70source_terms_parabolic = nothing,71boundary_conditions,72# `RealT` is used as real type for node locations etc.73# while `uEltype` is used as element type of solutions etc.74RealT = real(solver), uEltype = RealT)75equations, equations_parabolic = equations7677@assert ndims(mesh) == ndims(equations)78@assert ndims(mesh) == ndims(equations_parabolic)7980if !(nvariables(equations) == nvariables(equations_parabolic))81throw(ArgumentError("Current implementation of parabolic terms requires the same number of conservative and gradient variables."))82end8384boundary_conditions, boundary_conditions_parabolic = boundary_conditions8586cache = create_cache(mesh, equations, solver, RealT, uEltype)87_boundary_conditions = digest_boundary_conditions(boundary_conditions,88mesh, solver, cache)89check_periodicity_mesh_boundary_conditions(mesh, _boundary_conditions)9091cache_parabolic = create_cache_parabolic(mesh, equations, solver,92nelements(solver, cache), uEltype)9394_boundary_conditions_parabolic = digest_boundary_conditions(boundary_conditions_parabolic,95mesh, solver, cache)96check_periodicity_mesh_boundary_conditions(mesh, _boundary_conditions_parabolic)9798performance_counter = PerformanceCounterList{2}(false)99100return SemidiscretizationHyperbolicParabolic{typeof(mesh),101typeof(equations),102typeof(equations_parabolic),103typeof(initial_condition),104typeof(_boundary_conditions),105typeof(_boundary_conditions_parabolic),106typeof(source_terms),107typeof(source_terms_parabolic),108typeof(solver),109typeof(solver_parabolic),110typeof(cache),111typeof(cache_parabolic)}(mesh,112equations,113equations_parabolic,114initial_condition,115_boundary_conditions,116_boundary_conditions_parabolic,117source_terms,118source_terms_parabolic,119solver,120solver_parabolic,121cache,122cache_parabolic,123performance_counter)124end125126# Create a new semidiscretization but change some parameters compared to the input.127# `Base.similar` follows a related concept but would require us to `copy` the `mesh`,128# which would impact the performance. Instead, `SciMLBase.remake` has exactly the129# semantics we want to use here. In particular, it allows us to re-use mutable parts,130# e.g. `remake(semi).mesh === semi.mesh`.131function remake(semi::SemidiscretizationHyperbolicParabolic;132uEltype = real(semi.solver),133mesh = semi.mesh,134equations = semi.equations,135equations_parabolic = semi.equations_parabolic,136initial_condition = semi.initial_condition,137solver = semi.solver,138solver_parabolic = semi.solver_parabolic,139source_terms = semi.source_terms,140source_terms_parabolic = semi.source_terms_parabolic,141boundary_conditions = semi.boundary_conditions,142boundary_conditions_parabolic = semi.boundary_conditions_parabolic)143# TODO: Which parts do we want to `remake`? At least the solver needs some144# special care if shock-capturing volume integrals are used (because of145# the indicators and their own caches...).146return SemidiscretizationHyperbolicParabolic(mesh, (equations, equations_parabolic),147initial_condition, solver;148solver_parabolic, source_terms,149source_terms_parabolic,150boundary_conditions = (boundary_conditions,151boundary_conditions_parabolic),152uEltype)153end154155function Base.show(io::IO, semi::SemidiscretizationHyperbolicParabolic)156@nospecialize semi # reduce precompilation time157158print(io, "SemidiscretizationHyperbolicParabolic(")159print(io, semi.mesh)160print(io, ", ", semi.equations)161print(io, ", ", semi.equations_parabolic)162print(io, ", ", semi.initial_condition)163print(io, ", ", semi.boundary_conditions)164print(io, ", ", semi.boundary_conditions_parabolic)165print(io, ", ", semi.source_terms)166print(io, ", ", semi.source_terms_parabolic)167print(io, ", ", semi.solver)168print(io, ", ", semi.solver_parabolic)169print(io, ", cache(")170for (idx, key) in enumerate(keys(semi.cache))171idx > 1 && print(io, " ")172print(io, key)173end174print(io, "))")175return nothing176end177178function Base.show(io::IO, ::MIME"text/plain",179semi::SemidiscretizationHyperbolicParabolic)180@nospecialize semi # reduce precompilation time181182if get(io, :compact, false)183show(io, semi)184else185summary_header(io, "SemidiscretizationHyperbolicParabolic")186summary_line(io, "#spatial dimensions", ndims(semi.equations))187summary_line(io, "mesh", semi.mesh)188summary_line(io, "hyperbolic equations", semi.equations |> typeof |> nameof)189summary_line(io, "parabolic equations",190semi.equations_parabolic |> typeof |> nameof)191summary_line(io, "initial condition", semi.initial_condition)192193# print_boundary_conditions(io, semi)194195summary_line(io, "source terms", semi.source_terms)196summary_line(io, "source terms parabolic", semi.source_terms_parabolic)197summary_line(io, "solver", semi.solver |> typeof |> nameof)198summary_line(io, "parabolic solver", semi.solver_parabolic |> typeof |> nameof)199summary_line(io, "total #DOFs per field", ndofsglobal(semi))200summary_footer(io)201end202end203204@inline Base.ndims(semi::SemidiscretizationHyperbolicParabolic) = ndims(semi.mesh)205206@inline function nvariables(semi::SemidiscretizationHyperbolicParabolic)207return nvariables(semi.equations)208end209210@inline Base.real(semi::SemidiscretizationHyperbolicParabolic) = real(semi.solver)211212# retain dispatch on hyperbolic equations only213@inline function mesh_equations_solver_cache(semi::SemidiscretizationHyperbolicParabolic)214@unpack mesh, equations, solver, cache = semi215return mesh, equations, solver, cache216end217218function calc_error_norms(func, u_ode, t, analyzer,219semi::SemidiscretizationHyperbolicParabolic, cache_analysis)220@unpack mesh, equations, initial_condition, solver, cache = semi221u = wrap_array(u_ode, mesh, equations, solver, cache)222223return calc_error_norms(func, u, t, analyzer, mesh, equations, initial_condition,224solver,225cache, cache_analysis)226end227228function compute_coefficients(t, semi::SemidiscretizationHyperbolicParabolic)229# Call `compute_coefficients` in `src/semidiscretization/semidiscretization.jl`230return compute_coefficients(semi.initial_condition, t, semi)231end232233function compute_coefficients!(u_ode, t, semi::SemidiscretizationHyperbolicParabolic)234return compute_coefficients!(u_ode, semi.initial_condition, t, semi)235end236237# Required for storing `extra_node_variables` in the `SaveSolutionCallback`.238# Not to be confused with `get_node_vars` which returns the variables of the simulated equation.239function get_node_variables!(node_variables, u_ode,240semi::SemidiscretizationHyperbolicParabolic)241return get_node_variables!(node_variables, u_ode,242mesh_equations_solver_cache(semi)...,243semi.equations_parabolic, semi.cache_parabolic)244end245246"""247semidiscretize(semi::SemidiscretizationHyperbolicParabolic, tspan;248jac_prototype_parabolic::Union{AbstractMatrix, Nothing} = nothing,249colorvec_parabolic::Union{AbstractVector, Nothing} = nothing)250251Wrap the semidiscretization `semi` as a split ODE problem in the time interval `tspan`252that can be passed to `solve` from the [SciML ecosystem](https://diffeq.sciml.ai/latest/).253The parabolic right-hand side is the first function of the split ODE problem and254will be used by default by the implicit part of IMEX methods from the255SciML ecosystem.256257Optional keyword arguments:258- `jac_prototype_parabolic`: Expected to come from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl).259Specifies the sparsity structure of the parabolic function's Jacobian to enable e.g. efficient implicit time stepping.260The [`SplitODEProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/split_ode_types/#SciMLBase.SplitODEProblem) only expects the Jacobian261to be defined on the first function it takes in, which is treated implicitly. This corresponds to the parabolic right-hand side in Trixi.jl.262The hyperbolic right-hand side is expected to be treated explicitly, and therefore its Jacobian is irrelevant.263- `colorvec_parabolic`: Expected to come from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl).264Allows for even faster Jacobian computation. Not necessarily required when `jac_prototype_parabolic` is given.265"""266function semidiscretize(semi::SemidiscretizationHyperbolicParabolic, tspan;267jac_prototype_parabolic::Union{AbstractMatrix, Nothing} = nothing,268colorvec_parabolic::Union{AbstractVector, Nothing} = nothing,269reset_threads = true)270# Optionally reset Polyester.jl threads. See271# https://github.com/trixi-framework/Trixi.jl/issues/1583272# https://github.com/JuliaSIMD/Polyester.jl/issues/30273if reset_threads274Polyester.reset_threads!()275end276277u0_ode = compute_coefficients(first(tspan), semi)278# TODO: MPI, do we want to synchronize loading and print debug statements, e.g. using279# mpi_isparallel() && MPI.Barrier(mpi_comm())280# See https://github.com/trixi-framework/Trixi.jl/issues/328281iip = true # is-inplace, i.e., we modify a vector when calling rhs_parabolic!, rhs!282283# Check if Jacobian prototype is provided for sparse Jacobian284if jac_prototype_parabolic !== nothing285# Convert `jac_prototype_parabolic` to real type, as seen here:286# https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Declaring-a-Sparse-Jacobian-with-Automatic-Sparsity-Detection287parabolic_ode = SciMLBase.ODEFunction(rhs_parabolic!,288jac_prototype = convert.(eltype(u0_ode),289jac_prototype_parabolic),290colorvec = colorvec_parabolic) # coloring vector is optional291292# Note that the IMEX time integration methods of OrdinaryDiffEq.jl treat the293# first function implicitly and the second one explicitly. Thus, we pass the294# (potentially) stiffer parabolic function first.295return SplitODEProblem{iip}(parabolic_ode, rhs!, u0_ode, tspan, semi)296else297# We could also construct an `ODEFunction` explicitly without the Jacobian here,298# but we stick to the lean direct in-place functions `rhs_parabolic!` and299# let OrdinaryDiffEq.jl handle the rest300return SplitODEProblem{iip}(rhs_parabolic!, rhs!, u0_ode, tspan, semi)301end302end303304"""305semidiscretize(semi::SemidiscretizationHyperbolicParabolic, tspan,306restart_file::AbstractString;307jac_prototype_parabolic::Union{AbstractMatrix, Nothing} = nothing,308colorvec_parabolic::Union{AbstractVector, Nothing} = nothing)309310Wrap the semidiscretization `semi` as a split ODE problem in the time interval `tspan`311that can be passed to `solve` from the [SciML ecosystem](https://diffeq.sciml.ai/latest/).312The parabolic right-hand side is the first function of the split ODE problem and313will be used by default by the implicit part of IMEX methods from the314SciML ecosystem.315316The initial condition etc. is taken from the `restart_file`.317318Optional keyword arguments:319- `jac_prototype_parabolic`: Expected to come from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl).320Specifies the sparsity structure of the parabolic function's Jacobian to enable e.g. efficient implicit time stepping.321The [`SplitODEProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/split_ode_types/#SciMLBase.SplitODEProblem) only expects the Jacobian322to be defined on the first function it takes in, which is treated implicitly. This corresponds to the parabolic right-hand side in Trixi.jl.323The hyperbolic right-hand side is expected to be treated explicitly, and therefore its Jacobian is irrelevant.324- `colorvec_parabolic`: Expected to come from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl).325Allows for even faster Jacobian computation. Not necessarily required when `jac_prototype_parabolic` is given.326"""327function semidiscretize(semi::SemidiscretizationHyperbolicParabolic, tspan,328restart_file::AbstractString;329jac_prototype_parabolic::Union{AbstractMatrix, Nothing} = nothing,330colorvec_parabolic::Union{AbstractVector, Nothing} = nothing,331reset_threads = true)332# Optionally reset Polyester.jl threads. See333# https://github.com/trixi-framework/Trixi.jl/issues/1583334# https://github.com/JuliaSIMD/Polyester.jl/issues/30335if reset_threads336Polyester.reset_threads!()337end338339u0_ode = load_restart_file(semi, restart_file)340# TODO: MPI, do we want to synchronize loading and print debug statements, e.g. using341# mpi_isparallel() && MPI.Barrier(mpi_comm())342# See https://github.com/trixi-framework/Trixi.jl/issues/328343iip = true # is-inplace, i.e., we modify a vector when calling rhs_parabolic!, rhs!344345# Check if Jacobian prototype is provided for sparse Jacobian346if jac_prototype_parabolic !== nothing347# Convert `jac_prototype_parabolic` to real type, as seen here:348# https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Declaring-a-Sparse-Jacobian-with-Automatic-Sparsity-Detection349parabolic_ode = SciMLBase.ODEFunction(rhs_parabolic!,350jac_prototype = convert.(eltype(u0_ode),351jac_prototype_parabolic),352colorvec = colorvec_parabolic) # coloring vector is optional353354# Note that the IMEX time integration methods of OrdinaryDiffEq.jl treat the355# first function implicitly and the second one explicitly. Thus, we pass the356# (potentially) stiffer parabolic function first.357return SplitODEProblem{iip}(parabolic_ode, rhs!, u0_ode, tspan, semi)358else359# We could also construct an `ODEFunction` explicitly without the Jacobian here,360# but we stick to the lean direct in-place function `rhs_parabolic!` and361# let OrdinaryDiffEq.jl handle the rest362return SplitODEProblem{iip}(rhs_parabolic!, rhs!, u0_ode, tspan, semi)363end364end365366function rhs!(du_ode, u_ode, semi::SemidiscretizationHyperbolicParabolic, t)367@unpack mesh, equations, boundary_conditions, source_terms, solver, cache = semi368369u = wrap_array(u_ode, mesh, equations, solver, cache)370du = wrap_array(du_ode, mesh, equations, solver, cache)371backend = trixi_backend(u)372373# TODO: Taal decide, do we need to pass the mesh?374time_start = time_ns()375@trixi_timeit_ext backend timer() "rhs!" rhs!(du, u, t, mesh, equations,376boundary_conditions, source_terms,377solver, cache)378runtime = time_ns() - time_start379put!(semi.performance_counter.counters[1], runtime)380381return nothing382end383384function rhs_parabolic!(du_ode, u_ode, semi::SemidiscretizationHyperbolicParabolic, t)385@unpack mesh, equations_parabolic, boundary_conditions_parabolic, source_terms_parabolic, solver, solver_parabolic, cache, cache_parabolic = semi386387u = wrap_array(u_ode, mesh, equations_parabolic, solver, cache)388du = wrap_array(du_ode, mesh, equations_parabolic, solver, cache)389backend = trixi_backend(u)390391# TODO: Taal decide, do we need to pass the mesh?392time_start = time_ns()393@trixi_timeit_ext backend timer() "parabolic rhs!" rhs_parabolic!(du, u, t, mesh,394equations_parabolic,395boundary_conditions_parabolic,396source_terms_parabolic,397solver,398solver_parabolic,399cache,400cache_parabolic)401runtime = time_ns() - time_start402put!(semi.performance_counter.counters[2], runtime)403404return nothing405end406407"""408linear_structure(semi::SemidiscretizationHyperbolicParabolic;409t0 = zero(real(semi)))410411Wraps the right-hand side operator of the hyperbolic-parabolic semidiscretization `semi`412at time `t0` as an affine-linear operator given by a linear operator `A`413and a vector `b`:414```math415\\partial_t u(t) = A u(t) - b.416```417Works only for linear equations, i.e.,418equations which `have_constant_speed(equations) == True()` and `have_constant_diffusivity(equations_parabolic) == True()`419420This has the benefit of greatly reduced memory consumption compared to constructing421the full system matrix explicitly, as done for instance in422[`jacobian_fd`](@ref) and [`jacobian_ad_forward`](@ref).423424The returned linear operator `A` is a matrix-free representation which can be425supplied to iterative solvers from, e.g., [Krylov.jl](https://github.com/JuliaSmoothOptimizers/Krylov.jl).426"""427function linear_structure(semi::SemidiscretizationHyperbolicParabolic;428t0 = zero(real(semi)))429if (have_constant_speed(semi.equations) == False() ||430have_constant_diffusivity(semi.equations_parabolic) == False())431throw(ArgumentError("`linear_structure` expects linear equations."))432end433434# additional storage for parabolic part435dest_para = allocate_coefficients(mesh_equations_solver_cache(semi)...)436437apply_rhs! = function (dest, src)438rhs!(dest, src, semi, t0)439rhs_parabolic!(dest_para, src, semi, t0)440@. dest += dest_para441return dest442end443444return _linear_structure_from_rhs(semi, apply_rhs!)445end446447function _jacobian_ad_forward(semi::SemidiscretizationHyperbolicParabolic, t0, u0_ode,448du_ode, config)449new_semi = remake(semi, uEltype = eltype(config))450451du_ode_hyp = Vector{eltype(config)}(undef, length(du_ode))452J = ForwardDiff.jacobian(du_ode, u0_ode, config) do du_ode, u_ode453# Implementation of split ODE problem in OrdinaryDiffEq454rhs!(du_ode_hyp, u_ode, new_semi, t0)455rhs_parabolic!(du_ode, u_ode, new_semi, t0)456return du_ode .+= du_ode_hyp457end458459return J460end461462"""463linear_structure_parabolic(semi::SemidiscretizationHyperbolicParabolic;464t0 = zero(real(semi)))465466Wraps the **parabolic part** right-hand side operator of the hyperbolic-parabolic semidiscretization `semi`467at time `t0` as an affine-linear operator given by a linear operator `A`468and a vector `b`:469```math470\\partial_t u(t) = A u(t) - b.471```472Works only for linear parabolic equations, i.e.,473equations which `have_constant_diffusivity(equations_parabolic) == True()`.474475This has the benefit of greatly reduced memory consumption compared to constructing476the full system matrix explicitly, as done for instance in477[`jacobian_ad_forward_parabolic`](@ref).478479The returned linear operator `A` is a matrix-free representation which can be480supplied to iterative solvers from, e.g., [Krylov.jl](https://github.com/JuliaSmoothOptimizers/Krylov.jl).481482It is also possible to use this to construct a sparse matrix without the detour of constructing483first the full Jacobian by calling484```julia485using SparseArrays486A_map, b = linear_structure_parabolic(semi, t0 = t0)487A_sparse = sparse(A_map)488```489which can then be further used to construct for instance a490[`MatrixOperator`](https://docs.sciml.ai/SciMLOperators/stable/tutorials/getting_started/#Simplest-Operator:-MatrixOperator)491from [SciMLOperators.jl](https://docs.sciml.ai/SciMLOperators/stable/).492This is especially useful for IMEX schemes where the parabolic part is implicitly,493and for a `MatrixOperator` only factorized once.494"""495function linear_structure_parabolic(semi::SemidiscretizationHyperbolicParabolic;496t0 = zero(real(semi)))497if have_constant_diffusivity(semi.equations_parabolic) == False()498throw(ArgumentError("`linear_structure_parabolic` expects equations with constant diffusive terms."))499end500501apply_rhs! = function (dest, src)502return rhs_parabolic!(dest, src, semi, t0)503end504505return _linear_structure_from_rhs(semi, apply_rhs!)506end507508"""509jacobian_ad_forward_parabolic(semi::SemidiscretizationHyperbolicParabolic;510t0 = zero(real(semi)),511u0_ode = compute_coefficients(t0, semi))512513Uses the *parabolic part* of the right-hand side operator of the [`SemidiscretizationHyperbolicParabolic`](@ref) `semi`514and forward mode automatic differentiation to compute the Jacobian `J` of the515parabolic/diffusive contribution only at time `t0` and state `u0_ode`.516517This might be useful for operator-splitting methods, e.g., the construction of optimized518time integrators which optimize different methods for the hyperbolic and parabolic part separately.519"""520function jacobian_ad_forward_parabolic(semi::SemidiscretizationHyperbolicParabolic;521t0 = zero(real(semi)),522u0_ode = compute_coefficients(t0, semi))523return jacobian_ad_forward_parabolic(semi, t0, u0_ode)524end525526# The following version is for plain arrays527function jacobian_ad_forward_parabolic(semi::SemidiscretizationHyperbolicParabolic,528t0, u0_ode)529du_ode = similar(u0_ode)530config = ForwardDiff.JacobianConfig(nothing, du_ode, u0_ode)531532# Use a function barrier since the generation of the `config` we use above533# is not type-stable534return _jacobian_ad_forward_parabolic(semi, t0, u0_ode, du_ode, config)535end536537function _jacobian_ad_forward_parabolic(semi, t0, u0_ode, du_ode, config)538new_semi = remake(semi, uEltype = eltype(config))539# Create anonymous function passed as first argument to `ForwardDiff.jacobian` to match540# `ForwardDiff.jacobian(f!, y::AbstractArray, x::AbstractArray,541# cfg::JacobianConfig = JacobianConfig(f!, y, x), check=Val{true}())`542J = ForwardDiff.jacobian(du_ode, u0_ode, config) do du_ode, u_ode543return Trixi.rhs_parabolic!(du_ode, u_ode, new_semi, t0)544end545546return J547end548end # @muladd549550551