From d0c9b43b995fc68e68744fc56f9acada6c31da7c Mon Sep 17 00:00:00 2001 From: GuySten Date: Thu, 22 Jan 2026 17:38:50 +0200 Subject: [PATCH 1/6] fix for plotting model with multigroup cross sections --- openmc/material.py | 37 +++++++++++++++++++++++++ openmc/model/model.py | 11 ++++++++ openmc/universe.py | 1 + tests/unit_tests/test_universe.py | 46 +++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/openmc/material.py b/openmc/material.py index 735a0574326..9d52fe73704 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -163,6 +163,8 @@ def __init__( # The single instance of Macroscopic data present in this material # (only one is allowed, hence this is different than _nuclides, etc) self._macroscopic = None + + self._cross_sections = None # If specified, a list of table names self._sab = [] @@ -208,6 +210,15 @@ def __repr__(self) -> str: return string + @property + def cross_sections(self) -> Path | None: + return self._cross_sections + + @cross_sections.setter + def cross_sections(self, cross_sections): + if cross_sections is not None: + self._cross_sections = input_path(cross_sections) + @property def name(self) -> str | None: return self._name @@ -1918,6 +1929,30 @@ def cross_sections(self) -> Path | None: def cross_sections(self, cross_sections): if cross_sections is not None: self._cross_sections = input_path(cross_sections) + for mat in self: + mat.cross_sections = self.cross_sections + + def _setup_cross_sections(self, material): + """Copy cross_sections to material if exists + and copy cross_sections from material if not exists + + Parameters + ---------- + material : openmc.Material + + + """ + if self.cross_sections is not None: + if material.cross_sections is not None: + if material.cross_sections != self.cross_sections: + warn("Material {material.id} cross_sections value has been overriden.") + material.cross_sections = self.cross_sections + elif material.cross_sections is not None: + self.cross_sections = material.cross_sections + + def __setitem__(index: int, material): + self._setup_cross_sections(material) + super().__setitem__(index, material) def append(self, material): """Append material to collection @@ -1928,6 +1963,7 @@ def append(self, material): Material to append """ + self._setup_cross_sections(material) super().append(material) def insert(self, index: int, material): @@ -1941,6 +1977,7 @@ def insert(self, index: int, material): Material to insert """ + self._setup_cross_sections(material) super().insert(index, material) def make_isotropic_in_lab(self): diff --git a/openmc/model/model.py b/openmc/model/model.py index 6e4c1c5856d..3c362eb0213 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1159,6 +1159,17 @@ def plot( x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] + + # Determine whether any materials contains macroscopic data and if so, + # set energy mode accordingly + for mat in self.geometry.get_all_materials().values(): + if mat._macroscopic is not None: + self.settings.energy_mode = 'multi-group' + break + + # Convert cross_section path to absolute + if self.materials.cross_sections is not None: + self.materials.cross_sections = Path(self.materials.cross_sections).resolve() # Get ID map from the C API id_map = self.id_map( diff --git a/openmc/universe.py b/openmc/universe.py index 0e64693ba8e..3ed6a27bca3 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -337,6 +337,7 @@ def plot(self, *args, **kwargs): """ model = openmc.Model() model.geometry = openmc.Geometry(self) + model.materials = openmc.Materials(self.get_all_materials().values()) return model.plot(*args, **kwargs) def get_nuclides(self): diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index efe8552a648..0fe10e3649e 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -6,6 +6,46 @@ from tests.unit_tests import assert_unbounded +@pytest.fixture +def mg_lib(): + groups = openmc.mgxs.EnergyGroups(group_edges=[1e-5, 20.0e6]) + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([1.0]) + h2o_xsdata.set_absorption([0.5]) + scatter_matrix = np.array([[[0.5]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([h2o_xsdata]) + mg_cross_sections_file.export_to_hdf5() + return "mgxs.h5" + + +@pytest.fixture +def mg_model(mg_lib): + model = openmc.Model() + + # Create materials for the problem + h2o_data = openmc.Macroscopic('LWTR') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic(h2o_data) + + # Instantiate a Materials collection and export to XML + model.materials = openmc.Materials([water]) + model.materials.cross_sections = mg_lib + H = 1.0 + L = 100 + + sph00 = openmc.Sphere(r=10, boundary_type = "vacuum") + cell00 = openmc.Cell(region = -sph00 , fill = water) + univ = openmc.Universe(cells = [cell00], universe_id = 1) + model.geometry = openmc.Geometry(univ) + return model + + def test_basic(): c1 = openmc.Cell() c2 = openmc.Cell() @@ -102,6 +142,12 @@ def test_plot(run_in_tmpdir, sphere_model): # Close plots to avoid warning import matplotlib.pyplot as plt plt.close('all') + + +def test_mg_plot(mg_model): + univ = mg_model.geometry.root_universe + univ.plot(width=(200, 200), basis='yz', color_by='cell') + univ.plot(width=(200, 200), basis='yz', color_by='material') def test_get_nuclides(uo2): From 6ffa03808dccb6721ae7d2d4aa0d138b74b4f6cb Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:00:19 +0200 Subject: [PATCH 2/6] Fix __setitem__ method definition in material.py --- openmc/material.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 9d52fe73704..b6c5eb4b88a 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1950,7 +1950,7 @@ def _setup_cross_sections(self, material): elif material.cross_sections is not None: self.cross_sections = material.cross_sections - def __setitem__(index: int, material): + def __setitem__(self, index: int, material): self._setup_cross_sections(material) super().__setitem__(index, material) From ed3ccb6317051752a858cecbdb140cb2823e28d1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 29 Jan 2026 16:32:42 -0600 Subject: [PATCH 3/6] Remove Material.cross_sections, simplify test_mg_plot --- openmc/material.py | 37 --------------- openmc/model/model.py | 12 ++--- openmc/universe.py | 1 - tests/unit_tests/test_universe.py | 75 +++++++++++++------------------ 4 files changed, 34 insertions(+), 91 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index b6c5eb4b88a..735a0574326 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -163,8 +163,6 @@ def __init__( # The single instance of Macroscopic data present in this material # (only one is allowed, hence this is different than _nuclides, etc) self._macroscopic = None - - self._cross_sections = None # If specified, a list of table names self._sab = [] @@ -210,15 +208,6 @@ def __repr__(self) -> str: return string - @property - def cross_sections(self) -> Path | None: - return self._cross_sections - - @cross_sections.setter - def cross_sections(self, cross_sections): - if cross_sections is not None: - self._cross_sections = input_path(cross_sections) - @property def name(self) -> str | None: return self._name @@ -1929,30 +1918,6 @@ def cross_sections(self) -> Path | None: def cross_sections(self, cross_sections): if cross_sections is not None: self._cross_sections = input_path(cross_sections) - for mat in self: - mat.cross_sections = self.cross_sections - - def _setup_cross_sections(self, material): - """Copy cross_sections to material if exists - and copy cross_sections from material if not exists - - Parameters - ---------- - material : openmc.Material - - - """ - if self.cross_sections is not None: - if material.cross_sections is not None: - if material.cross_sections != self.cross_sections: - warn("Material {material.id} cross_sections value has been overriden.") - material.cross_sections = self.cross_sections - elif material.cross_sections is not None: - self.cross_sections = material.cross_sections - - def __setitem__(self, index: int, material): - self._setup_cross_sections(material) - super().__setitem__(index, material) def append(self, material): """Append material to collection @@ -1963,7 +1928,6 @@ def append(self, material): Material to append """ - self._setup_cross_sections(material) super().append(material) def insert(self, index: int, material): @@ -1977,7 +1941,6 @@ def insert(self, index: int, material): Material to insert """ - self._setup_cross_sections(material) super().insert(index, material) def make_isotropic_in_lab(self): diff --git a/openmc/model/model.py b/openmc/model/model.py index 3c362eb0213..b4427a4cf59 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1159,17 +1159,13 @@ def plot( x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] - + # Determine whether any materials contains macroscopic data and if so, # set energy mode accordingly for mat in self.geometry.get_all_materials().values(): if mat._macroscopic is not None: self.settings.energy_mode = 'multi-group' break - - # Convert cross_section path to absolute - if self.materials.cross_sections is not None: - self.materials.cross_sections = Path(self.materials.cross_sections).resolve() # Get ID map from the C API id_map = self.id_map( @@ -1190,8 +1186,8 @@ def plot( # Convert ID map to RGB image img = id_map_to_rgb( - id_map=id_map, - color_by=color_by, + id_map=id_map, + color_by=color_by, colors=colors, overlap_color=overlap_color ) @@ -1228,7 +1224,7 @@ def plot( extent=(x_min, x_max, y_min, y_max), **contour_kwargs ) - + # If only showing outline, set the axis limits and aspect explicitly if outline == 'only': axes.set_xlim(x_min, x_max) diff --git a/openmc/universe.py b/openmc/universe.py index 3ed6a27bca3..0e64693ba8e 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -337,7 +337,6 @@ def plot(self, *args, **kwargs): """ model = openmc.Model() model.geometry = openmc.Geometry(self) - model.materials = openmc.Materials(self.get_all_materials().values()) return model.plot(*args, **kwargs) def get_nuclides(self): diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 0fe10e3649e..411a00ca77a 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -6,46 +6,6 @@ from tests.unit_tests import assert_unbounded -@pytest.fixture -def mg_lib(): - groups = openmc.mgxs.EnergyGroups(group_edges=[1e-5, 20.0e6]) - h2o_xsdata = openmc.XSdata('LWTR', groups) - h2o_xsdata.order = 0 - h2o_xsdata.set_total([1.0]) - h2o_xsdata.set_absorption([0.5]) - scatter_matrix = np.array([[[0.5]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) - mg_cross_sections_file = openmc.MGXSLibrary(groups) - mg_cross_sections_file.add_xsdatas([h2o_xsdata]) - mg_cross_sections_file.export_to_hdf5() - return "mgxs.h5" - - -@pytest.fixture -def mg_model(mg_lib): - model = openmc.Model() - - # Create materials for the problem - h2o_data = openmc.Macroscopic('LWTR') - - water = openmc.Material(name='Water') - water.set_density('macro', 1.0) - water.add_macroscopic(h2o_data) - - # Instantiate a Materials collection and export to XML - model.materials = openmc.Materials([water]) - model.materials.cross_sections = mg_lib - H = 1.0 - L = 100 - - sph00 = openmc.Sphere(r=10, boundary_type = "vacuum") - cell00 = openmc.Cell(region = -sph00 , fill = water) - univ = openmc.Universe(cells = [cell00], universe_id = 1) - model.geometry = openmc.Geometry(univ) - return model - - def test_basic(): c1 = openmc.Cell() c2 = openmc.Cell() @@ -142,12 +102,37 @@ def test_plot(run_in_tmpdir, sphere_model): # Close plots to avoid warning import matplotlib.pyplot as plt plt.close('all') - -def test_mg_plot(mg_model): - univ = mg_model.geometry.root_universe - univ.plot(width=(200, 200), basis='yz', color_by='cell') - univ.plot(width=(200, 200), basis='yz', color_by='material') + +def test_mg_plot(run_in_tmpdir): + # Create a simple universe with macroscopic data + h2o_data = openmc.Macroscopic('LWTR') + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic(h2o_data) + sph = openmc.Sphere(r=10, boundary_type="vacuum") + + # Create MGXS library and export to HDF5 + groups = openmc.mgxs.EnergyGroups([1e-5, 20.0e6]) + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([1.0]) + h2o_xsdata.set_absorption([0.5]) + scatter_matrix = np.array([[[0.5]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + mg_library = openmc.MGXSLibrary(groups) + mg_library.add_xsdatas([h2o_xsdata]) + mg_library.export_to_hdf5('mgxs.h5') + + # Set MG cross sections in config and plot + with openmc.config.patch('mg_cross_sections', 'mgxs.h5'): + (-sph).plot(width=(200, 200), basis='yz', color_by='cell') + (-sph).plot(width=(200, 200), basis='yz', color_by='material') + + # Close plots to avoid warning + import matplotlib.pyplot as plt + plt.close('all') def test_get_nuclides(uo2): From 8474d0596f2dbacf6ba0f6bf2677ba25d6cce9dc Mon Sep 17 00:00:00 2001 From: GuySten Date: Fri, 30 Jan 2026 01:55:20 +0200 Subject: [PATCH 4/6] fix test to use mg materials and fix mg xs lookup if path is relative --- openmc/model/model.py | 6 +++++- tests/unit_tests/test_universe.py | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index b4427a4cf59..ebb899bd7ba 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1161,10 +1161,14 @@ def plot( y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] # Determine whether any materials contains macroscopic data and if so, - # set energy mode accordingly + # set energy mode accordingly and check that mg cross sections path is accessible for mat in self.geometry.get_all_materials().values(): if mat._macroscopic is not None: self.settings.energy_mode = 'multi-group' + try: + openmc.config['mg_cross_sections'] = Path(openmc.config['mg_cross_sections']).resolve() + except KeyError: + raise RuntimeError("'mg_cross_sections' path must be set before plotting.") break # Get ID map from the C API diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 411a00ca77a..3b7e511d9a2 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -111,6 +111,8 @@ def test_mg_plot(run_in_tmpdir): water.set_density('macro', 1.0) water.add_macroscopic(h2o_data) sph = openmc.Sphere(r=10, boundary_type="vacuum") + cell = openmc.Cell(region=-sph, fill=water) + univ = openmc.Universe(cells=[cell]) # Create MGXS library and export to HDF5 groups = openmc.mgxs.EnergyGroups([1e-5, 20.0e6]) @@ -127,8 +129,11 @@ def test_mg_plot(run_in_tmpdir): # Set MG cross sections in config and plot with openmc.config.patch('mg_cross_sections', 'mgxs.h5'): - (-sph).plot(width=(200, 200), basis='yz', color_by='cell') - (-sph).plot(width=(200, 200), basis='yz', color_by='material') + (univ).plot(width=(200, 200), basis='yz', color_by='cell') + (univ).plot(width=(200, 200), basis='yz', color_by='material') + + with pytest.raises(RuntimeError): + (univ).plot(width=(200, 200), basis='yz', color_by='cell') # Close plots to avoid warning import matplotlib.pyplot as plt From ca5e7f38cd81a70b5efbfcbce2b0446ddd21805a Mon Sep 17 00:00:00 2001 From: GuySten Date: Fri, 30 Jan 2026 02:03:04 +0200 Subject: [PATCH 5/6] patch mg_cross_sections only when needed --- openmc/model/model.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index ebb899bd7ba..bda396f591c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -6,6 +6,7 @@ from pathlib import Path import math from numbers import Integral, Real +import os import random import re from tempfile import NamedTemporaryFile, TemporaryDirectory @@ -1159,6 +1160,8 @@ def plot( x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] + + mg_cross_sections = os.devnull # Determine whether any materials contains macroscopic data and if so, # set energy mode accordingly and check that mg cross sections path is accessible @@ -1166,19 +1169,20 @@ def plot( if mat._macroscopic is not None: self.settings.energy_mode = 'multi-group' try: - openmc.config['mg_cross_sections'] = Path(openmc.config['mg_cross_sections']).resolve() + mg_cross_sections = Path(openmc.config['mg_cross_sections']).resolve() except KeyError: raise RuntimeError("'mg_cross_sections' path must be set before plotting.") break - - # Get ID map from the C API - id_map = self.id_map( - origin=origin, - width=width, - pixels=pixels, - basis=basis, - color_overlaps=show_overlaps - ) + + with openmc.config.patch('mg_cross_sections', mg_cross_sections): + # Get ID map from the C API + id_map = self.id_map( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + color_overlaps=show_overlaps + ) # Generate colors if not provided if colors is None and seed is not None: From 2c326e3e78098089d28bf61bc9e5759c866e2287 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 29 Jan 2026 18:19:57 -0600 Subject: [PATCH 6/6] Remove config patching from Model.plot --- openmc/model/model.py | 29 ++++++++++++----------------- tests/unit_tests/test_universe.py | 17 ++++++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index bda396f591c..62790dc8765 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -6,7 +6,6 @@ from pathlib import Path import math from numbers import Integral, Real -import os import random import re from tempfile import NamedTemporaryFile, TemporaryDirectory @@ -1160,29 +1159,25 @@ def plot( x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] - - mg_cross_sections = os.devnull # Determine whether any materials contains macroscopic data and if so, # set energy mode accordingly and check that mg cross sections path is accessible for mat in self.geometry.get_all_materials().values(): if mat._macroscopic is not None: self.settings.energy_mode = 'multi-group' - try: - mg_cross_sections = Path(openmc.config['mg_cross_sections']).resolve() - except KeyError: - raise RuntimeError("'mg_cross_sections' path must be set before plotting.") + if 'mg_cross_sections' not in openmc.config: + raise RuntimeError("'mg_cross_sections' path must be set in " + "openmc.config before plotting.") break - - with openmc.config.patch('mg_cross_sections', mg_cross_sections): - # Get ID map from the C API - id_map = self.id_map( - origin=origin, - width=width, - pixels=pixels, - basis=basis, - color_overlaps=show_overlaps - ) + + # Get ID map from the C API + id_map = self.id_map( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + color_overlaps=show_overlaps + ) # Generate colors if not provided if colors is None and seed is not None: diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 3b7e511d9a2..6fbf1cc383f 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -1,3 +1,5 @@ +from pathlib import Path + import lxml.etree as ET import numpy as np import openmc @@ -125,15 +127,16 @@ def test_mg_plot(run_in_tmpdir): h2o_xsdata.set_scatter_matrix(scatter_matrix) mg_library = openmc.MGXSLibrary(groups) mg_library.add_xsdatas([h2o_xsdata]) - mg_library.export_to_hdf5('mgxs.h5') + mgxs_path = Path.cwd() / 'mgxs.h5' + mg_library.export_to_hdf5(mgxs_path) # Set MG cross sections in config and plot - with openmc.config.patch('mg_cross_sections', 'mgxs.h5'): - (univ).plot(width=(200, 200), basis='yz', color_by='cell') - (univ).plot(width=(200, 200), basis='yz', color_by='material') - - with pytest.raises(RuntimeError): - (univ).plot(width=(200, 200), basis='yz', color_by='cell') + with openmc.config.patch('mg_cross_sections', mgxs_path): + univ.plot(width=(200, 200), basis='yz', color_by='cell') + univ.plot(width=(200, 200), basis='yz', color_by='material') + + with pytest.raises(RuntimeError): + univ.plot(width=(200, 200), basis='yz', color_by='cell') # Close plots to avoid warning import matplotlib.pyplot as plt