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

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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
8 changes: 8 additions & 0 deletions docker-dev-notes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# docker run -v $PWD:/root/OpenMC/openmc -it openmc_dagmc_libmesh

# in docker
# cd ~/OpenMC/build
# cmake ../openmc
# make -j 16 install
# cd ../openmc
# pip install .
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
27 changes: 27 additions & 0 deletions include/openmc/distribution_spatial.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,33 @@ 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;

//! Sample a point
//! \param seed Pseudorandom number seed pointer
//! \return Sampled point index
int32_t sample_point_index(uint64_t* seed) const;

double total_strength() { return this->point_idx_dist_.integral(); }
pshriwise marked this conversation as resolved.
Show resolved Hide resolved

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.x

Parameters
----------
positions : Iterable of 3-tuples
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 (3xN)
The points in space to be sampled with shape (3,N)
strengths : numpy.ndarray or None
An array of relative probabilities for each mesh point
"""

def __init__(self, positions, strengths=None):
self.positions = positions
self.strengths = strengths

@property
def positions(self):
return self._positions

@positions.setter
def positions(self, given_positions):
if given_positions is None:
raise ValueError('No positions were provided')
Comment on lines +794 to +795
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd opt to let the check_iterable_type throw an error here instead.

cv.check_iterable_type('position list passed in', given_positions, Real, 2, 2)

if isinstance(given_positions, list):
cv.check_length('first position entry', given_positions[0], 3, 3)
self._positions = np.asarray(given_positions)
elif isinstance(given_positions, np.ndarray):
self._positions = given_positions
Comment on lines +798 to +802
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another place to seek OpenMC design best practice about being tolerant to lots of data forms for this data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trend seems to be storing NumPy arrays for the internal attributes values when in doubt. I'll let @paulromano chime in here though in case his perspective differs.

else:
raise ValueError('Unable to interpret source positions')

@property
def strengths(self):
return self._strengths

@strengths.setter
def strengths(self, given_strengths):
if given_strengths is not None:
cv.check_type('strengths array passed in', given_strengths, Iterable, Real)
self._strengths = np.asarray(given_strengths, dtype=float).flatten()
else:
self._strengths = None

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

def to_xml_element(self):
"""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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the XML IO format spec will need to be updated to include this option.

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):
"""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.asarray([float(b) for b in coord_data.split()]).reshape((3,-1))

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

return cls(positions, strengths)



Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

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

Expand Down
50 changes: 50 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,54 @@ Position MeshSpatial::sample(uint64_t* seed) const
return this->sample_mesh(seed).second;
}

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

PointCloud::PointCloud(pugi::xml_node node)
{
std::vector<double> coords;

if (check_for_node(node, "coords")) {
coords = get_node_array<double>(node, "coords");
} else {
fatal_error("No coordinates were provided for the PointCloud "
"spatial distribution");
}
point_cloud_ = get_node_position_array(node, "coords");

std::vector<double> strengths(point_cloud_.size(), 1.0);

if (check_for_node(node, "strengths")) {
strengths = get_node_array<double>(node, "strengths");
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);
}

int32_t PointCloud::sample_point_index(uint64_t* seed) const
{
return point_idx_dist_.sample(seed);
}

Position PointCloud::sample(uint64_t* seed) const
{
return point_cloud_[this->sample_point_index(seed)];
}

//==============================================================================
// 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.resize(coords.size() / 3);
auto it = coords.begin();
for (int i = 0; i < positions.size(); i++) {
positions[i] = {*it++, *it++, *it++};
}
return positions;
}

Position get_node_position(
pugi::xml_node node, const char* name, bool lowercase)
{
Expand Down
51 changes: 51 additions & 0 deletions tests/unit_tests/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,57 @@ def test_spherical_uniform():

assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent)

def test_point_cloud():
point_list = [[1,0,0], [0,1,0], [0,0,1]]
positions = np.asarray(point_list)
strengths = [1,2,3]

space = openmc.stats.PointCloud(positions, strengths)
np.testing.assert_equal(space.positions, positions)
np.testing.assert_equal(space.strengths, strengths)

energy = openmc.stats.Discrete([1.0e6], [1.0])
angle = openmc.stats.Isotropic()

src = openmc.IndependentSource(space=space, angle=angle, energy=energy)
assert src.space == space
np.testing.assert_equal(src.space.positions, positions)
np.testing.assert_equal(src.space.strengths, strengths)

elem = src.to_xml_element()
src = openmc.IndependentSource.from_xml_element(elem)
np.testing.assert_equal(src.space.positions, positions)
np.testing.assert_equal(src.space.strengths, strengths)


def test_point_cloud_strengths(run_in_tmpdir, sphere_box_model):
point_list = [[1,0,0], [0,1,0], [0,0,1]]
positions = np.asarray(point_list)
strengths = [3,2,1]

space = openmc.stats.PointCloud(positions, strengths)

energy = openmc.stats.Discrete([1.0e6], [1.0])
angle = openmc.stats.Isotropic()

src = openmc.IndependentSource(space=space, angle=angle, energy=energy)
model = sphere_box_model[0]
model.settings.run_mode = 'fixed source'
model.settings.source = src

try:
model.init_lib()
n_samples = 50_000
sites = openmc.lib.sample_external_source(n_samples)
finally:
model.finalize_lib()

sites_df = sites.to_dataframe()
for i, (strength, coord) in enumerate(zip(strengths, ('x', 'y', 'z'))):
sampled_strength = len(sites_df[sites_df[coord] == 1.0]) / n_samples
expected_strength = pytest.approx(strength/sum(strengths), abs=0.02)
assert sampled_strength == expected_strength, f'Strength incorrect for {point_list[i]}'


def test_source_file():
filename = 'source.h5'
Expand Down