From ad5a876bee9f3e3b96ddc28867fdec0028cb7587 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 3 Dec 2025 01:48:38 -0600 Subject: [PATCH] Improved automatic MGXS generation for random ray (#3658) Co-authored-by: Paul Romano --- docs/source/usersguide/random_ray.rst | 28 ++- openmc/model/model.py | 186 +++++++++++++++--- .../infinite_medium/results_true.dat | 2 +- .../stochastic_slab/results_true.dat | 2 +- .../__init__.py | 0 .../infinite_medium/model/inputs_true.dat | 61 ++++++ .../infinite_medium/model/results_true.dat | 2 + .../infinite_medium/user/inputs_true.dat | 64 ++++++ .../infinite_medium/user/results_true.dat | 2 + .../stochastic_slab/model/inputs_true.dat | 61 ++++++ .../stochastic_slab/model/results_true.dat | 2 + .../stochastic_slab/user/inputs_true.dat | 64 ++++++ .../stochastic_slab/user/results_true.dat | 2 + .../test.py | 66 +++++++ 14 files changed, 511 insertions(+), 31 deletions(-) create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/test.py 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()