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

Randomized Quasi-Monte Carlo Sampling in The Random Ray Method #3268

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
6 changes: 6 additions & 0 deletions docs/source/io_formats/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,12 @@ found in the :ref:`random ray user guide <random_ray>`.

*Default*: None

:sample_method:
Specifies the method for sampling the starting ray distribution. This
element can be set to "prng" or "halton".

*Default*: prng

----------------------------------
``<resonance_scattering>`` Element
----------------------------------
Expand Down
18 changes: 18 additions & 0 deletions docs/source/usersguide/random_ray.rst
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,24 @@ acceptable ray source for a two-dimensional 2x2 lattice would look like:
provide physical particle fixed sources in addition to the random ray
source.

--------------------------
Quasi-Monte Carlo Sampling
--------------------------

By default OpenMC will use a pseudorandom number generator (PRNG) to sample ray
starting locations from a uniform distribution in space and angle.
Alternatively, a randomized Halton sequence may be sampled from, which is a form
of Randomized Qusi-Monte Carlo (RQMC) sampling. RQMC sampling with random ray
has been shown to offer reduced variance as compared to regular PRNG sampling,
as the Halton sequence offers a more uniform distribution of sampled points.
Randomized Halton sampling can be enabled as::

settings.random_ray['sample_method'] = 'halton'

Default behavior using OpenMC's native PRNG can be manually specified as::

settings.random_ray['sample_method'] = 'prng'

.. _subdivision_fsr:

----------------------------------
Expand Down
1 change: 1 addition & 0 deletions include/openmc/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ enum class SolverType { MONTE_CARLO, RANDOM_RAY };

enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID };
enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY };
enum class RandomRaySampleMethod { PRNG, HALTON };

//==============================================================================
// Geometry Constants
Expand Down
11 changes: 11 additions & 0 deletions include/openmc/random_dist.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ namespace openmc {

double uniform_distribution(double a, double b, uint64_t* seed);

//==============================================================================
//! Sample an integer from uniform distribution [a, b]
//
//! \param a Lower bound of uniform distribution
//! \param b Upper bound of uniform distribtion
//! \param seed A pointer to the pseudorandom seed
//! \return Sampled variate
//==============================================================================

int64_t uniform_int_distribution(int64_t a, int64_t b, uint64_t* seed);

//==============================================================================
//! Samples an energy from the Maxwell fission distribution based on a direct
//! sampling scheme.
Expand Down
3 changes: 3 additions & 0 deletions include/openmc/random_ray/random_ray.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ class RandomRay : public Particle {

void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain);
uint64_t transport_history_based_single_ray();
SourceSite sample_prng();
SourceSite sample_halton();

//----------------------------------------------------------------------------
// Static data members
static double distance_inactive_; // Inactive (dead zone) ray length
static double distance_active_; // Active ray length
static unique_ptr<Source> ray_source_; // Starting source for ray sampling
static RandomRaySourceShape source_shape_; // Flag for linear source
static RandomRaySampleMethod sample_method_; // Flag for sampling method

//----------------------------------------------------------------------------
// Public data members
Expand Down
8 changes: 8 additions & 0 deletions openmc/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ class Settings:
:adjoint:
Whether to run the random ray solver in adjoint mode (bool). The
default is 'False'.
:sample_method:
Sampling method for the ray starting location and direction of travel.
Options are `prng` (default) or 'halton`.

.. versionadded:: 0.15.0
resonance_scattering : dict
Expand Down Expand Up @@ -1131,6 +1134,9 @@ def random_ray(self, random_ray: dict):
cv.check_type('volume normalized flux tallies', random_ray[key], bool)
elif key == 'adjoint':
cv.check_type('adjoint', random_ray[key], bool)
elif key == 'sample_method':
cv.check_value('sample method', random_ray[key],
('prng', 'halton'))
else:
raise ValueError(f'Unable to set random ray to "{key}" which is '
'unsupported by OpenMC')
Expand Down Expand Up @@ -1948,6 +1954,8 @@ def _random_ray_from_xml_element(self, root):
self.random_ray['adjoint'] = (
child.text in ('true', '1')
)
elif child.tag == 'sample_method':
self.random_ray['sample_method'] = child.text

def to_xml_element(self, mesh_memo=None):
"""Create a 'settings' element to be written to an XML file.
Expand Down
5 changes: 5 additions & 0 deletions src/random_dist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ double uniform_distribution(double a, double b, uint64_t* seed)
return a + (b - a) * prn(seed);
}

int64_t uniform_int_distribution(int64_t a, int64_t b, uint64_t* seed)
{
return a + static_cast<int64_t>(prn(seed) * (b - a + 1));
}

double maxwell_spectrum(double T, uint64_t* seed)
{
// Set the random numbers
Expand Down
131 changes: 124 additions & 7 deletions src/random_ray/random_ray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#include "openmc/search.h"
#include "openmc/settings.h"
#include "openmc/simulation.h"

#include "openmc/distribution_spatial.h"
#include "openmc/random_dist.h"
#include "openmc/source.h"

namespace openmc {
Expand Down Expand Up @@ -174,6 +177,65 @@ float exponentialG2(float tau)
return num / den;
}

// Implementation of the Fisher-Yates shuffle algorithm.
// Algorithm adapted from:
// https://en.cppreference.com/w/cpp/algorithm/random_shuffle#Version_3
void fisher_yates_shuffle(vector<int>& arr, uint64_t* seed)
{
// Loop over the array from the last element down to the second
for (size_t i = arr.size() - 1; i > 0; --i) {
// Generate a random index in the range [0, i]
size_t j = uniform_int_distribution(0, i, seed);
// Swap arr[i] with arr[j]
std::swap(arr[i], arr[j]);
}
}

// Function to generate randomized Halton sequence samples
//
// Algorithm adapted from:
// A. B. Owen. A randomized halton algorithm in r. Arxiv, 6 2017.
// URL https://arxiv.org/abs/1706.02808
vector<vector<float>> rhalton(int N, int dim, uint64_t* seed, int64_t skip = 0)
{
int b;
double b2r;
vector<double> ans(N);
vector<int> ind(N);
vector<int> primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
vector<vector<float>> halton(N, vector<float>(dim, 0.0));

std::iota(ind.begin(), ind.end(), skip);

for (int D = 0; D < dim; ++D) {
b = primes[D];
b2r = 1.0 / b;
vector<int> res(ind);
std::fill(ans.begin(), ans.end(), 0.0);

while ((1.0 - b2r) < 1.0) {
vector<int> dig(N);
// randomaly permute a sequence from skip to skip+N
vector<int> perm(b);
std::iota(perm.begin(), perm.end(), 0);
fisher_yates_shuffle(perm, seed);

for (int i = 0; i < N; ++i) {
dig[i] = res[i] % b;
ans[i] += perm[dig[i]] * b2r;
res[i] = (res[i] - dig[i]) / b;
}
b2r /= b;
}

for (int i = 0; i < N; ++i) {
halton[i][D] = ans[i];
}
}

return halton;
}

Comment on lines +194 to +238
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Given that the particle positions and directions are stored in FP64, would it make sense to allow the sampling to also be 64-bit?

  2. This is a nice and high-level/abstract implementation of the sampling scheme, though the "abstractness" is a double edged sword as it requires a significant amount of dynamic memory allocation with all the usage of vector. My guess is that this causes most of the added expense in the sampling scheme as compared to PRNG sampling. With that in mind, as the function is only ever called in OpenMC as:

rhalton(1, 5, current_seed(), skip = skip)

we could in theory make a simpler and faster function that only generates a single 5 dimensional sample, thus eliminating the need for several vectors (or replacement with statically sized std::array). However, it's possible other areas in OpenMC may want to use Halton sampling in the future, in which case perhaps it is worthwhile to keep things abstract? At the very least, it seems like we can limit things to a single sample at a time (eliminating the N argument and thus several dynamically sized vectors)?

//==============================================================================
// RandomRay implementation
//==============================================================================
Expand All @@ -183,6 +245,7 @@ double RandomRay::distance_inactive_;
double RandomRay::distance_active_;
unique_ptr<Source> RandomRay::ray_source_;
RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT};
RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG};

RandomRay::RandomRay()
: angular_flux_(data::mg.num_energy_groups_),
Expand Down Expand Up @@ -509,14 +572,19 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
// set identifier for particle
id() = simulation::work_index[mpi::rank] + ray_id;

// set random number seed
int64_t particle_seed =
(simulation::current_batch - 1) * settings::n_particles + id();
init_particle_seeds(particle_seed, seeds());
stream() = STREAM_TRACKING;
// generate source site using sample method
SourceSite site;
switch (sample_method_) {
case RandomRaySampleMethod::PRNG:
site = sample_prng();
break;
case RandomRaySampleMethod::HALTON:
site = sample_halton();
break;
default:
fatal_error("Unknown sample method for random ray transport.");
}

// Sample from ray source distribution
SourceSite site {ray_source_->sample(current_seed())};
site.E = lower_bound_index(
data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E);
site.E = negroups_ - site.E - 1.;
Expand Down Expand Up @@ -545,4 +613,53 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
}
}

SourceSite RandomRay::sample_prng()
{
// set random number seed
int64_t particle_seed =
(simulation::current_batch - 1) * settings::n_particles + id();
init_particle_seeds(particle_seed, seeds());
stream() = STREAM_TRACKING;

// Sample from ray source distribution
SourceSite site {ray_source_->sample(current_seed())};

return site;
}

SourceSite RandomRay::sample_halton()
{
SourceSite site;

// Set random number seed
int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles;
int64_t skip = id();
init_particle_seeds(batch_seed, seeds());
stream() = STREAM_TRACKING;
Comment on lines +634 to +638
Copy link
Contributor

Choose a reason for hiding this comment

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

In a similar manner to my comment regarding simplification vs. leaving abstract for the rhalton function, if we end up deciding to just have it generate a single sample, can we change the interface to only rely on the current_seed() (appropriately offset of the particle ID), thus allowing for this code to be moved back up to the caller, such that it doesn't need to be (partially) reproduced between sample_rng() vs. sample_halton? The logic of RQMC sampling makes it seem like we need to keep the seed to be batch-specific, but wanted to double check!


// Calculate next samples in LDS
vector<vector<float>> samples = rhalton(1, 5, current_seed(), skip = skip);

// Get spatial box of ray_source_
SpatialBox* sb = dynamic_cast<SpatialBox*>(
dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());

// Sample spatial distribution
Position xi {samples[0][0], samples[0][1], samples[0][2]};
Position shift {1e-9, 1e-9, 1e-9};
Copy link
Contributor

Choose a reason for hiding this comment

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

Wherever possible, "magic numbers" in OpenMC are defined in constants.h, so it's easier to keep track of how many things like the 1e-9 shift here we have baked into the code. You could make a new macro in constants.h with this value, or potentially just reuse an existing macro since this shift is related to geometry floating point sensitivity. Some existing options from constants.h:

// Used for surface current tallies
constexpr double TINY_BIT {1e-8};

// User for precision in geometry
constexpr double FP_PRECISION {1e-14};
constexpr double FP_REL_PRECISION {1e-5};
constexpr double FP_COINCIDENT {1e-12};

site.r = (sb->lower_left() + shift) +
xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift));

// Sample Polar cosine and azimuthal angles
float mu = 2.0 * samples[0][3] - 1.0;
float azi = 2.0 * PI * samples[0][4];
// Convert to Cartesian coordinates
float c = std::sqrt(1.0 - mu * mu);
site.u.x = mu;
site.u.y = std::cos(azi) * c;
site.u.z = std::sin(azi) * c;

return site;
}

} // namespace openmc
11 changes: 11 additions & 0 deletions src/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ void get_run_parameters(pugi::xml_node node_base)
FlatSourceDomain::adjoint_ =
get_node_value_bool(random_ray_node, "adjoint");
}
if (check_for_node(random_ray_node, "sample_method")) {
std::string temp_str =
get_node_value(random_ray_node, "sample_method", true, true);
if (temp_str == "prng") {
RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
} else if (temp_str == "halton") {
RandomRay::sample_method_ = RandomRaySampleMethod::HALTON;
} else {
fatal_error("Unrecognized sample method: " + temp_str);
}
}
}
}

Expand Down
Empty file.
Loading
Loading