diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index d35aff83b78..57d3abfbdf2 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -652,8 +652,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 @@ -673,6 +673,17 @@ of these methods is given below: - * 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. + * - ``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 intra-material spatial variation that ``material_wise`` averages + away, e.g. a thick shield or a steep flux gradient within a single material + * Captures spatial self shielding between cells filled with the same material + - * Most expensive (one cross section set per cell) and a larger library + * Same far-from-source limitation as ``material_wise``: a cell that is not + tallied to yields zero cross sections for that cell * - ``stochastic_slab`` - * Medium Fidelity * Runs a CE simulation with a greatly simplified geometry, where materials diff --git a/openmc/model/model.py b/openmc/model/model.py index 3284942885e..e425e6f83aa 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2703,8 +2703,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 @@ -2772,6 +2776,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 @@ -2788,7 +2804,7 @@ def convert_to_multigroup( self._generate_infinite_medium_mgxs( groups, nparticles, mgxs_path, correction, tmpdir, source_energy, temperatures, temperature_settings) - elif method == "material_wise": + elif method in ("material_wise", "cell_wise"): self._generate_material_wise_mgxs( groups, nparticles, mgxs_path, correction, tmpdir, temperatures, temperature_settings) diff --git a/tests/unit_tests/dagmc/test_convert_to_multigroup.py b/tests/unit_tests/dagmc/test_convert_to_multigroup.py index 069dc658e52..22cc6191641 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 028a83b0bdc..f1192bd8ff2 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1067,3 +1067,34 @@ 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 + + +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"