Introduce SlicePlot and VoxelPlot to replace the Plot class (#3528)

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
Boris Polania 2025-12-05 12:17:09 -08:00 committed by GitHub
parent f28139250a
commit 9b675adda5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 618 additions and 112 deletions

View file

@ -176,7 +176,8 @@ Geometry Plotting
:nosignatures:
:template: myclass.rst
openmc.Plot
openmc.SlicePlot
openmc.VoxelPlot
openmc.WireframeRayTracePlot
openmc.SolidRayTracePlot
openmc.Plots

View file

@ -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
<usersguide_run_modes>`. The geometry plotting mode relies on the presence of a
:ref:`plots.xml <io_plots>` 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 <usersguide_run_modes>`. The geometry plotting
mode relies on the presence of a :ref:`plots.xml <io_plots>` 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 <https://www.w3.org/TR/SVG11/types.html#ColorKeywords>`_.
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)

View file

@ -1131,11 +1131,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])
@ -1215,11 +1214,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])

View file

@ -128,14 +128,14 @@ settings_file.export_to_xml()
# 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]

View file

@ -135,7 +135,7 @@ settings_file.export_to_xml()
# 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]

View file

@ -128,7 +128,7 @@ settings_file.export_to_xml()
# 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]

View file

@ -192,11 +192,10 @@ tallies.export_to_xml()
# 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])

View file

@ -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)

View file

@ -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

View file

@ -1170,7 +1170,7 @@ class Model:
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

View file

@ -2,6 +2,7 @@ from collections.abc import Iterable, Mapping
from numbers import Integral, Real
from pathlib import Path
from textwrap import dedent
import warnings
import h5py
import lxml.etree as ET
@ -626,14 +627,15 @@ class PlotBase(IDManagerMixin):
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 <https://www.w3.org/TR/SVG11/types.html#ColorKeywords>`_.
.. versionadded:: 0.15.4
Parameters
----------
plot_id : int
@ -648,7 +650,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'}
@ -671,11 +673,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
@ -688,10 +688,37 @@ class Plot(PlotBase):
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
@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
@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
@ -699,7 +726,7 @@ class Plot(PlotBase):
@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, 2)
self._width = width
@property
@ -712,15 +739,6 @@ class Plot(PlotBase):
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
@ -763,11 +781,10 @@ class Plot(PlotBase):
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)
@ -883,7 +900,7 @@ class Plot(PlotBase):
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
-------
@ -893,10 +910,8 @@ class Plot(PlotBase):
"""
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))
@ -942,8 +957,8 @@ class Plot(PlotBase):
Returns
-------
openmc.Plot
Plot object
openmc.SlicePlot
SlicePlot object
"""
plot_id = int(get_text(elem, "id"))
@ -952,9 +967,7 @@ class Plot(PlotBase):
if "filename" in elem.keys():
plot.filename = get_text(elem, "filename")
plot.color_by = get_text(elem, "color_by")
plot.type = get_text(elem, "type")
if plot.type == 'slice':
plot.basis = get_text(elem, "basis")
plot.basis = get_text(elem, "basis")
plot.origin = tuple(get_elem_list(elem, "origin", float))
plot.width = tuple(get_elem_list(elem, "width", float))
@ -1036,9 +1049,215 @@ class Plot(PlotBase):
# 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 <https://www.w3.org/TR/SVG11/types.html#ColorKeywords>`_.
.. 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.]
self._pixels = [400, 400, 400]
@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, 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, 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(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.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
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.
@ -1059,10 +1278,6 @@ class Plot(PlotBase):
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)
@ -1082,6 +1297,20 @@ class Plot(PlotBase):
return voxel_to_vtk(h5_voxel_file, output)
def Plot(plot_id=None, name=''):
"""Legacy Plot class for backward compatibility.
.. deprecated:: 0.15.4
Use :class:`SlicePlot` for 2D slice plots or :class:`VoxelPlot` for 3D voxel plots.
"""
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):
"""Definition of a camera's view of OpenMC geometry
@ -1737,16 +1966,16 @@ class SolidRayTracePlot(RayTracePlot):
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()
@ -1782,7 +2011,7 @@ class Plots(cv.CheckedList):
----------
index : int
Index in list
plot : openmc.Plot
plot : openmc.PlotBase
Plot to insert
"""
@ -1903,8 +2132,13 @@ class Plots(cv.CheckedList):
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

View file

@ -63,7 +63,7 @@ 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);
@ -150,13 +150,20 @@ struct McplApi {
load_symbol_platform("mcpl_create_outfile"));
hdr_set_srcname = reinterpret_cast<mcpl_hdr_set_srcname_fpt>(
load_symbol_platform("mcpl_hdr_set_srcname"));
hdr_add_data = reinterpret_cast<mcpl_hdr_add_data_fpt>(
load_symbol_platform("mcpl_hdr_add_data"));
add_particle = reinterpret_cast<mcpl_add_particle_fpt>(
load_symbol_platform("mcpl_add_particle"));
close_outfile = reinterpret_cast<mcpl_close_outfile_fpt>(
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<mcpl_hdr_add_data_fpt>(
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 {

View file

@ -73,7 +73,7 @@ class DistribmatTestHarness(PyAPITestHarness):
# 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 @@ class DistribmatTestHarness(PyAPITestHarness):
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'

View file

@ -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)
@ -911,7 +911,7 @@ 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)

View file

@ -9,12 +9,11 @@ from openmc.plots import _SVG_COLORS
@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)
@ -80,8 +79,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 +95,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')
@ -153,14 +152,14 @@ 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
def test_highlight_domains():
plot = openmc.Plot()
plot = openmc.SlicePlot()
plot.color_by = 'material'
plots = openmc.Plots([plot])
@ -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:
@ -200,11 +199,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 +212,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
@ -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
@ -288,10 +285,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.)

View file

@ -0,0 +1,273 @@
"""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
import pytest
import openmc
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_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")
openmc.Plot()
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)
assert "deprecated" in str(w[0].message).lower()
def test_plot_returns_slice_plot():
"""Test that Plot() returns a SlicePlot instance"""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
plot = openmc.Plot()
# Should be an actual SlicePlot instance
assert isinstance(plot, openmc.SlicePlot)
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()
with pytest.raises(TypeError, match="no longer supported"):
plot.type = 'voxel'
with pytest.raises(TypeError, match="no longer supported"):
plot.type = 'slice'
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()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
plot_type = plot.type
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():
"""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])