Merge pull request #1926 from gridley/projection_plots

Projection plots
This commit is contained in:
Patrick Shriwise 2023-04-05 15:08:37 -05:00 committed by GitHub
commit e0d32e6a29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1871 additions and 656 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -121,3 +121,91 @@ will depend on the 3D viewer, but should be straightforward.
program (Visit, ParaView, etc.) if the number of voxels is large (>10
million or so). Thus if you want an accurate picture that renders
smoothly, consider using only one voxel in a certain direction.
----------------
Projection Plots
----------------
.. image:: ../_images/hexlat_anim.gif
:width: 200px
The :class:`openmc.ProjectionPlot` class presents an alternative method
of producing 3D visualizations of OpenMC geometries. It was developed to
overcome the primary shortcoming of voxel plots, that an enormous number
of voxels must be employed to capture detailed geometric features.
Projection plots perform volume rendering on material or
cell volumes, with colors specified in the same manner as slice plots.
This is done using the native ray tracing capabilities within OpenMC,
so any geometry in which particles successfully run without overlaps
or leaks will work with projection plots.
One drawback of projection plots is that particle tracks cannot be overlaid
on them at present. Moreover, checking for overlap regions is not currently possible with projection plots. The image heading this section can
be created by adding the following code to the hexagonal lattice example packaged
with OpenMC, before exporting to plots.xml.
::
r = 5
import numpy as np
for i in range(100):
phi = 2 * np.pi * i/100
thisp = openmc.ProjectionPlot(plot_id = 4 + i)
thisp.filename = 'frame%s'%(str(i).zfill(3))
thisp.look_at = [0, 0, 0]
thisp.camera_position = [r * np.cos(phi), r * np.sin(phi), 6 * np.sin(phi)]
thisp.pixels = [200, 200]
thisp.color_by = 'material'
thisp.colorize(geometry)
thisp.set_transparent(geometry)
thisp.xs[fuel] = 1.0
thisp.xs[iron] = 1.0
thisp.wireframe_domains = [fuel]
thisp.wireframe_thickness = 2
plot_file.append(thisp)
This generates a sequence of png files which can be joined to form a gif.
Each image specifies a different camera position using some simple periodic
functions to create a perfectly looped gif. :attr:`ProjectionPlot.look_at`
defines where the camera's centerline should point at.
:attr:`ProjectionPlot.camera_position` similarly defines where the camera
is situated in the universe level we seek to plot. The other settings
resemble those employed by :class:`openmc.Plot`, with the exception of
the :class:`ProjectionPlot.set_transparent` method and :attr:`ProjectionPlot.xs`
dictionary. These are used to control volume rendering of material
volumes. "xs" here stands for cross section, and it defines material
opacities in units of inverse centimeters. Setting this value to a
large number would make a material or cell opaque, and setting it to
zero makes a material transparent. Thus, the :class:`ProjectionPlot.set_transparent`
can be used to make all materials in the geometry transparent. From there,
individual material or cell opacities can be tuned to produce the
desired result.
Two camera projections are available when using these plots, perspective
and orthographic. The default, perspective projection,
is a cone of rays passing through each pixel which radiate from the camera
position and span the field of view in the x and y positions. The horizontal
field of view can be set with the :attr: `ProjectionPlot.horizontal_field_of_view` attribute,
which is to be specified in units of degrees. The field of view only influences
behavior in perspective projection mode.
In the orthographic projection, rays follow the same angle but originate from
different points. The horizontal width of this plane of ray starting points
may be set with the :attr: `ProjectionPlot.orthographic_width` element. If this element
is nonzero, the orthographic projection is employed. Left to its default value
of zero, the perspective projection is employed.
Lastly, projection plots come packaged with wireframe generation that
can target either all surface/cell/material boundaries in the geometry,
or only wireframing around specific regions. In the above example, we
have set only the fuel region from the hexagonal lattice example to have
a wireframe drawn around it. This is accomplished by setting the
:attr: `ProjectionPlot.wireframe_domains`, which may be set to either material
IDs or cell IDs. The :attr:`ProjectionPlot.wireframe_thickness`
attribute sets the wireframe thickness in units of pixels.
.. note:: When setting specific material or cell regions to have wireframes
drawn around them, the plot must be colored by materials if wireframing
around specific materials and similarly colored by cell instance if
wireframing around specific cells.

View file

@ -168,8 +168,7 @@ private:
// Non-member functions
//==============================================================================
int32_t next_cell(
DAGUniverse* dag_univ, DAGCell* cur_cell, DAGSurface* surf_xed);
int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ);
} // namespace openmc

View file

@ -23,12 +23,13 @@ namespace openmc {
// Global variables
//===============================================================================
class Plot;
class PlottableInterface;
namespace model {
extern std::unordered_map<int, int> plot_map; //!< map of plot ids to index
extern vector<Plot> plots; //!< Plot instance container
extern vector<std::unique_ptr<PlottableInterface>>
plots; //!< Plot instance container
extern uint64_t plotter_seed; // Stream index used by the plotter
@ -66,6 +67,50 @@ struct RGBColor {
// some default colors
const RGBColor WHITE {255, 255, 255};
const RGBColor RED {255, 0, 0};
const RGBColor BLACK {0, 0, 0};
/*
* PlottableInterface classes just have to have a unique ID in the plots.xml
* file, and guarantee being able to create output in some way.
*/
class PlottableInterface {
private:
void set_id(pugi::xml_node plot_node);
int id_; // unique plot ID
void set_bg_color(pugi::xml_node plot_node);
void set_universe(pugi::xml_node plot_node);
void set_default_colors(pugi::xml_node plot_node);
void set_user_colors(pugi::xml_node plot_node);
void set_overlap_color(pugi::xml_node plot_node);
void set_mask(pugi::xml_node plot_node);
protected:
// Plot output filename, derived classes have logic to set it
std::string path_plot_;
public:
enum class PlotColorBy { cells = 0, mats = 1 };
// Creates the output image named path_plot_
virtual void create_output() const = 0;
// Print useful info to the terminal
virtual void print_info() const = 0;
const std::string& path_plot() const { return path_plot_; }
const int id() const { return id_; }
const int level() const { return level_; }
// Public color-related data
PlottableInterface(pugi::xml_node plot_node);
int level_; // Universe level to plot
bool color_overlaps_; // Show overlapping cells?
PlotColorBy color_by_; // Plot coloring (cell/material)
RGBColor not_found_ {WHITE}; // Plot background color
RGBColor overlap_color_ {RED}; // Plot overlap color
vector<RGBColor> colors_; // Plot colors
};
typedef xt::xtensor<RGBColor, 2> ImageData;
@ -93,32 +138,30 @@ struct PropertyData {
xt::xtensor<double, 3> data_; //!< 2D array of temperature & density data
};
enum class PlotType { slice = 1, voxel = 2 };
enum class PlotBasis { xy = 1, xz = 2, yz = 3 };
enum class PlotColorBy { cells = 0, mats = 1 };
//===============================================================================
// Plot class
//===============================================================================
class PlotBase {
class SlicePlotBase {
public:
template<class T>
T get_map() const;
enum class PlotBasis { xy = 1, xz = 2, yz = 3 };
// Members
public:
Position origin_; //!< Plot origin in geometry
Position width_; //!< Plot width in geometry
PlotBasis basis_; //!< Plot basis (XY/XZ/YZ)
array<size_t, 3> pixels_; //!< Plot size in pixels
bool color_overlaps_; //!< Show overlapping cells?
int level_; //!< Plot universe level
bool slice_color_overlaps_; //!< Show overlapping cells?
int slice_level_ {-1}; //!< Plot universe level
private:
};
template<class T>
T PlotBase::get_map() const
T SlicePlotBase::get_map() const
{
size_t width = pixels_[0];
@ -164,7 +207,7 @@ T PlotBase::get_map() const
p.r() = xyz;
p.u() = dir;
p.coord(0).universe = model::root_universe;
int level = level_;
int level = slice_level_;
int j {};
#pragma omp for
@ -182,7 +225,7 @@ T PlotBase::get_map() const
if (found_cell) {
data.set_value(y, x, p, j);
}
if (color_overlaps_ && check_cell_overlap(p, false)) {
if (slice_color_overlaps_ && check_cell_overlap(p, false)) {
data.set_overlap(y, x);
}
} // inner for
@ -192,61 +235,129 @@ T PlotBase::get_map() const
return data;
}
class Plot : public PlotBase {
// Represents either a voxel or pixel plot
class Plot : public PlottableInterface, public SlicePlotBase {
public:
// Constructor
Plot(pugi::xml_node plot);
enum class PlotType { slice = 1, voxel = 2 };
Plot(pugi::xml_node plot, PlotType type);
// Methods
private:
void set_id(pugi::xml_node plot_node);
void set_type(pugi::xml_node plot_node);
void set_output_path(pugi::xml_node plot_node);
void set_bg_color(pugi::xml_node plot_node);
void set_basis(pugi::xml_node plot_node);
void set_origin(pugi::xml_node plot_node);
void set_width(pugi::xml_node plot_node);
void set_universe(pugi::xml_node plot_node);
void set_default_colors(pugi::xml_node plot_node);
void set_user_colors(pugi::xml_node plot_node);
void set_meshlines(pugi::xml_node plot_node);
void set_mask(pugi::xml_node plot_node);
void set_overlap_color(pugi::xml_node plot_node);
// Members
public:
int id_; //!< Plot ID
// Add mesh lines to ImageData
void draw_mesh_lines(ImageData& data) const;
void create_image() const;
void create_voxel() const;
virtual void create_output() const;
virtual void print_info() const;
PlotType type_; //!< Plot type (Slice/Voxel)
PlotColorBy color_by_; //!< Plot coloring (cell/material)
int meshlines_width_; //!< Width of lines added to the plot
int index_meshlines_mesh_ {-1}; //!< Index of the mesh to draw on the plot
RGBColor meshlines_color_; //!< Color of meshlines on the plot
RGBColor not_found_ {WHITE}; //!< Plot background color
RGBColor overlap_color_ {RED}; //!< Plot overlap color
vector<RGBColor> colors_; //!< Plot colors
std::string path_plot_; //!< Plot output filename
};
class ProjectionPlot : public PlottableInterface {
public:
ProjectionPlot(pugi::xml_node plot);
virtual void create_output() const;
virtual void print_info() const;
private:
void set_output_path(pugi::xml_node plot_node);
void set_look_at(pugi::xml_node node);
void set_camera_position(pugi::xml_node node);
void set_field_of_view(pugi::xml_node node);
void set_pixels(pugi::xml_node node);
void set_opacities(pugi::xml_node node);
void set_orthographic_width(pugi::xml_node node);
void set_wireframe_thickness(pugi::xml_node node);
void set_wireframe_ids(pugi::xml_node node);
void set_wireframe_color(pugi::xml_node node);
/* If starting the particle from outside the geometry, we have to
* find a distance to the boundary in a non-standard surface intersection
* check. It's an exhaustive search over surfaces in the top-level universe.
*/
static int advance_to_boundary_from_void(Particle& p);
/* Checks if a vector of two TrackSegments is equivalent. We define this
* to mean not having matching intersection lengths, but rather having
* a matching sequence of surface/cell/material intersections.
*/
struct TrackSegment;
bool trackstack_equivalent(const vector<TrackSegment>& track1,
const vector<TrackSegment>& track2) const;
/* Used for drawing wireframe and colors. We record the list of
* surface/cell/material intersections and the corresponding lengths as a ray
* traverses the geometry, then color by iterating in reverse.
*/
struct TrackSegment {
int id; // material or cell ID (which is being colored)
double length; // length of this track intersection
/* Recording this allows us to draw edges on the wireframe. For instance
* if two surfaces bound a single cell, it allows drawing that sharp edge
* where the surfaces intersect.
*/
int surface; // last surface ID intersected in this segment
TrackSegment(int id_a, double length_a, int surface_a)
: id(id_a), length(length_a), surface(surface_a)
{}
};
// Max intersections before we assume ray tracing is caught in an infinite
// loop:
static const int MAX_INTERSECTIONS = 1000000;
std::array<int, 2> pixels_; // pixel dimension of resulting image
double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees
Position camera_position_; // where camera is
Position look_at_; // point camera is centered looking at
Direction up_ {0.0, 0.0, 1.0}; // which way is up
// which color IDs should be wireframed. If empty, all cells are wireframed.
vector<int> wireframe_ids_;
/* The horizontal thickness, if using an orthographic projection.
* If set to zero, we assume using a perspective projection.
*/
double orthographic_width_ {0.0};
// Thickness of the wireframe lines. Can set to zero for no wireframe.
int wireframe_thickness_ {1};
RGBColor wireframe_color_ {BLACK}; // wireframe color
vector<double> xs_; // macro cross section values for cell volume rendering
};
//===============================================================================
// Non-member functions
//===============================================================================
//! Add mesh lines to image data of a plot object
//! \param[in] plot object
//! \param[out] image data associated with the plot object
void draw_mesh_lines(Plot const& pl, ImageData& data);
//! Write a PPM image using a plot object's image data
//! \param[in] plot object
//! \param[out] image data associated with the plot object
void output_ppm(Plot const& pl, const ImageData& data);
/* Write a PPM image
* filename - name of output file
* data - image data to write
*/
void output_ppm(const std::string& filename, const ImageData& data);
#ifdef USE_LIBPNG
//! Write a PNG image using a plot object's image data
//! \param[in] plot object
//! \param[out] image data associated with the plot object
void output_png(Plot const& pl, const ImageData& data);
/* Write a PNG image
* filename - name of output file
* data - image data to write
*/
void output_png(const std::string& filename, const ImageData& data);
#endif
//! Initialize a voxel file
@ -286,14 +397,6 @@ void read_plots_xml(pugi::xml_node root);
//! Clear memory
void free_memory_plot();
//! Create an image for a plot object
//! \param[in] plot object
void create_image(Plot const& pl);
//! Create an hdf5 voxel file for a plot object
//! \param[in] plot object
void create_voxel(Plot const& pl);
//! Create a randomly generated RGB color
//! \return RGBColor with random value
RGBColor random_color();

View file

@ -83,6 +83,11 @@ struct Position {
return x * other.x + y * other.y + z * other.z;
}
inline double norm() const { return std::sqrt(x * x + y * y + z * z); }
inline Position cross(Position other) const
{
return {y * other.z - z * other.y, z * other.x - x * other.z,
x * other.y - y * other.x};
}
//! Reflect a direction across a normal vector
//! \param[in] other Vector to reflect across

View file

@ -79,3 +79,25 @@ def reorder_attributes(root):
attribs = sorted(attrib.items())
attrib.clear()
attrib.update(attribs)
def get_elem_tuple(elem, name, dtype=int):
'''Helper function to get a tuple of values from an elem
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element that should contain a tuple
name : str
Name of the subelement to obtain tuple from
dtype : data-type
The type of each element in the tuple
Returns
-------
tuple of dtype
Data read from the tuple
'''
subelem = elem.find(name)
if subelem is not None:
return tuple([dtype(x) for x in subelem.text.split()])

View file

@ -7,7 +7,7 @@ import numpy as np
import openmc
import openmc.checkvalue as cv
from ._xml import clean_indentation, reorder_attributes
from ._xml import clean_indentation, reorder_attributes, get_elem_tuple
from .mixin import IDManagerMixin
@ -178,14 +178,8 @@ def _get_plot_image(plot, cwd):
return Image(str(png_file))
class Plot(IDManagerMixin):
"""Definition of a finite region of space to be plotted.
OpenMC is capable of generating two-dimensional slice plots and
three-dimensional voxel 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
valid `SVG color <https://www.w3.org/TR/SVG11/types.html#ColorKeywords>`_.
class PlotBase(IDManagerMixin):
"""
Parameters
----------
plot_id : int
@ -199,20 +193,12 @@ class Plot(IDManagerMixin):
Unique identifier
name : str
Name of the plot
width : Iterable of float
Width of the plot in each basis direction
pixels : Iterable of int
Number of pixels to use in each basis direction
origin : tuple or list of ndarray
Origin (center) of the plot
Number of pixels to use in each direction
filename :
Path to write the plot to
color_by : {'cell', 'material'}
Indicate whether the plot should be colored by cell or by material
type : {'slice', 'voxel'}
The type of the plot
basis : {'xy', 'xz', 'yz'}
The basis directions for the plot
background : Iterable of int or str
Color of the background
mask_components : Iterable of openmc.Cell or openmc.Material or int
@ -230,10 +216,6 @@ class Plot(IDManagerMixin):
cell/material).
level : int
Universe depth to plot at
meshlines : dict
Dictionary defining type, id, linewidth and color of a mesh to be
plotted on top of a plot
"""
next_id = 1
@ -243,13 +225,9 @@ class Plot(IDManagerMixin):
# Initialize Plot class attributes
self.id = plot_id
self.name = name
self._width = [4.0, 4.0]
self._pixels = [400, 400]
self._origin = [0., 0., 0.]
self._filename = None
self._color_by = 'cell'
self._type = 'slice'
self._basis = 'xy'
self._background = None
self._mask_components = None
self._mask_background = None
@ -257,24 +235,15 @@ class Plot(IDManagerMixin):
self._overlap_color = None
self._colors = {}
self._level = None
self._meshlines = None
@property
def name(self):
return self._name
@property
def width(self):
return self._width
@property
def pixels(self):
return self._pixels
@property
def origin(self):
return self._origin
@property
def filename(self):
return self._filename
@ -283,14 +252,6 @@ class Plot(IDManagerMixin):
def color_by(self):
return self._color_by
@property
def type(self):
return self._type
@property
def basis(self):
return self._basis
@property
def background(self):
return self._background
@ -319,27 +280,11 @@ class Plot(IDManagerMixin):
def level(self):
return self._level
@property
def meshlines(self):
return self._meshlines
@name.setter
def name(self, name):
cv.check_type('plot name', name, str)
self._name = name
@width.setter
def width(self, width):
cv.check_type('plot width', width, Iterable, Real)
cv.check_length('plot width', width, 2, 3)
self._width = width
@origin.setter
def origin(self, origin):
cv.check_type('plot origin', origin, Iterable, Real)
cv.check_length('plot origin', origin, 3)
self._origin = origin
@pixels.setter
def pixels(self, pixels):
cv.check_type('plot pixels', pixels, Iterable, Integral)
@ -358,16 +303,6 @@ class Plot(IDManagerMixin):
cv.check_value('plot color_by', color_by, ['cell', 'material'])
self._color_by = color_by
@type.setter
def type(self, plottype):
cv.check_value('plot type', plottype, ['slice', 'voxel'])
self._type = plottype
@basis.setter
def basis(self, basis):
cv.check_value('plot basis', basis, _BASES)
self._basis = basis
@background.setter
def background(self, background):
self._check_color('plot background', background)
@ -410,6 +345,181 @@ class Plot(IDManagerMixin):
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()
# Set the seed for the random number generator
np.random.seed(seed)
# Generate random colors for each feature
for domain in domains:
self.colors[domain] = np.random.randint(0, 256, (3,))
def to_xml_element(self):
"""Save common plot attributes to XML element
Returns
-------
element : xml.etree.ElementTree.Element
XML element containing plot data
"""
element = ET.Element("plot")
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 Plot(PlotBase):
'''Definition of a finite region of space to be plotted.
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
valid `SVG color <https://www.w3.org/TR/SVG11/types.html#ColorKeywords>`_.
Parameters
----------
plot_id : int
Unique identifier for the plot
name : str
Name of the plot
Attributes
----------
width : Iterable of float
Width of the plot in each basis direction
origin : tuple or list of ndarray
Origin (center) of the plot
type : {'slice', 'voxel'}
The type of the plot
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._type = 'slice'
self._basis = 'xy'
self._meshlines = None
@property
def width(self):
return self._width
@property
def origin(self):
return self._origin
@property
def type(self):
return self._type
@property
def basis(self):
return self._basis
@property
def meshlines(self):
return self._meshlines
@width.setter
def width(self, width):
cv.check_type('plot width', width, Iterable, Real)
cv.check_length('plot width', width, 2, 3)
self._width = width
@origin.setter
def origin(self, origin):
cv.check_type('plot origin', origin, Iterable, Real)
cv.check_length('plot origin', origin, 3)
self._origin = origin
@type.setter
def type(self, plottype):
cv.check_value('plot type', plottype, ['slice', 'voxel', 'projection'])
self._type = plottype
@basis.setter
def basis(self, basis):
cv.check_value('plot basis', basis, _BASES)
self._basis = basis
@meshlines.setter
def meshlines(self, meshlines):
cv.check_type('plot meshlines', meshlines, dict)
@ -429,7 +539,8 @@ class Plot(IDManagerMixin):
equality=True)
if 'linewidth' in meshlines:
cv.check_type('plot mesh linewidth', meshlines['linewidth'], Integral)
cv.check_type('plot mesh linewidth',
meshlines['linewidth'], Integral)
cv.check_greater_than('plot mesh linewidth', meshlines['linewidth'],
0, equality=True)
@ -438,19 +549,6 @@ class Plot(IDManagerMixin):
self._meshlines = meshlines
@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)
def __repr__(self):
string = 'Plot\n'
string += '{: <16}=\t{}\n'.format('\tID', self._id)
@ -520,38 +618,6 @@ class Plot(IDManagerMixin):
plot.basis = basis
return plot
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()
# Set the seed for the random number generator
np.random.seed(seed)
# Generate random colors for each feature
for domain in domains:
self.colors[domain] = np.random.randint(0, 256, (3,))
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.
@ -604,7 +670,7 @@ class Plot(IDManagerMixin):
self._colors[domain] = (r, g, b)
def to_xml_element(self):
"""Return XML representation of the plot
"""Return XML representation of the slice/voxel plot
Returns
-------
@ -613,11 +679,8 @@ class Plot(IDManagerMixin):
"""
element = ET.Element("plot")
element = super().to_xml_element()
element.set("id", str(self._id))
if self._filename is not None:
element.set("filename", self._filename)
element.set("color_by", self._color_by)
element.set("type", self._type)
if self._type == 'slice':
@ -629,41 +692,15 @@ class Plot(IDManagerMixin):
subelement = ET.SubElement(element, "width")
subelement.text = ' '.join(map(str, self._width))
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)
# Helper function that returns the domain ID given either a
# Cell/Material object or the domain ID itself
def get_id(domain):
return domain if isinstance(domain, Integral) else domain.id
if self._colors:
for domain, color in sorted(self._colors.items(),
key=lambda x: get_id(x[0])):
key=lambda x: PlotBase._get_id(x[0])):
subelement = ET.SubElement(element, "color")
subelement.set("id", str(get_id(domain)))
subelement.set("id", str(PlotBase._get_id(domain)))
if isinstance(color, str):
color = _SVG_COLORS[color.lower()]
subelement.set("rgb", ' '.join(str(x) for x in color))
if self._mask_components is not None:
subelement = ET.SubElement(element, "mask")
subelement.set("components", ' '.join(
str(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._show_overlaps:
subelement = ET.SubElement(element, "show_overlaps")
subelement.text = "true"
@ -675,11 +712,6 @@ class Plot(IDManagerMixin):
subelement = ET.SubElement(element, "overlap_color")
subelement.text = ' '.join(str(x) for x in color)
if self._level is not None:
subelement = ET.SubElement(element, "level")
subelement.text = str(self._level)
if self._meshlines is not None:
subelement = ET.SubElement(element, "meshlines")
subelement.set("meshtype", self._meshlines['type'])
@ -716,37 +748,34 @@ class Plot(IDManagerMixin):
plot.type = elem.get("type")
plot.basis = elem.get("basis")
# Helper function to get a tuple of values
def get_tuple(elem, name, dtype=int):
subelem = elem.find(name)
if subelem is not None:
return tuple([dtype(x) for x in subelem.text.split()])
plot.origin = get_tuple(elem, "origin", float)
plot.width = get_tuple(elem, "width", float)
plot.pixels = get_tuple(elem, "pixels")
plot._background = get_tuple(elem, "background")
plot.origin = get_elem_tuple(elem, "origin", float)
plot.width = get_elem_tuple(elem, "width", float)
plot.pixels = get_elem_tuple(elem, "pixels")
plot._background = get_elem_tuple(elem, "background")
# Set plot colors
colors = {}
for color_elem in elem.findall("color"):
uid = int(color_elem.get("id"))
colors[uid] = tuple([int(x) for x in color_elem.get("rgb").split()])
colors[uid] = tuple([int(x)
for x in color_elem.get("rgb").split()])
plot.colors = colors
# Set masking information
mask_elem = elem.find("mask")
if mask_elem is not None:
plot.mask_components = [int(x) for x in mask_elem.get("components").split()]
plot.mask_components = [
int(x) for x in mask_elem.get("components").split()]
background = mask_elem.get("background")
if background is not None:
plot.mask_background = tuple([int(x) for x in background.split()])
plot.mask_background = tuple(
[int(x) for x in background.split()])
# show overlaps
overlap_elem = elem.find("show_overlaps")
if overlap_elem is not None:
plot.show_overlaps = (overlap_elem.text in ('true', '1'))
overlap_color = get_tuple(elem, "overlap_color")
overlap_color = get_elem_tuple(elem, "overlap_color")
if overlap_color is not None:
plot.overlap_color = overlap_color
@ -803,12 +832,357 @@ class Plot(IDManagerMixin):
return _get_plot_image(self, cwd)
class ProjectionPlot(PlotBase):
"""Definition of a camera's view of OpenMC geometry
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.
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.
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.
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 set_transparent method.
"""
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
self._wireframe_thickness = 1
self._wireframe_color = _SVG_COLORS['black']
self._wireframe_domains = []
self._xs = {}
@property
def horizontal_field_of_view(self):
return self._horizontal_field_of_view
@property
def camera_position(self):
return self._camera_position
@property
def look_at(self):
return self._look_at
@property
def up(self):
return self._up
@property
def orthographic_width(self):
return self._orthographic_width
@property
def wireframe_thickness(self):
return self._wireframe_thickness
@property
def wireframe_color(self):
return self._wireframe_color
@property
def wireframe_domains(self):
return self._wireframe_domains
@property
def xs(self):
return self._xs
@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
@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
@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
@up.setter
def up(self, up):
cv.check_type('plot up', up, Iterable, Real)
cv.check_length('plot up', up, 3)
self._up = up
@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
@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
@wireframe_color.setter
def wireframe_color(self, wireframe_color):
self._check_color('plot wireframe color', wireframe_color)
self._wireframe_color = wireframe_color
@wireframe_domains.setter
def wireframe_domains(self, wireframe_domains):
for region in wireframe_domains:
if self._color_by == 'material':
if not isinstance(region, openmc.Material):
raise Exception('Must provide a list of materials for \
wireframe_region if color_by=Material')
else:
if not isinstance(region, openmc.Cell):
raise Exception('Must provide a list of cells for \
wireframe_region if color_by=cell')
self._wireframe_domains = wireframe_domains
@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 to_xml_element(self):
"""Return XML representation of the projection plot
Returns
-------
element : xml.etree.ElementTree.Element
XML element containing plot data
"""
element = super().to_xml_element()
element.set("id", str(self._id))
element.set("type", "projection")
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, "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)
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]))
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 = 'Projection Plot\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('\tHorizontal FOV',
self._horizontal_field_of_view)
string += '{: <16}=\t{}\n'.format('\tOrthographic width',
self._orthographic_width)
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('\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('\tTransparencies', self._xs)
string += '{: <16}=\t{}\n'.format('\tLevel', self._level)
return string
@classmethod
def from_xml_element(cls, elem):
"""Generate plot object from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.ProjectionPlot
ProjectionPlot object
"""
plot_id = int(elem.get("id"))
plot = cls(plot_id)
if "filename" in elem.keys():
plot.filename = elem.get("filename")
plot.color_by = elem.get("color_by")
plot.type = "projection"
horizontal_fov = elem.find("horizontal_field_of_view")
if horizontal_fov is not None:
plot.horizontal_field_of_view = float(horizontal_fov.text)
tmp = elem.find("orthographic_width")
if tmp is not None:
self.orthographic_width = float(tmp)
plot.pixels = get_elem_tuple(elem, "pixels")
plot.camera_position = get_elem_tuple(elem, "camera_position", float)
plot.look_at = get_elem_tuple(elem, "look_at", float)
# Attempt to get wireframe thickness. May not be present
wireframe_thickness = elem.get("wireframe_thickness")
if wireframe_thickness:
plot.wireframe_thickness = int(wireframe_thickness)
wireframe_color = elem.get("wireframe_color")
if wireframe_color:
plot.wireframe_color = [int(item) for item in wireframe_color]
# Set plot colors
colors = {}
xs = {}
for color_elem in elem.findall("color"):
uid = color_elem.get("id")
colors[uid] = get_elem_tuple(color_elem, "rgb")
xs[uid] = float(color_elem.get("xs"))
# Set masking information
mask_elem = elem.find("mask")
if mask_elem is not None:
mask_components = [int(x)
for x in mask_elem.get("components").split()]
# TODO: set mask components (needs geometry information)
background = mask_elem.get("background")
if background is not None:
plot.mask_background = tuple(
[int(x) for x in background.split()])
# Set universe level
level = elem.find("level")
if level is not None:
plot.level = int(level.text)
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 a :class:`Plot`. It
behaves like a list as the following example demonstrates:
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()
@ -819,13 +1193,13 @@ class Plots(cv.CheckedList):
Parameters
----------
plots : Iterable of openmc.Plot
Plots to add to the collection
plots : Iterable of openmc.Plot or openmc.ProjectionPlot
plots to add to the collection
"""
def __init__(self, plots=None):
super().__init__(Plot, 'plots collection')
super().__init__((Plot, ProjectionPlot), 'plots collection')
self._plots_file = ET.Element("plots")
if plots is not None:
self += plots
@ -835,7 +1209,7 @@ class Plots(cv.CheckedList):
Parameters
----------
plot : openmc.Plot
plot : openmc.Plot or openmc.ProjectionPlot
Plot to append
"""
@ -873,7 +1247,6 @@ class Plots(cv.CheckedList):
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.
@ -925,7 +1298,8 @@ class Plots(cv.CheckedList):
# Clean the indentation in the file to be user-readable
clean_indentation(self._plots_file)
reorder_attributes(self._plots_file) # TODO: Remove when support is Python 3.8+
# TODO: Remove when support is Python 3.8+
reorder_attributes(self._plots_file)
return self._plots_file
@ -966,7 +1340,11 @@ class Plots(cv.CheckedList):
# Generate each plot
plots = cls()
for e in elem.findall('plot'):
plots.append(Plot.from_xml_element(e))
plot_type = e.get('type')
if plot_type == 'projection':
plots.append(ProjectionPlot.from_xml_element(e))
else:
plots.append(Plot.from_xml_element(e))
return plots
@classmethod
@ -987,5 +1365,3 @@ class Plots(cv.CheckedList):
tree = ET.parse(path)
root = tree.getroot()
return cls.from_xml_element(root)

View file

@ -603,6 +603,7 @@ std::pair<double, int32_t> DAGCell::distance(
// indicate that particle is lost
surf_idx = -1;
dist = INFINITY;
if (settings::run_mode == RunMode::PLOTTING) return {dist, surf_idx};
if (!dagmc_ptr_->is_implicit_complement(vol) ||
model::universe_map[dag_univ->id_] == model::root_universe) {
std::string material_id = p->material() == MATERIAL_VOID
@ -728,19 +729,22 @@ void check_dagmc_root_univ()
}
}
int32_t next_cell(
DAGUniverse* dag_univ, DAGCell* cur_cell, DAGSurface* surf_xed)
{
moab::EntityHandle surf =
surf_xed->dagmc_ptr()->entity_by_index(2, surf_xed->dag_index());
moab::EntityHandle vol =
cur_cell->dagmc_ptr()->entity_by_index(3, cur_cell->dag_index());
int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ) {
auto surfp = dynamic_cast<DAGSurface*>(model::surfaces[surf - 1].get());
auto cellp = dynamic_cast<DAGCell*>(model::cells[curr_cell].get());
auto univp = static_cast<DAGUniverse*>(model::universes[univ].get());
moab::EntityHandle surf_handle =
surfp->dagmc_ptr()->entity_by_index(2, surfp->dag_index());
moab::EntityHandle curr_vol =
cellp->dagmc_ptr()->entity_by_index(3, cellp->dag_index());
moab::EntityHandle new_vol;
cur_cell->dagmc_ptr()->next_vol(surf, vol, new_vol);
moab::ErrorCode rval = cellp->dagmc_ptr()->next_vol(surf_handle, curr_vol, new_vol);
if (rval != moab::MB_SUCCESS) return -1;
return cur_cell->dagmc_ptr()->index_by_handle(new_vol) +
dag_univ->cell_idx_offset_;
return cellp->dagmc_ptr()->index_by_handle(new_vol) +
univp->cell_idx_offset_;
}
} // namespace openmc
@ -756,8 +760,11 @@ void read_dagmc_universes(pugi::xml_node node)
"with DAGMC");
}
};
void check_dagmc_root_univ() {};
int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ);
} // namespace openmc
#endif // DAGMC

View file

@ -221,56 +221,11 @@ void print_plot()
if (settings::verbosity < 5)
return;
for (auto pl : model::plots) {
// Plot id
fmt::print("Plot ID: {}\n", pl.id_);
// Plot filename
fmt::print("Plot file: {}\n", pl.path_plot_);
// Plot level
fmt::print("Universe depth: {}\n", pl.level_);
// Plot type
if (PlotType::slice == pl.type_) {
fmt::print("Plot Type: Slice\n");
} else if (PlotType::voxel == pl.type_) {
fmt::print("Plot Type: Voxel\n");
}
// Plot parameters
fmt::print(
"Origin: {} {} {}\n", pl.origin_[0], pl.origin_[1], pl.origin_[2]);
if (PlotType::slice == pl.type_) {
fmt::print("Width: {:4} {:4}\n", pl.width_[0], pl.width_[1]);
} else if (PlotType::voxel == pl.type_) {
fmt::print(
"Width: {:4} {:4} {:4}\n", pl.width_[0], pl.width_[1], pl.width_[2]);
}
if (PlotColorBy::cells == pl.color_by_) {
fmt::print("Coloring: Cells\n");
} else if (PlotColorBy::mats == pl.color_by_) {
fmt::print("Coloring: Materials\n");
}
if (PlotType::slice == pl.type_) {
switch (pl.basis_) {
case PlotBasis::xy:
fmt::print("Basis: XY\n");
break;
case PlotBasis::xz:
fmt::print("Basis: XZ\n");
break;
case PlotBasis::yz:
fmt::print("Basis: YZ\n");
break;
}
fmt::print("Pixels: {} {}\n", pl.pixels_[0], pl.pixels_[1]);
} else if (PlotType::voxel == pl.type_) {
fmt::print(
"Voxels: {} {} {}\n", pl.pixels_[0], pl.pixels_[1], pl.pixels_[2]);
}
for (const auto& pl : model::plots) {
fmt::print("Plot ID: {}\n", pl->id());
fmt::print("Plot file: {}\n", pl->path_plot());
fmt::print("Universe depth: {}\n", pl->level());
pl->print_info(); // prints type-specific plot info
fmt::print("\n");
}
}

View file

@ -433,13 +433,7 @@ void Particle::cross_surface()
#ifdef DAGMC
// in DAGMC, we know what the next cell should be
if (surf->geom_type_ == GeometryType::DAG) {
auto surfp = dynamic_cast<DAGSurface*>(surf);
auto cellp =
dynamic_cast<DAGCell*>(model::cells[cell_last(n_coord() - 1)].get());
auto univp = static_cast<DAGUniverse*>(
model::universes[coord(n_coord() - 1).universe].get());
// determine the next cell for this crossing
int32_t i_cell = next_cell(univp, cellp, surfp) - 1;
int32_t i_cell = next_cell(i_surface, cell_last(n_coord() - 1), lowest_coord().universe) - 1;
// save material and temp
material_last() = material();
sqrtkT_last() = sqrtkT();

File diff suppressed because it is too large Load diff

View file

@ -48,14 +48,14 @@
</settings>
<plots>
<plot basis="xy" color_by="cell" filename="cellplot" id="1" type="slice">
<pixels>400 400</pixels>
<origin>0 0 0</origin>
<width>7 7</width>
<pixels>400 400</pixels>
</plot>
<plot basis="xy" color_by="material" filename="matplot" id="2" type="slice">
<pixels>400 400</pixels>
<origin>0 0 0</origin>
<width>7 7</width>
<pixels>400 400</pixels>
</plot>
</plots>
</model>

View file

@ -1,60 +1,7 @@
import glob
import hashlib
import os
import h5py
import openmc
from tests.testing_harness import TestHarness
from tests.testing_harness import PlotTestHarness
from tests.regression_tests import config
class PlotTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC plotting tests."""
def __init__(self, plot_names):
super().__init__(None)
self._plot_names = plot_names
def _run_openmc(self):
openmc.plot_geometry(openmc_exec=config['exe'])
def _test_output_created(self):
"""Make sure *.png has been created."""
for fname in self._plot_names:
assert os.path.exists(fname), 'Plot output file does not exist.'
def _cleanup(self):
super()._cleanup()
for fname in self._plot_names:
if os.path.exists(fname):
os.remove(fname)
def _get_results(self):
"""Return a string hash of the plot files."""
outstr = bytes()
for fname in self._plot_names:
if fname.endswith('.png'):
# Add PNG output to results
with open(fname, 'rb') as fh:
outstr += fh.read()
elif fname.endswith('.h5'):
# Add voxel data to results
with h5py.File(fname, 'r') as fh:
outstr += fh.attrs['filetype']
outstr += fh.attrs['num_voxels'].tobytes()
outstr += fh.attrs['lower_left'].tobytes()
outstr += fh.attrs['voxel_width'].tobytes()
outstr += fh['data'][()].tobytes()
# Hash the information and return.
sha512 = hashlib.sha512()
sha512.update(outstr)
outstr = sha512.hexdigest()
return outstr
def test_plot():
harness = PlotTestHarness(('plot_1.png', 'plot_2.png', 'plot_3.png',
'plot_4.h5'))

View file

@ -1,60 +1,7 @@
import glob
import hashlib
import os
import h5py
import openmc
from tests.testing_harness import TestHarness
from tests.testing_harness import PlotTestHarness
from tests.regression_tests import config
class PlotTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC plotting tests."""
def __init__(self, plot_names):
super().__init__(None)
self._plot_names = plot_names
def _run_openmc(self):
openmc.plot_geometry(openmc_exec=config['exe'])
def _test_output_created(self):
"""Make sure *.png has been created."""
for fname in self._plot_names:
assert os.path.exists(fname), 'Plot output file does not exist.'
def _cleanup(self):
super()._cleanup()
for fname in self._plot_names:
if os.path.exists(fname):
os.remove(fname)
def _get_results(self):
"""Return a string hash of the plot files."""
outstr = bytes()
for fname in self._plot_names:
if fname.endswith('.png'):
# Add PNG output to results
with open(fname, 'rb') as fh:
outstr += fh.read()
elif fname.endswith('.h5'):
# Add voxel data to results
with h5py.File(fname, 'r') as fh:
outstr += fh.attrs['filetype']
outstr += fh.attrs['num_voxels'].tobytes()
outstr += fh.attrs['lower_left'].tobytes()
outstr += fh.attrs['voxel_width'].tobytes()
outstr += fh['data'][()].tobytes()
# Hash the information and return.
sha512 = hashlib.sha512()
sha512.update(outstr)
outstr = sha512.hexdigest()
return outstr
def test_plot_overlap():
harness = PlotTestHarness(('plot_1.png', 'plot_2.png', 'plot_3.png',
'plot_4.h5'))

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<geometry>
<surface id="1" type="z-cylinder" coeffs="0 0 2" />
<surface id="2" type="z-cylinder" coeffs="0 0 5" />
<surface id="3" type="z-cylinder" coeffs="0 0 10" boundary="vacuum" />
<surface id="4" type="y-plane" coeffs="5.001" boundary="vacuum" />
<surface id="5" type="z-plane" coeffs="-5" boundary="vacuum" />
<surface id="6" type="z-plane" coeffs="5" boundary="vacuum" />
<cell id="1" material="1" region=" -1 -4 5 -6" />
<cell id="2" material="2" region="1 -2 -4 5 -6" />
<cell id="3" material="3" region="2 -3 -4 5 -6" />
</geometry>

View file

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<materials>
<material id="1">
<density value="4.5" units="g/cc" />
<nuclide name="U235" ao="1.0" />
</material>
<material id="2">
<density value="4.5" units="g/cc" />
<nuclide name="U235" ao="2.0" />
</material>
<material id="3">
<density value="4.5" units="g/cc" />
<nuclide name="U235" ao="3.0" />
</material>
</materials>

View file

@ -0,0 +1,55 @@
<?xml version="1.0"?>
<plots>
<plot id="1" type="projection">
<look_at>0. 0. 0.</look_at>
<camera_position>20. 20. 20.</camera_position>
<pixels>200 200</pixels>
<color id="1" rgb="0 0 255" xs="10"/>
<color id="2" rgb="0 255 0" xs="0.1"/>
<color id="3" rgb="255 0 0" xs="1.0"/>
<field_of_view>70</field_of_view>
</plot>
<plot id="2" type="projection">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 0.</camera_position>
<width>25 25</width>
<pixels>200 200</pixels>
<mask components="1 3" background="255 255 255" />
<field_of_view>90</field_of_view>
<wireframe_thickness>4</wireframe_thickness>
<filename>example1</filename>
</plot>
<plot id="3" color_by="material" type="projection">
<look_at>0. 0. 0.</look_at>
<camera_position>20. 20. 20.</camera_position>
<pixels>200 200</pixels>
<background>240 240 240</background>
<filename>example2.png</filename>
<wireframe_ids>2</wireframe_ids>
</plot>
<plot id="4" color_by="material" type="projection">
<look_at>0. 0. 0.</look_at>
<camera_position>0. 10.0 20.</camera_position>
<pixels>200 200</pixels>
<background>110 240 240</background>
<filename>example3.png</filename>
</plot>
<plot id="5" type="projection">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 10.</camera_position>
<width>25 25</width>
<pixels>200 200</pixels>
<orthographic_width>25.0</orthographic_width>
<wireframe_thickness>2</wireframe_thickness>
<filename>orthographic_example1</filename>
<color id="1" rgb="0 0 255" xs="10"/>
<color id="2" rgb="0 255 0" xs="0.1"/>
<color id="3" rgb="255 0 0" xs="1.0"/>
</plot>
</plots>

View file

@ -0,0 +1 @@
6448093f8acaa2af02452ec1b29461ffaf89628bb002cc8873a3a6831fe5d613645a197033597e5a064896388f19d0782f05e78ce474354ae2a76c35ff4551d7

View file

@ -0,0 +1,13 @@
<?xml version="1.0"?>
<settings>
<run_mode>plot</run_mode>
<mesh id="1">
<dimension>5 4 3</dimension>
<lower_left>-10 -10 -10</lower_left>
<upper_right>10 10 10</upper_right>
</mesh>
<entropy_mesh>1</entropy_mesh>
</settings>

View file

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<tallies>
<mesh id="2" type="rectilinear">
<x_grid>-10 10</x_grid>
<y_grid>-10 10</y_grid>
<z_grid>-10 0 5 7.5 8.75 10</z_grid>
</mesh>
<filter id="1">
<type>mesh</type>
<bins>2</bins>
</filter>
<tally id="1">
<filters>1</filters>
<scores>total</scores>
</tally>
</tallies>

View file

@ -0,0 +1,6 @@
from tests.testing_harness import PlotTestHarness
from tests.regression_tests import config
def test_plot():
harness = PlotTestHarness(('plot_1.png', 'example1.png', 'example2.png', 'example3.png', 'orthographic_example1.png'))
harness.main()

View file

@ -1,62 +1,11 @@
import glob
import hashlib
import os
from subprocess import check_call
import h5py
import openmc
import pytest
from tests.testing_harness import TestHarness
from tests.testing_harness import PlotTestHarness
from tests.regression_tests import config
vtk = pytest.importorskip('vtk')
class PlotVoxelTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC voxel plot tests."""
def __init__(self, plot_names):
super().__init__(None)
self._plot_names = plot_names
def _run_openmc(self):
openmc.plot_geometry(openmc_exec=config['exe'])
check_call(['../../../scripts/openmc-voxel-to-vtk'] +
glob.glob('plot_4.h5'))
def _test_output_created(self):
"""Make sure plots have been created."""
for fname in self._plot_names:
assert os.path.exists(fname), 'Plot output file does not exist.'
def _cleanup(self):
super()._cleanup()
for fname in self._plot_names:
if os.path.exists(fname):
os.remove(fname)
def _get_results(self):
"""Return a string hash of the plot files."""
outstr = bytes()
for fname in self._plot_names:
if fname.endswith('.h5'):
# Add voxel data to results
with h5py.File(fname, 'r') as fh:
outstr += fh.attrs['filetype']
outstr += fh.attrs['num_voxels'].tobytes()
outstr += fh.attrs['lower_left'].tobytes()
outstr += fh.attrs['voxel_width'].tobytes()
outstr += fh['data'][()].tobytes()
# Hash the information and return.
sha512 = hashlib.sha512()
sha512.update(outstr)
outstr = sha512.hexdigest()
return outstr
def test_plot_voxel():
harness = PlotVoxelTestHarness(('plot_4.h5', 'plot.vti'))
harness = PlotTestHarness(('plot_4.h5', 'plot.vti'), voxel_convert_checks=['plot_4.h5'])
harness.main()

View file

@ -1,6 +1,8 @@
from difflib import unified_diff
from subprocess import check_call
import filecmp
import glob
import h5py
import hashlib
import os
import shutil
@ -380,3 +382,55 @@ class HashedPyAPITestHarness(PyAPITestHarness):
def _get_results(self):
"""Digest info in the statepoint and return as a string."""
return super()._get_results(True)
class PlotTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC plotting tests."""
def __init__(self, plot_names, voxel_convert_checks=[]):
super().__init__(None)
self._plot_names = plot_names
self._voxel_convert_checks = voxel_convert_checks
def _run_openmc(self):
openmc.plot_geometry(openmc_exec=config['exe'])
# Check that voxel h5 can be converted to vtk
for voxel_h5_filename in self._voxel_convert_checks:
check_call(['../../../scripts/openmc-voxel-to-vtk'] +
glob.glob(voxel_h5_filename))
def _test_output_created(self):
"""Make sure *.png has been created."""
for fname in self._plot_names:
assert os.path.exists(fname), 'Plot output file does not exist.'
def _cleanup(self):
super()._cleanup()
for fname in self._plot_names:
if os.path.exists(fname):
os.remove(fname)
def _get_results(self):
"""Return a string hash of the plot files."""
outstr = bytes()
for fname in self._plot_names:
if fname.endswith('.png'):
# Add PNG output to results
with open(fname, 'rb') as fh:
outstr += fh.read()
elif fname.endswith('.h5'):
# Add voxel data to results
with h5py.File(fname, 'r') as fh:
outstr += fh.attrs['filetype']
outstr += fh.attrs['num_voxels'].tobytes()
outstr += fh.attrs['lower_left'].tobytes()
outstr += fh.attrs['voxel_width'].tobytes()
outstr += fh['data'][()].tobytes()
# Hash the information and return.
sha512 = hashlib.sha512()
sha512.update(outstr)
outstr = sha512.hexdigest()
return outstr

View file

@ -37,15 +37,47 @@ def myplot():
}
return plot
@pytest.fixture(scope='module')
def myprojectionplot():
plot = openmc.ProjectionPlot(name='myprojectionplot')
plot.look_at = (0.0, 0.0, 0.0)
plot.camera_position = (4.0, 3.0, 0.0)
plot.pixels = (500, 500)
plot.filename = 'myprojectionplot'
plot.background = (0, 0, 0)
plot.background = 'black'
plot.color_by = 'material'
m1, m2 = openmc.Material(), openmc.Material()
plot.colors = {m1: (0, 255, 0), m2: (0, 0, 255)}
plot.colors = {m1: 'green', m2: 'blue'}
plot.xs = {m1: 1.0, m2: 0.01}
plot.mask_components = [openmc.Material()]
plot.mask_background = (255, 255, 255)
plot.mask_background = 'white'
plot.overlap_color = (255, 211, 0)
plot.overlap_color = 'yellow'
plot.wireframe_thickness = 2
plot.level = 1
return plot
def test_attributes(myplot):
assert myplot.name == 'myplot'
def test_attributes_proj(myprojectionplot):
assert myprojectionplot.name == 'myprojectionplot'
def test_repr(myplot):
r = repr(myplot)
assert isinstance(r, str)
def test_repr_proj(myprojectionplot):
r = repr(myprojectionplot)
assert isinstance(r, str)
def test_from_geometry():
width = 25.
@ -89,6 +121,18 @@ def test_xml_element(myplot):
assert getattr(newplot, attr) == getattr(myplot, attr), attr
def test_to_xml_element_proj(myprojectionplot):
elem = myprojectionplot.to_xml_element()
assert 'id' in elem.attrib
assert 'color_by' in elem.attrib
assert 'type' in elem.attrib
assert elem.find('camera_position') is not None
assert elem.find('wireframe_thickness') is not None
assert elem.find('look_at') is not None
assert elem.find('pixels') is not None
assert elem.find('background').text == '0 0 0'
def test_plots(run_in_tmpdir):
p1 = openmc.Plot(name='plot1')
p1.origin = (5., 5., 5.)
@ -99,10 +143,14 @@ def test_plots(run_in_tmpdir):
plots = openmc.Plots([p1, p2])
assert len(plots) == 2
p3 = openmc.Plot(name='plot3')
plots.append(p3)
p3 = openmc.ProjectionPlot(name='plot3')
plots = openmc.Plots([p1, p2, p3])
assert len(plots) == 3
p4 = openmc.Plot(name='plot4')
plots.append(p4)
assert len(plots) == 4
plots.export_to_xml()
# from_xml