This commit is contained in:
John Tramm 2026-07-20 09:52:43 -05:00 committed by GitHub
commit 340bb127e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 875 additions and 194 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 the generation
settings (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 via the ``settings`` parameter. Only the attributes that are
populated on the passed :class:`openmc.Settings` object override the
generation defaults, so a sparse object adjusts just the fields you set. 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(settings=openmc.Settings(particles=100_000))
.. note::
MGXS transport correction (via setting the ``correction`` parameter in the
@ -739,12 +747,10 @@ 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
temperature settings used during cross section generation default to those
contained in the model and can be customized by setting
:attr:`openmc.Settings.temperature` on the object passed via the ``settings``
parameter. 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.
@ -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 setting ``weight_windows_file``
on the generation settings::
# 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.
settings = openmc.Settings(weight_windows_file="weight_windows.h5")
model.convert_to_multigroup(settings=settings, overwrite_mgxs_library=True)
A weight windows file on the generation settings 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,116 @@ 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 which 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,13 @@ 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 +2122,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 +2156,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 +2173,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 +2181,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 +2208,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 +2309,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 +2325,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 +2335,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 +2352,13 @@ 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 +2379,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 +2402,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 +2433,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 +2445,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 +2467,10 @@ class Model:
raw_mgxs_sets[temperature] = Model._isothermal_stochastic_slab_mgxs(
geo,
groups,
nparticles,
settings,
correction,
directory,
src,
temp_settings,
temperature
)
@ -2539,10 +2492,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 +2511,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 +2535,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 +2558,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 +2578,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 +2591,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 +2606,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 +2626,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,
settings: openmc.Settings | None = None,
):
"""Convert all materials from continuous energy to multigroup.
@ -2732,6 +2653,11 @@ 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
Set ``particles`` on the object passed via the ``settings``
argument instead.
overwrite_mgxs_library : bool, optional
Whether to overwrite an existing MGXS library file.
mgxs_path : str, optional
@ -2765,10 +2691,119 @@ 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
Set ``temperature`` on the object passed via the ``settings``
argument instead.
settings : openmc.Settings, optional
Settings overrides for the continuous energy simulation(s) used
to generate the MGXS library. Attributes populated on this
object take precedence over the generation defaults, so a
sparse object such as ``openmc.Settings(particles=100_000)``
adjusts just that field (see :meth:`openmc.Settings.update`).
The run mode cannot be overridden, as it is determined by the
generation method. 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'))
if settings is not None:
check_type('settings', settings, openmc.Settings)
# 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 a Settings object with the desired '
'attributes via the "settings" argument instead.',
FutureWarning)
if settings is not None:
raise ValueError(
'The deprecated "nparticles" and "temperature_settings" '
'arguments cannot be combined with the "settings" '
'argument.')
# 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 any attributes the
# user populated on the provided settings.
user_settings = settings
if method == 'material_wise':
settings = copy.deepcopy(self.settings)
else:
settings = openmc.Settings()
settings.temperature = copy.deepcopy(self.settings.temperature)
# The surrogate-geometry methods treat fission as capture
# (nu-fission is still tallied)
settings.create_fission_neutrons = False
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 sources defined in "settings" are ignored by the '
f'"{method}" MGXS generation method, which constructs '
'its own sources.')
settings.update(user_settings)
# 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.
if method == 'material_wise':
settings.run_mode = self.settings.run_mode
else:
settings.run_mode = 'fixed source'
# 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 set on the generation settings '
'is only applicable to the "material_wise" MGXS '
f'generation method and will be ignored for the '
f'"{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 +2839,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

@ -1,4 +1,5 @@
from collections.abc import Iterable, Mapping, MutableSequence, Sequence
import copy
from enum import Enum
import itertools
from math import ceil
@ -514,6 +515,51 @@ class Settings:
warnings.warn(msg, stacklevel=2)
super().__setattr__(name, value)
def update(self, other: 'Settings'):
"""Update this object with all populated attributes of another instance.
Every attribute of `other` that has been populated -- i.e., that is
not None and not an empty collection -- is copied to this object,
overwriting any existing value. Attributes that were never set on
`other` leave the corresponding attribute of this object untouched.
This allows a sparsely-populated Settings object to be applied as a
set of overrides on top of fully-populated defaults.
Note that :attr:`run_mode` always carries a value, so an explicitly
assigned 'eigenvalue' run mode cannot be distinguished from the
default; the run mode is therefore only copied from `other` when it
differs from 'eigenvalue'.
.. versionadded:: 0.15.4
Parameters
----------
other : openmc.Settings
Settings object whose populated attributes are applied to this
object.
"""
cv.check_type('other', other, Settings)
for name, value in vars(other).items():
# run_mode defaults to 'eigenvalue' rather than to an "unset"
# state, so only a non-default value is detectable (see note in
# the docstring).
if name == '_run_mode':
if value is not RunMode.EIGENVALUE:
self._run_mode = value
continue
# None or an empty collection means the attribute was never set
# on `other` -- the same convention to_xml_element() relies on
# to decide which elements to export.
if value is None or (hasattr(value, '__len__') and len(value) == 0):
continue
# Values on `other` were already validated by its property
# setters; deepcopy so later mutations of `other` cannot leak
# into this object.
setattr(self, name, copy.deepcopy(value))
@property
def run_mode(self) -> str:
return self._run_mode.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,25 @@ 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);
// The (1 - WEIGHT_WINDOW_REL_TOL) factor 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; the lower
// clamp of 2 preserves the guarantee that the split branch always splits.
double n_split =
std::max(2.0, std::ceil((1.0 - WEIGHT_WINDOW_REL_TOL) * weight /
weight_window.upper_weight));
double max_split = weight_window.max_split;
n_split = std::min(n_split, max_split);
@ -1025,7 +1041,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',
settings=openmc.Settings(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,103 @@
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,
settings=openmc.Settings(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_settings = openmc.Settings(particles=1)
boot_settings.weight_windows_file = Path('weight_windows.h5').resolve()
boot_model.convert_to_multigroup(
groups=GROUPS, settings=boot_settings,
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',
settings=openmc.Settings(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',
settings=openmc.Settings(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',
settings=openmc.Settings(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',
settings=openmc.Settings(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,
settings=openmc.Settings(particles=10),
overwrite_mgxs_library=True
)

View file

@ -1090,3 +1090,177 @@ 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_settings_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
user = openmc.Settings(particles=12345, seed=7)
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise', settings=user)
# User-populated attributes override 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 caller's object is never mutated
assert user.batches is None
# The run mode is owned by the generation method: material_wise always
# takes it from the model, even when set on the provided settings
model.settings.run_mode = 'eigenvalue'
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise',
settings=openmc.Settings(run_mode='fixed source'))
assert gen.run_mode == 'eigenvalue'
def test_convert_to_multigroup_settings_stochastic_slab(run_in_tmpdir, monkeypatch):
model = _steel_water_model()
user = openmc.Settings(particles=999, batches=50, max_history_splits=42)
user.source = openmc.IndependentSource(space=openmc.stats.Point())
with pytest.warns(UserWarning, match='constructs its own'):
gen = _capture_generation_settings(
monkeypatch, model, method='stochastic_slab', settings=user)
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 user.source[0] for s in gen.source)
# The caller's object is never mutated
assert len(user.source) == 1
def test_convert_to_multigroup_settings_weight_windows(run_in_tmpdir, monkeypatch):
model = _steel_water_model()
ww_path = Path('ww.h5').resolve()
user = openmc.Settings(weight_windows_file=ww_path)
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise', settings=user)
# A weight windows file on the generation settings 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
# The caller's object is never mutated
assert user.weight_windows_file == ww_path
# An explicit weight_windows_on=False rides along with the file and
# overrides the run-time file-implied enable
user = openmc.Settings(weight_windows_file=ww_path,
weight_windows_on=False)
gen = _capture_generation_settings(
monkeypatch, model, method='material_wise', settings=user)
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', settings=user)
assert gen.weight_windows_file is None
assert user.weight_windows_file == ww_path
def test_convert_to_multigroup_settings_validation(run_in_tmpdir):
model = _steel_water_model()
with pytest.raises(TypeError):
model.convert_to_multigroup(settings={'particles': 100})
with pytest.raises(ValueError):
model.convert_to_multigroup(method='not_a_method')
# The deprecated arguments cannot be combined with settings
with pytest.raises(ValueError, match='deprecated'), \
pytest.warns(FutureWarning):
model.convert_to_multigroup(nparticles=1000,
settings=openmc.Settings())
with pytest.raises(ValueError, match='deprecated'), \
pytest.warns(FutureWarning):
model.convert_to_multigroup(
temperature_settings={'method': 'interpolation'},
settings=openmc.Settings())
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, monkeypatch):
# 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())
def fake_auto_generate(gen_model, groups, correction, directory):
raise _GenerationCaptured()
monkeypatch.setattr(openmc.Model, '_auto_generate_mgxs_lib',
fake_auto_generate)
with pytest.raises(_GenerationCaptured):
model.convert_to_multigroup(method='stochastic_slab')
assert [m.id for m in model.materials] == ids

View file

@ -192,6 +192,35 @@ def test_export_to_xml(run_in_tmpdir):
assert s.free_gas_threshold == 800.0
def test_update():
base = openmc.Settings(particles=100, batches=10,
output={'tallies': False})
other = openmc.Settings(particles=500)
other.source = openmc.IndependentSource(space=openmc.stats.Point())
base.update(other)
# Populated attributes of `other` overwrite those of `base`...
assert base.particles == 500
assert len(base.source) == 1
# ...while unset attributes leave `base` untouched
assert base.batches == 10
assert base.output == {'tallies': False}
# Copied values are independent of `other`
assert base.source[0] is not other.source[0]
# run_mode always carries a value, so only a non-default ('eigenvalue')
# run mode is detectable and copied
base.run_mode = 'fixed source'
base.update(openmc.Settings())
assert base.run_mode == 'fixed source'
base.update(openmc.Settings(run_mode='volume'))
assert base.run_mode == 'volume'
with pytest.raises(TypeError):
base.update({'particles': 5})
def test_properties_file_load(tmp_path, mpi_intracomm):
model = openmc.examples.pwr_assembly()

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,
settings=openmc.Settings(particles=10),
groups="CASMO-2"
)