From 6979b4bcd8af5782836dd71c4a2d33f1649e9ebf Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Tue, 5 Aug 2025 23:07:53 -0700 Subject: [PATCH 01/11] Add stat:sum field to MCPL files for proper weight normalization (#3514) - Implements stat:sum field (key: "openmc_np1") in MCPL file headers - Initially sets to -1 for crash safety, updates with particle count before closing - Compatible with MCPL >= 2.1.0, gracefully degrades for older versions - Enables proper file merging and McStas/McXtrace integration - Adds C++ and Python unit tests --- include/openmc/mcpl_interface.h | 13 ++- src/mcpl_interface.cpp | 46 +++++++- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_mcpl_stat_sum.cpp | 108 ++++++++++++++++++ tests/unit_tests/test_mcpl_stat_sum.py | 115 ++++++++++++++++++++ 5 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 tests/cpp_unit_tests/test_mcpl_stat_sum.cpp create mode 100644 tests/unit_tests/test_mcpl_stat_sum.py diff --git a/include/openmc/mcpl_interface.h b/include/openmc/mcpl_interface.h index f7323e10a67..a76d72e6494 100644 --- a/include/openmc/mcpl_interface.h +++ b/include/openmc/mcpl_interface.h @@ -19,8 +19,17 @@ namespace openmc { //! \return Vector of source sites vector mcpl_source_sites(std::string path); -//! Write an MCPL source file -// +//! Write an MCPL source file with stat:sum metadata +//! +//! This function writes particle data to an MCPL file. For MCPL >= 2.1.0, +//! it includes a stat:sum field (key: "openmc_np1") containing the total +//! number of source particles, which is essential for proper file merging +//! and weight normalization when using MCPL files with McStas/McXtrace. +//! +//! The stat:sum field follows the crash-safety pattern: +//! - Initially set to -1 when opening (indicates incomplete file) +//! - Updated with actual particle count before closing +//! //! \param[in] filename Path to MCPL file //! \param[in] source_bank Vector of SourceSites to write to file for this //! MPI rank. diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 13915c3b8e9..c6e12870ead 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -58,7 +58,8 @@ struct mcpl_outfile_t { // Function pointer types for the dynamically loaded MCPL library using mcpl_open_file_fpt = mcpl_file_t* (*)(const char* filename); using mcpl_hdr_nparticles_fpt = uint64_t (*)(mcpl_file_t* file_handle); -using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); +using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t * + file_handle); using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); @@ -67,6 +68,10 @@ using mcpl_hdr_set_srcname_fpt = void (*)( using mcpl_add_particle_fpt = void (*)( mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle); using mcpl_close_outfile_fpt = void (*)(mcpl_outfile_t* outfile_handle); +using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* outfile_handle, + const char* key, uint32_t datalength, const char* data); +using mcpl_hdr_add_stat_sum_fpt = void (*)( + mcpl_outfile_t* outfile_handle, const char* key, double value); namespace openmc { @@ -110,6 +115,8 @@ struct McplApi { mcpl_hdr_set_srcname_fpt hdr_set_srcname; mcpl_add_particle_fpt add_particle; mcpl_close_outfile_fpt close_outfile; + mcpl_hdr_add_data_fpt hdr_add_data; + mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum; explicit McplApi(LibraryHandleType lib_handle) { @@ -147,6 +154,24 @@ struct McplApi { load_symbol_platform("mcpl_add_particle")); close_outfile = reinterpret_cast( load_symbol_platform("mcpl_close_outfile")); + + // Try to load mcpl_hdr_add_data (available in MCPL >= 2.1.0) + // Set to nullptr if not available for graceful fallback + try { + hdr_add_data = reinterpret_cast( + load_symbol_platform("mcpl_hdr_add_data")); + } catch (const std::runtime_error&) { + hdr_add_data = nullptr; + } + + // Try to load mcpl_hdr_add_stat_sum (available in MCPL >= 2.1.0) + // Set to nullptr if not available for graceful fallback + try { + hdr_add_stat_sum = reinterpret_cast( + load_symbol_platform("mcpl_hdr_add_stat_sum")); + } catch (const std::runtime_error&) { + hdr_add_stat_sum = nullptr; + } } }; @@ -498,12 +523,31 @@ void write_mcpl_source_point(const char* filename, span source_bank, "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); } g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str()); + + // Initialize stat:sum with -1 to indicate incomplete file (issue #3514) + // This follows MCPL >= 2.1.0 convention for tracking simulation statistics + // The -1 value indicates "not available" if file creation is interrupted + if (g_mcpl_api->hdr_add_stat_sum) { + // Using key "openmc_np1" following tkittel's recommendation + // Initial value of -1 prevents misleading values in case of crashes + g_mcpl_api->hdr_add_stat_sum(file_id, "openmc_np1", -1.0); + } } write_mcpl_source_bank_internal(file_id, source_bank, bank_index); if (mpi::master) { if (file_id) { + // Update stat:sum with actual particle count before closing (issue #3514) + // This represents the original number of source particles in the + // simulation + if (g_mcpl_api->hdr_add_stat_sum) { + int64_t total_particles = bank_index.empty() ? 0 : bank_index.back(); + // Update with actual count - this overwrites the initial -1 value + g_mcpl_api->hdr_add_stat_sum( + file_id, "openmc_np1", static_cast(total_particles)); + } + g_mcpl_api->close_outfile(file_id); } } diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index f0f5f2853ad..8fedc2daa57 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -4,6 +4,7 @@ set(TEST_NAMES test_tally test_interpolate test_math + test_mcpl_stat_sum # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp new file mode 100644 index 00000000000..909830e035c --- /dev/null +++ b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include + +#include "openmc/bank.h" +#include "openmc/mcpl_interface.h" + +// Test the MCPL stat:sum functionality (issue #3514) +TEST_CASE("MCPL stat:sum field") +{ + // Check if MCPL interface is available + if (!openmc::is_mcpl_interface_available()) { + SKIP("MCPL library not available"); + } + + SECTION("stat:sum field is written to MCPL files") + { + // Create a temporary filename + std::string filename = "test_stat_sum.mcpl"; + + // Create some test particles + std::vector source_bank(100); + std::vector bank_index = {0, 100}; // 100 particles total + + // Initialize test particles + for (int i = 0; i < 100; ++i) { + source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].r = {i * 0.1, i * 0.2, i * 0.3}; + source_bank[i].u = {0.0, 0.0, 1.0}; + source_bank[i].E = 2.0e6; // 2 MeV + source_bank[i].time = 0.0; + source_bank[i].wgt = 1.0; + } + + // Write the MCPL file + openmc::write_mcpl_source_point(filename.c_str(), source_bank, bank_index); + + // Verify the file was created + FILE* f = std::fopen(filename.c_str(), "r"); + REQUIRE(f != nullptr); + std::fclose(f); + + // Read the file back to check stat:sum + // Note: This would require mcpl_open_file and checking the header + // Since we can't easily read MCPL headers in C++ without the full MCPL API, + // we rely on the Python test to verify the actual content + + // Clean up + std::remove(filename.c_str()); + } + + SECTION("stat:sum uses correct particle count") + { + std::string filename = "test_count.mcpl"; + + // Test with different particle counts + std::vector test_counts = {1, 10, 100, 1000}; + + for (int count : test_counts) { + std::vector source_bank(count); + std::vector bank_index = {0, count}; + + // Initialize particles + for (int i = 0; i < count; ++i) { + source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].r = {0.0, 0.0, 0.0}; + source_bank[i].u = {0.0, 0.0, 1.0}; + source_bank[i].E = 1.0e6; + source_bank[i].time = 0.0; + source_bank[i].wgt = 1.0; + } + + // Write MCPL file + openmc::write_mcpl_source_point( + filename.c_str(), source_bank, bank_index); + + // The stat:sum should equal count (verified by Python test) + // Here we just verify the file was created successfully + FILE* f = std::fopen(filename.c_str(), "r"); + REQUIRE(f != nullptr); + std::fclose(f); + + // Clean up + std::remove(filename.c_str()); + } + } + + SECTION("stat:sum handles empty particle bank") + { + std::string filename = "test_empty.mcpl"; + + // Create empty particle bank + std::vector source_bank; + std::vector bank_index = {0}; + + // This should still create a valid MCPL file with stat:sum = 0 + openmc::write_mcpl_source_point(filename.c_str(), source_bank, bank_index); + + // Verify file was created + FILE* f = std::fopen(filename.c_str(), "r"); + REQUIRE(f != nullptr); + std::fclose(f); + + // Clean up + std::remove(filename.c_str()); + } +} diff --git a/tests/unit_tests/test_mcpl_stat_sum.py b/tests/unit_tests/test_mcpl_stat_sum.py new file mode 100644 index 00000000000..ca74ae1a6c9 --- /dev/null +++ b/tests/unit_tests/test_mcpl_stat_sum.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +"""Test for MCPL stat:sum functionality (issue #3514)""" + +import os +import shutil +import tempfile +import pytest +import openmc + +# Skip test if MCPL is not available +pytestmark = pytest.mark.skipif( + shutil.which("mcpl-config") is None, + reason="mcpl-config command not found in PATH; MCPL is likely not available." +) + + +def test_mcpl_stat_sum_field(): + """Test that MCPL files contain proper stat:sum field with particle count.""" + + # Only run if mcpl module is available for verification + mcpl = pytest.importorskip("mcpl") + + with tempfile.TemporaryDirectory() as tmpdir: + # Create a simple model + model = openmc.Model() + + # Create a simple material + mat = openmc.Material() + mat.add_nuclide('U235', 1.0) + mat.set_density('g/cm3', 10.0) + model.materials = [mat] + + # Create a simple geometry (sphere) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + model.geometry = openmc.Geometry([cell]) + + # Configure settings for MCPL output + model.settings = openmc.Settings() + model.settings.batches = 5 + model.settings.inactive = 2 + model.settings.particles = 1000 + model.settings.source = openmc.Source(space=openmc.stats.Point()) + + # Enable MCPL source file writing + model.settings.source_point = {'mcpl': True, 'separate': True} + + # Run the simulation + cwd = os.getcwd() + try: + os.chdir(tmpdir) + model.run(output=False) + + # Find the MCPL file + import glob + mcpl_files = glob.glob('source.*.mcpl*') + assert len(mcpl_files) > 0, "No MCPL source files were created" + + # Open and check the MCPL file + mcpl_file = mcpl_files[0] + with mcpl.MCPLFile(mcpl_file) as f: + # Check if stat:sum field exists + # The stat:sum should be accessible through the header + # Look for the "openmc_np1" key + comments = f.comments if hasattr(f, 'comments') else [] + + # Check for stat:sum in comments (MCPL stores these as comments) + stat_sum_found = False + stat_sum_value = None + + for comment in comments: + if 'stat:sum:openmc_np1' in comment: + stat_sum_found = True + # Extract the value + parts = comment.split(':') + if len(parts) >= 4: + stat_sum_value = float(parts[3].strip()) + break + + assert stat_sum_found, "stat:sum:openmc_np1 field not found in MCPL file" + + # The value should be the total number of source particles + # For 5 batches with 1000 particles each = 5000 total + expected_particles = model.settings.batches * model.settings.particles + + # If stat:sum was properly updated, it should equal expected_particles + # If it's still -1, the update before closing didn't work + assert stat_sum_value != -1, "stat:sum was not updated from initial -1 value" + assert stat_sum_value == expected_particles, \ + f"stat:sum value {stat_sum_value} doesn't match expected {expected_particles}" + + finally: + os.chdir(cwd) + + +def test_mcpl_stat_sum_crash_safety(): + """Test that incomplete MCPL files have stat:sum = -1.""" + + # This test would verify that if file creation is interrupted, + # the stat:sum field remains at -1 to indicate incomplete file + # This is harder to test without mocking the MCPL library internals + + # For now, we can at least document the expected behavior: + # 1. When mcpl_create_outfile is called, stat:sum should be set to -1 + # 2. Only when mcpl_close_outfile is called should it be updated + # 3. If the program crashes between these calls, stat:sum remains -1 + + # This could be tested with a C++ unit test that directly uses the + # mcpl_interface functions and simulates a crash + pass + + +if __name__ == "__main__": + # Allow running this test directly + test_mcpl_stat_sum_field() \ No newline at end of file From aa39d63d3a535758b094546d70c32282593dc4c6 Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Mon, 11 Aug 2025 08:56:11 -0700 Subject: [PATCH 02/11] feat: split Plot class into SlicePlot and VoxelPlot This refactoring improves the plot interface by: - Creating dedicated SlicePlot class for 2D slice plots - Creating dedicated VoxelPlot class for 3D voxel plots - Maintaining backward compatibility with deprecated Plot class - Properly separating attributes specific to each plot type The width attribute now accepts 2 values for slice plots and 3 for voxel plots. The basis attribute only exists on SlicePlot as it doesn't apply to voxel plots. Addresses #3507 --- openmc/plots.py | 551 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 511 insertions(+), 40 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 9e097e2b9bf..b6a1d62f845 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -624,14 +624,15 @@ def to_xml_element(self): return element -class Plot(PlotBase): - """Definition of a finite region of space to be plotted. +class SlicePlot(PlotBase): + """Definition of a 2D slice plot of the geometry. - OpenMC is capable of generating two-dimensional slice plots, or - three-dimensional voxel or projection plots. Colors that are used in plots can be given as - RGB tuples, e.g. (255, 255, 255) would be white, or by a string indicating a + Colors that are used in plots can be given as RGB tuples, e.g. + (255, 255, 255) would be white, or by a string indicating a valid `SVG color `_. + .. versionadded:: 0.15.1 + Parameters ---------- plot_id : int @@ -646,7 +647,7 @@ class Plot(PlotBase): name : str Name of the plot pixels : Iterable of int - Number of pixels to use in each direction + Number of pixels to use in each direction (2 values) filename : str Path to write the plot to color_by : {'cell', 'material'} @@ -669,11 +670,9 @@ class Plot(PlotBase): level : int Universe depth to plot at width : Iterable of float - Width of the plot in each basis direction + Width of the plot in each basis direction (2 values) origin : tuple or list of ndarray - Origin (center) of the plot - type : {'slice', 'voxel'} - The type of the plot + Origin (center) of the plot (3 values) basis : {'xy', 'xz', 'yz'} The basis directions for the plot meshlines : dict @@ -686,7 +685,6 @@ def __init__(self, plot_id=None, name=''): super().__init__(plot_id, name) self._width = [4.0, 4.0] self._origin = [0., 0., 0.] - self._type = 'slice' self._basis = 'xy' self._meshlines = None @@ -697,7 +695,7 @@ def width(self): @width.setter def width(self, width): cv.check_type('plot width', width, Iterable, Real) - cv.check_length('plot width', width, 2, 3) + cv.check_length('plot width', width, 2) self._width = width @property @@ -710,15 +708,6 @@ def origin(self, origin): cv.check_length('plot origin', origin, 3) self._origin = origin - @property - def type(self): - return self._type - - @type.setter - def type(self, plottype): - cv.check_value('plot type', plottype, ['slice', 'voxel']) - self._type = plottype - @property def basis(self): return self._basis @@ -761,11 +750,10 @@ def meshlines(self, meshlines): self._meshlines = meshlines def __repr__(self): - string = 'Plot\n' + string = 'SlicePlot\n' string += '{: <16}=\t{}\n'.format('\tID', self._id) string += '{: <16}=\t{}\n'.format('\tName', self._name) string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) - string += '{: <16}=\t{}\n'.format('\tType', self._type) string += '{: <16}=\t{}\n'.format('\tBasis', self._basis) string += '{: <16}=\t{}\n'.format('\tWidth', self._width) string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin) @@ -881,7 +869,7 @@ def highlight_domains(self, geometry, domains, seed=1, self._colors[domain] = (r, g, b) def to_xml_element(self): - """Return XML representation of the slice/voxel plot + """Return XML representation of the slice plot Returns ------- @@ -891,10 +879,8 @@ def to_xml_element(self): """ element = super().to_xml_element() - element.set("type", self._type) - - if self._type == 'slice': - element.set("basis", self._basis) + element.set("type", "slice") + element.set("basis", self._basis) subelement = ET.SubElement(element, "origin") subelement.text = ' '.join(map(str, self._origin)) @@ -940,8 +926,8 @@ def from_xml_element(cls, elem): Returns ------- - openmc.Plot - Plot object + openmc.SlicePlot + SlicePlot object """ plot_id = int(elem.get("id")) @@ -950,9 +936,7 @@ def from_xml_element(cls, elem): if "filename" in elem.keys(): plot.filename = elem.get("filename") plot.color_by = elem.get("color_by") - plot.type = elem.get("type") - if plot.type == 'slice': - plot.basis = elem.get("basis") + plot.basis = elem.get("basis") plot.origin = get_elem_tuple(elem, "origin", float) plot.width = get_elem_tuple(elem, "width", float) @@ -1037,9 +1021,246 @@ def to_ipython_image(self, openmc_exec='openmc', cwd='.'): # Return produced image return _get_plot_image(self, cwd) + + +class VoxelPlot(PlotBase): + """Definition of a 3D voxel plot of the geometry. + + Colors that are used in plots can be given as RGB tuples, e.g. + (255, 255, 255) would be white, or by a string indicating a + valid `SVG color `_. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + plot_id : int + Unique identifier for the plot + name : str + Name of the plot + + Attributes + ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction (3 values) + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + background : Iterable of int or str + Color of the background + mask_components : Iterable of openmc.Cell or openmc.Material or int + The cells or materials (or corresponding IDs) to mask + mask_background : Iterable of int or str + Color to apply to all cells/materials listed in mask_components + show_overlaps : bool + Indicate whether or not overlapping regions are shown + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + level : int + Universe depth to plot at + width : Iterable of float + Width of the plot in each dimension (3 values) + origin : tuple or list of ndarray + Origin (center) of the plot (3 values) + + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._width = [4.0, 4.0, 4.0] + self._origin = [0., 0., 0.] + + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 3) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + cv.check_type('plot width', width, Iterable, Real) + cv.check_length('plot width', width, 3) + self._width = width + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, origin): + cv.check_type('plot origin', origin, Iterable, Real) + cv.check_length('plot origin', origin, 3) + self._origin = origin + + def __repr__(self): + string = 'VoxelPlot\n' + string += '{: <16}=\t{}\n'.format('\tID', self._id) + string += '{: <16}=\t{}\n'.format('\tName', self._name) + string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) + string += '{: <16}=\t{}\n'.format('\tWidth', self._width) + string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin) + string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) + string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) + string += '{: <16}=\t{}\n'.format('\tBackground', self._background) + string += '{: <16}=\t{}\n'.format('\tMask components', + self._mask_components) + string += '{: <16}=\t{}\n'.format('\tMask background', + self._mask_background) + string += '{: <16}=\t{}\n'.format('\tOverlap Color', + self._overlap_color) + string += '{: <16}=\t{}\n'.format('\tColors', self._colors) + string += '{: <16}=\t{}\n'.format('\tLevel', self._level) + return string + + def to_xml_element(self): + """Return XML representation of the voxel plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + + element = super().to_xml_element() + element.set("type", "voxel") + + subelement = ET.SubElement(element, "origin") + subelement.text = ' '.join(map(str, self._origin)) + + subelement = ET.SubElement(element, "width") + subelement.text = ' '.join(map(str, self._width)) + + if self._colors: + self._colors_to_xml(element) + + if self._show_overlaps: + subelement = ET.SubElement(element, "show_overlaps") + subelement.text = "true" + + if self._overlap_color is not None: + color = self._overlap_color + if isinstance(color, str): + color = _SVG_COLORS[color.lower()] + subelement = ET.SubElement(element, "overlap_color") + subelement.text = ' '.join(str(x) for x in color) + + return element + + @classmethod + def from_xml_element(cls, elem): + """Generate plot object from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.VoxelPlot + VoxelPlot object + + """ + plot_id = int(elem.get("id")) + name = get_text(elem, 'name', '') + plot = cls(plot_id, name) + if "filename" in elem.keys(): + plot.filename = elem.get("filename") + plot.color_by = elem.get("color_by") + + plot.origin = get_elem_tuple(elem, "origin", float) + plot.width = get_elem_tuple(elem, "width", float) + plot.pixels = get_elem_tuple(elem, "pixels") + plot._background = get_elem_tuple(elem, "background") + + # Set plot colors + colors = {} + for color_elem in elem.findall("color"): + uid = int(color_elem.get("id")) + colors[uid] = tuple([int(x) + for x in color_elem.get("rgb").split()]) + plot.colors = colors + + # Set masking information + mask_elem = elem.find("mask") + if mask_elem is not None: + plot.mask_components = [ + int(x) for x in mask_elem.get("components").split()] + background = mask_elem.get("background") + if background is not None: + plot.mask_background = tuple( + [int(x) for x in background.split()]) + + # show overlaps + overlap_elem = elem.find("show_overlaps") + if overlap_elem is not None: + plot.show_overlaps = (overlap_elem.text in ('true', '1')) + overlap_color = get_elem_tuple(elem, "overlap_color") + if overlap_color is not None: + plot.overlap_color = overlap_color + + # Set universe level + level = elem.find("level") + if level is not None: + plot.level = int(level.text) + + return plot + + def to_ipython_image(self, openmc_exec='openmc', cwd='.'): + """Render plot as an image + + This method runs OpenMC in plotting mode to produce a .png file. + + .. versionchanged:: 0.13.0 + The *convert_exec* argument was removed since OpenMC now produces + .png images directly. + + Parameters + ---------- + openmc_exec : str + Path to OpenMC executable + cwd : str, optional + Path to working directory to run in + + Returns + ------- + IPython.display.Image + Image generated + + """ + # Create plots.xml + Plots([self]).export_to_xml(cwd) + + # Run OpenMC in geometry plotting mode + openmc.plot_geometry(False, openmc_exec, cwd) + + # Return produced image + return _get_plot_image(self, cwd) + def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): - """Render plot as an voxel image + """Render plot as a voxel image This method runs OpenMC in plotting mode to produce a .vti file. @@ -1060,10 +1281,6 @@ def to_vtk(self, output: PathLike | None = None, Path of the .vti file produced """ - if self.type != 'voxel': - raise ValueError( - 'Generating a VTK file only works for voxel plots') - # Create plots.xml Plots([self]).export_to_xml(cwd) @@ -1083,6 +1300,255 @@ def to_vtk(self, output: PathLike | None = None, return voxel_to_vtk(h5_voxel_file, output) +class Plot(SlicePlot): + """Legacy Plot class for backward compatibility. + + .. deprecated:: 0.15.1 + Use :class:`SlicePlot` for 2D slice plots or :class:`VoxelPlot` for 3D voxel plots. + + """ + + def __init__(self, plot_id=None, name=''): + import warnings + warnings.warn( + "The Plot class is deprecated. Use SlicePlot for 2D slice plots " + "or VoxelPlot for 3D voxel plots.", + DeprecationWarning, + stacklevel=2 + ) + super().__init__(plot_id, name) + self._type = 'slice' + + @property + def type(self): + return self._type + + @type.setter + def type(self, plottype): + cv.check_value('plot type', plottype, ['slice', 'voxel']) + self._type = plottype + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + cv.check_type('plot width', width, Iterable, Real) + cv.check_length('plot width', width, 2, 3) + self._width = width + + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 2, 3) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + + def to_xml_element(self): + """Return XML representation of the plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + if self._type == 'voxel': + # Convert to VoxelPlot for proper XML generation + voxel_plot = VoxelPlot(self.id, self.name) + voxel_plot._width = self._width + voxel_plot._origin = self._origin + voxel_plot._pixels = self._pixels + voxel_plot._filename = self._filename + voxel_plot._color_by = self._color_by + voxel_plot._background = self._background + voxel_plot._mask_components = self._mask_components + voxel_plot._mask_background = self._mask_background + voxel_plot._show_overlaps = self._show_overlaps + voxel_plot._overlap_color = self._overlap_color + voxel_plot._colors = self._colors + voxel_plot._level = self._level + return voxel_plot.to_xml_element() + else: + # Use parent SlicePlot implementation + return super().to_xml_element() + + @classmethod + def from_xml_element(cls, elem): + """Generate plot object from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.Plot + Plot object + + """ + plot_type = elem.get('type') + if plot_type == 'voxel': + # Create a Plot but with voxel type + plot_id = int(elem.get("id")) + name = get_text(elem, 'name', '') + plot = cls(plot_id, name) + plot._type = 'voxel' + if "filename" in elem.keys(): + plot.filename = elem.get("filename") + plot.color_by = elem.get("color_by") + + plot.origin = get_elem_tuple(elem, "origin", float) + plot.width = get_elem_tuple(elem, "width", float) + plot.pixels = get_elem_tuple(elem, "pixels") + plot._background = get_elem_tuple(elem, "background") + + # Set plot colors + colors = {} + for color_elem in elem.findall("color"): + uid = int(color_elem.get("id")) + colors[uid] = tuple([int(x) + for x in color_elem.get("rgb").split()]) + plot.colors = colors + + # Set masking information + mask_elem = elem.find("mask") + if mask_elem is not None: + plot.mask_components = [ + int(x) for x in mask_elem.get("components").split()] + background = mask_elem.get("background") + if background is not None: + plot.mask_background = tuple( + [int(x) for x in background.split()]) + + # show overlaps + overlap_elem = elem.find("show_overlaps") + if overlap_elem is not None: + plot.show_overlaps = (overlap_elem.text in ('true', '1')) + overlap_color = get_elem_tuple(elem, "overlap_color") + if overlap_color is not None: + plot.overlap_color = overlap_color + + # Set universe level + level = elem.find("level") + if level is not None: + plot.level = int(level.text) + + return plot + else: + # Use SlicePlot.from_xml_element but return as Plot + plot_id = int(elem.get("id")) + name = get_text(elem, 'name', '') + plot = cls(plot_id, name) + if "filename" in elem.keys(): + plot.filename = elem.get("filename") + plot.color_by = elem.get("color_by") + plot.basis = elem.get("basis") + + plot.origin = get_elem_tuple(elem, "origin", float) + plot.width = get_elem_tuple(elem, "width", float) + plot.pixels = get_elem_tuple(elem, "pixels") + plot._background = get_elem_tuple(elem, "background") + + # Set plot colors + colors = {} + for color_elem in elem.findall("color"): + uid = int(color_elem.get("id")) + colors[uid] = tuple([int(x) + for x in color_elem.get("rgb").split()]) + plot.colors = colors + + # Set masking information + mask_elem = elem.find("mask") + if mask_elem is not None: + plot.mask_components = [ + int(x) for x in mask_elem.get("components").split()] + background = mask_elem.get("background") + if background is not None: + plot.mask_background = tuple( + [int(x) for x in background.split()]) + + # show overlaps + overlap_elem = elem.find("show_overlaps") + if overlap_elem is not None: + plot.show_overlaps = (overlap_elem.text in ('true', '1')) + overlap_color = get_elem_tuple(elem, "overlap_color") + if overlap_color is not None: + plot.overlap_color = overlap_color + + # Set universe level + level = elem.find("level") + if level is not None: + plot.level = int(level.text) + + # Set meshlines + mesh_elem = elem.find("meshlines") + if mesh_elem is not None: + meshlines = {'type': mesh_elem.get('meshtype')} + if 'id' in mesh_elem.keys(): + meshlines['id'] = int(mesh_elem.get('id')) + if 'linewidth' in mesh_elem.keys(): + meshlines['linewidth'] = int(mesh_elem.get('linewidth')) + if 'color' in mesh_elem.keys(): + meshlines['color'] = tuple( + [int(x) for x in mesh_elem.get('color').split()] + ) + plot.meshlines = meshlines + + return plot + + def to_vtk(self, output: PathLike | None = None, + openmc_exec: str = 'openmc', cwd: str = '.'): + """Render plot as a voxel image + + This method runs OpenMC in plotting mode to produce a .vti file. + + .. versionadded:: 0.14.0 + + Parameters + ---------- + output : path-like + Path of the output .vti file produced + openmc_exec : str + Path to OpenMC executable + cwd : str, optional + Path to working directory to run in + + Returns + ------- + Path + Path of the .vti file produced + + """ + if self.type != 'voxel': + raise ValueError( + 'Generating a VTK file only works for voxel plots') + + # Convert to VoxelPlot and call its to_vtk method + voxel_plot = VoxelPlot(self.id, self.name) + voxel_plot._width = self._width + voxel_plot._origin = self._origin + voxel_plot._pixels = self._pixels + voxel_plot._filename = self._filename + voxel_plot._color_by = self._color_by + voxel_plot._background = self._background + voxel_plot._mask_components = self._mask_components + voxel_plot._mask_background = self._mask_background + voxel_plot._show_overlaps = self._show_overlaps + voxel_plot._overlap_color = self._overlap_color + voxel_plot._colors = self._colors + voxel_plot._level = self._level + + return voxel_plot.to_vtk(output, openmc_exec, cwd) + + class RayTracePlot(PlotBase): """Definition of a camera's view of OpenMC geometry @@ -1904,8 +2370,13 @@ def from_xml_element(cls, elem): plots.append(WireframeRayTracePlot.from_xml_element(e)) elif plot_type == 'solid_raytrace': plots.append(SolidRayTracePlot.from_xml_element(e)) - elif plot_type in ('slice', 'voxel'): - plots.append(Plot.from_xml_element(e)) + elif plot_type == 'slice': + plots.append(SlicePlot.from_xml_element(e)) + elif plot_type == 'voxel': + plots.append(VoxelPlot.from_xml_element(e)) + elif plot_type is None: + # For backward compatibility, assume slice if no type specified + plots.append(SlicePlot.from_xml_element(e)) else: raise ValueError("Unknown plot type: {}".format(plot_type)) return plots From 64d72b09ea2b0a605de95863291c9e3f4fde630e Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Mon, 11 Aug 2025 09:46:18 -0700 Subject: [PATCH 03/11] fix: enforce strict width/pixels validation for plot classes - SlicePlot now strictly accepts 2 values for width and pixels - VoxelPlot now strictly accepts 3 values for width and pixels - Fixed check_length calls to use both min and max parameters - Ensures proper separation between 2D and 3D plot types --- openmc/plots.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index b6a1d62f845..04b9632c7d7 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -688,6 +688,18 @@ def __init__(self, plot_id=None, name=''): self._basis = 'xy' self._meshlines = None + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 2, 2) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + @property def width(self): return self._width @@ -695,7 +707,7 @@ def width(self): @width.setter def width(self, width): cv.check_type('plot width', width, Iterable, Real) - cv.check_length('plot width', width, 2) + cv.check_length('plot width', width, 2, 2) self._width = width @property @@ -1087,7 +1099,7 @@ def pixels(self): @pixels.setter def pixels(self, pixels): cv.check_type('plot pixels', pixels, Iterable, Integral) - cv.check_length('plot pixels', pixels, 3) + cv.check_length('plot pixels', pixels, 3, 3) for dim in pixels: cv.check_greater_than('plot pixels', dim, 0) self._pixels = pixels @@ -1099,7 +1111,7 @@ def width(self): @width.setter def width(self, width): cv.check_type('plot width', width, Iterable, Real) - cv.check_length('plot width', width, 3) + cv.check_length('plot width', width, 3, 3) self._width = width @property From 3ffb2f44491b20a6aeee32f8d5ac30222ebdc5e6 Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Mon, 11 Aug 2025 09:51:50 -0700 Subject: [PATCH 04/11] test: update tests to use new SlicePlot and VoxelPlot classes - Updated test_voxel_plot to use VoxelPlot directly - Updated test_plot_directory to use SlicePlot - Updated test_highlight_domains to use SlicePlot - Updated test_plots to use both SlicePlot and VoxelPlot - Fixed VoxelPlot default pixels to be [400, 400, 400] - Kept some tests using legacy Plot class for backward compatibility testing --- openmc/plots.py | 1 + tests/unit_tests/test_plots.py | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 04b9632c7d7..87157f560c9 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1091,6 +1091,7 @@ def __init__(self, plot_id=None, name=''): super().__init__(plot_id, name) self._width = [4.0, 4.0, 4.0] self._origin = [0., 0., 0.] + self._pixels = [400, 400, 400] @property def pixels(self): diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index fad574ee697..3f01985a0ca 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -80,8 +80,7 @@ def test_voxel_plot(run_in_tmpdir): geometry.export_to_xml() materials = openmc.Materials() materials.export_to_xml() - vox_plot = openmc.Plot() - vox_plot.type = 'voxel' + vox_plot = openmc.VoxelPlot() vox_plot.id = 12 vox_plot.width = (1500., 1500., 1500.) vox_plot.pixels = (200, 200, 200) @@ -97,8 +96,9 @@ def test_voxel_plot(run_in_tmpdir): assert Path('h5_voxel_plot.h5').is_file() assert Path('another_test_voxel_plot.vti').is_file() - slice_plot = openmc.Plot() - with pytest.raises(ValueError): + # SlicePlot should not have to_vtk method + slice_plot = openmc.SlicePlot() + with pytest.raises(AttributeError): slice_plot.to_vtk('shimmy.vti') @@ -160,7 +160,7 @@ def test_from_geometry(): def test_highlight_domains(): - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.color_by = 'material' plots = openmc.Plots([plot]) @@ -200,11 +200,11 @@ def test_to_xml_element_proj(myprojectionplot): def test_plots(run_in_tmpdir): - p1 = openmc.Plot(name='plot1') + p1 = openmc.SlicePlot(name='plot1') p1.origin = (5., 5., 5.) p1.colors = {10: (255, 100, 0)} p1.mask_components = [2, 4, 6] - p2 = openmc.Plot(name='plot2') + p2 = openmc.SlicePlot(name='plot2') p2.origin = (-3., -3., -3.) plots = openmc.Plots([p1, p2]) assert len(plots) == 2 @@ -213,7 +213,7 @@ def test_plots(run_in_tmpdir): plots = openmc.Plots([p1, p2, p3]) assert len(plots) == 3 - p4 = openmc.Plot(name='plot4') + p4 = openmc.VoxelPlot(name='plot4') plots.append(p4) assert len(plots) == 4 @@ -288,10 +288,9 @@ def test_phong_plot_roundtrip(): def test_plot_directory(run_in_tmpdir): pwr_pin = openmc.examples.pwr_pin_cell() - # create a standard plot, expected to work - plot = openmc.Plot() + # create a standard slice plot, expected to work + plot = openmc.SlicePlot() plot.filename = 'plot_1' - plot.type = 'slice' plot.pixels = (10, 10) plot.color_by = 'material' plot.width = (100., 100.) From 4574244bfaf436d3148fb0ce11fbad350a20ea97 Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Mon, 11 Aug 2025 13:08:24 -0700 Subject: [PATCH 05/11] test: add comprehensive tests for SlicePlot and VoxelPlot classes - Test initialization with proper defaults - Test width/pixels validation (2D vs 3D) - Test basis attribute presence/absence - Test meshlines attribute presence/absence - Test XML serialization/deserialization - Test backward compatibility with Plot class - Test deprecation warnings - Test Plots collection with mixed types - Follow OpenMC testing conventions --- tests/unit_tests/test_slice_voxel_plots.py | 327 +++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 tests/unit_tests/test_slice_voxel_plots.py diff --git a/tests/unit_tests/test_slice_voxel_plots.py b/tests/unit_tests/test_slice_voxel_plots.py new file mode 100644 index 00000000000..a97ae002ef4 --- /dev/null +++ b/tests/unit_tests/test_slice_voxel_plots.py @@ -0,0 +1,327 @@ +"""Tests for SlicePlot and VoxelPlot classes + +This module tests the functionality of the new SlicePlot and VoxelPlot +classes that replace the legacy Plot class. +""" +import warnings +from pathlib import Path + +import numpy as np +import pytest + +import openmc +from openmc.plots import _SVG_COLORS + + +def test_slice_plot_initialization(): + """Test SlicePlot initialization with defaults""" + plot = openmc.SlicePlot() + assert plot.width == [4.0, 4.0] + assert plot.pixels == [400, 400] + assert plot.basis == 'xy' + assert plot.origin == [0., 0., 0.] + + +def test_slice_plot_width_validation(): + """Test that SlicePlot only accepts 2 values for width""" + plot = openmc.SlicePlot() + + # Should accept 2 values + plot.width = [10.0, 20.0] + assert plot.width == [10.0, 20.0] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "2"'): + plot.width = [10.0] + + # Should reject 3 values + with pytest.raises(ValueError, match='must be of length "2"'): + plot.width = [10.0, 20.0, 30.0] + + +def test_slice_plot_pixels_validation(): + """Test that SlicePlot only accepts 2 values for pixels""" + plot = openmc.SlicePlot() + + # Should accept 2 values + plot.pixels = [100, 200] + assert plot.pixels == [100, 200] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "2"'): + plot.pixels = [100] + + # Should reject 3 values + with pytest.raises(ValueError, match='must be of length "2"'): + plot.pixels = [100, 200, 300] + + +def test_slice_plot_basis(): + """Test that SlicePlot has basis attribute""" + plot = openmc.SlicePlot() + + # Test all valid basis values + for basis in ['xy', 'xz', 'yz']: + plot.basis = basis + assert plot.basis == basis + + # Test invalid basis + with pytest.raises(ValueError): + plot.basis = 'invalid' + + +def test_slice_plot_meshlines(): + """Test that SlicePlot has meshlines attribute""" + plot = openmc.SlicePlot() + + meshlines = { + 'type': 'tally', + 'id': 1, + 'linewidth': 2, + 'color': (255, 0, 0) + } + plot.meshlines = meshlines + assert plot.meshlines == meshlines + + +def test_slice_plot_xml_roundtrip(): + """Test SlicePlot XML serialization and deserialization""" + plot = openmc.SlicePlot(name='test_slice') + plot.width = [15.0, 25.0] + plot.pixels = [150, 250] + plot.basis = 'xz' + plot.origin = [1.0, 2.0, 3.0] + plot.color_by = 'material' + plot.filename = 'test_plot' + + # Convert to XML and back + elem = plot.to_xml_element() + new_plot = openmc.SlicePlot.from_xml_element(elem) + + # Check all attributes preserved + assert new_plot.name == plot.name + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.basis == plot.basis + assert new_plot.origin == pytest.approx(plot.origin) + assert new_plot.color_by == plot.color_by + assert new_plot.filename == plot.filename + + +def test_slice_plot_from_geometry(): + """Test creating SlicePlot from geometry""" + # Create simple geometry + s = openmc.Sphere(r=10.0, boundary_type='vacuum') + c = openmc.Cell(region=-s) + univ = openmc.Universe(cells=[c]) + geom = openmc.Geometry(univ) + + # Test all basis options + for basis in ['xy', 'xz', 'yz']: + plot = openmc.SlicePlot.from_geometry(geom, basis=basis) + assert plot.basis == basis + assert plot.width == pytest.approx([20.0, 20.0]) + assert plot.origin == pytest.approx([0.0, 0.0, 0.0]) + + +def test_voxel_plot_initialization(): + """Test VoxelPlot initialization with defaults""" + plot = openmc.VoxelPlot() + assert plot.width == [4.0, 4.0, 4.0] + assert plot.pixels == [400, 400, 400] + assert plot.origin == [0., 0., 0.] + + +def test_voxel_plot_width_validation(): + """Test that VoxelPlot only accepts 3 values for width""" + plot = openmc.VoxelPlot() + + # Should accept 3 values + plot.width = [10.0, 20.0, 30.0] + assert plot.width == [10.0, 20.0, 30.0] + + # Should reject 2 values + with pytest.raises(ValueError, match='must be of length "3"'): + plot.width = [10.0, 20.0] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "3"'): + plot.width = [10.0] + + +def test_voxel_plot_pixels_validation(): + """Test that VoxelPlot only accepts 3 values for pixels""" + plot = openmc.VoxelPlot() + + # Should accept 3 values + plot.pixels = [100, 200, 300] + assert plot.pixels == [100, 200, 300] + + # Should reject 2 values + with pytest.raises(ValueError, match='must be of length "3"'): + plot.pixels = [100, 200] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "3"'): + plot.pixels = [100] + + +def test_voxel_plot_no_basis(): + """Test that VoxelPlot does not have basis attribute""" + plot = openmc.VoxelPlot() + assert not hasattr(plot, 'basis') + assert not hasattr(plot, '_basis') + + +def test_voxel_plot_no_meshlines(): + """Test that VoxelPlot does not have meshlines attribute""" + plot = openmc.VoxelPlot() + assert not hasattr(plot, 'meshlines') + assert not hasattr(plot, '_meshlines') + + +def test_voxel_plot_has_to_vtk(): + """Test that VoxelPlot has to_vtk method""" + plot = openmc.VoxelPlot() + assert hasattr(plot, 'to_vtk') + assert callable(plot.to_vtk) + + +def test_voxel_plot_xml_roundtrip(): + """Test VoxelPlot XML serialization and deserialization""" + plot = openmc.VoxelPlot(name='test_voxel') + plot.width = [10.0, 20.0, 30.0] + plot.pixels = [100, 200, 300] + plot.origin = [1.0, 2.0, 3.0] + plot.color_by = 'cell' + plot.filename = 'voxel_plot' + + # Convert to XML and back + elem = plot.to_xml_element() + new_plot = openmc.VoxelPlot.from_xml_element(elem) + + # Check all attributes preserved + assert new_plot.name == plot.name + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.origin == pytest.approx(plot.origin) + assert new_plot.color_by == plot.color_by + assert new_plot.filename == plot.filename + + +def test_plot_deprecation_warning(): + """Test that Plot class raises deprecation warning""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + plot = openmc.Plot() + + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "deprecated" in str(w[0].message).lower() + + +def test_plot_slice_compatibility(): + """Test Plot class with slice type""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + plot.type = 'slice' + plot.width = [10.0, 20.0] + plot.pixels = [100, 200] + plot.basis = 'yz' + + assert plot.type == 'slice' + assert plot.width == [10.0, 20.0] + assert plot.pixels == [100, 200] + assert plot.basis == 'yz' + + +def test_plot_voxel_compatibility(): + """Test Plot class with voxel type""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + plot.type = 'voxel' + plot.width = [10.0, 20.0, 30.0] + plot.pixels = [100, 200, 300] + + assert plot.type == 'voxel' + assert plot.width == [10.0, 20.0, 30.0] + assert plot.pixels == [100, 200, 300] + + +def test_plot_xml_roundtrip_slice(): + """Test XML roundtrip for Plot with slice type""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot(name='legacy_slice') + plot.type = 'slice' + plot.width = [5.0, 10.0] + plot.pixels = [50, 100] + plot.basis = 'xz' + + elem = plot.to_xml_element() + new_plot = openmc.Plot.from_xml_element(elem) + + assert new_plot.type == plot.type + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.basis == plot.basis + + +def test_plot_xml_roundtrip_voxel(): + """Test XML roundtrip for Plot with voxel type""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot(name='legacy_voxel') + plot.type = 'voxel' + plot.width = [5.0, 10.0, 15.0] + plot.pixels = [50, 100, 150] + + elem = plot.to_xml_element() + new_plot = openmc.Plot.from_xml_element(elem) + + assert new_plot.type == plot.type + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + + +def test_plots_collection_mixed_types(): + """Test Plots collection with different plot types""" + slice_plot = openmc.SlicePlot(name='slice') + voxel_plot = openmc.VoxelPlot(name='voxel') + wireframe_plot = openmc.WireframeRayTracePlot(name='wireframe') + + plots = openmc.Plots([slice_plot, voxel_plot, wireframe_plot]) + + assert len(plots) == 3 + assert isinstance(plots[0], openmc.SlicePlot) + assert isinstance(plots[1], openmc.VoxelPlot) + assert isinstance(plots[2], openmc.WireframeRayTracePlot) + + +def test_plots_collection_xml_roundtrip(run_in_tmpdir): + """Test XML export and import with new plot types""" + s1 = openmc.SlicePlot(name='slice1') + s1.width = [10.0, 20.0] + s1.basis = 'xz' + + v1 = openmc.VoxelPlot(name='voxel1') + v1.width = [10.0, 20.0, 30.0] + + plots = openmc.Plots([s1, v1]) + plots.export_to_xml() + + # Read back + new_plots = openmc.Plots.from_xml() + + assert len(new_plots) == 2 + assert isinstance(new_plots[0], openmc.SlicePlot) + assert isinstance(new_plots[1], openmc.VoxelPlot) + assert new_plots[0].name == 'slice1' + assert new_plots[1].name == 'voxel1' + assert new_plots[0].basis == 'xz' + assert new_plots[0].width == pytest.approx([10.0, 20.0]) + assert new_plots[1].width == pytest.approx([10.0, 20.0, 30.0]) \ No newline at end of file From ed24df12aa144c91e6e3fc450796fe3686ccbcdb Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Mon, 11 Aug 2025 21:12:36 -0700 Subject: [PATCH 06/11] Fix Plot.from_geometry to avoid deprecation warning when used as class method The Plot.from_geometry() class method now suppresses the deprecation warning internally since it's a legitimate use case for backward compatibility. --- openmc/plots.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/openmc/plots.py b/openmc/plots.py index 86fbb5e6382..0489345ff31 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1512,6 +1512,38 @@ def from_xml_element(cls, elem): return plot + @classmethod + def from_geometry(cls, geometry, + basis: str = 'xy', + slice_coord: float = 0.): + """Generate plot from a geometry object + + Parameters + ---------- + geometry : openmc.Geometry + Geometry object to create plot from + basis : {'xy', 'xz', 'yz'} + The basis directions + slice_coord : float + The position of the slice + + Returns + ------- + openmc.Plot + Plot object + + """ + import warnings + # Suppress deprecation warning when called as class method + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + plot = super().from_geometry(geometry, basis, slice_coord) + # Convert from SlicePlot to Plot + new_plot = cls.__new__(cls) + new_plot.__dict__.update(plot.__dict__) + new_plot._type = 'slice' + return new_plot + def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): """Render plot as a voxel image From cb1169d6e65a2b9642c24e162a4a9bd6a46712cb Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Mon, 11 Aug 2025 21:53:31 -0700 Subject: [PATCH 07/11] Fix C++ formatting in mcpl_interface.cpp Remove incorrect line break in mcpl_read_fpt type definition that was introduced during merge conflict resolution. --- src/mcpl_interface.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 27c1d7e4cae..2439d27837a 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -58,8 +58,7 @@ struct mcpl_outfile_t { // Function pointer types for the dynamically loaded MCPL library using mcpl_open_file_fpt = mcpl_file_t* (*)(const char* filename); using mcpl_hdr_nparticles_fpt = uint64_t (*)(mcpl_file_t* file_handle); -using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t * - file_handle); +using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); From cb7e1715a714c323d77e02ab25e0c231fe65f600 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 4 Dec 2025 16:12:31 -0600 Subject: [PATCH 08/11] Remove to_ipython_image from VoxelPlot --- docs/source/pythonapi/base.rst | 3 +- openmc/plots.py | 75 +++++++--------------- tests/unit_tests/test_slice_voxel_plots.py | 72 ++++++++++----------- 3 files changed, 60 insertions(+), 90 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 2a9d0876cd2..609deda3e74 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -176,7 +176,8 @@ Geometry Plotting :nosignatures: :template: myclass.rst - openmc.Plot + openmc.SlicePlot + openmc.VoxelPlot openmc.WireframeRayTracePlot openmc.SolidRayTracePlot openmc.Plots diff --git a/openmc/plots.py b/openmc/plots.py index 0489345ff31..6c87d020d69 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -627,11 +627,11 @@ def to_xml_element(self): class SlicePlot(PlotBase): """Definition of a 2D slice plot of the geometry. - Colors that are used in plots can be given as RGB tuples, e.g. + Colors that are used in plots can be given as RGB tuples, e.g. (255, 255, 255) would be white, or by a string indicating a valid `SVG color `_. - .. versionadded:: 0.15.1 + .. versionadded:: 0.15.4 Parameters ---------- @@ -1035,7 +1035,7 @@ def to_ipython_image(self, openmc_exec='openmc', cwd='.'): class VoxelPlot(PlotBase): """Definition of a 3D voxel plot of the geometry. - Colors that are used in plots can be given as RGB tuples, e.g. + Colors that are used in plots can be given as RGB tuples, e.g. (255, 255, 255) would be white, or by a string indicating a valid `SVG color `_. @@ -1236,37 +1236,6 @@ def from_xml_element(cls, elem): return plot - def to_ipython_image(self, openmc_exec='openmc', cwd='.'): - """Render plot as an image - - This method runs OpenMC in plotting mode to produce a .png file. - - .. versionchanged:: 0.13.0 - The *convert_exec* argument was removed since OpenMC now produces - .png images directly. - - Parameters - ---------- - openmc_exec : str - Path to OpenMC executable - cwd : str, optional - Path to working directory to run in - - Returns - ------- - IPython.display.Image - Image generated - - """ - # Create plots.xml - Plots([self]).export_to_xml(cwd) - - # Run OpenMC in geometry plotting mode - openmc.plot_geometry(False, openmc_exec, cwd) - - # Return produced image - return _get_plot_image(self, cwd) - def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): """Render plot as a voxel image @@ -1413,21 +1382,21 @@ def from_xml_element(cls, elem): if "filename" in elem.keys(): plot.filename = get_text(elem, "filename") plot.color_by = get_text(elem, "color_by") - + plot.origin = tuple(get_elem_list(elem, "origin", float)) plot.width = tuple(get_elem_list(elem, "width", float)) plot.pixels = tuple(get_elem_list(elem, "pixels")) background = get_elem_list(elem, "background") if background is not None: plot._background = tuple(background) - + # Set plot colors colors = {} for color_elem in elem.findall("color"): uid = int(get_text(color_elem, "id")) colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) plot.colors = colors - + # Set masking information mask_elem = elem.find("mask") if mask_elem is not None: @@ -1435,7 +1404,7 @@ def from_xml_element(cls, elem): background = get_elem_list(mask_elem, "background", int) if background is not None: plot.mask_background = tuple(background) - + # show overlaps overlap = get_text(elem, "show_overlaps") if overlap is not None: @@ -1443,12 +1412,12 @@ def from_xml_element(cls, elem): overlap_color = get_elem_list(elem, "overlap_color", int) if overlap_color is not None: plot.overlap_color = tuple(overlap_color) - + # Set universe level level = get_text(elem, "level") if level is not None: plot.level = int(level) - + return plot else: # Use SlicePlot.from_xml_element but return as Plot @@ -1459,21 +1428,21 @@ def from_xml_element(cls, elem): plot.filename = get_text(elem, "filename") plot.color_by = get_text(elem, "color_by") plot.basis = get_text(elem, "basis") - + plot.origin = tuple(get_elem_list(elem, "origin", float)) plot.width = tuple(get_elem_list(elem, "width", float)) plot.pixels = tuple(get_elem_list(elem, "pixels")) background = get_elem_list(elem, "background") if background is not None: plot._background = tuple(background) - + # Set plot colors colors = {} for color_elem in elem.findall("color"): uid = int(get_text(color_elem, "id")) colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) plot.colors = colors - + # Set masking information mask_elem = elem.find("mask") if mask_elem is not None: @@ -1481,7 +1450,7 @@ def from_xml_element(cls, elem): background = get_elem_list(mask_elem, "background", int) if background is not None: plot.mask_background = tuple(background) - + # show overlaps overlap = get_text(elem, "show_overlaps") if overlap is not None: @@ -1489,12 +1458,12 @@ def from_xml_element(cls, elem): overlap_color = get_elem_list(elem, "overlap_color", int) if overlap_color is not None: plot.overlap_color = tuple(overlap_color) - + # Set universe level level = get_text(elem, "level") if level is not None: plot.level = int(level) - + # Set meshlines mesh_elem = elem.find("meshlines") if mesh_elem is not None: @@ -1509,15 +1478,15 @@ def from_xml_element(cls, elem): if color is not None: meshlines['color'] = tuple(color) plot.meshlines = meshlines - + return plot - @classmethod + @classmethod def from_geometry(cls, geometry, basis: str = 'xy', slice_coord: float = 0.): """Generate plot from a geometry object - + Parameters ---------- geometry : openmc.Geometry @@ -1526,12 +1495,12 @@ def from_geometry(cls, geometry, The basis directions slice_coord : float The position of the slice - + Returns ------- openmc.Plot Plot object - + """ import warnings # Suppress deprecation warning when called as class method @@ -1570,7 +1539,7 @@ def to_vtk(self, output: PathLike | None = None, if self.type != 'voxel': raise ValueError( 'Generating a VTK file only works for voxel plots') - + # Convert to VoxelPlot and call its to_vtk method voxel_plot = VoxelPlot(self.id, self.name) voxel_plot._width = self._width @@ -1585,7 +1554,7 @@ def to_vtk(self, output: PathLike | None = None, voxel_plot._overlap_color = self._overlap_color voxel_plot._colors = self._colors voxel_plot._level = self._level - + return voxel_plot.to_vtk(output, openmc_exec, cwd) diff --git a/tests/unit_tests/test_slice_voxel_plots.py b/tests/unit_tests/test_slice_voxel_plots.py index a97ae002ef4..7cc7e2dffc8 100644 --- a/tests/unit_tests/test_slice_voxel_plots.py +++ b/tests/unit_tests/test_slice_voxel_plots.py @@ -25,15 +25,15 @@ def test_slice_plot_initialization(): def test_slice_plot_width_validation(): """Test that SlicePlot only accepts 2 values for width""" plot = openmc.SlicePlot() - + # Should accept 2 values plot.width = [10.0, 20.0] assert plot.width == [10.0, 20.0] - + # Should reject 1 value with pytest.raises(ValueError, match='must be of length "2"'): plot.width = [10.0] - + # Should reject 3 values with pytest.raises(ValueError, match='must be of length "2"'): plot.width = [10.0, 20.0, 30.0] @@ -42,15 +42,15 @@ def test_slice_plot_width_validation(): def test_slice_plot_pixels_validation(): """Test that SlicePlot only accepts 2 values for pixels""" plot = openmc.SlicePlot() - + # Should accept 2 values plot.pixels = [100, 200] assert plot.pixels == [100, 200] - + # Should reject 1 value with pytest.raises(ValueError, match='must be of length "2"'): plot.pixels = [100] - + # Should reject 3 values with pytest.raises(ValueError, match='must be of length "2"'): plot.pixels = [100, 200, 300] @@ -59,12 +59,12 @@ def test_slice_plot_pixels_validation(): def test_slice_plot_basis(): """Test that SlicePlot has basis attribute""" plot = openmc.SlicePlot() - + # Test all valid basis values for basis in ['xy', 'xz', 'yz']: plot.basis = basis assert plot.basis == basis - + # Test invalid basis with pytest.raises(ValueError): plot.basis = 'invalid' @@ -73,7 +73,7 @@ def test_slice_plot_basis(): def test_slice_plot_meshlines(): """Test that SlicePlot has meshlines attribute""" plot = openmc.SlicePlot() - + meshlines = { 'type': 'tally', 'id': 1, @@ -93,11 +93,11 @@ def test_slice_plot_xml_roundtrip(): plot.origin = [1.0, 2.0, 3.0] plot.color_by = 'material' plot.filename = 'test_plot' - + # Convert to XML and back elem = plot.to_xml_element() new_plot = openmc.SlicePlot.from_xml_element(elem) - + # Check all attributes preserved assert new_plot.name == plot.name assert new_plot.width == pytest.approx(plot.width) @@ -115,7 +115,7 @@ def test_slice_plot_from_geometry(): c = openmc.Cell(region=-s) univ = openmc.Universe(cells=[c]) geom = openmc.Geometry(univ) - + # Test all basis options for basis in ['xy', 'xz', 'yz']: plot = openmc.SlicePlot.from_geometry(geom, basis=basis) @@ -135,15 +135,15 @@ def test_voxel_plot_initialization(): def test_voxel_plot_width_validation(): """Test that VoxelPlot only accepts 3 values for width""" plot = openmc.VoxelPlot() - + # Should accept 3 values plot.width = [10.0, 20.0, 30.0] assert plot.width == [10.0, 20.0, 30.0] - + # Should reject 2 values with pytest.raises(ValueError, match='must be of length "3"'): plot.width = [10.0, 20.0] - + # Should reject 1 value with pytest.raises(ValueError, match='must be of length "3"'): plot.width = [10.0] @@ -152,15 +152,15 @@ def test_voxel_plot_width_validation(): def test_voxel_plot_pixels_validation(): """Test that VoxelPlot only accepts 3 values for pixels""" plot = openmc.VoxelPlot() - + # Should accept 3 values plot.pixels = [100, 200, 300] assert plot.pixels == [100, 200, 300] - + # Should reject 2 values with pytest.raises(ValueError, match='must be of length "3"'): plot.pixels = [100, 200] - + # Should reject 1 value with pytest.raises(ValueError, match='must be of length "3"'): plot.pixels = [100] @@ -195,11 +195,11 @@ def test_voxel_plot_xml_roundtrip(): plot.origin = [1.0, 2.0, 3.0] plot.color_by = 'cell' plot.filename = 'voxel_plot' - + # Convert to XML and back elem = plot.to_xml_element() new_plot = openmc.VoxelPlot.from_xml_element(elem) - + # Check all attributes preserved assert new_plot.name == plot.name assert new_plot.width == pytest.approx(plot.width) @@ -214,7 +214,7 @@ def test_plot_deprecation_warning(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") plot = openmc.Plot() - + assert len(w) == 1 assert issubclass(w[0].category, DeprecationWarning) assert "deprecated" in str(w[0].message).lower() @@ -225,12 +225,12 @@ def test_plot_slice_compatibility(): with warnings.catch_warnings(): warnings.simplefilter("ignore") plot = openmc.Plot() - + plot.type = 'slice' plot.width = [10.0, 20.0] plot.pixels = [100, 200] plot.basis = 'yz' - + assert plot.type == 'slice' assert plot.width == [10.0, 20.0] assert plot.pixels == [100, 200] @@ -242,11 +242,11 @@ def test_plot_voxel_compatibility(): with warnings.catch_warnings(): warnings.simplefilter("ignore") plot = openmc.Plot() - + plot.type = 'voxel' plot.width = [10.0, 20.0, 30.0] plot.pixels = [100, 200, 300] - + assert plot.type == 'voxel' assert plot.width == [10.0, 20.0, 30.0] assert plot.pixels == [100, 200, 300] @@ -261,10 +261,10 @@ def test_plot_xml_roundtrip_slice(): plot.width = [5.0, 10.0] plot.pixels = [50, 100] plot.basis = 'xz' - + elem = plot.to_xml_element() new_plot = openmc.Plot.from_xml_element(elem) - + assert new_plot.type == plot.type assert new_plot.width == pytest.approx(plot.width) assert new_plot.pixels == tuple(plot.pixels) @@ -279,10 +279,10 @@ def test_plot_xml_roundtrip_voxel(): plot.type = 'voxel' plot.width = [5.0, 10.0, 15.0] plot.pixels = [50, 100, 150] - + elem = plot.to_xml_element() new_plot = openmc.Plot.from_xml_element(elem) - + assert new_plot.type == plot.type assert new_plot.width == pytest.approx(plot.width) assert new_plot.pixels == tuple(plot.pixels) @@ -293,9 +293,9 @@ def test_plots_collection_mixed_types(): slice_plot = openmc.SlicePlot(name='slice') voxel_plot = openmc.VoxelPlot(name='voxel') wireframe_plot = openmc.WireframeRayTracePlot(name='wireframe') - + plots = openmc.Plots([slice_plot, voxel_plot, wireframe_plot]) - + assert len(plots) == 3 assert isinstance(plots[0], openmc.SlicePlot) assert isinstance(plots[1], openmc.VoxelPlot) @@ -307,16 +307,16 @@ def test_plots_collection_xml_roundtrip(run_in_tmpdir): s1 = openmc.SlicePlot(name='slice1') s1.width = [10.0, 20.0] s1.basis = 'xz' - + v1 = openmc.VoxelPlot(name='voxel1') v1.width = [10.0, 20.0, 30.0] - + plots = openmc.Plots([s1, v1]) plots.export_to_xml() - + # Read back new_plots = openmc.Plots.from_xml() - + assert len(new_plots) == 2 assert isinstance(new_plots[0], openmc.SlicePlot) assert isinstance(new_plots[1], openmc.VoxelPlot) @@ -324,4 +324,4 @@ def test_plots_collection_xml_roundtrip(run_in_tmpdir): assert new_plots[1].name == 'voxel1' assert new_plots[0].basis == 'xz' assert new_plots[0].width == pytest.approx([10.0, 20.0]) - assert new_plots[1].width == pytest.approx([10.0, 20.0, 30.0]) \ No newline at end of file + assert new_plots[1].width == pytest.approx([10.0, 20.0, 30.0]) From a49ec1ef4e2ed63e52ff4f74cf913aac2382ae1b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 4 Dec 2025 16:31:58 -0600 Subject: [PATCH 09/11] Simplify backward compatibility --- openmc/plots.py | 297 ++++-------------------------------------------- 1 file changed, 24 insertions(+), 273 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 6c87d020d69..d9e17609f54 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,6 +1,7 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path +import warnings import h5py import lxml.etree as ET @@ -688,6 +689,22 @@ def __init__(self, plot_id=None, name=''): self._basis = 'xy' self._meshlines = None + @property + def type(self): + warnings.warn( + "The 'type' attribute is deprecated and will be removed in a future version. " + "This is a SlicePlot instance.", + FutureWarning, stacklevel=2 + ) + return 'slice' + + @type.setter + def type(self, value): + raise TypeError( + "Setting plot.type is no longer supported. " + "Use openmc.SlicePlot() for 2D slice plots or openmc.VoxelPlot() for 3D voxel plots." + ) + @property def pixels(self): return self._pixels @@ -1278,284 +1295,18 @@ def to_vtk(self, output: PathLike | None = None, return voxel_to_vtk(h5_voxel_file, output) -class Plot(SlicePlot): +def Plot(plot_id=None, name=''): """Legacy Plot class for backward compatibility. - .. deprecated:: 0.15.1 + .. deprecated:: 0.15.4 Use :class:`SlicePlot` for 2D slice plots or :class:`VoxelPlot` for 3D voxel plots. """ - - def __init__(self, plot_id=None, name=''): - import warnings - warnings.warn( - "The Plot class is deprecated. Use SlicePlot for 2D slice plots " - "or VoxelPlot for 3D voxel plots.", - DeprecationWarning, - stacklevel=2 - ) - super().__init__(plot_id, name) - self._type = 'slice' - - @property - def type(self): - return self._type - - @type.setter - def type(self, plottype): - cv.check_value('plot type', plottype, ['slice', 'voxel']) - self._type = plottype - - @property - def width(self): - return self._width - - @width.setter - def width(self, width): - cv.check_type('plot width', width, Iterable, Real) - cv.check_length('plot width', width, 2, 3) - self._width = width - - @property - def pixels(self): - return self._pixels - - @pixels.setter - def pixels(self, pixels): - cv.check_type('plot pixels', pixels, Iterable, Integral) - cv.check_length('plot pixels', pixels, 2, 3) - for dim in pixels: - cv.check_greater_than('plot pixels', dim, 0) - self._pixels = pixels - - def to_xml_element(self): - """Return XML representation of the plot - - Returns - ------- - element : lxml.etree._Element - XML element containing plot data - - """ - if self._type == 'voxel': - # Convert to VoxelPlot for proper XML generation - voxel_plot = VoxelPlot(self.id, self.name) - voxel_plot._width = self._width - voxel_plot._origin = self._origin - voxel_plot._pixels = self._pixels - voxel_plot._filename = self._filename - voxel_plot._color_by = self._color_by - voxel_plot._background = self._background - voxel_plot._mask_components = self._mask_components - voxel_plot._mask_background = self._mask_background - voxel_plot._show_overlaps = self._show_overlaps - voxel_plot._overlap_color = self._overlap_color - voxel_plot._colors = self._colors - voxel_plot._level = self._level - return voxel_plot.to_xml_element() - else: - # Use parent SlicePlot implementation - return super().to_xml_element() - - @classmethod - def from_xml_element(cls, elem): - """Generate plot object from an XML element - - Parameters - ---------- - elem : lxml.etree._Element - XML element - - Returns - ------- - openmc.Plot - Plot object - - """ - plot_type = get_text(elem, 'type') - if plot_type == 'voxel': - # Create a Plot but with voxel type - plot_id = int(get_text(elem, "id")) - name = get_text(elem, 'name', '') - plot = cls(plot_id, name) - plot._type = 'voxel' - if "filename" in elem.keys(): - plot.filename = get_text(elem, "filename") - plot.color_by = get_text(elem, "color_by") - - plot.origin = tuple(get_elem_list(elem, "origin", float)) - plot.width = tuple(get_elem_list(elem, "width", float)) - plot.pixels = tuple(get_elem_list(elem, "pixels")) - background = get_elem_list(elem, "background") - if background is not None: - plot._background = tuple(background) - - # Set plot colors - colors = {} - for color_elem in elem.findall("color"): - uid = int(get_text(color_elem, "id")) - colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) - plot.colors = colors - - # Set masking information - mask_elem = elem.find("mask") - if mask_elem is not None: - plot.mask_components = get_elem_list(mask_elem, "components", int) - background = get_elem_list(mask_elem, "background", int) - if background is not None: - plot.mask_background = tuple(background) - - # show overlaps - overlap = get_text(elem, "show_overlaps") - if overlap is not None: - plot.show_overlaps = (overlap in ('true', '1')) - overlap_color = get_elem_list(elem, "overlap_color", int) - if overlap_color is not None: - plot.overlap_color = tuple(overlap_color) - - # Set universe level - level = get_text(elem, "level") - if level is not None: - plot.level = int(level) - - return plot - else: - # Use SlicePlot.from_xml_element but return as Plot - plot_id = int(get_text(elem, "id")) - name = get_text(elem, 'name', '') - plot = cls(plot_id, name) - if "filename" in elem.keys(): - plot.filename = get_text(elem, "filename") - plot.color_by = get_text(elem, "color_by") - plot.basis = get_text(elem, "basis") - - plot.origin = tuple(get_elem_list(elem, "origin", float)) - plot.width = tuple(get_elem_list(elem, "width", float)) - plot.pixels = tuple(get_elem_list(elem, "pixels")) - background = get_elem_list(elem, "background") - if background is not None: - plot._background = tuple(background) - - # Set plot colors - colors = {} - for color_elem in elem.findall("color"): - uid = int(get_text(color_elem, "id")) - colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) - plot.colors = colors - - # Set masking information - mask_elem = elem.find("mask") - if mask_elem is not None: - plot.mask_components = get_elem_list(mask_elem, "components", int) - background = get_elem_list(mask_elem, "background", int) - if background is not None: - plot.mask_background = tuple(background) - - # show overlaps - overlap = get_text(elem, "show_overlaps") - if overlap is not None: - plot.show_overlaps = (overlap in ('true', '1')) - overlap_color = get_elem_list(elem, "overlap_color", int) - if overlap_color is not None: - plot.overlap_color = tuple(overlap_color) - - # Set universe level - level = get_text(elem, "level") - if level is not None: - plot.level = int(level) - - # Set meshlines - mesh_elem = elem.find("meshlines") - if mesh_elem is not None: - meshlines = {'type': get_text(mesh_elem, 'meshtype')} - mesh_id = get_text(mesh_elem, 'id') - if mesh_id is not None: - meshlines['id'] = int(mesh_id) - linewidth = get_text(mesh_elem, 'linewidth') - if linewidth is not None: - meshlines['linewidth'] = int(linewidth) - color = get_elem_list(mesh_elem, 'color', int) - if color is not None: - meshlines['color'] = tuple(color) - plot.meshlines = meshlines - - return plot - - @classmethod - def from_geometry(cls, geometry, - basis: str = 'xy', - slice_coord: float = 0.): - """Generate plot from a geometry object - - Parameters - ---------- - geometry : openmc.Geometry - Geometry object to create plot from - basis : {'xy', 'xz', 'yz'} - The basis directions - slice_coord : float - The position of the slice - - Returns - ------- - openmc.Plot - Plot object - - """ - import warnings - # Suppress deprecation warning when called as class method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - plot = super().from_geometry(geometry, basis, slice_coord) - # Convert from SlicePlot to Plot - new_plot = cls.__new__(cls) - new_plot.__dict__.update(plot.__dict__) - new_plot._type = 'slice' - return new_plot - - def to_vtk(self, output: PathLike | None = None, - openmc_exec: str = 'openmc', cwd: str = '.'): - """Render plot as a voxel image - - This method runs OpenMC in plotting mode to produce a .vti file. - - .. versionadded:: 0.14.0 - - Parameters - ---------- - output : path-like - Path of the output .vti file produced - openmc_exec : str - Path to OpenMC executable - cwd : str, optional - Path to working directory to run in - - Returns - ------- - Path - Path of the .vti file produced - - """ - if self.type != 'voxel': - raise ValueError( - 'Generating a VTK file only works for voxel plots') - - # Convert to VoxelPlot and call its to_vtk method - voxel_plot = VoxelPlot(self.id, self.name) - voxel_plot._width = self._width - voxel_plot._origin = self._origin - voxel_plot._pixels = self._pixels - voxel_plot._filename = self._filename - voxel_plot._color_by = self._color_by - voxel_plot._background = self._background - voxel_plot._mask_components = self._mask_components - voxel_plot._mask_background = self._mask_background - voxel_plot._show_overlaps = self._show_overlaps - voxel_plot._overlap_color = self._overlap_color - voxel_plot._colors = self._colors - voxel_plot._level = self._level - - return voxel_plot.to_vtk(output, openmc_exec, cwd) + warnings.warn( + "The Plot class is deprecated. Use SlicePlot for 2D slice plots " + "or VoxelPlot for 3D voxel plots.", FutureWarning + ) + return SlicePlot(plot_id, name) class RayTracePlot(PlotBase): From f3e3f3288fccc53bc05248cb498c7b5cd6f4da7b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 4 Dec 2025 18:01:09 -0600 Subject: [PATCH 10/11] Update use of Plot throughout codebase --- docs/source/usersguide/plots.rst | 46 +++++----- docs/source/usersguide/random_ray.rst | 6 +- examples/lattice/hexagonal/build_xml.py | 4 +- examples/lattice/nested/build_xml.py | 2 +- examples/lattice/simple/build_xml.py | 2 +- examples/pincell_random_ray/build_xml.py | 3 +- openmc/examples.py | 6 +- openmc/executor.py | 2 +- openmc/model/model.py | 2 +- openmc/plots.py | 10 +-- tests/regression_tests/distribmat/test.py | 4 +- tests/unit_tests/test_model.py | 6 +- tests/unit_tests/test_plots.py | 13 ++- tests/unit_tests/test_slice_voxel_plots.py | 98 +++++----------------- 14 files changed, 71 insertions(+), 133 deletions(-) diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index da0c69bdd8d..b5c29a3e886 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -6,13 +6,14 @@ Geometry Visualization .. currentmodule:: openmc -OpenMC is capable of producing two-dimensional slice plots of a geometry as well -as three-dimensional voxel plots using the geometry plotting :ref:`run mode -`. The geometry plotting mode relies on the presence of a -:ref:`plots.xml ` file that indicates what plots should be created. To -create this file, one needs to create one or more :class:`openmc.Plot` -instances, add them to a :class:`openmc.Plots` collection, and then use the -:class:`Plots.export_to_xml` method to write the ``plots.xml`` file. +OpenMC is capable of producing two-dimensional slice plots of a geometry, +three-dimensional voxel plots, and three-dimensional raytrace plots using the +geometry plotting :ref:`run mode `. The geometry plotting +mode relies on the presence of a :ref:`plots.xml ` file that indicates +what plots should be created. To create this file, one needs to create one or +more instances of the various plot classes described below, add them to a +:class:`openmc.Plots` collection, and then use the :class:`Plots.export_to_xml` +method to write the ``plots.xml`` file. ----------- Slice Plots @@ -21,15 +22,14 @@ Slice Plots .. image:: ../_images/atr.png :width: 300px -By default, when an instance of :class:`openmc.Plot` is created, it indicates -that a 2D slice plot should be made. You can specify the origin of the plot -(:attr:`Plot.origin`), the width of the plot in each direction -(:attr:`Plot.width`), the number of pixels to use in each direction -(:attr:`Plot.pixels`), and the basis directions for the plot. For example, to -create a :math:`x` - :math:`z` plot centered at (5.0, 2.0, 3.0) with a width of -(50., 50.) and 400x400 pixels:: +The :class:`openmc.SlicePlot` class indicates that a 2D slice plot should be +made. You can specify the origin of the plot (:attr:`SlicePlot.origin`), the +width of the plot in each direction (:attr:`SlicePlot.width`), the number of +pixels to use in each direction (:attr:`SlicePlot.pixels`), and the basis +directions for the plot. For example, to create a :math:`x` - :math:`z` plot +centered at (5.0, 2.0, 3.0) with a width of (50., 50.) and 400x400 pixels:: - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.basis = 'xz' plot.origin = (5.0, 2.0, 3.0) plot.width = (50., 50.) @@ -47,7 +47,7 @@ that location. By default, a unique color will be assigned to each cell in the geometry. If you want your plot to be colored by material instead, change the -:attr:`Plot.color_by` attribute:: +:attr:`SlicePlot.color_by` attribute:: plot.color_by = 'material' @@ -68,8 +68,8 @@ particular cells/materials should be given colors of your choosing:: Note that colors can be given as RGB tuples or by a string indicating a valid `SVG color `_. -When you're done creating your :class:`openmc.Plot` instances, you need to then -assign them to a :class:`openmc.Plots` collection and export it to XML:: +When you're done creating your :class:`openmc.SlicePlot` instances, you need to +then assign them to a :class:`openmc.Plots` collection and export it to XML:: plots = openmc.Plots([plot1, plot2, plot3]) plots.export_to_xml() @@ -97,13 +97,11 @@ Voxel Plots .. image:: ../_images/3dba.png :width: 200px -The :class:`openmc.Plot` class can also be told to generate a 3D voxel plot -instead of a 2D slice plot. Simply change the :attr:`Plot.type` attribute to -'voxel'. In this case, the :attr:`Plot.width` and :attr:`Plot.pixels` attributes -should be three items long, e.g.:: +The :class:`openmc.VoxelPlot` class enables the generation of a 3D voxel plot +instead of a 2D slice plot. In this case, the :attr:`VoxelPlot.width` and +:attr:`VoxelPlot.pixels` attributes should be three items long, e.g.:: - vox_plot = openmc.Plot() - vox_plot.type = 'voxel' + vox_plot = openmc.VoxelPlot() vox_plot.width = (100., 100., 50.) vox_plot.pixels = (400, 400, 200) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 138ae910c9a..12c703fd174 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1105,11 +1105,10 @@ given below: tallies.export_to_xml() # Create voxel plot - plot = openmc.Plot() + plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [2*pitch, 2*pitch, 1] plot.pixels = [1000, 1000, 1] - plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) @@ -1189,11 +1188,10 @@ given below: tallies.export_to_xml() # Create voxel plot - plot = openmc.Plot() + plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [2*pitch, 2*pitch, 1] plot.pixels = [1000, 1000, 1] - plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) diff --git a/examples/lattice/hexagonal/build_xml.py b/examples/lattice/hexagonal/build_xml.py index 9485d0aa45f..2624e52b4d8 100644 --- a/examples/lattice/hexagonal/build_xml.py +++ b/examples/lattice/hexagonal/build_xml.py @@ -128,14 +128,14 @@ # Exporting to OpenMC plots.xml file ############################################################################### -plot_xy = openmc.Plot(plot_id=1) +plot_xy = openmc.SlicePlot(plot_id=1) plot_xy.filename = 'plot_xy' plot_xy.origin = [0, 0, 0] plot_xy.width = [6, 6] plot_xy.pixels = [400, 400] plot_xy.color_by = 'material' -plot_yz = openmc.Plot(plot_id=2) +plot_yz = openmc.SlicePlot(plot_id=2) plot_yz.filename = 'plot_yz' plot_yz.basis = 'yz' plot_yz.origin = [0, 0, 0] diff --git a/examples/lattice/nested/build_xml.py b/examples/lattice/nested/build_xml.py index 2db23a46b33..a1d9c092dd9 100644 --- a/examples/lattice/nested/build_xml.py +++ b/examples/lattice/nested/build_xml.py @@ -135,7 +135,7 @@ # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot(plot_id=1) +plot = openmc.SlicePlot(plot_id=1) plot.origin = [0, 0, 0] plot.width = [4, 4] plot.pixels = [400, 400] diff --git a/examples/lattice/simple/build_xml.py b/examples/lattice/simple/build_xml.py index 56c46612169..44531edd8a1 100644 --- a/examples/lattice/simple/build_xml.py +++ b/examples/lattice/simple/build_xml.py @@ -128,7 +128,7 @@ # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot(plot_id=1) +plot = openmc.SlicePlot(plot_id=1) plot.origin = [0, 0, 0] plot.width = [4, 4] plot.pixels = [400, 400] diff --git a/examples/pincell_random_ray/build_xml.py b/examples/pincell_random_ray/build_xml.py index b3dd8020a51..5ff4c0082fc 100644 --- a/examples/pincell_random_ray/build_xml.py +++ b/examples/pincell_random_ray/build_xml.py @@ -192,11 +192,10 @@ # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot() +plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [pitch, pitch, pitch] plot.pixels = [1000, 1000, 1] -plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) diff --git a/openmc/examples.py b/openmc/examples.py index 5578d513ead..01dd9d01f97 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -83,7 +83,7 @@ def pwr_pin_cell() -> openmc.Model: constraints={'fissionable': True} ) - plot = openmc.Plot.from_geometry(model.geometry) + plot = openmc.SlicePlot.from_geometry(model.geometry) plot.pixels = (300, 300) plot.color_by = 'material' model.plots.append(plot) @@ -429,7 +429,7 @@ def pwr_core() -> openmc.Model: model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( [-160, -160, -183], [160, 160, 183])) - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = (125, 125, 0) plot.width = (250, 250) plot.pixels = (3000, 3000) @@ -544,7 +544,7 @@ def pwr_assembly() -> openmc.Model: constraints={'fissionable': True} ) - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = (0.0, 0.0, 0) plot.width = (21.42, 21.42) plot.pixels = (300, 300) diff --git a/openmc/executor.py b/openmc/executor.py index aacc48b3fac..9cd29934542 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -164,7 +164,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', path_input=None): Parameters ---------- - plots : Iterable of openmc.Plot + plots : Iterable of openmc.PlotBase Plots to display openmc_exec : str Path to OpenMC executable diff --git a/openmc/model/model.py b/openmc/model/model.py index 03fdcda1fc8..fc326f288a1 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1082,7 +1082,7 @@ def plot( self.settings.plot_seed = seed # Create plot object matching passed arguments - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = origin plot.width = width plot.pixels = pixels diff --git a/openmc/plots.py b/openmc/plots.py index d9e17609f54..9b62431c41e 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1964,16 +1964,16 @@ def from_xml_element(cls, elem): class Plots(cv.CheckedList): - """Collection of Plots used for an OpenMC simulation. + """Collection of plots used for an OpenMC simulation. This class corresponds directly to the plots.xml input file. It can be thought of as a normal Python list where each member is inherits from :class:`PlotBase`. It behaves like a list as the following example demonstrates: - >>> xz_plot = openmc.Plot() - >>> big_plot = openmc.Plot() - >>> small_plot = openmc.Plot() + >>> xz_plot = openmc.SlicePlot() + >>> big_plot = openmc.VoxelPlot() + >>> small_plot = openmc.SlicePlot() >>> p = openmc.Plots((xz_plot, big_plot)) >>> p.append(small_plot) >>> small_plot = p.pop() @@ -2009,7 +2009,7 @@ def insert(self, index, plot): ---------- index : int Index in list - plot : openmc.Plot + plot : openmc.PlotBase Plot to insert """ diff --git a/tests/regression_tests/distribmat/test.py b/tests/regression_tests/distribmat/test.py index 02f7e773e58..dd09eec36c3 100644 --- a/tests/regression_tests/distribmat/test.py +++ b/tests/regression_tests/distribmat/test.py @@ -73,7 +73,7 @@ def __init__(self, *args, **kwargs): # Plots #################### - plot1 = openmc.Plot(plot_id=1) + plot1 = openmc.SlicePlot(plot_id=1) plot1.basis = 'xy' plot1.color_by = 'cell' plot1.filename = 'cellplot' @@ -81,7 +81,7 @@ def __init__(self, *args, **kwargs): plot1.width = (7, 7) plot1.pixels = (400, 400) - plot2 = openmc.Plot(plot_id=2) + plot2 = openmc.SlicePlot(plot_id=2) plot2.basis = 'xy' plot2.color_by = 'material' plot2.filename = 'matplot' diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 6e4dec00fff..9e535b5e4f5 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -73,13 +73,13 @@ def pin_model_attributes(): tal.scores = ['flux', 'fission'] tals.append(tal) - plot1 = openmc.Plot(plot_id=1) + plot1 = openmc.SlicePlot(plot_id=1) plot1.origin = (0., 0., 0.) plot1.width = (pitch, pitch) plot1.pixels = (300, 300) plot1.color_by = 'material' plot1.filename = 'test' - plot2 = openmc.Plot(plot_id=2) + plot2 = openmc.SlicePlot(plot_id=2) plot2.origin = (0., 0., 0.) plot2.width = (pitch, pitch) plot2.pixels = (300, 300) @@ -902,6 +902,6 @@ def test_setter_from_list(): model = openmc.Model(tallies=[tally]) assert isinstance(model.tallies, openmc.Tallies) - plot = openmc.Plot() + plot = openmc.SlicePlot() model = openmc.Model(plots=[plot]) assert isinstance(model.plots, openmc.Plots) diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index 3f01985a0ca..98a93e44b54 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -9,12 +9,11 @@ @pytest.fixture(scope='module') def myplot(): - plot = openmc.Plot(name='myplot') + plot = openmc.SlicePlot(name='myplot') plot.width = (100., 100.) plot.origin = (2., 3., -10.) plot.pixels = (500, 500) plot.filename = './not-a-dir/myplot' - plot.type = 'slice' plot.basis = 'yz' plot.background = 'black' plot.background = (0, 0, 0) @@ -153,7 +152,7 @@ def test_from_geometry(): geom = openmc.Geometry(univ) for basis in ('xy', 'yz', 'xz'): - plot = openmc.Plot.from_geometry(geom, basis) + plot = openmc.SlicePlot.from_geometry(geom, basis) assert plot.origin == pytest.approx((0., 0., 0.)) assert plot.width == pytest.approx((width, width)) assert plot.basis == basis @@ -179,8 +178,8 @@ def test_xml_element(myplot): assert elem.find('pixels') is not None assert elem.find('background').text == '0 0 0' - newplot = openmc.Plot.from_xml_element(elem) - attributes = ('id', 'color_by', 'filename', 'type', 'basis', 'level', + newplot = openmc.SlicePlot.from_xml_element(elem) + attributes = ('id', 'color_by', 'filename', 'basis', 'level', 'meshlines', 'show_overlaps', 'origin', 'width', 'pixels', 'background', 'mask_background') for attr in attributes: @@ -230,8 +229,7 @@ def test_plots(run_in_tmpdir): def test_voxel_plot_roundtrip(): # Define a voxel plot and create XML element - plot = openmc.Plot(name='my voxel plot') - plot.type = 'voxel' + plot = openmc.VoxelPlot(name='my voxel plot') plot.filename = 'voxel1' plot.pixels = (50, 50, 50) plot.origin = (0., 0., 0.) @@ -243,7 +241,6 @@ def test_voxel_plot_roundtrip(): new_plot = plot.from_xml_element(elem) assert new_plot.name == plot.name assert new_plot.filename == plot.filename - assert new_plot.type == plot.type assert new_plot.pixels == plot.pixels assert new_plot.origin == plot.origin assert new_plot.width == plot.width diff --git a/tests/unit_tests/test_slice_voxel_plots.py b/tests/unit_tests/test_slice_voxel_plots.py index 7cc7e2dffc8..48ca31b7a91 100644 --- a/tests/unit_tests/test_slice_voxel_plots.py +++ b/tests/unit_tests/test_slice_voxel_plots.py @@ -4,13 +4,9 @@ classes that replace the legacy Plot class. """ import warnings -from pathlib import Path -import numpy as np import pytest - import openmc -from openmc.plots import _SVG_COLORS def test_slice_plot_initialization(): @@ -166,27 +162,6 @@ def test_voxel_plot_pixels_validation(): plot.pixels = [100] -def test_voxel_plot_no_basis(): - """Test that VoxelPlot does not have basis attribute""" - plot = openmc.VoxelPlot() - assert not hasattr(plot, 'basis') - assert not hasattr(plot, '_basis') - - -def test_voxel_plot_no_meshlines(): - """Test that VoxelPlot does not have meshlines attribute""" - plot = openmc.VoxelPlot() - assert not hasattr(plot, 'meshlines') - assert not hasattr(plot, '_meshlines') - - -def test_voxel_plot_has_to_vtk(): - """Test that VoxelPlot has to_vtk method""" - plot = openmc.VoxelPlot() - assert hasattr(plot, 'to_vtk') - assert callable(plot.to_vtk) - - def test_voxel_plot_xml_roundtrip(): """Test VoxelPlot XML serialization and deserialization""" plot = openmc.VoxelPlot(name='test_voxel') @@ -213,79 +188,50 @@ def test_plot_deprecation_warning(): """Test that Plot class raises deprecation warning""" with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") - plot = openmc.Plot() + openmc.Plot() assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) + assert issubclass(w[0].category, FutureWarning) assert "deprecated" in str(w[0].message).lower() -def test_plot_slice_compatibility(): - """Test Plot class with slice type""" +def test_plot_returns_slice_plot(): + """Test that Plot() returns a SlicePlot instance""" with warnings.catch_warnings(): warnings.simplefilter("ignore") plot = openmc.Plot() - plot.type = 'slice' - plot.width = [10.0, 20.0] - plot.pixels = [100, 200] - plot.basis = 'yz' - - assert plot.type == 'slice' - assert plot.width == [10.0, 20.0] - assert plot.pixels == [100, 200] - assert plot.basis == 'yz' + # Should be an actual SlicePlot instance + assert isinstance(plot, openmc.SlicePlot) -def test_plot_voxel_compatibility(): - """Test Plot class with voxel type""" +def test_plot_type_setter_raises_error(): + """Test that setting plot.type raises a helpful error""" with warnings.catch_warnings(): warnings.simplefilter("ignore") plot = openmc.Plot() - plot.type = 'voxel' - plot.width = [10.0, 20.0, 30.0] - plot.pixels = [100, 200, 300] + with pytest.raises(TypeError, match="no longer supported"): + plot.type = 'voxel' - assert plot.type == 'voxel' - assert plot.width == [10.0, 20.0, 30.0] - assert plot.pixels == [100, 200, 300] + with pytest.raises(TypeError, match="no longer supported"): + plot.type = 'slice' -def test_plot_xml_roundtrip_slice(): - """Test XML roundtrip for Plot with slice type""" +def test_plot_type_getter_warns(): + """Test that getting plot.type raises a deprecation warning""" with warnings.catch_warnings(): warnings.simplefilter("ignore") - plot = openmc.Plot(name='legacy_slice') - plot.type = 'slice' - plot.width = [5.0, 10.0] - plot.pixels = [50, 100] - plot.basis = 'xz' - - elem = plot.to_xml_element() - new_plot = openmc.Plot.from_xml_element(elem) - - assert new_plot.type == plot.type - assert new_plot.width == pytest.approx(plot.width) - assert new_plot.pixels == tuple(plot.pixels) - assert new_plot.basis == plot.basis - - -def test_plot_xml_roundtrip_voxel(): - """Test XML roundtrip for Plot with voxel type""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - plot = openmc.Plot(name='legacy_voxel') - plot.type = 'voxel' - plot.width = [5.0, 10.0, 15.0] - plot.pixels = [50, 100, 150] + plot = openmc.Plot() - elem = plot.to_xml_element() - new_plot = openmc.Plot.from_xml_element(elem) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + plot_type = plot.type - assert new_plot.type == plot.type - assert new_plot.width == pytest.approx(plot.width) - assert new_plot.pixels == tuple(plot.pixels) + assert plot_type == 'slice' + assert len(w) == 1 + assert issubclass(w[0].category, FutureWarning) + assert "deprecated" in str(w[0].message).lower() def test_plots_collection_mixed_types(): From 49b3bd19dd2e44a663c32cfc606f3e1edff602ee Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 5 Dec 2025 10:59:13 -0600 Subject: [PATCH 11/11] Fix conflicting mcpl_hdr_add_data_fpt definitions --- src/mcpl_interface.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 35d39566ab9..256f3343fc6 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -63,15 +63,13 @@ using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* file_handle, - const char* key, int32_t ldata, const char* data); + const char* key, uint32_t datalength, const char* data); using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); using mcpl_hdr_set_srcname_fpt = void (*)( mcpl_outfile_t* outfile_handle, const char* srcname); using mcpl_add_particle_fpt = void (*)( mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle); using mcpl_close_outfile_fpt = void (*)(mcpl_outfile_t* outfile_handle); -using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* outfile_handle, - const char* key, uint32_t datalength, const char* data); using mcpl_hdr_add_stat_sum_fpt = void (*)( mcpl_outfile_t* outfile_handle, const char* key, double value); @@ -118,7 +116,6 @@ struct McplApi { mcpl_hdr_add_data_fpt hdr_add_data; mcpl_add_particle_fpt add_particle; mcpl_close_outfile_fpt close_outfile; - mcpl_hdr_add_data_fpt hdr_add_data; mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum; explicit McplApi(LibraryHandleType lib_handle) @@ -153,8 +150,6 @@ struct McplApi { load_symbol_platform("mcpl_create_outfile")); hdr_set_srcname = reinterpret_cast( load_symbol_platform("mcpl_hdr_set_srcname")); - hdr_add_data = reinterpret_cast( - load_symbol_platform("mcpl_hdr_add_data")); add_particle = reinterpret_cast( load_symbol_platform("mcpl_add_particle")); close_outfile = reinterpret_cast(