Solving
The solve entry point drives the implicit time integration; its keywords select the simulation modes (ALE, radiation model, Darcy transport), tolerances, Jacobian backend, callbacks, and diagnostics. The User Guide's solver chapter documents every option with examples, and parameter sweeps covers the sweep harness.
CommonSolve.solve — Method
solve(problem::PyrolysisProblem; kwargs...) -> PyrolysisSolutionSolve a pyrolysis problem using DifferentialEquations.jl.
Arguments
problem: The PyrolysisProblem to solve.solvedoes not modify it: the run integrates a copy with its own mesh, so every solve of a problem, and every parameter-sweep point that reuses its mesh, starts from the same initial geometry.solution.problemis that copy; its mesh holds the run's final geometry, live-cell count and boundary faces.
Keyword Arguments
integrator=nothing: ODE integrator constructor (e.g.KenCarp4) or fully constructed algorithm (e.g.KenCarp4(linsolve=KLUFactorization())).nothingchoosesKenCarp4with the defaultlinear_solver.linear_solver=nothing: LinearSolve backend.nothingchooses by the Jacobian pattern (see "Linear Solver"):UMFPACKFactorization()for the exact ALE Jacobian,KLUFactorization()for every other Jacobian. Pair withintegratoronly whenintegratoris a constructor.jacobian=nothing: Jacobian backend.nothingpicksStructured(strategy = DirectSolve())on a fixed mesh andStructured(strategy = ApproxSolve())under ALE. AStructuredbackend such asStructured(strategy = DirectSolve())overrides the default; the spec is built for this solve's own mesh copy, mode and options.verify_jacobian=nothing: NamedTuple(rtol=, atol=, sample_times=)installs aDiscreteCallbackthat compares the active backend against aDenseAD_ForwardDiffreference at the listed sample times. Reports via@warn; does not throw.abstol=1e-8: Absolute tolerancereltol=1e-6: Relative tolerancemaxiters=1000000: Maximum iterationsdtmin=1e-12: Minimum timestepdtmax=Inf: Maximum timestepsaveat=problem.output_times: Times to save outputcallback=nothing: Additional callbacksprogress=true: Show progress bar during solvevalidate=true: Validate problem before solvinguse_ale=false: Enable ALE mesh motion for shrinking/swelling materialsmin_thickness=1e-6: Minimum live thickness before termination (ALE only) [m]handle_depletion=false: Park depleted cells (see "Cell depletion" below)depletion_threshold=0.05: Depletion trigger. ALE: a cell is parked when its thickness falls below this fraction of its own initial thickness. Fixed mesh: the top live cell is parked when its condensed concentration falls below this fraction of its initial value.min_cells=2: Minimum number of live cells; the solve terminates instead of parking below itwarm_start=nothing: Reuse a priorPyrolysisSolution's final state as the initial state (u0). Component count, mesh size, and ALE/χ modes must match, and the prior solve must not have parked cells.tspan[1]still defines the integrator's start time, so the warm-started solve treats the prior final state as fresh initial data.
Jacobian backend
Pass jacobian = backend to override the auto-default; the spec is built for this solve's mode. The backend is Structured; behavior is selected by its strategy:
| Spec | When to use |
|---|---|
Structured(strategy = DirectSolve()) | Fixed-mesh default; exact assembly + sparse direct |
Structured(strategy = ApproxSolve()) | ALE default; drops the structured couplings (reduced pattern) |
Returns
A PyrolysisSolution containing:
- Temperature and concentration profiles at saved times
- Mass loss rate history
- Surface temperature history
- Thickness history (when use_ale=true)
- Solver return code and timing
ALE Mode
When use_ale=true, the solver uses the unified ProblemDef/Workspace residual. Node positions evolve simultaneously with temperature and concentration fields, maintaining proper conservation (GCL compliance). The thickness history is extracted from the state vector and stored in solution.extras.thickness_history.
Cell Depletion Handling
When handle_depletion=true, in either mesh mode, the solver monitors every live cell. Under ALE a cell that has shrunk below depletion_threshold of its own initial thickness (a per-cell reference, so stretched meshes are judged cell-by-cell) is parked; on a fixed mesh the top live cell is parked once its condensed concentration has fallen below the same fraction of its initial value. A fixed-mesh solve stops with a warning (retcode :Terminated) when a cell below the top live cell becomes a void: the material has gasified from within, the model carries no heat across the void, and a regressing material belongs on the ALE mesh. A park merges the cell's mass and sensible enthalpy into its neighbour and moves the freed state slot above the live surface as an inert ghost: the state vector, mesh arrays and Jacobian keep their size, every kernel runs over the live range 1:mesh.n_active_cells, and the boundary conditions act at the live surface. Vanishing cells therefore never enter the nonlinear system (their 1/Δz² conditioning is what stalls an implicit solve), and the structured Jacobian stays valid through burnout. Ghost slots read as NaN in solution.T, solution.ξ and solution.z; solution.extras.live_cells and solution.extras.park_times record the events. Parameter sensitivities are valid only on horizons without park events.
Recommended Solvers
The default is KenCarp4(autodiff = AutoFiniteDiff()) with the default Jacobian (see "Default Jacobian") and the default linear solver (see "Linear Solver"). This is the production baseline for stiff pyrolysis runs.
Other stiff ODE algorithms such as KenCarp3, TRBDF2, Rodas5P, and QNDF can be passed through integrator. Rosenbrock and BDF methods may reevaluate or refactor the Jacobian more often on these problems, so benchmark against the default before changing the solver family. Note: under OrdinaryDiffEq v7 only the default solver set is re-exported by the umbrella package, so passing e.g. QNDF/KenCarp3/TRBDF2 requires loading the relevant sublibrary (OrdinaryDiffEqBDF, OrdinaryDiffEqSDIRK, …) yourself.
ILUGMRESFactorization is available for large-ALE experiments where direct factorization cost dominates, but it is not the default because inexact linear solves can increase Newton rejections on strongly Arrhenius transients.
Default Jacobian
With jacobian = nothing (the default), a fixed mesh uses Structured(strategy = DirectSolve()) and ALE uses Structured(strategy = ApproxSolve()). The Structured backend assembles per-operator contributions into a StructuredJacobian. DirectSolve densifies the structured couplings (ALE prefix-sum, χ rank-1, Beer–Lambert suffix-sum) at solve time, so the linear solve sees the exact Jacobian. ApproxSolve drops them: under ALE the exact pattern couples every node to every cell below it, and the reduced one keeps the Newton matrix local while the modified Newton iteration still converges. Pass jacobian = Structured(strategy = DirectSolve()) for the exact ALE Jacobian.
Linear Solver
With linear_solver = nothing the sparse factorization follows the Jacobian pattern. Structured(strategy = DirectSolve()) under ALE materializes the geometry block, in which every cell couples to every node below it; that pattern uses UMFPACKFactorization(), whose supernodal factorization handles the dense block. Every other sparse Jacobian — every ApproxSolve Jacobian, and DirectSolve on an Eulerian mesh, Beer–Lambert suffix included — uses KLUFactorization(), which refactors the fixed pattern in place without allocating. Pass linear_solver = SparspakFactorization(), another LinearSolve backend, or a fully constructed integrator to choose the factorization.
Example
problem = PyrolysisProblem(mesh, material, bcs, (0.0, 100.0))
solution = solve(problem)
using OrdinaryDiffEqBDF # QNDF lives in a sublibrary under OrdinaryDiffEq v7
solution = solve(problem, integrator=QNDF, abstol=1e-10)
solution = solve(problem, progress=false) # Disable progress bar
# With ALE mesh motion for shrinking materials:
solution = solve(problem, use_ale=true, min_thickness=0.1e-3)
thickness_history = solution.extras.thickness_history
# Choose the factorization instead of the pattern-based default:
solution = solve(problem, use_ale=true, linear_solver=SparspakFactorization())
# The exact ALE Jacobian instead of the reduced default (factored by UMFPACK):
solution = solve(problem, use_ale=true, jacobian=Structured(strategy=DirectSolve()))
# With depletion handling (cells park at 1% of their initial thickness):
solution = solve(problem, use_ale=true, handle_depletion=true, depletion_threshold=0.01)
# Differential-correctness verification at sample times:
solution = solve(problem,
verify_jacobian=(rtol=1e-6, atol=1e-10, sample_times=[0.0, 1.0, 10.0]))Pyrolysis.Solver.parameter_sweep — Function
parameter_sweep(problem, grid;
modify::Function = default_modify,
extract::Function = identity,
parallel::Bool = nworkers() > 1,
solve_kwargs...) -> VectorSweep problem over grid and return a vector of results, one per grid point. Each grid point p ∈ grid is materialised into a fresh problem via modify(problem, p), solved with solve_kwargs, then reduced via extract(solution) before being returned.
Arguments
problem::PyrolysisProblem: The base problem. Not mutated.grid: Any iterable of parameter points. Common shapes:Vector{<:NamedTuple},Iterators.product(...),Vector{Vector{Float64}}.modify:(base_problem, point) -> new_problem. Must return a freshPyrolysisProblemfor each call; do not mutatebase_problem. The new problem may reuse the base problem's mesh:solveworks on its own copy of the mesh, so every point starts from the base geometry. The default raises a helpful error pointing to this kwarg.extract:solution -> summary. Defaults toidentity. For large grids prefer a smallNamedTuple— fullPyrolysisSolutioninstances are expensive to ship across worker boundaries.parallel: UseDistributed.pmapwhentrue, otherwise fall back to a serialmap. Defaults to parallel when more than one worker is available (nworkers() > 1).solve_kwargs: Forwarded tosolve().
Notes
- Use
Distributed.addprocs()(and@everywhere using Pyrolysis) before calling this function to make workers available. - Failed solves are not retried. Wrap the body of
extractin atryif you need fault tolerance.
Example
using Distributed
addprocs(4)
@everywhere using Pyrolysis
grid = [(A = a, E = e) for a in (1e12, 1e13, 1e14), e in (1.5e5, 2.0e5)]
function modify(prob, p)
# Rebuild the material with the swept Arrhenius parameters.
mat = prob.material
new_rxn = Reaction(:decomp, 1 => 2; A = p.A, E = p.E, h = 200e3)
new_mat = Material(name = mat.name, components = mat.components,
reactions = (new_rxn,), mixing = mat.mixing,
lateral_shrinkage_law = mat.lateral_shrinkage_law)
return PyrolysisProblem(mesh = prob.mesh, material = new_mat,
bc_set = prob.bc_set, T_initial = prob.T_initial,
ξ_initial = prob.ξ_initial, tspan = prob.tspan,
dt_initial = prob.dt_initial,
output_times = prob.output_times)
end
extract(sol) = (peak_T = maximum(sol.surface_T),
final_mass = sol.extras.total_mass[end])
results = parameter_sweep(problem, grid; modify, extract)