From 6726cc378f3025a9cd1c7dfe4245e245188204fd Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Sun, 9 Jan 2022 01:54:38 -0500 Subject: [PATCH] add unit test, select wireframing by id --- include/openmc/plot.h | 8 +- openmc/plots.py | 513 +++++++++++++++++++++++++-------- src/plot.cpp | 89 +++++- tests/unit_tests/test_plots.py | 52 +++- 4 files changed, 530 insertions(+), 132 deletions(-) diff --git a/include/openmc/plot.h b/include/openmc/plot.h index b076c0870..b09727835 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -282,6 +282,8 @@ private: 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); /* Used for drawing wireframe and colors. We record the list of * surface/cell/material intersections and the corresponding lengths as a ray @@ -306,6 +308,8 @@ private: 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 + std::vector + wireframe_ids_; // which color IDs should be wireframed. If empty, all /* The horizontal thickness, if using an orthographic projection. * If set to zero, we assume using a perspective projection. @@ -329,8 +333,8 @@ private: * to mean not having matching intersection lengths, but rather having * a matching sequence of surface/cell/material intersections. */ - static bool trackstack_equivalent(const std::vector& track1, - const std::vector& track2); + bool trackstack_equivalent(const std::vector& track1, + const std::vector& track2) const; // Closed form 3x3 matrix inversion static std::vector invert_matrix(const std::vector& input); diff --git a/openmc/plots.py b/openmc/plots.py index 4b7d58e3b..7b67fee2c 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -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 `_. - +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 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,132 @@ 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) + + 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,)) + +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 `_. + + 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']) + 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) @@ -438,19 +499,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) @@ -474,6 +522,7 @@ class Plot(IDManagerMixin): 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. @@ -520,38 +569,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 +621,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 ------- @@ -802,13 +819,271 @@ class Plot(IDManagerMixin): # Return produced image 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. + camera_position : tuple or list of ndarray + Position of the camera in 3D space + look_at : tuple or list of ndarray + The center of the camera's image points to this place in 3D space + 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 + 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. + + wireframe_thickness : int + Line thickness employed for drawing wireframes around cells or + material regions. Can be set to zero for no wireframes + wireframe_color : tuple of ints + RGB color of the wireframe lines + wireframe_regions : 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. + """ + + 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_regions = [] + 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_regions(self): + return self._wireframe_regions + + @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) + self._orthographic_width = orthographic_width + + @wireframe_thickness.setter + def wireframe_thickness(self, wireframe_thickness): + cv.check_type('plot orthographic width', 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_regions.setter + def wireframe_regions(self, wireframe_regions): + for region in wireframe_regions: + 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_regions = wireframe_regions + + @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 = ET.Element("plot") + 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", "projection") + + subelement = ET.SubElement(element, "pixels") + subelement.text = ' '.join(map(str, self._pixels)) + + 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_regions: + id_list = [x.id for x in self._wireframe_regions] + subelement = ET.SubElement(element, "wireframe_ids") + subelement.text = ' '.join([str(x) for x in id_list]) + + 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(d.id) 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._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 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 +1094,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 +1110,7 @@ class Plots(cv.CheckedList): Parameters ---------- - plot : openmc.Plot + plot : openmc.Plot or openmc.ProjectionPlot Plot to append """ diff --git a/src/plot.cpp b/src/plot.cpp index 2e849c907..285f7f5a7 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1035,6 +1035,8 @@ ProjectionPlot::ProjectionPlot(pugi::xml_node node) : PlottableInterface(node) set_opacities(node); set_orthographic_width(node); set_wireframe_thickness(node); + set_wireframe_ids(node); + set_wireframe_color(node); if (check_for_node(node, "orthographic_width") && check_for_node(node, "field_of_view")) @@ -1042,6 +1044,19 @@ ProjectionPlot::ProjectionPlot(pugi::xml_node node) : PlottableInterface(node) "parameters."); } +void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node) +{ + // Copy plot background color + if (check_for_node(plot_node, "wireframe_color")) { + vector w_rgb = get_node_array(plot_node, "wireframe_color"); + if (w_rgb.size() == 3) { + wireframe_color_ = w_rgb; + } else { + fatal_error(fmt::format("Bad wireframe RGB in plot {}", id())); + } + } +} + void ProjectionPlot::set_output_path(pugi::xml_node node) { // Set output file path @@ -1091,17 +1106,55 @@ int ProjectionPlot::advance_to_boundary_from_void(Particle& p) bool ProjectionPlot::trackstack_equivalent( const std::vector& track1, - const std::vector& track2) + const std::vector& track2) const { - if (track1.size() != track2.size()) - return false; - for (int i = 0; i < track1.size(); ++i) { - if (track1[i].id != track2[i].id || - track1[i].surface != track2[i].surface) { - return false; + if (wireframe_ids_.empty()) { + // TODO old version + return true; + } else { + // This runs in O(nm) where n is the intersection stack size + // and m is the number of IDs we are wireframing. A simpler + // algorithm can likely be found. + for (const int id : wireframe_ids_) { + int t1_i = 0; + int t2_i = 0; + + // Advance to first instance of the ID + while (t1_i < track1.size() && t2_i < track2.size()) { + while (t1_i < track1.size() && track1[t1_i].id != id) + t1_i++; + while (t2_i < track2.size() && track2[t2_i].id != id) + t2_i++; + + // This one is really important! + if ((t1_i == track1.size() && t2_i != track2.size()) || + (t1_i != track1.size() && t2_i == track2.size())) + return false; + if (t1_i == track1.size() && t2_i == track2.size()) + break; + // Check if surface different + if (track1[t1_i].surface != track2[t2_i].surface) + return false; + + // Pretty sure this should not be used: + // if (t2_i != track2.size() - 1 && + // t1_i != track1.size() - 1 && + // track1[t1_i+1].id != track2[t2_i+1].id) return false; + if (t2_i != 0 && t1_i != 0 && + track1[t1_i - 1].surface != track2[t2_i - 1].surface) + return false; + + // Check if neighboring cells are different + // if (track1[t1_i ? t1_i - 1 : 0].id != track2[t2_i ? t2_i - 1 : 0].id) + // return false; if (track1[t1_i < track1.size() - 1 ? t1_i + 1 : t1_i + // ].id != + // track2[t2_i < track2.size() - 1 ? t2_i + 1 : t2_i].id) return + // false; + t1_i++, t2_i++; + } } + return true; } - return true; } void ProjectionPlot::create_output() const @@ -1247,7 +1300,10 @@ void ProjectionPlot::create_output() const // edges on the model boundary for the same cell. if (first_inside_model) { this_line_segments[tid][horiz].emplace_back( - 0, 0.0, first_surface); + color_by_ == PlotColorBy::mats + ? p.material() + : p.coord(p.n_coord() - 1).cell, + 0.0, first_surface); first_inside_model = false; } @@ -1435,6 +1491,21 @@ void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node) } } +void ProjectionPlot::set_wireframe_ids(pugi::xml_node node) +{ + if (check_for_node(node, "wireframe_ids")) { + wireframe_ids_ = get_node_array(node, "wireframe_ids"); + // It is read in as actual ID values, but we have to convert to indices in + // mat/cell array + for (auto& x : wireframe_ids_) + x = color_by_ == PlotColorBy::mats ? model::material_map[x] + : model::cell_map[x]; + } + // We make sure the list is sorted in order to later use + // std::binary_search. + std::sort(wireframe_ids_.begin(), wireframe_ids_.end()); +} + void ProjectionPlot::set_pixels(pugi::xml_node node) { vector pxls = get_node_array(node, "pixels"); diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index 2db396cd5..183341c26 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -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