diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 81db61ae062..3f35625b1f9 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -650,8 +650,8 @@ model to use these multigroup cross sections. An example is given below:: ) The most important parameter to set is the ``method`` parameter, which can be -either "stochastic_slab", "material_wise", or "infinite_medium". An overview -of these methods is given below: +one of "material_wise", "cell_wise", "stochastic_slab", or +"infinite_medium". An overview of these methods is given below: .. list-table:: Comparison of Automatic MGXS Generation Methods :header-rows: 1 @@ -665,7 +665,8 @@ of these methods is given below: - * Higher Fidelity * Runs a CE simulation with the original geometry and source, tallying cross sections with a material filter. - - * Typically the most accurate of the three methods + - * Typically more accurate than the surrogate-geometry methods + (``stochastic_slab`` and ``infinite_medium``) * Accurately captures (averaged over the full problem domain) both spatial and resonance self shielding effects - * Potentially slower as the full geometry must be run @@ -673,6 +674,22 @@ of these methods is given below: to in the CE simulation, the MGXS will be zero for that material. This can be mitigated by supplying weight windows via ``weight_windows_file`` (see :ref:`mgxs_bootstrap`). + * - ``cell_wise`` + - * Highest Fidelity + * Like ``material_wise``, but clones the material in each cell so every + cell gets its own cross sections (each material-filled cell is assigned a + distinct macroscopic). + - * Resolves spatial variation between distinct cells that share a material, + which ``material_wise`` averages away (e.g. a shield wall modelled as + several cells of one material, each covering a different depth) + * Captures spatial self shielding between cells filled with the same material + - * Most expensive (one cross section set per cell) and a larger library + * Fidelity is limited by the cell definitions, not the source region mesh: + a single large cell uses one cross section set throughout, so a steep + gradient is only resolved if the geometry is split into several cells + * Same far-from-source limitation as ``material_wise``: a cell that is not + tallied to yields zero cross sections for that cell (the same weight + window mitigation applies) * - ``stochastic_slab`` - * Medium Fidelity * Runs a CE simulation with a greatly simplified geometry, where materials @@ -691,6 +708,13 @@ of these methods is given below: between materials) * May hang if a material has a k-infinity greater than 1.0 +.. note:: + The ``cell_wise`` method generates one cross section set per cell definition, + not per cell instance. A cell that appears in several locations of a lattice + therefore shares a single cross section set across all of those locations. If + you need distinct cross sections per instance, subdivide the geometry into + separate cells. + 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 ``correction`` @@ -769,12 +793,12 @@ the fidelity of the generated MGXS data. 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 +The ``"material_wise"`` and ``"cell_wise"`` methods run a continuous energy +simulation of the original geometry, so they produce the highest fidelity cross +sections of the available methods. However, they have 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. @@ -800,8 +824,8 @@ described in the :ref:`FW-CADIS user guide `. The resulting model.convert_to_multigroup( weight_windows_file="weight_windows.h5", overwrite_mgxs_library=True) -The ``weight_windows_file`` setting is only used with the -``"material_wise"`` method, as the ``"stochastic_slab"`` and +The ``weight_windows_file`` setting is only used with the ``"material_wise"`` +and ``"cell_wise"`` methods, 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 diff --git a/openmc/model/model.py b/openmc/model/model.py index c3bfa3b55a1..22439f1369f 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2638,8 +2638,12 @@ def convert_to_multigroup( Parameters ---------- - method : {"material_wise", "stochastic_slab", "infinite_medium"}, optional - Method to generate the MGXS. + method : {"material_wise", "stochastic_slab", "infinite_medium", \ + "cell_wise"}, optional + Method to generate the MGXS. "cell_wise" is like + "material_wise" but gives each cell its own cross sections. The + material in each material-filled cell is cloned, so the per-material + generation produces one cross section set per cell. groups : openmc.mgxs.EnergyGroups, str, or sequence of float, optional Energy group structure for the MGXS. Can be an :class:`openmc.mgxs.EnergyGroups` object, a string name of a @@ -2699,9 +2703,9 @@ def convert_to_multigroup( also construct their own sources and always disable ``create_fission_neutrons`` so that fission is treated as capture. A ``weight_windows_file`` is applied during ``"material_wise"`` - generation and ignored with a warning by the other methods; see the - user guide for the weight window "bootstrapping" workflow this - enables. Cannot be combined with the deprecated ``nparticles`` or + and ``"cell_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 @@ -2710,7 +2714,8 @@ def convert_to_multigroup( groups = openmc.mgxs.EnergyGroups(groups) check_value('method', method, - ('material_wise', 'stochastic_slab', 'infinite_medium')) + ('material_wise', 'cell_wise', 'stochastic_slab', + 'infinite_medium')) # Keyword arguments are Settings attributes applied as overrides on # the generation defaults @@ -2739,10 +2744,11 @@ def convert_to_multigroup( # Resolve the settings for the MGXS generation run(s) in three layers, # with later layers taking precedence: the model's own settings - # ("material_wise") or a fresh Settings object (surrogate methods), then - # the generation defaults, then the user's keyword-argument overrides. + # ("material_wise"/"cell_wise") or a fresh Settings object (surrogate + # methods), then the generation defaults, then the user's + # keyword-argument overrides. user_settings = settings - if method == 'material_wise': + if method in ('material_wise', 'cell_wise'): settings = copy.deepcopy(self.settings) else: settings = openmc.Settings() @@ -2760,7 +2766,8 @@ def convert_to_multigroup( if user_settings is not None: # The surrogate-geometry methods construct their own sources - if method != "material_wise" and len(user_settings.source) > 0: + if method not in ("material_wise", "cell_wise") \ + and len(user_settings.source) > 0: warnings.warn( 'The given "source" setting is ignored by the ' f'"{method}" MGXS generation method, which constructs ' @@ -2773,11 +2780,12 @@ def convert_to_multigroup( # The run mode is the one attribute that cannot be detected as # user-populated (a fresh Settings object defaults it to 'eigenvalue'), - # so it is owned by the generation method: "material_wise" always takes - # it from the model, while the surrogate-geometry methods always run in - # fixed source mode. The surrogate-geometry methods also treat fission - # as capture (nu-fission is still tallied) - if method == 'material_wise': + # so it is owned by the generation method: "material_wise" and + # "cell_wise" always take it from the model, while the + # surrogate-geometry methods always run in fixed source mode. The + # surrogate-geometry methods also treat fission as capture (nu-fission + # is still tallied) + if method in ('material_wise', 'cell_wise'): settings.run_mode = self.settings.run_mode else: settings.run_mode = 'fixed source' @@ -2785,19 +2793,19 @@ def convert_to_multigroup( # 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 + # "material_wise" and "cell_wise" methods' 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": + method not in ("material_wise", "cell_wise"): warnings.warn( 'The "weight_windows_file" setting is only applicable to ' - 'the "material_wise" MGXS generation method and will be ' - f'ignored for the "{method}" method.' + 'the "material_wise" and "cell_wise" MGXS generation methods ' + f'and will be ignored for the "{method}" method.' ) settings.weight_windows_file = None @@ -2822,6 +2830,18 @@ def convert_to_multigroup( self.settings.run_mode = original_run_mode break + # For "cell_wise", give each cell its own cross sections by + # cloning the material in every material-filled cell. Each clone gets + # a unique id, so the per-material generation below produces (and + # assigns) one cross section set per cell. + if method == "cell_wise": + cell_materials = [] + for cell in self.geometry.get_all_cells().values(): + if isinstance(cell.fill, openmc.Material): + cell.fill = cell.fill.clone() + cell_materials.append(cell.fill) + self.materials = openmc.Materials(cell_materials) + # Temporarily replace each material's name with a unique, valid HDF5 # dataset name (its name plus ID) for use as its MGXS library entry # and macroscopic. The ID keeps the name unique even when materials @@ -2838,7 +2858,7 @@ def convert_to_multigroup( self._generate_infinite_medium_mgxs( groups, settings, mgxs_path, correction, tmpdir, source_energy, temperatures) - elif method == "material_wise": + elif method in ("material_wise", "cell_wise"): self._generate_material_wise_mgxs( groups, settings, mgxs_path, correction, tmpdir, temperatures) diff --git a/tests/unit_tests/dagmc/test_convert_to_multigroup.py b/tests/unit_tests/dagmc/test_convert_to_multigroup.py index 57c93727e7f..a293f43610a 100644 --- a/tests/unit_tests/dagmc/test_convert_to_multigroup.py +++ b/tests/unit_tests/dagmc/test_convert_to_multigroup.py @@ -51,3 +51,43 @@ def test_convert_to_multigroup_without_particles_batches(run_in_tmpdir): # Verify the model was converted successfully assert model.settings.energy_mode == 'multi-group' + + +def test_convert_to_multigroup_cell_wise(run_in_tmpdir): + """cell_wise gives each DAGMC volume its own cross sections, so two + cells filled with the same material end up with distinct macroscopics.""" + openmc.reset_auto_ids() + + # dagmc.h5m has two fuel volumes (both "no-void fuel"), one water volume, a + # graveyard and an implicit complement. + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0) + u235.set_density("g/cm3", 11.0) + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + water.id = 41 + + dagmc_file = Path(__file__).parent / "dagmc.h5m" + model = openmc.Model() + model.materials = openmc.Materials([u235, water]) + model.geometry = openmc.Geometry(openmc.DAGMCUniverse(dagmc_file)) + model.settings = openmc.Settings() + model.settings.run_mode = "fixed source" + source = openmc.IndependentSource() + source.energy = openmc.stats.delta_function(2.0e6) + model.settings.source = source + + # Pre-create the library so MGXS generation/transport is skipped; this + # exercises the per-cell material cloning for the DAGMC cells only. + Path("mgxs.h5").touch() + model.convert_to_multigroup( + method="cell_wise", groups="CASMO-2", mgxs_path="mgxs.h5") + + # The three material-filled volumes (two fuel, one water) each get their own + # cloned material with a distinct macroscopic; the void cells are skipped. + assert model.settings.energy_mode == "multi-group" + assert len(model.materials) == 3 + macros = [m._macroscopic for m in model.materials] + assert len(set(macros)) == 3 diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 5e3ba7e314f..9d720ab2b3d 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1092,6 +1092,37 @@ def test_convert_to_multigroup_preserves_material_names(run_in_tmpdir): assert len(set(macro)) == 2 +def test_convert_to_multigroup_cell_wise(run_in_tmpdir): + """cell_wise clones the material in each cell so two cells sharing one + material end up with distinct (spatially-resolved) cross sections rather than a + single shared set.""" + water = openmc.Material(name="water") + water.add_element("H", 2.0) + water.add_element("O", 1.0) + water.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=water, region=-s1) + c2 = openmc.Cell(fill=water, region=+s1 & -s2) # same material, distinct cell + model = openmc.Model(openmc.Geometry([c1, c2]), openmc.Materials([water])) + + # Pre-create the library so MGXS generation (and transport) is skipped; this + # exercises the per-cell material cloning and macroscopic assignment only. + Path("mgxs.h5").touch() + model.convert_to_multigroup(method="cell_wise", mgxs_path="mgxs.h5") + + # Each cell now holds its own cloned material reading a distinct macroscopic. + assert len(model.materials) == 2 + assert c1.fill is not c2.fill + assert c1.fill._macroscopic == f"water_{c1.fill.id}" + assert c2.fill._macroscopic == f"water_{c2.fill.id}" + assert c1.fill._macroscopic != c2.fill._macroscopic + # The user's original material object is left untouched. + assert water.name == "water" + assert model.settings.energy_mode == "multi-group" + + class _GenerationCaptured(Exception): """Raised by the patched MGXS library builder to end generation early.""" @@ -1206,6 +1237,26 @@ def test_convert_to_multigroup_weight_windows(run_in_tmpdir, monkeypatch): assert gen.weight_windows_file is None +def test_convert_to_multigroup_cell_wise_settings(run_in_tmpdir, monkeypatch): + # "cell_wise" runs the same full-geometry continuous energy simulation as + # "material_wise", so it inherits the model's settings and run mode and + # accepts a weight windows file + model = _steel_water_model() + model.settings.run_mode = 'fixed source' + model.settings.source = openmc.IndependentSource(space=openmc.stats.Point()) + ww_path = Path('ww.h5').resolve() + + gen = _capture_generation_settings( + monkeypatch, model, method='cell_wise', particles=777, + weight_windows_file=ww_path) + + assert gen.particles == 777 + assert gen.batches == 200 # generation default + assert gen.run_mode == 'fixed source' + assert len(gen.source) == 1 + assert gen.weight_windows_file == ww_path + + def test_convert_to_multigroup_validation(run_in_tmpdir): model = _steel_water_model()