diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst
index d5d752a83..881498a0a 100644
--- a/docs/source/usersguide/random_ray.rst
+++ b/docs/source/usersguide/random_ray.rst
@@ -644,7 +644,8 @@ model to use these multigroup cross sections. An example is given below::
nparticles=2000,
overwrite_mgxs_library=False,
mgxs_path="mgxs.h5",
- correction=None
+ correction=None,
+ source_energy=None
)
The most important parameter to set is the ``method`` parameter, which can be
@@ -706,6 +707,31 @@ generation and use an existing library file.
with a :math:`\rho` default value of 1.0, which can be adjusted with the
``settings.random_ray['diagonal_stabilization_rho']`` parameter.
+When generating MGXS data with either the ``stochastic_slab`` or
+``infinite_medium`` methods, by default the simulation will use a uniform source
+distribution spread evenly over all energy groups. This ensures that all energy
+groups receive tallies and therefore produce non-zero total multigroup cross
+sections. Additionally, the function will convert any sources in the model into
+simplified spatial sources that retain the original energy distributions. If
+sources are present, they will be used 99% of the time to sample source energies
+during MGXS generation. The other 1% of the time, energies will be sampled
+uniformly over all energy groups to ensure that all groups receive some tallies.
+However, the user may wish to specify a different source energy spectrum (for
+instance, if they are using a FileSource, such that the energy distribution
+cannot be extracted from the python source object). This can be done by
+providing a :class:`openmc.stats.Univariate` distribution as the
+``source_energy`` parameter of the :meth:`openmc.Model.convert_to_multigroup`
+method. If provided, it will override any sources present in the model and will
+be used 99% of the time to sample source energies during MGXS generation. The
+other 1% of the time, energies will be sampled uniformly over all energy groups
+to ensure that all groups receive some tallies.
+
+For instance, a D-D fusion simulation may involve a complex file source. In this
+case, the user may wish to provide a discrete 2.45 MeV energy source
+distribution for MGXS generation as::
+
+ source_energy = openmc.stats.delta_function(2.45e6)
+
Ultimately, the methods described above are all just approximations.
Approximations in the generated MGXS data will fundamentally limit the potential
accuracy of the random ray solver. However, the methods described above are all
diff --git a/openmc/model/model.py b/openmc/model/model.py
index 48ae9f0b9..299724759 100644
--- a/openmc/model/model.py
+++ b/openmc/model/model.py
@@ -1687,6 +1687,91 @@ class Model:
self.geometry.get_all_materials().values()
)
+ def _create_mgxs_sources(
+ self,
+ groups: openmc.mgxs.EnergyGroups,
+ spatial_dist: openmc.stats.Spatial,
+ source_energy: openmc.stats.Univariate | None = None,
+ ) -> list[openmc.IndependentSource]:
+ """Create a list of independent sources to use with MGXS generation.
+
+ Note that in all cases, a discrete source that is uniform over all
+ energy groups is created (strength = 0.01) to ensure that total cross
+ sections are generated for all energy groups. In the case that the user
+ has provided a source_energy distribution as an argument, an additional
+ source (strength = 0.99) is created using that energy distribution. If
+ the user has not provided a source_energy distribution, but the model
+ has sources defined, and all of those sources are of IndependentSource
+ type, then additional sources are created based on the model's existing
+ sources, keeping their energy distributions but replacing their
+ spatial/angular distributions, with their combined strength being 0.99.
+ If the user has not provided a source_energy distribution and no sources
+ are defined on the model and the run mode is 'eigenvalue', then a
+ default Watt spectrum source (strength = 0.99) is added.
+
+ Parameters
+ ----------
+ groups : openmc.mgxs.EnergyGroups
+ Energy group structure for the MGXS.
+ spatial_dist : openmc.stats.Spatial
+ Spatial distribution to use for all sources.
+ source_energy : openmc.stats.Univariate, optional
+ Energy distribution to use when generating MGXS data, replacing any
+ existing sources in the model.
+
+ Returns
+ -------
+ list[openmc.IndependentSource]
+ A list of independent sources to use for MGXS generation.
+ """
+ # Make a discrete source that is uniform over the bins of the group structure
+ midpoints = []
+ strengths = []
+ for i in range(groups.num_groups):
+ bounds = groups.get_group_bounds(i+1)
+ midpoints.append((bounds[0] + bounds[1]) / 2.0)
+ strengths.append(1.0)
+
+ uniform_energy = openmc.stats.Discrete(x=midpoints, p=strengths)
+ uniform_distribution = openmc.IndependentSource(spatial_dist, energy=uniform_energy, strength=0.01)
+ sources = [uniform_distribution]
+
+ # If the user provided an energy distribution, use that
+ if source_energy is not None:
+ user_energy = openmc.IndependentSource(
+ space=spatial_dist, energy=source_energy, strength=0.99)
+ sources.append(user_energy)
+
+ # If the user did not provide an energy distribution, create sources
+ # based on what is in their model, keeping the energy spectrum but
+ # replacing the spatial/angular distributions. We only do this if ALL
+ # sources are of IndependentSource type, as we can't pull the energy
+ # distribution from e.g. CompiledSource or FileSource types.
+ else:
+ if self.settings.source is not None:
+ for src in self.settings.source:
+ if not isinstance(src, openmc.IndependentSource):
+ break
+ else:
+ n_user_sources = len(self.settings.source)
+ for src in self.settings.source:
+ # Create a new IndependentSource with adjusted strength, space, and angle
+ user_source = openmc.IndependentSource(
+ space=spatial_dist,
+ energy=src.energy,
+ strength=0.99 / n_user_sources
+ )
+ sources.append(user_source)
+ else:
+ # No user sources defined. If we are in eigenvalue mode, then use the default Watt spectrum.
+ if self.settings.run_mode == 'eigenvalue':
+ watt_energy = openmc.stats.Watt()
+ watt_source = openmc.IndependentSource(
+ space=spatial_dist, energy=watt_energy, strength=0.99)
+ sources.append(watt_source)
+
+ return sources
+
def _generate_infinite_medium_mgxs(
self,
groups: openmc.mgxs.EnergyGroups,
@@ -1694,6 +1779,7 @@ class Model:
mgxs_path: PathLike,
correction: str | None,
directory: PathLike,
+ source_energy: openmc.stats.Univariate | None = None,
):
"""Generate a MGXS library by running multiple OpenMC simulations, each
representing an infinite medium simulation of a single isolated
@@ -1702,6 +1788,20 @@ class Model:
method that ignores all spatial self shielding effects and all resonance
shielding effects between materials.
+ Note that in all cases, a discrete source that is uniform over all
+ energy groups is created (strength = 0.01) to ensure that total cross
+ sections are generated for all energy groups. In the case that the user
+ has provided a source_energy distribution as an argument, an additional
+ source (strength = 0.99) is created using that energy distribution. If
+ the user has not provided a source_energy distribution, but the model
+ has sources defined, and all of those sources are of IndependentSource
+ type, then additional sources are created based on the model's existing
+ sources, keeping their energy distributions but replacing their
+ spatial/angular distributions, with their combined strength being 0.99.
+ If the user has not provided a source_energy distribution and no sources
+ are defined on the model and the run mode is 'eigenvalue', then a
+ default Watt spectrum source (strength = 0.99) is added.
+
Parameters
----------
groups : openmc.mgxs.EnergyGroups
@@ -1715,9 +1815,10 @@ class Model:
"P0".
directory : str
Directory to run the simulation in, so as to contain XML files.
+ source_energy : openmc.stats.Univariate, optional
+ Energy distribution to use when generating MGXS data, replacing any
+ existing sources in the model.
"""
- warnings.warn("The infinite medium method of generating MGXS may hang "
- "if a material has a k-infinity > 1.0.")
mgxs_sets = []
for material in self.materials:
model = openmc.Model()
@@ -1728,20 +1829,16 @@ class Model:
# Settings
model.settings.batches = 100
model.settings.particles = nparticles
+
+ model.settings.source = self._create_mgxs_sources(
+ groups,
+ spatial_dist=openmc.stats.Point(),
+ source_energy=source_energy
+ )
+
model.settings.run_mode = 'fixed source'
+ model.settings.create_fission_neutrons = False
- # Make a discrete source that is uniform over the bins of the group structure
- n_groups = groups.num_groups
- midpoints = []
- strengths = []
- for i in range(n_groups):
- bounds = groups.get_group_bounds(i+1)
- midpoints.append((bounds[0] + bounds[1]) / 2.0)
- strengths.append(1.0)
-
- energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths)
- model.settings.source = openmc.IndependentSource(
- space=openmc.stats.Point(), energy=energy_distribution)
model.settings.output = {'summary': True, 'tallies': False}
# Geometry
@@ -1891,6 +1988,7 @@ class Model:
mgxs_path: PathLike,
correction: str | None,
directory: PathLike,
+ source_energy: openmc.stats.Univariate | None = None,
) -> None:
"""Generate MGXS assuming a stochastic "sandwich" of materials in a layered
slab geometry. While geometry-specific spatial shielding effects are not
@@ -1915,6 +2013,23 @@ class Model:
"P0".
directory : str
Directory to run the simulation in, so as to contain XML files.
+ source_energy : openmc.stats.Univariate, optional
+ Energy distribution to use when generating MGXS data, replacing any
+ existing sources in the model. In all cases, a discrete source that
+ is uniform over all energy groups is created (strength = 0.01) to
+ ensure that total cross sections are generated for all energy
+ groups. In the case that the user has provided a source_energy
+ distribution as an argument, an additional source (strength = 0.99)
+ is created using that energy distribution. If the user has not
+ provided a source_energy distribution, but the model has sources
+ defined, and all of those sources are of IndependentSource type,
+ then additional sources are created based on the model's existing
+ sources, keeping their energy distributions but replacing their
+ spatial/angular distributions, with their combined strength being
+ 0.99. If the user has not provided a source_energy distribution and
+ no sources are defined on the model and the run mode is
+ 'eigenvalue', then a default Watt spectrum source (strength = 0.99)
+ is added.
"""
model = openmc.Model()
model.materials = self.materials
@@ -1924,24 +2039,20 @@ class Model:
model.settings.inactive = 100
model.settings.particles = nparticles
model.settings.output = {'summary': True, 'tallies': False}
- model.settings.run_mode = self.settings.run_mode
# Stochastic slab geometry
model.geometry, spatial_distribution = Model._create_stochastic_slab_geometry(
model.materials)
- # Make a discrete source that is uniform over the bins of the group structure
- n_groups = groups.num_groups
- midpoints = []
- strengths = []
- for i in range(n_groups):
- bounds = groups.get_group_bounds(i+1)
- midpoints.append((bounds[0] + bounds[1]) / 2.0)
- strengths.append(1.0)
+ # Define the sources
+ model.settings.source = self._create_mgxs_sources(
+ groups,
+ spatial_dist=spatial_distribution,
+ source_energy=source_energy
+ )
- energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths)
- model.settings.source = [openmc.IndependentSource(
- space=spatial_distribution, energy=energy_distribution, strength=1.0)]
+ model.settings.run_mode = 'fixed source'
+ model.settings.create_fission_neutrons = False
model.settings.output = {'summary': True, 'tallies': False}
@@ -2099,6 +2210,7 @@ class Model:
overwrite_mgxs_library: bool = False,
mgxs_path: PathLike = "mgxs.h5",
correction: str | None = None,
+ source_energy: openmc.stats.Univariate | None = None,
):
"""Convert all materials from continuous energy to multigroup.
@@ -2121,6 +2233,24 @@ class Model:
correction : str, optional
Transport correction to apply to the MGXS. Options are None and
"P0".
+ source_energy : openmc.stats.Univariate, optional
+ Energy distribution to use when generating MGXS data, replacing any
+ existing sources in the model. In all cases, a discrete source that
+ is uniform over all energy groups is created (strength = 0.01) to
+ ensure that total cross sections are generated for all energy
+ groups. In the case that the user has provided a source_energy
+ distribution as an argument, an additional source (strength = 0.99)
+ is created using that energy distribution. If the user has not
+ provided a source_energy distribution, but the model has sources
+ defined, and all of those sources are of IndependentSource type,
+ then additional sources are created based on the model's existing
+ sources, keeping their energy distributions but replacing their
+ spatial/angular distributions, with their combined strength being
+ 0.99. If the user has not provided a source_energy distribution and
+ no sources are defined on the model and the run mode is
+ 'eigenvalue', then a default Watt spectrum source (strength = 0.99)
+ is added. Note that this argument is only used when using the
+ "stochastic_slab" or "infinite_medium" MGXS generation methods.
"""
if isinstance(groups, str):
groups = openmc.mgxs.EnergyGroups(groups)
@@ -2150,13 +2280,13 @@ 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)
+ groups, nparticles, mgxs_path, correction, tmpdir, source_energy)
elif method == "material_wise":
self._generate_material_wise_mgxs(
groups, nparticles, mgxs_path, correction, tmpdir)
elif method == "stochastic_slab":
self._generate_stochastic_slab_mgxs(
- groups, nparticles, mgxs_path, correction, tmpdir)
+ groups, nparticles, mgxs_path, correction, tmpdir, source_energy)
else:
raise ValueError(
f'MGXS generation method "{method}" not recognized')
diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat
index ee396fa74..f984f3718 100644
--- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat
+++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat
@@ -1,2 +1,2 @@
k-combined:
-7.797252E-01 1.055731E-02
+7.479770E-01 1.624548E-02
diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat
index 86e08a710..674dee4aa 100644
--- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat
+++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat
@@ -1,2 +1,2 @@
k-combined:
-7.496641E-01 8.282032E-03
+6.413334E-01 2.083132E-02
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py b/tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat
new file mode 100644
index 000000000..80a166c67
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat
@@ -0,0 +1,61 @@
+
+
+
+ mgxs.h5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ eigenvalue
+ 100
+ 10
+ 5
+
+
+ 7000000.0 1.0
+
+
+ multi-group
+
+
+
+ -0.63 -0.63 -1.0 0.63 0.63 1.0
+
+
+ 30.0
+ 150.0
+
+
+
+
+
+ linear
+
+
+ 2 2
+ -0.63 -0.63
+ 0.63 0.63
+
+
+
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat
new file mode 100644
index 000000000..1fb09fd68
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat
@@ -0,0 +1,2 @@
+k-combined:
+7.657815E-01 2.317564E-02
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat
new file mode 100644
index 000000000..464c89a5d
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat
@@ -0,0 +1,64 @@
+
+
+
+ mgxs.h5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ eigenvalue
+ 100
+ 10
+ 5
+
+
+ -0.63 -0.63 -1 0.63 0.63 1
+
+
+ true
+
+
+ multi-group
+
+
+
+ -0.63 -0.63 -1.0 0.63 0.63 1.0
+
+
+ 30.0
+ 150.0
+
+
+
+
+
+ linear
+
+
+ 2 2
+ -0.63 -0.63
+ 0.63 0.63
+
+
+
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat
new file mode 100644
index 000000000..073c5c99f
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat
@@ -0,0 +1,2 @@
+k-combined:
+7.827784E-01 2.062954E-02
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat
new file mode 100644
index 000000000..80a166c67
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat
@@ -0,0 +1,61 @@
+
+
+
+ mgxs.h5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ eigenvalue
+ 100
+ 10
+ 5
+
+
+ 7000000.0 1.0
+
+
+ multi-group
+
+
+
+ -0.63 -0.63 -1.0 0.63 0.63 1.0
+
+
+ 30.0
+ 150.0
+
+
+
+
+
+ linear
+
+
+ 2 2
+ -0.63 -0.63
+ 0.63 0.63
+
+
+
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat
new file mode 100644
index 000000000..c5cdf8e29
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat
@@ -0,0 +1,2 @@
+k-combined:
+7.479571E-01 2.398563E-02
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat
new file mode 100644
index 000000000..464c89a5d
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat
@@ -0,0 +1,64 @@
+
+
+
+ mgxs.h5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ eigenvalue
+ 100
+ 10
+ 5
+
+
+ -0.63 -0.63 -1 0.63 0.63 1
+
+
+ true
+
+
+ multi-group
+
+
+
+ -0.63 -0.63 -1.0 0.63 0.63 1.0
+
+
+ 30.0
+ 150.0
+
+
+
+
+
+ linear
+
+
+ 2 2
+ -0.63 -0.63
+ 0.63 0.63
+
+
+
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat
new file mode 100644
index 000000000..c6cce2e39
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat
@@ -0,0 +1,2 @@
+k-combined:
+7.620306E-01 2.175179E-02
diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/test.py b/tests/regression_tests/random_ray_auto_convert_source_energy/test.py
new file mode 100644
index 000000000..bb9119d89
--- /dev/null
+++ b/tests/regression_tests/random_ray_auto_convert_source_energy/test.py
@@ -0,0 +1,66 @@
+import os
+
+import openmc
+from openmc.examples import pwr_pin_cell
+from openmc import RegularMesh
+from openmc.utility_funcs import change_directory
+import pytest
+
+from tests.testing_harness import TolerantPyAPITestHarness
+
+
+class MGXSTestHarness(TolerantPyAPITestHarness):
+ def _cleanup(self):
+ super()._cleanup()
+ f = 'mgxs.h5'
+ if os.path.exists(f):
+ os.remove(f)
+
+
+@pytest.mark.parametrize("source_type", ["model", "user"])
+@pytest.mark.parametrize("method", ["stochastic_slab", "infinite_medium"])
+def test_random_ray_auto_convert_source_energy(method, source_type):
+ dirname = f"{method}/{source_type}"
+ with change_directory(dirname):
+ openmc.reset_auto_ids()
+
+ # Start with a normal continuous energy model
+ model = pwr_pin_cell()
+
+ # Define the source energy distribution, using different methods
+ source_energy = None
+ if source_type == "model":
+ model.settings.source = openmc.IndependentSource(
+ energy=openmc.stats.delta_function(7.0e6)
+ )
+ elif source_type == "user":
+ source_energy = openmc.stats.delta_function(1.0e4)
+
+ # Convert to a multi-group model
+ model.convert_to_multigroup(
+ method=method, groups='CASMO-8', nparticles=100,
+ overwrite_mgxs_library=False, mgxs_path="mgxs.h5",
+ source_energy=source_energy
+ )
+
+ # Convert to a random ray model
+ model.convert_to_random_ray()
+
+ # Set the number of particles
+ model.settings.particles = 100
+
+ # Overlay a basic 2x2 mesh
+ n = 2
+ mesh = RegularMesh()
+ mesh.dimension = (n, n)
+ bbox = model.geometry.bounding_box
+ mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1])
+ mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1])
+ model.settings.random_ray['source_region_meshes'] = [
+ (mesh, [model.geometry.root_universe])]
+
+ # Set the source shape to linear
+ model.settings.random_ray['source_shape'] = 'linear'
+
+ harness = MGXSTestHarness('statepoint.10.h5', model)
+ harness.main()