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.

Developer 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:

  1. VariableMap(pdesys, discretization) normalizes symbolic variables and domains.
  2. interface_errors rejects unsupported systems before work is allocated.
  3. parse_bcs creates the boundary map, then check_boundarymap validates it.
  4. should_transform optionally enables transform_pde_system!.
  5. construct_disc_state, construct_discrete_space, and construct_var_equation_mapping create the discretizer state.
  6. construct_differential_discretizer precomputes derivative data.
  7. For each PDE, get_eqvar selects its discrete variable and discretize_equation! updates the state in place.
  8. generate_ic_defaults creates discrete initial values.
  9. generate_metadata stores data needed by the generated problem and solution.
  10. generate_system constructs 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
end

AbstractEquationSystemDiscretization 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 from sys.
  • 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
end
source
PDEBase.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
end
source
PDEBase.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: true when the transformation hook should run, otherwise false.
source
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 with pdesys.
  • 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)
end
source

Construction

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[])
source
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))
source
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 from pdesys.
  • pdesys::PDESystem: The normalized PDE system being discretized.
  • disc::AbstractOptimizationSystemDiscretization: The discretization being applied.

Returns

  • The discrete space passed as s to the remaining hooks.
source
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 by construct_discrete_space.
  • disc::AbstractDiscretization: The discretization scheme.

Returns

  • AbstractVarEqMapping: A mapping consumed by get_eqvar and discretize_equation!.
source
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 by discretize_equation!.
source

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 by construct_disc_state.
  • pde::Equation: The PDE equation currently being discretized.
  • vareqmap::AbstractVarEqMapping: Mapping returned by construct_var_equation_mapping.
  • eqvar: The discrete variable solved for by pde.
  • bcmap: Boundary map with conditions on the time variable removed.
  • depvars: Dependent variables appearing in pde.
  • s::AbstractDiscreteSpace: The discrete space.
  • derivweights::AbstractDifferentialDiscretizer: Derivative data returned by construct_differential_discretizer.
  • indexmap: Map from each independent variable in eqvar to its index.
  • discretization::AbstractDiscretization: The active discretization.

Returns

  • nothing: The state is updated in place.
source
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 by construct_disc_state.
  • eq::Equation: The equation, cardinalized to lhs - rhs ~ 0.
  • kind::Symbol: :pde or :bc.
  • s: The discrete space created by construct_discrete_space.
  • derivweights: The value returned by construct_differential_discretizer.
  • disc::AbstractOptimizationSystemDiscretization: The discretization being applied.

Returns

  • nothing: The state is mutated in place.
source
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 by generate_system.

Examples

PDEBase.generate_ic_defaults(ics, space::MySpace, ::MyDiscretization) =
    [evaluate_initial_condition(ic, space) for ic in ics]
source
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 by generate_ic_defaults.

Returns

  • AbstractDiscretizationMetadata: Metadata consumed by generate_system.
source
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 by discretize_equation!.
  • s::AbstractDiscreteSpace: The discrete space.
  • u0: Initial values returned by generate_ic_defaults.
  • tspan: The time interval, or nothing for a steady-state system.
  • metadata::AbstractDiscretizationMetadata: Metadata returned by generate_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
end
source

Accessor 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 nothing for a steady-state discretization.
source
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.
source
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.
source
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 by construct_var_equation_mapping.
  • eq: The PDE equation being visited.

Returns

  • The discrete dependent variable associated with eq.
source
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.
source

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 of u;
  • sol[x]: the grid of the independent variable x;
  • sol(t, x; dv = u(t, x)): evaluation at arbitrary numbers or ranges, interpolating or evaluating as the method allows; without dv, 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:

  1. attaches its metadata, a subtype of SciMLBase.AbstractDiscretizationMetadata{Val(hasTime)} (a SciMLBase type), to the generated System through ModelingToolkit's ProblemTypeCtx metadata, so the discretized problem carries it as its problem_type and solve hands it to SciMLBase.wrap_sol;
  2. defines SciMLBase.PDETimeSeriesSolution(sol, metadata::D) or SciMLBase.PDENoTimeSolution(sol, metadata::D) for its metadata type D, filling the fields above from the trained or integrated sol;
  3. extends Base.getindex(sol::PDENoTimeSolution{T, N, S, D}, sym::Num) (and the time-series counterpart) to map dependent variables to sol.u[sym] and independent variables to their ivdomain entry, and the call (sol::PDENoTimeSolution{T, N, S, D})(args...; dv = nothing) for evaluation at arbitrary points, dispatching on its own D so 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
end

The 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.