mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 06:25:30 -04:00
Some checks are pending
Tests and Coverage / filter-changes (push) Waiting to run
Tests and Coverage / Python 3.13 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.14 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.14t (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=n, mpi=n, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=n, mpi=y, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=y, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=, event=y (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=y, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=, libmesh=y, event= (push) Blocked by required conditions
Tests and Coverage / coverage (push) Blocked by required conditions
Tests and Coverage / Check CI status (push) Blocked by required conditions
dockerhub-publish-develop / main (push) Waiting to run
dockerhub-publish-develop-dagmc-libmesh / main (push) Waiting to run
dockerhub-publish-develop-dagmc / main (push) Waiting to run
dockerhub-publish-develop-libmesh / main (push) Waiting to run
Co-authored-by: Jonathan Shimwell <jon@proximafusion.com> Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
2244 lines
76 KiB
Python
2244 lines
76 KiB
Python
from collections.abc import Iterable, Mapping, Sequence
|
|
from numbers import Integral, Real
|
|
from pathlib import Path
|
|
from textwrap import dedent
|
|
import warnings
|
|
|
|
import h5py
|
|
import lxml.etree as ET
|
|
import numpy as np
|
|
|
|
import openmc
|
|
import openmc.checkvalue as cv
|
|
from openmc.checkvalue import PathLike
|
|
|
|
from ._xml import clean_indentation, get_elem_list, get_text
|
|
from .mixin import IDManagerMixin
|
|
|
|
_BASES = {'xy', 'xz', 'yz'}
|
|
|
|
_BASIS_INDICES = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)}
|
|
|
|
_SVG_COLORS = {
|
|
'aliceblue': (240, 248, 255),
|
|
'antiquewhite': (250, 235, 215),
|
|
'aqua': (0, 255, 255),
|
|
'aquamarine': (127, 255, 212),
|
|
'azure': (240, 255, 255),
|
|
'beige': (245, 245, 220),
|
|
'bisque': (255, 228, 196),
|
|
'black': (0, 0, 0),
|
|
'blanchedalmond': (255, 235, 205),
|
|
'blue': (0, 0, 255),
|
|
'blueviolet': (138, 43, 226),
|
|
'brown': (165, 42, 42),
|
|
'burlywood': (222, 184, 135),
|
|
'cadetblue': (95, 158, 160),
|
|
'chartreuse': (127, 255, 0),
|
|
'chocolate': (210, 105, 30),
|
|
'coral': (255, 127, 80),
|
|
'cornflowerblue': (100, 149, 237),
|
|
'cornsilk': (255, 248, 220),
|
|
'crimson': (220, 20, 60),
|
|
'cyan': (0, 255, 255),
|
|
'darkblue': (0, 0, 139),
|
|
'darkcyan': (0, 139, 139),
|
|
'darkgoldenrod': (184, 134, 11),
|
|
'darkgray': (169, 169, 169),
|
|
'darkgreen': (0, 100, 0),
|
|
'darkgrey': (169, 169, 169),
|
|
'darkkhaki': (189, 183, 107),
|
|
'darkmagenta': (139, 0, 139),
|
|
'darkolivegreen': (85, 107, 47),
|
|
'darkorange': (255, 140, 0),
|
|
'darkorchid': (153, 50, 204),
|
|
'darkred': (139, 0, 0),
|
|
'darksalmon': (233, 150, 122),
|
|
'darkseagreen': (143, 188, 143),
|
|
'darkslateblue': (72, 61, 139),
|
|
'darkslategray': (47, 79, 79),
|
|
'darkslategrey': (47, 79, 79),
|
|
'darkturquoise': (0, 206, 209),
|
|
'darkviolet': (148, 0, 211),
|
|
'deeppink': (255, 20, 147),
|
|
'deepskyblue': (0, 191, 255),
|
|
'dimgray': (105, 105, 105),
|
|
'dimgrey': (105, 105, 105),
|
|
'dodgerblue': (30, 144, 255),
|
|
'firebrick': (178, 34, 34),
|
|
'floralwhite': (255, 250, 240),
|
|
'forestgreen': (34, 139, 34),
|
|
'fuchsia': (255, 0, 255),
|
|
'gainsboro': (220, 220, 220),
|
|
'ghostwhite': (248, 248, 255),
|
|
'gold': (255, 215, 0),
|
|
'goldenrod': (218, 165, 32),
|
|
'gray': (128, 128, 128),
|
|
'green': (0, 128, 0),
|
|
'greenyellow': (173, 255, 47),
|
|
'grey': (128, 128, 128),
|
|
'honeydew': (240, 255, 240),
|
|
'hotpink': (255, 105, 180),
|
|
'indianred': (205, 92, 92),
|
|
'indigo': (75, 0, 130),
|
|
'ivory': (255, 255, 240),
|
|
'khaki': (240, 230, 140),
|
|
'lavender': (230, 230, 250),
|
|
'lavenderblush': (255, 240, 245),
|
|
'lawngreen': (124, 252, 0),
|
|
'lemonchiffon': (255, 250, 205),
|
|
'lightblue': (173, 216, 230),
|
|
'lightcoral': (240, 128, 128),
|
|
'lightcyan': (224, 255, 255),
|
|
'lightgoldenrodyellow': (250, 250, 210),
|
|
'lightgray': (211, 211, 211),
|
|
'lightgreen': (144, 238, 144),
|
|
'lightgrey': (211, 211, 211),
|
|
'lightpink': (255, 182, 193),
|
|
'lightsalmon': (255, 160, 122),
|
|
'lightseagreen': (32, 178, 170),
|
|
'lightskyblue': (135, 206, 250),
|
|
'lightslategray': (119, 136, 153),
|
|
'lightslategrey': (119, 136, 153),
|
|
'lightsteelblue': (176, 196, 222),
|
|
'lightyellow': (255, 255, 224),
|
|
'lime': (0, 255, 0),
|
|
'limegreen': (50, 205, 50),
|
|
'linen': (250, 240, 230),
|
|
'magenta': (255, 0, 255),
|
|
'maroon': (128, 0, 0),
|
|
'mediumaquamarine': (102, 205, 170),
|
|
'mediumblue': (0, 0, 205),
|
|
'mediumorchid': (186, 85, 211),
|
|
'mediumpurple': (147, 112, 219),
|
|
'mediumseagreen': (60, 179, 113),
|
|
'mediumslateblue': (123, 104, 238),
|
|
'mediumspringgreen': (0, 250, 154),
|
|
'mediumturquoise': (72, 209, 204),
|
|
'mediumvioletred': (199, 21, 133),
|
|
'midnightblue': (25, 25, 112),
|
|
'mintcream': (245, 255, 250),
|
|
'mistyrose': (255, 228, 225),
|
|
'moccasin': (255, 228, 181),
|
|
'navajowhite': (255, 222, 173),
|
|
'navy': (0, 0, 128),
|
|
'oldlace': (253, 245, 230),
|
|
'olive': (128, 128, 0),
|
|
'olivedrab': (107, 142, 35),
|
|
'orange': (255, 165, 0),
|
|
'orangered': (255, 69, 0),
|
|
'orchid': (218, 112, 214),
|
|
'palegoldenrod': (238, 232, 170),
|
|
'palegreen': (152, 251, 152),
|
|
'paleturquoise': (175, 238, 238),
|
|
'palevioletred': (219, 112, 147),
|
|
'papayawhip': (255, 239, 213),
|
|
'peachpuff': (255, 218, 185),
|
|
'peru': (205, 133, 63),
|
|
'pink': (255, 192, 203),
|
|
'plum': (221, 160, 221),
|
|
'powderblue': (176, 224, 230),
|
|
'purple': (128, 0, 128),
|
|
'red': (255, 0, 0),
|
|
'rosybrown': (188, 143, 143),
|
|
'royalblue': (65, 105, 225),
|
|
'saddlebrown': (139, 69, 19),
|
|
'salmon': (250, 128, 114),
|
|
'sandybrown': (244, 164, 96),
|
|
'seagreen': (46, 139, 87),
|
|
'seashell': (255, 245, 238),
|
|
'sienna': (160, 82, 45),
|
|
'silver': (192, 192, 192),
|
|
'skyblue': (135, 206, 235),
|
|
'slateblue': (106, 90, 205),
|
|
'slategray': (112, 128, 144),
|
|
'slategrey': (112, 128, 144),
|
|
'snow': (255, 250, 250),
|
|
'springgreen': (0, 255, 127),
|
|
'steelblue': (70, 130, 180),
|
|
'tan': (210, 180, 140),
|
|
'teal': (0, 128, 128),
|
|
'thistle': (216, 191, 216),
|
|
'tomato': (255, 99, 71),
|
|
'turquoise': (64, 224, 208),
|
|
'violet': (238, 130, 238),
|
|
'wheat': (245, 222, 179),
|
|
'white': (255, 255, 255),
|
|
'whitesmoke': (245, 245, 245),
|
|
'yellow': (255, 255, 0),
|
|
'yellowgreen': (154, 205, 50)
|
|
}
|
|
|
|
_PLOT_PARAMS = dedent("""\
|
|
|
|
Parameters
|
|
----------
|
|
origin : iterable of float
|
|
Coordinates at the origin of the plot. If left as None,
|
|
the center of the bounding box will be used to attempt to ascertain
|
|
the origin with infinite values being replaced by 0.
|
|
width : iterable of float
|
|
Width of the plot in each basis direction. If left as none then the
|
|
width of the bounding box will be used to attempt to
|
|
ascertain the plot width. Defaults to (10, 10) if the bounding box
|
|
contains inf values.
|
|
pixels : Iterable of int or int
|
|
If an iterable of ints is provided then this directly sets the
|
|
number of pixels to use in each basis direction. If a single int
|
|
is provided then this sets the total number of pixels in the plot
|
|
and the number of pixels in each basis direction is calculated
|
|
from this total and the image aspect ratio based on the width
|
|
argument.
|
|
basis : {'xy', 'xz', 'yz'}
|
|
The basis directions for the plot
|
|
color_by : {'cell', 'material'}
|
|
Indicate whether the plot should be colored by cell or by material
|
|
colors : dict
|
|
Assigns colors to specific materials or cells. Keys are instances of
|
|
:class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA
|
|
4-tuples, or strings indicating SVG color names. Red, green, blue,
|
|
and alpha should all be integers in the range [0, 255], for example:
|
|
|
|
.. code-block:: python
|
|
|
|
# Make water blue
|
|
water = openmc.Cell(fill=h2o)
|
|
universe.plot(..., colors={water: (0, 0, 255))
|
|
seed : int
|
|
Seed for the random number generator
|
|
openmc_exec : str
|
|
Path to OpenMC executable.
|
|
axes : matplotlib.Axes
|
|
Axes to draw to
|
|
|
|
.. versionadded:: 0.13.1
|
|
legend : bool
|
|
Whether a legend showing material or cell names should be drawn
|
|
|
|
.. versionadded:: 0.14.0
|
|
axis_units : {'km', 'm', 'cm', 'mm'}
|
|
Units used on the plot axis
|
|
|
|
.. versionadded:: 0.14.0
|
|
outline : bool or str
|
|
Whether outlines between color boundaries should be drawn. If set to
|
|
'only', only outlines will be drawn.
|
|
|
|
.. versionadded:: 0.14.0
|
|
show_overlaps: bool
|
|
Indicate whether or not overlapping regions are shown.
|
|
Default is False.
|
|
overlap_color: Iterable of int or str
|
|
Color to apply to overlapping regions. Default is red.
|
|
n_samples : int, optional
|
|
The number of source particles to sample and add to plot. Defaults
|
|
to None which doesn't plot any particles on the plot.
|
|
plane_tolerance: float
|
|
When plotting a plane the source locations within the plane +/-
|
|
the plane_tolerance will be included and those outside of the
|
|
plane_tolerance will not be shown
|
|
legend_kwargs : dict
|
|
Keyword arguments passed to :func:`matplotlib.pyplot.legend`.
|
|
|
|
.. versionadded:: 0.14.0
|
|
source_kwargs : dict, optional
|
|
Keyword arguments passed to :func:`matplotlib.pyplot.scatter`.
|
|
contour_kwargs : dict, optional
|
|
Keyword arguments passed to :func:`matplotlib.pyplot.contour`.
|
|
**kwargs
|
|
Keyword arguments passed to :func:`matplotlib.pyplot.imshow`.
|
|
|
|
Returns
|
|
-------
|
|
matplotlib.axes.Axes
|
|
Axes containing resulting image
|
|
""")
|
|
|
|
|
|
# Decorator for consistently adding plot parameters to docstrings (Model.plot,
|
|
# Geometry.plot, Universe.plot, etc.)
|
|
def add_plot_params(func):
|
|
func.__doc__ += _PLOT_PARAMS
|
|
return func
|
|
|
|
|
|
def _get_plot_image(plot, cwd):
|
|
from IPython.display import Image
|
|
|
|
# Make sure .png file was created
|
|
png_filename = plot.filename if plot.filename is not None else f'plot_{plot.id}'
|
|
|
|
# Add file extension if not already present. The C++ code added it
|
|
# automatically if it wasn't present.
|
|
if Path(png_filename).suffix != ".png":
|
|
png_filename += ".png"
|
|
|
|
png_file = Path(cwd) / png_filename
|
|
if not png_file.exists():
|
|
raise FileNotFoundError(
|
|
f"Could not find .png image for plot {plot.id}. Your version of "
|
|
"OpenMC may not be built against libpng.")
|
|
|
|
return Image(str(png_file))
|
|
|
|
|
|
def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'):
|
|
"""Converts a voxel HDF5 file to a VTK file
|
|
|
|
.. versionadded:: 0.14.0
|
|
|
|
Parameters
|
|
----------
|
|
voxel_file : path-like
|
|
Path of the input h5 to convert
|
|
output : path-like
|
|
Path of the output vti file produced
|
|
|
|
Returns
|
|
-------
|
|
Path
|
|
Path of the .vti file produced
|
|
"""
|
|
|
|
# imported vtk only if used as vtk is an option dependency
|
|
import vtk
|
|
|
|
_min_version = (2, 0)
|
|
|
|
# Read data from voxel file
|
|
with h5py.File(voxel_file, "r") as fh:
|
|
# check version
|
|
version = tuple(fh.attrs["version"])
|
|
if version < _min_version:
|
|
old_version = ".".join(map(str, version))
|
|
min_version = ".".join(map(str, _min_version))
|
|
err_msg = (
|
|
f"This voxel file's version is {old_version}. This function only "
|
|
f" supports voxel files with version {min_version} or higher. "
|
|
"Please generate a new voxel file using a newer version of OpenMC."
|
|
)
|
|
raise ValueError(err_msg)
|
|
|
|
dimension = fh.attrs["num_voxels"]
|
|
width = fh.attrs["voxel_width"]
|
|
lower_left = fh.attrs["lower_left"]
|
|
|
|
nx, ny, nz = dimension
|
|
|
|
grid = vtk.vtkImageData()
|
|
grid.SetDimensions(nx + 1, ny + 1, nz + 1)
|
|
grid.SetOrigin(*lower_left)
|
|
grid.SetSpacing(*width)
|
|
|
|
# transpose data from OpenMC ordering (zyx) to VTK ordering (xyz)
|
|
# and flatten to 1-D array
|
|
h5data = fh["data"][...]
|
|
|
|
data = vtk.vtkIntArray()
|
|
data.SetName("id")
|
|
# set the array using the h5data array
|
|
data.SetArray(h5data, h5data.size, True)
|
|
# add data to image grid
|
|
grid.GetCellData().AddArray(data)
|
|
|
|
writer = vtk.vtkXMLImageDataWriter()
|
|
if vtk.vtkVersion.GetVTKMajorVersion() > 5:
|
|
writer.SetInputData(grid)
|
|
else:
|
|
writer.SetInput(grid)
|
|
output = str(output)
|
|
if not output.endswith(".vti"):
|
|
output += ".vti"
|
|
writer.SetFileName(output)
|
|
writer.Write()
|
|
|
|
return output
|
|
|
|
|
|
def id_map_to_rgb(
|
|
id_map: np.ndarray,
|
|
color_by: str = 'cell',
|
|
colors: dict | None = None,
|
|
overlap_color: Sequence[int] | str = (255, 0, 0)
|
|
) -> np.ndarray:
|
|
"""Convert ID map array to RGB image array.
|
|
|
|
Parameters
|
|
----------
|
|
id_map : numpy.ndarray
|
|
Array with shape (v_pixels, h_pixels, 3) containing cell IDs,
|
|
cell instances, and material IDs
|
|
color_by : {'cell', 'material'}
|
|
Whether to color by cell or material
|
|
colors : dict, optional
|
|
Dictionary mapping cells/materials to colors
|
|
overlap_color : sequence of int or str, optional
|
|
Color to use for overlaps. Defaults to red (255, 0, 0).
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray
|
|
RGB image array with shape (v_pixels, h_pixels, 3) with values
|
|
in range [0, 1] for matplotlib
|
|
"""
|
|
# Initialize RGB array with white background (values between 0 and 1 for matplotlib)
|
|
img = np.ones(id_map.shape, dtype=float)
|
|
|
|
# Get the appropriate index based on color_by
|
|
if color_by == 'cell':
|
|
id_index = 0 # Cell IDs are in the first channel
|
|
elif color_by == 'material':
|
|
id_index = 2 # Material IDs are in the third channel
|
|
else:
|
|
raise ValueError("color_by must be either 'cell' or 'material'")
|
|
|
|
# Get all unique IDs in the plot
|
|
unique_ids = np.unique(id_map[:, :, id_index])
|
|
|
|
# Generate default colors if not provided
|
|
if colors is None:
|
|
colors = {}
|
|
|
|
# Convert colors dict to use IDs as keys
|
|
color_map = {}
|
|
for key, color in colors.items():
|
|
if isinstance(key, (openmc.Cell, openmc.Material)):
|
|
color_map[key.id] = color
|
|
else:
|
|
color_map[key] = color
|
|
|
|
# Generate random colors for IDs not in color_map
|
|
rng = np.random.RandomState(1)
|
|
for uid in unique_ids:
|
|
if uid > 0 and uid not in color_map:
|
|
color_map[uid] = rng.randint(0, 256, (3,))
|
|
|
|
# Apply colors to each pixel
|
|
for uid in unique_ids:
|
|
if uid == -1: # Background/void
|
|
continue
|
|
elif uid == -3: # Overlap (only present if color_overlaps was True)
|
|
if isinstance(overlap_color, str):
|
|
rgb = _SVG_COLORS[overlap_color.lower()]
|
|
else:
|
|
rgb = overlap_color
|
|
mask = id_map[:, :, id_index] == uid
|
|
img[mask] = np.array(rgb) / 255.0
|
|
elif uid in color_map:
|
|
color = color_map[uid]
|
|
if isinstance(color, str):
|
|
rgb = _SVG_COLORS[color.lower()]
|
|
else:
|
|
rgb = color
|
|
mask = id_map[:, :, id_index] == uid
|
|
img[mask] = np.array(rgb) / 255.0
|
|
|
|
return img
|
|
|
|
class PlotBase(IDManagerMixin):
|
|
"""
|
|
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
|
|
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
|
|
"""
|
|
|
|
next_id = 1
|
|
used_ids = set()
|
|
|
|
def __init__(self, plot_id=None, name=''):
|
|
# Initialize Plot class attributes
|
|
self.id = plot_id
|
|
self.name = name
|
|
self._pixels = [400, 400]
|
|
self._filename = None
|
|
self._color_by = 'cell'
|
|
self._background = None
|
|
self._mask_components = None
|
|
self._mask_background = None
|
|
self._show_overlaps = False
|
|
self._overlap_color = None
|
|
self._colors = {}
|
|
self._level = None
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, name):
|
|
cv.check_type('plot name', name, str)
|
|
self._name = name
|
|
|
|
@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
|
|
|
|
@property
|
|
def filename(self):
|
|
return self._filename
|
|
|
|
@filename.setter
|
|
def filename(self, filename):
|
|
cv.check_type('filename', filename, (str, PathLike))
|
|
self._filename = filename
|
|
|
|
@property
|
|
def color_by(self):
|
|
return self._color_by
|
|
|
|
@color_by.setter
|
|
def color_by(self, color_by):
|
|
cv.check_value('plot color_by', color_by, ['cell', 'material'])
|
|
self._color_by = color_by
|
|
|
|
@property
|
|
def background(self):
|
|
return self._background
|
|
|
|
@background.setter
|
|
def background(self, background):
|
|
self._check_color('plot background', background)
|
|
self._background = background
|
|
|
|
@property
|
|
def mask_components(self):
|
|
return self._mask_components
|
|
|
|
@mask_components.setter
|
|
def mask_components(self, mask_components):
|
|
cv.check_type('plot mask components', mask_components, Iterable,
|
|
(openmc.Cell, openmc.Material, Integral))
|
|
self._mask_components = mask_components
|
|
|
|
@property
|
|
def mask_background(self):
|
|
return self._mask_background
|
|
|
|
@mask_background.setter
|
|
def mask_background(self, mask_background):
|
|
self._check_color('plot mask background', mask_background)
|
|
self._mask_background = mask_background
|
|
|
|
@property
|
|
def show_overlaps(self):
|
|
return self._show_overlaps
|
|
|
|
@show_overlaps.setter
|
|
def show_overlaps(self, show_overlaps):
|
|
cv.check_type(f'Show overlaps flag for Plot ID="{self.id}"',
|
|
show_overlaps, bool)
|
|
self._show_overlaps = show_overlaps
|
|
|
|
@property
|
|
def overlap_color(self):
|
|
return self._overlap_color
|
|
|
|
@overlap_color.setter
|
|
def overlap_color(self, overlap_color):
|
|
self._check_color('plot overlap color', overlap_color)
|
|
self._overlap_color = overlap_color
|
|
|
|
@property
|
|
def colors(self):
|
|
return self._colors
|
|
|
|
@colors.setter
|
|
def colors(self, colors):
|
|
cv.check_type('plot colors', colors, Mapping)
|
|
for key, value in colors.items():
|
|
cv.check_type('plot color key', key,
|
|
(openmc.Cell, openmc.Material, Integral))
|
|
self._check_color('plot color value', value)
|
|
self._colors = colors
|
|
|
|
@property
|
|
def level(self):
|
|
return self._level
|
|
|
|
@level.setter
|
|
def level(self, plot_level):
|
|
cv.check_type('plot level', plot_level, Integral)
|
|
cv.check_greater_than('plot level', plot_level, 0, equality=True)
|
|
self._level = plot_level
|
|
|
|
@staticmethod
|
|
def _check_color(err_string, color):
|
|
cv.check_type(err_string, color, Iterable)
|
|
if isinstance(color, str):
|
|
if color.lower() not in _SVG_COLORS:
|
|
raise ValueError(f"'{color}' is not a valid color.")
|
|
else:
|
|
cv.check_length(err_string, color, 3)
|
|
for rgb in color:
|
|
cv.check_type(err_string, rgb, Real)
|
|
cv.check_greater_than('RGB component', rgb, 0, True)
|
|
cv.check_less_than('RGB component', rgb, 256)
|
|
|
|
# Helper function that returns the domain ID given either a
|
|
# Cell/Material object or the domain ID itself
|
|
@staticmethod
|
|
def _get_id(domain):
|
|
return domain if isinstance(domain, Integral) else domain.id
|
|
|
|
def colorize(self, geometry, seed=1):
|
|
"""Generate a color scheme for each domain in the plot.
|
|
|
|
This routine may be used to generate random, reproducible color schemes.
|
|
The colors generated are based upon cell/material IDs in the geometry.
|
|
|
|
Parameters
|
|
----------
|
|
geometry : openmc.Geometry
|
|
The geometry for which the plot is defined
|
|
seed : Integral
|
|
The random number seed used to generate the color scheme
|
|
|
|
"""
|
|
|
|
cv.check_type('geometry', geometry, openmc.Geometry)
|
|
cv.check_type('seed', seed, Integral)
|
|
cv.check_greater_than('seed', seed, 1, equality=True)
|
|
|
|
# Get collections of the domains which will be plotted
|
|
if self.color_by == 'material':
|
|
domains = geometry.get_all_materials().values()
|
|
else:
|
|
domains = geometry.get_all_cells().values()
|
|
|
|
rng = np.random.RandomState(seed)
|
|
|
|
# Generate random colors for each feature
|
|
for domain in domains:
|
|
self.colors[domain] = rng.randint(0, 256, (3,))
|
|
|
|
def _colors_to_xml(self, element):
|
|
for domain, color in sorted(self._colors.items(),
|
|
key=lambda x: self._get_id(x[0])):
|
|
subelement = ET.SubElement(element, "color")
|
|
subelement.set("id", str(self._get_id(domain)))
|
|
if isinstance(color, str):
|
|
color = _SVG_COLORS[color.lower()]
|
|
subelement.set("rgb", ' '.join(str(x) for x in color))
|
|
|
|
def to_xml_element(self):
|
|
"""Save common plot attributes to XML element
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing plot data
|
|
|
|
"""
|
|
|
|
element = ET.Element("plot")
|
|
element.set("id", str(self._id))
|
|
if len(self._name) > 0:
|
|
element.set("name", str(self.name))
|
|
if self._filename is not None:
|
|
element.set("filename", self._filename)
|
|
element.set("color_by", self._color_by)
|
|
|
|
subelement = ET.SubElement(element, "pixels")
|
|
subelement.text = ' '.join(map(str, self._pixels))
|
|
|
|
if self._background is not None:
|
|
subelement = ET.SubElement(element, "background")
|
|
color = self._background
|
|
if isinstance(color, str):
|
|
color = _SVG_COLORS[color.lower()]
|
|
subelement.text = ' '.join(str(x) for x in color)
|
|
|
|
if self._mask_components is not None:
|
|
subelement = ET.SubElement(element, "mask")
|
|
subelement.set("components", ' '.join(
|
|
str(PlotBase._get_id(d)) for d in self._mask_components))
|
|
color = self._mask_background
|
|
if color is not None:
|
|
if isinstance(color, str):
|
|
color = _SVG_COLORS[color.lower()]
|
|
subelement.set("background", ' '.join(
|
|
str(x) for x in color))
|
|
|
|
if self._level is not None:
|
|
subelement = ET.SubElement(element, "level")
|
|
subelement.text = str(self._level)
|
|
|
|
return element
|
|
|
|
|
|
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.
|
|
(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
|
|
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 (2 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 basis direction (2 values)
|
|
origin : tuple or list of ndarray
|
|
Origin (center) of the plot (3 values)
|
|
basis : {'xy', 'xz', 'yz'}
|
|
The basis directions for the plot
|
|
meshlines : dict
|
|
Dictionary defining type, id, linewidth and color of a mesh to be
|
|
plotted on top of a plot
|
|
|
|
"""
|
|
|
|
def __init__(self, plot_id=None, name=''):
|
|
super().__init__(plot_id, name)
|
|
self._width = [4.0, 4.0]
|
|
self._origin = [0., 0., 0.]
|
|
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
|
|
|
|
@width.setter
|
|
def width(self, width):
|
|
cv.check_type('plot width', width, Iterable, Real)
|
|
cv.check_length('plot width', width, 2, 2)
|
|
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
|
|
|
|
@property
|
|
def basis(self):
|
|
return self._basis
|
|
|
|
@basis.setter
|
|
def basis(self, basis):
|
|
cv.check_value('plot basis', basis, _BASES)
|
|
self._basis = basis
|
|
|
|
@property
|
|
def meshlines(self):
|
|
return self._meshlines
|
|
|
|
@meshlines.setter
|
|
def meshlines(self, meshlines):
|
|
cv.check_type('plot meshlines', meshlines, dict)
|
|
if 'type' not in meshlines:
|
|
msg = f'Unable to set the meshlines to "{meshlines}" which ' \
|
|
'does not have a "type" key'
|
|
raise ValueError(msg)
|
|
|
|
elif meshlines['type'] not in ['tally', 'entropy', 'ufs', 'cmfd']:
|
|
msg = f"Unable to set the meshlines with type \"{meshlines['type']}\""
|
|
raise ValueError(msg)
|
|
|
|
if 'id' in meshlines:
|
|
cv.check_type('plot meshlines id', meshlines['id'], Integral)
|
|
cv.check_greater_than('plot meshlines id', meshlines['id'], 0,
|
|
equality=True)
|
|
|
|
if 'linewidth' in meshlines:
|
|
cv.check_type('plot mesh linewidth',
|
|
meshlines['linewidth'], Integral)
|
|
cv.check_greater_than('plot mesh linewidth', meshlines['linewidth'],
|
|
0, equality=True)
|
|
|
|
if 'color' in meshlines:
|
|
self._check_color('plot meshlines color', meshlines['color'])
|
|
|
|
self._meshlines = meshlines
|
|
|
|
def __repr__(self):
|
|
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('\tBasis', self._basis)
|
|
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)
|
|
string += '{: <16}=\t{}\n'.format('\tMeshlines', self._meshlines)
|
|
return string
|
|
|
|
@classmethod
|
|
def from_geometry(cls, geometry, basis='xy', slice_coord=0.):
|
|
"""Return plot that encompasses a geometry.
|
|
|
|
Parameters
|
|
----------
|
|
geometry : openmc.Geometry
|
|
The geometry to base the plot off of
|
|
basis : {'xy', 'xz', 'yz'}
|
|
The basis directions for the plot
|
|
slice_coord : float
|
|
The level at which the slice plot should be plotted. For example, if
|
|
the basis is 'xy', this would indicate the z value used in the
|
|
origin.
|
|
|
|
"""
|
|
cv.check_type('geometry', geometry, openmc.Geometry)
|
|
cv.check_value('basis', basis, _BASES)
|
|
|
|
# Decide which axes to keep
|
|
if basis == 'xy':
|
|
pick_index = (0, 1)
|
|
slice_index = 2
|
|
elif basis == 'yz':
|
|
pick_index = (1, 2)
|
|
slice_index = 0
|
|
elif basis == 'xz':
|
|
pick_index = (0, 2)
|
|
slice_index = 1
|
|
|
|
# Get lower-left and upper-right coordinates for desired axes
|
|
lower_left, upper_right = geometry.bounding_box
|
|
lower_left = lower_left[np.array(pick_index)]
|
|
upper_right = upper_right[np.array(pick_index)]
|
|
|
|
if np.any(np.isinf((lower_left, upper_right))):
|
|
raise ValueError('The geometry does not appear to be bounded '
|
|
f'in the {basis} plane.')
|
|
|
|
plot = cls()
|
|
plot.origin = np.insert((lower_left + upper_right)/2,
|
|
slice_index, slice_coord)
|
|
plot.width = upper_right - lower_left
|
|
plot.basis = basis
|
|
return plot
|
|
|
|
def highlight_domains(self, geometry, domains, seed=1,
|
|
alpha=0.5, background='gray'):
|
|
"""Use alpha compositing to highlight one or more domains in the plot.
|
|
|
|
This routine generates a color scheme and applies alpha compositing to
|
|
make all domains except the highlighted ones appear partially
|
|
transparent.
|
|
|
|
Parameters
|
|
----------
|
|
geometry : openmc.Geometry
|
|
The geometry for which the plot is defined
|
|
domains : Iterable of openmc.Cell or openmc.Material
|
|
A collection of the domain IDs to highlight in the plot
|
|
seed : int
|
|
The random number seed used to generate the color scheme
|
|
alpha : float
|
|
The value between 0 and 1 to apply in alpha compositing
|
|
background : 3-tuple of int or str
|
|
The background color to apply in alpha compositing
|
|
|
|
"""
|
|
|
|
cv.check_type('domains', domains, Iterable,
|
|
(openmc.Cell, openmc.Material))
|
|
cv.check_type('alpha', alpha, Real)
|
|
cv.check_greater_than('alpha', alpha, 0., equality=True)
|
|
cv.check_less_than('alpha', alpha, 1., equality=True)
|
|
cv.check_type('background', background, Iterable)
|
|
|
|
# Get a background (R,G,B) tuple to apply in alpha compositing
|
|
if isinstance(background, str):
|
|
if background.lower() not in _SVG_COLORS:
|
|
raise ValueError(f"'{background}' is not a valid color.")
|
|
background = _SVG_COLORS[background.lower()]
|
|
|
|
# Generate a color scheme
|
|
self.colorize(geometry, seed)
|
|
|
|
# Apply alpha compositing to the colors for all domains
|
|
# other than those the user wishes to highlight
|
|
for domain, color in self.colors.items():
|
|
if domain not in domains:
|
|
if isinstance(color, str):
|
|
color = _SVG_COLORS[color.lower()]
|
|
r, g, b = color
|
|
r = int(((1-alpha) * background[0]) + (alpha * r))
|
|
g = int(((1-alpha) * background[1]) + (alpha * g))
|
|
b = int(((1-alpha) * background[2]) + (alpha * b))
|
|
self._colors[domain] = (r, g, b)
|
|
|
|
def to_xml_element(self):
|
|
"""Return XML representation of the slice plot
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing plot data
|
|
|
|
"""
|
|
|
|
element = super().to_xml_element()
|
|
element.set("type", "slice")
|
|
element.set("basis", self._basis)
|
|
|
|
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)
|
|
|
|
if self._meshlines is not None:
|
|
subelement = ET.SubElement(element, "meshlines")
|
|
subelement.set("meshtype", self._meshlines['type'])
|
|
if 'id' in self._meshlines:
|
|
subelement.set("id", str(self._meshlines['id']))
|
|
if 'linewidth' in self._meshlines:
|
|
subelement.set("linewidth", str(self._meshlines['linewidth']))
|
|
if 'color' in self._meshlines:
|
|
subelement.set("color", ' '.join(map(
|
|
str, self._meshlines['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.SlicePlot
|
|
SlicePlot 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.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")}
|
|
if 'id' in mesh_elem.keys():
|
|
meshlines['id'] = int(get_text(mesh_elem, "id"))
|
|
if 'linewidth' in mesh_elem.keys():
|
|
meshlines['linewidth'] = int(get_text(mesh_elem, "linewidth"))
|
|
if 'color' in mesh_elem.keys():
|
|
meshlines['color'] = tuple(get_elem_list(mesh_elem, "color", int))
|
|
plot.meshlines = meshlines
|
|
|
|
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)
|
|
|
|
|
|
|
|
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 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
|
|
|
|
"""
|
|
# Create plots.xml
|
|
Plots([self]).export_to_xml(cwd)
|
|
|
|
# Run OpenMC in geometry plotting mode and produces a h5 file
|
|
openmc.plot_geometry(False, openmc_exec, cwd)
|
|
|
|
h5_voxel_filename = self.filename if self.filename is not None else f'plot_{self.id}'
|
|
|
|
# Add file extension if not already present
|
|
if Path(h5_voxel_filename).suffix != ".h5":
|
|
h5_voxel_filename += ".h5"
|
|
|
|
h5_voxel_file = Path(cwd) / h5_voxel_filename
|
|
if output is None:
|
|
output = h5_voxel_file.with_suffix('.vti')
|
|
|
|
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
|
|
|
|
The camera projection may either by orthographic or perspective. Perspective
|
|
projections are more similar to a pinhole camera, and orthographic
|
|
projections preserve parallel lines and distances.
|
|
|
|
This is an abstract base class that :class:`WireframeRayTracePlot` and
|
|
:class:`SolidRayTracePlot` finish the implementation of.
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
Parameters
|
|
----------
|
|
plot_id : int
|
|
Unique identifier for the plot
|
|
name : str
|
|
Name of the plot
|
|
|
|
Attributes
|
|
----------
|
|
horizontal_field_of_view : float
|
|
Field of view horizontally, in units of degrees, defaults to 70.
|
|
camera_position : tuple or list of ndarray
|
|
Position of the camera in 3D space. Defaults to (1, 0, 0).
|
|
look_at : tuple or list of ndarray
|
|
The center of the camera's image points to this place in 3D space.
|
|
Set to (0, 0, 0) by default.
|
|
up : tuple or list of ndarray
|
|
Which way is up for the camera. Must not be parallel to the
|
|
line between look_at and camera_position. Set to (0, 0, 1) by default.
|
|
orthographic_width : float
|
|
If set to a nonzero value, an orthographic projection is used.
|
|
All rays traced from the orthographic pixel array travel in the
|
|
same direction. The width of the starting array must be specified,
|
|
unlike with the default perspective projection. The height of the
|
|
array is deduced from the ratio of pixel dimensions for the image.
|
|
Defaults to zero, i.e. using perspective projection.
|
|
"""
|
|
|
|
def __init__(self, plot_id=None, name=''):
|
|
# Initialize Plot class attributes
|
|
super().__init__(plot_id, name)
|
|
self._horizontal_field_of_view = 70.0
|
|
self._camera_position = (1.0, 0.0, 0.0)
|
|
self._look_at = (0.0, 0.0, 0.0)
|
|
self._up = (0.0, 0.0, 1.0)
|
|
self._orthographic_width = 0.0
|
|
|
|
@property
|
|
def horizontal_field_of_view(self):
|
|
return self._horizontal_field_of_view
|
|
|
|
@horizontal_field_of_view.setter
|
|
def horizontal_field_of_view(self, horizontal_field_of_view):
|
|
cv.check_type('plot horizontal field of view', horizontal_field_of_view,
|
|
Real)
|
|
assert horizontal_field_of_view > 0.0
|
|
assert horizontal_field_of_view < 180.0
|
|
self._horizontal_field_of_view = horizontal_field_of_view
|
|
|
|
@property
|
|
def camera_position(self):
|
|
return self._camera_position
|
|
|
|
@camera_position.setter
|
|
def camera_position(self, camera_position):
|
|
cv.check_type('plot camera position', camera_position, Iterable, Real)
|
|
cv.check_length('plot camera position', camera_position, 3)
|
|
self._camera_position = camera_position
|
|
|
|
@property
|
|
def look_at(self):
|
|
return self._look_at
|
|
|
|
@look_at.setter
|
|
def look_at(self, look_at):
|
|
cv.check_type('plot look at', look_at, Iterable, Real)
|
|
cv.check_length('plot look at', look_at, 3)
|
|
self._look_at = look_at
|
|
|
|
@property
|
|
def up(self):
|
|
return self._up
|
|
|
|
@up.setter
|
|
def up(self, up):
|
|
cv.check_type('plot up', up, Iterable, Real)
|
|
cv.check_length('plot up', up, 3)
|
|
self._up = up
|
|
|
|
@property
|
|
def orthographic_width(self):
|
|
return self._orthographic_width
|
|
|
|
@orthographic_width.setter
|
|
def orthographic_width(self, orthographic_width):
|
|
cv.check_type('plot orthographic width', orthographic_width, Real)
|
|
assert orthographic_width >= 0.0
|
|
self._orthographic_width = orthographic_width
|
|
|
|
def _check_domains_consistent_with_color_by(self, domains):
|
|
"""Check domains are the same as the type we are coloring by"""
|
|
for region in domains:
|
|
# if an integer is passed, we have to assume it was a valid ID
|
|
if isinstance(region, int):
|
|
continue
|
|
|
|
if self._color_by == 'material':
|
|
if not isinstance(region, openmc.Material):
|
|
raise Exception('Domain list must be materials if '
|
|
'color_by=material')
|
|
else:
|
|
if not isinstance(region, openmc.Cell):
|
|
raise Exception('Domain list must be cells if '
|
|
'color_by=cell')
|
|
|
|
def to_xml_element(self):
|
|
"""Return XML representation of the ray trace plot
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing plot data
|
|
|
|
"""
|
|
|
|
element = super().to_xml_element()
|
|
element.set("id", str(self._id))
|
|
|
|
subelement = ET.SubElement(element, "camera_position")
|
|
subelement.text = ' '.join(map(str, self._camera_position))
|
|
|
|
subelement = ET.SubElement(element, "look_at")
|
|
subelement.text = ' '.join(map(str, self._look_at))
|
|
|
|
subelement = ET.SubElement(element, "horizontal_field_of_view")
|
|
subelement.text = str(self._horizontal_field_of_view)
|
|
|
|
# do not need to write if orthographic_width == 0.0
|
|
if self._orthographic_width > 0.0:
|
|
subelement = ET.SubElement(element, "orthographic_width")
|
|
subelement.text = str(self._orthographic_width)
|
|
|
|
return element
|
|
|
|
def __repr__(self):
|
|
string = ''
|
|
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('\tHorizontal FOV',
|
|
self._horizontal_field_of_view)
|
|
string += '{: <16}=\t{}\n'.format('\tOrthographic width',
|
|
self._orthographic_width)
|
|
string += '{: <16}=\t{}\n'.format('\tCamera position',
|
|
self._camera_position)
|
|
string += '{: <16}=\t{}\n'.format('\tLook at', self._look_at)
|
|
string += '{: <16}=\t{}\n'.format('\tUp', self._up)
|
|
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('\tColors', self._colors)
|
|
string += '{: <16}=\t{}\n'.format('\tLevel', self._level)
|
|
return string
|
|
|
|
def _read_xml_attributes(self, elem):
|
|
"""Helper function called by from_xml_element
|
|
of child classes. These are common vaues to be
|
|
read by any ray traced plot.
|
|
|
|
Returns
|
|
-------
|
|
None
|
|
"""
|
|
|
|
filename = get_text(elem, "filename")
|
|
if filename is not None:
|
|
self.filename = filename
|
|
self.color_by = get_text(elem, "color_by")
|
|
|
|
horizontal_fov = get_text(elem, "horizontal_field_of_view")
|
|
if horizontal_fov is not None:
|
|
self.horizontal_field_of_view = float(horizontal_fov)
|
|
|
|
orthographic_width = get_text(elem, "orthographic_width")
|
|
if orthographic_width is not None:
|
|
self.orthographic_width = float(orthographic_width)
|
|
|
|
self.pixels = tuple(get_elem_list(elem, "pixels", int))
|
|
self.camera_position = tuple(get_elem_list(elem, "camera_position", float))
|
|
self.look_at = tuple(get_elem_list(elem, "look_at", float))
|
|
|
|
background = get_elem_list(elem, "background", int)
|
|
if background is not None:
|
|
self.background = tuple(background)
|
|
|
|
# Set masking information
|
|
if (mask_elem := elem.find("mask")) is not None:
|
|
mask_components = get_elem_list(mask_elem, "components", int)
|
|
# TODO: set mask components(needs geometry information)
|
|
background = get_elem_list(mask_elem, "background", int)
|
|
if background is not None:
|
|
self.mask_background = tuple(background)
|
|
|
|
# Set universe level
|
|
level = get_text(elem, "level")
|
|
if level is not None:
|
|
self.level = int(level)
|
|
|
|
|
|
class WireframeRayTracePlot(RayTracePlot):
|
|
"""Plots wireframes of geometry with volume rendered colors
|
|
|
|
Colors are defined in the same manner as the Plot class, but with the
|
|
addition of a coloring parameter resembling a macroscopic cross section in
|
|
units of inverse centimeters. The volume rendering technique is used to
|
|
color regions of the model. An infinite cross section denotes a fully opaque
|
|
region, and zero represents a transparent region which will expose the color
|
|
of the regions behind it.
|
|
|
|
.. versionchanged:: 0.15.1
|
|
Renamed from ProjectionPlot to WireframeRayTracePlot
|
|
|
|
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
|
|
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
|
|
horizontal_field_of_view : float
|
|
Field of view horizontally, in units of degrees, defaults to 70.
|
|
camera_position : tuple or list of ndarray
|
|
Position of the camera in 3D space. Defaults to (1, 0, 0).
|
|
look_at : tuple or list of ndarray
|
|
The center of the camera's image points to this place in 3D space.
|
|
Set to (0, 0, 0) by default.
|
|
up : tuple or list of ndarray
|
|
Which way is up for the camera. Must not be parallel to the
|
|
line between look_at and camera_position. Set to (0, 0, 1) by default.
|
|
orthographic_width : float
|
|
If set to a nonzero value, an orthographic projection is used.
|
|
All rays traced from the orthographic pixel array travel in the
|
|
same direction. The width of the starting array must be specified,
|
|
unlike with the default perspective projection. The height of the
|
|
array is deduced from the ratio of pixel dimensions for the image.
|
|
Defaults to zero, i.e. using perspective projection.
|
|
wireframe_thickness : int
|
|
Line thickness employed for drawing wireframes around cells or material
|
|
regions. Can be set to zero for no wireframes at all. Defaults to one
|
|
pixel.
|
|
wireframe_color : tuple of ints
|
|
RGB color of the wireframe lines. Defaults to black.
|
|
wireframe_domains : iterable of either Material or Cells
|
|
If provided, the wireframe is only drawn around these. If color_by is by
|
|
material, it must be a list of materials, else cells.
|
|
xs : dict
|
|
A mapping from cell/material IDs to floats. The floating point values
|
|
are macroscopic cross sections influencing the volume rendering opacity
|
|
of each geometric region. Zero corresponds to perfect transparency, and
|
|
infinity equivalent to opaque. These must be set by the user, but
|
|
default values can be obtained using the :meth:`set_transparent` method.
|
|
"""
|
|
|
|
def __init__(self, plot_id=None, name=''):
|
|
super().__init__(plot_id, name)
|
|
self._wireframe_thickness = 1
|
|
self._wireframe_color = _SVG_COLORS['black']
|
|
self._wireframe_domains = []
|
|
self._xs = {}
|
|
|
|
@property
|
|
def wireframe_thickness(self):
|
|
return self._wireframe_thickness
|
|
|
|
@wireframe_thickness.setter
|
|
def wireframe_thickness(self, wireframe_thickness):
|
|
cv.check_type('plot wireframe thickness',
|
|
wireframe_thickness, Integral)
|
|
assert wireframe_thickness >= 0
|
|
self._wireframe_thickness = wireframe_thickness
|
|
|
|
@property
|
|
def wireframe_color(self):
|
|
return self._wireframe_color
|
|
|
|
@wireframe_color.setter
|
|
def wireframe_color(self, wireframe_color):
|
|
self._check_color('plot wireframe color', wireframe_color)
|
|
self._wireframe_color = wireframe_color
|
|
|
|
@property
|
|
def wireframe_domains(self):
|
|
return self._wireframe_domains
|
|
|
|
@wireframe_domains.setter
|
|
def wireframe_domains(self, wireframe_domains):
|
|
self._wireframe_domains = wireframe_domains
|
|
|
|
@property
|
|
def xs(self):
|
|
return self._xs
|
|
|
|
@xs.setter
|
|
def xs(self, xs):
|
|
cv.check_type('plot xs', xs, Mapping)
|
|
for key, value in xs.items():
|
|
cv.check_type('plot xs key', key, (openmc.Cell, openmc.Material))
|
|
cv.check_type('plot xs value', value, Real)
|
|
assert value >= 0.0
|
|
self._xs = xs
|
|
|
|
def set_transparent(self, geometry):
|
|
"""Sets all volume rendering XS to zero for the model
|
|
|
|
Parameters
|
|
----------
|
|
geometry : openmc.Geometry
|
|
The geometry for which the plot is defined
|
|
"""
|
|
|
|
cv.check_type('geometry', geometry, openmc.Geometry)
|
|
|
|
# Get collections of the domains which will be plotted
|
|
if self.color_by == 'material':
|
|
domains = geometry.get_all_materials().values()
|
|
else:
|
|
domains = geometry.get_all_cells().values()
|
|
|
|
# Generate random colors for each feature
|
|
for domain in domains:
|
|
self.xs[domain] = 0.0
|
|
|
|
def __repr__(self):
|
|
string = 'Wireframe Ray-traced Plot\n'
|
|
string += super().__repr__()
|
|
string += '{: <16}=\t{}\n'.format('\tWireframe thickness',
|
|
self._wireframe_thickness)
|
|
string += '{: <16}=\t{}\n'.format('\tWireframe color',
|
|
self._wireframe_color)
|
|
string += '{: <16}=\t{}\n'.format('\tWireframe domains',
|
|
self._wireframe_domains)
|
|
string += '{: <16}=\t{}\n'.format('\tTransparencies', self._xs)
|
|
return string
|
|
|
|
def to_xml_element(self):
|
|
"""Return XML representation of the projection plot
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing plot data
|
|
|
|
"""
|
|
element = super().to_xml_element()
|
|
element.set("type", "wireframe_raytrace")
|
|
|
|
subelement = ET.SubElement(element, "wireframe_thickness")
|
|
subelement.text = str(self._wireframe_thickness)
|
|
|
|
subelement = ET.SubElement(element, "wireframe_color")
|
|
color = self._wireframe_color
|
|
if isinstance(color, str):
|
|
color = _SVG_COLORS[color.lower()]
|
|
subelement.text = ' '.join(str(x) for x in color)
|
|
|
|
self._check_domains_consistent_with_color_by(self.wireframe_domains)
|
|
|
|
if self._wireframe_domains:
|
|
id_list = [x.id for x in self._wireframe_domains]
|
|
subelement = ET.SubElement(element, "wireframe_ids")
|
|
subelement.text = ' '.join([str(x) for x in id_list])
|
|
|
|
# note that this differs from the slice plot colors
|
|
# in that "xs" must also be specified
|
|
if self._colors:
|
|
for domain, color in sorted(self._colors.items(),
|
|
key=lambda x: x[0].id):
|
|
subelement = ET.SubElement(element, "color")
|
|
subelement.set("id", str(domain.id))
|
|
if isinstance(color, str):
|
|
color = _SVG_COLORS[color.lower()]
|
|
subelement.set("rgb", ' '.join(str(x) for x in color))
|
|
subelement.set("xs", str(self._xs[domain]))
|
|
|
|
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.WireframeRayTracePlot
|
|
WireframeRayTracePlot object
|
|
|
|
"""
|
|
|
|
plot_id = int(get_text(elem, "id"))
|
|
plot_name = get_text(elem, 'name', '')
|
|
plot = cls(plot_id, plot_name)
|
|
plot.type = "wireframe_raytrace"
|
|
|
|
plot._read_xml_attributes(elem)
|
|
|
|
# Attempt to get wireframe thickness.May not be present
|
|
wireframe_thickness = get_text(elem, "wireframe_thickness")
|
|
if wireframe_thickness is not None:
|
|
plot.wireframe_thickness = int(wireframe_thickness)
|
|
wireframe_color = get_elem_list(elem, "wireframe_color", int)
|
|
if wireframe_color:
|
|
plot.wireframe_color = wireframe_color
|
|
|
|
# Set plot colors
|
|
for color_elem in elem.findall("color"):
|
|
uid = int(get_text(color_elem, "id"))
|
|
plot.colors[uid] = tuple(get_elem_list(color_elem, "rgb", int))
|
|
plot.xs[uid] = float(get_text(color_elem, "xs"))
|
|
|
|
return plot
|
|
|
|
|
|
class SolidRayTracePlot(RayTracePlot):
|
|
"""Phong shading-based rendering of an OpenMC geometry
|
|
|
|
This class defines a plot that uses Phong shading to enhance the
|
|
visualization of an OpenMC geometry by incorporating diffuse lighting and
|
|
configurable opacity for certain regions. It extends :class:`RayTracePlot`
|
|
by adding parameters related to lighting and transparency.
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
Parameters
|
|
----------
|
|
plot_id : int, optional
|
|
Unique identifier for the plot
|
|
name : str, optional
|
|
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
|
|
filename : str
|
|
Path to write the plot to
|
|
color_by : {'cell', 'material'}
|
|
Indicate whether the plot should be colored by cell or by material
|
|
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).
|
|
horizontal_field_of_view : float
|
|
Field of view horizontally, in units of degrees, defaults to 70.
|
|
camera_position : tuple or list of ndarray
|
|
Position of the camera in 3D space. Defaults to (1, 0, 0).
|
|
look_at : tuple or list of ndarray
|
|
The center of the camera's image points to this place in 3D space.
|
|
Set to (0, 0, 0) by default.
|
|
up : tuple or list of ndarray
|
|
Which way is up for the camera. Must not be parallel to the
|
|
line between look_at and camera_position. Set to (0, 0, 1) by default.
|
|
orthographic_width : float
|
|
If set to a nonzero value, an orthographic projection is used.
|
|
All rays traced from the orthographic pixel array travel in the
|
|
same direction. The width of the starting array must be specified,
|
|
unlike with the default perspective projection. The height of the
|
|
array is deduced from the ratio of pixel dimensions for the image.
|
|
Defaults to zero, i.e. using perspective projection.
|
|
light_position : tuple or list of float
|
|
Position of the light source in 3D space. Defaults to None, which places
|
|
the light at the camera position.
|
|
diffuse_fraction : float
|
|
Fraction of lighting that is diffuse (non-directional). Defaults to 0.1.
|
|
Must be between 0 and 1.
|
|
opaque_domains : list
|
|
List of domains (e.g., cells or materials) that should be rendered as
|
|
opaque rather than allowing transparency.
|
|
"""
|
|
|
|
def __init__(self, plot_id=None, name=''):
|
|
super().__init__(plot_id, name)
|
|
self._light_position = None
|
|
self._diffuse_fraction = 0.1
|
|
self._opaque_domains = []
|
|
|
|
@property
|
|
def light_position(self):
|
|
return self._light_position
|
|
|
|
@light_position.setter
|
|
def light_position(self, x):
|
|
cv.check_type('plot light position', x, Iterable, Real)
|
|
cv.check_length('plot light position', x, 3)
|
|
self._light_position = x
|
|
|
|
@property
|
|
def diffuse_fraction(self):
|
|
return self._diffuse_fraction
|
|
|
|
@diffuse_fraction.setter
|
|
def diffuse_fraction(self, x):
|
|
cv.check_type('diffuse fraction', x, Real)
|
|
cv.check_greater_than('diffuse fraction', x, 0.0, equality=True)
|
|
cv.check_less_than('diffuse fraction', x, 1.0, equality=True)
|
|
self._diffuse_fraction = x
|
|
|
|
@property
|
|
def opaque_domains(self):
|
|
return self._opaque_domains
|
|
|
|
@opaque_domains.setter
|
|
def opaque_domains(self, x):
|
|
# Note that _check_domains_consistent_with_color_by checks
|
|
# the types within later. This is because we don't necessarily
|
|
# know what types are acceptable until the user has set the
|
|
# color_by attribute, too.
|
|
cv.check_type('opaque domains', x, Iterable)
|
|
self._opaque_domains = x
|
|
|
|
def __repr__(self):
|
|
string = 'Solid Ray-traced Plot\n'
|
|
string += super().__repr__()
|
|
string += '{: <16}=\t{}\n'.format('\tDiffuse Fraction',
|
|
self._diffuse_fraction)
|
|
string += '{: <16}=\t{}\n'.format('\tLight position',
|
|
self._light_position)
|
|
string += '{: <16}=\t{}\n'.format('\tOpaque domains',
|
|
self._opaque_domains)
|
|
return string
|
|
|
|
def to_xml_element(self):
|
|
"""Return XML representation of the solid ray-traced plot
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing plot data
|
|
|
|
"""
|
|
element = super().to_xml_element()
|
|
element.set("type", "solid_raytrace")
|
|
|
|
# no light position means put it at the camera
|
|
if self._light_position:
|
|
subelement = ET.SubElement(element, "light_position")
|
|
subelement.text = ' '.join(map(str, self._light_position))
|
|
|
|
# no diffuse fraction defaults to 0.1
|
|
if self._diffuse_fraction:
|
|
subelement = ET.SubElement(element, "diffuse_fraction")
|
|
subelement.text = str(self._diffuse_fraction)
|
|
|
|
self._check_domains_consistent_with_color_by(self.opaque_domains)
|
|
subelement = ET.SubElement(element, "opaque_ids")
|
|
|
|
# Extract all IDs, or use the integer value passed in
|
|
# explicitly if that was given
|
|
subelement.text = ' '.join(
|
|
[str(domain) if isinstance(domain, int) else
|
|
str(domain.id) for domain in self._opaque_domains])
|
|
|
|
if self._colors:
|
|
self._colors_to_xml(element)
|
|
|
|
return element
|
|
|
|
def _read_phong_attributes(self, elem):
|
|
"""Read attributes specific to the Phong plot from an XML element"""
|
|
light_position = get_elem_list(elem, 'light_position', float)
|
|
if light_position is not None:
|
|
self.light_position = tuple(light_position)
|
|
|
|
diffuse_fraction = get_text(elem, "diffuse_fraction")
|
|
if diffuse_fraction is not None:
|
|
self.diffuse_fraction = float(diffuse_fraction)
|
|
|
|
opaque_domains = get_elem_list(elem, 'opaque_ids', int)
|
|
if opaque_domains is not None:
|
|
self.opaque_domains = opaque_domains
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem):
|
|
"""Generate plot object from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.WireframeRayTracePlot
|
|
WireframeRayTracePlot object
|
|
|
|
"""
|
|
|
|
plot_id = int(get_text(elem, "id"))
|
|
plot_name = get_text(elem, 'name', '')
|
|
plot = cls(plot_id, plot_name)
|
|
plot.type = "solid_raytrace"
|
|
|
|
plot._read_xml_attributes(elem)
|
|
plot._read_phong_attributes(elem)
|
|
|
|
# Set plot colors
|
|
for color_elem in elem.findall("color"):
|
|
uid = get_text(color_elem, "id")
|
|
plot.colors[uid] = tuple(get_elem_list(color_elem, "rgb", int))
|
|
|
|
return plot
|
|
|
|
|
|
class Plots(cv.CheckedList):
|
|
"""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.SlicePlot()
|
|
>>> big_plot = openmc.VoxelPlot()
|
|
>>> small_plot = openmc.SlicePlot()
|
|
>>> p = openmc.Plots((xz_plot, big_plot))
|
|
>>> p.append(small_plot)
|
|
>>> small_plot = p.pop()
|
|
|
|
Parameters
|
|
----------
|
|
plots : Iterable of openmc.PlotBase
|
|
plots to add to the collection
|
|
|
|
"""
|
|
|
|
def __init__(self, plots=None):
|
|
super().__init__(PlotBase, 'plots collection')
|
|
self._plots_file = ET.Element("plots")
|
|
if plots is not None:
|
|
self += plots
|
|
|
|
def append(self, plot):
|
|
"""Append plot to collection
|
|
|
|
Parameters
|
|
----------
|
|
plot : openmc.PlotBase
|
|
Plot to append
|
|
|
|
"""
|
|
super().append(plot)
|
|
|
|
def insert(self, index, plot):
|
|
"""Insert plot before index
|
|
|
|
Parameters
|
|
----------
|
|
index : int
|
|
Index in list
|
|
plot : openmc.PlotBase
|
|
Plot to insert
|
|
|
|
"""
|
|
super().insert(index, plot)
|
|
|
|
def colorize(self, geometry, seed=1):
|
|
"""Generate a consistent color scheme for each domain in each plot.
|
|
|
|
This routine may be used to generate random, reproducible color schemes.
|
|
The colors generated are based upon cell/material IDs in the geometry.
|
|
The color schemes will be consistent for all plots in "plots.xml".
|
|
|
|
Parameters
|
|
----------
|
|
geometry : openmc.Geometry
|
|
The geometry for which the plots are defined
|
|
seed : Integral
|
|
The random number seed used to generate the color scheme
|
|
|
|
"""
|
|
|
|
for plot in self:
|
|
plot.colorize(geometry, seed)
|
|
|
|
def highlight_domains(self, geometry, domains, seed=1,
|
|
alpha=0.5, background='gray'):
|
|
"""Use alpha compositing to highlight one or more domains in the plot.
|
|
|
|
This routine generates a color scheme and applies alpha compositing to
|
|
make all domains except the highlighted ones appear partially
|
|
transparent.
|
|
|
|
Parameters
|
|
----------
|
|
geometry : openmc.Geometry
|
|
The geometry for which the plot is defined
|
|
domains : Iterable of openmc.Cell or openmc.Material
|
|
A collection of the domain IDs to highlight in the plot
|
|
seed : int
|
|
The random number seed used to generate the color scheme
|
|
alpha : float
|
|
The value between 0 and 1 to apply in alpha compositing
|
|
background : 3-tuple of int or str
|
|
The background color to apply in alpha compositing
|
|
|
|
"""
|
|
|
|
for plot in self:
|
|
plot.highlight_domains(geometry, domains, seed, alpha, background)
|
|
|
|
def _create_plot_subelements(self):
|
|
for plot in self:
|
|
xml_element = plot.to_xml_element()
|
|
|
|
if len(plot.name) > 0:
|
|
self._plots_file.append(ET.Comment(plot.name))
|
|
|
|
self._plots_file.append(xml_element)
|
|
|
|
def to_xml_element(self):
|
|
"""Create a 'plots' element to be written to an XML file.
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing all plot elements
|
|
|
|
"""
|
|
# Reset xml element tree
|
|
self._plots_file.clear()
|
|
|
|
self._create_plot_subelements()
|
|
|
|
# Clean the indentation in the file to be user-readable
|
|
clean_indentation(self._plots_file)
|
|
|
|
return self._plots_file
|
|
|
|
def export_to_xml(self, path='plots.xml'):
|
|
"""Export plot specifications to an XML file.
|
|
|
|
Parameters
|
|
----------
|
|
path : str
|
|
Path to file to write. Defaults to 'plots.xml'.
|
|
|
|
"""
|
|
# Check if path is a directory
|
|
p = Path(path)
|
|
if p.is_dir():
|
|
p /= 'plots.xml'
|
|
|
|
self.to_xml_element()
|
|
# Write the XML Tree to the plots.xml file
|
|
tree = ET.ElementTree(self._plots_file)
|
|
tree.write(str(p), xml_declaration=True, encoding='utf-8')
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem):
|
|
"""Generate plots collection from XML file
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.Plots
|
|
Plots collection
|
|
|
|
"""
|
|
# Generate each plot
|
|
plots = cls()
|
|
for e in elem.findall('plot'):
|
|
plot_type = get_text(e, "type")
|
|
if plot_type == 'wireframe_raytrace':
|
|
plots.append(WireframeRayTracePlot.from_xml_element(e))
|
|
elif plot_type == 'solid_raytrace':
|
|
plots.append(SolidRayTracePlot.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
|
|
|
|
@classmethod
|
|
def from_xml(cls, path='plots.xml'):
|
|
"""Generate plots collection from XML file
|
|
|
|
Parameters
|
|
----------
|
|
path : str, optional
|
|
Path to plots XML file
|
|
|
|
Returns
|
|
-------
|
|
openmc.Plots
|
|
Plots collection
|
|
|
|
"""
|
|
parser = ET.XMLParser(huge_tree=True)
|
|
tree = ET.parse(path, parser=parser)
|
|
root = tree.getroot()
|
|
return cls.from_xml_element(root)
|