Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/source/io_formats/depletion_chain.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,53 @@ A depletion chain file has a ``<depletion_chain>`` root element with one or more
``<nuclide>`` child elements. The decay, reaction, and fission product data for
each nuclide appears as child elements of ``<nuclide>``.

.. _io_chain_source_metadata:

-----------------------------
``<source_metadata>`` Element
-----------------------------

An optional ``<source_metadata>`` child of ``<depletion_chain>`` records the
ENDF evaluations supplied when constructing the chain. It contains one
``<source>`` element for each distinct library, version, and release in each
component. These source records are separate from the particle-emission
``<source>`` elements inside a ``<nuclide>``.

Each metadata ``<source>`` has the following required attributes:

:component:
``neutron``, ``decay``, or ``fission_yield``.

:library:
ENDF library name, such as ``ENDF/B``.

:version:
Nonnegative integer version from the ENDF evaluation header.

:release:
Nonnegative integer release from the ENDF evaluation header.

For example, a chain constructed from three ENDF/B-VII.1 sublibraries can
contain:

.. code-block:: xml

<source_metadata>
<source component="neutron" library="ENDF/B" version="7" release="1"/>
<source component="decay" library="ENDF/B" version="7" release="1"/>
<source component="fission_yield" library="ENDF/B" version="7" release="1"/>
</source_metadata>

A component may have several records when its inputs come from different
libraries or releases. Missing metadata means the source was not recorded;
it does not imply a particular library. Existing chain files without this
element remain supported.

This is construction provenance, not a selection of transport cross sections
or a record of subsequent processing and manual edits. Chain reduction
preserves these original construction records, including sources for nuclides
that may have been removed.

---------------------
``<nuclide>`` Element
---------------------
Expand Down
29 changes: 29 additions & 0 deletions docs/source/usersguide/depletion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,35 @@ transfers between materials via ``destination_material``. See
:ref:`methods_depletion` for the augmented-matrix formulation used when both
features are active.

Inspecting Depletion-Chain Sources
==================================

:meth:`openmc.deplete.Chain.from_endf` records the library name, version, and
release of the supplied neutron, decay, and fission-product-yield evaluations
separately. The records survive XML export and import::

chain = openmc.deplete.Chain.from_endf(
decay_files, fission_yield_files, neutron_files)
chain.export_to_xml("chain.xml")
reloaded = openmc.deplete.Chain.from_xml("chain.xml")
for component, sources in reloaded.source_metadata.items():
for source in sources:
print(component, source["library"],
source["version"], source["release"])

Each component can contain several source records if its input evaluations
come from different libraries or releases. For a chain file without source
metadata, :attr:`~openmc.deplete.Chain.source_metadata` is an empty dictionary.
The XML representation is described in :ref:`io_chain_source_metadata`.

These records describe the evaluations supplied to construct the chain, not
every later adjustment to branching ratios, fission Q values, or yields.
:meth:`~openmc.deplete.Chain.reduce` retains the original construction records.
They do not select the cross sections used to calculate reaction rates:
transport-coupled depletion uses the model's cross-section library, while
transport-independent depletion uses the supplied microscopic cross sections
and fluxes. Record those inputs separately when comparing calculations.

Comparing to Other Codes
========================

Expand Down
108 changes: 105 additions & 3 deletions openmc/deplete/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

import lxml.etree as ET

from openmc.checkvalue import check_type, check_length, check_greater_than, PathLike
from openmc.checkvalue import (
check_type, check_length, check_greater_than, check_value, PathLike)
from openmc.data import gnds_name, zam
from openmc.exceptions import DataError
from .nuclide import FissionYieldDistribution, Nuclide
Expand Down Expand Up @@ -120,6 +121,41 @@
__all__ = ["Chain", "REACTIONS"]


def _normalize_source_metadata(metadata):
"""Validate and copy source records into a deterministic order."""
check_type('source metadata', metadata, Mapping)
normalized = {}
for component, records in metadata.items():
check_value('source component', component,
('neutron', 'decay', 'fission_yield'))
check_type('source records', records, list)
unique = set()
for record in records:
check_type('source record', record, Mapping)
if set(record) != {'library', 'version', 'release'}:
raise ValueError(
'Source records require library, version, and release')
library = record['library']
check_type('source library', library, str)
if not library.strip():
raise ValueError('Source library must not be empty')
for name in ('version', 'release'):
value = record[name]
check_type(f'source {name}', value, Integral)
if isinstance(value, bool):
raise TypeError(
f'Source {name} must be an integer, not bool')
check_greater_than(f'source {name}', value, 0, equality=True)
unique.add((library, int(record['version']),
int(record['release'])))
if unique:
normalized[component] = [
dict(library=library, version=version, release=release)
for library, version, release in sorted(unique)
]
return dict(sorted(normalized.items()))


def replace_missing(product, decay_data):
"""Replace missing product with suitable decay daughter.

Expand Down Expand Up @@ -262,6 +298,14 @@ class Chain:
Otherwise, an entry can be added for each material to be burned.
Ordering should be identical to how the operator orders reaction
rates for burnable materials.
source_metadata : dict of str to list of dict
Source libraries used to construct the chain, grouped by ``neutron``,
``decay``, and ``fission_yield`` component. Each record contains a
``library`` name and nonnegative integer ``version`` and ``release``.
Multiple records identify mixed-library inputs; missing components have
no recorded source. This describes construction inputs, not a complete
processing history or the transport cross sections used for reaction
rates. Defaults to an empty dictionary.
"""

def __init__(self):
Expand All @@ -270,6 +314,7 @@ def __init__(self):
self.nuclide_dict = {}
self._fission_yields = None
self._decay_matrix = None
self._source_metadata = {}

def __contains__(self, nuclide):
return nuclide in self.nuclide_dict
Expand All @@ -282,6 +327,15 @@ def __len__(self):
"""Number of nuclides in chain."""
return len(self.nuclides)

@property
def source_metadata(self):
"""Source libraries used to construct each component of the chain."""
return self._source_metadata

@source_metadata.setter
def source_metadata(self, metadata):
self._source_metadata = _normalize_source_metadata(metadata)

@property
def stable_nuclides(self) -> List[Nuclide]:
"""List of stable nuclides available in the chain"""
Expand Down Expand Up @@ -357,13 +411,24 @@ def from_endf(cls, decay_files, fpy_files, neutron_files,

"""
transmutation_reactions = reactions
source_metadata = defaultdict(list)

def record_source(component, evaluation):
source = evaluation.info.get('library')
if source is not None:
library, version, release = source
record = dict(library=library, version=version,
release=release)
if record not in source_metadata[component]:
source_metadata[component].append(record)

# Create dictionary mapping target to filename
if progress:
print('Processing neutron sub-library files...')
reactions = {}
for f in neutron_files:
evaluation = openmc.data.endf.as_evaluation(f)
record_source('neutron', evaluation)
name = evaluation.gnds_name
reactions[name] = {}
for mf, mt, nc, mod in evaluation.reaction_list:
Expand All @@ -378,17 +443,21 @@ def from_endf(cls, decay_files, fpy_files, neutron_files,
print('Processing decay sub-library files...')
decay_data = {}
for f in decay_files:
data = openmc.data.Decay(f)
evaluation = openmc.data.endf.as_evaluation(f)
data = openmc.data.Decay(evaluation)
# Skip decay data for neutron itself
if data.nuclide['atomic_number'] == 0:
continue
record_source('decay', evaluation)
decay_data[data.nuclide['name']] = data

if progress:
print('Processing fission product yield sub-library files...')
fpy_data = {}
for f in fpy_files:
data = openmc.data.FissionProductYields(f)
evaluation = openmc.data.endf.as_evaluation(f)
data = openmc.data.FissionProductYields(evaluation)
record_source('fission_yield', evaluation)
fpy_data[data.nuclide['name']] = data

if progress:
Expand All @@ -399,6 +468,7 @@ def from_endf(cls, decay_files, fpy_files, neutron_files,
missing_fp = []

chain = cls()
chain.source_metadata = source_metadata
for idx, parent in enumerate(sorted(decay_data, key=openmc.data.zam)):
data = decay_data[parent]

Expand Down Expand Up @@ -557,6 +627,26 @@ def from_xml(cls, filename, fission_q=None):
# Load XML tree
root = ET.parse(str(filename))

metadata_elements = root.findall('source_metadata')
if len(metadata_elements) > 1:
raise ValueError(
'A depletion chain may have only one source_metadata element')
if metadata_elements:
metadata = defaultdict(list)
for source in metadata_elements[0]:
if not isinstance(source.tag, str):
continue
if (source.tag != 'source' or set(source.attrib) !=
{'component', 'library', 'version', 'release'}):
raise ValueError(
'Invalid depletion chain source metadata record')
metadata[source.get('component')].append({
'library': source.get('library'),
'version': int(source.get('version')),
'release': int(source.get('release')),
})
chain.source_metadata = metadata

for i, nuclide_elem in enumerate(root.findall('nuclide')):
this_q = fission_q.get(get_text(nuclide_elem, "name"))

Expand All @@ -575,7 +665,15 @@ def export_to_xml(self, filename):

"""

# Validate again because callers may modify the metadata in place.
metadata = _normalize_source_metadata(self.source_metadata)
root_elem = ET.Element('depletion_chain')
if metadata:
metadata_elem = ET.SubElement(root_elem, 'source_metadata')
for component, records in metadata.items():
for record in records:
ET.SubElement(metadata_elem, 'source', component=component,
**{k: str(v) for k, v in record.items()})
for nuclide in self.nuclides:
root_elem.append(nuclide.to_xml_element())

Expand Down Expand Up @@ -1227,6 +1325,9 @@ def reduce(self, initial_isotopes, level=None):
total destruction rate and decay rate of included isotopes
will be preserved.

Source metadata is copied from the original chain. It records the
original construction inputs, not only sources for retained nuclides.

Parameters
----------
initial_isotopes : iterable of str
Expand Down Expand Up @@ -1259,6 +1360,7 @@ def reduce(self, initial_isotopes, level=None):
name_sort = sorted(all_isotopes)

new_chain = type(self)()
new_chain.source_metadata = self.source_metadata

for idx, iso in enumerate(sorted(all_isotopes, key=openmc.data.zam)):
previous = self[iso]
Expand Down
Loading
Loading