OpenMC/tests/unit_tests/test_deplete_microxs.py
Paul Romano 608a1c3386
Some checks failed
Tests and Coverage / filter-changes (push) Has been cancelled
dockerhub-publish-develop / main (push) Has been cancelled
dockerhub-publish-develop-dagmc-libmesh / main (push) Has been cancelled
dockerhub-publish-develop-dagmc / main (push) Has been cancelled
dockerhub-publish-develop-libmesh / main (push) Has been cancelled
Tests and Coverage / Python 3.13 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.14 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.14t (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=n, mpi=n, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=n, mpi=y, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=y, event= (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=, event=y (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=y, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=, libmesh=y, event= (push) Has been cancelled
Tests and Coverage / coverage (push) Has been cancelled
Tests and Coverage / Check CI status (push) Has been cancelled
Fix several issues related to independent operator depletion (#3977)
2026-06-23 09:00:47 +02:00

313 lines
11 KiB
Python

"""Basic unit tests for openmc.deplete.IndependentOperator instantiation
Modifies and resets environment variable OPENMC_CROSS_SECTIONS
to a custom file with new depletion_chain node
"""
from os import remove
from pathlib import Path
from unittest.mock import patch
import pytest
import openmc
from openmc.deplete import MicroXS, get_microxs_and_flux
import numpy as np
ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv"
CHAIN_FILE = Path(__file__).parents[1] / "chain_simple.xml"
def test_from_array():
nuclides = [
'U234',
'U235',
'U238',
'U236',
'O16',
'O17',
'I135',
'Xe135',
'Xe136',
'Cs135',
'Gd157',
'Gd156']
reactions = ['fission', '(n,gamma)']
# These values are placeholders and are not at all
# physically meaningful.
data = np.array([[0.1, 0.],
[0.1, 0.],
[0.9, 0.],
[0.4, 0.],
[0., 0.],
[0., 0.],
[0., 0.1],
[0., 0.9],
[0., 0.],
[0., 0.],
[0., 0.1],
[0., 0.1]])
data.shape = (12, 2, 1)
MicroXS(data, nuclides, reactions)
with pytest.raises(ValueError, match='Data array must be 3D'):
MicroXS(data[:, 0], nuclides, reactions)
def test_csv():
ref_xs = MicroXS.from_csv(ONE_GROUP_XS)
ref_xs.to_csv('temp_xs.csv')
temp_xs = MicroXS.from_csv('temp_xs.csv')
assert np.all(ref_xs.data == temp_xs.data)
remove('temp_xs.csv')
def test_from_multigroup_flux():
energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7]
flux = [1.1e-7, 1.2e-6, 1.3e-5, 1.4e-4]
chain_file = Path(__file__).parents[1] / 'chain_simple.xml'
kwargs = {'multigroup_flux': flux, 'chain_file': chain_file}
# test with energy group structure from string
microxs = MicroXS.from_multigroup_flux(energies='CASMO-4', **kwargs)
assert isinstance(microxs, MicroXS)
# test with energy group structure as floats
microxs = MicroXS.from_multigroup_flux(energies=energies, **kwargs)
assert isinstance(microxs, MicroXS)
# test with nuclides provided
microxs = MicroXS.from_multigroup_flux(
energies=energies, nuclides=['Gd157', 'H1'], **kwargs
)
assert isinstance(microxs, MicroXS)
assert microxs.nuclides == ['Gd157', 'H1']
# test with reactions provided
microxs = MicroXS.from_multigroup_flux(
energies=energies, reactions=['fission', '(n,2n)'], **kwargs
)
assert isinstance(microxs, MicroXS)
assert microxs.reactions == ['fission', '(n,2n)']
def test_multigroup_flux_same():
chain_file = Path(__file__).parents[1] / 'chain_simple.xml'
# Generate micro XS based on 4-group flux
energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7]
flux_per_ev = [0.3, 0.3, 1.0, 1.0]
flux = flux_per_ev * np.diff(energies)
flux_sum = flux.sum()
microxs_4g = MicroXS.from_multigroup_flux(
energies=energies, multigroup_flux=flux, chain_file=chain_file)
# from_multigroup_flux should not modify the flux
assert flux.sum() == flux_sum
# Generate micro XS based on 2-group flux, where the boundaries line up with
# the 4 group flux and have the same flux per eV across the full energy
# range
energies = [0., 5.53e3, 2.0e7]
flux_per_ev = [0.3, 1.0]
flux = flux_per_ev * np.diff(energies)
microxs_2g = MicroXS.from_multigroup_flux(
energies=energies, multigroup_flux=flux, chain_file=chain_file)
assert microxs_4g.data == pytest.approx(microxs_2g.data)
def test_microxs_zero_flux():
chain_file = Path(__file__).parents[1] / 'chain_simple.xml'
# Generate micro XS based on zero flux
energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7]
flux = [0.0, 0.0, 0.0, 0.0]
microxs = MicroXS.from_multigroup_flux(
energies=energies, multigroup_flux=flux, chain_file=chain_file)
# All microscopic cross sections should be zero
assert np.all(microxs.data == 0.0)
def test_hybrid_tally_setup():
"""In hybrid mode a 1-group RR tally is added alongside the flux tally."""
# Create a simple model with one material and a few nuclides for testing
model = openmc.Model()
mat = openmc.Material(components={'U235': 1.0, 'O16': 2.0})
sphere = openmc.Sphere(r=10.0, boundary_type='vacuum')
cell = openmc.Cell(region=-sphere, fill=mat)
model.geometry = openmc.Geometry([cell])
model.settings.batches = 2
model.settings.particles = 10
# Define 2-group energy structure for the test
energies = [0., 0.625, 2.0e7]
# Function to replace Model.run and capture the tallies that were created
captured = {}
def capture_run(**kwargs):
captured['tallies'] = list(model.tallies)
raise StopIteration
# Call get_microxs_and_flux but replace Model.run with a function that
# captures the tallies and raises StopIteration to exit early
with patch.object(model, 'run', side_effect=capture_run):
with pytest.raises(StopIteration):
get_microxs_and_flux(
model, [mat],
nuclides=['U235', 'O16'],
reactions=['fission', '(n,gamma)'],
energies=energies,
reaction_rate_mode='flux',
reaction_rate_opts={'nuclides': ['U235'], 'reactions': ['fission']},
chain_file=CHAIN_FILE,
)
# Check that both tallies were created with the expected properties
tally_names = [t.name for t in captured['tallies']]
assert 'MicroXS flux 0' in tally_names
assert 'MicroXS RR 0' in tally_names
# Check that the RR tally has the expected nuclides and reactions
rr = next(t for t in captured['tallies'] if t.name == 'MicroXS RR 0')
assert rr.nuclides == ['U235']
assert rr.scores == ['fission']
# RR tally must use a 1-group energy filter spanning the full energy range
ef = next(f for f in rr.filters if isinstance(f, openmc.EnergyFilter))
assert len(ef.values) == 2
assert ef.values[0] == pytest.approx(energies[0])
assert ef.values[-1] == pytest.approx(energies[-1])
def _simple_model():
model = openmc.Model()
mat = openmc.Material(components={'H1': 1.0, 'H2': 1.0},
density=5.0, density_units='g/cm3')
sphere = openmc.Sphere(r=10.0, boundary_type='vacuum')
cell = openmc.Cell(region=-sphere, fill=mat)
model.geometry = openmc.Geometry([cell])
model.settings.particles = 100
model.settings.batches = 5
model.settings.run_mode = 'fixed source'
return model, mat
def test_hybrid_tally_defaults_to_all_nuclides(run_in_tmpdir):
energies = [0., 0.625, 2.0e7]
kwargs = {
'nuclides': ['H1', 'H2'],
'reactions': ['(n,2n)', '(n,gamma)'],
'energies': energies,
'reaction_rate_mode': 'flux',
'chain_file': CHAIN_FILE,
}
model, mat = _simple_model()
default_fluxes, default_micros = get_microxs_and_flux(
model, [mat], reaction_rate_opts={'reactions': ['(n,2n)']}, **kwargs
)
model, mat = _simple_model()
explicit_fluxes, explicit_micros = get_microxs_and_flux(
model, [mat],
reaction_rate_opts={
'nuclides': ['H1', 'H2'],
'reactions': ['(n,2n)']
},
**kwargs
)
np.testing.assert_allclose(default_fluxes[0], explicit_fluxes[0])
np.testing.assert_allclose(default_micros[0].data, explicit_micros[0].data)
assert default_micros[0].nuclides == explicit_micros[0].nuclides
assert default_micros[0].reactions == explicit_micros[0].reactions
def test_flux_mode_returns_one_group_flux(run_in_tmpdir):
model, mat = _simple_model()
fluxes, micros = get_microxs_and_flux(
model, [mat],
nuclides=['H1'],
reactions=['(n,2n)'],
energies=[0., 0.625, 2.0e7],
reaction_rate_mode='flux',
chain_file=CHAIN_FILE,
)
assert fluxes[0].shape == (1,)
assert micros[0].data.shape == (1, 1, 1)
assert fluxes[0][0] > 0.0
# ---------------------------------------------------------------------------
# Tests for MicroXS.merge()
# ---------------------------------------------------------------------------
def _make_microxs(nuclides, reactions, values, groups=1):
"""Helper: build a MicroXS from a flat list of values (nuclide-major order)."""
data = np.array(values, dtype=float).reshape(
len(nuclides), len(reactions), groups)
return MicroXS(data, nuclides, reactions)
def test_merge_disjoint():
"""Merging two MicroXS with no overlapping nuclides or reactions."""
m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'],
[1., 2., 3., 4.])
m2 = _make_microxs(['Pu239'], ['(n,2n)'], [5.])
merged = m1.merge(m2)
assert merged.nuclides == ['U235', 'U238', 'Pu239']
assert merged.reactions == ['fission', '(n,gamma)', '(n,2n)']
assert merged.data.shape == (3, 3, 1)
# Self data preserved
assert merged['U235', 'fission'] == pytest.approx([1.])
assert merged['U238', '(n,gamma)'] == pytest.approx([4.])
# New data from other
assert merged['Pu239', '(n,2n)'] == pytest.approx([5.])
# Cross-terms that had no data should be zero
assert merged['U235', '(n,2n)'] == pytest.approx([0.])
assert merged['Pu239', 'fission'] == pytest.approx([0.])
def test_merge_prefer_other():
"""prefer='other': other overwrites shared entries, adds new ones."""
# m1: U235 and U238, reactions fission and (n,gamma)
m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'],
[1., 2., 3., 4.])
# m2: only U235, reactions fission (overlap) and (n,2n) (new)
m2 = _make_microxs(['U235'], ['fission', '(n,2n)'], [9., 5.])
merged = m1.merge(m2)
# Nuclide/reaction sets
assert set(merged.nuclides) == {'U235', 'U238'}
assert set(merged.reactions) == {'fission', '(n,gamma)', '(n,2n)'}
# U235/fission overwritten by m2
assert merged['U235', 'fission'] == pytest.approx([9.])
# U235/(n,gamma) untouched
assert merged['U235', '(n,gamma)'] == pytest.approx([2.])
# New reaction added from m2
assert merged['U235', '(n,2n)'] == pytest.approx([5.])
# U238 data preserved
assert merged['U238', 'fission'] == pytest.approx([3.])
assert merged['U238', '(n,gamma)'] == pytest.approx([4.])
# U238/(n,2n) not in either → zero
assert merged['U238', '(n,2n)'] == pytest.approx([0.])
def test_merge_prefer_self():
"""prefer='self': shared pairs keep self's value; new entries use other's value."""
m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'],
[1., 2., 3., 4.])
m2 = _make_microxs(['U235'], ['fission', '(n,2n)'], [9., 5.])
merged = m1.merge(m2, prefer='self')
# U235/fission: self wins
assert merged['U235', 'fission'] == pytest.approx([1.])
# U235/(n,2n): other used
assert merged['U235', '(n,2n)'] == pytest.approx([5.])