Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PointCloud spatial distribution #3161

Merged
merged 40 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c427b88
Add PointCloud spatial distribution
gonuke Oct 5, 2024
93afabf
clang-format
gonuke Oct 5, 2024
954191d
PointCloud should be handled like a standard IndependentSource in set…
gonuke Oct 5, 2024
2077a35
clean up reference to mesh info in PointCloud
gonuke Oct 5, 2024
74532a7
missing commas
gonuke Oct 5, 2024
0c78f39
add missing function declaration
gonuke Oct 5, 2024
a50d7af
dumb typo
gonuke Oct 5, 2024
733337d
remove stale enumeration
gonuke Oct 5, 2024
3138b92
another dumb typo
gonuke Oct 5, 2024
27ce9fc
testing object equivalence is invalid
gonuke Oct 5, 2024
bbb2d4b
add input stream operator for Position
gonuke Oct 11, 2024
3f6e5e3
update XML of list of positions to be flattened list of coords
gonuke Oct 11, 2024
3a5a9e6
fix constructor arguments
gonuke Oct 11, 2024
c016666
linting
gonuke Oct 11, 2024
f8933d7
back away from istream operator for Position
gonuke Oct 12, 2024
85663d3
I forgot to store the position data
gonuke Oct 12, 2024
d8a0fd7
Create a new XML function for reading a flat set of Positions from an…
pshriwise Oct 15, 2024
2c7eb6c
Adding check for source strengths based using sampled source
pshriwise Oct 15, 2024
7f2b39a
C++ formatting
pshriwise Oct 15, 2024
3641f99
PEP8
pshriwise Oct 15, 2024
5fafc7d
Use a simpler model to cut down on initialization time. Increase tole…
pshriwise Oct 16, 2024
ed3b1d3
grammar fix
gonuke Oct 17, 2024
f4b5901
improve comment grammar
gonuke Oct 17, 2024
3dd55a7
capitalization standard
gonuke Oct 17, 2024
d9ca528
remove extra blank line
gonuke Oct 17, 2024
ea462a9
improve error message
gonuke Oct 17, 2024
3487966
only get coords once with new XML function
gonuke Oct 21, 2024
cbe2058
remove extra blank line for formatting
gonuke Oct 21, 2024
8c7aa8a
allow existing variable checking to throw error
gonuke Oct 21, 2024
3528f3d
remove this file from the PR
gonuke Oct 21, 2024
8ab1dbb
add point cloud to settings docs and add missing mesh spatial info
gonuke Oct 21, 2024
b8d7f7f
clarify that volume_normalized is optional and default false
gonuke Oct 21, 2024
43efcf4
Update docs/source/io_formats/settings.rst
gonuke Oct 21, 2024
25c4f63
Refactor PointCloud constructor a bit
pshriwise Oct 21, 2024
b6c5c9e
Save a live; save a life
pshriwise Oct 21, 2024
603131f
Updatest test_point_cloud to catch error
paulromano Nov 13, 2024
8bb6dd7
Simplify PointCloud C++ implementation
paulromano Nov 13, 2024
8462db7
Update docs
paulromano Nov 13, 2024
42eb127
Make fixes and add typing in PointCloud Python class
paulromano Nov 13, 2024
66c57d1
Add test for invalid input to PointCloud
paulromano Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions docs/source/io_formats/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -579,24 +579,38 @@ attributes/sub-elements:

:type:
The type of spatial distribution. Valid options are "box", "fission",
"point", "cartesian", "cylindrical", and "spherical". A "box" spatial
distribution has coordinates sampled uniformly in a parallelepiped. A
"fission" spatial distribution samples locations from a "box"
"point", "cartesian", "cylindrical", "spherical", "mesh", and "cloud".

A "box" spatial distribution has coordinates sampled uniformly in a
parallelepiped.

A "fission" spatial distribution samples locations from a "box"
distribution but only locations in fissionable materials are accepted.

A "point" spatial distribution has coordinates specified by a triplet.

A "cartesian" spatial distribution specifies independent distributions of
x-, y-, and z-coordinates. A "cylindrical" spatial distribution specifies
independent distributions of r-, phi-, and z-coordinates where phi is the
azimuthal angle and the origin for the cylindrical coordinate system is
specified by origin. A "spherical" spatial distribution specifies
independent distributions of r-, cos_theta-, and phi-coordinates where
cos_theta is the cosine of the angle with respect to the z-axis, phi is
the azimuthal angle, and the sphere is centered on the coordinate
(x0,y0,z0). A "mesh" spatial distribution samples source sites from a mesh element
x-, y-, and z-coordinates.

A "cylindrical" spatial distribution specifies independent distributions
of r-, phi-, and z-coordinates where phi is the azimuthal angle and the
origin for the cylindrical coordinate system is specified by origin.

A "spherical" spatial distribution specifies independent distributions of
r-, cos_theta-, and phi-coordinates where cos_theta is the cosine of the
angle with respect to the z-axis, phi is the azimuthal angle, and the
sphere is centered on the coordinate (x0,y0,z0).

A "mesh" spatial distribution samples source sites from a mesh element
based on the relative strengths provided in the node. Source locations
within an element are sampled isotropically. If no strengths are provided,
the space within the mesh is uniformly sampled.

A "cloud" spatial distribution samples source sites from a list of spatial
positions provided in the node, based on the relative strengths provided
in the node. If no strengths are provided, the positions are uniformly
sampled.

*Default*: None

:parameters:
Expand Down Expand Up @@ -662,6 +676,26 @@ attributes/sub-elements:
For "cylindrical and "spherical" distributions, this element specifies
the coordinates for the origin of the coordinate system.

:mesh_id:
For "mesh" spatial distributions, this element specifies which mesh ID to
use for the geometric description of the mesh.

:coords:
For "cloud" distributions, this element specifies a list of coordinates
for each of the points in the cloud.

:strengths:
For "mesh" and "cloud" spatial distributions, this element specifies the
relative source strength of each mesh element or each point in the cloud.

:volume_normalized:
For "mesh" spatial distrubtions, this optional boolean element specifies
whether the vector of relative strengths should be multiplied by the mesh
element volume. This is most common if the strengths represent a source
per unit volume.

*Default*: false

:angle:
An element specifying the angular distribution of source sites. This element
has the following attributes:
Expand Down
1 change: 1 addition & 0 deletions docs/source/pythonapi/stats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Spatial Distributions
openmc.stats.Box
openmc.stats.Point
openmc.stats.MeshSpatial
openmc.stats.PointCloud

.. autosummary::
:toctree: generated
Expand Down
4 changes: 3 additions & 1 deletion docs/source/usersguide/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ distributions using spherical or cylindrical coordinates, you can use
:class:`openmc.stats.SphericalIndependent` or
:class:`openmc.stats.CylindricalIndependent`, respectively. Meshes can also be
used to represent spatial distributions with :class:`openmc.stats.MeshSpatial`
by specifying a mesh and source strengths for each mesh element.
by specifying a mesh and source strengths for each mesh element. It is also
possible to define a "cloud" of source points, each with a different relative
probability, using :class:`openmc.stats.PointCloud`.

The angular distribution can be set equal to a sub-class of
:class:`openmc.stats.UnitSphere` such as :class:`openmc.stats.Isotropic`,
Expand Down
20 changes: 20 additions & 0 deletions include/openmc/distribution_spatial.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ class MeshSpatial : public SpatialDistribution {
//!< mesh element indices
};

//==============================================================================
//! Distribution of points
//==============================================================================

class PointCloud : public SpatialDistribution {
public:
explicit PointCloud(pugi::xml_node node);
explicit PointCloud(
std::vector<Position> point_cloud, gsl::span<const double> strengths);

//! Sample a position from the distribution
//! \param seed Pseudorandom number seed pointer
//! \return Sampled position
Position sample(uint64_t* seed) const override;

private:
std::vector<Position> point_cloud_;
DiscreteIndex point_idx_dist_; //!< Distribution of Position indices
};

//==============================================================================
//! Uniform distribution of points over a box
//==============================================================================
Expand Down
3 changes: 3 additions & 0 deletions include/openmc/xml_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ xt::xarray<T> get_node_xarray(
return xt::adapt(v, shape);
}

std::vector<Position> get_node_position_array(
pugi::xml_node node, const char* name, bool lowercase = false);

Position get_node_position(
pugi::xml_node node, const char* name, bool lowercase = false);

Expand Down
114 changes: 114 additions & 0 deletions openmc/stats/multivariate.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ def from_xml_element(cls, elem, meshes=None):
return Point.from_xml_element(elem)
elif distribution == 'mesh':
return MeshSpatial.from_xml_element(elem, meshes)
elif distribution == 'cloud':
return PointCloud.from_xml_element(elem)


class CartesianIndependent(Spatial):
Expand Down Expand Up @@ -756,6 +758,118 @@ def from_xml_element(cls, elem, meshes):
return cls(meshes[mesh_id], strengths, volume_normalized)


class PointCloud(Spatial):
"""Spatial distribution from a point cloud.

This distribution specifies a discrete list of points, with corresponding
relative probabilities.

.. versionadded:: 0.15.1

Parameters
----------
positions : iterable of 3-tuples
gonuke marked this conversation as resolved.
Show resolved Hide resolved
The points in space to be sampled
strengths : iterable of float, optional
An iterable of values that represents the relative probabilty of each
point.

Attributes
----------
positions : numpy.ndarray
The points in space to be sampled with shape (N, 3)
strengths : numpy.ndarray or None
An array of relative probabilities for each mesh point
"""

def __init__(
self,
positions: Sequence[Sequence[float]],
strengths: Sequence[float] | None = None
):
self.positions = positions
self.strengths = strengths

@property
def positions(self) -> np.ndarray:
return self._positions

@positions.setter
def positions(self, positions):
positions = np.array(positions, dtype=float)
if positions.ndim != 2:
raise ValueError('positions must be a 2D array')
elif positions.shape[1] != 3:
raise ValueError('Each position must have 3 values')
self._positions = positions

@property
def strengths(self) -> np.ndarray:
return self._strengths

@strengths.setter
def strengths(self, strengths):
if strengths is not None:
strengths = np.array(strengths, dtype=float)
if strengths.ndim != 1:
raise ValueError('strengths must be a 1D array')
elif strengths.size != self.positions.shape[0]:
raise ValueError('strengths must have the same length as positions')
self._strengths = strengths

@property
def num_strength_bins(self) -> int:
if self.strengths is None:
raise ValueError('Strengths are not set')
return self.strengths.size

def to_xml_element(self) -> ET.Element:
"""Return XML representation of the spatial distribution

Returns
-------
element : lxml.etree._Element
XML element containing spatial distribution data

"""
element = ET.Element('space')
element.set('type', 'cloud')

subelement = ET.SubElement(element, 'coords')
pshriwise marked this conversation as resolved.
Show resolved Hide resolved
subelement.text = ' '.join(str(e) for e in self.positions.flatten())

if self.strengths is not None:
subelement = ET.SubElement(element, 'strengths')
subelement.text = ' '.join(str(e) for e in self.strengths)

return element

@classmethod
def from_xml_element(cls, elem: ET.Element) -> PointCloud:
"""Generate spatial distribution from an XML element

Parameters
----------
elem : lxml.etree._Element
XML element

Returns
-------
openmc.stats.PointCloud
Spatial distribution generated from XML element


"""
coord_data = get_text(elem, 'coords')
positions = np.array([float(b) for b in coord_data.split()]).reshape((-1, 3))

strengths = get_text(elem, 'strengths')
if strengths is not None:
strengths = [float(b) for b in strengths.split()]

return cls(positions, strengths)


class Box(Spatial):
"""Uniform distribution of coordinates in a rectangular cuboid.

Expand Down
45 changes: 45 additions & 0 deletions src/distribution_spatial.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ unique_ptr<SpatialDistribution> SpatialDistribution::create(pugi::xml_node node)
return UPtrSpace {new SphericalIndependent(node)};
} else if (type == "mesh") {
return UPtrSpace {new MeshSpatial(node)};
} else if (type == "cloud") {
return UPtrSpace {new PointCloud(node)};
} else if (type == "box") {
return UPtrSpace {new SpatialBox(node)};
} else if (type == "fission") {
Expand Down Expand Up @@ -298,6 +300,49 @@ Position MeshSpatial::sample(uint64_t* seed) const
return this->sample_mesh(seed).second;
}

//==============================================================================
// PointCloud implementation
//==============================================================================

PointCloud::PointCloud(pugi::xml_node node)
{
if (check_for_node(node, "coords")) {
point_cloud_ = get_node_position_array(node, "coords");
} else {
fatal_error("No coordinates were provided for the PointCloud "
"spatial distribution");
}

std::vector<double> strengths;

if (check_for_node(node, "strengths"))
strengths = get_node_array<double>(node, "strengths");
else
strengths.resize(point_cloud_.size(), 1.0);

if (strengths.size() != point_cloud_.size()) {
fatal_error(
fmt::format("Number of entries for the strengths array {} does "
"not match the number of spatial points provided {}.",
strengths.size(), point_cloud_.size()));
}

point_idx_dist_.assign(strengths);
}

PointCloud::PointCloud(
std::vector<Position> point_cloud, gsl::span<const double> strengths)
{
point_cloud_.assign(point_cloud.begin(), point_cloud.end());
point_idx_dist_.assign(strengths);
}

Position PointCloud::sample(uint64_t* seed) const
{
int32_t index = point_idx_dist_.sample(seed);
return point_cloud_[index];
}

//==============================================================================
// SpatialBox implementation
//==============================================================================
Expand Down
19 changes: 19 additions & 0 deletions src/xml_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "openmc/error.h"
#include "openmc/string_utils.h"
#include "openmc/vector.h"

namespace openmc {

Expand Down Expand Up @@ -48,6 +49,24 @@ bool get_node_value_bool(pugi::xml_node node, const char* name)
return false;
}

vector<Position> get_node_position_array(
pugi::xml_node node, const char* name, bool lowercase)
{
vector<double> coords = get_node_array<double>(node, name, lowercase);
if (coords.size() % 3 != 0) {
fatal_error(fmt::format(
"Incorect number of coordinates in Position array ({}) for \"{}\"",
coords.size(), name));
}
vector<Position> positions;
positions.reserve(coords.size() / 3);
auto it = coords.begin();
for (size_t i = 0; i < coords.size(); i += 3) {
positions.push_back({coords[i], coords[i + 1], coords[i + 2]});
}
return positions;
}

Position get_node_position(
pugi::xml_node node, const char* name, bool lowercase)
{
Expand Down
Loading