Depletion
Burnup matrices, the Bateman equation, and the CRAM solver used by Nucleide.
Nucleide solves nuclide transmutation over a single time step using a burnup matrix formulation and the Chebyshev Rational Approximation Method (CRAM) for the matrix exponential.
Overview
For a vector of nuclide atom densities , the linearized depletion equation is
where is the burnup matrix. Each column of describes how a parent nuclide is lost and what it produces; each row describes how a daughter nuclide is gained.
Matrix construction is in crates/depletion/src/matrix.rs and the CRAM solver
is in crates/depletion/src/cram.rs. The public driver is deplete in
crates/depletion/src/lib.rs.
Building the burnup matrix
Nucleide builds from a Chain and a set of
one-group reaction rates.
Decay
For nuclide with half-life , the decay constant is
The diagonal entry loses atoms at rate :
Each decay mode with branching ratio adds production to the off-diagonal:
A decay entry may omit target (or set target="nothing", the same
convention as target-less reactions): the daughter falls outside the
modeled chain, so the diagonal loss still applies but no gain term is
built — pure loss. An explicit target naming a nuclide absent from the
chain stays a loud error, never silent loss.
Reactions
For a one-group reaction rate of type on nuclide :
- Loss: once per reaction type, even if the chain lists multiple branched entries for that type.
- Production: each reaction entry with branching ratio and target contributes
- Light-particle secondaries: reactions such as , , and emit light nuclides that are tracked when they appear in the chain (e.g. ).
- Fission: if fission yields are present, production is distributed over all fission products:
The final matrix has units of and is sparse, with one diagonal entry per chain nuclide even if it is stable.
CRAM: matrix exponential by partial fractions
CRAM evaluates the action of the matrix exponential on in incomplete partial fraction (IPF) product form:
where for order-16 and for order-48. The coefficients , , and are pre-computed complex numbers published by Pusa. Each pole is applied sequentially to the running solution vector; these IPF coefficients are not interchangeable with the residues of the classic partial-fraction sum form.
Each step is a
sparse linear solve rather than a full matrix exponential. Nucleide uses a
sparse LU factorization from the linalg facade, and the symbolic pattern is
reused across all poles.
Coefficients
Nucleide ships the CRAM-16 and CRAM-48 coefficient sets verbatim. CRAM-16 is cheaper; CRAM-48 is the production default.
Both sets are the incomplete-partial-fraction (IPF) coefficients, not
the residues of the classic partial-fraction sum form — the two forms are
not interchangeable (see the IPF product form above). Order-16 follows
Pusa–Leppänen (2010)pusa-2010; the order-48 set follows
the higher-order treatment in Pusa (2016)pusa-2016. The
code comment in crates/depletion/src/cram.rs cites this as “Pusa & Li”;
that string is imprecise and should name the two references above (with
the exact published table for order-48 once confirmed against the
coefficients — no table number is claimed here). Pinned anchors straight
from the code: (order-16),
(order-48); order-16
pole with
.
The order-16 table below is a full transcription guard; the order-48
anchors are pinned by the
coefficient_tests::pinned_coefficient_anchors transcription-guard test
in crates/depletion/src/cram.rs (recorded in the decision log —
coefficient values must not change).
| k | Re(θ) | Im(θ) | Re(α) | Im(α) |
|---|---|---|---|---|
| 1 | 3.509103608414918 | 8.436198985884374 | 5464.93057687021 | -37979.83575308356 |
| 2 | 5.948152268951177 | 3.587457362018322 | 90.45112476907548 | -1115.537522430261 |
| 3 | -5.264971343442647 | 16.22022147316793 | 234.4818070467641 | -422.8020157070496 |
| 4 | 1.419375897185666 | 10.92536348449672 | 94.53304067358312 | -295.1294291446048 |
| 5 | 6.416177699099435 | 1.194122393370139 | 728.3792954673409 | -120564.6080220011 |
| 6 | 4.993174737717997 | 5.996881713603942 | 36.48229059594851 | -115.5509621409682 |
| 7 | -1.413928462488886 | 13.49772569889275 | 25.47321630156819 | -26.39500283021502 |
| 8 | -10.84391707869699 | 19.27744616718165 | 23.94538338734709 | -5.650522971778156 |
The CRAM-16 scaling constant is .
Analytic Bateman fast path
For decay-only chains the burnup matrix is lower-triangular and the depletion equation admits a closed form (Bateman, Proc. Cambridge Philos. Soc. 15 (1910), 423–427)bateman-1910:
with for stable nuclides. is assembled from the same decay data as (spontaneous-fission daughters skipped, / secondaries from alpha/proton decay included).
When the diagonal entries are distinct and the chain is acyclic, the eigendecomposition with has the unit-diagonal recursion (Amaku–Pascholati–Vanin)amaku-2010
Activity and cumulative-decay observables follow (E8) of
crates/depletion/src/bateman.rs: (implemented by
activity_vec in crates/depletion/src/integrate.rs) with the diagonal
cumulative integral
( limit ; cumulative_decays in
crates/depletion/src/inventory.rs). Labels (E6)–(E7) are intentionally
unassigned in both the code and this page so the two stay consistent; a
code-side close-the-gap renumber is left to the depletion owner (no formula
changes either way).
/ are time-independent, so Nucleide builds them
once per chain (BatemanCache in crates/depletion/src/bateman.rs,
generated at runtime from the crate’s own chain data) and reuses them across
timesteps; each solve is two triangular matvecs. The single-chain Bateman sum
is the special case of this form for a linear chain, and the two/three-nuclide
formulas in the worked example above drop out directly.
Two precision variants share the cache: bateman (plain accumulation) and
bateman_hp (f64-careful summation only — per-row terms sorted by magnitude
and summed with Neumaier compensation, plus exp_m1 forms for small
; no extended-precision types, no new dependencies). On a
synthetic 10-order-spread chain (year-scale parent, microsecond-scale
progeny) the HP variant beats-or-matches standard summation while both
conserve atoms to .
The dispatcher falls back to CRAM-48 — never an error — when the closed
form does not apply: (D1) near-equal decay constants with a live coupling
path (, uncoupled pairs such as
two stables take the limit form instead); (D2) cyclic or
out-of-order decay topology (any gain above the diagonal); (D3) stable
nuclides (, handled inline via limit forms);
(D4) reactions or fission active (assembled matrix differs from the
pure-decay reconstruction). A zero timestep returns the input exactly.
Select the kernel with method="cram16", "cram48", "bateman", or
"bateman_hp" on deplete, deplete_series, DepletionSystem.solve,
and Inventory.decay (default "cram48").
Algorithm flow
Worked example: sequential decay chain
Consider a three-nuclide chain:
with , , and stable. The burnup matrix is
Starting from atoms and , the analytic solution after is
.
.
.
Nucleide’s CRAM-48 result matches this analytic solution to better than one part
in for and one part in for . This exact chain is
exercised in crates/depletion/src/lib.rs as analytic_two_step_decay_cram48.
Time-series integrators
For schedules with per-step rates, crates/depletion/src/integrate.rs
chains single-step CRAM solves. Each Step carries a length dt in seconds
plus prescribed one-group rates in the same layout as
DepletionSystem::build; the matrix is rebuilt from each step’s rates, so
time-varying rates are handled exactly per step.
Integrator selects the method: Predictor (one CRAM solve per step),
Cecm (predictor plus a corrector re-solve from the step start with the
averaged matrix), or Cf4 (fourth-order commutator-free quasi-Magnus, two
half-step CRAM solves). Each step’s exponential additionally honors the
Method kernel: Cram runs the CRAM order named above, while
Bateman/BatemanHp evaluate the analytic closed form per step (steps with
live rates fall back to CRAM-48; CF4 stages scale the timestep rather than
the matrix so per-stage systems stay in decay form). Because rates here are
prescribed rather than recomputed from the predicted composition, the
corrector matrix equals the predictor matrix and Cecm reproduces
Predictor up to roundoff; the two-solve structure is kept so flux-coupled
extensions can plug a recomputed matrix into the same code path. Under the
same piecewise-constant reconstruction each Cf4 stage is a half-step
exponential, so its accuracy is at least as good as Predictor on the
analytic test chain.
Results come back as TimeSeries with one row per time node in
chain-nuclide order (times[0] == 0.0 echoes the initial state):
times: cumulative time nodes in seconds.atoms: atom counts per node.activity: in Bq, clamped at zero.decay_heat: in W, clamped at zero.
Per-nuclide decay energies resolve chain-first: the chain’s decay_energy
attribute (eV converted to MeV) when positive and finite, else the
ENDF/B-VII.1 prompt-decay-energy table
(nucleide_nuclei::data::decay_energy_mev_by_name), else 0.0 (stable and
synthetic nuclides) — one evaluated table, no parallel hand-kept fallback.
nucleide.depletion.deplete_series wraps integrate with an integrator
argument ("predictor", "cecm", "cf4") and a method argument
("cram16", "cram48", "bateman", "bateman_hp"; an explicitly
non-default method overrides order). The per-step decay_heat
maps use the same chain → table → 0.0 resolution as the core, so they
match integrate exactly; the core t = 0 row is omitted. See the
Run depletion tutorial.
Year-length convention
Production code uses the Julian year: 1 yr = 365.25 d = 31,557,600 s
(SECONDS_PER_YEAR in crates/depletion/src/inventory.rs; the y unit in
crates/alara-io/src/schedule.rs and crates/alara-io/src/deck.rs).
Rationale: the Julian year is the standard year-scale interval for
time-unit conversion, and both the depletion solver and the ALARA
schedule/deck layers convert through it.
Two deliberate neighbors differ and must not be “unified” by accident:
crates/fispact-io/src/inventory.rsuses a 365-day year to match the ALARA reference tool’s file-format convention — format fidelity, not physics.- Some test anchors divide by the Gregorian mean year 365.2425 d
( s/yr), e.g. the Cs-137 30.08 yr anchor in the
nucleihalf-life tests. The gap between conventions there is yr against a yr tolerance, so no validation gate moves either way.
Decision (no gate movement): keep the Julian production convention, document
the split here, and migrate test anchors to 31,557,600 s/yr only where the
current tolerance already hides the drift — tolerances themselves stay
untouched. The alara-io and fispact-io year definitions are referenced
from here rather than restated.
Assumptions and limitations
- One-group reaction rates: CRAM in Nucleide uses a single scalar rate per reaction channel. Multigroup fluxes must be collapsed externally.
- Fixed cross sections over the step: is treated as constant during ; multi-step schedules rebuild it from each step’s prescribed rates, with no flux renormalization between steps.
- Fission yields are single-energy: the yield set at the lowest incident neutron energy is used, matching OpenMC’s default.
- Decay branching ratios: chains read from XML keep the file values verbatim; only programmatically built chains are renormalized (the largest branch is adjusted so the modes sum to exactly 1.0).
- Stable nuclides have zero diagonal: they act as sinks in the chain. The Bateman fast path handles them through limit forms ().
- CRAM order: order-16 is adequate for short, simple chains; order-48 is
recommended for production burnup matrices. Pure-decay chains may use the
bateman/bateman_hpclosed form instead (samemethod=selector; automatic CRAM-48 fallback outside decay-only triangular topology). - Bateman conditioning: the closed form sums alternating-sign terms, so
long chains (tens of members) or extreme half-life spreads can suffer
catastrophic cancellation in float64 —
bateman_hp(sorted Neumaier summation) extends the usable range, but ill-conditioned chains should use CRAM-48, which remains the default. The automatic fallback covers non-triangular topology, not ill-conditioning: pick the method deliberately for long chains.
Related work beyond Nucleide
Adjacent capabilities found in PyNE, OpenMC, and the literature that Nucleide does not implement:
- Multi-step time integration: OpenMC ships predictor-corrector
integrators (CECM, CELI, CF4, EPCRK4, and their SI variants) that recompute
reaction rates between steps; Nucleide’s
Integrator::{Predictor, Cecm, Cf4}uses prescribed per-step rates, soCecmcurrently degenerates toPredictor. - Substepping: OpenMC’s CRAM solver can subdivide a step while reusing the LU factorizations; Nucleide has no substep parameter.
- Transport-coupled depletion: OpenMC normalizes tally reaction rates to absolute rates from a power or source normalization; Nucleide assumes rates are already given in .
- Energy-dependent fission yields: OpenMC selects between constant, thermal, and thermal+fast yield modes; Nucleide uses the lowest-energy yield set.
- Feed and removal terms: OpenMC adds transfer rates and external source vectors to the burnup matrix for MSR-style continuous processing.
- Chain truncation: PyNE’s transmutagen-generated solver truncates the transmutation tree by density tolerance; Nucleide uses a fixed chain.
- Krylov subspace methods: an alternative to CRAM for the Bateman equations; see Josey et al. (2017)josey-2017.
- Precomputed decay datasets: the
radioactivedecaypackage ships precomputed decay matrices for fast lookup; Nucleide’sBatemanCachefollows the same time-independent-cache idea but generates the eigendecomposition at runtime from its own chain data (clean-room textbook implementation, no vendored data).