Companion to shimwell/openmc#121 (see also shimwell/openmc#122 and shimwell/openmc#123): once openmc.deplete.Chain.from_endf supports the isomeric_branching and branching_files arguments, the chain generation scripts here can store energy-dependent isomeric production data (ENDF MF=9 yields and MF=10 partial cross sections, kept verbatim) on the chains they build, and supplement nuclides whose primary evaluation lacks MF=8/9/10 data with per-nuclide TENDL files.
Depends on #30 (TENDL-2025 release entry), which should merge first; the diff below applies on top of it.
The complete change is below as a single diff so it can be applied directly (git apply). It was developed and verified end to end: the ENDF-B/VIII.1 chain built with it comes out at 31.7 MB (baseline 27.7 MB), carries verbatim data for 3,193 reaction-target pairs on 408 parents, and loads and validates in unmodified OpenMC 0.15.3. Numbers and spot checks are in shimwell/openmc#121.
Summary of the changes
src/openmc_data/depletion/isomeric_supplement.py (new): helpers to find evaluations that provide an MF=3 cross section for a transmutation reaction without MF=9/10 isomeric data for it (assessed per reaction: ENDF-B/VIII.1 Nb93 has MF=10 for (n,n') but nothing for (n,2n)), to download per-nuclide TENDL files into a cache (404 means no TENDL evaluation and is reported and skipped; metastable targets use the TENDL m/n/o suffixes).
src/openmc_data/urls_chain.py: neutron_file_url templates for per-nuclide downloads on the 2021 and 2025 entries. Note the 2025 release uses neutron_files (plural) in its URL path while 2021 uses neutron_file; both templates were verified live.
src/openmc_data/depletion/generate_endf_chain.py: a single mode flag rather than a separate boolean, with a guard that raises a clear error when the installed openmc predates Chain.from_endf(isomeric_branching=...) (the new kwargs are only passed when the feature is enabled, so --branching-source off keeps working on older openmc):
--branching-source {tendl-2025,tendl-2021,PATH,primary,off} (default: tendl-2025)
a TENDL release or directory of ENDF files supplements nuclides
missing MF=8/9/10 data; 'primary' stores only the primary library's
native data; 'off' stores no isomeric data (historical chains)
--scalar-branching {none,thermal} (default: none)
Supplement downloads go to the script's standard download directory and are skipped when already present (the existing openmc_data.utils.download size check, plus a per-file short circuit), so the user only ever provides the release string.
src/openmc_data/depletion/generate_tendl_chain.py: the same opt-in flag (TENDL is self sufficient, so no supplement logic).
README.md: documentation of the new behaviour.
No log files are written by the scripts, consistent with the other data the chain generation adds; the level-to-isomer mapping report remains available through the isomer_mapping_log argument of Chain.from_endf for anyone who wants it.
One data observation from the full build: 33 reactions carry TENDL MF=9 yields that do not sum to one (worst cases are Pt196/Pt198 capture and Ag110m capture at 1.94). These are source-data defects, stored verbatim by design and reported as warnings by chain.validate.
Diff (on top of #30)
diff --git a/README.md b/README.md
index f209a5a..297e6e4 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,20 @@ A few categories of scripts are available:
|generate_serpent_fissq | | |
|generate_endf71_chain_casl | ENDF/B | |
+By default ``generate_endf_chain`` also stores energy-dependent isomeric
+production data (ENDF MF=9 yields and MF=10 partial cross sections, kept
+verbatim) on the chain, adding metastable reaction targets. Nuclides whose
+primary evaluation lacks MF=8/9/10 data for a transmutation reaction are
+supplemented with per-nuclide files from TENDL (default
+``--branching-source tendl-2025``, cached in ``tendl-branching-cache``).
+Use ``--branching-source off`` to reproduce the historical chains,
+``--branching-source primary`` to store only the primary library's native
+data, and ``--scalar-branching thermal`` to also set the scalar branching
+ratio attributes from the yields at 0.0253 eV. Downloads are cached and
+skipped when already present. Anything other than
+``--branching-source off`` requires a version of openmc whose
+``Chain.from_endf`` supports the ``isomeric_branching`` argument.
+
### Download chain files
| Script name | Library | Release | Branching options|
diff --git a/src/openmc_data/depletion/generate_endf_chain.py b/src/openmc_data/depletion/generate_endf_chain.py
index 8e7c579..f9bab38 100644
--- a/src/openmc_data/depletion/generate_endf_chain.py
+++ b/src/openmc_data/depletion/generate_endf_chain.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import argparse
+import inspect
from pathlib import Path
from urllib.parse import urljoin
@@ -8,6 +9,8 @@ import openmc.deplete
from openmc_data.utils import download, extract
from openmc_data import all_decay_release_details
+from openmc_data.depletion.isomeric_supplement import (
+ deficient_parents, download_supplements)
# Parse command line arguments
@@ -26,6 +29,27 @@ parser.add_argument(
help="filename of the chain file xml file produced. If left as None then "
"the filename will follow this format 'chain_endf_{release}.xml'",
)
+parser.add_argument(
+ "--branching-source",
+ default="tendl-2025",
+ help="Controls the energy-dependent isomeric production data (ENDF "
+ "MF=9 yields and MF=10 partial cross sections, stored verbatim on the "
+ "chain with metastable reaction targets). A TENDL release such as "
+ "'tendl-2025' or 'tendl-2021', or a path to a directory of ENDF files, "
+ "supplements nuclides missing MF=8/9/10 data in the primary release. "
+ "'primary' stores only the primary library's native data. 'off' stores "
+ "no isomeric data, reproducing the historical chains and supporting "
+ "older openmc; anything else requires openmc with "
+ "Chain.from_endf(isomeric_branching=...) support.",
+)
+parser.add_argument(
+ "--scalar-branching",
+ choices=["none", "thermal"],
+ default="none",
+ help="How the scalar branching_ratio attributes are computed for "
+ "reactions with isomeric targets. 'none' keeps the historical values "
+ "(full rate to the ground state).",
+)
args = parser.parse_args()
@@ -70,11 +94,50 @@ def main():
if not flist:
raise IOError(f"No {ftype} endf files found in {endf_files_dir}")
+ from_endf_kwargs = {}
+ source = args.branching_source
+ if source == 'off':
+ if args.scalar_branching != 'none':
+ raise SystemExit(
+ "--branching-source off cannot be combined with "
+ "--scalar-branching")
+ else:
+ if 'isomeric_branching' not in inspect.signature(
+ openmc.deplete.Chain.from_endf).parameters:
+ raise SystemExit(
+ "The installed openmc does not support isomeric branching "
+ "in Chain.from_endf. Update openmc or pass "
+ "--branching-source off.")
+
+ branching_files = []
+ if source != 'primary':
+ print('Identifying evaluations without MF=9/10 isomeric data...')
+ needy = deficient_parents(neutron_files)
+ print(f'{len(needy)} of {len(neutron_files)} evaluations need '
+ 'supplementary isomeric data')
+ if Path(source).is_dir():
+ branching_files = sorted(
+ p for p in Path(source).rglob('*') if p.is_file())
+ elif source.startswith('tendl-'):
+ release = source.split('-', 1)[1]
+ branching_files = download_supplements(
+ needy, release, download_path / f'{source}-branching')
+ else:
+ raise SystemExit(
+ f"Unrecognised --branching-source '{source}'")
+
+ from_endf_kwargs = dict(
+ isomeric_branching=True,
+ branching_files=branching_files,
+ scalar_branching=args.scalar_branching,
+ )
+
chain = openmc.deplete.Chain.from_endf(
decay_files=decay_files,
fpy_files=fpy_files,
neutron_files=neutron_files,
- reactions=list(openmc.deplete.chain.REACTIONS.keys())
+ reactions=list(openmc.deplete.chain.REACTIONS.keys()),
+ **from_endf_kwargs
)
if args.destination is None:
diff --git a/src/openmc_data/depletion/generate_tendl_chain.py b/src/openmc_data/depletion/generate_tendl_chain.py
index 9023650..3d9b9ac 100755
--- a/src/openmc_data/depletion/generate_tendl_chain.py
+++ b/src/openmc_data/depletion/generate_tendl_chain.py
@@ -36,6 +36,16 @@ parser.add_argument(
"version. The currently supported options are 2015, 2017, 2019, "
"2021, and 2025.",
)
+parser.add_argument(
+ "--isomeric-branching",
+ action=argparse.BooleanOptionalAction,
+ default=False,
+ help="Store energy-dependent isomeric production data (ENDF MF=9 yields "
+ "and MF=10 partial cross sections) verbatim on the chain, adding "
+ "metastable reaction targets. TENDL carries this data natively, so no "
+ "supplementary files are needed. Requires openmc with "
+ "Chain.from_endf(isomeric_branching=...) support.",
+)
parser.add_argument(
"-d",
"--destination",
@@ -143,8 +153,21 @@ def main():
extract(nfy_zip, nfy_dir)
nfy_files = list(nfy_dir.rglob("*.endf"))
+ from_endf_kwargs = {}
+ if args.isomeric_branching:
+ import inspect
+ if 'isomeric_branching' not in inspect.signature(
+ dep.Chain.from_endf).parameters:
+ raise SystemExit(
+ "The installed openmc does not support isomeric branching "
+ "in Chain.from_endf. Update openmc or pass "
+ "--no-isomeric-branching.")
+ from_endf_kwargs = dict(isomeric_branching=True)
+
chain = dep.Chain.from_endf(
- decay_files, nfy_files, neutron_files, reactions=dep.chain.REACTIONS.keys()
+ decay_files, nfy_files, neutron_files,
+ reactions=dep.chain.REACTIONS.keys(),
+ **from_endf_kwargs
)
if args.destination is None:
diff --git a/src/openmc_data/depletion/isomeric_supplement.py b/src/openmc_data/depletion/isomeric_supplement.py
new file mode 100644
index 0000000..d8c72f7
--- /dev/null
+++ b/src/openmc_data/depletion/isomeric_supplement.py
@@ -0,0 +1,126 @@
+"""Helpers for obtaining supplementary isomeric branching data.
+
+Chain generation can store energy-dependent isomeric production data
+(ENDF MF=9 yields and MF=10 partial cross sections) verbatim in the
+chain file. General purpose libraries such as ENDF/B provide these
+sections for only a small set of nuclides, so the missing parents are
+supplemented from TENDL, which evaluates them for essentially every
+nuclide. Only the MF=8/9/10 sections of the supplement files are read
+by openmc.deplete.Chain.from_endf, so they cannot change which
+reactions exist in the chain.
+"""
+
+from pathlib import Path
+from urllib.error import HTTPError
+
+import openmc.data
+import openmc.data.endf
+
+from openmc_data.utils import download
+from openmc_data.urls_chain import all_decay_release_details
+
+# TENDL encodes metastable target states with a letter suffix
+_METASTABLE_SUFFIX = {0: '', 1: 'm', 2: 'n', 3: 'o'}
+
+
+def deficient_parents(neutron_files, reactions=None):
+ """Find evaluations lacking MF=9/10 data for some transmutation reaction.
+
+ An evaluation is deficient when it provides a cross section (MF=3)
+ for a transmutation reaction without providing MF=9/10 isomeric
+ production data for it. A parent can therefore be deficient even if
+ it carries isomeric data for other reactions, e.g. the ENDF/B-VIII.1
+ Nb93 evaluation has MF=10 for (n,n') but nothing for (n,2n).
+
+ Parameters
+ ----------
+ neutron_files : iterable of str or pathlib.Path
+ Incident neutron evaluations of the primary library
+ reactions : iterable of str, optional
+ Transmutation reaction names to consider. Defaults to all
+ reactions in openmc.deplete.chain.REACTIONS.
+
+ Returns
+ -------
+ list of str
+ GNDS names of the deficient evaluations
+ """
+ from openmc.deplete.chain import REACTIONS
+ if reactions is None:
+ reactions = list(REACTIONS)
+ mts_sets = [REACTIONS[name].mts for name in reactions]
+
+ names = []
+ for path in neutron_files:
+ evaluation = openmc.data.endf.Evaluation(path)
+ mf3 = {mt for mf, mt in evaluation.section if mf == 3}
+ iso = {mt for mf, mt in evaluation.section if mf in (9, 10)}
+ for mts in mts_sets:
+ if (mts & mf3) and not (mts & iso):
+ names.append(evaluation.gnds_name)
+ break
+ return names
+
+
+def _tendl_token(nuclide):
+ """TENDL file token for a GNDS name, e.g. 'Nb93' becomes 'Nb093'.
+
+ Returns None for metastable states TENDL does not provide.
+ """
+ z, a, m = openmc.data.zam(nuclide)
+ suffix = _METASTABLE_SUFFIX.get(m)
+ if suffix is None:
+ return None
+ return f'{openmc.data.ATOMIC_SYMBOL[z]}{a:03d}{suffix}'
+
+
+def download_supplements(nuclides, release, cache_dir):
+ """Download per-nuclide TENDL evaluations for use as branching files.
+
+ Files already present in ``cache_dir`` are not downloaded again.
+ Nuclides without a TENDL evaluation are reported and skipped.
+
+ Parameters
+ ----------
+ nuclides : iterable of str
+ GNDS names of the parents needing supplementary data
+ release : str
+ TENDL release year, e.g. '2025'
+ cache_dir : str or pathlib.Path
+ Directory in which downloaded files are cached
+
+ Returns
+ -------
+ list of pathlib.Path
+ Paths of the available supplement files
+ """
+ details = all_decay_release_details['tendl'][release]['neutron']
+ url_template = details['neutron_file_url']
+ cache_dir = Path(cache_dir)
+ cache_dir.mkdir(parents=True, exist_ok=True)
+
+ paths = []
+ missing = []
+ for nuclide in sorted(set(nuclides)):
+ token = _tendl_token(nuclide)
+ if token is None:
+ missing.append(nuclide)
+ continue
+ local = cache_dir / f'n-{token}.tendl'
+ if not local.exists():
+ z, _a, _m = openmc.data.zam(nuclide)
+ url = url_template.format(
+ symbol=openmc.data.ATOMIC_SYMBOL[z], token=token)
+ try:
+ download(url, output_path=cache_dir)
+ except HTTPError as err:
+ if err.code == 404:
+ missing.append(nuclide)
+ continue
+ raise
+ paths.append(local)
+
+ if missing:
+ print(f'No TENDL-{release} evaluation for {len(missing)} '
+ 'nuclides: ' + ', '.join(missing))
+ return paths
diff --git a/src/openmc_data/urls_chain.py b/src/openmc_data/urls_chain.py
index 4744730..e3e2486 100644
--- a/src/openmc_data/urls_chain.py
+++ b/src/openmc_data/urls_chain.py
@@ -82,12 +82,14 @@ all_decay_release_details = {
'neutron':{
'base_url': 'https://tendl.imperial.ac.uk/tendl_2021/tar_files/',
'compressed_files': ['TENDL-n.tgz'],
+ 'neutron_file_url': 'https://tendl.imperial.ac.uk/tendl_2021/neutron_file/{symbol}/{token}/lib/endf/n-{token}.tendl',
}
},
'2025': {
'neutron':{
'base_url': 'https://tendl.imperial.ac.uk/tendl_2025/tar_files/',
'compressed_files': ['TENDL-n.tgz'],
+ 'neutron_file_url': 'https://tendl.imperial.ac.uk/tendl_2025/neutron_files/{symbol}/{token}/lib/endf/n-{token}.tendl',
}
}
},
Companion to shimwell/openmc#121 (see also shimwell/openmc#122 and shimwell/openmc#123): once
openmc.deplete.Chain.from_endfsupports theisomeric_branchingandbranching_filesarguments, the chain generation scripts here can store energy-dependent isomeric production data (ENDF MF=9 yields and MF=10 partial cross sections, kept verbatim) on the chains they build, and supplement nuclides whose primary evaluation lacks MF=8/9/10 data with per-nuclide TENDL files.Depends on #30 (TENDL-2025 release entry), which should merge first; the diff below applies on top of it.
The complete change is below as a single diff so it can be applied directly (
git apply). It was developed and verified end to end: the ENDF-B/VIII.1 chain built with it comes out at 31.7 MB (baseline 27.7 MB), carries verbatim data for 3,193 reaction-target pairs on 408 parents, and loads and validates in unmodified OpenMC 0.15.3. Numbers and spot checks are in shimwell/openmc#121.Summary of the changes
src/openmc_data/depletion/isomeric_supplement.py(new): helpers to find evaluations that provide an MF=3 cross section for a transmutation reaction without MF=9/10 isomeric data for it (assessed per reaction: ENDF-B/VIII.1 Nb93 has MF=10 for (n,n') but nothing for (n,2n)), to download per-nuclide TENDL files into a cache (404 means no TENDL evaluation and is reported and skipped; metastable targets use the TENDL m/n/o suffixes).src/openmc_data/urls_chain.py:neutron_file_urltemplates for per-nuclide downloads on the 2021 and 2025 entries. Note the 2025 release usesneutron_files(plural) in its URL path while 2021 usesneutron_file; both templates were verified live.src/openmc_data/depletion/generate_endf_chain.py: a single mode flag rather than a separate boolean, with a guard that raises a clear error when the installed openmc predatesChain.from_endf(isomeric_branching=...)(the new kwargs are only passed when the feature is enabled, so--branching-source offkeeps working on older openmc):Supplement downloads go to the script's standard download directory and are skipped when already present (the existing
openmc_data.utils.downloadsize check, plus a per-file short circuit), so the user only ever provides the release string.src/openmc_data/depletion/generate_tendl_chain.py: the same opt-in flag (TENDL is self sufficient, so no supplement logic).README.md: documentation of the new behaviour.No log files are written by the scripts, consistent with the other data the chain generation adds; the level-to-isomer mapping report remains available through the
isomer_mapping_logargument ofChain.from_endffor anyone who wants it.One data observation from the full build: 33 reactions carry TENDL MF=9 yields that do not sum to one (worst cases are Pt196/Pt198 capture and Ag110m capture at 1.94). These are source-data defects, stored verbatim by design and reported as warnings by
chain.validate.Diff (on top of #30)