You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OpenMC depletion chains currently carry a single, spectrum-averaged branching ratio per transmutation reaction. Chain.from_endf always routes each reaction to the ground-state daughter with branching_ratio=1.0, and metastable splits are only injected afterwards as constants (Chain.set_branch_ratios, driven by the Serpent-derived JSONs in openmc_data). For fusion spectra this is a significant accuracy limitation. Recent FNS decay-heat benchmarking with full energy-dependent isomeric branching in OpenMC deplete (see upstream issue openmc-dev#2757, PR openmc-dev#3885, and the iso-gendf branches on the yrrepy fork) reduced the overall benchmark MAPE from 30.5% to 11%.
The yrrepy implementation keeps the branching data outside the chain: the chain only carries flags, and multigroup GENDF libraries are read at runtime. This issue proposes the complementary route that was parked in openmc-dev#3885: make the chain file fully self-contained by storing the evaluations' isomeric production data verbatim in the chain, so users never need side files. The eventual move of the chain format to HDF5/Arrow absorbs the file-size cost. Runtime consumption of the data is explicitly out of scope here. This issue covers chain production only.
What data, and where it comes from
The quantity to store is the evaluations' own MF=9 isomeric yield functions Y_LFS(E) and MF=10 partial production cross sections sigma_LFS(E), kept verbatim (original energy grids, values, and multi-region interpolation laws). Ratios are never pre-computed or renormalized, because a stored ratio is only exact at its grid points and cannot be re-weighted correctly afterwards. Where an evaluation provides both MF=9 and MF=10 for a state, both are stored. Each table carries provenance (source library, MF, MT).
ENDF-B/VIII.1 alone is not sufficient. Counted from FISPACT-processed libraries (PREPRO folds MF=9 and MF=10, so the counts reflect native content):
Library
Nuclides
With isomeric branching data
Final states
ENDF-B/VIII.1
557
66
114
JEFF-4.0
593
467
4,299
TENDL-2021
2,813
2,290
21,703
ENDF-B/VIII.1 covers only MT 16/17/37/102/103/107, has no (n,n') isomeric data at all, and has zero isomeric data for Nb93, In115, Ag107, Co59, Ni58, Mo92, Cu63, Eu151, Hf179 and Re187, which are exactly the fusion-relevant cases. TENDL supplies MF=9/10 for essentially every nuclide, so the plan is: ENDF-B/VIII.1 native data wins where present, TENDL-2025 fills the gaps, and every table is provenance-tagged.
Facts verified against the real evaluations:
Am241 (n,gamma) is MF=9 with two 9-point tables (INT=3). Thermal split is exactly Y(ground)=0.90, Y(m1)=0.10. The metastable subsection has LFS=2 with ELFS=48630 eV, i.e. the level number is not the isomer number (the bug flagged in Adding isomeric branching to chain openmc-dev/openmc#3885 review).
Nb93 (n,n') is MF=10: a single 34-point lin-lin partial cross section for the m1 state (LFS=1, ELFS=30730 eV), with no ground-state subsection.
In115 (n,gamma) MF=9 gives only the In116_m1 yield (constant 0.79) with the ground share implicit, so per-state yields do not generally sum to 1.
Proposed chain XML format
A new optional child element per <reaction> entry. Old readers parse only the <reaction> attributes and ignore child elements, so the format is backwards compatible. Scalar branching_ratio attributes are unchanged (new metastable entries get 0.0, ground keeps 1.0, and the per-type sum stays 1), so legacy workflows including add_branching_ratios.py behave exactly as today.
level is the verbatim ENDF LFS level number; excitation_energy [eV] comes from MF=8 ELFS when present, else QM minus QI. The target name (_m1 etc.) is resolved separately by excitation-energy matching (below).
A <reaction> may hold several <isomeric_production> children: its own state plus any unmappable levels folded onto the ground entry, each kept verbatim. Consumers sum; the producer never does arithmetic on the data.
<table> siblings with mf=9 and mf=10 are allowed on one level. breakpoints/interpolation follow the Tabulated1D HDF5 convention and preserve multi-region ENDF interpolation exactly.
A grouped per-nuclide element (as in Adding isomeric branching to chain openmc-dev/openmc#3885, or the group-wise embedded variant later abandoned on the yrrepy side) was rejected: it forces a shared re-interpolated energy grid, which is precisely the fidelity loss this design forbids.
API changes (openmc)
New storage in openmc/deplete/nuclide.py (the 4-field ReactionTuple stays untouched since it is unpacked positionally in several places):
Chain.from_endf gains library-agnostic arguments, mirroring the existing decay/fpy/neutron file lists:
Chain.from_endf(decay_files, fpy_files, neutron_files,
reactions=(...),
isomeric_branching=False, # default off, today's behaviorbranching_files=None, # extra ENDF-6 evaluations consulted for MF=8/9/10# where the primary neutron file lacks themscalar_branching='none', # 'none' | 'thermal' | (energies, flux)elis_rtol=0.5,
isomer_mapping_log=None)
Extraction parses MF=8/9/10 from the raw section text already held by openmc.data.endf.Evaluation using the endf-python record readers (get_tab1_record already returns Tabulated1D with breakpoints and interpolation preserved). MF=8 subsections with LMF not in (9, 10) are skipped, which filters the TENDL MT=5 LMF=6 blocks. No endf-python changes are needed.
Level-to-isomer mapping (the openmc-dev#3885 review fix): the decay sublibrary headers already provide (LISO, ELIS) per nuclide. Each production level's excitation energy (MF=8 ELFS, else QM minus QI) is matched to the nearest decay metastable within a relative tolerance (default 0.5), with positional pairing as fallback and fold-to-ground for unmappable levels. Every decision lands in a mapping report file. Targets are never named _m{LFS}.
Chain.reduce carries the data for retained targets, and set_branch_ratios preserves attached tables through scalar rewrites (warning or raising when a rewrite would discard data).
(n,n') support (separate follow-up PR)
Metastable activation by inelastic scattering (In115, Nb93, Pb204 self-loops) needs (n,n') as a chain reaction: REACTIONS["(n,n')"] = ReactionInfo({4}, ()), DADZ entry (0, 0), REACTION_MT entry, and the C++ REACTION_TYPE_MAP entry so existing operators can tally it. Entries are emitted only when isomeric records exist for the parent (no blanket self-loops), and the ground self-loop entry is emitted so the Bateman loss/gain terms stay exact. Kept as its own PR because chains containing (n,n') require an openmc version with the map entries (older versions raise KeyError), which deserves isolated review and release notes.
Only parents that exist in the primary sublibrary but lack MF=9/10 need supplements (roughly 450 to 500 nuclides), so per-nuclide TENDL fetches (about 2 GB cached) are preferred over the multi-GB TENDL-n.tgz tarball, which remains the offline fallback and is already listed in urls.py for tendl 2025. Expected size impact: ENDF-only data adds under 2 MB to the 27.7 MB baseline chain; the TENDL-supplemented chain lands around 45 to 55 MB.
Testing
Round-trip and validation unit tests against the real Am241/Nb93/In115 section contents (values above), including multi-region interpolation, mf=9 plus mf=10 siblings, folded levels, and failure cases (MF=9 yields above 1, negative MF=10, per-type MF=9 sums above 1 when a ground table exists).
ELIS-mapping unit tests: Am242 LFS=2 to m1, Ir192 m1/m2 (56720/168140 eV, no cross-match), positional fallback, unmappable fold, zero-ELIS metastable skip, tolerance boundary.
Integration test (gated on OPENMC_ENDF_DATA) asserting the Am241 0.90/0.10 thermal split and the Nb93 (n,n') 34-point table from the stored chain.
End-to-end: build the ENDF-B/VIII.1 + TENDL-2025 chain, confirm it loads in unmodified upstream OpenMC, chain.validate(strict=True) passes, and spot-check provenance tags.
Implementation plan
PR A (this fork, develop): chain XML schema, nuclide.py storage and XML I/O, validate extension, reduce/set_branch_ratios preservation, MF=8/9/10 extraction and ELIS mapping in Chain.from_endf, mapping report, docs, tests.
PR B (this fork, develop):(n,n') reaction support.
PR C (openmc_data): CLI flags, TENDL-2025 per-nuclide supplement download with caching and tarball fallback, regenerated chain_endf_b8.1_isomeric.xml, CI smoke test.
Known limitations
Only ENDF scheme 1 (MF=8/9/10) is captured. Evaluations that encode isomer production via MF=6 product distributions or discrete-level 600-series MTs are out of scope; TENDL supplements provide scheme-1 data for essentially all nuclides, which closes the practical gap.
MF=10-only reactions (no ground partial, e.g. Nb93 (n,n')) cannot yield an in-chain ratio without an external total cross section. The data is stored verbatim and the combination is left to the consumer. MF=3 is deliberately not embedded.
MF=9 anomalies (per-state sums above 1) are reported by validate but never renormalized in the stored data.
Motivation
OpenMC depletion chains currently carry a single, spectrum-averaged branching ratio per transmutation reaction.
Chain.from_endfalways routes each reaction to the ground-state daughter withbranching_ratio=1.0, and metastable splits are only injected afterwards as constants (Chain.set_branch_ratios, driven by the Serpent-derived JSONs in openmc_data). For fusion spectra this is a significant accuracy limitation. Recent FNS decay-heat benchmarking with full energy-dependent isomeric branching in OpenMC deplete (see upstream issue openmc-dev#2757, PR openmc-dev#3885, and the iso-gendf branches on the yrrepy fork) reduced the overall benchmark MAPE from 30.5% to 11%.The yrrepy implementation keeps the branching data outside the chain: the chain only carries flags, and multigroup GENDF libraries are read at runtime. This issue proposes the complementary route that was parked in openmc-dev#3885: make the chain file fully self-contained by storing the evaluations' isomeric production data verbatim in the chain, so users never need side files. The eventual move of the chain format to HDF5/Arrow absorbs the file-size cost. Runtime consumption of the data is explicitly out of scope here. This issue covers chain production only.
What data, and where it comes from
The quantity to store is the evaluations' own MF=9 isomeric yield functions Y_LFS(E) and MF=10 partial production cross sections sigma_LFS(E), kept verbatim (original energy grids, values, and multi-region interpolation laws). Ratios are never pre-computed or renormalized, because a stored ratio is only exact at its grid points and cannot be re-weighted correctly afterwards. Where an evaluation provides both MF=9 and MF=10 for a state, both are stored. Each table carries provenance (source library, MF, MT).
ENDF-B/VIII.1 alone is not sufficient. Counted from FISPACT-processed libraries (PREPRO folds MF=9 and MF=10, so the counts reflect native content):
ENDF-B/VIII.1 covers only MT 16/17/37/102/103/107, has no (n,n') isomeric data at all, and has zero isomeric data for Nb93, In115, Ag107, Co59, Ni58, Mo92, Cu63, Eu151, Hf179 and Re187, which are exactly the fusion-relevant cases. TENDL supplies MF=9/10 for essentially every nuclide, so the plan is: ENDF-B/VIII.1 native data wins where present, TENDL-2025 fills the gaps, and every table is provenance-tagged.
Facts verified against the real evaluations:
Proposed chain XML format
A new optional child element per
<reaction>entry. Old readers parse only the<reaction>attributes and ignore child elements, so the format is backwards compatible. Scalarbranching_ratioattributes are unchanged (new metastable entries get 0.0, ground keeps 1.0, and the per-type sum stays 1), so legacy workflows includingadd_branching_ratios.pybehave exactly as today.Rules:
levelis the verbatim ENDF LFS level number;excitation_energy[eV] comes from MF=8 ELFS when present, else QM minus QI. The target name (_m1etc.) is resolved separately by excitation-energy matching (below).<reaction>may hold several<isomeric_production>children: its own state plus any unmappable levels folded onto the ground entry, each kept verbatim. Consumers sum; the producer never does arithmetic on the data.<table>siblings with mf=9 and mf=10 are allowed on one level.breakpoints/interpolationfollow theTabulated1DHDF5 convention and preserve multi-region ENDF interpolation exactly.API changes (openmc)
New storage in
openmc/deplete/nuclide.py(the 4-fieldReactionTuplestays untouched since it is unpacked positionally in several places):Chain.from_endfgains library-agnostic arguments, mirroring the existing decay/fpy/neutron file lists:Extraction parses MF=8/9/10 from the raw section text already held by
openmc.data.endf.Evaluationusing the endf-python record readers (get_tab1_recordalready returnsTabulated1Dwith breakpoints and interpolation preserved). MF=8 subsections with LMF not in (9, 10) are skipped, which filters the TENDL MT=5 LMF=6 blocks. No endf-python changes are needed.Level-to-isomer mapping (the openmc-dev#3885 review fix): the decay sublibrary headers already provide (LISO, ELIS) per nuclide. Each production level's excitation energy (MF=8 ELFS, else QM minus QI) is matched to the nearest decay metastable within a relative tolerance (default 0.5), with positional pairing as fallback and fold-to-ground for unmappable levels. Every decision lands in a mapping report file. Targets are never named
_m{LFS}.Chain.reducecarries the data for retained targets, andset_branch_ratiospreserves attached tables through scalar rewrites (warning or raising when a rewrite would discard data).(n,n') support (separate follow-up PR)
Metastable activation by inelastic scattering (In115, Nb93, Pb204 self-loops) needs
(n,n')as a chain reaction:REACTIONS["(n,n')"] = ReactionInfo({4}, ()),DADZentry (0, 0),REACTION_MTentry, and the C++REACTION_TYPE_MAPentry so existing operators can tally it. Entries are emitted only when isomeric records exist for the parent (no blanket self-loops), and the ground self-loop entry is emitted so the Bateman loss/gain terms stay exact. Kept as its own PR because chains containing(n,n')require an openmc version with the map entries (older versions raise KeyError), which deserves isolated review and release notes.openmc_data pipeline
generate_endf_chain.pygrows:Only parents that exist in the primary sublibrary but lack MF=9/10 need supplements (roughly 450 to 500 nuclides), so per-nuclide TENDL fetches (about 2 GB cached) are preferred over the multi-GB
TENDL-n.tgztarball, which remains the offline fallback and is already listed inurls.pyfor tendl 2025. Expected size impact: ENDF-only data adds under 2 MB to the 27.7 MB baseline chain; the TENDL-supplemented chain lands around 45 to 55 MB.Testing
OPENMC_ENDF_DATA) asserting the Am241 0.90/0.10 thermal split and the Nb93 (n,n') 34-point table from the stored chain.chain.validate(strict=True)passes, and spot-check provenance tags.Implementation plan
nuclide.pystorage and XML I/O,validateextension,reduce/set_branch_ratiospreservation, MF=8/9/10 extraction and ELIS mapping inChain.from_endf, mapping report, docs, tests.(n,n')reaction support.chain_endf_b8.1_isomeric.xml, CI smoke test.Known limitations
validatebut never renormalized in the stored data.