Raytrace plots (#2655)

Co-authored-by: Patrick Shriwise <pshriwise@gmail.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
Gavin Ridley 2025-02-17 21:11:54 -06:00 committed by GitHub
parent a5b26de041
commit 81b7388624
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1487 additions and 476 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View file

@ -7,13 +7,18 @@ Geometry Plotting Specification -- plots.xml
Basic plotting capabilities are available in OpenMC by creating a plots.xml file
and subsequently running with the ``--plot`` command-line flag. The root element
of the plots.xml is simply ``<plots>`` and any number output plots can be
defined with ``<plot>`` sub-elements. Two plot types are currently implemented
defined with ``<plot>`` sub-elements. Four plot types are currently implemented
in openMC:
* ``slice`` 2D pixel plot along one of the major axes. Produces a PNG image
file.
* ``voxel`` 3D voxel data dump. Produces an HDF5 file containing voxel xyz
position and cell or material id.
* ``wireframe_raytrace`` 2D pixel plot of a three-dimensional view of a
geometry using wireframes around cells or materials and coloring by depth
through each material.
* ``solid_raytrace`` 2D pixel plot of a three-dimensional view of a geometry
with solid colored surfaces of a set of cells or materials.
------------------
@ -66,21 +71,22 @@ sub-elements:
*Default*: None - Required entry
:type:
Keyword for type of plot to be produced. Currently only "slice" and "voxel"
plots are implemented. The "slice" plot type creates 2D pixel maps saved in
the PNG file format. The "voxel" plot type produces a binary datafile
containing voxel grid positioning and the cell or material (specified by the
``color`` tag) at the center of each voxel. Voxel plot files can be
processed into VTK files using the :func:`openmc.voxel_to_vtk` function and
subsequently viewed with a 3D viewer such as VISIT or Paraview. See the
:ref:`io_voxel` for information about the datafile structure.
Keyword for type of plot to be produced. Currently "slice", "voxel",
"wireframe_raytrace", and "solid_raytrace" plots are implemented. The
"slice" plot type creates 2D pixel maps saved in the PNG file format. The
"voxel" plot type produces a binary datafile containing voxel grid
positioning and the cell or material (specified by the ``color`` tag) at the
center of each voxel. Voxel plot files can be processed into VTK files using
the :func:`openmc.voxel_to_vtk` function and subsequently viewed with a 3D
viewer such as VISIT or Paraview. See :ref:`io_voxel` for information about
the datafile structure.
.. note:: High-resolution voxel files produced by OpenMC can be quite large,
but the equivalent VTK files will be significantly smaller.
*Default*: "slice"
``<plot>`` elements of ``type`` "slice" and "voxel" must contain the ``pixels``
All ``<plot>`` elements must contain the ``pixels``
attribute or sub-element:
:pixels:
@ -96,7 +102,7 @@ attribute or sub-element:
``width``/``pixels`` along that basis direction may not appear
in the plot.
*Default*: None - Required entry for "slice" and "voxel" plots
*Default*: None - Required entry for all plots
``<plot>`` elements of ``type`` "slice" can also contain the following
attributes or sub-elements. These are not used in "voxel" plots:
@ -125,6 +131,11 @@ attributes or sub-elements. These are not used in "voxel" plots:
Specifies the custom color for the cell or material. Should be 3 integers
separated by spaces.
:xs:
The attenuation coefficient for volume rendering of color in units of
inverse centimeters. Zero corresponds to transparency. Only for plot type
"wireframe_raytrace".
As an example, if your plot is colored by material and you want material 23
to be blue, the corresponding ``color`` element would look like:
@ -191,3 +202,80 @@ attributes or sub-elements. These are not used in "voxel" plots:
*Default*: 0 0 0 (black)
*Default*: None
``<plot>`` elements of ``type`` "wireframe_raytrace" or "solid_raytrace" can contain the
following attributes or sub-elements.
:camera_position:
Location in 3D Cartesian space the camera is at.
*Default*: None - Required for all ``wireframe_raytrace`` or
``solid_raytrace`` plots
:look_at:
Location in 3D Cartesian space the camera is looking at.
*Default*: None - Required for all ``wireframe_raytrace`` or
``solid_raytrace`` plots
:field_of_view:
The horizontal field of view in degrees. Defaults to roughly the same value
as for the human eye.
*Default*: 70
:orthographic_width:
If set to a nonzero value, an orthographic rather than perspective
projection for the camera is employed. An orthographic projection puts out
parallel rays from the camera of a width prescribed here in the horizontal
direction, with the width in the vertical direction decided by the pixel
aspect ratio.
*Default*: 0
``<plot>`` elements of ``type`` "solid_raytrace" can contain the following attributes or
sub-elements.
:opaque_ids:
List of integer IDs of cells or materials to be treated as visible in the
plot. Whether the integers are interpreted as cell or material IDs depends
on ``color_by``.
*Default*: None - Required for all phong plots
:light_position:
Location in 3D Cartesian space of the light.
*Default*: Same location as ``camera_position``
:diffuse_fraction:
Fraction of light originating from non-directional sources. If set to one,
the coloring is not influenced by surface curvature, and no shadows appear.
If set to zero, only regions illuminated by the light are not black.
*Default*: 0.1
``<plot>`` elements of ``type`` "wireframe_raytrace" can contain the following
attributes or sub-elements.
:wireframe_color:
RGB value of the wireframe's color
*Default*: 0, 0, 0 (black)
:wireframe_thickness:
Integer number of pixels that the wireframe takes up. The value is a radius
of the wireframe. Setting to zero removes any wireframing.
*Default*: 0
:wireframe_ids:
Integer IDs of cells or materials of regions to draw wireframes around.
Whether the integers are interpreted as cell or material IDs depends on
``color_by``.
*Default*: None

View file

@ -165,7 +165,8 @@ Geometry Plotting
:template: myclass.rst
openmc.Plot
openmc.ProjectionPlot
openmc.WireframeRayTracePlot
openmc.SolidRayTracePlot
openmc.Plots
Running OpenMC

View file

@ -52,7 +52,7 @@ Compatibility Notes and Deprecations
New Features
------------
- A new :class:`openmc.ProjectionPlot` class enables the generation of orthographic or
- A new :class:`openmc.WireframeRayTracePlot` class enables the generation of orthographic or
perspective projection plots. (`#1926
<https://github.com/openmc-dev/openmc/pull/1926>`_)
- The :class:`openmc.model.RightCircularCylinder` class now supports optional

View file

@ -122,27 +122,82 @@ for doing this will depend on the 3D viewer, but should be straightforward.
million or so). Thus if you want an accurate picture that renders
smoothly, consider using only one voxel in a certain direction.
----------------
Projection Plots
----------------
----------------------
Solid Ray-traced Plots
----------------------
.. image:: ../_images/phong_triso.png
:width: 300px
The :class:`openmc.SolidRayTracePlot` class allows three dimensional
visualization of detailed geometric features without voxelization. The plot
above visualizes a geometry created by :class:`openmc.TRISO`, with the materials
in the fuel kernel distinguished by color. It was enclosed in a bounding box
such that some kernels are cut off, revealing the inner structure of the kernel.
The `Phong reflection model
<https://en.wikipedia.org/wiki/Phong_reflection_model>`_ approximates how light
reflects off of a surface. On a diffusely light-scattering material, the Phong
model prescribes the amount of light reflected from a surface as proportional to
the dot product between the normal vector of the surface and the vector between
that point on the surface and the light. With this assumption, visually
appealing plots of simulation geometries can be created.
Solid ray-traced plots use the same ray tracing functions that neutrons and
photons do in OpenMC, so any input that does not leak particles can be
visualized in 3D using a solid ray-traced plot. That being said, these plots are
not useful for detecting overlap or undefined regions, so it is recommended to
use the slice plot approach for geometry debugging.
Only a few inputs are required for a solid ray-traced plot. The camera location,
where the camera is looking, and a set of opaque material or cell IDs are
required. The colors of materials or cells are prescribed in the same way as
slice plots. The set of IDs that are opaque in the plot must correspond to
materials if coloring by material, or cells if coloring by cell.
A minimal solid ray-traced plot input could be::
plot = openmc.SolidRayTracePlot()
plot.pixels = (600, 600)
plot.camera_position = (10.0, 20.0, -30.0)
plot.look_at = (4.0, 5.0, 1.0)
plot.color_by = 'cell'
# optional. defaults to camera_position
plot.light_position = (10, 20, 30)
# controls ambient lighting. Defaults to 10%
plot.diffuse_fraction = 0.1
plot.opaque_domains = [cell2, cell3]
These plots are then stored into a :class:`openmc.Plots` instance, just like the
slice plots.
---------------
Wireframe Plots
---------------
.. only:: html
.. 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.
The :class:`openmc.WireframeRayTracePlot` class also produces 3D visualizations
of OpenMC geometries without voxelization but is intended to show the inside of
a model using wireframing of cell or material boundaries in addition to cell
coloring based on the path length of camera rays through the model. The coloring
in these plots is a bit like turning the model into partially transparent
colored glass that can be seen through, without any refractive effects. This is
called volume rendering. The colors are specified in exactly the same interface
employed by slice plots.
One drawback of projection plots is that particle tracks cannot be overlaid on
Similar to solid ray-traced plots, these use the native ray tracing capabilities
within OpenMC, so any geometry in which particles successfully run without
overlaps or leaks will work with wireframe plots.
One drawback of wireframe 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
possible with wireframe 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.
@ -152,7 +207,7 @@ before exporting to plots.xml.
import numpy as np
for i in range(100):
phi = 2 * np.pi * i/100
thisp = openmc.ProjectionPlot(plot_id = 4 + i)
thisp = openmc.WireframeRayTracePlot(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)]
@ -167,42 +222,45 @@ before exporting to plots.xml.
plot_file.append(thisp)
This generates a sequence of png files which can be joined to form a gif. Each
This generates a sequence of png files that 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.
to create a perfectly looped gif. :attr:`~WireframeRayTracePlot.look_at` defines
where the camera's centerline should point at.
:attr:`~WireframeRayTracePlot.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
:meth:`~WireframeRayTracePlot.set_transparent` method and
:attr:`~WireframeRayTracePlot.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
:meth:`~WireframeRayTracePlot.set_transparent` method 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
:attr:`~WireframeRayTracePlot.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.
be set with the :attr:`~WireframeRayTracePlot.orthographic_width` attribute. 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
Most importantly, wireframe 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:`~WireframeRayTracePlot.wireframe_domains` attribute, which may be set to
either material IDs or cell IDs. The
:attr:`~WireframeRayTracePlot.wireframe_thickness` attribute sets the wireframe
thickness in units of pixels.
.. note:: When setting specific material or cell regions to have wireframes

View file

@ -115,7 +115,7 @@ private:
//!
//! Uses the comobination of half-spaces and binary operators to determine
//! if short circuiting can be used. Short cicuiting uses the relative and
//! absolute depth of parenthases in the expression.
//! absolute depth of parentheses in the expression.
bool contains_complex(Position r, Direction u, int32_t on_surface) const;
//! BoundingBox if the paritcle is in a simple cell.

View file

@ -49,7 +49,8 @@ public:
double evaluate(Position r) const override;
double distance(Position r, Direction u, bool coincident) const override;
Direction normal(Position r) const override;
Direction reflect(Position r, Direction u, GeometryState* p) const override;
Direction reflect(
Position r, Direction u, GeometryState* p = nullptr) const override;
inline void to_hdf5_inner(hid_t group_id) const override {};

View file

@ -39,10 +39,6 @@ public:
double speed() const;
//! moves the particle by the distance length to its next location
//! \param length the distance the particle is moved
void move_distance(double length);
//! create a secondary particle
//
//! stores the current phase space attributes of the particle in the

View file

@ -191,6 +191,13 @@ struct BoundaryInfo {
array<int, 3>
lattice_translation {}; //!< which way lattice indices will change
void reset()
{
distance = INFINITY;
surface = SURFACE_NONE;
coord_level = 0;
lattice_translation = {0, 0, 0};
}
// TODO: off-by-one
int surface_index() const { return std::abs(surface) - 1; }
};
@ -226,6 +233,12 @@ public:
n_coord_last_ = 1;
}
//! moves the particle by the specified distance to its next location
//! \param distance the distance the particle is moved
void move_distance(double distance);
void advance_to_boundary_from_void();
// Initialize all internal state from position and direction
void init_from_r_u(Position r_a, Direction u_a)
{
@ -565,7 +578,6 @@ public:
int& cell_born() { return cell_born_; }
const int& cell_born() const { return cell_born_; }
// index of the current and last material
// Total number of collisions suffered by particle
int& n_collision() { return n_collision_; }
const int& n_collision() const { return n_collision_; }

View file

@ -4,6 +4,7 @@
#include <cmath>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include "pugixml.hpp"
#include "xtensor/xarray.hpp"
@ -61,6 +62,14 @@ struct RGBColor {
return red == other.red && green == other.green && blue == other.blue;
}
RGBColor& operator*=(const double x)
{
red *= x;
green *= x;
blue *= x;
return *this;
}
// Members
uint8_t red, green, blue;
};
@ -70,9 +79,14 @@ 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
* \brief Interface for plottable objects.
*
* PlottableInterface classes must have a unique ID in the plots.xml file.
* They guarantee the ability to create output in some form. This interface
* is designed to be implemented by classes that produce plot-relevant data
* which can be visualized.
*/
class PlottableInterface {
private:
@ -232,8 +246,8 @@ T SlicePlotBase::get_map() const
data.set_overlap(y, x);
}
} // inner for
} // outer for
} // omp parallel
}
}
return data;
}
@ -268,32 +282,110 @@ public:
RGBColor meshlines_color_; //!< Color of meshlines on the plot
};
class ProjectionPlot : public PlottableInterface {
/**
* \class RaytracePlot
* \brief Base class for plots that generate images through ray tracing.
*
* This class serves as a base for plots that create their visuals by tracing
* rays from a camera through the problem geometry. It inherits from
* PlottableInterface, ensuring that it provides an implementation for
* generating output specific to ray-traced visualization. WireframeRayTracePlot
* and SolidRayTracePlot provide concrete implementations of this class.
*/
class RayTracePlot : public PlottableInterface {
public:
RayTracePlot(pugi::xml_node plot);
// Standard getters. No setting since it's done from XML.
const Position& camera_position() const { return camera_position_; }
const Position& look_at() const { return look_at_; }
const double& horizontal_field_of_view() const
{
return horizontal_field_of_view_;
}
virtual void print_info() const;
protected:
Direction camera_x_axis() const
{
return {camera_to_model_[0], camera_to_model_[3], camera_to_model_[6]};
}
Direction camera_y_axis() const
{
return {camera_to_model_[1], camera_to_model_[4], camera_to_model_[7]};
}
Direction camera_z_axis() const
{
return {camera_to_model_[2], camera_to_model_[5], camera_to_model_[8]};
}
void set_output_path(pugi::xml_node plot_node);
/*
* Gets the starting position and direction for the pixel corresponding
* to this horizontal and vertical position.
*/
std::pair<Position, Direction> get_pixel_ray(int horiz, int vert) const;
std::array<int, 2> pixels_; // pixel dimension of resulting image
private:
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_orthographic_width(pugi::xml_node node);
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
/* The horizontal thickness, if using an orthographic projection.
* If set to zero, we assume using a perspective projection.
*/
double orthographic_width_ {C_NONE};
/*
* Cached camera-to-model matrix with column vectors of axes. The x-axis is
* the vector between the camera_position_ and look_at_; the y-axis is the
* cross product of the x-axis with the up_ vector, and the z-axis is the
* cross product of the x and y axes.
*/
std::array<double, 9> camera_to_model_;
};
class ProjectionRay;
/**
* \class WireframeRayTracePlot
* \brief Creates plots that are like colorful x-ray imaging
*
* WireframeRayTracePlot is a specialized form of RayTracePlot designed for
* creating projection plots. This involves tracing rays from a camera through
* the problem geometry and rendering the results based on depth of penetration
* through materials or cells and their colors.
*/
class WireframeRayTracePlot : public RayTracePlot {
friend class ProjectionRay;
public:
ProjectionPlot(pugi::xml_node plot);
WireframeRayTracePlot(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(GeometryState& 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.
@ -314,30 +406,15 @@ private:
* 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
int surface_index {-1}; // last surface index intersected in this segment
TrackSegment(int id_a, double length_a, int surface_a)
: id(id_a), length(length_a), surface(surface_a)
: id(id_a), length(length_a), surface_index(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};
@ -345,6 +422,124 @@ private:
vector<double> xs_; // macro cross section values for cell volume rendering
};
/**
* \class SolidRayTracePlot
* \brief Plots 3D objects as the eye might see them.
*
* Plots a geometry with single-scattered Phong lighting plus a diffuse lighting
* contribution. The result is a physically reasonable, aesthetic 3D view of a
* geometry.
*/
class SolidRayTracePlot : public RayTracePlot {
friend class PhongRay;
public:
SolidRayTracePlot(pugi::xml_node plot);
virtual void create_output() const;
virtual void print_info() const;
private:
void set_opaque_ids(pugi::xml_node node);
void set_light_position(pugi::xml_node node);
void set_diffuse_fraction(pugi::xml_node node);
std::unordered_set<int> opaque_ids_;
double diffuse_fraction_ {0.1};
// By default, the light is at the camera unless otherwise specified.
Position light_location_;
};
// Base class that implements ray tracing logic, not necessarily through
// defined regions of the geometry but also outside of it.
class Ray : public GeometryState {
public:
Ray(Position r, Direction u) { init_from_r_u(r, u); }
// Called at every surface intersection within the model
virtual void on_intersection() = 0;
/*
* Traces the ray through the geometry, calling on_intersection
* at every surface boundary.
*/
void trace();
// Stops the ray and exits tracing when called from on_intersection
void stop() { stop_ = true; }
// Sets the dist_ variable
void compute_distance();
protected:
// Records how far the ray has traveled
double traversal_distance_ {0.0};
private:
// Max intersections before we assume ray tracing is caught in an infinite
// loop:
static const int MAX_INTERSECTIONS = 1000000;
bool hit_something_ {false};
bool stop_ {false};
unsigned event_counter_ {0};
};
class ProjectionRay : public Ray {
public:
ProjectionRay(Position r, Direction u, const WireframeRayTracePlot& plot,
vector<WireframeRayTracePlot::TrackSegment>& line_segments)
: Ray(r, u), plot_(plot), line_segments_(line_segments)
{}
virtual void on_intersection() override;
private:
/* Store a reference to the plot object which is running this ray, in order
* to access some of the plot settings which influence the behavior where
* intersections are.
*/
const WireframeRayTracePlot& plot_;
/* The ray runs through the geometry, and records the lengths of ray segments
* and cells they lie in along the way.
*/
vector<WireframeRayTracePlot::TrackSegment>& line_segments_;
};
class PhongRay : public Ray {
public:
PhongRay(Position r, Direction u, const SolidRayTracePlot& plot)
: Ray(r, u), plot_(plot)
{
result_color_ = plot_.not_found_;
}
virtual void on_intersection() override;
const RGBColor& result_color() { return result_color_; }
private:
const SolidRayTracePlot& plot_;
/* After the ray is reflected, it is moving towards the
* camera. It does that in order to see if the exposed surface
* is shadowed by something else.
*/
bool reflected_ {false};
// Have to record the first hit ID, so that if the region
// does get shadowed, we recall what its color should be
// when tracing from the surface to the light.
int orig_hit_id_ {-1};
RGBColor result_color_;
};
//===============================================================================
// Non-member functions
//===============================================================================

View file

@ -94,8 +94,24 @@ struct Position {
//! \result Reflected vector
Position reflect(Position n) const;
//! Rotate the position based on a rotation matrix
Position rotate(const vector<double>& rotation) const;
//! Rotate the position by applying a rotation matrix
template<typename T>
Position rotate(const T& rotation) const
{
return {x * rotation[0] + y * rotation[1] + z * rotation[2],
x * rotation[3] + y * rotation[4] + z * rotation[5],
x * rotation[6] + y * rotation[7] + z * rotation[8]};
}
//! Rotate the position by applying the inverse of a rotation matrix
//! using the fact that rotation matrices are orthonormal.
template<typename T>
Position inverse_rotate(const T& rotation) const
{
return {x * rotation[0] + y * rotation[3] + z * rotation[6],
x * rotation[1] + y * rotation[4] + z * rotation[7],
x * rotation[2] + y * rotation[5] + z * rotation[8]};
}
// Data members
double x = 0.;

View file

@ -62,7 +62,7 @@ public:
Position r, Direction u, GeometryState* p = nullptr) const;
virtual Direction diffuse_reflect(
Position r, Direction u, uint64_t* seed, GeometryState* p = nullptr) const;
Position r, Direction u, uint64_t* seed) const;
//! Evaluate the equation describing the surface.
//!

View file

@ -82,7 +82,7 @@ def reorder_attributes(root):
def get_elem_tuple(elem, name, dtype=int):
'''Helper function to get a tuple of values from an elem
"""Helper function to get a tuple of values from an elem
Parameters
----------
@ -97,7 +97,7 @@ def get_elem_tuple(elem, name, dtype=int):
-------
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

@ -124,7 +124,7 @@ class Model:
@plots.setter
def plots(self, plots):
check_type('plots', plots, Iterable, openmc.Plot)
check_type('plots', plots, Iterable, openmc.PlotBase)
if isinstance(plots, openmc.Plots):
self._plots = plots
else:
@ -220,7 +220,8 @@ class Model:
materials = openmc.Materials.from_xml(materials)
geometry = openmc.Geometry.from_xml(geometry, materials)
settings = openmc.Settings.from_xml(settings)
tallies = openmc.Tallies.from_xml(tallies) if Path(tallies).exists() else None
tallies = openmc.Tallies.from_xml(
tallies) if Path(tallies).exists() else None
plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None
return cls(geometry, materials, settings, tallies, plots)
@ -242,12 +243,16 @@ class Model:
model = cls()
meshes = {}
model.settings = openmc.Settings.from_xml_element(root.find('settings'), meshes)
model.materials = openmc.Materials.from_xml_element(root.find('materials'))
model.geometry = openmc.Geometry.from_xml_element(root.find('geometry'), model.materials)
model.settings = openmc.Settings.from_xml_element(
root.find('settings'), meshes)
model.materials = openmc.Materials.from_xml_element(
root.find('materials'))
model.geometry = openmc.Geometry.from_xml_element(
root.find('geometry'), model.materials)
if root.find('tallies') is not None:
model.tallies = openmc.Tallies.from_xml_element(root.find('tallies'), meshes)
model.tallies = openmc.Tallies.from_xml_element(
root.find('tallies'), meshes)
if root.find('plots') is not None:
model.plots = openmc.Plots.from_xml_element(root.find('plots'))
@ -538,11 +543,13 @@ class Model:
if self.tallies:
tallies_element = self.tallies.to_xml_element(mesh_memo)
xml.clean_indentation(tallies_element, level=1, trailing_indent=self.plots)
xml.clean_indentation(
tallies_element, level=1, trailing_indent=self.plots)
fh.write(ET.tostring(tallies_element, encoding="unicode"))
if self.plots:
plots_element = self.plots.to_xml_element()
xml.clean_indentation(plots_element, level=1, trailing_indent=False)
xml.clean_indentation(
plots_element, level=1, trailing_indent=False)
fh.write(ET.tostring(plots_element, encoding="unicode"))
fh.write("</model>\n")

View file

@ -555,13 +555,21 @@ class PlotBase(IDManagerMixin):
else:
domains = geometry.get_all_cells().values()
# Set the seed for the random number generator
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
@ -887,13 +895,7 @@ class Plot(PlotBase):
subelement.text = ' '.join(map(str, self._width))
if self._colors:
for domain, color in sorted(self._colors.items(),
key=lambda x: PlotBase._get_id(x[0])):
subelement = ET.SubElement(element, "color")
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))
self._colors_to_xml(element)
if self._show_overlaps:
subelement = ET.SubElement(element, "show_overlaps")
@ -1051,7 +1053,8 @@ class Plot(PlotBase):
"""
if self.type != 'voxel':
raise ValueError('Generating a VTK file only works for voxel plots')
raise ValueError(
'Generating a VTK file only works for voxel plots')
# Create plots.xml
Plots([self]).export_to_xml(cwd)
@ -1072,20 +1075,17 @@ class Plot(PlotBase):
return voxel_to_vtk(h5_voxel_file, output)
class ProjectionPlot(PlotBase):
class RayTracePlot(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.
projections are more similar to a pinhole camera, and orthographic
projections preserve parallel lines and distances.
.. versionadded:: 0.14.0
This is an abstract base class that :class:`WireframeRayTracePlot` and
:class:`SolidRayTracePlot` finish the implementation of.
.. versionadded:: 0.15.1
Parameters
----------
@ -1113,21 +1113,6 @@ class ProjectionPlot(PlotBase):
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=''):
@ -1138,10 +1123,6 @@ class ProjectionPlot(PlotBase):
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):
@ -1195,6 +1176,161 @@ class ProjectionPlot(PlotBase):
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
"""
if "filename" in elem.keys():
self.filename = elem.get("filename")
self.color_by = elem.get("color_by")
horizontal_fov = elem.find("horizontal_field_of_view")
if horizontal_fov is not None:
self.horizontal_field_of_view = float(horizontal_fov.text)
if (tmp := elem.find("orthographic_width")) is not None:
self.orthographic_width = float(tmp)
self.pixels = get_elem_tuple(elem, "pixels")
self.camera_position = get_elem_tuple(elem, "camera_position", float)
self.look_at = get_elem_tuple(elem, "look_at", float)
if elem.find("background") is not None:
self.background = get_elem_tuple(elem, "background")
# Set masking information
if (mask_elem := elem.find("mask")) 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:
self.mask_background = tuple(
[int(x) for x in background.split()])
# Set universe level
level = elem.find("level")
if level is not None:
self.level = int(level.text)
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
----------
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
@ -1221,15 +1357,6 @@ class ProjectionPlot(PlotBase):
@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
@property
@ -1266,6 +1393,18 @@ class ProjectionPlot(PlotBase):
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
@ -1275,15 +1414,8 @@ class ProjectionPlot(PlotBase):
XML element containing plot data
"""
element = super().to_xml_element()
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))
element.set("type", "wireframe_raytrace")
subelement = ET.SubElement(element, "wireframe_thickness")
subelement.text = str(self._wireframe_thickness)
@ -1294,6 +1426,8 @@ class ProjectionPlot(PlotBase):
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")
@ -1311,43 +1445,8 @@ class ProjectionPlot(PlotBase):
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
@ -1359,60 +1458,193 @@ class ProjectionPlot(PlotBase):
Returns
-------
openmc.ProjectionPlot
ProjectionPlot object
openmc.WireframeRayTracePlot
WireframeRayTracePlot 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"
plot_name = get_text(elem, 'name', '')
plot = cls(plot_id, plot_name)
plot.type = "wireframe_raytrace"
horizontal_fov = elem.find("horizontal_field_of_view")
if horizontal_fov is not None:
plot.horizontal_field_of_view = float(horizontal_fov.text)
plot._read_xml_attributes(elem)
tmp = elem.find("orthographic_width")
if tmp is not None:
plot.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)
# Attempt to get wireframe thickness.May not be present
wireframe_thickness = elem.find("wireframe_thickness")
if wireframe_thickness is not None:
plot.wireframe_thickness = int(wireframe_thickness.text)
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 = int(color_elem.get("id"))
plot.colors[uid] = tuple(int(i)
for i in get_text(color_elem, 'rgb').split())
plot.xs[uid] = float(color_elem.get("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
----------
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"""
if elem.find('light_position') is not None:
self.light_position = get_elem_tuple(elem, 'light_position', float)
diffuse_fraction = elem.find('diffuse_fraction')
if diffuse_fraction is not None:
self.diffuse_fraction = float(diffuse_fraction.text)
if elem.find('opaque_ids') is not None:
self.opaque_domains = list(get_elem_tuple(elem, 'opaque_ids', int))
@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(elem.get("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 = 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)
plot.colors[uid] = get_elem_tuple(color_elem, "rgb")
return plot
@ -1434,13 +1666,13 @@ class Plots(cv.CheckedList):
Parameters
----------
plots : Iterable of openmc.Plot or openmc.ProjectionPlot
plots : Iterable of openmc.PlotBase
plots to add to the collection
"""
def __init__(self, plots=None):
super().__init__((Plot, ProjectionPlot), 'plots collection')
super().__init__(PlotBase, 'plots collection')
self._plots_file = ET.Element("plots")
if plots is not None:
self += plots
@ -1450,7 +1682,7 @@ class Plots(cv.CheckedList):
Parameters
----------
plot : openmc.Plot or openmc.ProjectionPlot
plot : openmc.PlotBase
Plot to append
"""
@ -1582,10 +1814,14 @@ class Plots(cv.CheckedList):
plots = cls()
for e in elem.findall('plot'):
plot_type = e.get('type')
if plot_type == 'projection':
plots.append(ProjectionPlot.from_xml_element(e))
else:
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 in ('slice', 'voxel'):
plots.append(Plot.from_xml_element(e))
else:
raise ValueError("Unknown plot type: {}".format(plot_type))
return plots
@classmethod

View file

@ -808,15 +808,12 @@ Direction DAGSurface::normal(Position r) const
Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const
{
Expects(p);
p->history().reset_to_last_intersection();
moab::ErrorCode rval;
moab::EntityHandle surf = dagmc_ptr_->entity_by_index(2, dag_index_);
double pnt[3] = {r.x, r.y, r.z};
double dir[3];
rval = dagmc_ptr_->get_angle(surf, pnt, dir, &p->history());
moab::ErrorCode rval =
dagmc_ptr_->get_angle(mesh_handle(), pnt, dir, &p->history());
MB_CHK_ERR_CONT(rval);
p->last_dir() = u.reflect(dir);
return p->last_dir();
return u.reflect(dir);
}
//==============================================================================

View file

@ -75,13 +75,6 @@ double Particle::speed() const
}
}
void Particle::move_distance(double length)
{
for (int j = 0; j < n_coord(); ++j) {
coord(j).r += length * coord(j).u;
}
}
void Particle::create_secondary(
double wgt, Direction u, double E, ParticleType type)
{

View file

@ -15,6 +15,11 @@
namespace openmc {
void GeometryState::mark_as_lost(const char* message)
{
fatal_error(message);
}
void GeometryState::mark_as_lost(const std::string& message)
{
mark_as_lost(message.c_str());
@ -25,11 +30,6 @@ void GeometryState::mark_as_lost(const std::stringstream& message)
mark_as_lost(message.str());
}
void GeometryState::mark_as_lost(const char* message)
{
fatal_error(message);
}
void LocalCoord::rotate(const vector<double>& rotation)
{
r = r.rotate(rotation);
@ -56,6 +56,40 @@ GeometryState::GeometryState()
clear();
}
void GeometryState::advance_to_boundary_from_void()
{
auto root_coord = this->coord(0);
const auto& root_universe = model::universes[model::root_universe];
boundary().reset();
for (auto c_i : root_universe->cells_) {
auto dist =
model::cells.at(c_i)->distance(root_coord.r, root_coord.u, 0, this);
if (dist.first < boundary().distance) {
boundary().distance = dist.first;
boundary().surface = dist.second;
}
}
// if no intersection or near-infinite intersection, reset
// boundary information
if (boundary().distance > 1e300) {
boundary().distance = INFTY;
boundary().surface = SURFACE_NONE;
return;
}
// move the particle up to (and just past) the boundary
move_distance(boundary().distance + TINY_BIT);
}
void GeometryState::move_distance(double length)
{
for (int j = 0; j < n_coord(); ++j) {
coord(j).r += length * coord(j).u;
}
}
ParticleData::ParticleData()
{
zero_delayed_bank();

View file

@ -204,18 +204,21 @@ void read_plots_xml(pugi::xml_node root)
int id = std::stoi(id_string);
if (check_for_node(node, "type")) {
std::string type_str = get_node_value(node, "type", true);
if (type_str == "slice")
if (type_str == "slice") {
model::plots.emplace_back(
std::make_unique<Plot>(node, Plot::PlotType::slice));
else if (type_str == "voxel")
} else if (type_str == "voxel") {
model::plots.emplace_back(
std::make_unique<Plot>(node, Plot::PlotType::voxel));
else if (type_str == "projection")
model::plots.emplace_back(std::make_unique<ProjectionPlot>(node));
else
} else if (type_str == "wireframe_raytrace") {
model::plots.emplace_back(
std::make_unique<WireframeRayTracePlot>(node));
} else if (type_str == "solid_raytrace") {
model::plots.emplace_back(std::make_unique<SolidRayTracePlot>(node));
} else {
fatal_error(
fmt::format("Unsupported plot type '{}' in plot {}", type_str, id));
}
model::plot_map[model::plots.back()->id()] = model::plots.size() - 1;
} else {
fatal_error(fmt::format("Must specify plot type in plot {}", id));
@ -264,8 +267,8 @@ void Plot::create_image() const
}
data(x, y) = colors_[model::material_map[id]];
} // color_by if-else
} // x for loop
} // y for loop
}
}
// draw mesh lines if present
if (index_meshlines_mesh_ >= 0) {
@ -1036,26 +1039,49 @@ RGBColor random_color(void)
int(prn(&model::plotter_seed) * 255), int(prn(&model::plotter_seed) * 255)};
}
ProjectionPlot::ProjectionPlot(pugi::xml_node node) : PlottableInterface(node)
RayTracePlot::RayTracePlot(pugi::xml_node node) : PlottableInterface(node)
{
set_output_path(node);
set_look_at(node);
set_camera_position(node);
set_field_of_view(node);
set_pixels(node);
set_opacities(node);
set_orthographic_width(node);
set_wireframe_thickness(node);
set_wireframe_ids(node);
set_wireframe_color(node);
set_output_path(node);
if (check_for_node(node, "orthographic_width") &&
check_for_node(node, "field_of_view"))
fatal_error("orthographic_width and field_of_view are mutually exclusive "
"parameters.");
// Get centerline vector for camera-to-model. We create vectors around this
// that form a pixel array, and then trace rays along that.
auto up = up_ / up_.norm();
Direction looking_direction = look_at_ - camera_position_;
looking_direction /= looking_direction.norm();
if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9)
fatal_error("Up vector cannot align with vector between camera position "
"and look_at!");
Direction cam_yaxis = looking_direction.cross(up);
cam_yaxis /= cam_yaxis.norm();
Direction cam_zaxis = cam_yaxis.cross(looking_direction);
cam_zaxis /= cam_zaxis.norm();
// Cache the camera-to-model matrix
camera_to_model_ = {looking_direction.x, cam_yaxis.x, cam_zaxis.x,
looking_direction.y, cam_yaxis.y, cam_zaxis.y, looking_direction.z,
cam_yaxis.z, cam_zaxis.z};
}
void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node)
WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node)
: RayTracePlot(node)
{
set_opacities(node);
set_wireframe_thickness(node);
set_wireframe_ids(node);
set_wireframe_color(node);
}
void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node)
{
// Copy plot background color
if (check_for_node(plot_node, "wireframe_color")) {
@ -1068,7 +1094,7 @@ void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node)
}
}
void ProjectionPlot::set_output_path(pugi::xml_node node)
void RayTracePlot::set_output_path(pugi::xml_node node)
{
// Set output file path
std::string filename;
@ -1089,33 +1115,7 @@ void ProjectionPlot::set_output_path(pugi::xml_node node)
path_plot_ = filename;
}
// Advances to the next boundary from outside the geometry
// Returns -1 if no intersection found, and the surface index
// if an intersection was found.
int ProjectionPlot::advance_to_boundary_from_void(GeometryState& p)
{
constexpr double scoot = 1e-5;
double min_dist = {INFINITY};
auto coord = p.coord(0);
Universe* uni = model::universes[model::root_universe].get();
int intersected_surface = -1;
for (auto c_i : uni->cells_) {
auto dist = model::cells.at(c_i)->distance(coord.r, coord.u, 0, &p);
if (dist.first < min_dist) {
min_dist = dist.first;
intersected_surface = dist.second;
}
}
if (min_dist > 1e300)
return -1;
else { // advance the particle
for (int j = 0; j < p.n_coord(); ++j)
p.coord(j).r += (min_dist + scoot) * p.coord(j).u;
return std::abs(intersected_surface);
}
}
bool ProjectionPlot::trackstack_equivalent(
bool WireframeRayTracePlot::trackstack_equivalent(
const std::vector<TrackSegment>& track1,
const std::vector<TrackSegment>& track2) const
{
@ -1125,7 +1125,7 @@ bool ProjectionPlot::trackstack_equivalent(
return false;
for (int i = 0; i < track1.size(); ++i) {
if (track1[i].id != track2[i].id ||
track1[i].surface != track2[i].surface) {
track1[i].surface_index != track2[i].surface_index) {
return false;
}
}
@ -1152,7 +1152,7 @@ bool ProjectionPlot::trackstack_equivalent(
if (t1_i == track1.size() && t2_i == track2.size())
break;
// Check if surface different
if (track1[t1_i].surface != track2[t2_i].surface)
if (track1[t1_i].surface_index != track2[t2_i].surface_index)
return false;
// Pretty sure this should not be used:
@ -1160,7 +1160,7 @@ bool ProjectionPlot::trackstack_equivalent(
// 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)
track1[t1_i - 1].surface_index != track2[t2_i - 1].surface_index)
return false;
// Check if neighboring cells are different
@ -1176,44 +1176,58 @@ bool ProjectionPlot::trackstack_equivalent(
}
}
void ProjectionPlot::create_output() const
std::pair<Position, Direction> RayTracePlot::get_pixel_ray(
int horiz, int vert) const
{
// Get centerline vector for camera-to-model. We create vectors around this
// that form a pixel array, and then trace rays along that.
auto up = up_ / up_.norm();
Direction looking_direction = look_at_ - camera_position_;
looking_direction /= looking_direction.norm();
if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9)
fatal_error("Up vector cannot align with vector between camera position "
"and look_at!");
Direction cam_yaxis = looking_direction.cross(up);
cam_yaxis /= cam_yaxis.norm();
Direction cam_zaxis = cam_yaxis.cross(looking_direction);
cam_zaxis /= cam_zaxis.norm();
// Transformation matrix for directions
std::vector<double> camera_to_model = {looking_direction.x, cam_yaxis.x,
cam_zaxis.x, looking_direction.y, cam_yaxis.y, cam_zaxis.y,
looking_direction.z, cam_yaxis.z, cam_zaxis.z};
// Now we convert to the polar coordinate system with the polar angle
// measuring the angle from the vector up_. Phi is the rotation about up_. For
// now, up_ is hard-coded to be +z.
// Compute field of view in radians
constexpr double DEGREE_TO_RADIAN = M_PI / 180.0;
double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN;
double p0 = static_cast<double>(pixels_[0]);
double p1 = static_cast<double>(pixels_[1]);
double vert_fov_radians = horiz_fov_radians * p1 / p0;
double dphi = horiz_fov_radians / p0;
double dmu = vert_fov_radians / p1;
// focal_plane_dist can be changed to alter the perspective distortion
// effect. This is in units of cm. This seems to look good most of the
// time. TODO let this variable be set through XML.
constexpr double focal_plane_dist = 10.0;
const double dx = 2.0 * focal_plane_dist * std::tan(0.5 * horiz_fov_radians);
const double dy = p1 / p0 * dx;
std::pair<Position, Direction> result;
// Generate the starting position/direction of the ray
if (orthographic_width_ == C_NONE) { // perspective projection
Direction camera_local_vec;
camera_local_vec.x = focal_plane_dist;
camera_local_vec.y = -0.5 * dx + horiz * dx / p0;
camera_local_vec.z = 0.5 * dy - vert * dy / p1;
camera_local_vec /= camera_local_vec.norm();
result.first = camera_position_;
result.second = camera_local_vec.rotate(camera_to_model_);
} else { // orthographic projection
double x_pix_coord = (static_cast<double>(horiz) - p0 / 2.0) / p0;
double y_pix_coord = (static_cast<double>(vert) - p1 / 2.0) / p1;
result.first = camera_position_ +
camera_y_axis() * x_pix_coord * orthographic_width_ +
camera_z_axis() * y_pix_coord * orthographic_width_;
result.second = camera_x_axis();
}
return result;
}
void WireframeRayTracePlot::create_output() const
{
size_t width = pixels_[0];
size_t height = pixels_[1];
ImageData data({width, height}, not_found_);
// This array marks where the initial wireframe was drawn.
// We convolve it with a filter that gets adjusted with the
// wireframe thickness in order to thicken the lines.
// This array marks where the initial wireframe was drawn. We convolve it with
// a filter that gets adjusted with the wireframe thickness in order to
// thicken the lines.
xt::xtensor<int, 2> wireframe_initial({width, height}, 0);
/* Holds all of the track segments for the current rendered line of pixels.
@ -1242,16 +1256,13 @@ void ProjectionPlot::create_output() const
const int n_threads = num_threads();
const int tid = thread_num();
GeometryState p;
p.u() = {1.0, 0.0, 0.0};
int vert = tid;
for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) {
// Save bottom line of current work chunk to compare against later
// I used to have this inside the below if block, but it causes a
// spurious line to be drawn at the bottom of the image. Not sure
// why, but moving it here fixes things.
// Save bottom line of current work chunk to compare against later. This
// used to be inside the below if block, but it causes a spurious line to
// be drawn at the bottom of the image. Not sure why, but moving it here
// fixes things.
if (tid == n_threads - 1)
old_segments = this_line_segments[n_threads - 1];
@ -1259,126 +1270,48 @@ void ProjectionPlot::create_output() const
for (int horiz = 0; horiz < pixels_[0]; ++horiz) {
// Projection mode below decides ray starting conditions
Position init_r;
Direction init_u;
// Generate the starting position/direction of the ray
if (orthographic_width_ == 0.0) { // perspective projection
double this_phi =
-horiz_fov_radians / 2.0 + dphi * horiz + 0.5 * dphi;
double this_mu =
-vert_fov_radians / 2.0 + dmu * vert + M_PI / 2.0 + 0.5 * dmu;
Direction camera_local_vec;
camera_local_vec.x = std::cos(this_phi) * std::sin(this_mu);
camera_local_vec.y = std::sin(this_phi) * std::sin(this_mu);
camera_local_vec.z = std::cos(this_mu);
init_u = camera_local_vec.rotate(camera_to_model);
init_r = camera_position_;
} else { // orthographic projection
init_u = looking_direction;
double x_pix_coord = (static_cast<double>(horiz) - p0 / 2.0) / p0;
double y_pix_coord = (static_cast<double>(vert) - p1 / 2.0) / p0;
init_r = camera_position_;
init_r += cam_yaxis * x_pix_coord * orthographic_width_;
init_r += cam_zaxis * y_pix_coord * orthographic_width_;
}
// Resets internal geometry state of particle
p.init_from_r_u(init_r, init_u);
bool hitsomething = false;
bool intersection_found = true;
int loop_counter = 0;
// RayTracePlot implements camera ray generation
std::pair<Position, Direction> ru = get_pixel_ray(horiz, vert);
this_line_segments[tid][horiz].clear();
ProjectionRay ray(
ru.first, ru.second, *this, this_line_segments[tid][horiz]);
int first_surface =
-1; // surface first passed when entering the model
bool first_inside_model = true; // false after entering the model
while (intersection_found) {
bool inside_cell = false;
int32_t i_surface = p.surface_index();
if (i_surface > 0 &&
model::surfaces[i_surface]->geom_type() == GeometryType::DAG) {
#ifdef DAGMC
int32_t i_cell = next_cell(i_surface,
p.cell_last(p.n_coord() - 1), p.lowest_coord().universe);
inside_cell = i_cell >= 0;
#else
fatal_error(
"Not compiled for DAGMC, but somehow you have a DAGCell!");
#endif
} else {
inside_cell = exhaustive_find_cell(p);
}
if (inside_cell) {
// This allows drawing wireframes with surface intersection
// edges on the model boundary for the same cell.
if (first_inside_model) {
this_line_segments[tid][horiz].emplace_back(
color_by_ == PlotColorBy::mats ? p.material()
: p.lowest_coord().cell,
0.0, first_surface);
first_inside_model = false;
}
hitsomething = true;
intersection_found = true;
auto dist = distance_to_boundary(p);
this_line_segments[tid][horiz].emplace_back(
color_by_ == PlotColorBy::mats ? p.material()
: p.lowest_coord().cell,
dist.distance, std::abs(dist.surface));
// Advance particle
for (int lev = 0; lev < p.n_coord(); ++lev) {
p.coord(lev).r += dist.distance * p.coord(lev).u;
}
p.surface() = dist.surface;
p.n_coord_last() = p.n_coord();
p.n_coord() = dist.coord_level;
if (dist.lattice_translation[0] != 0 ||
dist.lattice_translation[1] != 0 ||
dist.lattice_translation[2] != 0) {
cross_lattice(p, dist);
}
} else {
first_surface = advance_to_boundary_from_void(p);
intersection_found =
first_surface != -1; // -1 if no surface found
}
loop_counter++;
if (loop_counter > MAX_INTERSECTIONS)
fatal_error("Infinite loop in projection plot");
}
ray.trace();
// Now color the pixel based on what we have intersected...
// Loops backwards over intersections.
Position current_color(
not_found_.red, not_found_.green, not_found_.blue);
const auto& segments = this_line_segments[tid][horiz];
for (unsigned i = segments.size(); i-- > 0;) {
// There must be at least two cell intersections to color, front and
// back of the cell. Maybe an infinitely thick cell could be present
// with no back, but why would you want to color that? It's easier to
// just skip that edge case and not even color it.
if (segments.size() <= 1)
continue;
for (int i = segments.size() - 2; i >= 0; --i) {
int colormap_idx = segments[i].id;
RGBColor seg_color = colors_[colormap_idx];
Position seg_color_vec(
seg_color.red, seg_color.green, seg_color.blue);
double mixing = std::exp(-xs_[colormap_idx] * segments[i].length);
double mixing =
std::exp(-xs_[colormap_idx] *
(segments[i + 1].length - segments[i].length));
current_color =
current_color * mixing + (1.0 - mixing) * seg_color_vec;
RGBColor result;
result.red = static_cast<uint8_t>(current_color.x);
result.green = static_cast<uint8_t>(current_color.y);
result.blue = static_cast<uint8_t>(current_color.z);
data(horiz, vert) = result;
}
// save result converting from double-precision color coordinates to
// byte-sized
RGBColor result;
result.red = static_cast<uint8_t>(current_color.x);
result.green = static_cast<uint8_t>(current_color.y);
result.blue = static_cast<uint8_t>(current_color.z);
data(horiz, vert) = result;
// Check to draw wireframe in horizontal direction. No inter-thread
// comm.
if (horiz > 0) {
@ -1451,9 +1384,8 @@ void ProjectionPlot::create_output() const
#endif
}
void ProjectionPlot::print_info() const
void RayTracePlot::print_info() const
{
fmt::print("Plot Type: Projection\n");
fmt::print("Camera position: {} {} {}\n", camera_position_.x,
camera_position_.y, camera_position_.z);
fmt::print("Look at: {} {} {}\n", look_at_.x, look_at_.y, look_at_.z);
@ -1462,7 +1394,13 @@ void ProjectionPlot::print_info() const
fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]);
}
void ProjectionPlot::set_opacities(pugi::xml_node node)
void WireframeRayTracePlot::print_info() const
{
fmt::print("Plot Type: Wireframe ray-traced\n");
RayTracePlot::print_info();
}
void WireframeRayTracePlot::set_opacities(pugi::xml_node node)
{
xs_.resize(colors_.size(), 1e6); // set to large value for opaque by default
@ -1492,7 +1430,7 @@ void ProjectionPlot::set_opacities(pugi::xml_node node)
}
}
void ProjectionPlot::set_orthographic_width(pugi::xml_node node)
void RayTracePlot::set_orthographic_width(pugi::xml_node node)
{
if (check_for_node(node, "orthographic_width")) {
double orthographic_width =
@ -1503,7 +1441,7 @@ void ProjectionPlot::set_orthographic_width(pugi::xml_node node)
}
}
void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node)
void WireframeRayTracePlot::set_wireframe_thickness(pugi::xml_node node)
{
if (check_for_node(node, "wireframe_thickness")) {
int wireframe_thickness =
@ -1514,7 +1452,7 @@ void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node)
}
}
void ProjectionPlot::set_wireframe_ids(pugi::xml_node node)
void WireframeRayTracePlot::set_wireframe_ids(pugi::xml_node node)
{
if (check_for_node(node, "wireframe_ids")) {
wireframe_ids_ = get_node_array<int>(node, "wireframe_ids");
@ -1529,7 +1467,7 @@ void ProjectionPlot::set_wireframe_ids(pugi::xml_node node)
std::sort(wireframe_ids_.begin(), wireframe_ids_.end());
}
void ProjectionPlot::set_pixels(pugi::xml_node node)
void RayTracePlot::set_pixels(pugi::xml_node node)
{
vector<int> pxls = get_node_array<int>(node, "pixels");
if (pxls.size() != 2)
@ -1539,19 +1477,19 @@ void ProjectionPlot::set_pixels(pugi::xml_node node)
pixels_[1] = pxls[1];
}
void ProjectionPlot::set_camera_position(pugi::xml_node node)
void RayTracePlot::set_camera_position(pugi::xml_node node)
{
vector<double> camera_pos = get_node_array<double>(node, "camera_position");
if (camera_pos.size() != 3) {
fatal_error(
fmt::format("look_at element must have three floating point values"));
fatal_error(fmt::format(
"camera_position element must have three floating point values"));
}
camera_position_.x = camera_pos[0];
camera_position_.y = camera_pos[1];
camera_position_.z = camera_pos[2];
}
void ProjectionPlot::set_look_at(pugi::xml_node node)
void RayTracePlot::set_look_at(pugi::xml_node node)
{
vector<double> look_at = get_node_array<double>(node, "look_at");
if (look_at.size() != 3) {
@ -1562,7 +1500,7 @@ void ProjectionPlot::set_look_at(pugi::xml_node node)
look_at_.z = look_at[2];
}
void ProjectionPlot::set_field_of_view(pugi::xml_node node)
void RayTracePlot::set_field_of_view(pugi::xml_node node)
{
// Defaults to 70 degree horizontal field of view (see .h file)
if (check_for_node(node, "field_of_view")) {
@ -1576,6 +1514,352 @@ void ProjectionPlot::set_field_of_view(pugi::xml_node node)
}
}
SolidRayTracePlot::SolidRayTracePlot(pugi::xml_node node) : RayTracePlot(node)
{
set_opaque_ids(node);
set_diffuse_fraction(node);
set_light_position(node);
}
void SolidRayTracePlot::print_info() const
{
fmt::print("Plot Type: Solid ray-traced\n");
RayTracePlot::print_info();
}
void SolidRayTracePlot::create_output() const
{
size_t width = pixels_[0];
size_t height = pixels_[1];
ImageData data({width, height}, not_found_);
#pragma omp parallel for schedule(dynamic) collapse(2)
for (int horiz = 0; horiz < pixels_[0]; ++horiz) {
for (int vert = 0; vert < pixels_[1]; ++vert) {
// RayTracePlot implements camera ray generation
std::pair<Position, Direction> ru = get_pixel_ray(horiz, vert);
PhongRay ray(ru.first, ru.second, *this);
ray.trace();
data(horiz, vert) = ray.result_color();
}
}
#ifdef USE_LIBPNG
output_png(path_plot(), data);
#else
output_ppm(path_plot(), data);
#endif
}
void SolidRayTracePlot::set_opaque_ids(pugi::xml_node node)
{
if (check_for_node(node, "opaque_ids")) {
auto opaque_ids_tmp = get_node_array<int>(node, "opaque_ids");
// It is read in as actual ID values, but we have to convert to indices in
// mat/cell array
for (auto& x : opaque_ids_tmp)
x = color_by_ == PlotColorBy::mats ? model::material_map[x]
: model::cell_map[x];
opaque_ids_.insert(opaque_ids_tmp.begin(), opaque_ids_tmp.end());
}
}
void SolidRayTracePlot::set_light_position(pugi::xml_node node)
{
if (check_for_node(node, "light_position")) {
auto light_pos_tmp = get_node_array<double>(node, "light_position");
if (light_pos_tmp.size() != 3)
fatal_error("Light position must be given as 3D coordinates");
light_location_.x = light_pos_tmp[0];
light_location_.y = light_pos_tmp[1];
light_location_.z = light_pos_tmp[2];
} else {
light_location_ = camera_position();
}
}
void SolidRayTracePlot::set_diffuse_fraction(pugi::xml_node node)
{
if (check_for_node(node, "diffuse_fraction")) {
diffuse_fraction_ = std::stod(get_node_value(node, "diffuse_fraction"));
if (diffuse_fraction_ < 0.0 || diffuse_fraction_ > 1.0) {
fatal_error("Must have 0 <= diffuse fraction <= 1");
}
}
}
void Ray::compute_distance()
{
boundary() = distance_to_boundary(*this);
}
void Ray::trace()
{
// To trace the ray from its origin all the way through the model, we have
// to proceed in two phases. In the first, the ray may or may not be found
// inside the model. If the ray is already in the model, phase one can be
// skipped. Otherwise, the ray has to be advanced to the boundary of the
// model where all the cells are defined. Importantly, this is assuming that
// the model is convex, which is a very reasonable assumption for any
// radiation transport model.
//
// After phase one is done, we can starting tracing from cell to cell within
// the model. This step can use neighbor lists to accelerate the ray tracing.
// Attempt to initialize the particle. We may have to enter a loop to move
// it up to the edge of the model.
bool inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10);
// Advance to the boundary of the model
while (!inside_cell) {
advance_to_boundary_from_void();
inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10);
// If true this means no surface was intersected. See cell.cpp and search
// for numeric_limits to see where we return it.
if (surface() == std::numeric_limits<int>::max()) {
warning(fmt::format("Lost a ray, r = {}, u = {}", r(), u()));
return;
}
// Exit this loop and enter into cell-to-cell ray tracing (which uses
// neighbor lists)
if (inside_cell)
break;
// if there is no intersection with the model, we're done
if (boundary().surface == SURFACE_NONE)
return;
event_counter_++;
if (event_counter_ > MAX_INTERSECTIONS) {
warning("Likely infinite loop in ray traced plot");
return;
}
}
// Call the specialized logic for this type of ray. This is for the
// intersection for the first intersection if we had one.
if (boundary().surface != SURFACE_NONE) {
// set the geometry state's surface attribute to be used for
// surface normal computation
surface() = boundary().surface;
on_intersection();
if (stop_)
return;
}
// reset surface attribute to zero after the first intersection so that it
// doesn't perturb surface crossing logic from here on out
surface() = 0;
// This is the ray tracing loop within the model. It exits after exiting
// the model, which is equivalent to assuming that the model is convex.
// It would be nice to factor out the on_intersection at the end of this
// loop and then do "while (inside_cell)", but we can't guarantee it's
// on a surface in that case. There might be some other way to set it
// up that is perhaps a little more elegant, but this is what works just
// fine.
while (true) {
compute_distance();
// There are no more intersections to process
// if we hit the edge of the model, so stop
// the particle in that case. Also, just exit
// if a negative distance was somehow computed.
if (boundary().distance == INFTY || boundary().distance == INFINITY ||
boundary().distance < 0) {
return;
}
// See below comment where call_on_intersection is checked in an
// if statement for an explanation of this.
bool call_on_intersection {true};
if (boundary().distance < 10 * TINY_BIT) {
call_on_intersection = false;
}
// DAGMC surfaces expect us to go a little bit further than the advance
// distance to properly check cell inclusion.
boundary().distance += TINY_BIT;
// Advance particle, prepare for next intersection
for (int lev = 0; lev < n_coord(); ++lev) {
coord(lev).r += boundary().distance * coord(lev).u;
}
surface() = boundary().surface;
n_coord_last() = n_coord();
n_coord() = boundary().coord_level;
if (boundary().lattice_translation[0] != 0 ||
boundary().lattice_translation[1] != 0 ||
boundary().lattice_translation[2] != 0) {
cross_lattice(*this, boundary(), settings::verbosity >= 10);
}
// Record how far the ray has traveled
traversal_distance_ += boundary().distance;
inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10);
// Call the specialized logic for this type of ray. Note that we do not
// call this if the advance distance is very small. Unfortunately, it seems
// darn near impossible to get the particle advanced to the model boundary
// and through it without sometimes accidentally calling on_intersection
// twice. This incorrectly shades the region as occluded when it might not
// actually be. By screening out intersection distances smaller than a
// threshold 10x larger than the scoot distance used to advance up to the
// model boundary, we can avoid that situation.
if (call_on_intersection) {
on_intersection();
if (stop_)
return;
}
if (!inside_cell)
return;
event_counter_++;
if (event_counter_ > MAX_INTERSECTIONS) {
warning("Likely infinite loop in ray traced plot");
return;
}
}
}
void ProjectionRay::on_intersection()
{
// This records a tuple with the following info
//
// 1) ID (material or cell depending on color_by_)
// 2) Distance traveled by the ray through that ID
// 3) Index of the intersected surface (starting from 1)
line_segments_.emplace_back(
plot_.color_by_ == PlottableInterface::PlotColorBy::mats
? material()
: lowest_coord().cell,
traversal_distance_, boundary().surface_index());
}
void PhongRay::on_intersection()
{
// Check if we hit an opaque material or cell
int hit_id = plot_.color_by_ == PlottableInterface::PlotColorBy::mats
? material()
: lowest_coord().cell;
// If we are reflected and have advanced beyond the camera,
// the ray is done. This is checked here because we should
// kill the ray even if the material is not opaque.
if (reflected_ && (r() - plot_.camera_position()).dot(u()) >= 0.0) {
stop();
return;
}
// Anything that's not opaque has zero impact on the plot.
if (plot_.opaque_ids_.find(hit_id) == plot_.opaque_ids_.end())
return;
if (!reflected_) {
// reflect the particle and set the color to be colored by
// the normal or the diffuse lighting contribution
reflected_ = true;
result_color_ = plot_.colors_[hit_id];
Direction to_light = plot_.light_location_ - r();
to_light /= to_light.norm();
// TODO
// Not sure what can cause a surface token to be invalid here, although it
// sometimes happens for a few pixels. It's very very rare, so proceed by
// coloring the pixel with the overlap color. It seems to happen only for a
// few pixels on the outer boundary of a hex lattice.
//
// We cannot detect it in the outer loop, and it only matters here, so
// that's why the error handling is a little different than for a lost
// ray.
if (surface() == 0) {
result_color_ = plot_.overlap_color_;
stop();
return;
}
// Get surface pointer
const auto& surf = model::surfaces.at(surface_index());
Direction normal = surf->normal(r_local());
normal /= normal.norm();
// Need to apply translations to find the normal vector in
// the base level universe's coordinate system.
for (int lev = n_coord() - 2; lev >= 0; --lev) {
if (coord(lev + 1).rotated) {
const Cell& c {*model::cells[coord(lev).cell]};
normal = normal.inverse_rotate(c.rotation_);
}
}
// use the normal opposed to the ray direction
if (normal.dot(u()) > 0.0) {
normal *= -1.0;
}
// Facing away from the light means no lighting
double dotprod = normal.dot(to_light);
dotprod = std::max(0.0, dotprod);
double modulation =
plot_.diffuse_fraction_ + (1.0 - plot_.diffuse_fraction_) * dotprod;
result_color_ *= modulation;
// Now point the particle to the camera. We now begin
// checking to see if it's occluded by another surface
u() = to_light;
orig_hit_id_ = hit_id;
// OpenMC native CSG and DAGMC surfaces have some slight differences
// in how they interpret particles that are sitting on a surface.
// I don't know exactly why, but this makes everything work beautifully.
if (surf->geom_type() == GeometryType::DAG) {
surface() = 0;
} else {
surface() = -surface(); // go to other side
}
// Must fully restart coordinate search. Why? Not sure.
clear();
// Note this could likely be faster if we cached the previous
// cell we were in before the reflection. This is the easiest
// way to fully initialize all the sub-universe coordinates and
// directions though.
bool found = exhaustive_find_cell(*this);
if (!found) {
fatal_error("Lost particle after reflection.");
}
// Must recalculate distance to boundary due to the
// direction change
compute_distance();
} else {
// If it's not facing the light, we color with the diffuse contribution, so
// next we check if we're going to occlude the last reflected surface. if
// so, color by the diffuse contribution instead
if (orig_hit_id_ == -1)
fatal_error("somehow a ray got reflected but not original ID set?");
result_color_ = plot_.colors_[orig_hit_id_];
result_color_ *= plot_.diffuse_fraction_;
stop();
}
}
extern "C" int openmc_id_map(const void* plot, int32_t* data_out)
{

View file

@ -75,13 +75,6 @@ Position Position::operator-() const
return {-x, -y, -z};
}
Position Position::rotate(const vector<double>& rotation) const
{
return {x * rotation[0] + y * rotation[1] + z * rotation[2],
x * rotation[3] + y * rotation[4] + z * rotation[5],
x * rotation[6] + y * rotation[7] + z * rotation[8]};
}
std::ostream& operator<<(std::ostream& os, Position r)
{
os << "(" << r.x << ", " << r.y << ", " << r.z << ")";

View file

@ -141,7 +141,7 @@ Direction Surface::reflect(Position r, Direction u, GeometryState* p) const
}
Direction Surface::diffuse_reflect(
Position r, Direction u, uint64_t* seed, GeometryState* p) const
Position r, Direction u, uint64_t* seed) const
{
// Diffuse reflect direction according to the normal.
// cosine distribution

View file

@ -1,7 +1,7 @@
<?xml version="1.0"?>
<plots>
<plot id="1" type="projection">
<plot id="1" type="wireframe_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>20. 20. 20.</camera_position>
<pixels>200 200</pixels>
@ -11,7 +11,7 @@
<field_of_view>70</field_of_view>
</plot>
<plot id="2" type="projection">
<plot id="2" type="wireframe_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 0.</camera_position>
<width>25 25</width>
@ -22,7 +22,7 @@
<filename>example1</filename>
</plot>
<plot id="3" color_by="material" type="projection">
<plot id="3" color_by="material" type="wireframe_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>20. 20. 20.</camera_position>
<pixels>200 200</pixels>
@ -31,7 +31,7 @@
<wireframe_ids>2</wireframe_ids>
</plot>
<plot id="4" color_by="material" type="projection">
<plot id="4" color_by="material" type="wireframe_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>0. 10.0 20.</camera_position>
<pixels>200 200</pixels>
@ -39,7 +39,7 @@
<filename>example3.png</filename>
</plot>
<plot id="5" type="projection">
<plot id="5" type="wireframe_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 10.</camera_position>
<width>25 25</width>
@ -52,4 +52,39 @@
<color id="3" rgb="255 0 0" xs="1.0"/>
</plot>
<plot id="6" color_by="material" type="solid_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 10.</camera_position>
<pixels>200 200</pixels>
<filename>phong.png</filename>
<opaque_ids>1 3</opaque_ids>
<color id="1" rgb="0 0 255"/>
<color id="2" rgb="0 255 0"/>
<color id="3" rgb="255 0 100"/>
</plot>
<plot id="7" color_by="material" type="solid_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 10.</camera_position>
<diffuse_fraction>0.5</diffuse_fraction>
<pixels>200 200</pixels>
<filename>phong_diffuse.png</filename>
<opaque_ids>1 3</opaque_ids>
<color id="1" rgb="0 0 255"/>
<color id="2" rgb="0 255 0"/>
<color id="3" rgb="255 0 100"/>
</plot>
<plot id="8" color_by="material" type="solid_raytrace">
<look_at>0. 0. 0.</look_at>
<camera_position>10. 10. 10.</camera_position>
<light_position>0. 10. 10.</light_position>
<pixels>200 200</pixels>
<filename>phong_move_light.png</filename>
<opaque_ids>1 3</opaque_ids>
<color id="1" rgb="0 0 255"/>
<color id="2" rgb="0 255 0"/>
<color id="3" rgb="255 0 100"/>
</plot>
</plots>

View file

@ -1 +1 @@
24fb0f41ee018ea086962dbd6bcd0b536d11d4b34644bfef4f0e74f8b462fe41a84af39c7ff79046d5d7cfe209084eac54712fa0ec01038e97eb43df1abd0334
025804f1522eafd6e0e9566ce6b9b5603962f278de222c842fe3e06471290bb575676255bcd55e4f084bdcca4ee56d3c219827cb1ef2b5c3a90f7666986b55e9

View file

@ -2,5 +2,8 @@ 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 = PlotTestHarness(('plot_1.png', 'example1.png', 'example2.png',
'example3.png', 'orthographic_example1.png',
'phong.png', 'phong_diffuse.png',
'phong_move_light.png'))
harness.main()

View file

@ -4,6 +4,8 @@ import openmc
import openmc.examples
import pytest
from openmc.plots import _SVG_COLORS
@pytest.fixture(scope='module')
def myplot():
@ -42,7 +44,7 @@ def myplot():
@pytest.fixture(scope='module')
def myprojectionplot():
plot = openmc.ProjectionPlot(name='myprojectionplot')
plot = openmc.WireframeRayTracePlot(name='myprojectionplot')
plot.look_at = (0.0, 0.0, 0.0)
plot.camera_position = (4.0, 3.0, 0.0)
plot.pixels = (500, 500)
@ -118,6 +120,31 @@ def test_repr_proj(myprojectionplot):
assert isinstance(r, str)
def test_projection_plot_roundtrip(myprojectionplot):
elem = myprojectionplot.to_xml_element()
xml_plot = openmc.WireframeRayTracePlot.from_xml_element(elem)
svg_colors = _SVG_COLORS
assert xml_plot.name == myprojectionplot.name
assert xml_plot.look_at == myprojectionplot.look_at
assert xml_plot.camera_position == myprojectionplot.camera_position
assert xml_plot.pixels == myprojectionplot.pixels
assert xml_plot.filename == myprojectionplot.filename
assert xml_plot.background == svg_colors[myprojectionplot.background]
assert xml_plot.color_by == myprojectionplot.color_by
expected_colors = {m.id: svg_colors[c] for m, c in myprojectionplot.colors.items()}
assert xml_plot.colors == expected_colors
# TODO: needs geometry information
# assert xml_plot.mask_components == myprojectionplot.mask_components
assert xml_plot.mask_background == svg_colors[myprojectionplot.mask_background]
# assert xml_plot.overlap_color == svg_colors[myprojectionplot.overlap_color]
assert xml_plot.wireframe_thickness == myprojectionplot.wireframe_thickness
assert xml_plot.level == myprojectionplot.level
def test_from_geometry():
width = 25.
s = openmc.Sphere(r=width/2, boundary_type='vacuum')
@ -182,7 +209,7 @@ def test_plots(run_in_tmpdir):
plots = openmc.Plots([p1, p2])
assert len(plots) == 2
p3 = openmc.ProjectionPlot(name='plot3')
p3 = openmc.WireframeRayTracePlot(name='plot3')
plots = openmc.Plots([p1, p2, p3])
assert len(plots) == 3
@ -223,6 +250,41 @@ def test_voxel_plot_roundtrip():
assert new_plot.color_by == plot.color_by
def test_phong_plot_roundtrip():
plot = openmc.SolidRayTracePlot(name='my phong plot')
plot.id = 2300
plot.filename = 'phong1'
plot.pixels = (50, 50)
plot.look_at = (11., 12., 13.)
plot.camera_position = (22., 23., 24.)
plot.diffuse_fraction = 0.5
plot.horizontal_field_of_view = 90.0
plot.color_by = 'material'
plot.light_position = (8., 9., 10.)
plot.opaque_domains = [6, 7, 8]
elem = plot.to_xml_element()
repr(plot)
new_plot = openmc.SolidRayTracePlot.from_xml_element(elem)
assert new_plot.name == plot.name
assert new_plot.id == plot.id
assert new_plot.filename == plot.filename
assert new_plot.pixels == plot.pixels
assert new_plot.look_at == plot.look_at
assert new_plot.camera_position == plot.camera_position
assert new_plot.diffuse_fraction == plot.diffuse_fraction
assert new_plot.horizontal_field_of_view == plot.horizontal_field_of_view
assert new_plot.color_by == plot.color_by
assert new_plot.light_position == plot.light_position
assert new_plot.opaque_domains == plot.opaque_domains
# ensure the new object is valid to re-write to XML
new_elem = new_plot.to_xml_element()
def test_plot_directory(run_in_tmpdir):
pwr_pin = openmc.examples.pwr_pin_cell()