Adaptivity
Error indicators, monitor functions, r-refinement, mesh-quality metrics, and the surface-depletion machinery. These names live in Pyrolysis.Adaptivity (also re-exported by Pyrolysis.Internal); the production surface-depletion path is enabled through solve keywords. See the User Guide's adaptive-meshing chapter and the Technical Reference's AMR chapter.
Pyrolysis.Adaptivity — Module
AdaptivityAdaptive mesh refinement for the Pyrolysis.jl package.
This module provides:
- Error indicators (gradient, curvature, reaction rate, composite)
- h-refinement (cell splitting)
- h-coarsening (cell merging)
- r-refinement (node relocation via equidistribution)
- Mesh quality metrics (aspect ratio, smoothness, Jacobian)
- AMR controller for orchestrating adaptation
- Cell depletion detection and handling (both surface and interior)
Usage
using Pyrolysis.Adaptivity
# Create error indicator
indicator = GradientIndicator(:T)
# Compute indicators
η = compute_indicator(indicator, mesh)
# Create AMR controller
controller = AMRController(
error_indicator = indicator,
θ_refine = 0.7,
θ_coarsen = 0.1,
h_min = 1e-6,
max_level = 4
)
# Adapt mesh
result = adapt_mesh!(mesh, controller)
# Handle depletion of a thin cell by merging it into a neighbor
neighbor = find_merge_neighbor(mesh, cell_id)
if neighbor > 0
merge_adjacent_cells!(mesh, cell_id, neighbor; material = material)
endPyrolysis.Adaptivity.AspectRatioMetric — Type
AspectRatioMetric <: AbstractQualityMetricLimits the ratio of adjacent cell sizes.
Fields
max_ratio::Float64: Maximum allowed ratio Δzi / Δz{i±1}
Pyrolysis.Adaptivity.CompositeIndicator — Type
CompositeIndicator <: AbstractErrorIndicatorCombines multiple error indicators.
Component indicators carry incommensurable dimensions (a temperature-gradient indicator is in K, a reaction-rate indicator in 1/s, …), so each component is normalized by its own maximum over active cells before combination. The combined η is dimensionless; the component weights act on these normalized scales.
Fields
indicators::Vector{AbstractErrorIndicator}: Component indicatorscombination::Symbol: How to combine (:max, :l2, :sum)
Pyrolysis.Adaptivity.CompositeMonitor — Type
CompositeMonitor <: MonitorFunctionCombines multiple monitor functions.
Fields
monitors::Vector{MonitorFunction}combination::Symbol: :max, :sum, or :product
Pyrolysis.Adaptivity.CompositeQualityMetric — Type
CompositeQualityMetric <: AbstractQualityMetricCombines multiple quality metrics.
Fields
metrics::Vector{AbstractQualityMetric}
Pyrolysis.Adaptivity.CurvatureIndicator — Type
CurvatureIndicator <: AbstractErrorIndicatorCurvature-based error indicator: ηi = hi² · |∇²φ|_i
More sensitive to sharp fronts than gradient indicator.
Fields
variable::Union{Symbol,Int}: Variable to track —:Tfor temperature, or a component indexjto track ξ[j, :]weight::Float64: Weight for combining with other indicators
Pyrolysis.Adaptivity.GradientIndicator — Type
GradientIndicator <: AbstractErrorIndicatorGradient-based error indicator: ηi = hi · |∇φ|_i
Refines where gradients are steep (temperature fronts, reaction zones).
Fields
variable::Union{Symbol,Int}: Variable to track —:Tfor temperature, or a component indexjto track ξ[j, :]weight::Float64: Weight for combining with other indicators
Pyrolysis.Adaptivity.GradientMonitor — Type
GradientMonitor <: MonitorFunctionMonitor based on solution gradient: ω = √(1 + α|∇φ|²)
Fields
α::Float64: Scaling parameter for gradient sensitivityvariable::Symbol: Variable to monitor (:T or component index)
Pyrolysis.Adaptivity.InterfaceMonitor — Type
InterfaceMonitor <: MonitorFunctionMonitor that concentrates nodes near an interface.
ω = ωbase + ωpeak · exp(-(z - z_interface)² / (2σ²))
Fields
z_interface::Float64: Interface positionσ::Float64: Width of concentration zoneω_base::Float64: Base monitor valueω_peak::Float64: Peak monitor value at interface
Pyrolysis.Adaptivity.InterfaceProximityIndicator — Type
InterfaceProximityIndicator <: AbstractErrorIndicatorInterface proximity indicator: ηi = 1 / (1 + dinterface / h_target)
Forces refinement near tracked interfaces.
Fields
interface_position::Float64: Current interface z-positionh_target::Float64: Target cell size near interfaceweight::Float64: Weight for combining with other indicators
Pyrolysis.Adaptivity.JacobianMetric — Type
JacobianMetric <: AbstractQualityMetricEnsures positive cell volumes (Jacobian > 0).
Fields
min_jacobian::Float64: Minimum allowed volume (typically 0 or small positive)
Pyrolysis.Adaptivity.MinCellSizeMetric — Type
MinCellSizeMetric <: AbstractQualityMetricEnsures cells don't become too small.
Fields
h_min::Float64: Minimum allowed cell size
Pyrolysis.Adaptivity.MonitorFunction — Type
MonitorFunctionAbstract type for monitor functions used in r-refinement.
Pyrolysis.Adaptivity.ReactionRateIndicator — Type
ReactionRateIndicator <: AbstractErrorIndicatorReaction rate error indicator: ηi = hi · maxj(|rj|i / max(ξj, ε))
Refines where reactions are active.
Requires per-cell reaction rates: compute_indicator(ind, mesh, T, ξ; rates = r) with r sized like ξ for the current mesh. It therefore cannot run inside the adapt_mesh! cycle (which recomputes indicators after topology changes, when any precomputed rates are stale) — use it standalone, or supply gradient/curvature indicators to the controller instead.
Fields
weight::Float64: Weight for combining with other indicators
Pyrolysis.Adaptivity.SmoothnessMetric — Type
SmoothnessMetric <: AbstractQualityMetricLimits the size jump between adjacent cells.
Fields
max_size_jump::Float64: Maximum allowed |Δzi - Δz{i±1}| / Δz_i
Pyrolysis.Adaptivity.are_cells_adjacent — Method
are_cells_adjacent(mesh::Mesh1D, cell_a_id::Int, cell_b_id::Int) -> BoolCheck if two cells share a face.
Pyrolysis.Adaptivity.assert_amr_invariants — Method
assert_amr_invariants(mesh::Mesh1D)Cheap consistency check on AMR bookkeeping. Throws an AssertionError if n_active_cells does not match the active mask, or if any of the parallel per-cell arrays disagree in length. Intended to be called after every refinement / coarsening step.
Pyrolysis.Adaptivity.check_quality — Function
check_quality(mesh::Mesh1D, metric::AbstractQualityMetric) -> BoolCheck if mesh satisfies quality metric.
Returns
trueif mesh passes quality check,falseotherwise
Pyrolysis.Adaptivity.compact_mesh_after_merge! — Method
compact_mesh_after_merge!(mesh::Mesh1D{NC}, removed_cell_id::Int) where NCRemove an inactive cell and its orphaned node/face from mesh arrays. Works for any cell position (surface, interior, or bottom-adjacent).
This is the generalized version of compact_mesh_after_surface_merge!.
Algorithm
After mergeadjacentcells!, the mesh has:
- One cell marked inactive (the removed cell)
- One orphaned node (not connected to any active cell)
- One orphaned interior face (was between the merged cells)
This function:
- Identifies orphaned elements
- Removes elements using SHIFT (O(N) per element, maintains ordering)
- Renumbers all indices consistently (simple decrement for elements after removed)
- Updates connectivity and geometry
Note: We use shift instead of swap-and-pop to maintain the invariant that cell/node/face indices correspond to spatial ordering (lower index = lower z). This is important for 1D mesh operations that rely on left/right semantics.
Arguments
mesh: Mesh to compact (modified in place)removed_cell_id: ID of the cell that was merged away (should be inactive)
Returns
Nothing. Modifies mesh in place.
Pyrolysis.Adaptivity.compact_mesh_after_surface_merge! — Method
compact_mesh_after_surface_merge!(mesh::Mesh1D{NC}) where NCRemove the inactive surface cell and orphaned surface node/face from mesh arrays.
This function should be called AFTER merge_surface_cell! to actually remove the inactive cell from the arrays. This is necessary because:
merge_surface_cell!only marks the surface cell inactive- The ODE state vector actually removes elements
- The mesh arrays must match the state vector length
Algorithm
After surface merge, the mesh has:
- Surface cell marked inactive (last active becomes new surface)
- Surface node orphaned (not connected to any active cell)
- Interior face between merged cells orphaned
This function:
- Identifies orphaned elements (cell, node, face)
- Removes them from arrays
- Renumbers all indices consistently
- Updates connectivity
- Updates geometry caches
Returns
Nothing. Modifies mesh in place.
Note
This is specifically for surface cell removal where the surface cell is always the highest-indexed active cell. For general compaction with arbitrary inactive cells, a more complex algorithm would be needed.
Pyrolysis.Adaptivity.compute_aspect_ratios — Method
compute_aspect_ratios(mesh::Mesh1D) -> Vector{Float64}Compute aspect ratio for each cell (ratio to smaller neighbor).
Pyrolysis.Adaptivity.compute_cell_gradient — Method
compute_cell_gradient(φ::Vector{Float64}, mesh::Mesh1D, i::Int) -> Float64Compute gradient of field φ at cell i.
Pyrolysis.Adaptivity.compute_curvature_1d — Method
compute_curvature_1d(φ::Vector{Float64}, mesh::Mesh1D, i::Int) -> Float64Compute second derivative of field φ at cell i.
Pyrolysis.Adaptivity.compute_gradient_1d — Method
compute_gradient_1d(φ::Vector{Float64}, mesh::Mesh1D, i::Int) -> Float64Compute gradient of field φ at cell i using central differences.
Pyrolysis.Adaptivity.compute_indicator — Function
compute_indicator(ind::AbstractErrorIndicator, mesh::Mesh1D,
T::Vector{Float64}, ξ::Matrix{Float64}) -> Vector{Float64}Compute error indicator values for all active cells.
Arguments
ind: Error indicator typemesh: 1D meshT: Temperature at cell centers [K]ξ: Mass concentrations [kg/m³], size (NC, n_cells)
Returns
- Vector of indicator values for each cell
Pyrolysis.Adaptivity.compute_indicator — Method
compute_indicator(ind::AbstractErrorIndicator, mesh::Mesh1D; kwargs...) -> Vector{Float64}Convenience method that extracts fields from mesh automatically.
Pyrolysis.Adaptivity.compute_smoothness — Method
compute_smoothness(mesh::Mesh1D) -> Vector{Float64}Compute smoothness metric for each cell.
Pyrolysis.Adaptivity.compute_target_positions — Method
compute_target_positions(mesh::Mesh1D, ω::Vector{Float64}) -> Vector{Float64}Compute target node positions using equidistribution.
Arguments
mesh: Current meshω: Monitor function values at cell centers
Returns
- Vector of target z-positions for interior nodes
Pyrolysis.Adaptivity.enforce_quality! — Method
enforce_quality!(mesh::Mesh1D, metric::AbstractQualityMetric;
max_iter::Int=10) -> IntAttempt to fix quality violations through node smoothing.
Arguments
mesh: Mesh to modifymetric: Quality metric to satisfymax_iter: Maximum smoothing iterations
Returns
- Number of violations remaining after enforcement
Pyrolysis.Adaptivity.evaluate_monitor — Function
evaluate_monitor(mon::MonitorFunction, mesh::Mesh1D,
T::Vector{Float64}, ξ::Matrix{Float64}) -> Vector{Float64}Evaluate monitor function at cell centers.
Pyrolysis.Adaptivity.extract_fields — Method
extract_fields(mesh::Mesh1D{NC}) -> (T::Vector{Float64}, ξ::Matrix{Float64})Extract temperature and concentration fields from mesh cell states.
Pyrolysis.Adaptivity.extract_state_arrays — Method
extract_state_arrays(mesh::Mesh1D{NC}) -> (T, ξ)Extract temperature and concentration arrays from mesh.
Pyrolysis.Adaptivity.find_bottom_cell — Method
find_bottom_cell(mesh::Mesh1D) -> IntFind the ID of the bottom cell (lowest z, active).
This is the cell at z = 0 (substrate side).
Pyrolysis.Adaptivity.find_exterior_neighbor — Method
find_exterior_neighbor(mesh::Mesh1D, cell_id::Int) -> IntFind the neighboring cell toward the exterior (higher z).
For an interior cell, this returns the adjacent cell at higher z. Returns 0 if no exterior neighbor exists (cell is at top boundary).
Pyrolysis.Adaptivity.find_interior_neighbor — Method
find_interior_neighbor(mesh::Mesh1D, cell_id::Int) -> IntFind the neighboring cell toward the interior (lower z).
For a surface cell at z = L, this returns the adjacent cell at lower z. Returns 0 if no interior neighbor exists (cell is at bottom boundary).
Pyrolysis.Adaptivity.find_merge_neighbor — Method
find_merge_neighbor(mesh::Mesh1D, cell_id::Int) -> IntFind the best neighbor to merge with for a thin cell.
Selection strategy:
- If cell has only one neighbor (boundary cell): use that neighbor
- If cell has two neighbors: prefer interior neighbor (toward substrate)
The interior neighbor is preferred for thermal stability since the substrate side typically has more stable thermal conditions.
Arguments
mesh: Current meshcell_id: ID of the thin cell
Returns
Neighbor cell ID, or 0 if no valid neighbor exists
Pyrolysis.Adaptivity.find_shared_face — Method
find_shared_face(mesh::Mesh1D, cell_a_id::Int, cell_b_id::Int) -> IntFind the face shared between two adjacent cells.
Returns
- Face ID of shared face, or 0 if cells are not adjacent
Pyrolysis.Adaptivity.find_surface_cell — Method
find_surface_cell(mesh::Mesh1D) -> IntFind the ID of the current surface cell (highest z, active).
In the coordinate convention:
- z = 0 is the bottom (substrate)
- z = L is the top (exposed surface)
The surface cell is the active cell with the highest z center.
Pyrolysis.Adaptivity.merge_adjacent_cells! — Method
merge_adjacent_cells!(mesh::Mesh1D{NC}, cell_to_remove_id::Int,
cell_to_keep_id::Int; material=nothing) where NC -> IntMerge two adjacent cells, removing one and expanding the other.
This is an extended h-coarsening that works on ANY adjacent pair, not just sibling cells from refinement. The key difference from coarsen_cells! is that we don't require a parent relationship.
Arguments
mesh: The mesh to modifycell_to_remove_id: Cell to remove (typically depleted surface cell)cell_to_keep_id: Cell to expand (will absorb the removed cell's volume/mass)material=nothing: Optional material for energy-conserving temperature merge. If provided, uses ρcₚ-weighted energy conservation. Otherwise, uses simple volume-weighted averaging.
Returns
- ID of the merged cell (same as celltokeep_id)
Algorithm
- Verify cells are adjacent
- Conservative merge: combine states with mass/energy conservation
- Expand the kept cell's geometry to encompass both
- Update face connectivity
- Mark removed cell as inactive
Physical Use Case
During pyrolysis, surface cells become depleted as solid decomposes. When the surface cell is mostly gas, we merge it with the interior neighbor to remove the singularity in dT/dt = Q/(ρcₚ).
Conservation
- Mass: ξmerged = (Va * ξa + Vb * ξb) / Vmerged
- Energy: When
materialis provided, uses ρcₚ-weighted energy conservation. Otherwise, uses simple volume-weighted temperature averaging.
Note
Unlike coarsen_cells!, this function:
- Does NOT require a parent-child relationship
- Can work on any adjacent cell pair
- Is designed for removing depleted cells, not low-error coarsening
Pyrolysis.Adaptivity.merge_surface_cell! — Method
merge_surface_cell!(mesh::Mesh1D{NC}; material=nothing) where NC -> IntConvenience function to merge the depleted surface cell with its interior neighbor.
Arguments
mesh: The mesh to modifymaterial=nothing: Optional material for energy-conserving temperature merge. If provided, uses ρcₚ-weighted energy conservation. Otherwise, uses simple volume-weighted averaging.
Returns
- ID of the merged cell, or 0 if no merge was possible
Usage
using Pyrolysis.Adaptivity
# When surface depletion is detected
if is_surface_depleted(mesh, material, 0.01)
merged_id = merge_surface_cell!(mesh; material=material)
if merged_id > 0
# Update state vector etc.
end
endPyrolysis.Adaptivity.merge_temperatures_conservatively — Method
merge_temperatures_conservatively(V_a::Float64, state_a::CellState{NC},
V_b::Float64, state_b::CellState{NC},
material::Material{NC}) where NC -> Float64Compute the merged temperature by conserving the condensed-phase sensible enthalpy.
Principle
The conserved functional is
H(T) = Σ_{j ∉ gas} m_j ∫_{T_REF}^{T} cₚ,j(T′) dT′ , m_j = V·ξ_jand T_merged solves H_merged(T_merged) = H_a(T_a) + H_b(T_b). Two choices here are deliberate:
- ∫cₚ dT, not cₚ(T)·T. For temperature-dependent cₚ the product form is not an enthalpy: with cₚ = a + bT it carries the b-term with coefficient 1 instead of ½, a few-K systematic bias per merge when a hot surface cell merges into a cooler interior one. The integral form is exact for any cₚ(T) (4-point Gauss–Legendre per component). The datum
T_REFcancels because each component's total mass is identical before and after the merge. - Gas components are excluded. Formulation A stores thermal energy in the condensed matrix only (the gas is thermally quasi-steady), so gas heat capacity does not belong in the merge weighting.
The solve is a bracketed Newton iteration on [min(T_a,T_b), max(T_a,T_b)]; H is strictly monotone (cₚ > 0) so the root is unique and always inside the bracket.
Arguments
V_a: Volume of cell a [m³]state_a: Cell state for cell a (T, ξ)V_b: Volume of cell b [m³]state_b: Cell state for cell b (T, ξ)material: Material definition with component heat capacity functions
Returns
Merged temperature [K] conserving condensed-phase sensible enthalpy.
Edge case
If neither cell carries condensed heat capacity (both essentially pure gas), the condensed energy balance is degenerate and the volume-weighted average is returned.
Pyrolysis.Adaptivity.merge_temperatures_conservatively_from_values — Method
merge_temperatures_conservatively_from_values(V_a::Float64, T_a::Float64, ξ_a::NTuple{NC,Float64},
V_b::Float64, T_b::Float64, ξ_b::NTuple{NC,Float64},
material::Material{NC}) where NC -> Float64Alternate interface for energy-conserving temperature merge using raw values instead of CellState. Useful when working directly with state vectors.
Pyrolysis.Adaptivity.mesh_quality_summary — Method
mesh_quality_summary(mesh::Mesh1D) -> NamedTupleCompute summary statistics of mesh quality.
Returns
NamedTuple with:
n_active_cells: Number of active cellsmin_cell_size: Minimum cell sizemax_cell_size: Maximum cell sizemax_aspect_ratio: Maximum aspect ratiomax_smoothness: Maximum smoothness deviationall_positive_volume: Whether all volumes are positive
Pyrolysis.Adaptivity.quality_violations — Function
quality_violations(mesh::Mesh1D, metric::AbstractQualityMetric) -> Vector{Int}Find cells that violate quality metric.
Returns
- Vector of cell indices that fail the quality check
Pyrolysis.Adaptivity.relocate_nodes! — Method
relocate_nodes!(mesh::Mesh1D{NC}; monitor::MonitorFunction=GradientMonitor(),
max_iter::Int=10, relaxation::Float64=0.5,
tol::Float64=1e-3, h_min::Float64=1e-8) where NCRelocate interior nodes using equidistribution.
Arguments
mesh: Mesh to modify (in-place)monitor: Monitor function for equidistributionmax_iter: Maximum iterationsrelaxation: Relaxation factor (0 < β ≤ 1)tol: Convergence tolerance (max relative node movement)h_min: Minimum allowed cell size
Returns
- Number of iterations performed