Developer Extension API
PDEBase defines a small, versioned protocol for packages that discretize a ModelingToolkit.PDESystem. The protocol turns symbolic equations and boundary conditions into a symbolic ODE, DAE, or optimization system. It is an extension API for discretizer authors, not an application-level solver API.
The functions on this page are public so that discretizer packages can extend them. Ordinary applications should call a discretizer such as MethodOfLines.jl and should not build a solver directly on these hooks. Extension packages should preserve the call order and return contracts below.
Contract
The generic pipeline calls the hooks in this order:
VariableMap(pdesys, discretization)normalizes symbolic variables and domains.interface_errorsrejects unsupported systems before work is allocated.parse_bcscreates the boundary map, thencheck_boundarymapvalidates it.should_transformoptionally enablestransform_pde_system!.construct_disc_state,construct_discrete_space, andconstruct_var_equation_mappingcreate the discretizer state.construct_differential_discretizerprecomputes derivative data.- For each PDE,
get_eqvarselects its discrete variable anddiscretize_equation!updates the state in place. generate_ic_defaultscreates discrete initial values.generate_metadatastores data needed by the generated problem and solution.generate_systemconstructs the final symbolic system.
The default methods are intentionally conservative no-ops. A production discretizer must override the hooks that construct its space, map, derivative data, equations, metadata, and final system. A hook should return only the value specified in its docstring; in-place hooks should mutate their supplied state and return nothing.
Input Rules
The PDESystem supplied to the protocol is expected to satisfy these rules:
- Every independent variable has one finite domain interval.
- Dependent variables use a consistent argument order throughout equations and boundary conditions.
- Every spatial boundary required by the discretizer is represented in the boundary conditions.
- Time-dependent systems provide initial conditions on the time variable.
- Periodic conditions occur as matching lower/upper pairs.
- Interface conditions connect variables with compatible argument signatures; only the interface coordinate may differ.
VariableMap excludes time from indvars(v) but retains it in all_ivs(v) and in each dependent variable's argument signature. A discretizer should use the accessors rather than infer dimension order from a field layout.
Abstract Types
Discretizers should subtype one of the output categories and the supporting abstract types as appropriate:
struct MyDiscretization <: PDEBase.AbstractEquationSystemDiscretization
kwargs::NamedTuple
end
struct MySpace <: PDEBase.AbstractCartesianDiscreteSpace
grid::Vector{Float64}
end
struct MyMapping <: PDEBase.AbstractVarEqMapping
equation_variables::Dict
end
struct MyDerivativeData <: PDEBase.AbstractDifferentialDiscretizer
weights::Dict
end
struct MyState <: PDEBase.AbstractDiscretizationState
equations::Vector
endAbstractEquationSystemDiscretization is for ODE/DAE-like symbolic systems; AbstractOptimizationSystemDiscretization is for optimization systems. AbstractDiscreteSpace, AbstractVarEqMapping, AbstractDifferentialDiscretizer, and AbstractDiscretizationState describe the values exchanged by the protocol and are intentionally free of concrete mesh or solver assumptions.
Validation and Transformation
These hooks run before equation generation. Validation hooks should throw a domain-specific error for unsupported input and return nothing for valid input. should_transform must be a pure decision for the supplied system; transform_pde_system! is called only when it returns true and must return the system consumed by later hooks.
PDEBase.interface_errors — Function
interface_errors(sys::PDESystem, v::VariableMap, disc::AbstractDiscretization)Check whether disc supports the structure of sys represented by v.
Discretization packages should extend this function with checks for features that cannot be handled by their implementation. Return nothing when the system is supported; otherwise throw an informative exception before any boundary parsing or discretization work begins.
Arguments
sys::PDESystem: The PDE system being discretized.v::VariableMap: The variable map constructed fromsys.disc::AbstractDiscretization: The discretization being applied.
Returns
nothing: The system is supported.
Examples
function PDEBase.interface_errors(sys, v, disc::MyDiscretization)
isempty(v.x̄) && throw(ArgumentError("MyDiscretization requires a spatial variable"))
return nothing
endPDEBase.check_boundarymap — Function
check_boundarymap(bmap, v::VariableMap, disc::AbstractDiscretization)Validate the parsed boundary map before discretizing equations.
Extend this function when a discretizer imposes requirements beyond the PDESystem boundary syntax, such as requiring both sides of every spatial dimension. The map is the nested representation produced by PDEBase, with dependent-variable operations at the outer level and independent variables at the inner level.
Arguments
bmap: The parsed boundary map.v::VariableMap: The variable map for the PDE system.disc::AbstractDiscretization: The discretization being applied.
Returns
nothing: The boundary map is valid.
Examples
function PDEBase.check_boundarymap(bmap, v, disc::MyDiscretization)
isempty(PDEBase.flatten_vardict(bmap)) &&
throw(ArgumentError("MyDiscretization requires boundary conditions"))
return nothing
endPDEBase.should_transform — Function
should_transform(pdesys::PDESystem, disc::AbstractDiscretization, boundarymap)Report whether pdesys must be transformed before discretization.
This hook is called after boundary validation. A true result must be paired with a transform_pde_system! method for the same discretizer; a false result leaves the original system unchanged.
Arguments
pdesys::PDESystem: The validated PDE system.disc::AbstractDiscretization: The discretization being applied.boundarymap: The parsed boundary map.
Returns
Bool:truewhen the transformation hook should run, otherwisefalse.
PDEBase.transform_pde_system! — Function
transform_pde_system!(v::VariableMap, boundarymap, pdesys::PDESystem, disc::AbstractDiscretization)Transform a PDE system into the representation required by disc.
This hook is called only when should_transform returns true. Update or replace the system consistently with v and boundarymap; the returned system is used for all subsequent interface calls. A transformation must preserve the meaning of the equations and boundary conditions.
Arguments
v::VariableMap: The variable map built before transformation.boundarymap: The parsed boundary map associated withpdesys.pdesys::PDESystem: The system to transform.disc::AbstractDiscretization: The discretization that will consume it.
Returns
PDESystem: The transformed system passed to the construction hooks.
Examples
function PDEBase.transform_pde_system!(v, boundarymap, pdesys, disc::MyDiscretization)
return rebuild_for_my_backend(pdesys)
endConstruction
Construction hooks establish the values shared by every equation visit. The mapping returned by construct_var_equation_mapping must support get_eqvar for every PDE, and the derivative data returned by construct_differential_discretizer must support every derivative order listed in orders.
PDEBase.construct_disc_state — Function
construct_disc_state(disc::AbstractDiscretization)Construct the mutable state that accumulates discretized equations.
The returned object is passed to discretize_equation! for every PDE and then to generate_system. It must be mutable itself or contain mutable fields that can hold the generated interior and boundary equations.
Arguments
disc::AbstractDiscretization: The discretization being applied.
Returns
AbstractDiscretizationState: State consumed by the discretization hooks.
Examples
struct MyState <: PDEBase.AbstractDiscretizationState
equations::Vector
end
PDEBase.construct_disc_state(::MyDiscretization) = MyState(Any[])PDEBase.construct_discrete_space — Function
construct_discrete_space(v::VariableMap, disc::AbstractDiscretization)Construct the discrete spatial representation used by a discretizer.
Arguments
v::VariableMap: The variable map containing the dependent and independent variables.disc::AbstractDiscretization: The discretization scheme.
Returns
AbstractDiscreteSpace: A grid or mesh representation and any associated discrete variables.
Examples
struct MySpace <: PDEBase.AbstractCartesianDiscreteSpace
grid::Vector{Float64}
end
PDEBase.construct_discrete_space(v, ::MyDiscretization) = MySpace(collect(0:0.1:1))construct_discrete_space(v::VariableMap, pdesys::PDESystem, disc::AbstractOptimizationSystemDiscretization)Create the space on which an optimization-system discretization evaluates its residuals. Unlike the grid of an equation-system discretization it may depend on the boundary conditions of pdesys (for example to keep interior collocation points off the boundaries), so the system is passed along with the variable map. The default forwards to construct_discrete_space(v, disc).
Arguments
v::VariableMap: The variable map constructed frompdesys.pdesys::PDESystem: The normalized PDE system being discretized.disc::AbstractOptimizationSystemDiscretization: The discretization being applied.
Returns
- The discrete space passed as
sto the remaining hooks.
PDEBase.construct_var_equation_mapping — Function
construct_var_equation_mapping(pdeeqs, bmap, s::AbstractDiscreteSpace, disc::AbstractDiscretization)Construct the mapping from each PDE to the discrete variable it determines.
The mapping is also the place to precompute information needed when boundary conditions are applied. get_eqvar must be able to retrieve the variable for each equation in pdeeqs from the returned object.
Arguments
pdeeqs: The PDE equations after any optional transformation.bmap: The parsed boundary map.s::AbstractDiscreteSpace: The discrete space returned byconstruct_discrete_space.disc::AbstractDiscretization: The discretization scheme.
Returns
AbstractVarEqMapping: A mapping consumed byget_eqvaranddiscretize_equation!.
PDEBase.construct_differential_discretizer — Function
construct_differential_discretizer(pdesys::PDESystem, s::AbstractDiscreteSpace,
disc::AbstractDiscretization, orders)Construct the derivative approximation data for disc.
orders maps each independent variable to the derivative orders appearing in the PDE and boundary conditions. This hook is where a discretizer should do precomputation such as finite-difference weights or element-local operators.
Arguments
pdesys::PDESystem: The transformed PDE system.s::AbstractDiscreteSpace: The discrete space on which derivatives act.disc::AbstractDiscretization: The discretization scheme.orders: A map from independent variables to required derivative orders.
Returns
AbstractDifferentialDiscretizer: Derivative data consumed bydiscretize_equation!.
Equation and Finalization Hooks
discretize_equation! is the only per-equation hook. It should append or mutate the state and leave system construction to generate_system. The finalization hooks should keep the metadata and initial-value representations consistent; generate_system receives the checks keyword and should forward it to the symbolic system constructor.
PDEBase.discretize_equation! — Function
discretize_equation!(disc_state::AbstractDiscretizationState, pde::Equation, vareqmap::AbstractVarEqMapping,
eqvar, bcmap, depvars, s::AbstractDiscreteSpace,
derivweights::AbstractDifferentialDiscretizer, indexmap, discretization::AbstractDiscretization)Add one PDE and its associated spatial boundary conditions to disc_state.
This hook is called once for every PDE after the equation-to-variable mapping has been built. It should append or mutate the discrete interior and boundary equations in disc_state; it should not construct the final symbolic system.
Arguments
disc_state::AbstractDiscretizationState: Accumulator returned byconstruct_disc_state.pde::Equation: The PDE equation currently being discretized.vareqmap::AbstractVarEqMapping: Mapping returned byconstruct_var_equation_mapping.eqvar: The discrete variable solved for bypde.bcmap: Boundary map with conditions on the time variable removed.depvars: Dependent variables appearing inpde.s::AbstractDiscreteSpace: The discrete space.derivweights::AbstractDifferentialDiscretizer: Derivative data returned byconstruct_differential_discretizer.indexmap: Map from each independent variable ineqvarto its index.discretization::AbstractDiscretization: The active discretization.
Returns
nothing: The state is updated in place.
discretize_equation!(disc_state, eq::Equation, kind::Symbol, s, derivweights, disc::AbstractOptimizationSystemDiscretization)Lower one equation of the PDESystem for an optimization-system discretization and record the result in disc_state. kind is :pde for the equations and :bc for the boundary conditions; each is lowered on its own residual domain, so boundary conditions are not required to lie on a boundary of the domain.
Arguments
disc_state: The mutable state created byconstruct_disc_state.eq::Equation: The equation, cardinalized tolhs - rhs ~ 0.kind::Symbol::pdeor:bc.s: The discrete space created byconstruct_discrete_space.derivweights: The value returned byconstruct_differential_discretizer.disc::AbstractOptimizationSystemDiscretization: The discretization being applied.
Returns
nothing: The state is mutated in place.
PDEBase.generate_ic_defaults — Function
generate_ic_defaults(ics, s::AbstractDiscreteSpace, disc::AbstractDiscretization)Generate discrete initial-condition values and validate the input conditions.
Arguments
ics: Conditions on the time variable extracted from the boundary map.s::AbstractDiscreteSpace: The discrete space.disc::AbstractDiscretization: The discretization.
Returns
u0: Initial values in the representation expected bygenerate_system.
Examples
PDEBase.generate_ic_defaults(ics, space::MySpace, ::MyDiscretization) =
[evaluate_initial_condition(ic, space) for ic in ics]PDEBase.generate_metadata — Function
generate_metadata(s, discretization, pdesys, boundarymap, complexmap, u0)Create metadata needed to construct a problem or reconstruct a solution.
The metadata is passed unchanged to generate_system. Returning a subtype of SciMLBase.AbstractDiscretizationMetadata allows the generic add_metadata! hook to attach the original symbolic system.
Arguments
s: The discrete space.discretization: The active discretization.pdesys: The transformed PDE system.boundarymap: The parsed boundary map.complexmap: The map used while normalizing complex equations.u0: The discrete initial conditions returned bygenerate_ic_defaults.
Returns
AbstractDiscretizationMetadata: Metadata consumed bygenerate_system.
PDEBase.generate_system — Function
generate_system(disc_state::AbstractDiscretizationState, s::AbstractDiscreteSpace, u0, tspan, metadata::AbstractDiscretizationMetadata, discretization::AbstractDiscretization)Build the final symbolic system from the accumulated discretization results.
The returned value is passed back through SciMLBase.symbolic_discretize; in the usual time-dependent case it is a tuple (system, tspan). Set tspan on the system as well, so that a caller who keeps only the system can still build a time-dependent problem without supplying a time span. Use checks when constructing the symbolic system and forward it to the constructor rather than silently changing validation behavior.
Arguments
disc_state::AbstractDiscretizationState: State populated bydiscretize_equation!.s::AbstractDiscreteSpace: The discrete space.u0: Initial values returned bygenerate_ic_defaults.tspan: The time interval, ornothingfor a steady-state system.metadata::AbstractDiscretizationMetadata: Metadata returned bygenerate_metadata.discretization::AbstractDiscretization: The active discretization.
Keywords
checks::Bool = true: Enable symbolic-system consistency checks.
Returns
Tuple: The generated symbolic system and its time span.
Examples
function PDEBase.generate_system(state, space, u0, tspan, metadata,
disc::MyDiscretization; checks = true)
system = ODESystem(state.equations, get_time(disc); checks)
return system, tspan
endAccessor Hooks
These accessors keep downstream packages independent of concrete field names:
PDEBase.get_time — Function
get_time(discretization::AbstractDiscretization)Return the independent time variable used by discretization.
Return nothing for steady-state discretizations. The result is used to separate initial conditions from spatial boundary conditions and to determine whether the generated system is time dependent.
Arguments
discretization::AbstractDiscretization: The discretization being applied.
Returns
- A symbolic time variable, or
nothingfor a steady-state discretization.
PDEBase.get_discvars — Function
get_discvars(s::AbstractDiscreteSpace)Return the discrete variables associated with s.
The result is consumed by the final system constructor to identify the unknowns created by the discretizer. Discretizations without a separate variable map may return an empty collection.
Arguments
s::AbstractDiscreteSpace: The discrete space.
Returns
- A collection or map of symbolic discrete variables.
PDEBase.get_system_unknowns — Function
get_system_unknowns(s::AbstractDiscreteSpace)Return the unknowns of the system generate_system builds from s, as a vector of symbolic variables.
The default flattens get_discvars into its scalar entries, one unknown per discrete variable. A discretizer that represents each dependent variable as one array-valued symbolic, such as u(t)[1:n], can override this to return those arrays so that the system has one unknown per dependent variable. The scalar entries of get_discvars then index into these arrays and remain what the discretized equations, boundary conditions and initial conditions are written in terms of.
Arguments
s::AbstractDiscreteSpace: The discrete space.
Returns
- A vector of symbolic unknowns.
PDEBase.get_eqvar — Function
get_eqvar(vareqmap::AbstractVarEqMapping, eq)Return the discrete variable solved for by equation eq.
Every equation passed to discretize_equation! must have a corresponding variable. The returned variable must use the same independent-variable ordering that ivs and x2i expect for the associated VariableMap.
Arguments
vareqmap::AbstractVarEqMapping: Mapping returned byconstruct_var_equation_mapping.eq: The PDE equation being visited.
Returns
- The discrete dependent variable associated with
eq.
PDEBase.add_metadata! — Function
add_metadata!(metadata::AbstractDiscretizationMetadata, value)Attach value to the mutable metadata slot in metadata.
This hook is used by PDEBase to retain the original symbolic system after a compiled system is built. Metadata implementations should provide a mutable metadata field, typically a Ref, when they use the default method.
Arguments
metadata::AbstractDiscretizationMetadata: Metadata object to update.value: Value to store in its metadata slot.
Returns
- The assigned
value.
Solution Interface
solve on the problem returned by discretize gives the user a solution expressed in the PDESystem's own variables, and every discretizer exposes it the same way (see the ModelingToolkit PDESystem page for the user-facing description):
sol[u(t, x)]: the dependent variable on the discretization (or evaluation) grid, one axis per argument ofu;sol[x]: the grid of the independent variablex;sol(t, x; dv = u(t, x)): evaluation at arbitrary numbers or ranges, interpolating or evaluating as the method allows; withoutdv, the values of every dependent variable;sol.original_sol: the solution of the discretized problem itself.
The wrappers are SciMLBase's PDETimeSeriesSolution (systems with a time variable) and PDENoTimeSolution. Their fields hold ivs, dvs, ivdomain (the grid of each independent variable) and u (a map from each dependent variable to its values on that grid), plus original_sol and the discretizer's metadata in disc_data. To provide the interface, a discretizer:
- attaches its metadata, a subtype of
SciMLBase.AbstractDiscretizationMetadata{Val(hasTime)}(a SciMLBase type), to the generatedSystemthrough ModelingToolkit'sProblemTypeCtxmetadata, so the discretized problem carries it as itsproblem_typeandsolvehands it toSciMLBase.wrap_sol; - defines
SciMLBase.PDETimeSeriesSolution(sol, metadata::D)orSciMLBase.PDENoTimeSolution(sol, metadata::D)for its metadata typeD, filling the fields above from the trained or integratedsol; - extends
Base.getindex(sol::PDENoTimeSolution{T, N, S, D}, sym::Num)(and the time-series counterpart) to map dependent variables tosol.u[sym]and independent variables to theirivdomainentry, and the call(sol::PDENoTimeSolution{T, N, S, D})(args...; dv = nothing)for evaluation at arbitrary points, dispatching on its ownDso that discretizers do not collide.
make_pdesys_compatible flattens array-valued dependent variables before discretization. Use replaced_vars(v::VariableMap) to translate between each generated scalar variable and the original indexed variable. Its dictionary maps flattened scalar variables to user-facing indexed variables, and is empty when no array-valued dependent variables are present. A solution wrapper can reverse these pairs when serving scalar queries such as sol[u(x)[1]]. For an array-valued query such as sol[u(x)], iterate the entries of the queried array (for example collect(u(x))) and look each component up in the map so the returned components follow the array's index order.
MethodOfLines.jl (MOLMetadata) and NeuralPDE.jl (PINNMetadata) are the reference implementations.
Minimal Implementation
The following skeleton shows the intended generic dispatch. A real package must also implement the numerical work inside discretize_equation! and construct a concrete symbolic system in generate_system.
using PDEBase
struct MyDiscretization <: PDEBase.AbstractEquationSystemDiscretization
time::Any
end
struct MySpace <: PDEBase.AbstractCartesianDiscreteSpace
grid::Vector{Float64}
end
PDEBase.get_time(disc::MyDiscretization) = disc.time
PDEBase.construct_discrete_space(v, ::MyDiscretization) = MySpace(collect(0:0.1:1))
function PDEBase.construct_var_equation_mapping(eqs, bmap, space::MySpace,
disc::MyDiscretization)
return MyMapping(Dict(eq => space.grid for eq in eqs))
end
function PDEBase.discretize_equation!(state, pde, mapping, eqvar, bcmap,
depvars, space, derivative_data, indexmap, disc)
push!(state.equations, pde)
return nothing
endThe generic interface tests in PDEBase exercise the default methods and a minimal concrete dispatch using only these hooks. New discretizers should add a similar contract test for their concrete invariants.
See the Discretization Workflow for the data flow and the VariableMap and Boundary Conditions pages for the symbolic input structures.