Skip to content

Commit dab8af5

Browse files
paulromanoshimwell
andauthored
Support flux collapse method in get_microxs_and_flux (#3466)
Co-authored-by: Jonathan Shimwell <drshimwell@gmail.com>
1 parent b939f90 commit dab8af5

9 files changed

Lines changed: 163 additions & 122 deletions

openmc/deplete/microxs.py

Lines changed: 98 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
"""
66

77
from __future__ import annotations
8-
from collections.abc import Iterable, Sequence
8+
from collections.abc import Sequence
9+
import shutil
910
from tempfile import TemporaryDirectory
11+
from typing import Union, TypeAlias
1012

1113
import pandas as pd
1214
import numpy as np
@@ -27,19 +29,39 @@
2729
_valid_rxns.append('damage-energy')
2830

2931

32+
# TODO: Replace with type statement when support is Python 3.12+
33+
DomainTypes: TypeAlias = Union[
34+
Sequence[openmc.Material],
35+
Sequence[openmc.Cell],
36+
Sequence[openmc.Universe],
37+
openmc.MeshBase,
38+
openmc.Filter
39+
]
40+
41+
3042
def get_microxs_and_flux(
31-
model: openmc.Model,
32-
domains,
33-
nuclides: Iterable[str] | None = None,
34-
reactions: Iterable[str] | None = None,
35-
energies: Iterable[float] | str | None = None,
36-
chain_file: PathLike | Chain | None = None,
37-
run_kwargs=None
38-
) -> tuple[list[np.ndarray], list[MicroXS]]:
39-
"""Generate a microscopic cross sections and flux from a Model
43+
model: openmc.Model,
44+
domains: DomainTypes,
45+
nuclides: Sequence[str] | None = None,
46+
reactions: Sequence[str] | None = None,
47+
energies: Sequence[float] | str | None = None,
48+
reaction_rate_mode: str = 'direct',
49+
chain_file: PathLike | Chain | None = None,
50+
path_statepoint: PathLike | None = None,
51+
run_kwargs=None
52+
) -> tuple[list[np.ndarray], list[MicroXS]]:
53+
"""Generate microscopic cross sections and fluxes for multiple domains.
54+
55+
This function runs a neutron transport solve to obtain the flux and reaction
56+
rates in the specified domains and computes multigroup microscopic cross
57+
sections that can be used in depletion calculations with the
58+
:class:`~openmc.deplete.IndependentOperator` class.
4059
4160
.. versionadded:: 0.14.0
4261
62+
.. versionchanged:: 0.15.3
63+
Added `reaction_rate_mode` and `path_statepoint` arguments.
64+
4365
Parameters
4466
----------
4567
model : openmc.Model
@@ -53,12 +75,22 @@ def get_microxs_and_flux(
5375
Reactions to get cross sections for. If not specified, all neutron
5476
reactions listed in the depletion chain file are used.
5577
energies : iterable of float or str
56-
Energy group boundaries in [eV] or the name of the group structure
78+
Energy group boundaries in [eV] or the name of the group structure.
79+
If left as None energies will default to [0.0, 100e6]
80+
reaction_rate_mode : {"direct", "flux"}, optional
81+
Indicate how reaction rates should be calculated. The "direct" method
82+
tallies reaction rates directly. The "flux" method tallies a multigroup
83+
flux spectrum and then collapses multigroup reaction rates after a
84+
transport solve (with an option to tally some reaction rates directly).
5785
chain_file : PathLike or Chain, optional
5886
Path to the depletion chain XML file or an instance of
5987
openmc.deplete.Chain. Used to determine cross sections for materials not
6088
present in the inital composition. Defaults to
6189
``openmc.config['chain_file']``.
90+
path_statepoint : path-like, optional
91+
Path to write the statepoint file from the neutron transport solve to.
92+
By default, The statepoint file is written to a temporary directory and
93+
is not kept.
6294
run_kwargs : dict, optional
6395
Keyword arguments passed to :meth:`openmc.Model.run`
6496
@@ -69,7 +101,13 @@ def get_microxs_and_flux(
69101
list of MicroXS
70102
Cross section data in [b] for each domain
71103
104+
See Also
105+
--------
106+
openmc.deplete.IndependentOperator
107+
72108
"""
109+
check_value('reaction_rate_mode', reaction_rate_mode, {'direct', 'flux'})
110+
73111
# Save any original tallies on the model
74112
original_tallies = model.tallies
75113

@@ -85,8 +123,8 @@ def get_microxs_and_flux(
85123

86124
# Set up the reaction rate and flux tallies
87125
if energies is None:
88-
energy_filter = openmc.EnergyFilter([0.0, 100.0e6])
89-
elif isinstance(energies, str):
126+
energies = [0.0, 100.0e6]
127+
if isinstance(energies, str):
90128
energy_filter = openmc.EnergyFilter.from_group_structure(energies)
91129
else:
92130
energy_filter = openmc.EnergyFilter(energies)
@@ -104,16 +142,18 @@ def get_microxs_and_flux(
104142
else:
105143
raise ValueError(f"Unsupported domain type: {type(domains[0])}")
106144

107-
rr_tally = openmc.Tally(name='MicroXS RR')
108-
rr_tally.filters = [domain_filter, energy_filter]
109-
rr_tally.nuclides = nuclides
110-
rr_tally.multiply_density = False
111-
rr_tally.scores = reactions
112-
113145
flux_tally = openmc.Tally(name='MicroXS flux')
114146
flux_tally.filters = [domain_filter, energy_filter]
115147
flux_tally.scores = ['flux']
116-
model.tallies = openmc.Tallies([rr_tally, flux_tally])
148+
model.tallies = [flux_tally]
149+
150+
if reaction_rate_mode == 'direct':
151+
rr_tally = openmc.Tally(name='MicroXS RR')
152+
rr_tally.filters = [domain_filter, energy_filter]
153+
rr_tally.nuclides = nuclides
154+
rr_tally.multiply_density = False
155+
rr_tally.scores = reactions
156+
model.tallies.append(rr_tally)
117157

118158
if openmc.lib.is_initialized:
119159
openmc.lib.finalize()
@@ -134,33 +174,55 @@ def get_microxs_and_flux(
134174
statepoint_path = model.run(**run_kwargs)
135175

136176
if comm.rank == 0:
177+
# Move the statepoint file if it is being saved to a specific path
178+
if path_statepoint is not None:
179+
shutil.move(statepoint_path, path_statepoint)
180+
statepoint_path = path_statepoint
181+
137182
with StatePoint(statepoint_path) as sp:
138-
rr_tally = sp.tallies[rr_tally.id]
139-
rr_tally._read_results()
183+
if reaction_rate_mode == 'direct':
184+
rr_tally = sp.tallies[rr_tally.id]
185+
rr_tally._read_results()
140186
flux_tally = sp.tallies[flux_tally.id]
141187
flux_tally._read_results()
142188

143-
rr_tally = comm.bcast(rr_tally)
189+
# Get flux values and make energy groups last dimension
144190
flux_tally = comm.bcast(flux_tally)
145-
# Get reaction rates and flux values
146-
reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions)
147191
flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1)
148-
149-
# Make energy groups last dimension
150-
reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups)
151192
flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups)
152193

153-
# Divide RR by flux to get microscopic cross sections
154-
xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups)
155-
d, _, _, g = np.nonzero(flux)
156-
xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g]
194+
# Create list where each item corresponds to one domain
195+
fluxes = list(flux.squeeze((1, 2)))
196+
197+
if reaction_rate_mode == 'direct':
198+
# Get reaction rates
199+
rr_tally = comm.bcast(rr_tally)
200+
reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions)
201+
202+
# Make energy groups last dimension
203+
reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups)
204+
205+
# Divide RR by flux to get microscopic cross sections. The indexing
206+
# ensures that only non-zero flux values are used, and broadcasting is
207+
# applied to align the shapes of reaction_rates and flux for division.
208+
xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups)
209+
d, _, _, g = np.nonzero(flux)
210+
xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g]
211+
212+
# Create lists where each item corresponds to one domain
213+
micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs]
214+
else:
215+
micros = [MicroXS.from_multigroup_flux(
216+
energies=energies,
217+
multigroup_flux=flux_i,
218+
chain_file=chain_file,
219+
nuclides=nuclides,
220+
reactions=reactions
221+
) for flux_i in fluxes]
157222

158223
# Reset tallies
159224
model.tallies = original_tallies
160225

161-
# Create lists where each item corresponds to one domain
162-
fluxes = list(flux.squeeze((1, 2)))
163-
micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs]
164226
return fluxes, micros
165227

166228

@@ -230,7 +292,7 @@ def from_multigroup_flux(
230292
----------
231293
energies : iterable of float or str
232294
Energy group boundaries in [eV] or the name of the group structure
233-
multi_group_flux : iterable of float
295+
multigroup_flux : iterable of float
234296
Energy-dependent multigroup flux values
235297
chain_file : PathLike or Chain, optional
236298
Path to the depletion chain XML file or an instance of

openmc/filter_expansion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import lxml.etree as ET
44

55
import openmc.checkvalue as cv
6-
from . import Filter
6+
from .filter import Filter
77

88

99
class ExpansionFilter(Filter):

tests/regression_tests/microxs/test.py

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,56 @@
1313
@pytest.fixture(scope="module")
1414
def model():
1515
fuel = openmc.Material(name="uo2")
16-
fuel.add_element("U", 1, percent_type="ao", enrichment=4.25)
17-
fuel.add_element("O", 2)
16+
fuel.add_nuclide("U235", 1.0)
17+
fuel.add_nuclide("O16", 2.0)
1818
fuel.set_density("g/cc", 10.4)
1919

20-
clad = openmc.Material(name="clad")
21-
clad.add_element("Zr", 1)
22-
clad.set_density("g/cc", 6)
23-
24-
water = openmc.Material(name="water")
25-
water.add_element("O", 1)
26-
water.add_element("H", 2)
27-
water.set_density("g/cc", 1.0)
28-
water.add_s_alpha_beta("c_H_in_H2O")
29-
30-
radii = [0.42, 0.45]
31-
fuel.volume = np.pi * radii[0] ** 2
32-
33-
materials = openmc.Materials([fuel, clad, water])
34-
35-
pin_surfaces = [openmc.ZCylinder(r=r) for r in radii]
36-
pin_univ = openmc.model.pin(pin_surfaces, materials)
37-
bound_box = openmc.model.RectangularPrism(1.24, 1.24, boundary_type="reflective")
38-
root_cell = openmc.Cell(fill=pin_univ, region=-bound_box)
39-
geometry = openmc.Geometry([root_cell])
20+
sphere = openmc.Sphere(r=10.0, boundary_type='vacuum')
21+
cell = openmc.Cell(region=-sphere, fill=fuel)
22+
geometry = openmc.Geometry([cell])
4023

4124
settings = openmc.Settings()
4225
settings.particles = 1000
4326
settings.inactive = 5
4427
settings.batches = 10
4528

46-
return openmc.Model(geometry, materials, settings)
29+
return openmc.Model(geometry, settings=settings)
4730

4831

49-
@pytest.mark.parametrize("domain_type", ["materials", "mesh"])
50-
def test_from_model(model, domain_type):
32+
@pytest.mark.parametrize(
33+
"domain_type, rr_mode",
34+
[
35+
("materials", "direct"),
36+
("materials", "flux"),
37+
("mesh", "direct"),
38+
("mesh", "flux"),
39+
]
40+
)
41+
def test_from_model(model, domain_type, rr_mode):
5142
if domain_type == 'materials':
52-
domains = model.materials[:1]
43+
domains = list(model.geometry.get_all_materials().values())
5344
elif domain_type == 'mesh':
5445
mesh = openmc.RegularMesh()
55-
mesh.lower_left = (-0.62, -0.62)
56-
mesh.upper_right = (0.62, 0.62)
57-
mesh.dimension = (3, 3)
46+
mesh.lower_left = (-10., -10.)
47+
mesh.upper_right = (10., 10.)
48+
mesh.dimension = (1, 1)
5849
domains = mesh
59-
nuclides = ['U234', 'U235', 'U238', 'U236', 'O16', 'O17', 'I135', 'Xe135',
60-
'Xe136', 'Cs135', 'Gd157', 'Gd156']
61-
_, test_xs = get_microxs_and_flux(model, domains, nuclides, chain_file=CHAIN_FILE)
50+
nuclides = ['U235', 'O16', 'Xe135']
51+
kwargs = {
52+
'reaction_rate_mode': rr_mode,
53+
'chain_file': CHAIN_FILE,
54+
'path_statepoint': 'neutron_transport.h5',
55+
}
56+
if rr_mode == 'flux':
57+
kwargs['energies'] = 'CASMO-40'
58+
_, test_xs = get_microxs_and_flux(model, domains, nuclides, **kwargs)
6259
if config['update']:
63-
test_xs[0].to_csv(f'test_reference_{domain_type}.csv')
64-
65-
ref_xs = MicroXS.from_csv(f'test_reference_{domain_type}.csv')
60+
test_xs[0].to_csv(f'test_reference_{domain_type}_{rr_mode}.csv')
6661

62+
# Make sure results match reference results
63+
ref_xs = MicroXS.from_csv(f'test_reference_{domain_type}_{rr_mode}.csv')
6764
np.testing.assert_allclose(test_xs[0].data, ref_xs.data, rtol=1e-11)
65+
66+
# Make sure statepoint file was saved
67+
assert Path('neutron_transport.h5').exists()
68+
Path('neutron_transport.h5').unlink()

tests/regression_tests/microxs/test_reference_materials.csv

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
nuclides,reactions,groups,xs
2+
U235,"(n,gamma)",1,0.14765501510184456
3+
U235,fission,1,1.2517200956290817
4+
O16,"(n,gamma)",1,0.00010872314985710938
5+
O16,fission,1,0.0
6+
Xe135,"(n,gamma)",1,0.014333667335215764
7+
Xe135,fission,1,0.0
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
nuclides,reactions,groups,xs
2+
U235,"(n,gamma)",1,0.15018326758942505
3+
U235,fission,1,1.2603151141390958
4+
O16,"(n,gamma)",1,0.00012159621938463765
5+
O16,fission,1,0.0
6+
Xe135,"(n,gamma)",1,0.015180177095633546
7+
Xe135,fission,1,0.0

tests/regression_tests/microxs/test_reference_mesh.csv

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
nuclides,reactions,groups,xs
2+
U235,"(n,gamma)",1,0.14765501510184456
3+
U235,fission,1,1.2517200956290815
4+
O16,"(n,gamma)",1,0.00010872314985710936
5+
O16,fission,1,0.0
6+
Xe135,"(n,gamma)",1,0.014333667335215761
7+
Xe135,fission,1,0.0

0 commit comments

Comments
 (0)