OpenMC/tests/unit_tests/test_d1s.py
Jon Shimwell ddabe1c8c5
Avoid storing inconsistent sum/sum_sq on summed D1S tallies (#3949)
Co-authored-by: shimwell <mail@jshimwell.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
2026-05-30 03:25:22 +00:00

156 lines
5.5 KiB
Python

from pathlib import Path
from math import exp
import numpy as np
import pytest
import openmc
import openmc.deplete
from openmc.deplete import d1s
CHAIN_PATH = Path(__file__).parents[1] / "chain_ni.xml"
@pytest.fixture
def model():
"""Simple model with natural Ni"""
mat = openmc.Material()
mat.add_element('Ni', 1.0)
geom = openmc.Geometry([openmc.Cell(fill=mat)])
return openmc.Model(geometry=geom)
def test_get_radionuclides(model):
# Check that radionuclides are correct and are unstable
chain = openmc.deplete.Chain.from_xml(CHAIN_PATH)
nuclides = d1s.get_radionuclides(model, chain)
assert sorted(nuclides) == [
'Co58', 'Co60', 'Co61', 'Co62', 'Co64',
'Fe55', 'Fe59', 'Fe61', 'Ni57', 'Ni59', 'Ni63', 'Ni65'
]
for nuc in nuclides:
assert openmc.data.half_life(nuc) is not None
@pytest.mark.parametrize("nuclide", ['Co60', 'Ni63', 'H3', 'Na24', 'K40'])
def test_time_correction_factors(nuclide):
# Irradiation schedule turning unit neutron source on and off
timesteps = [1.0, 1.0, 1.0]
source_rates = [1.0, 0.0, 1.0]
# Compute expected solution
decay_rate = openmc.data.decay_constant(nuclide)
g = exp(-decay_rate)
expected = [0.0, (1 - g), (1 - g)*g, (1 - g)*(1 + g*g)]
# Test against expected solution
tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates)
assert tcf[nuclide] == pytest.approx(expected)
# Make sure all values at first timestep and onward are positive (K40 case
# has very small decay constant that stresses this)
assert np.all(tcf[nuclide][1:] > 0.0)
# Timesteps as a tuple
timesteps = [(1.0, 's'), (1.0, 's'), (1.0, 's')]
tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates)
assert tcf[nuclide] == pytest.approx(expected)
# Test changing units
timesteps = [1.0/60.0, 1.0/60.0, 1.0/60.0]
tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates,
timestep_units='min')
assert tcf[nuclide] == pytest.approx(expected)
def test_prepare_tallies(model):
tally = openmc.Tally()
tally.filters = [openmc.ParticleFilter('photon')]
tally.scores = ['flux']
model.tallies = [tally]
# Check that prepare_tallies adds a ParentNuclideFilter
nuclides = ['Co58', 'Co60', 'Fe55']
d1s.prepare_tallies(model, nuclides, chain_file=CHAIN_PATH)
assert tally.contains_filter(openmc.ParentNuclideFilter)
assert list(tally.filters[-1].bins) == nuclides
# Get rid of parent nuclide filter
tally.filters.pop()
# With no nuclides specified, filter should use get_radionuclides
radionuclides = d1s.get_radionuclides(model, CHAIN_PATH)
d1s.prepare_tallies(model, chain_file=CHAIN_PATH)
assert tally.contains_filter(openmc.ParentNuclideFilter)
assert sorted(tally.filters[-1].bins) == sorted(radionuclides)
assert len(tally.filters) == 2
# calling prepare_tallies twice should not add another ParentNuclideFilter
d1s.prepare_tallies(model, chain_file=CHAIN_PATH)
assert len(tally.filters) == 2
def test_apply_time_correction(run_in_tmpdir):
# Make simple sphere model with elemental Ni
mat = openmc.Material()
mat.add_element('Ni', 1.0)
sphere = openmc.Sphere(r=10.0, boundary_type='vacuum')
cell = openmc.Cell(fill=mat, region=-sphere)
model = openmc.Model()
model.geometry = openmc.Geometry([cell])
model.settings.run_mode = 'fixed source'
model.settings.batches = 3
model.settings.particles = 10
model.settings.photon_transport = True
model.settings.use_decay_photons = True
particle_filter = openmc.ParticleFilter('photon')
tally = openmc.Tally()
tally.filters = [particle_filter]
tally.scores = ['flux']
model.tallies = [tally]
# Prepare tallies for D1S and compute time correction factors
nuclides = d1s.prepare_tallies(model, chain_file=CHAIN_PATH)
factors = d1s.time_correction_factors(nuclides, [1.0e10], [1.0])
# Run OpenMC and get tally result
with openmc.config.patch('chain_file', CHAIN_PATH):
output_path = model.run()
with openmc.StatePoint(output_path) as sp:
tally = sp.tallies[tally.id]
flux = tally.mean.flatten()
# Copy attributes from original tally
tally_filters = list(tally.filters)
tally_sum = tally.sum.copy()
tally_sum_sq = tally.sum_sq.copy()
tally_mean = tally.mean.copy()
tally_std_dev = tally.std_dev.copy()
# Apply TCF and make sure results are consistent
result = d1s.apply_time_correction(tally, factors, sum_nuclides=False)
tcf = np.array([factors[nuc][-1] for nuc in nuclides])
assert result.mean.flatten() == pytest.approx(tcf * flux)
# Make sure summed results match a manual sum
result_summed = d1s.apply_time_correction(tally, factors)
assert result_summed.mean.flatten()[0] == pytest.approx(result.mean.sum())
# Make sure original tally is unchanged
assert tally.filters == tally_filters
assert np.all(tally.sum == tally_sum)
assert np.all(tally.sum_sq == tally_sum_sq)
assert np.all(tally.mean == tally_mean)
assert np.all(tally.std_dev == tally_std_dev)
# Make sure various tally methods work
result.get_values()
result_summed.get_values()
result.get_reshaped_data()
result_summed.get_reshaped_data()
result.get_pandas_dataframe()
result_summed.get_pandas_dataframe()
# The summed tally is derived, so sum/sum_sq are None
assert result_summed.sum is None
assert result_summed.sum_sq is None