Skip to content

Store energy-dependent isomeric branching data on generated chains, with TENDL supplements for missing MF=8/9/10 #29

Description

@jon-proximafusion

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',
             }
         }
     },

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions