8. Running the Solver
Once you have a PyrolysisProblem (Chapters 5–7), the single function that integrates it forward in time is solve. This chapter is the complete option reference for solve(problem; ...): how to pick the time integrator and its tolerances, how to choose and configure the structured Jacobian and the linear solver behind it, how to switch on optional physics (ALE mesh motion, radiation, depletion handling), how callbacks are wired in, how warm-starting and output saving work, and how to read mass-loss-rate output for variable cross-sections. Every keyword shown here is a real solve keyword — solve rejects unknown keywords with an error, so there is no room for typos. The physics behind these options is derived in the Technical Reference; this chapter tells you which knob to turn and what each one does. For the underlying temporal integration and Jacobian theory, see Technical Reference §15.
8.1 The solve entry point
solve takes the problem as a positional argument and everything else as keywords:
using Pyrolysis
solution = solve(problem) # all defaults: KenCarp4 + structured Jacobian, no radiationThat call already does a great deal: it validates the problem, builds the immutable problem definition and a mutable workspace, packs the initial state vector, constructs the default structured Jacobian, assembles the callback set, runs an implicit ODE integrator, and extracts a PyrolysisSolution. It leaves problem unchanged: the run works on a copy with its own mesh, so a problem can be solved again (or shared by parameter-sweep points) and always starts from the geometry you built. The copy is kept as solution.problem; its mesh holds the final geometry, live cells and boundary states. The remaining sections explain how to override each part.
The full keyword signature, with defaults, is:
| Keyword | Default | What it controls |
|---|---|---|
integrator | nothing → KenCarp4 | ODE integrator (constructor or built instance); §8.2 |
linear_solver | nothing → KLUFactorization(), or UMFPACKFactorization() for the exact ALE Jacobian | Sparse factorization paired with a constructor integrator; §8.4 |
abstol | 1e-8 | Absolute error tolerance; §8.3 |
reltol | 1e-6 | Relative error tolerance; §8.3 |
maxiters | 1000000 | Maximum integrator steps |
dtmin | 1e-12 | Minimum time step [s] |
dtmax | Inf | Maximum time step [s] |
jacobian | nothing → Structured(strategy = DirectSolve()) (fixed mesh) or Structured(strategy = ApproxSolve()) (ALE) | Jacobian backend; §8.5 |
verify_jacobian | nothing | Compare active Jacobian to a dense AD reference; §8.6 |
use_ale | false | Enable ALE moving-mesh integration; §8.7 |
radiation_model | NO_RADIATION | NO_RADIATION / SURFACE_ABSORPTION / BEER_LAMBERT; §8.8 |
min_thickness | 1e-6 | Terminate ALE solve when sample thins below this [m]; §8.7, §8.9 |
handle_depletion | false | Park depleted cells (ALE or fixed mesh); §8.9 |
depletion_threshold | 0.05 | Fraction of a cell's initial thickness (ALE) or condensed concentration (fixed mesh) that triggers a park; §8.9 |
min_cells | 2 | Minimum live cells; the solve terminates instead of parking below it; §8.9 |
diagnostics | false | Log a sensible-energy snapshot during the solve; §8.10 |
diagnostics_stride | 10 | Record a diagnostics snapshot every N accepted steps; §8.10 |
callback | nothing | One extra user callback; §8.10 |
warm_start | nothing | Reuse a prior solution's final state as the IC; §8.11 |
saveat | problem.output_times | Times at which to store output; §8.12 |
progress | true | Show a progress bar |
validate | true | Run validate_problem before solving |
solve always returns a PyrolysisSolution; see Chapter 9 for its fields and post-processing accessors.
8.2 Choosing the time integrator
Pyrolysis problems are stiff: Arrhenius reaction rates change by orders of magnitude across a narrow temperature band, and conduction over fine cells imposes a small explicit stability limit. The solver therefore defaults to an implicit, stiffly stable method. When integrator = nothing (the default), solve builds
KenCarp4(autodiff = AutoFiniteDiff(), linsolve = KLUFactorization())with UMFPACKFactorization() in place of KLU for the exact ALE Jacobian (§8.4).
KenCarp4 is a 4th-order ESDIRK method — a good general-purpose choice for the moderately stiff systems that pyrolysis produces. solve always attaches the structured Jacobian (§8.5), so the integrator uses it rather than finite-differencing the right-hand side; autodiff = AutoFiniteDiff() only sets the integrator's own differentiation mode.
You can pass the integrator in three forms:
1. A constructor (a type or function). solve instantiates it for you and attaches the chosen linear solver:
using OrdinaryDiffEqSDIRK: KenCarp4, TRBDF2 # SDIRK methods (add OrdinaryDiffEqSDIRK to your environment)
using OrdinaryDiffEq: FBDF # OrdinaryDiffEq re-exports only its default set (FBDF, Rodas5P, …)
solve(problem; integrator = KenCarp4) # rebuilt with default linsolve
solve(problem; integrator = TRBDF2) # another SDIRK alternative
solve(problem; integrator = FBDF) # multistep BDF, good for very stiff TGA-like runsThe integrator names in the examples of this guide (KenCarp4, TRBDF2, FBDF, Rodas5P) are not exported by using Pyrolysis; import them as above.
2. A constructor with a paired linear_solver. Use this when you want a specific sparse factorization but still want solve to wire it together:
solve(problem; integrator = KenCarp4,
linear_solver = SparspakFactorization())3. A fully constructed algorithm instance. Build it yourself, including the linear solver, and pass it as integrator. In this case do not also pass linear_solver — solve raises an error if you pair linear_solver with an already-built instance:
solve(problem;
integrator = KenCarp4(linsolve = SparspakFactorization()))This third form is what the shipped Douglas-fir example uses (examples/07_douglas_fir_cone.jl).
Recommended starting points:
| Situation | Integrator |
|---|---|
| Default cone / TGA / general use | KenCarp4 (the default) |
| Very stiff, slow to converge | FBDF or TRBDF2 |
| Tight accuracy near a sharp reaction front | KenCarp4 with tighter reltol |
All integrators come from OrdinaryDiffEq.jl; any stiff solver from that package works. See Technical Reference §15 for the implicit-RK formulation and the W = I/(γΔt) − J matrix the integrator factors each step.
8.3 Tolerances and step-size limits
The integrator adapts its time step to keep the local error estimate within abstol (absolute) and reltol (relative). The defaults are:
solve(problem; abstol = 1e-8, reltol = 1e-6)Guidance:
- Loosen (
reltol = 1e-4,abstol = 1e-6) for quick exploratory runs or parameter sweeps where you only need trends. - Tighten (
reltol = 1e-7,abstol = 1e-9) for convergence studies, Jacobian verification, or when comparing against reference codes. The Douglas-fir example usesabstol = 1e-9, reltol = 1e-7. - Tolerances mix temperature (≈ 10²–10³ K) and concentration (≈ 0–10³ kg/m³) in one state vector, so prefer driving accuracy through
reltoland keepabstolcomfortably below the smallest concentration you care about.
Step-size limits:
solve(problem;
dtmin = 1e-14, # allow tiny steps through a rapid reaction front
dtmax = Inf, # no upper cap (default)
maxiters = 2_000_000)dtmin matters in ALE/charring runs: as cells thin, the diffusive stability scale (dt ∝ Δz²) shrinks, and the integrator may need very small steps to push through the front. If a solve stops with retcode == :DtLessThanMin, lower dtmin. If it stops with :MaxIters, raise maxiters (or loosen tolerances).
solve also caps the step after Newton failures. When an accepted step needed Newton retries, the next 20 steps are held at or below 1.25 times that step, so the controller cannot regrow the step straight back into the range where the Newton failed. A run without Newton failures is unaffected. Compare sol.raw_solution.stats.nnonlinconvfail with the number of accepted steps (naccept) to see how often the Newton fails.
8.4 The linear solver
Each implicit step solves a sparse linear system in the W matrix. With linear_solver = nothing, solve picks a sparse direct factorization from the Jacobian's sparsity pattern:
UMFPACKFactorization()for the exact ALE Jacobian (Structured(strategy = DirectSolve())under ALE). Its geometry block couples every cell to every node below it, and UMFPACK's supernodal factorization is the fastest on that dense block.KLUFactorization()for every other sparse Jacobian: everyApproxSolveJacobian (including the ALE default) and every Eulerian one. KLU refactors the fixed pattern in place without allocating, and on these patterns it runs several times faster end to end than Sparspak.
The choice depends on the mode and the Jacobian strategy, not on the mesh size.
You select the linear solver either by pairing it with a constructor integrator:
using LinearSolve: KLUFactorization, SparspakFactorization, UMFPACKFactorization
solve(problem; integrator = KenCarp4,
linear_solver = UMFPACKFactorization())
solve(problem; integrator = KenCarp4,
linear_solver = SparspakFactorization()) # pure-Julia sparse LUor by baking it into a fully built integrator:
solve(problem; integrator = KenCarp4(linsolve = KLUFactorization()))KLU, UMFPACK and Sparspak are interchangeable sparse direct solvers; the default only picks the one suited to the pattern. An explicit linear_solver always wins, and any other LinearSolve factorization is passed the same way — for example MKLPardisoFactorize() once Pardiso/MKL is installed and loaded. Iterative ILU-preconditioned solvers for large ALE problems are covered in §8.13.
8.5 Jacobian backend selection
The single most important performance lever is the Jacobian. solve always assembles the structured analytical Jacobian, Structured, with a linear-solve strategy chosen by the mesh mode when jacobian = nothing (the default):
Structured(strategy = DirectSolve()) # fixed (Eulerian) mesh
Structured(strategy = ApproxSolve()) # ALEIt assembles the Jacobian from per-operator analytical and AD contributions (reaction, conduction, gas transport, radiation, boundary conditions, and the ALE structured couplings) and hands a sparse matrix to the linear solver. It is far faster than letting the integrator finite-difference the whole RHS. See Technical Reference §15 for the operator-based architecture.
You override the strategy through the jacobian keyword, passing the backend itself; solve builds the sparsity patterns and AD caches for its own mesh copy, mode and options.
Linear-solve strategies
| Strategy | Behavior | When to use |
|---|---|---|
DirectSolve() | Materialize the full Jacobian (block + densified structured couplings) and factor it. Exact. | Fixed-mesh default; the exact ALE Jacobian when asked for |
ApproxSolve() | Drop the structured couplings (ALE prefix-sum, χ rank-1, Beer–Lambert suffix-sum; a Schur-style approximation) and solve only the local block. Approximate but much cheaper. | ALE default |
The verifier-only backend DenseAD_ForwardDiff() runs ForwardDiff over the entire residual to produce a dense reference Jacobian. It is correct by construction but slow, and it serves only as the reference of verify_jacobian (§8.6); jacobian accepts only a Structured backend, so passing DenseAD_ForwardDiff() (or a prebuilt JacobianSpec) raises a TypeError.
Examples
Production defaults (no keyword needed):
solve(problem) # Structured(strategy = DirectSolve())
solve(problem; use_ale = true) # Structured(strategy = ApproxSolve())Opt into the exact ALE Jacobian instead of the reduced default:
solve(problem; use_ale = true, jacobian = Structured(strategy = DirectSolve()))The exact Jacobian carries the dense mesh-velocity coupling, which the reduced default drops; both follow the same trajectory, and the exact one costs several times more per step (Technical Reference §15.10.2). It is factored by UMFPACKFactorization() unless you pass a linear_solver (§8.4).
Depletion handling (
handle_depletion = true) keeps the state size and works with either strategy. See §8.9 and Technical Reference §14.3.
8.6 Verifying the Jacobian during a solve
To check that your chosen backend agrees with the dense AD reference at runtime, pass verify_jacobian a NamedTuple. At each listed sample time the solver compares the active Jacobian to a DenseAD_ForwardDiff() reference and warns (it does not throw) if the deviation exceeds tolerance:
solve(problem;
verify_jacobian = (rtol = 1e-6, atol = 1e-10,
sample_times = [0.0, 1.0, 10.0]))Allowed keys are exactly :rtol, :atol, and :sample_times; any other key is rejected. rtol defaults to 1e-6 and atol to 1e-10 if omitted. Use this when diagnosing a "Jacobian mismatch" symptom (slow Newton convergence, unexpected step rejections). It works with depletion handling too. The exact DirectSolve Jacobian is compared entry by entry; the approximate ApproxSolve Jacobian deliberately drops couplings, so it is compared only on its own support, as a sanity check. For an out-of-solve check at a single state point, call verify_jacobian_at directly (see Technical Reference §15 and Chapter 11).
8.7 ALE moving-mesh integration
By default the mesh is Eulerian (fixed). For charring, ablating, swelling, or intumescent materials whose surface recedes or expands, enable the Arbitrary Lagrangian–Eulerian (ALE) formulation, which adds the node positions z to the state vector and moves the mesh with the material:
solve(problem; use_ale = true)With use_ale = true:
- Node positions evolve as state variables, so the domain thickness
L = z[end] − z[1]changes in time and is reported insolution.extras.thickness_history. - A thickness-termination callback stops the integration when the sample thins below
min_thickness(default1e-6m), preventing a singularρc_p → 0. Set it to your physical stopping criterion, e.g.min_thickness = 0.5e-3. - If the material carries a lateral-shrinkage law, the WithChi cross-section state
χis integrated too, and the cross-sectional areaA(t)varies (see §8.12 and Technical Reference §10).
A typical opaque charring cone run:
solve(problem;
use_ale = true,
radiation_model = SURFACE_ABSORPTION,
min_thickness = 0.5e-3, # stop below 0.5 mm
integrator = KenCarp4(linsolve = SparspakFactorization()))For Eulerian problems (use_ale = false), the mesh never moves, the thickness is constant, and solution.z is left empty. ALE node-motion theory and the Geometric Conservation Law are in Technical Reference §11.
8.8 Radiation model selection
The radiation_model keyword selects how incident radiant flux (supplied by a RadiativeFluxBC on the top boundary, see Chapter 5) is deposited into the domain. The three core-path values are:
| Value | Physics | Cost / Jacobian |
|---|---|---|
NO_RADIATION (default) | No volumetric radiative heating | O(1), tridiagonal |
SURFACE_ABSORPTION | Absorbed flux α·Φ applied at the surface (opaque) | O(1), tridiagonal |
BEER_LAMBERT | Exponential in-depth absorption q''' = α·I (semi-transparent) | O(n), non-local upper-triangular Jacobian |
solve(problem; radiation_model = SURFACE_ABSORPTION) # opaque char, wood
solve(problem; radiation_model = BEER_LAMBERT) # semi-transparent polymerNotes and guards (all enforced by solve):
SURFACE_ABSORPTIONwith absorption coefficients set. If your material components carry non-zero mass absorptionα,SURFACE_ABSORPTIONignores them and applies the absorbed flux at the surface;solvewarns. UseBEER_LAMBERTif you want the in-depth profile.BEER_LAMBERTwithout absorption coefficients. If no component has anα,BEER_LAMBERTlets all radiation pass through unabsorbed;solvewarns. Setα(in m²/kg) on the components.BEER_LAMBERTwith a bottomRadiativeFluxBC. The in-depth sweep models top-incident radiation only, and the boundary dispatch zeroes aRadiativeFluxBC's face flux underBEER_LAMBERT— back-side heating would be silently lost, sosolveraises anArgumentError. UseSURFACE_ABSORPTION(face deposit) or aHeatFluxBCfor back-side radiant heating.P1_QUASI_STEADYis not available on the core path. It lives inPyrolysis.Experimental.RadiationP1, andsolveraises an error if you pass it. Drive P1 through the Experimental primitives directly.
Time-varying radiation flux is always tracked
Whenever radiation_model is SURFACE_ABSORPTION or BEER_LAMBERT, solve re-evaluates the incident flux I(t) of the top RadiativeFluxBC at every RHS call — there is no up-front constancy probe, so a ramped, delayed, or modulated cone flux is tracked exactly no matter when it starts varying. The cost is one call of your flux function per RHS evaluation, which is negligible. Just make the RadiativeFluxBC flux a function of t for a time-dependent source:
top_bc = RadiativeFluxBC(flux = t -> 25e3 + 250.0 * t, absorptivity = 0.9) +
RadiativeBC(ε = 0.9, T_inf = 300) +
ConvectiveBC(h = 10, T_inf = 300)Re-radiation losses (RadiativeBC) and convective losses (ConvectiveBC) are ordinary thermal boundary conditions and are applied regardless of radiation_model. The radiation physics and the surface energy balance are derived in Technical Reference §8 and §12.
8.9 Depletion handling and termination
When a layer burns out, its cells shrink (ALE) or empty (fixed mesh) toward a state the implicit integrator cannot resolve — a vanishing cell's equations are conditioned like 1/Δz². Depletion handling parks such cells: it merges each one into a neighbour, conserving mass and sensible enthalpy, and moves its state slot above the live surface as an inert ghost. The state vector, mesh and Jacobian keep their size; the boundary conditions act at the live surface.
solve(problem;
use_ale = true,
handle_depletion = true, # park depleted cells
depletion_threshold = 0.05, # park a cell once it is < 5% of its initial thickness
min_cells = 2, # keep at least 2 live cells; otherwise terminate
min_thickness = 0.01e-3) # terminate when the live thickness < 0.01 mmHow it works:
- After every accepted step a callback scans the live cells. Under ALE a cell is depleted when its thickness is below
depletion_threshold ×its own initial thickness (default0.05, range(0,1); the reference is per cell, so stretched meshes are judged cell-by-cell). On a fixed mesh (use_ale = false) the top live cell is depleted when its condensed concentration has fallen below the same fraction of its initial value. - On a fixed mesh only the top live cell parks. When a cell beneath it becomes a void first, the material has gasified from within: the solve stops with a warning and retcode
:Terminated, and a regressing material belongs on the ALE mesh (§10.2.1). - The depleted cell is merged into its interior neighbour with an energy-conserving temperature merge and a volume-weighted concentration merge, and its slot is parked; one cell per step, surface first.
- When only
min_cellslive cells remain (default2), the next depleted cell terminates the solve with a warning (retcode:Terminated) instead of parking; the thickness-termination callback ends an ALE solve when the live thickness drops belowmin_thickness. - Ghost slots read as
NaNinsol.T,sol.ξandsol.z;sol.extras.live_cellsgives the live count at each output time andsol.extras.park_timesthe event times.
examples/06_cone_calorimeter.jl uses this configuration (handle_depletion = true, depletion_threshold = 0.05). examples/07_douglas_fir_cone.jl instead lets ALE carry the thin cells and stops purely on min_thickness (handle_depletion = false).
Jacobian: depletion handling keeps the structured Jacobian. The ALE default,
Structured(strategy = ApproxSolve()), runs through the parks, andDirectSolvestays available throughjacobian. Theory in Technical Reference §14.3.
8.10 Callbacks and runtime diagnostics
solve assembles a CallbackSet internally; you do not manage most of these. In order, they are: a step-update callback (always present; syncs the integrator state into the mutable mesh), a thickness-termination callback (ALE), a depletion callback (handle_depletion), the Newton-failure step cap (§8.3; integrators that solve their stages by Newton), a progress bar (progress = true), an optional diagnostics callback, an optional Jacobian-verification callback, and finally any user callback you supply.
Diagnostics log
Set diagnostics = true to log a sensible-energy snapshot during the solve. Snapshots are recorded every diagnostics_stride accepted steps (default 10) and are appended to solution.extras.diagnostics_log:
solution = solve(problem; diagnostics = true, diagnostics_stride = 20)
rows = solution.extras.diagnostics_log # Vector{DiagnosticsRow}Each row captures the time t and the sensible energy E_sensible; its cumulative-ledger fields (energy_residual, energy_relative, mass_max_relative_error) are NaN; only record_substep! fills them (§9.8). This is snapshot logging — not a continuous time history. diagnostics_stride must be ≥ 1. See Chapter 9 for reading diagnostics and Technical Reference §16 for the ledgers.
A custom user callback
You may pass one additional callback (any OrdinaryDiffEq callback type) via callback. It runs last in the set:
using OrdinaryDiffEq: ContinuousCallback, terminate! # the callback types
stop_at_T = ContinuousCallback(
(u, t, integ) -> u[1] - 700.0, # back-face cell (index 1) reaches 700 K
integ -> terminate!(integ))
solve(problem; callback = stop_at_T)For automated convergence checks against the dense AD reference, use verify_jacobian (§8.6) rather than a hand-rolled callback.
Progress bar
progress = true (default) shows a percent-of-time-span progress bar, advanced after accepted steps; it leaves the step sequence unchanged. Set progress = false for batch runs and parameter sweeps (the sweep harness does this automatically).
8.11 Warm-starting from a prior solution
To start a new solve from the final state of a previous one — useful for continuation studies and for warm-starting nearby parameter-sweep points — pass the prior PyrolysisSolution as warm_start:
sol1 = solve(problem1)
sol2 = solve(problem2; warm_start = sol1)The prior solution's final raw state vector overwrites the new initial condition. Compatibility is enforced by a state-vector length check, which implicitly requires matching component count, mesh size, ALE mode, and χ mode; mismatches raise a clear error (and the prior solve must have stored its raw solution). A solve that parked cells cannot seed a warm start: its final state holds ghost slots, while a new solve starts with every cell live. Note that tspan[1] of the new problem still sets the integrator's start time — warm-starting treats the prior final state as fresh initial data, it does not continue the clock.
Caution: warm-starting can introduce subtle artifacts in sensitivity studies because the perturbed solve does not begin from the canonical initial condition. Prefer cold starts when computing gradients (Chapter 13).
8.12 Output times, saving, and MLR for variable cross-section
By default solve saves the solution at problem.output_times (set when you built the problem). Override with saveat:
# Save every second over a 600 s run
solve(problem; saveat = collect(0.0:1.0:600.0))saveat accepts a vector of times or a step. Fewer save points reduce interpolation overhead and memory; for the raw dense interpolant use interpolate_solution(solution, t) (Chapter 9).
Mass-loss-rate gauge normalization
For variable cross-section (WithChi) runs, the mass-loss rate (top-face flux plus any back-face venting; the bottom share is zero for the default impermeable holder) is reported two ways in the solution:
solution.mass_loss_rate[i]— the local MLR per current areaA(t), in kg·m⁻²·s⁻¹ (both faces; the bottom term is zero for an impermeable holder).solution.extras.MLR_gauge[i]— the gauge MLR normalized to the initial areaA_0, i.e.MLR_local × A(t)/A_0. This is the cone-calorimeter convention, since a cone gauge measures mass loss per fixed sample footprint.
For constant cross-section (no lateral-shrinkage law) the two coincide because A(t) = A_0. Use MLR_gauge when comparing to cone-calorimeter data on a shrinking or swelling sample. The total domain mass in solution.extras.total_mass is likewise normalized by the initial area. See Technical Reference §10 and §16 for the gauge convention.
8.13 Performance tips and iterative linear solvers
A short checklist for fast, reliable solves:
- Keep the default Jacobian strategy.
solvealways assembles the structured Jacobian, which is dramatically faster than finite-differencing the right-hand side; the default strategy suits each mesh mode (§8.5). - Right-size the mesh. Resolution costs scale super-linearly (smaller cells force smaller diffusion-limited time steps). Use a stretched mesh refined near the surface (
create_stretched_mesh(L, n, Val(NC), 1.1; stretch_from = :top)) instead of a uniformly fine mesh; see Chapter 6 and theconvergence_douglas_firexample. - Under ALE, keep the reduced default.
ApproxSolvedrops the dense ALE couplings and the Newton iteration still converges; passDirectSolveonly to check a configuration against the exact Jacobian (§8.5), or verify withverify_jacobian. - Loosen tolerances for sweeps.
reltol = 1e-4is plenty for trend studies. - Disable the progress bar (
progress = false) and use smallextractclosures in parameter sweeps (Chapter 12).
ILU-preconditioned iterative solvers
For very large ALE problems where direct factorization dominates the cost, two ILU-preconditioned Krylov solvers ship with the package and plug in as the integrator's linear solver:
using Pyrolysis.Solver: ILUGMRESFactorization, ILUBiCGSTABFactorization
# ILU-preconditioned GMRES
solve(problem;
integrator = KenCarp4(linsolve = ILUGMRESFactorization(
τ = 0.1, # ILU drop tolerance
gmres_rtol = 1e-8, # GMRES relative tolerance
restart = 30))) # GMRES restart frequency
# ILU-preconditioned BiCGSTAB (fixed memory, no restart)
solve(problem;
integrator = KenCarp4(linsolve = ILUBiCGSTABFactorization(
τ = 0.1, rtol = 1e-8)))ILUGMRESFactorization and ILUBiCGSTABFactorization live in the Solver submodule — bring them in with using Pyrolysis.Solver: ILUGMRESFactorization, ILUBiCGSTABFactorization (or using Pyrolysis.Internal), or qualify as Pyrolysis.ILUGMRESFactorization. If the ILU factorization becomes singular during a sharp transient, both solvers fall back to diagonal preconditioning automatically.
Trade-off: inexact iterative solves can cause more Newton-step rejections on stiff Arrhenius transients, and
solvewarns when you select ILU-GMRES. Benchmark an iterative configuration end-to-end against the default direct factorization, and reserve it for large ALE meshes where the dense structured coupling makes direct factorization the bottleneck.
Any other LinearSolve factorization is passed the same way, as linear_solver or inside a built integrator (§8.4, Chapter 11). The linear-solve strategies and ILU internals are described in Technical Reference §15.
8.14 Reading the return code
solve always returns a PyrolysisSolution; check solution.retcode to confirm the run finished as intended:
solution = solve(problem)
println("retcode = ", solution.retcode) # e.g. :Success
println("solve time = ", solution.solve_time, " s")Common return codes (from OrdinaryDiffEq):
retcode | Meaning | Typical fix |
|---|---|---|
:Success | Integrated to tspan[2] cleanly | — |
:Terminated | A callback stopped it (thickness/depletion/user) | Expected for charring; widen min_thickness if premature |
:MaxIters | Hit maxiters before the end | Raise maxiters or loosen tolerances |
:DtLessThanMin | Step fell below dtmin | Lower dtmin; check for unphysical states / over-fine mesh |
A :Terminated code is normal and expected for ALE charring runs that deplete the sample. For :MaxIters or :DtLessThanMin, see the troubleshooting guidance in Chapter 15.
8.15 A complete annotated solve call
Putting the pieces together, here is a fully specified solve for an opaque charring cone-calorimeter case with ALE mesh motion (mirroring the shipped Douglas-fir example):
using Pyrolysis
using OrdinaryDiffEqSDIRK: KenCarp4 # not exported by Pyrolysis
using LinearSolve: SparspakFactorization
solution = solve(
problem;
# accuracy
abstol = 1e-9,
reltol = 1e-7,
dtmin = 1e-14, # allow tiny steps through the reaction front
# integrator + linear solver (built-instance form)
integrator = KenCarp4(linsolve = SparspakFactorization()),
# physics
use_ale = true, # moving mesh: surface recedes
radiation_model = SURFACE_ABSORPTION, # opaque: deposit all flux at the surface
min_thickness = 0.5e-3, # stop when the sample is < 0.5 mm
handle_depletion = false, # let ALE carry thin cells (no parking)
# diagnostics + output
diagnostics = true, # log sensible-energy snapshots
saveat = collect(0.0:1.0:600.0), # save once per second
progress = true,
)
@assert solution.retcode in (:Success, :Terminated)The default jacobian = nothing gives the reduced ApproxSolve Jacobian for this ALE problem; for the exact one, add jacobian = Structured(strategy = DirectSolve()).
From here, Chapter 9 covers the PyrolysisSolution object — extracting temperature and concentration profiles, surface and back-face temperatures, mass-loss rate, and plotting.