Explore cross_section.xml and HDF5 data from ENDF/B-VIII.1,
comparing direct XML/HDF5 inspection with the OpenMC Python API.
Exploring ENDF/B-VIII.1
├── Directory structure
├── Look inside `cross_sections.xml`
│ ├── 1. Manual parsing
│ ├── 2. Using XML parser
│ ├── 3. Using OpenMC
│ └── Comparison of the three XML interfaces
├── Look inside `HDF5` files
│ ├── Using `h5py`
│ ├── Using openmc.data.IncidentNeutron.from_hdf5()
│ └── Interpretation of MT numbers
└── Materials × MT availabilityThe data location is configured in openmc%pip install -e /absolute/path/to/openmc-workbench in a notebook cell, replacing the path with your repository checkout. The default installation is ~/data; edit DATA_ROOT in the shared module to use another location. Restart the kernel after changing the configuration.
Scope of this notebook
This notebook explores how OpenMC nuclear-data libraries are organized and accessed, both directly through their underlying XML/HDF5 files and through the OpenMC Python API.
Imports are introduced at the point of use to highlight the dependencies specific to each plotting approach.
The manual methods are included for exploratory and educational purposes, to expose the underlying file structures and illustrate how the higher-level interfaces work. They are not intended as production implementations.
For production applications, prefer established parsers and the OpenMC API, which provide better abstraction, validation, and compatibility with supported data formats. Direct access to XML/HDF5 remains appropriate when the task specifically requires inspecting or manipulating the underlying data structures.
Directory structure¶
from pathlib import Path
from openmc_workbench_config import ENDF_B81
nuc_dat_path = ENDF_B81
assert nuc_dat_path.is_dir()
for tis in Path(nuc_dat_path).iterdir():
if tis.is_file():
print(tis)
# Top-level directory contains just a xml file../../data/endf-b8.1/cross_sections.xml
This will be the argument for openmc
Assign it to xs_xml.
xs_xml = nuc_dat_path / 'cross_sections.xml'
assert xs_xml.is_file()cross_sections.xml reminds me of MCNP’s xsdir: they provide index lookup.
| MCNP’s xsdir | OpenMC’s cross_sections.xml | |
|---|---|---|
| Index file | xsdir | cross_sections.xml |
| Index format | Plain text | XML |
| Nuclear-data format | ACE | HDF5 |
| Nuclide identification | ZAID | materials attribute |
| Data type | ZAID suffix e.g. .80c | type attribute |
for tis in Path(nuc_dat_path).iterdir():
if tis.is_dir():
print(tis)
# Top-level directory contains these subdirectories.../../data/endf-b8.1/thermal
../../data/endf-b8.1/neutron
../../data/endf-b8.1/photon
max_file_depth = 0
for path in nuc_dat_path.rglob('*'):
if path.is_file():
depth = len(path.relative_to(nuc_dat_path).parts)
max_file_depth = max(max_file_depth, depth)
max_file_depth2Look inside cross_sections.xml¶
Ways of looking inside:
manual parsing line by line;
using XML parser
xml.etree.ElementTree;using OpenMC.
1. Manual parsing¶
The regular expression relies on XML attribute order and whitespace; this is intentional manual parsing.
import re
import pandas as pd # Hard to live without pandas as an ML person.
xs_manual = [] # This will become a dataframe when loop exits, just borrowing a list's efficiency.
with open(xs_xml) as fp:
lines = fp.read().splitlines()
for nline, line in enumerate(lines):
match = re.search(
r' <library materials="(.*)" path="(.*)" type="(.*)" />',
line,
)
if match:
xs_manual.append(match.groups())
else:
print(f'{nline}/{len(lines)}', line)
xs_manual = pd.DataFrame(xs_manual, columns=['materials', 'path', 'type'])
assert ( # `type` consistently agrees with the subdirectory name: no surprises.
xs_manual['path'].map(lambda x: Path(x).parts[0])
== xs_manual['type']
).all()
xs_manual
# Apart from the header row, and the start and end `cross_sections` tags,
# all remaining rows have the same pattern: <library materials=... path=... type=.../>.0/771 <?xml version='1.0' encoding='utf-8'?>
1/771 <cross_sections>
770/771 </cross_sections>
assert xs_manual['path'].map(lambda x: (nuc_dat_path / x).is_file()).all()
# Every single entry corresponds to an HDF5 file (extension `.h5`) in the respective subdirectory.2. Using XML parser¶
import xml.etree.ElementTree as ET
root = ET.parse(xs_xml).getroot()
xs_etree = pd.DataFrame([
element.attrib
for element in root.findall('library')
])
3. Using OpenMC¶
import openmc
library = openmc.data.DataLibrary.from_xml(xs_xml)
for tis in library.libraries:
assert isinstance(tis, dict)
assert {'materials', 'path', 'type'} <= tis.keys()
# Consistent with what we found when looking inside the xml:
# it contains simple length-of-three dictionaries. No nesting.
library.librariesxs_openmc = pd.DataFrame(library.libraries)
xs_openmc['materials'] = xs_openmc['materials'].map(lambda x: x[0])
xs_openmc['path'] = xs_openmc['path'].map(lambda x: str(Path(x).relative_to(nuc_dat_path)))
xs_openmc = xs_openmc[xs_etree.columns]
xs_openmcComparison of the three XML interfaces¶
The three approaches—manual text parsing, Python’s xml.etree.ElementTree, and OpenMC’s DataLibrary—produce equivalent tables for the particular cross_sections.xml file examined here, after normalizing their representations.
This equivalence is not guaranteed for arbitrary XML files. The manual parser relies on the file’s formatting and attribute order. ElementTree interprets XML according to its structure, while OpenMC’s DataLibrary converts certain attributes into Python objects and resolves library paths.
For example, OpenMC represents materials as a list and library paths may be resolved relative to the XML file’s location. The comparison therefore requires converting these values back into the representation used in the original XML file.
The equality assertions validate the results for this dataset; they do not establish that the three methods are universally interchangeable.
# Confirm `xs_manual`==`xs_etree`==`xs_openmc`:
pd.testing.assert_frame_equal(xs_manual, xs_etree)
pd.testing.assert_frame_equal(xs_manual, xs_openmc)
# Rename as `df`, remove dataframes we no longer need.
df = xs_etree
del xs_manual, xs_etree, xs_openmcLook inside HDF5 files¶
HDF5 files are binary files. HDF is an acronym for Hierarchical Data Format. The format is cross platform, with no vendor lock-in. Ways of looking inside:
using
h5py;
Using h5py¶
import h5py
# First, try with a single material.
pic = (df['materials'] == 'Li6') & (df['type'] == 'neutron')
filename = df.loc[pic, 'path'].iloc[0]
with h5py.File(nuc_dat_path / filename) as ff:
print(list(ff.keys()))
ff.visit(print)
# A single nuclide's HDF5 file contains neutron cross sections at multiple temperatures.['Li6']
Li6
Li6/energy
Li6/energy/0K
Li6/energy/1200K
Li6/energy/2500K
Li6/energy/250K
Li6/energy/294K
Li6/energy/600K
Li6/energy/900K
Li6/kTs
Li6/kTs/1200K
Li6/kTs/2500K
Li6/kTs/250K
Li6/kTs/294K
Li6/kTs/600K
Li6/kTs/900K
Li6/reactions
Li6/reactions/reaction_002
Li6/reactions/reaction_002/0K
Li6/reactions/reaction_002/0K/xs
Li6/reactions/reaction_002/1200K
Li6/reactions/reaction_002/1200K/xs
Li6/reactions/reaction_002/2500K
Li6/reactions/reaction_002/2500K/xs
Li6/reactions/reaction_002/250K
Li6/reactions/reaction_002/250K/xs
Li6/reactions/reaction_002/294K
Li6/reactions/reaction_002/294K/xs
Li6/reactions/reaction_002/600K
Li6/reactions/reaction_002/600K/xs
Li6/reactions/reaction_002/900K
Li6/reactions/reaction_002/900K/xs
Li6/reactions/reaction_002/product_0
Li6/reactions/reaction_002/product_0/distribution_0
Li6/reactions/reaction_002/product_0/distribution_0/angle
Li6/reactions/reaction_002/product_0/distribution_0/angle/energy
Li6/reactions/reaction_002/product_0/distribution_0/angle/mu
Li6/reactions/reaction_002/product_0/yield
Li6/reactions/reaction_032
Li6/reactions/reaction_032/1200K
Li6/reactions/reaction_032/1200K/xs
Li6/reactions/reaction_032/2500K
Li6/reactions/reaction_032/2500K/xs
Li6/reactions/reaction_032/250K
Li6/reactions/reaction_032/250K/xs
Li6/reactions/reaction_032/294K
Li6/reactions/reaction_032/294K/xs
Li6/reactions/reaction_032/600K
Li6/reactions/reaction_032/600K/xs
Li6/reactions/reaction_032/900K
Li6/reactions/reaction_032/900K/xs
Li6/reactions/reaction_032/product_0
Li6/reactions/reaction_032/product_0/distribution_0
Li6/reactions/reaction_032/product_0/distribution_0/applicability
Li6/reactions/reaction_032/product_0/distribution_0/energy
Li6/reactions/reaction_032/product_0/distribution_0/energy_out
Li6/reactions/reaction_032/product_0/distribution_0/mu
Li6/reactions/reaction_032/product_0/yield
Li6/reactions/reaction_041
Li6/reactions/reaction_041/1200K
Li6/reactions/reaction_041/1200K/xs
Li6/reactions/reaction_041/2500K
Li6/reactions/reaction_041/2500K/xs
Li6/reactions/reaction_041/250K
Li6/reactions/reaction_041/250K/xs
Li6/reactions/reaction_041/294K
Li6/reactions/reaction_041/294K/xs
Li6/reactions/reaction_041/600K
Li6/reactions/reaction_041/600K/xs
Li6/reactions/reaction_041/900K
Li6/reactions/reaction_041/900K/xs
Li6/reactions/reaction_041/product_0
Li6/reactions/reaction_041/product_0/distribution_0
Li6/reactions/reaction_041/product_0/distribution_0/applicability
Li6/reactions/reaction_041/product_0/yield
Li6/reactions/reaction_052
Li6/reactions/reaction_052/1200K
Li6/reactions/reaction_052/1200K/xs
Li6/reactions/reaction_052/2500K
Li6/reactions/reaction_052/2500K/xs
Li6/reactions/reaction_052/250K
Li6/reactions/reaction_052/250K/xs
Li6/reactions/reaction_052/294K
Li6/reactions/reaction_052/294K/xs
Li6/reactions/reaction_052/600K
Li6/reactions/reaction_052/600K/xs
Li6/reactions/reaction_052/900K
Li6/reactions/reaction_052/900K/xs
Li6/reactions/reaction_052/product_0
Li6/reactions/reaction_052/product_0/distribution_0
Li6/reactions/reaction_052/product_0/distribution_0/angle
Li6/reactions/reaction_052/product_0/distribution_0/angle/energy
Li6/reactions/reaction_052/product_0/distribution_0/angle/mu
Li6/reactions/reaction_052/product_0/distribution_0/applicability
Li6/reactions/reaction_052/product_0/distribution_0/energy
Li6/reactions/reaction_052/product_0/yield
Li6/reactions/reaction_102
Li6/reactions/reaction_102/1200K
Li6/reactions/reaction_102/1200K/xs
Li6/reactions/reaction_102/2500K
Li6/reactions/reaction_102/2500K/xs
Li6/reactions/reaction_102/250K
Li6/reactions/reaction_102/250K/xs
Li6/reactions/reaction_102/294K
Li6/reactions/reaction_102/294K/xs
Li6/reactions/reaction_102/600K
Li6/reactions/reaction_102/600K/xs
Li6/reactions/reaction_102/900K
Li6/reactions/reaction_102/900K/xs
Li6/reactions/reaction_102/product_0
Li6/reactions/reaction_102/product_0/distribution_0
Li6/reactions/reaction_102/product_0/distribution_0/angle
Li6/reactions/reaction_102/product_0/distribution_0/angle/energy
Li6/reactions/reaction_102/product_0/distribution_0/angle/mu
Li6/reactions/reaction_102/product_0/distribution_0/energy
Li6/reactions/reaction_102/product_0/yield
Li6/reactions/reaction_102/product_1
Li6/reactions/reaction_102/product_1/distribution_0
Li6/reactions/reaction_102/product_1/distribution_0/angle
Li6/reactions/reaction_102/product_1/distribution_0/angle/energy
Li6/reactions/reaction_102/product_1/distribution_0/angle/mu
Li6/reactions/reaction_102/product_1/distribution_0/energy
Li6/reactions/reaction_102/product_1/yield
Li6/reactions/reaction_102/product_2
Li6/reactions/reaction_102/product_2/distribution_0
Li6/reactions/reaction_102/product_2/distribution_0/angle
Li6/reactions/reaction_102/product_2/distribution_0/angle/energy
Li6/reactions/reaction_102/product_2/distribution_0/angle/mu
Li6/reactions/reaction_102/product_2/distribution_0/energy
Li6/reactions/reaction_102/product_2/yield
Li6/reactions/reaction_103
Li6/reactions/reaction_103/1200K
Li6/reactions/reaction_103/1200K/xs
Li6/reactions/reaction_103/2500K
Li6/reactions/reaction_103/2500K/xs
Li6/reactions/reaction_103/250K
Li6/reactions/reaction_103/250K/xs
Li6/reactions/reaction_103/294K
Li6/reactions/reaction_103/294K/xs
Li6/reactions/reaction_103/600K
Li6/reactions/reaction_103/600K/xs
Li6/reactions/reaction_103/900K
Li6/reactions/reaction_103/900K/xs
Li6/reactions/reaction_105
Li6/reactions/reaction_105/1200K
Li6/reactions/reaction_105/1200K/xs
Li6/reactions/reaction_105/2500K
Li6/reactions/reaction_105/2500K/xs
Li6/reactions/reaction_105/250K
Li6/reactions/reaction_105/250K/xs
Li6/reactions/reaction_105/294K
Li6/reactions/reaction_105/294K/xs
Li6/reactions/reaction_105/600K
Li6/reactions/reaction_105/600K/xs
Li6/reactions/reaction_105/900K
Li6/reactions/reaction_105/900K/xs
Li6/reactions/reaction_203
Li6/reactions/reaction_203/1200K
Li6/reactions/reaction_203/1200K/xs
Li6/reactions/reaction_203/2500K
Li6/reactions/reaction_203/2500K/xs
Li6/reactions/reaction_203/250K
Li6/reactions/reaction_203/250K/xs
Li6/reactions/reaction_203/294K
Li6/reactions/reaction_203/294K/xs
Li6/reactions/reaction_203/600K
Li6/reactions/reaction_203/600K/xs
Li6/reactions/reaction_203/900K
Li6/reactions/reaction_203/900K/xs
Li6/reactions/reaction_204
Li6/reactions/reaction_204/1200K
Li6/reactions/reaction_204/1200K/xs
Li6/reactions/reaction_204/2500K
Li6/reactions/reaction_204/2500K/xs
Li6/reactions/reaction_204/250K
Li6/reactions/reaction_204/250K/xs
Li6/reactions/reaction_204/294K
Li6/reactions/reaction_204/294K/xs
Li6/reactions/reaction_204/600K
Li6/reactions/reaction_204/600K/xs
Li6/reactions/reaction_204/900K
Li6/reactions/reaction_204/900K/xs
Li6/reactions/reaction_205
Li6/reactions/reaction_205/1200K
Li6/reactions/reaction_205/1200K/xs
Li6/reactions/reaction_205/2500K
Li6/reactions/reaction_205/2500K/xs
Li6/reactions/reaction_205/250K
Li6/reactions/reaction_205/250K/xs
Li6/reactions/reaction_205/294K
Li6/reactions/reaction_205/294K/xs
Li6/reactions/reaction_205/600K
Li6/reactions/reaction_205/600K/xs
Li6/reactions/reaction_205/900K
Li6/reactions/reaction_205/900K/xs
Li6/reactions/reaction_207
Li6/reactions/reaction_207/1200K
Li6/reactions/reaction_207/1200K/xs
Li6/reactions/reaction_207/2500K
Li6/reactions/reaction_207/2500K/xs
Li6/reactions/reaction_207/250K
Li6/reactions/reaction_207/250K/xs
Li6/reactions/reaction_207/294K
Li6/reactions/reaction_207/294K/xs
Li6/reactions/reaction_207/600K
Li6/reactions/reaction_207/600K/xs
Li6/reactions/reaction_207/900K
Li6/reactions/reaction_207/900K/xs
Li6/reactions/reaction_301
Li6/reactions/reaction_301/1200K
Li6/reactions/reaction_301/1200K/xs
Li6/reactions/reaction_301/2500K
Li6/reactions/reaction_301/2500K/xs
Li6/reactions/reaction_301/250K
Li6/reactions/reaction_301/250K/xs
Li6/reactions/reaction_301/294K
Li6/reactions/reaction_301/294K/xs
Li6/reactions/reaction_301/600K
Li6/reactions/reaction_301/600K/xs
Li6/reactions/reaction_301/900K
Li6/reactions/reaction_301/900K/xs
Li6/reactions/reaction_444
Li6/reactions/reaction_444/1200K
Li6/reactions/reaction_444/1200K/xs
Li6/reactions/reaction_444/2500K
Li6/reactions/reaction_444/2500K/xs
Li6/reactions/reaction_444/250K
Li6/reactions/reaction_444/250K/xs
Li6/reactions/reaction_444/294K
Li6/reactions/reaction_444/294K/xs
Li6/reactions/reaction_444/600K
Li6/reactions/reaction_444/600K/xs
Li6/reactions/reaction_444/900K
Li6/reactions/reaction_444/900K/xs
Li6/reactions/reaction_901
Li6/reactions/reaction_901/1200K
Li6/reactions/reaction_901/1200K/xs
Li6/reactions/reaction_901/2500K
Li6/reactions/reaction_901/2500K/xs
Li6/reactions/reaction_901/250K
Li6/reactions/reaction_901/250K/xs
Li6/reactions/reaction_901/294K
Li6/reactions/reaction_901/294K/xs
Li6/reactions/reaction_901/600K
Li6/reactions/reaction_901/600K/xs
Li6/reactions/reaction_901/900K
Li6/reactions/reaction_901/900K/xs
# Next, loop over all materials
def get_reactions(filename):
with h5py.File(nuc_dat_path / filename) as ff:
nuclide_name = list(ff.keys())[0]
reactions = ff[nuclide_name]['reactions']
mts = []
for name in reactions:
if name.startswith('reaction_'):
mt = int(name.removeprefix('reaction_'))
mts.append(mt)
return sorted(mts)
df['MTs'] = None
pic = df['type'] == 'neutron'
df.loc[pic, 'MTs'] = df.loc[pic, 'path'].map(lambda x: get_reactions(x))
dfUsing openmc.data.IncidentNeutron.from_hdf5()¶
# Demonstrating with just one material.
# Looping through every material would be too CPU-expensive.
library.get_by_material("Li6"){'path': '../../data/endf-b8.1/neutron/Li6.h5',
'type': 'neutron',
'materials': ['Li6']}pic = (df['materials'] == 'Li6') & (df['type'] == 'neutron')
filename = f"{nuc_dat_path}/{df.loc[pic, 'path'].values[0]}"
data = openmc.data.IncidentNeutron.from_hdf5(filename)
mts_openmc = set(data.reactions)
mts_openmc{np.int64(2),
np.int64(32),
np.int64(41),
np.int64(52),
np.int64(102),
np.int64(103),
np.int64(105),
np.int64(203),
np.int64(204),
np.int64(205),
np.int64(207),
np.int64(301),
np.int64(444),
np.int64(901)}# That matches what we compiled (manually) from `HDF5`, in `df`.
mts_hdf5 = set(df.loc[pic, 'MTs'].values[0])
assert mts_openmc == mts_hdf5Interpretation of MT numbers¶
ENDF MT numbers identify different types of nuclear-reaction information. Many correspond to physical reaction channels, such as elastic scattering (MT 2), radiative capture (MT 102), and neutron-induced triton production, (n,t) (MT 105).
However, not every MT identifier represents a distinct physical reaction channel. Some identify derived or processed quantities, including heating or energy-deposition information (MT 301) and damage-energy production (MT 444).
Consequently, the list of MT identifiers stored in an OpenMC HDF5 file should not be interpreted simply as a list of independent physical reactions. It may also contain quantities calculated from or associated with nuclear reactions.
def get_reaction_names(mts):
if mts is None:
return None
return [
openmc.data.REACTION_NAME.get(mt, 'Unknown')
for mt in mts
]
df['reaction_names'] = df['MTs'].map(get_reaction_names)
dfMaterials × MT availability¶
Rows are neutron-data materials and columns are MT numbers.
True indicates that the material’s HDF5 file contains that MT; False indicates it does not.
# Expand the MT lists, then pivot into a material-by-MT presence table.
pic = df['type'] == 'neutron'
neutron_mts = df.loc[pic, ['materials', 'MTs']]
neutron_mtsmaterial_mt_pairs = neutron_mts.explode('MTs')
material_mt_pairs.rename(columns={'MTs': 'MT'}, inplace=True)
material_mt_pairs['MT'] = material_mt_pairs['MT'].astype(int)
material_mt_pairs['available'] = True
material_mt_pairsmaterial_mt_table = material_mt_pairs.pivot_table(
index='materials',
columns='MT',
values='available',
aggfunc='any',
fill_value=False,
)
material_mt_table