Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
fe602df
inclusion of the mass attenuation data
marco-de-pietri Dec 2, 2025
6ed9020
ci test
marco-de-pietri Dec 2, 2025
66527ce
ci test 2
marco-de-pietri Dec 2, 2025
f6f8ff7
ci test 3
marco-de-pietri Dec 2, 2025
27df25b
fix in the test_data_mu_en_coefficients.py
marco-de-pietri Dec 2, 2025
cb9a0cf
fix test mu_en
marco-de-pietri Dec 2, 2025
bcca64a
fix test mu_en 2
marco-de-pietri Dec 2, 2025
b9100e7
mu_tests
marco-de-pietri Dec 5, 2025
bc73963
initial structure for gamma contact dose rate
marco-de-pietri Dec 5, 2025
5c594ef
intermediate development of the cdr integrand
marco-de-pietri Dec 8, 2025
0761158
photon attenuation file creation
marco-de-pietri Dec 9, 2025
4eb2efb
photon attenuation intermediate commit
marco-de-pietri Dec 9, 2025
11f6be6
fix typo bug in material.py
marco-de-pietri Dec 10, 2025
705a6d3
fix bug material
marco-de-pietri Dec 10, 2025
e38b15f
unit tests for the linear attenuation
marco-de-pietri Dec 10, 2025
176d2d5
unit tests for the linear attenuation - fix bug
marco-de-pietri Dec 10, 2025
f2afe1f
linear attenuation tests - specific values
marco-de-pietri Dec 10, 2025
e9ac569
linear attenuation tests - specific values - relax criteria
marco-de-pietri Dec 10, 2025
2fa91db
linear attenuation tests - specific values - relax criteria 2
marco-de-pietri Dec 10, 2025
82b7248
fix bug photon_mass_attenuation
marco-de-pietri Dec 11, 2025
9fadb2e
material.photon_mass_attenuation_coefficient tests
marco-de-pietri Dec 11, 2025
2a9cc2d
fix bug in type checking
marco-de-pietri Dec 11, 2025
2d92038
fixed bug in linear attenuation coefficient
marco-de-pietri Dec 11, 2025
bb0db16
adjust tests
marco-de-pietri Dec 11, 2025
2ad9ab3
cobalt test
marco-de-pietri Dec 11, 2025
7602c26
fix bug
marco-de-pietri Dec 11, 2025
40aeb7d
spectrum distribution tests
marco-de-pietri Dec 11, 2025
866dbd3
fix typos and polish
marco-de-pietri Dec 11, 2025
ab7c326
first draft of approximate spectrum fispact style
marco-de-pietri Dec 11, 2025
fe62a93
intermediate commit
marco-de-pietri Dec 12, 2025
d358194
temporary removal of approximate spectrum function
marco-de-pietri Dec 17, 2025
1827ba4
removal of Sum function usage
marco-de-pietri Dec 19, 2025
fd45bdd
Revert "removal of Sum function usage"
marco-de-pietri Dec 19, 2025
a8ff92d
format
marco-de-pietri Dec 21, 2025
d4da233
function name change
marco-de-pietri Dec 29, 2025
582d6b3
added linear att test
marco-de-pietri Dec 29, 2025
52cac3a
fix linear attenuation test
marco-de-pietri Dec 29, 2025
fcae999
definition of the mass_attenuation energy distribution generator
marco-de-pietri Dec 29, 2025
a6ffcbd
fix circular import
marco-de-pietri Dec 29, 2025
dc6f0f9
fix test logic
marco-de-pietri Dec 29, 2025
67d6844
simplified material method for computing the attenuation
marco-de-pietri Dec 29, 2025
e41bfde
first version of the contact gamma dose rate / gamma only
marco-de-pietri Dec 30, 2025
75bf2ce
restored decay file - formatting issue
marco-de-pietri Dec 30, 2025
40657e8
revert small formatting changes in material.py
marco-de-pietri Dec 30, 2025
4b82744
simplification of mass-energy absorption storage of tabulated data
marco-de-pietri Jan 2, 2026
9f12223
removal of temperature dependancy for computing linear attenuation
marco-de-pietri Jan 2, 2026
9d19573
reorganization of mass attenuation material method
marco-de-pietri Jan 2, 2026
8750ace
update of mass attenuation testing
marco-de-pietri Jan 2, 2026
3e75fef
inclusion of reset id function in new tests
marco-de-pietri Jan 7, 2026
8ee9cda
reset auto ids in sphere_model fixture to fix test ID mismatch
marco-de-pietri Jan 7, 2026
803993d
bug with Tabulated1D definitions
marco-de-pietri Jan 15, 2026
999fd58
remove zeros that do not affect the cdr computation
marco-de-pietri Jan 15, 2026
1cfb589
fixed issue with log log interpolation
marco-de-pietri Jan 15, 2026
039724f
finalized interpolation tabular mechanism - tested
marco-de-pietri Jan 15, 2026
173e562
implemented the capability to choose between absorbed dose in air and…
marco-de-pietri Jan 16, 2026
035d8f0
doc string
marco-de-pietri Jan 16, 2026
9206f47
test
marco-de-pietri Jan 17, 2026
ded6652
computation of continuos xs for photons with the plotter module
marco-de-pietri Jan 27, 2026
c09f0fc
inclusion of photon tests
marco-de-pietri Jan 27, 2026
00049dd
fallback if photon mts are missing
marco-de-pietri Jan 27, 2026
9484d66
replacement of material method
marco-de-pietri Jan 28, 2026
6316b4f
update of cdr method
marco-de-pietri Jan 28, 2026
ded4df8
removed prior redundant tests
marco-de-pietri Jan 28, 2026
14a979e
docstring specification
marco-de-pietri Jan 28, 2026
67299e9
fix sum xs bug
marco-de-pietri Jan 28, 2026
825cc3f
relaxed test criteria
marco-de-pietri Jan 29, 2026
66dbbd6
Add mass attenuation coefficients HDF5 file
paulromano Feb 27, 2026
fc1cb66
Initial refactor using mass_attenuation.h5
paulromano Feb 27, 2026
0aea26f
Improve computation of linear attenuation
paulromano Feb 28, 2026
38bb5cd
Rename and restructure into dose/ directory
paulromano Feb 28, 2026
6e9de6c
Cleaning up
paulromano Feb 28, 2026
5f02be7
More clean up
paulromano Mar 2, 2026
1ba567e
Revert changes to plotter.py
paulromano Mar 2, 2026
b8a3426
More clean up
paulromano Mar 2, 2026
bfda81c
More simplification
paulromano Mar 2, 2026
523aff7
Simplify histogram interpolation
paulromano Mar 2, 2026
eb4621c
Variable renaming
paulromano Mar 2, 2026
d511ebd
Update docs
paulromano Mar 2, 2026
06a8dba
Add/update tests
paulromano Mar 2, 2026
8725bc4
Group mass attenuation and mass energy-absorption
paulromano Mar 2, 2026
0f37030
Expand docstring
paulromano Mar 2, 2026
53d7ab8
Copy get_reaction_components
paulromano Mar 2, 2026
b92145b
Merge branch 'develop' into pr/marco-de-pietri/3700
paulromano Mar 2, 2026
530a6d2
Remove unused import
paulromano Mar 2, 2026
80ecc3c
Remove get_reaction_components
paulromano Mar 2, 2026
0f85342
Use NIST SRD 8 to cover all elements
paulromano Mar 3, 2026
93a621a
Update reference values in CDR test
paulromano Mar 3, 2026
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
2 changes: 2 additions & 0 deletions docs/source/pythonapi/data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Core Functions
isotopes
kalbach_slope
linearize
mass_attenuation_coefficient
mass_energy_absorption_coefficient
thin
water_density
zam
Expand Down
4 changes: 3 additions & 1 deletion openmc/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@
from .function import *
from .vectfit import *

from .effective_dose.dose import dose_coefficients
from .dose.dose import dose_coefficients
from .dose.mass_attenuation import \
mass_energy_absorption_coefficient, mass_attenuation_coefficient
File renamed without changes.
File renamed without changes.
Binary file added openmc/data/dose/mass_attenuation.h5
Binary file not shown.
153 changes: 153 additions & 0 deletions openmc/data/dose/mass_attenuation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from pathlib import Path

import numpy as np
import h5py

import openmc.checkvalue as cv
from openmc.data import EV_PER_MEV
from ..data import ATOMIC_NUMBER
from ..function import Tabulated1D

# Embedded NIST-126 data
# Air (Dry Near Sea Level) — NIST Standard Reference Database 126 Table 4 (doi: 10.18434/T4D01F)
# Columns: Energy (MeV), μ_en/ρ (cm^2/g)
_NIST126_AIR = np.array([
[1.00000e-03, 3.599e03],
[1.50000e-03, 1.188e03],
[2.00000e-03, 5.262e02],
[3.00000e-03, 1.614e02],
[3.20290e-03, 1.330e02],
[3.20290e-03, 1.460e02],
[4.00000e-03, 7.636e01],
[5.00000e-03, 3.931e01],
[6.00000e-03, 2.270e01],
[8.00000e-03, 9.446e00],
[1.00000e-02, 4.742e00],
[1.50000e-02, 1.334e00],
[2.00000e-02, 5.389e-01],
[3.00000e-02, 1.537e-01],
[4.00000e-02, 6.833e-02],
[5.00000e-02, 4.098e-02],
[6.00000e-02, 3.041e-02],
[8.00000e-02, 2.407e-02],
[1.00000e-01, 2.325e-02],
[1.50000e-01, 2.496e-02],
[2.00000e-01, 2.672e-02],
[3.00000e-01, 2.872e-02],
[4.00000e-01, 2.949e-02],
[5.00000e-01, 2.966e-02],
[6.00000e-01, 2.953e-02],
[8.00000e-01, 2.882e-02],
[1.00000e00, 2.789e-02],
[1.25000e00, 2.666e-02],
[1.50000e00, 2.547e-02],
[2.00000e00, 2.345e-02],
[3.00000e00, 2.057e-02],
[4.00000e00, 1.870e-02],
[5.00000e00, 1.740e-02],
[6.00000e00, 1.647e-02],
[8.00000e00, 1.525e-02],
[1.00000e01, 1.450e-02],
[1.50000e01, 1.353e-02],
[2.00000e01, 1.311e-02],
])

# Registry of embedded tables: (data_source, material) -> ndarray
# Table shape: (N, 2) with columns [Energy (MeV), μen/ρ (cm^2/g)]
_MUEN_TABLES = {
("nist126", "air"): _NIST126_AIR,
}


def mass_energy_absorption_coefficient(
material: str, data_source: str = "nist126"
) -> Tabulated1D:
"""Return the mass energy-absorption coefficient as a function of energy.

The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is
defined as the fraction of incident photon energy absorbed in a material per
unit mass less the energy carried away by scattered photons. It is obtained
from `NIST Standard Reference Database 126
<https://doi.org/10.18434/T4D01F>`_: X-Ray Mass Attenuation Coefficients.

Parameters
----------
material : {'air'}
Material compound for which to load coefficients.
data_source : {'nist126'}
Source library.

Returns
-------
Tabulated1D
Mass energy-absorption coefficient [cm^2/g] as a function of photon
energy [eV], using log-log interpolation.

"""
cv.check_value("material", material, {"air"})
cv.check_value("data_source", data_source, {"nist126"})

key = (data_source, material)
if key not in _MUEN_TABLES:
available = sorted({m for (ds, m) in _MUEN_TABLES.keys() if ds == data_source})
raise ValueError(
f"No mass energy-absorption data for '{material}' in data source "
f"'{data_source}'. Available materials: {available}"
)

data = _MUEN_TABLES[key]
energy = data[:, 0].copy() * EV_PER_MEV # MeV -> eV
mu_en_coeffs = data[:, 1].copy()
return Tabulated1D(energy, mu_en_coeffs,
breakpoints=[len(energy)], interpolation=[5])


# Used in mass_attenuation_coefficient function as a cache.
# Maps atomic number Z (int) -> Tabulated1D of (mu/rho) [cm^2/g] vs E [eV]
_MASS_ATTENUATION: dict[int, object] = {}


def mass_attenuation_coefficient(element):
"""Return the photon mass attenuation coefficient as a function of energy.

The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is
defined as the fraction of incident photon energy absorbed in a material per
unit mass. Values for each element are obtained from `NIST Standard
Reference Database 8 <https://doi.org/10.18434/T48G6X>`_: XCOM Photon Cross
Sections Database.

Parameters
----------
element : str or int
Element symbol (e.g., 'Fe') or atomic number (e.g., 26).

Returns
-------
Tabulated1D
Mass attenuation coefficient [cm^2/g] as a function of photon energy
[eV], using log-log interpolation.

"""
if not _MASS_ATTENUATION:
data_file = Path(__file__).with_name('mass_attenuation.h5')
with h5py.File(data_file, 'r') as f:
for key, dataset in f.items():
energies, mu_rho = dataset[()] # shape (2, N)
_MASS_ATTENUATION[int(key)] = Tabulated1D(
energies, mu_rho,
breakpoints=[len(energies)],
interpolation=[5] # log-log
)

# Resolve element argument to atomic number
if isinstance(element, str):
if element not in ATOMIC_NUMBER:
raise ValueError(f"'{element}' is not a recognized element symbol")
Z = ATOMIC_NUMBER[element]
else:
Z = int(element)

if Z not in _MASS_ATTENUATION:
raise ValueError(f"No mass attenuation data available for Z={Z}")

return _MASS_ATTENUATION[Z]
191 changes: 189 additions & 2 deletions openmc/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import defaultdict, namedtuple, Counter
from collections.abc import Iterable
from copy import deepcopy
from functools import reduce
from numbers import Real
from pathlib import Path
import re
Expand All @@ -22,8 +23,10 @@
from .utility_funcs import input_path
from . import waste
from openmc.checkvalue import PathLike
from openmc.stats import Univariate, Discrete, Mixture
from openmc.data.data import _get_element_symbol
from openmc.stats import Univariate, Discrete, Mixture, Tabular
from openmc.data.data import _get_element_symbol, JOULE_PER_EV
from openmc.data.function import Tabulated1D
from openmc.data import mass_energy_absorption_coefficient, dose_coefficients


# Units for density supported by OpenMC
Expand Down Expand Up @@ -409,6 +412,190 @@ def get_decay_photon_energy(

return combined

def get_photon_contact_dose_rate(
self,
dose_quantity: str = "absorbed-air",
build_up: float = 2.0,
by_nuclide: bool = False
) -> float | dict[str, float]:
"""Compute the photon contact dose rate (CDR) produced by radioactive decay
of the material.

The contact dose rate is calculated from decay photon energy spectra for
each nuclide in the material, combined with photon mass attenuation data
for the material and the appropriate response function for the dose quantity.
A slab-geometry approximation and a photon build-up factor are used.

Absorbed-air dose:
The approach follows the FISPACT-II manual (UKAEA-CCFE-RE(21)02 - May 2021).
Appendix C.7.1.
This method integrates over the photon energy:

(B/2) * (mu_en_air(E) / mu_material(E)) * E * S(E)

Effective dose:
The approach uses ICRP-116 effective dose coefficients to convert the photon
fluence due to decay photons to effective dose.
This method integrates over the photon energy:

(B/2) * (h_e(E) / mu_material(E)) * S(E)

where:
- mu_en_air(E) is the air mass energy-absorption coefficient,
- mu_material(E) is the photon mass attenuation coefficient of the material,
- S(E) is the photon emission spectrum per atom,
- h_e(E) is the ICRP-116 effective dose coefficient,
- B is the build-up factor,
- E is the photon energy.

Parameters
----------
dose_quantity : {'absorbed-air', 'effective'}, optional
Specifies the dose quantity to be calculated.
The only supported options are 'absorbed-air' which implements the methodology
from FISPACT-II, and 'effective' which uses ICRP-116 effective dose coefficients.
build_up : float, optional. The default value is 2.0 as suggested in the FISPACT-II
manual.
by_nuclide : bool, optional
Specifies if the cdr should be returned for the material as a
whole or per nuclide. Default is False.

Limitations
----------
This method does not implement correction from Bremsstrahlung particles which can be
relevant at close distances.
In addition, it computes the gamma contact dose rate only for the unstable nuclides
for which the radiation source specification is present in the chain file.

Returns
-------
cdr : float or dict[str, float]
Contact Dose Rate due to decay photons.
'absorbed-air': returns the absorbed dose in air [Gy/hr].
'effective': returns the effective dose [Sv/hr].
"""

cv.check_type("by_nuclide", by_nuclide, bool)
cv.check_type("dose_quantity", dose_quantity, str)
cv.check_value("dose_quantity", dose_quantity, {'absorbed-air', 'effective'})
cv.check_type("build_up", build_up, Real)
cv.check_greater_than("build_up", build_up, 0.0)

nuc_densities = self.get_nuclide_atom_densities()
if not nuc_densities:
raise ValueError("Material has no nuclides; cannot compute mass attenuation")

# Collect partial mass densities ρ_i [g/cm³] and elemental mass
# attenuation coefficients µ_i/ρ_i [cm²/g] per nuclide
nuc_attenuation = []
for nuc, atom_density_bcm in nuc_densities.items():
Z = openmc.data.zam(nuc)[0]
mu_over_rho = openmc.data.mass_attenuation_coefficient(Z)
rho_i = (
atom_density_bcm * 1.0e24
* openmc.data.atomic_mass(nuc) / openmc.data.AVOGADRO
)
nuc_attenuation.append((rho_i, mu_over_rho))

# Build union energy grid across all nuclides
mu_e_vals = reduce(np.union1d, [t.x for _, t in nuc_attenuation])

# Build the material linear attenuation coefficient µ_material(E) [cm⁻¹]
# as the sum of ρ_i * (µ_i/ρ_i)(E) over all nuclides
mu_material_vals = np.zeros(len(mu_e_vals))
for rho_i, mu_over_rho in nuc_attenuation:
mu_material_vals += rho_i * mu_over_rho(mu_e_vals)
mu_material = Tabulated1D(
mu_e_vals, mu_material_vals, breakpoints=[len(mu_e_vals)], interpolation=[5])

# CDR computation
cdr = {}

geometry_factor_slab = 0.5

# ancillary conversion factors for clarity
seconds_per_hour = 3600.0
grams_per_kg = 1000.0
sv_per_psv = 1e-12

if dose_quantity == 'absorbed-air':
# mu_en/rho for air [cm²/g] as a function of energy [eV]
response_f = mass_energy_absorption_coefficient("air", data_source="nist126")

# Factor to convert [eV cm²/(b g s)] to [Gy/h]
multiplier = (build_up * geometry_factor_slab * seconds_per_hour
* grams_per_kg * 1e24 * JOULE_PER_EV)

elif dose_quantity == 'effective':
# effective dose as a function of photon fluence [pSv cm²]
response_f_x, response_f_y = dose_coefficients(
"photon", geometry='AP', data_source='icrp116')
response_f = Tabulated1D(response_f_x, response_f_y, breakpoints=[
len(response_f_x)], interpolation=[5])

# Convert [pSv cm²/(b-s)] to [Sv/h]
multiplier = (build_up * geometry_factor_slab * seconds_per_hour
* sv_per_psv * 1e24)

for nuc, nuc_atoms_per_bcm in self.get_nuclide_atom_densities().items():
photon_source_per_atom = openmc.data.decay_photon_energy(nuc)

# nuclides with no contribution
if photon_source_per_atom is None or nuc_atoms_per_bcm <= 0.0:
cdr[nuc] = 0.0
continue

if not isinstance(photon_source_per_atom, (Discrete, Tabular)):
raise ValueError(
f"Unknown decay photon energy data type for nuclide {nuc}"
f"value returned: {type(photon_source_per_atom)}"
)

e_vals = photon_source_per_atom.x
p_vals = photon_source_per_atom.p

# Construct list of energies from (photon source, response function,
# mu_en_air) for clipping to common energy range
e_lists = [e_vals, response_f.x, mu_e_vals]

# clip distributions for values outside the tabulated values
left_bound = max(a.min() for a in e_lists)
right_bound = min(a.max() for a in e_lists)

mask = (e_vals >= left_bound) & (e_vals <= right_bound)
e_vals = e_vals[mask]
p_vals = p_vals[mask]

if isinstance(photon_source_per_atom, Tabular):
# limit the computation to the tabulated mu_en_air range
e_union = reduce(np.union1d, e_lists)
e_union = e_union[(e_union >= left_bound) & (e_union <= right_bound)]
if len(e_union) < 2:
raise ValueError("Not enough overlapping energy points to compute CDR")

# Histogram interpolation: each new point inherits the value of
# the nearest original point to its left
p_vals = p_vals[np.searchsorted(e_vals, e_union, side='right') - 1]
e_vals = e_union

mu_vals = mu_material(e_vals)
if dose_quantity == 'absorbed-air':
# Compute (µ_en_air(E) / µ_material(E)) * E * S(E)
integrand = (response_f(e_vals) / mu_vals) * p_vals * e_vals
elif dose_quantity == 'effective':
# Compute (h_e(E) / µ_material(E)) * S(E)
integrand = (response_f(e_vals) / mu_vals) * p_vals

if isinstance(photon_source_per_atom, Discrete):
cdr_nuc = np.sum(integrand)
elif isinstance(photon_source_per_atom, Tabular):
cdr_nuc = np.trapezoid(integrand, e_vals)

# Compute air-absorbed dose [Gy/h] or effective dose [Sv/h]
cdr[nuc] = float(cdr_nuc * nuc_atoms_per_bcm * multiplier)

return cdr if by_nuclide else sum(cdr.values())

@classmethod
def from_hdf5(cls, group: h5py.Group) -> Material:
"""Create material from HDF5 group
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ include = ['openmc*']
exclude = ['tests*']

[tool.setuptools.package-data]
"openmc.data.effective_dose" = ["**/*.txt"]
"openmc.data.dose" = ["**/*.txt"]
"openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"]
"openmc.lib" = ["libopenmc.dylib", "libopenmc.so"]

Expand Down
Loading
Loading