MGXS Bootstrapping (#3965)
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

Co-authored-by: John Tramm <jtramm@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
John Tramm 2026-07-22 00:38:20 -05:00 committed by GitHub
parent 852f92780c
commit 54b661d39f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 787 additions and 204 deletions

View file

@ -11,6 +11,7 @@ Simple Models
:template: myfunction.rst
openmc.examples.slab_mg
openmc.examples.sphere_with_shielded_pocket
Reactor Models
--------------

View file

@ -642,13 +642,11 @@ model to use these multigroup cross sections. An example is given below::
model.convert_to_multigroup(
method="material_wise",
groups="CASMO-2",
nparticles=2000,
overwrite_mgxs_library=False,
mgxs_path="mgxs.h5",
correction=None,
source_energy=None,
temperatures=None,
temperature_settings=None
temperatures=None
)
The most important parameter to set is the ``method`` parameter, which can be
@ -672,7 +670,9 @@ of these methods is given below:
both spatial and resonance self shielding effects
- * Potentially slower as the full geometry must be run
* If a material is only present far from the source and doesn't get tallied
to in the CE simulation, the MGXS will be zero for that material.
to in the CE simulation, the MGXS will be zero for that material. This
can be mitigated by supplying weight windows via ``weight_windows_file``
(see :ref:`mgxs_bootstrap`).
* - ``stochastic_slab``
- * Medium Fidelity
* Runs a CE simulation with a greatly simplified geometry, where materials
@ -693,12 +693,20 @@ of these methods is given below:
When selecting a non-default energy group structure, you can manually define
group boundaries or specify the name of a known group structure (a list of which
can be found at :data:`openmc.mgxs.GROUP_STRUCTURES`). The ``nparticles``
parameter can be adjusted upward to improve the fidelity of the generated cross
section library. The ``correction`` parameter can be set to ``"P0"`` to enable
P0 transport correction. The ``overwrite_mgxs_library`` parameter can be set to
``True`` to overwrite an existing MGXS library file, or ``False`` to skip
generation and use an existing library file.
can be found at :data:`openmc.mgxs.GROUP_STRUCTURES`). The ``correction``
parameter can be set to ``"P0"`` to enable P0 transport correction. The
``overwrite_mgxs_library`` parameter can be set to ``True`` to overwrite an
existing MGXS library file, or ``False`` to skip generation and use an existing
library file.
The continuous energy simulations used to generate the cross section library
can be customized by passing :class:`openmc.Settings` attributes as keyword
arguments; only the fields you set override the generation defaults. For
example, the number of particles per batch (2,000 by default) can be
increased to improve the fidelity of the generated cross section library
as::
model.convert_to_multigroup(particles=100_000)
.. note::
MGXS transport correction (via setting the ``correction`` parameter in the
@ -739,15 +747,13 @@ The ``temperatures`` parameter can be provided if temperature-dependent
multi-group cross sections are desired for multi-physics simulations. An
individual cross section generation calculation is run for each temperature
provided, where the materials in the model are set to the temperature. The
temperature settings used during cross section generation can be specified with the
``temperature_settings`` parameter. If no ``temperature_settings`` are provided,
the settings contained in the model will be used. The valid keys and values in the
``temperature_settings`` dictionary are identical to
:attr:`openmc.Settings.temperature_settings`; more information can be found in
:class:`openmc.Settings` . This approach yields isothermal cross section interpolation
tables, which can be inaccurate for systems with large differences between temperatures
in each material (often the case in fission reactors). If a more sophisticated
temperature-dependence is required, we recommend generating cross sections manually.
temperature settings used during cross section generation default to those
contained in the model and can be customized with the ``temperature`` keyword
argument to :meth:`openmc.Model.convert_to_multigroup`. This approach yields
isothermal cross section interpolation tables, which can be inaccurate for
systems with large differences between temperatures in each material (often the
case in fission reactors). If a more sophisticated temperature-dependence is
required, we recommend generating cross sections manually.
Ultimately, the methods described above are all just approximations.
Approximations in the generated MGXS data will fundamentally limit the potential
@ -757,6 +763,50 @@ simulation, and if more fidelity is needed the user may wish to follow the
instructions below or experiment with transport correction techniques to improve
the fidelity of the generated MGXS data.
.. _mgxs_bootstrap:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Bootstrapping Material-Wise MGXS with Weight Windows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``"material_wise"`` method runs a continuous energy simulation of the
original geometry, so it produces the highest fidelity cross sections of the
three methods. However, it has a notable weakness: if a material only appears
far from the source (for example, a detector or structural material located
outside a thick shield), an analog continuous energy simulation may be unable to
transport any particles to that material. No tallies are scored there, and the
resulting cross sections for that material are zero. This situation is common in
shielding problems.
This limitation can be overcome by "bootstrapping" the cross section generation
with weight windows. The idea is to first cheaply produce a set of weight
windows that cover the entire problem and then reuse them to push particles into
the far regions during the higher fidelity ``"material_wise"`` solve. The weight
windows are generated using the ``"stochastic_slab"`` method (which produces
cross sections for *all* materials regardless of their location) together with
the random ray solver and a :class:`~openmc.WeightWindowGenerator`, exactly as
described in the :ref:`FW-CADIS user guide <usersguide_fw_cadis>`. The resulting
``weight_windows.h5`` file is then supplied to a second, higher fidelity
``"material_wise"`` cross section generation by passing
``weight_windows_file``::
# First, generate weight windows with the stochastic slab method and random
# ray (see the FW-CADIS user guide), producing a weight_windows.h5 file.
...
# Then, bootstrap a higher fidelity material-wise library, applying those
# weight windows during the continuous energy solve so that particles can
# reach materials far from the source.
model.convert_to_multigroup(
weight_windows_file="weight_windows.h5", overwrite_mgxs_library=True)
The ``weight_windows_file`` setting is only used with the
``"material_wise"`` method, as the ``"stochastic_slab"`` and
``"infinite_medium"`` methods use simplified surrogate geometries that are
incompatible with a weight window mesh defined over the original geometry (and
do not need weight windows, since they already tally all materials). A warning
is issued and the file is ignored if it is supplied to another method.
~~~~~~~~~~~~
The Hard Way
~~~~~~~~~~~~
@ -1075,9 +1125,9 @@ The adjoint flux random ray solver mode can be enabled as::
settings.random_ray['adjoint'] = True
When enabled, OpenMC will first run a forward transport simulation if there are
no user-specified adjoint sources present, followed by an adjoint transport
simulation. Fixed adjoint sources can be specified on the
When enabled, OpenMC will first run a forward transport simulation if there are
no user-specified adjoint sources present, followed by an adjoint transport
simulation. Fixed adjoint sources can be specified on the
:attr:`openmc.Settings.random_ray` dictionary as follows::
# Geometry definition
@ -1090,21 +1140,21 @@ simulation. Fixed adjoint sources can be specified on the
energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths)
adj_source = openmc.IndependentSource(
energy=energy_distribution,
energy=energy_distribution,
constraints={'domains': [detector_cell]}
)
# Add to random_ray dict
settings.random_ray['adjoint_source'] = adj_source
The same constraints apply to the user-defined adjoint source as to the forward
source, described in the :ref:`Fixed Source and Eigenvalue section
<usersguide_random_ray_run_modes>`. If this source is not provided, a forward
solve must take place to compute the adjoint external source when a forward
external source is present in the problem. Simulation settings (e.g., number of
rays, batches, etc.) will be identical for both calculations. At the
conclusion of the run, all results (e.g., tallies, plots, etc.) will be
derived from the adjoint flux rather than the forward flux but are not labeled
The same constraints apply to the user-defined adjoint source as to the forward
source, described in the :ref:`Fixed Source and Eigenvalue section
<usersguide_random_ray_run_modes>`. If this source is not provided, a forward
solve must take place to compute the adjoint external source when a forward
external source is present in the problem. Simulation settings (e.g., number of
rays, batches, etc.) will be identical for both calculations. At the
conclusion of the run, all results (e.g., tallies, plots, etc.) will be
derived from the adjoint flux rather than the forward flux but are not labeled
any differently. When an initial forward solve is performed (i.e., when no
user-specified adjoint source is present), its output files are also written to
disk with a ``forward`` infix, so they are not overwritten by the subsequent
@ -1116,10 +1166,10 @@ generating FW-CADIS weight windows, no weight window file is written for the
forward solve, as only the final adjoint-derived weight windows are meaningful.
.. note::
Use of the automated
:ref:`FW-CADIS weight window generator<usersguide_fw_cadis>` is not
currently compatible with user-defined adjoint sources. Instead, the
initial forward calculation is used to assign "forward-weighted" adjoint
Use of the automated
:ref:`FW-CADIS weight window generator<usersguide_fw_cadis>` is not
currently compatible with user-defined adjoint sources. Instead, the
initial forward calculation is used to assign "forward-weighted" adjoint
sources to the tally regions of interest.
---------------------------------------

View file

@ -75,6 +75,19 @@ constexpr double ZERO_FLUX_CUTOFF {1e-22};
// value will be converted to pure void.
constexpr double MINIMUM_MACRO_XS {1e-6};
// Relative dead band applied to weight window comparisons: particles split
// only above upper * (1 + tol) and roulette only below lower * (1 - tol).
// Weight window arithmetic can land a particle's weight exactly back on a
// bound value (e.g., a roulette survivor is assigned survival_ratio * lower
// and a later split divides that back down), in which case the branch taken
// would be decided by the last ulp of the bound. Since window data carries
// ulp-level noise from non-associative parallel reductions in the solver that
// generated it, transport results would otherwise be chaotically sensitive to
// bit-level differences in the weight window file. Treating weights within
// the band as inside the window is statistically negligible, and weight
// window games are unbiased regardless of where the thresholds sit.
constexpr double WEIGHT_WINDOW_REL_TOL {1e-9};
// ============================================================================
// MATH AND PHYSICAL CONSTANTS

View file

@ -1314,17 +1314,17 @@ def random_ray_three_region_cube() -> openmc.Model:
def random_ray_three_region_cube_with_detectors() -> openmc.Model:
"""Create a three region cube model with two external tally regions.
This is an adaptation of the simple monoenergetic problem of a cube with
three concentric cubic regions. The innermost region is near void (with
Sigma_t around 10^-5) and contains an external isotropic source term, the
middle region is a mild scatterer (with Sigma_t around 10^-3), and the
outer region of the cube is a scatterer and absorber (with Sigma_t around
This is an adaptation of the simple monoenergetic problem of a cube with
three concentric cubic regions. The innermost region is near void (with
Sigma_t around 10^-5) and contains an external isotropic source term, the
middle region is a mild scatterer (with Sigma_t around 10^-3), and the
outer region of the cube is a scatterer and absorber (with Sigma_t around
1).
Two cubic "detector" regions are found outside this geometry, one along the
y-axis near z=0, and the other in the upper right corner of the system.
The size of each detector is scaled to be equal to that of the source
region. The model returned by this function contains cell tallies on each
Two cubic "detector" regions are found outside this geometry, one along the
y-axis near z=0, and the other in the upper right corner of the system.
The size of each detector is scaled to be equal to that of the source
region. The model returned by this function contains cell tallies on each
detector.
Returns
@ -1498,29 +1498,29 @@ def random_ray_three_region_cube_with_detectors() -> openmc.Model:
fill=absorber_mat,
region=detector2_region
)
external_x = (
+x_high & +y_low & +z_low & -x_outer &
+x_high & +y_low & +z_low & -x_outer &
((-y_outer & -z_high) | (-y_high & +z_high & -z_outer))
)
external_y = (
+y_high & -y_outer &
+y_high & -y_outer &
(
(+detector1_right & -x_high & +z_low & -z_outer) |
(-detector1_right & +x_low & +detector1_top & -z_outer) |
(+detector1_right & -x_high & +z_low & -z_outer) |
(-detector1_right & +x_low & +detector1_top & -z_outer) |
(+x_high & -x_outer & +z_low & -z_high)
)
)
external_z = (
+x_low & +y_low & +z_high & -z_outer &
+x_low & +y_low & +z_high & -z_outer &
((-y_outer & -x_high) | (-y_high & +x_high & -x_outer))
)
external_cell = openmc.Cell(fill=cavity_mat,
region=(external_x | external_y | external_z),
external_cell = openmc.Cell(fill=cavity_mat,
region=(external_x | external_y | external_z),
name='outside cube')
root = openmc.Universe(
name='root universe',
name='root universe',
cells=[cube_domain, detector1, detector2, external_cell]
)
@ -1604,8 +1604,8 @@ def random_ray_three_region_cube_with_detectors() -> openmc.Model:
source_tally.estimator = estimator
# Instantiate a Tallies collection and export to XML
tallies = openmc.Tallies([detector1_tally,
detector2_tally,
tallies = openmc.Tallies([detector1_tally,
detector2_tally,
absorber_tally,
cavity_tally,
source_tally])
@ -1619,3 +1619,114 @@ def random_ray_three_region_cube_with_detectors() -> openmc.Model:
model.tallies = tallies
return model
def sphere_with_shielded_pocket() -> openmc.Model:
"""Create a continuous energy deep-shielding model with a far detector pocket.
A concrete sphere is centered at the origin. A 2 MeV isotropic neutron
source sits in a small air cavity just inside the sphere surface on the -x
side, and a small steel pocket is embedded flush with the surface on the
+x axis, so roughly a meter of concrete separates the source from the
pocket while only a few centimeters of concrete back the cavity. The
sphere is enclosed in a vacuum-bounded box, with a void gap in between, so
that solvers that sample uniformly over a rectangular domain (e.g.,
random ray) can be applied directly. The geometry is designed for testing
weight window and variance reduction workflows:
- The probability that an analog source neutron reaches the steel pocket is
~4e-5 (the product of the concrete attenuation and the pocket's small
solid angle), so an analog simulation with a few hundred histories
essentially never tallies the steel, while even crude global weight
windows allow particles to reach it reliably.
- Because the cavity sits near the surface, deep shielding (and thus a wide
weight window dynamic range) exists only within the small solid angle
subtended by the pocket, which keeps weight window splitting cheap and
convergent and the whole model fast enough for regression testing.
Returns
-------
model : openmc.Model
A deep-shielding model with a steel pocket behind a thick concrete
shield
"""
model = openmc.Model()
###########################################################################
# Materials (few nuclides, to keep data loading cheap in multi-solve tests)
air = openmc.Material(name='Air')
air.set_density('g/cm3', 0.001225)
air.add_nuclide('N14', 0.79, 'ao')
air.add_nuclide('O16', 0.21, 'ao')
concrete = openmc.Material(name='Concrete')
concrete.set_density('g/cm3', 2.3)
concrete.add_nuclide('H1', 0.168759, 'ao')
concrete.add_nuclide('O16', 0.562489, 'ao')
concrete.add_nuclide('Si28', 0.203031, 'ao')
concrete.add_nuclide('Ca40', 0.044849, 'ao')
concrete.add_nuclide('Al27', 0.020872, 'ao')
steel = openmc.Material(name='Steel')
steel.set_density('g/cm3', 7.87)
steel.add_nuclide('Fe56', 1.0)
###########################################################################
# Geometry
# ~92 cm of concrete separates the cavity from the pocket face, while only
# ~6 cm of concrete backs the cavity on the -x side, so deep shielding is
# confined to the solid angle subtended by the pocket.
r_sphere = 66.0
box_half_width = 70.0
cavity_center_x = -54.0
cavity_half_width = 6.0
pocket_inner_face = 44.0
pocket_half_width = 4.0
sphere = openmc.Sphere(r=r_sphere)
outer_box = openmc.model.RectangularParallelepiped(
-box_half_width, box_half_width,
-box_half_width, box_half_width,
-box_half_width, box_half_width, boundary_type='vacuum')
cavity_box = openmc.model.RectangularParallelepiped(
cavity_center_x - cavity_half_width, cavity_center_x + cavity_half_width,
-cavity_half_width, cavity_half_width,
-cavity_half_width, cavity_half_width)
# The pocket box extends past the sphere surface and is clipped by it, so
# the pocket sits flush with (and just inside) the outer surface.
pocket_box = openmc.model.RectangularParallelepiped(
pocket_inner_face, r_sphere + 1.0,
-pocket_half_width, pocket_half_width,
-pocket_half_width, pocket_half_width)
cavity_cell = openmc.Cell(name='cavity', fill=air, region=-cavity_box)
pocket_cell = openmc.Cell(name='pocket', fill=steel,
region=-pocket_box & -sphere)
concrete_cell = openmc.Cell(
name='concrete', fill=concrete,
region=-sphere & +cavity_box & ~(-pocket_box & -sphere))
void_cell = openmc.Cell(name='void', region=+sphere & -outer_box)
model.geometry = openmc.Geometry(
[cavity_cell, pocket_cell, concrete_cell, void_cell])
###########################################################################
# Source and settings
source = openmc.IndependentSource()
source.space = openmc.stats.Box(
[cavity_center_x - cavity_half_width, -cavity_half_width, -cavity_half_width],
[cavity_center_x + cavity_half_width, cavity_half_width, cavity_half_width])
source.constraints = {'domains': [cavity_cell]}
source.angle = openmc.stats.Isotropic()
source.energy = openmc.stats.delta_function(2.0e6)
model.settings.run_mode = 'fixed source'
model.settings.source = source
model.settings.particles = 1000
model.settings.batches = 10
return model

View file

@ -1898,12 +1898,10 @@ class Model:
Parameters
----------
model : openmc.Model
The model to generate the MGXS library from.
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
mgxs_path : str
Filename for the MGXS HDF5 file.
correction : str
Transport correction to apply to the MGXS. Options are None and
"P0".
@ -2055,11 +2053,10 @@ class Model:
def _isothermal_infinite_media_mgxs(
material: openmc.Material,
groups: openmc.mgxs.EnergyGroups,
nparticles: int,
settings: openmc.Settings,
correction: str | None,
directory: PathLike,
source: openmc.IndependentSource,
temperature_settings: dict,
temperature: float | None = None,
) -> openmc.XSdata:
"""Generate a single MGXS set for one material, where the geometry is an
@ -2071,8 +2068,9 @@ class Model:
The material to generate MGXS for
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
settings : openmc.Settings
Settings for the generation run, used verbatim except for the
fields owned by the infinite medium method.
correction : str
Transport correction to apply to the MGXS. Options are None and
"P0".
@ -2080,10 +2078,6 @@ class Model:
Directory to run the simulation in, so as to contain XML files.
source : openmc.IndependentSource
Source to use when generating MGXS.
temperature_settings : dict
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
temperature : float, optional
The isothermal temperature value to apply to the material. If not specified,
defaults to the temperature in the material.
@ -2100,18 +2094,11 @@ class Model:
if temperature is not None:
model.materials[-1].temperature = temperature
# Settings
model.settings.batches = 100
model.settings.particles = nparticles
# The provided settings are used verbatim, except for the fields
# owned by the infinite medium method
model.settings = copy.deepcopy(settings)
model.settings.source = source
model.settings.run_mode = 'fixed source'
model.settings.create_fission_neutrons = False
model.settings.output = {'summary': True, 'tallies': False}
model.settings.temperature = temperature_settings
# Geometry
box = openmc.model.RectangularPrism(
100000.0, 100000.0, boundary_type='reflective')
@ -2133,13 +2120,12 @@ class Model:
def _generate_infinite_medium_mgxs(
self,
groups: openmc.mgxs.EnergyGroups,
nparticles: int,
settings: openmc.Settings,
mgxs_path: PathLike,
correction: str | None,
directory: PathLike,
source_energy: openmc.stats.Univariate | None = None,
temperatures: Sequence[float] | None = None,
temperature_settings: dict | None = None,
) -> None:
"""Generate a MGXS library by running multiple OpenMC simulations, each
representing an infinite medium simulation of a single isolated
@ -2168,8 +2154,9 @@ class Model:
----------
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
settings : openmc.Settings
Settings for the generation run(s), used verbatim except for the
fields owned by the infinite medium method.
mgxs_path : str
Filename for the MGXS HDF5 file.
correction : str
@ -2184,10 +2171,6 @@ class Model:
A list of temperatures to generate MGXS at. Each infinite material region
is isothermal at a given temperature data point for cross
section generation.
temperature_settings : dict, optional
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
"""
src = self._create_mgxs_sources(
@ -2196,23 +2179,16 @@ class Model:
source_energy=source_energy
)
temp_settings = {}
if temperature_settings is None:
temp_settings = self.settings.temperature
else:
temp_settings = temperature_settings
if temperatures is None:
mgxs_sets = []
for material in self.materials:
xs_data = Model._isothermal_infinite_media_mgxs(
material,
groups,
nparticles,
settings,
correction,
directory,
src,
temp_settings
src
)
mgxs_sets.append(xs_data)
@ -2230,11 +2206,10 @@ class Model:
xs_data = Model._isothermal_infinite_media_mgxs(
material,
groups,
nparticles,
settings,
correction,
directory,
src,
temp_settings,
temperature
)
raw_mgxs_sets[temperature].append(xs_data)
@ -2332,11 +2307,10 @@ class Model:
def _isothermal_stochastic_slab_mgxs(
stoch_geom: openmc.Geometry,
groups: openmc.mgxs.EnergyGroups,
nparticles: int,
settings: openmc.Settings,
correction: str | None,
directory: PathLike,
source: openmc.IndependentSource,
temperature_settings: dict,
temperature: float | None = None,
) -> dict[str, openmc.XSdata]:
"""Generate MGXS assuming a stochastic "sandwich" of materials in a layered
@ -2349,8 +2323,9 @@ class Model:
The stochastic slab geometry.
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
settings : openmc.Settings
Settings for the generation run, used verbatim except for the
fields owned by the stochastic slab method.
correction : str
Transport correction to apply to the MGXS. Options are None and
"P0".
@ -2358,10 +2333,6 @@ class Model:
Directory to run the simulation in, so as to contain XML files.
source : openmc.IndependentSource
Source to use when generating MGXS.
temperature_settings : dict
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
temperature : float, optional
The isothermal temperature value to apply to the materials in the
slab. If not specified, defaults to the temperature in the materials.
@ -2379,21 +2350,11 @@ class Model:
for material in model.geometry.get_all_materials().values():
material.temperature = temperature
# Settings
model.settings.batches = 200
model.settings.inactive = 100
model.settings.particles = nparticles
model.settings.output = {'summary': True, 'tallies': False}
model.settings.temperature = temperature_settings
# Define the sources
# The provided settings are used verbatim, except for the fields
# owned by the stochastic slab method
model.settings = copy.deepcopy(settings)
model.settings.source = source
model.settings.run_mode = 'fixed source'
model.settings.create_fission_neutrons = False
model.settings.output = {'summary': True, 'tallies': False}
# Generate MGXS
mgxs_lib = Model._auto_generate_mgxs_lib(
model, groups, correction, directory)
@ -2414,13 +2375,12 @@ class Model:
def _generate_stochastic_slab_mgxs(
self,
groups: openmc.mgxs.EnergyGroups,
nparticles: int,
settings: openmc.Settings,
mgxs_path: PathLike,
correction: str | None,
directory: PathLike,
source_energy: openmc.stats.Univariate | None = None,
temperatures: Sequence[float] | None = None,
temperature_settings: dict | None = None,
) -> None:
"""Generate MGXS assuming a stochastic "sandwich" of materials in a layered
slab geometry. While geometry-specific spatial shielding effects are not
@ -2438,8 +2398,9 @@ class Model:
----------
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
settings : openmc.Settings
Settings for the generation run(s), used verbatim except for the
fields owned by the stochastic slab method.
mgxs_path : str
Filename for the MGXS HDF5 file.
correction : str
@ -2468,10 +2429,6 @@ class Model:
A list of temperatures to generate MGXS at. Each infinite material region
is isothermal at a given temperature data point for cross
section generation.
temperature_settings : dict, optional
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
"""
# Stochastic slab geometry
@ -2484,21 +2441,14 @@ class Model:
source_energy=source_energy
)
temp_settings = {}
if temperature_settings is None:
temp_settings = self.settings.temperature
else:
temp_settings = temperature_settings
if temperatures is None:
mgxs_sets = Model._isothermal_stochastic_slab_mgxs(
geo,
groups,
nparticles,
settings,
correction,
directory,
src,
temp_settings
src
).values()
# Write the file to disk.
@ -2513,11 +2463,10 @@ class Model:
raw_mgxs_sets[temperature] = Model._isothermal_stochastic_slab_mgxs(
geo,
groups,
nparticles,
settings,
correction,
directory,
src,
temp_settings,
temperature
)
@ -2539,10 +2488,9 @@ class Model:
def _isothermal_materialwise_mgxs(
input_model: openmc.Model,
groups: openmc.mgxs.EnergyGroups,
nparticles: int,
settings: openmc.Settings,
correction: str | None,
directory: PathLike,
temperature_settings: dict,
temperature: float | None = None,
) -> dict[str, openmc.XSdata]:
"""Generate a material-wise MGXS library for the model by running the
@ -2559,17 +2507,13 @@ class Model:
The model to use when computing material-wise MGXS.
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
settings : openmc.Settings
Settings for the generation run, used verbatim.
correction : str
Transport correction to apply to the MGXS. Options are None and
"P0".
directory : str
Directory to run the simulation in, so as to contain XML files.
temperature_settings : dict
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
temperature : float, optional
The isothermal temperature value to apply to the materials in the
input model. If not specified, defaults to the temperatures in the
@ -2587,12 +2531,8 @@ class Model:
for material in model.geometry.get_all_materials().values():
material.temperature = temperature
# Settings
model.settings.batches = 200
model.settings.inactive = 100
model.settings.particles = nparticles
model.settings.output = {'summary': True, 'tallies': False}
model.settings.temperature = temperature_settings
# The provided settings are used verbatim
model.settings = copy.deepcopy(settings)
# Generate MGXS
mgxs_lib = Model._auto_generate_mgxs_lib(
@ -2614,12 +2554,11 @@ class Model:
def _generate_material_wise_mgxs(
self,
groups: openmc.mgxs.EnergyGroups,
nparticles: int,
settings: openmc.Settings,
mgxs_path: PathLike,
correction: str | None,
directory: PathLike,
temperatures: Sequence[float] | None = None,
temperature_settings: dict | None = None,
) -> None:
"""Generate a material-wise MGXS library for the model by running the
original continuous energy OpenMC simulation of the full material
@ -2635,8 +2574,8 @@ class Model:
----------
groups : openmc.mgxs.EnergyGroups
Energy group structure for the MGXS.
nparticles : int
Number of particles to simulate per batch when generating MGXS.
settings : openmc.Settings
Settings for the generation run(s), used verbatim.
mgxs_path : PathLike
Filename for the MGXS HDF5 file.
correction : str
@ -2648,26 +2587,10 @@ class Model:
A list of temperatures to generate MGXS at. Each infinite material region
is isothermal at a given temperature data point for cross
section generation.
temperature_settings : dict, optional
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
"""
temp_settings = {}
if temperature_settings is None:
temp_settings = self.settings.temperature
else:
temp_settings = temperature_settings
if temperatures is None:
mgxs_sets = Model._isothermal_materialwise_mgxs(
self,
groups,
nparticles,
correction,
directory,
temp_settings
).values()
self, groups, settings, correction, directory).values()
# Write the file to disk.
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
@ -2679,14 +2602,7 @@ class Model:
raw_mgxs_sets = {}
for temperature in temperatures:
raw_mgxs_sets[temperature] = Model._isothermal_materialwise_mgxs(
self,
groups,
nparticles,
correction,
directory,
temp_settings,
temperature
)
self, groups, settings, correction, directory, temperature)
# Unpack the isothermal XSData objects and build a single XSData object per material.
mgxs_sets = []
@ -2706,13 +2622,14 @@ class Model:
self,
method: str = "material_wise",
groups: str | Sequence[float] | openmc.mgxs.EnergyGroups = "CASMO-2",
nparticles: int = 2000,
nparticles: int | None = None,
overwrite_mgxs_library: bool = False,
mgxs_path: PathLike = "mgxs.h5",
correction: str | None = None,
source_energy: openmc.stats.Univariate | None = None,
temperatures: Sequence[float] | None = None,
temperature_settings: dict | None = None,
**kwargs,
):
"""Convert all materials from continuous energy to multigroup.
@ -2732,6 +2649,10 @@ class Model:
Defaults to ``"CASMO-2"``.
nparticles : int, optional
Number of particles to simulate per batch when generating MGXS.
Defaults to 2000.
.. deprecated:: 0.15.4
Pass ``particles`` as a keyword argument instead.
overwrite_mgxs_library : bool, optional
Whether to overwrite an existing MGXS library file.
mgxs_path : str, optional
@ -2765,10 +2686,121 @@ class Model:
A dictionary of temperature settings to use when generating MGXS.
Valid entries for temperature_settings are the same as the valid
entries in openmc.Settings.temperature_settings.
.. deprecated:: 0.15.4
Pass ``temperature`` as a keyword argument instead.
**kwargs
:class:`openmc.Settings` attributes used to customize the continuous
energy simulation(s) that generate the MGXS library. Only the
attributes given override the generation defaults. For example,
``model.convert_to_multigroup(particles=100_000)`` adjusts only the
particle count. The run mode cannot be overridden, as it is
determined by the generation method. The surrogate-geometry methods
also construct their own sources and always disable
``create_fission_neutrons`` so that fission is treated as capture.
A ``weight_windows_file`` is applied during ``"material_wise"``
generation and ignored with a warning by the other methods; see the
user guide for the weight window "bootstrapping" workflow this
enables. Cannot be combined with the deprecated ``nparticles`` or
``temperature_settings`` arguments.
.. versionadded:: 0.15.4
"""
if not isinstance(groups, openmc.mgxs.EnergyGroups):
groups = openmc.mgxs.EnergyGroups(groups)
check_value('method', method,
('material_wise', 'stochastic_slab', 'infinite_medium'))
# Keyword arguments are Settings attributes applied as overrides on
# the generation defaults
settings = openmc.Settings(**kwargs) if kwargs else None
# The model may reference its materials only through the geometry.
# The materials are converted in place and library-wide attributes
# like cross_sections must persist on the model afterwards, so
# populate the explicit collection if it is empty (sorted by ID for
# reproducibility, since geometry traversal order is arbitrary)
if not self.materials:
self.materials = openmc.Materials(sorted(
self.geometry.get_all_materials().values(),
key=lambda mat: mat.id))
if nparticles is not None or temperature_settings is not None:
warnings.warn(
'The "nparticles" and "temperature_settings" arguments are '
'deprecated. Pass "particles" and "temperature" as keyword '
'arguments instead.', FutureWarning)
if settings is not None:
raise ValueError(
'The deprecated "nparticles" and "temperature_settings" '
'arguments cannot be combined with Settings keyword '
'arguments.')
# Resolve the settings for the MGXS generation run(s) in three layers,
# with later layers taking precedence: the model's own settings
# ("material_wise") or a fresh Settings object (surrogate methods), then
# the generation defaults, then the user's keyword-argument overrides.
user_settings = settings
if method == 'material_wise':
settings = copy.deepcopy(self.settings)
else:
settings = openmc.Settings()
settings.temperature = copy.deepcopy(self.settings.temperature)
settings.batches = 100 if method == 'infinite_medium' else 200
if method != 'infinite_medium':
settings.inactive = 100
settings.particles = 2000
settings.output = {'summary': True, 'tallies': False}
if nparticles is not None:
settings.particles = nparticles
if temperature_settings is not None:
settings.temperature = temperature_settings
if user_settings is not None:
# The surrogate-geometry methods construct their own sources
if method != "material_wise" and len(user_settings.source) > 0:
warnings.warn(
'The given "source" setting is ignored by the '
f'"{method}" MGXS generation method, which constructs '
'its own sources.')
# Apply the settings attributes passed by the caller
for name in kwargs:
if hasattr(type(settings), name):
setattr(settings, name, copy.deepcopy(
getattr(user_settings, name)))
# The run mode is the one attribute that cannot be detected as
# user-populated (a fresh Settings object defaults it to 'eigenvalue'),
# so it is owned by the generation method: "material_wise" always takes
# it from the model, while the surrogate-geometry methods always run in
# fixed source mode. The surrogate-geometry methods also treat fission
# as capture (nu-fission is still tallied)
if method == 'material_wise':
settings.run_mode = self.settings.run_mode
else:
settings.run_mode = 'fixed source'
settings.create_fission_neutrons = False
# A weight windows file on the generation settings is loaded and
# applied (specifying a file turns weight windows on) during the
# "material_wise" method's continuous energy simulation of the
# original geometry, allowing materials far from the source --
# which an analog simulation may struggle to reach -- to still be
# tallied, and thus obtain nonzero cross sections. The
# "stochastic_slab" and "infinite_medium" methods use simplified
# surrogate geometries for which weight windows defined over the
# original geometry are neither applicable nor needed.
if settings.weight_windows_file is not None and \
method != "material_wise":
warnings.warn(
'The "weight_windows_file" setting is only applicable to '
'the "material_wise" MGXS generation method and will be '
f'ignored for the "{method}" method.'
)
settings.weight_windows_file = None
# Do all work (including MGXS generation) in a temporary directory
# to avoid polluting the working directory with residual XML files
with TemporaryDirectory() as tmpdir:
@ -2804,16 +2836,16 @@ class Model:
if not Path(mgxs_path).is_file() or overwrite_mgxs_library:
if method == "infinite_medium":
self._generate_infinite_medium_mgxs(
groups, nparticles, mgxs_path, correction, tmpdir, source_energy,
temperatures, temperature_settings)
groups, settings, mgxs_path, correction, tmpdir,
source_energy, temperatures)
elif method == "material_wise":
self._generate_material_wise_mgxs(
groups, nparticles, mgxs_path, correction, tmpdir,
temperatures, temperature_settings)
groups, settings, mgxs_path, correction, tmpdir,
temperatures)
elif method == "stochastic_slab":
self._generate_stochastic_slab_mgxs(
groups, nparticles, mgxs_path, correction, tmpdir, source_energy,
temperatures, temperature_settings)
groups, settings, mgxs_path, correction, tmpdir,
source_energy, temperatures)
else:
raise ValueError(
f'MGXS generation method "{method}" not recognized')

View file

@ -241,7 +241,7 @@ class Settings:
also tends to dampen the convergence rate of the solver, thus requiring
more iterations to converge.
:adjoint_source:
Source object used to define localized adjoint source/detector response
Source object used to define localized adjoint source/detector response
function.
.. versionadded:: 0.15.0
@ -2031,11 +2031,11 @@ class Settings:
path = f"./mesh[@id='{mesh.id}']"
if root.find(path) is None:
root.append(mesh.to_xml_element())
if mesh_memo is not None:
if mesh_memo is not None:
mesh_memo.add(mesh.id)
elif key == 'adjoint_source':
subelement = ET.SubElement(element, 'adjoint_source')
# Check that all entries are valid SourceBase instances, in case
# Check that all entries are valid SourceBase instances, in case
# the random_ray setter was not used to populate dict entries.
if not isinstance(value, MutableSequence):
value = [value]

View file

@ -392,6 +392,11 @@ void RandomRaySimulation::simulate()
// Begin main simulation timer
simulation::time_total.start();
// Reset per-solve accumulators, as simulate() may run more than once on the
// same object (e.g. forward then adjoint when generating weight windows)
avg_miss_rate_ = 0.0;
total_geometric_intersections_ = 0;
// Random ray power iteration loop
while (simulation::current_batch < settings::n_batches) {
// Initialize the current batch

View file

@ -946,6 +946,11 @@ void apply_weight_windows(Particle& p)
if (!settings::weight_windows_on)
return;
// Random ray rays are not Monte Carlo particles and must not be biased by
// weight windows; the solver generates weight windows but never applies them
if (settings::solver_type == SolverType::RANDOM_RAY)
return;
// WW on photon and neutron only
if (!p.type().is_neutron() && !p.type().is_photon())
return;
@ -1004,14 +1009,27 @@ void apply_weight_window(Particle& p, WeightWindow weight_window)
if (p.ww_factor() > 1.0)
weight_window.scale(p.ww_factor());
// if particle's weight is above the weight window split until they are within
// the window
if (weight > weight_window.upper_weight) {
// If the particle's weight is above the weight window, split it until the
// resulting particles are within the window. The comparisons use a relative
// dead band so that the branch taken is insensitive to bit-level differences
// in the window bounds (see WEIGHT_WINDOW_REL_TOL).
if (weight > weight_window.upper_weight * (1.0 + WEIGHT_WINDOW_REL_TOL)) {
// do not further split the particle if above the limit
if (p.n_split() >= settings::max_history_splits)
return;
double n_split = std::ceil(weight / weight_window.upper_weight);
// Dividing by the same dead-banded bound used in the branch condition
// keeps the number of splits stable when the weight-to-bound ratio sits
// within rounding of an exact integer, which the weight window arithmetic
// itself can produce (e.g., a roulette survivor assigned weight *
// max_split, later split against an upper bound that is an exact multiple
// of the same lower bound). Ratios within the dead band of an integer
// consistently round down, and the branch condition guarantees the ratio
// exceeds one; the lower clamp of 2 makes the always-splits invariant
// explicit.
double n_split = std::max(2.0,
std::ceil(
weight / ((1.0 + WEIGHT_WINDOW_REL_TOL) * weight_window.upper_weight)));
double max_split = weight_window.max_split;
n_split = std::min(n_split, max_split);
@ -1025,7 +1043,8 @@ void apply_weight_window(Particle& p, WeightWindow weight_window)
// remaining weight is applied to current particle
p.wgt() = weight / n_split;
} else if (weight <= weight_window.lower_weight) {
} else if (weight <
weight_window.lower_weight * (1.0 - WEIGHT_WINDOW_REL_TOL)) {
// if the particle weight is below the window, play Russian roulette
double weight_survive =
std::min(weight * weight_window.max_split, weight_window.survival_weight);

View file

@ -27,7 +27,8 @@ def test_random_ray_auto_convert(method):
# Convert to a multi-group model
model.convert_to_multigroup(
method=method, groups='CASMO-2', nparticles=100,
method=method, groups='CASMO-2',
particles=100,
overwrite_mgxs_library=False, mgxs_path="mgxs.h5"
)

View file

@ -0,0 +1,80 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" name="Air">
<density value="0.001225" units="g/cm3"/>
<nuclide name="N14" ao="0.79"/>
<nuclide name="O16" ao="0.21"/>
</material>
<material id="2" name="Concrete">
<density value="2.3" units="g/cm3"/>
<nuclide name="H1" ao="0.168759"/>
<nuclide name="O16" ao="0.562489"/>
<nuclide name="Si28" ao="0.203031"/>
<nuclide name="Ca40" ao="0.044849"/>
<nuclide name="Al27" ao="0.020872"/>
</material>
<material id="3" name="Steel">
<density value="7.87" units="g/cm3"/>
<nuclide name="Fe56" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="1" name="cavity" material="1" region="-9 8 -11 10 -13 12" universe="1"/>
<cell id="2" name="pocket" material="3" region="-15 14 -17 16 -19 18 -1" universe="1"/>
<cell id="3" name="concrete" material="2" region="-1 (9 | -8 | 11 | -10 | 13 | -12) (15 | -14 | 17 | -16 | 19 | -18 | 1)" universe="1"/>
<cell id="4" name="void" material="void" region="1 -3 2 -5 4 -7 6" universe="1"/>
<surface id="1" type="sphere" coeffs="0.0 0.0 0.0 66.0"/>
<surface id="2" type="x-plane" boundary="vacuum" coeffs="-70.0"/>
<surface id="3" type="x-plane" boundary="vacuum" coeffs="70.0"/>
<surface id="4" type="y-plane" boundary="vacuum" coeffs="-70.0"/>
<surface id="5" type="y-plane" boundary="vacuum" coeffs="70.0"/>
<surface id="6" type="z-plane" boundary="vacuum" coeffs="-70.0"/>
<surface id="7" type="z-plane" boundary="vacuum" coeffs="70.0"/>
<surface id="8" type="x-plane" coeffs="-60.0"/>
<surface id="9" type="x-plane" coeffs="-48.0"/>
<surface id="10" type="y-plane" coeffs="-6.0"/>
<surface id="11" type="y-plane" coeffs="6.0"/>
<surface id="12" type="z-plane" coeffs="-6.0"/>
<surface id="13" type="z-plane" coeffs="6.0"/>
<surface id="14" type="x-plane" coeffs="44.0"/>
<surface id="15" type="x-plane" coeffs="67.0"/>
<surface id="16" type="y-plane" coeffs="-4.0"/>
<surface id="17" type="y-plane" coeffs="4.0"/>
<surface id="18" type="z-plane" coeffs="-4.0"/>
<surface id="19" type="z-plane" coeffs="4.0"/>
</geometry>
<settings>
<run_mode>fixed source</run_mode>
<particles>20</particles>
<batches>10</batches>
<source type="independent" strength="1.0" particle="neutron">
<space type="box">
<parameters>-60.0 -6.0 -6.0 -48.0 6.0 6.0</parameters>
</space>
<angle type="isotropic"/>
<energy type="discrete">
<parameters>2000000.0 1.0</parameters>
</energy>
<constraints>
<domain_type>cell</domain_type>
<domain_ids>1</domain_ids>
</constraints>
</source>
<weight_windows_file>weight_windows.h5</weight_windows_file>
<weight_window_checkpoints>
<collision>true</collision>
<surface>true</surface>
</weight_window_checkpoints>
<max_history_splits>100000</max_history_splits>
</settings>
<tallies>
<filter id="193" type="cell">
<bins>1 2 3 4</bins>
</filter>
<tally id="405" name="flux">
<filters>193</filters>
<scores>flux</scores>
</tally>
</tallies>
</model>

View file

@ -0,0 +1,9 @@
tally 1:
9.500337E+01
9.709273E+02
2.417257E-04
1.153221E-08
9.046900E+02
8.904339E+04
1.034722E+02
1.134807E+03

View file

@ -0,0 +1,101 @@
import copy
import os
from pathlib import Path
import numpy as np
import openmc
import openmc.mgxs
from openmc.examples import sphere_with_shielded_pocket
from tests.testing_harness import TolerantPyAPITestHarness
GROUPS = 'CASMO-4'
class MGXSTestHarness(TolerantPyAPITestHarness):
def _cleanup(self):
super()._cleanup()
for f in ('mgxs.h5', 'weight_windows.h5'):
if os.path.exists(f):
os.remove(f)
def generate_weight_windows(model, mesh):
"""Convert a multigroup model to random ray and run a FW-CADIS weight
window generation solve, producing weight_windows.h5."""
model.convert_to_random_ray()
rr = model.settings.random_ray
rr['source_region_meshes'] = [(mesh, [model.geometry.root_universe])]
rr['distance_inactive'] = 100.0
rr['distance_active'] = 400.0
rr['source_shape'] = 'flat'
rr['sample_method'] = 'halton'
rr['volume_estimator'] = 'hybrid'
model.settings.particles = 1000
model.settings.batches = 20
model.settings.inactive = 15
model.settings.weight_window_generators = openmc.WeightWindowGenerator(
method='fw_cadis', mesh=mesh, max_realizations=20,
energy_bounds=openmc.mgxs.EnergyGroups(GROUPS).group_edges)
model.run()
def test_random_ray_auto_convert_bootstrap():
# Tests the weight window bootstrapped MGXS generation workflow, which
# consists of five OpenMC runs: a stochastic_slab MGXS generation, a random
# ray FW-CADIS weight window generation, a material_wise MGXS generation
# using those weight windows (which reach the steel pocket that an analog
# solve essentially never tallies), a second FW-CADIS generation using the
# bootstrapped library, and a final continuous energy Monte Carlo solve
# using the improved weight windows, whose tallies are compared against
# the reference results.
openmc.reset_auto_ids()
model = sphere_with_shielded_pocket()
# Used by the continuous energy solves below; these have no effect on the
# random ray solves (random ray generates weight windows but never applies
# them).
model.settings.weight_window_checkpoints = {
'collision': True, 'surface': True}
model.settings.max_history_splits = 100_000
# Overlay a ~10 cm source region / weight window mesh on the geometry
bbox = model.geometry.bounding_box
mesh = openmc.RegularMesh()
mesh.dimension = np.round(
(np.asarray(bbox.upper_right) - np.asarray(bbox.lower_left)) / 10.0
).astype(int)
mesh.lower_left = bbox.lower_left
mesh.upper_right = bbox.upper_right
# Generate weight windows covering the whole problem with the
# stochastic_slab method and a random ray FW-CADIS solve
slab_model = copy.deepcopy(model)
slab_model.convert_to_multigroup(
method='stochastic_slab', groups=GROUPS, particles=50,
overwrite_mgxs_library=True, mgxs_path='mgxs.h5')
generate_weight_windows(slab_model, mesh)
# Bootstrap the material-wise MGXS generation with those weight windows,
# then regenerate the weight windows from the higher-fidelity library
boot_model = copy.deepcopy(model)
boot_model.convert_to_multigroup(
groups=GROUPS, particles=1,
weight_windows_file=Path('weight_windows.h5').resolve(),
overwrite_mgxs_library=True, mgxs_path='mgxs.h5')
generate_weight_windows(boot_model, mesh)
# Run the continuous energy model with the improved weight windows,
# tallying the flux in every region
model.settings.weight_windows_file = 'weight_windows.h5'
model.settings.particles = 20
model.settings.batches = 10
tally = openmc.Tally(name='flux')
tally.filters = [
openmc.CellFilter(list(model.geometry.get_all_cells().values()))]
tally.scores = ['flux']
model.tallies = openmc.Tallies([tally])
harness = MGXSTestHarness('statepoint.10.h5', model)
harness.main()

View file

@ -27,7 +27,8 @@ def test_random_ray_auto_convert(method):
# Convert to a multi-group model
model.convert_to_multigroup(
method=method, groups='CASMO-2', nparticles=100,
method=method, groups='CASMO-2',
particles=100,
overwrite_mgxs_library=False, mgxs_path="mgxs.h5"
)

View file

@ -38,7 +38,8 @@ def test_random_ray_auto_convert_source_energy(method, source_type):
# Convert to a multi-group model
model.convert_to_multigroup(
method=method, groups='CASMO-8', nparticles=100,
method=method, groups='CASMO-8',
particles=100,
overwrite_mgxs_library=False, mgxs_path="mgxs.h5",
source_energy=source_energy
)

View file

@ -34,9 +34,10 @@ def test_random_ray_auto_convert(method):
# Convert to a multi-group model
model.convert_to_multigroup(
method=method, groups='CASMO-2', nparticles=100,
method=method, groups='CASMO-2',
particles=100, temperature=temp_settings,
overwrite_mgxs_library=False, mgxs_path="mgxs.h5",
temperatures=[294.0, 394.0], temperature_settings=temp_settings
temperatures=[294.0, 394.0]
)
# Convert to a random ray model

View file

@ -1,5 +1,6 @@
import os
import openmc
from openmc.examples import pwr_pin_cell
from openmc import RegularMesh
@ -23,7 +24,8 @@ def test_random_ray_diagonal_stabilization():
# MGXS data with some negatives on the diagonal, in order
# to trigger diagonal correction.
model.convert_to_multigroup(
method='material_wise', groups='CASMO-70', nparticles=13,
method='material_wise', groups='CASMO-70',
particles=13,
overwrite_mgxs_library=True, mgxs_path="mgxs.h5", correction='P0'
)

View file

@ -1 +1 @@
a78972fe3c0dadfc256cfb139c5ca8c7b634738e1895ad2d6286ed5e2567c6dc518e499363908e9058432684148a706a179a39110f703d5322491184a1d0c3e4
41d8ba482783b724c61bf058a67ff701109076ac460f0be69b3a33c7a9e3e2aa5b5773442d33561b71c69500a9535f1ffb0f5e81f6ce364288c5094bbbc0a292

View file

@ -45,7 +45,7 @@ def test_convert_to_multigroup_without_particles_batches(run_in_tmpdir):
model.convert_to_multigroup(
method='material_wise',
groups='CASMO-2',
nparticles=10,
particles=10,
overwrite_mgxs_library=True
)

View file

@ -1090,3 +1090,159 @@ def test_convert_to_multigroup_preserves_material_names(run_in_tmpdir):
macro = [m._macroscopic for m in model.materials]
assert macro == [f"Steel_Plate__1_{a.id}", f"Steel_Plate__1_{b.id}"]
assert len(set(macro)) == 2
class _GenerationCaptured(Exception):
"""Raised by the patched MGXS library builder to end generation early."""
def _steel_water_model():
a = openmc.Material(name='steel')
a.add_element('Fe', 1.0)
a.set_density('g/cm3', 7.9)
b = openmc.Material(name='water')
b.add_element('H', 2.0)
b.add_element('O', 1.0)
b.set_density('g/cm3', 1.0)
s1 = openmc.Sphere(r=1.0)
s2 = openmc.Sphere(r=2.0, boundary_type='vacuum')
c1 = openmc.Cell(fill=a, region=-s1)
c2 = openmc.Cell(fill=b, region=+s1 & -s2)
return openmc.Model(openmc.Geometry([c1, c2]), openmc.Materials([a, b]))
def _capture_generation_settings(monkeypatch, model, **kwargs):
"""Return the Settings of the MGXS generation run by capturing the
generation model just before transport would start."""
captured = {}
def fake_auto_generate(gen_model, groups, correction, directory):
captured['settings'] = gen_model.settings
raise _GenerationCaptured()
monkeypatch.setattr(openmc.Model, '_auto_generate_mgxs_lib',
fake_auto_generate)
with pytest.raises(_GenerationCaptured):
model.convert_to_multigroup(**kwargs)
return captured['settings']
def test_convert_to_multigroup_material_wise(run_in_tmpdir, monkeypatch):
model = _steel_water_model()
model.settings.run_mode = 'fixed source'
model.settings.source = openmc.IndependentSource(space=openmc.stats.Point())
model.settings.photon_transport = True
model.settings.batches = 1200 # tuned for the final multigroup solve
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise', particles=12345, seed=7)
# Keyword-argument overrides take precedence over the generation
# defaults...
assert gen.particles == 12345
assert gen.seed == 7
# ...the generation defaults override the model's own settings...
assert gen.batches == 200
assert gen.inactive == 100
assert gen.output == {'summary': True, 'tallies': False}
# ...and everything else is inherited from the model
assert gen.run_mode == 'fixed source'
assert gen.photon_transport is True
assert len(gen.source) == 1
# The run mode is owned by the generation method: material_wise always
# takes it from the model, even when given as an override
model.settings.run_mode = 'eigenvalue'
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise', run_mode='fixed source')
assert gen.run_mode == 'eigenvalue'
def test_convert_to_multigroup_stochastic_slab(run_in_tmpdir, monkeypatch):
model = _steel_water_model()
src = openmc.IndependentSource(space=openmc.stats.Point())
with pytest.warns(UserWarning, match='constructs its own'):
gen = _capture_generation_settings(
monkeypatch, model, method='stochastic_slab', particles=999,
batches=50, max_history_splits=42, source=src)
assert gen.particles == 999
assert gen.batches == 50
assert gen.max_history_splits == 42
# The method constructs its own source and run mode; the user's source
# was discarded with a warning
assert gen.run_mode == 'fixed source'
assert gen.create_fission_neutrons is False
assert len(gen.source) > 0
assert all(s is not src for s in gen.source)
def test_convert_to_multigroup_weight_windows(run_in_tmpdir, monkeypatch):
model = _steel_water_model()
ww_path = Path('ww.h5').resolve()
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise',
weight_windows_file=ww_path)
# A weight windows file is passed through to the "material_wise"
# generation run (specifying a file turns weight windows on at run time)
assert gen.weight_windows_file == ww_path
# An explicit weight_windows_on=False rides along with the file and
# overrides the run-time file-implied enable
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise',
weight_windows_file=ww_path, weight_windows_on=False)
assert gen.weight_windows_file == ww_path
assert gen.weight_windows_on is False
# The surrogate-geometry methods ignore the file with a warning
with pytest.warns(UserWarning, match='material_wise'):
gen = _capture_generation_settings(
monkeypatch, model, method='stochastic_slab',
weight_windows_file=ww_path)
assert gen.weight_windows_file is None
def test_convert_to_multigroup_validation(run_in_tmpdir):
model = _steel_water_model()
with pytest.raises(ValueError):
model.convert_to_multigroup(method='not_a_method')
# The deprecated arguments cannot be combined with keyword overrides
with pytest.raises(ValueError, match='deprecated'), \
pytest.warns(FutureWarning):
model.convert_to_multigroup(nparticles=1000, particles=500)
with pytest.raises(ValueError, match='deprecated'), \
pytest.warns(FutureWarning):
model.convert_to_multigroup(
temperature_settings={'method': 'interpolation'},
temperature={'method': 'interpolation'})
def test_convert_to_multigroup_deprecated_args(run_in_tmpdir, monkeypatch):
# The deprecated arguments still work, with a FutureWarning
model = _steel_water_model()
with pytest.warns(FutureWarning, match='deprecated'):
gen = _capture_generation_settings(
monkeypatch, model, method='stochastic_slab', nparticles=1234,
temperature_settings={'method': 'interpolation'})
assert gen.particles == 1234
assert gen.temperature == {'method': 'interpolation'}
assert gen.batches == 200 # generation default unchanged
def test_convert_to_multigroup_materials_from_geometry(run_in_tmpdir):
# A model whose materials are only referenced through the geometry gets
# its materials collection populated (sorted by ID) for the conversion
model = _steel_water_model()
model.materials = openmc.Materials()
ids = sorted(model.geometry.get_all_materials())
Path('mgxs.h5').touch()
model.convert_to_multigroup(
method='stochastic_slab', mgxs_path='mgxs.h5')
assert [m.id for m in model.materials] == ids

View file

@ -249,7 +249,7 @@ def test_photon_heating(run_in_tmpdir, shared_secondary):
model.settings.run_mode = 'fixed source'
model.settings.batches = 5
model.settings.particles = 101
model.settings.particles = 100
model.settings.shared_secondary_bank = shared_secondary
tally = openmc.Tally()

View file

@ -366,7 +366,7 @@ def test_ww_generation_with_dagmc(run_in_tmpdir):
rr_model.convert_to_multigroup(
method="stochastic_slab",
overwrite_mgxs_library=True,
nparticles=10,
particles=10,
groups="CASMO-2"
)