mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 14:35:27 -04:00
Source biasing capabilities (#3460)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
parent
37e2feb34b
commit
0486e433d2
24 changed files with 2003 additions and 252 deletions
|
|
@ -844,13 +844,18 @@ attributes/sub-elements:
|
|||
relative source strength of each mesh element or each point in the cloud.
|
||||
|
||||
:volume_normalized:
|
||||
For "mesh" spatial distrubtions, this optional boolean element specifies
|
||||
For "mesh" spatial distributions, 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
|
||||
|
||||
:bias:
|
||||
For "mesh" and "cloud" spatial distributions, this optional element
|
||||
specifies floating point values corresponding to alternative probabilities
|
||||
for each value/component to use for biased sampling.
|
||||
|
||||
:angle:
|
||||
An element specifying the angular distribution of source sites. This element
|
||||
has the following attributes:
|
||||
|
|
@ -883,6 +888,10 @@ attributes/sub-elements:
|
|||
are those of a univariate probability distribution (see the description in
|
||||
:ref:`univariate`).
|
||||
|
||||
:bias:
|
||||
For "isotropic" angular distributions, this optional element specifies a
|
||||
"mu-phi" angular distribution used for biased sampling.
|
||||
|
||||
:energy:
|
||||
An element specifying the energy distribution of source sites. The necessary
|
||||
sub-elements/attributes are those of a univariate probability distribution
|
||||
|
|
@ -906,6 +915,10 @@ attributes/sub-elements:
|
|||
mesh element and follows the format for :ref:`source_element`. The number of
|
||||
``<source>`` sub-elements should correspond to the number of mesh elements.
|
||||
|
||||
.. note:: Biased sampling can be applied to the spatial and energy distributions
|
||||
of a source by using the ``<bias>`` sub-element (see
|
||||
:ref:`univariate` for details on how to specify bias distributions).
|
||||
|
||||
:constraints:
|
||||
This sub-element indicates the presence of constraints on sampled source
|
||||
sites (see :ref:`usersguide_source_constraints` for details). It may have
|
||||
|
|
@ -998,13 +1011,26 @@ variable and whose sub-elements/attributes are as follows:
|
|||
*Default*: histogram
|
||||
|
||||
:pair:
|
||||
For a "mixture" distribution, this element provides a distribution and its corresponding probability.
|
||||
For a "mixture" distribution, this element provides a distribution and its
|
||||
corresponding probability.
|
||||
|
||||
:probability:
|
||||
An attribute or ``pair`` that provides the probability of a univariate distribution within a "mixture" distribution.
|
||||
An attribute or ``pair`` that provides the probability of a univariate
|
||||
distribution within a "mixture" distribution.
|
||||
|
||||
:dist:
|
||||
This sub-element of a ``pair`` element provides information on the corresponding univariate distribution.
|
||||
This sub-element of a ``pair`` element provides information on the
|
||||
corresponding univariate distribution.
|
||||
|
||||
:bias:
|
||||
This optional element specifies a biased distribution for importance sampling.
|
||||
For continuous distributions, the ``bias`` element should contain another
|
||||
univariate distribution with the same support (interval) as the parent
|
||||
distribution. For discrete distributions, the ``bias`` element should contain
|
||||
floating point values corresponding to alternative probabilities for each
|
||||
value/component to be used for biased sampling.
|
||||
|
||||
*Default*: None
|
||||
|
||||
---------------------------------------
|
||||
``<source_rejection_fraction>`` Element
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ not experience a single scoring event, even after billions of analog histories.
|
|||
Variance reduction techniques aim to either flatten the global uncertainty
|
||||
distribution, such that all regions of phase space have a fairly similar
|
||||
uncertainty, or to reduce the uncertainty in specific locations (such as a
|
||||
detector). There are two strategies available in OpenMC for variance reduction:
|
||||
the Monte Carlo MAGIC method and the FW-CADIS method. Both strategies work by
|
||||
developing a weight window mesh that can be utilized by subsequent Monte Carlo
|
||||
solves to split particles heading towards areas of lower flux densities while
|
||||
terminating particles in higher flux regions---all while maintaining a fair
|
||||
game.
|
||||
detector). There are three strategies available in OpenMC for variance
|
||||
reduction: weight windows generated via the MAGIC method or the FW-CADIS method,
|
||||
and source biasing. Both weight windowing strategies work by developing a mesh
|
||||
that can be utilized by subsequent Monte Carlo solves to split particles heading
|
||||
towards areas of lower flux densities while terminating particles in higher flux
|
||||
regions. In contrast, source biasing modifies source site sampling behavior to
|
||||
preferentially track particles more likely to reach phase space regions of
|
||||
interest.
|
||||
|
||||
------------
|
||||
MAGIC Method
|
||||
|
|
@ -132,3 +134,71 @@ aware of this.
|
|||
:label: variance_fom
|
||||
|
||||
\text{FOM} = \frac{1}{\text{Time} \times \sigma^2}
|
||||
|
||||
.. _methods_source_biasing:
|
||||
|
||||
--------------
|
||||
Source Biasing
|
||||
--------------
|
||||
|
||||
In contrast to the previous two methods that introduce population controls
|
||||
during transport, source biasing modifies the sampling of the external source
|
||||
distribution. The basic premise of the technique is that for each spatial,
|
||||
angular, energy, or time distribution of a source, an additional distribution
|
||||
can be specified provided that the two share a common support (set of points
|
||||
where the distribution is nonzero). Samples are then drawn from this "bias"
|
||||
distribution, which can be chosen to preferentially direct particles towards
|
||||
phase space regions of interest. In order to avoid biasing the tally results,
|
||||
however, a weight adjustment is applied to each sampled site as described below.
|
||||
|
||||
Assume that the unbiased probability density function of a random variable
|
||||
:math:`X:x \rightarrow \mathbb{R}` is given by :math:`f(x)`, but that using the
|
||||
biased distribution :math:`g(x)` will result in a greater number of particle
|
||||
trajectories reaching some phase space region of interest. Then a sample
|
||||
:math:`x_0` may be drawn from :math:`g(x)` while maintaining a fair game,
|
||||
provided that its weight is adjusted as:
|
||||
|
||||
.. math::
|
||||
:label: source_bias
|
||||
|
||||
w = w_0 \times \frac{f(x_0)}{g(x_0)}
|
||||
|
||||
where :math:`w_0` is the weight of an unbiased sample from :math:`f(x)`,
|
||||
typically unity.
|
||||
|
||||
Returning now to Equation :eq:`source_bias`, the requirement for common support
|
||||
becomes evident. If :math:`\mathrm{supp} (g)` fully contains but is not
|
||||
identical to :math:`\mathrm{supp} (f)`, then some samples from :math:`g(x)` will
|
||||
correspond to points where :math:`f(x) = 0`. Thus these source sites would be
|
||||
assigned a starting weight of 0, meaning the particles would be killed
|
||||
immediately upon transport, effectively wasting computation time. Conversely, if
|
||||
:math:`\mathrm{supp} (g)` is fully contained by but not identical to
|
||||
:math:`\mathrm{supp} (f)`, the contributions of some regions outside
|
||||
:math:`\mathrm{supp} (g)` will not be counted towards the integral, potentially
|
||||
biasing the tally. The weight assigned to such points would be undefined since
|
||||
:math:`g(x) = \mathbf{0}` at these points.
|
||||
|
||||
When an independent source is sampled in OpenMC, the particle's coordinate in
|
||||
each variable of phase space :math:`(\mathbf{r},\mathbf{\Omega},E,t)` is
|
||||
successively drawn from an independent probability distribution. Multiple
|
||||
variables can be biased, in which case the resultant weight :math:`w` applied to
|
||||
the particle is the product of the weights assigned from all sampled
|
||||
distributions: space, angle, energy, and time, as shown in Equation
|
||||
:eq:`tot_wgt`.
|
||||
|
||||
.. math::
|
||||
:label: tot_wgt
|
||||
|
||||
w = w_r \times w_{\Omega} \times w_E \times w_t
|
||||
|
||||
Finally, source biasing and weight windows serve different purposes. Source
|
||||
biasing changes how particles are born, allowing the initial source sites to be
|
||||
sampled preferentially from important regions of phase space (space, angle,
|
||||
energy, and time) with an accompanying weight adjustment. Weight windows, by
|
||||
contrast, apply population control during transport (splitting and Russian
|
||||
roulette) to help particles reach and contribute in important regions as they
|
||||
move through the system. Because particle transport proceeds as usual after a
|
||||
biased source is sampled, particle attenuation in optically thick regions
|
||||
outside the source volume will not be affected by source biasing; in such
|
||||
scenarios, transport biasing techniques such as weight windows are often more
|
||||
effective.
|
||||
|
|
|
|||
|
|
@ -272,6 +272,12 @@ option::
|
|||
settings.source = [src1, src2]
|
||||
settings.uniform_source_sampling = True
|
||||
|
||||
Additionally, sampling from an :class:`openmc.IndependentSource` may be biased
|
||||
for local or global variance reduction by modifying the
|
||||
:attr:`~openmc.IndependentSource.bias` attribute of each of its four main
|
||||
distributions. Further discussion of source biasing can be found in
|
||||
:ref:`source_biasing`.
|
||||
|
||||
Finally, the :attr:`IndependentSource.particle` attribute can be used to
|
||||
indicate the source should be composed of particles other than neutrons. For
|
||||
example, the following would generate a photon source::
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ Variance Reduction
|
|||
==================
|
||||
|
||||
Global variance reduction in OpenMC is accomplished by weight windowing
|
||||
techniques. OpenMC is capable of generating weight windows using either the
|
||||
MAGIC or FW-CADIS methods. Both techniques will produce a ``weight_windows.h5``
|
||||
file that can be loaded and used later on. In this section, we break down the
|
||||
steps required to both generate and then apply weight windows.
|
||||
or source biasing techniques, the latter of which additionally provides a
|
||||
local variance reduction capability. OpenMC is capable of generating weight
|
||||
windows using either the MAGIC or FW-CADIS methods. Both techniques will
|
||||
produce a ``weight_windows.h5`` file that can be loaded and used later on. In
|
||||
this section, we first break down the steps required to generate and apply
|
||||
weight windows, then describe how source biasing may be applied.
|
||||
|
||||
.. _ww_generator:
|
||||
|
||||
|
|
@ -172,3 +174,148 @@ Weight window mesh information is embedded into the weight window file, so the
|
|||
mesh does not need to be redefined. Monte Carlo solves that load a weight window
|
||||
file as above will utilize weight windows to reduce the variance of the
|
||||
simulation.
|
||||
|
||||
.. _source_biasing:
|
||||
|
||||
--------------
|
||||
Source Biasing
|
||||
--------------
|
||||
|
||||
In fixed source problems, source biasing provides a means to reduce the variance
|
||||
on global or localized responses, depending on the biasing scheme. In either
|
||||
case, the premise of the method is to sample source sites from a biased
|
||||
distribution that directs a larger fraction of the simulated histories towards
|
||||
phase space regions of interest than would be found there under analog sampling.
|
||||
In order to preserve an unbiased estimate of the tally mean, the weight of these
|
||||
with analog sampling, divided by the probability assigned by the biased
|
||||
distribution. While the assignment of statistical weights is outlined in the
|
||||
:ref:`methods section <methods_source_biasing>`, this section demonstrates the
|
||||
implementation of source biasing to problems in OpenMC.
|
||||
|
||||
Source biasing in OpenMC is accomplished by applying a distribution to the
|
||||
:attr:`bias` attribute of one or more of the univariate or independent
|
||||
multivariate distributions which make up an :class:`~openmc.IndependentSource`
|
||||
instance as follows::
|
||||
|
||||
# First create the biased distribution
|
||||
biased_dist = openmc.stats.PowerLaw(a=0, b=3, n=3)
|
||||
|
||||
# Construct a new distribution with the bias applied
|
||||
dist = openmc.stats.PowerLaw(a=0, b=3, n=2, bias=biased_dist)
|
||||
|
||||
# The bias attribute can also be set on an existing "analog" distribution:
|
||||
sphere_dist = openmc.stats.spherical_uniform(r_outer=3)
|
||||
sphere_dist.r.bias = biased_dist
|
||||
|
||||
Univariate distributions may be sampled via the Python API, returning the
|
||||
sample(s) along with the associated weight(s)::
|
||||
|
||||
sample_vec, wgt_vec = dist.sample(n_samples=100)
|
||||
|
||||
Here, if the distribution is unbiased, the weight of each sample will be unity.
|
||||
Finally, :class:`~openmc.IndependentSource` instances can be constructed with
|
||||
biased distributions::
|
||||
|
||||
# Create a source with a biased spatial distribution
|
||||
source = openmc.IndependentSource(space=sphere_dist)
|
||||
|
||||
During the simulation, source sites are then sampled using the biased
|
||||
distributions where available and given starting statistical weights
|
||||
corresponding to the cumulative product of the weights assigned by each
|
||||
distribution in the source object. Hence multiple source variables (e.g.,
|
||||
direction and energy) may be biased and the resulting source sites will have
|
||||
their weights adjusted accordingly.
|
||||
|
||||
.. note::
|
||||
Combining source biasing with weight windows can be a powerful variance
|
||||
reduction technique if each is constructed appropriately for the response
|
||||
of interest. For example, if a source biasing scheme is devised for
|
||||
variance reduction of a specific localized response, the user may be able
|
||||
to specify their own weight window structure that results in more efficient
|
||||
transport than if weight windows were generated by either of OpenMC's
|
||||
automatic weight window generators, which are intended for global variance
|
||||
reduction.
|
||||
|
||||
Biased distributions that could result in degenerate weight mappings are not
|
||||
recommended; this is most commonly seen when biasing the :math:`\phi`-coordinate
|
||||
of spherical or cylindrical independent multivariate distributions. In such
|
||||
cases degenerate behavior will be observed at the pole about which :math:`\phi`
|
||||
is measured, with all values of :math:`\phi` (hence many possible statistical
|
||||
weights) mapping to the same point for :math:`r=0` or :math:`\mu=0`, and large
|
||||
weight gradients in the vicinity. In most cases requiring a spherical
|
||||
independent source, it would be preferable to reorient the reference vector of
|
||||
the distribution such that biasing could be applied to the
|
||||
:math:`\mu`-coordinate instead.
|
||||
|
||||
When biasing a distribution, care should also be taken to ensure that both the
|
||||
unbiased and biased distribution share a common support---that is, every region
|
||||
of phase space mapped to a nonzero probability density by the unbiased
|
||||
distribution should likewise map to nonzero probability under the biased
|
||||
distribution, and vice versa. In OpenMC, this places restrictions on the set of
|
||||
compatible distributions that may be used to bias sampling of each distribution
|
||||
type. The following table summarizes the method for each distribution in OpenMC
|
||||
that permits biased sampling.
|
||||
|
||||
.. list-table:: **Distributions that support biased sampling**
|
||||
:header-rows: 1
|
||||
:widths: 35 65
|
||||
|
||||
* - Discrete Univariate PDFs
|
||||
- Biasing Method
|
||||
* - :class:`openmc.stats.Discrete`
|
||||
- Apply a vector of alternative probabilities to the :attr:`bias`
|
||||
attribute
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 35 65
|
||||
|
||||
* - Continuous Univariate PDFs
|
||||
- Biasing Method
|
||||
* - :class:`openmc.stats.Uniform`,
|
||||
:class:`openmc.stats.PowerLaw`,
|
||||
:class:`openmc.stats.Maxwell`,
|
||||
:class:`openmc.stats.Watt`,
|
||||
:class:`openmc.stats.Normal`,
|
||||
:class:`openmc.stats.Tabular`
|
||||
- Apply a second, unbiased continous univariate PDF to the :attr:`bias`
|
||||
attribute, ensuring that the :attr:`support` attribute of each
|
||||
distribution is the same
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 35 65
|
||||
|
||||
* - Mixed Univariate PDFs
|
||||
- Biasing Method
|
||||
* - :class:`openmc.stats.Mixture`
|
||||
- May be constructed from multiple biased univariate distributions, or a
|
||||
second, unbiased continous univariate PDF may be applied to the
|
||||
:attr:`bias` attribute
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 35 65
|
||||
|
||||
* - Discrete Multivariate PDFs
|
||||
- Biasing Method
|
||||
* - :class:`openmc.stats.PointCloud`,
|
||||
:class:`openmc.stats.MeshSpatial`
|
||||
- Apply a vector of the new relative probabilities of each point or mesh
|
||||
element under biased sampling to the :attr:`bias` attribute
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 35 65
|
||||
|
||||
* - Continuous Multivariate PDFs
|
||||
- Biasing Method
|
||||
* - :class:`openmc.stats.CartesianIndependent`,
|
||||
:class:`openmc.stats.CylindricalIndependent`,
|
||||
:class:`openmc.stats.SphericalIndependent`,
|
||||
:class:`openmc.stats.PolarAzimuthal`
|
||||
- Construct from biased univariate distributions for :attr:`x`, :attr:`y`,
|
||||
:attr:`z`, etc.
|
||||
* - :class:`openmc.stats.Isotropic`
|
||||
- Apply an unbiased :class:`openmc.stats.PolarAzimuthal` to the
|
||||
:attr:`bias` attribute
|
||||
|
|
|
|||
|
|
@ -15,6 +15,17 @@
|
|||
|
||||
namespace openmc {
|
||||
|
||||
//==============================================================================
|
||||
// Helper function for computing importance weights from biased sampling
|
||||
//==============================================================================
|
||||
|
||||
//! Compute importance weights for biased sampling
|
||||
//! \param p Unnormalized original probability vector
|
||||
//! \param b Unnormalized bias probability vector
|
||||
//! \return Vector of importance weights (p_norm[i] / b_norm[i])
|
||||
vector<double> compute_importance_weights(
|
||||
const vector<double>& p, const vector<double>& b);
|
||||
|
||||
//==============================================================================
|
||||
//! Abstract class representing a univariate probability distribution
|
||||
//==============================================================================
|
||||
|
|
@ -22,11 +33,41 @@ namespace openmc {
|
|||
class Distribution {
|
||||
public:
|
||||
virtual ~Distribution() = default;
|
||||
virtual double sample(uint64_t* seed) const = 0;
|
||||
|
||||
//! Sample a value from the distribution, handling biasing automatically
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return (sampled value, importance weight)
|
||||
virtual std::pair<double, double> sample(uint64_t* seed) const;
|
||||
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
virtual double evaluate(double x) const;
|
||||
|
||||
//! Return integral of distribution
|
||||
//! \return Integral of distribution
|
||||
virtual double integral() const { return 1.0; };
|
||||
|
||||
//! Set bias distribution
|
||||
virtual void set_bias(std::unique_ptr<Distribution> bias)
|
||||
{
|
||||
bias_ = std::move(bias);
|
||||
}
|
||||
|
||||
const Distribution* bias() const { return bias_.get(); }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
virtual double sample_unbiased(uint64_t* seed) const = 0;
|
||||
|
||||
//! Read bias distribution from XML
|
||||
//! \param node XML node that may contain a bias child element
|
||||
void read_bias_from_xml(pugi::xml_node node);
|
||||
|
||||
// Biasing distribution
|
||||
unique_ptr<Distribution> bias_;
|
||||
};
|
||||
|
||||
using UPtrDist = unique_ptr<Distribution>;
|
||||
|
|
@ -50,7 +91,7 @@ public:
|
|||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
//! \return Sampled index
|
||||
size_t sample(uint64_t* seed) const;
|
||||
|
||||
// Properties
|
||||
|
|
@ -67,7 +108,7 @@ private:
|
|||
//! Normalize distribution so that probabilities sum to unity
|
||||
void normalize();
|
||||
|
||||
//! Initialize alias tables for distribution
|
||||
//! Initialize alias table for sampling
|
||||
void init_alias();
|
||||
};
|
||||
|
||||
|
|
@ -82,20 +123,30 @@ public:
|
|||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! \return (sampled value, sample weight)
|
||||
std::pair<double, double> sample(uint64_t* seed) const override;
|
||||
|
||||
double integral() const override { return di_.integral(); };
|
||||
|
||||
//! Override set_bias as no-op (bias handled in constructor)
|
||||
void set_bias(std::unique_ptr<Distribution> bias) override {}
|
||||
|
||||
// Properties
|
||||
const vector<double>& x() const { return x_; }
|
||||
const vector<double>& prob() const { return di_.prob(); }
|
||||
const vector<size_t>& alias() const { return di_.alias(); }
|
||||
const vector<double>& weight() const { return weight_; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
vector<double> x_; //!< Possible outcomes
|
||||
DiscreteIndex di_; //!< discrete probability distribution of
|
||||
//!< outcome indices
|
||||
vector<double> x_; //!< Possible outcomes
|
||||
vector<double> weight_; //!< Importance weights (empty if unbiased)
|
||||
DiscreteIndex di_; //!< Discrete probability distribution of outcome indices
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -107,14 +158,20 @@ public:
|
|||
explicit Uniform(pugi::xml_node node);
|
||||
Uniform(double a, double b) : a_ {a}, b_ {b} {};
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
double a() const { return a_; }
|
||||
double b() const { return b_; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
double a_; //!< Lower bound of distribution
|
||||
double b_; //!< Upper bound of distribution
|
||||
|
|
@ -131,15 +188,21 @@ public:
|
|||
: offset_ {std::pow(a, n + 1)}, span_ {std::pow(b, n + 1) - offset_},
|
||||
ninv_ {1 / (n + 1)} {};
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
double a() const { return std::pow(offset_, ninv_); }
|
||||
double b() const { return std::pow(offset_ + span_, ninv_); }
|
||||
double n() const { return 1 / ninv_ - 1; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
//! Store processed values in object to allow for faster sampling
|
||||
double offset_; //!< a^(n+1)
|
||||
|
|
@ -156,13 +219,19 @@ public:
|
|||
explicit Maxwell(pugi::xml_node node);
|
||||
Maxwell(double theta) : theta_ {theta} {};
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
double theta() const { return theta_; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
double theta_; //!< Factor in exponential [eV]
|
||||
};
|
||||
|
|
@ -176,14 +245,20 @@ public:
|
|||
explicit Watt(pugi::xml_node node);
|
||||
Watt(double a, double b) : a_ {a}, b_ {b} {};
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
double a() const { return a_; }
|
||||
double b() const { return b_; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
double a_; //!< Factor in exponential [eV]
|
||||
double b_; //!< Factor in square root [1/eV]
|
||||
|
|
@ -200,14 +275,20 @@ public:
|
|||
Normal(double mean_value, double std_dev)
|
||||
: mean_value_ {mean_value}, std_dev_ {std_dev} {};
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
double mean_value() const { return mean_value_; }
|
||||
double std_dev() const { return std_dev_; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
double mean_value_; //!< middle of distribution [eV]
|
||||
double std_dev_; //!< standard deviation [eV]
|
||||
|
|
@ -223,10 +304,10 @@ public:
|
|||
Tabular(const double* x, const double* p, int n, Interpolation interp,
|
||||
const double* c = nullptr);
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
// properties
|
||||
vector<double>& x() { return x_; }
|
||||
|
|
@ -235,6 +316,12 @@ public:
|
|||
Interpolation interp() const { return interp_; }
|
||||
double integral() const override { return integral_; };
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
vector<double> x_; //!< tabulated independent variable
|
||||
vector<double> p_; //!< tabulated probability density
|
||||
|
|
@ -259,13 +346,19 @@ public:
|
|||
explicit Equiprobable(pugi::xml_node node);
|
||||
Equiprobable(const double* x, int n) : x_ {x, x + n} {};
|
||||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! Evaluate probability density, f(x), at a point
|
||||
//! \param x Point to evaluate f(x)
|
||||
//! \return f(x)
|
||||
double evaluate(double x) const override;
|
||||
|
||||
const vector<double>& x() const { return x_; }
|
||||
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
vector<double> x_; //! Possible outcomes
|
||||
};
|
||||
|
|
@ -280,18 +373,25 @@ public:
|
|||
|
||||
//! Sample a value from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample(uint64_t* seed) const override;
|
||||
//! \return (sampled value, sample weight)
|
||||
std::pair<double, double> sample(uint64_t* seed) const override;
|
||||
|
||||
double integral() const override { return integral_; }
|
||||
|
||||
private:
|
||||
// Storrage for probability + distribution
|
||||
using DistPair = std::pair<double, UPtrDist>;
|
||||
//! Override set_bias as no-op (bias handled in constructor)
|
||||
void set_bias(std::unique_ptr<Distribution> bias) override {}
|
||||
|
||||
vector<DistPair>
|
||||
distribution_; //!< sub-distributions + cummulative probabilities
|
||||
double integral_; //!< integral of distribution
|
||||
protected:
|
||||
//! Sample a value (unbiased) from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled value
|
||||
double sample_unbiased(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
vector<UPtrDist> distribution_; //!< Sub-distributions
|
||||
vector<double> weight_; //!< Importance weights for component selection
|
||||
DiscreteIndex di_; //!< Discrete probability distribution of indices
|
||||
double integral_; //!< Integral of distribution
|
||||
};
|
||||
|
||||
} // namespace openmc
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ public:
|
|||
|
||||
//! Sample a direction from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Direction sampled
|
||||
virtual Direction sample(uint64_t* seed) const = 0;
|
||||
//! \return (sampled Direction, sample weight)
|
||||
virtual std::pair<Direction, double> sample(uint64_t* seed) const = 0;
|
||||
|
||||
Direction u_ref_ {0.0, 0.0, 1.0}; //!< reference direction
|
||||
};
|
||||
|
|
@ -43,14 +43,28 @@ public:
|
|||
|
||||
//! Sample a direction from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Direction sampled
|
||||
Direction sample(uint64_t* seed) const override;
|
||||
//! \return (sampled Direction, sample weight)
|
||||
std::pair<Direction, double> sample(uint64_t* seed) const override;
|
||||
|
||||
//! Sample a direction and return evaluation of the PDF for biased sampling.
|
||||
//! Note that bias distributions are intended to return unit-weight samples.
|
||||
//! \param seed Pseudorandom number seed points
|
||||
//! \return (sampled Direction, value of the PDF at this Direction)
|
||||
std::pair<Direction, double> sample_as_bias(uint64_t* seed) const;
|
||||
|
||||
// Observing pointers
|
||||
Distribution* mu() const { return mu_.get(); }
|
||||
Distribution* phi() const { return phi_.get(); }
|
||||
|
||||
private:
|
||||
//! Common sampling implementation
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \param return_pdf If true, return PDF evaluation; if false, return
|
||||
//! importance weight
|
||||
//! \return (sampled Direction, weight or PDF value)
|
||||
std::pair<Direction, double> sample_impl(
|
||||
uint64_t* seed, bool return_pdf) const;
|
||||
|
||||
Direction v_ref_ {1.0, 0.0, 0.0}; //!< reference direction
|
||||
Direction w_ref_;
|
||||
UPtrDist mu_; //!< Distribution of polar angle
|
||||
|
|
@ -66,11 +80,24 @@ Direction isotropic_direction(uint64_t* seed);
|
|||
class Isotropic : public UnitSphereDistribution {
|
||||
public:
|
||||
Isotropic() {};
|
||||
explicit Isotropic(pugi::xml_node node);
|
||||
|
||||
//! Sample a direction from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled direction
|
||||
Direction sample(uint64_t* seed) const override;
|
||||
//! \return (sampled direction, sample weight)
|
||||
std::pair<Direction, double> sample(uint64_t* seed) const override;
|
||||
|
||||
// Set or get bias distribution
|
||||
void set_bias(std::unique_ptr<PolarAzimuthal> bias)
|
||||
{
|
||||
bias_ = std::move(bias);
|
||||
}
|
||||
|
||||
const PolarAzimuthal* bias() const { return bias_.get(); }
|
||||
|
||||
protected:
|
||||
// Biasing distribution
|
||||
unique_ptr<PolarAzimuthal> bias_;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -85,8 +112,8 @@ public:
|
|||
|
||||
//! Sample a direction from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled direction
|
||||
Direction sample(uint64_t* seed) const override;
|
||||
//! \return (sampled direction, sample weight)
|
||||
std::pair<Direction, double> sample(uint64_t* seed) const override;
|
||||
};
|
||||
|
||||
using UPtrAngle = unique_ptr<UnitSphereDistribution>;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ public:
|
|||
virtual ~SpatialDistribution() = default;
|
||||
|
||||
//! Sample a position from the distribution
|
||||
virtual Position sample(uint64_t* seed) const = 0;
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled (position, importance weight)
|
||||
virtual std::pair<Position, double> sample(uint64_t* seed) const = 0;
|
||||
|
||||
static unique_ptr<SpatialDistribution> create(pugi::xml_node node);
|
||||
};
|
||||
|
|
@ -34,8 +36,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
// Observer pointers
|
||||
Distribution* x() const { return x_.get(); }
|
||||
|
|
@ -58,8 +60,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
Distribution* r() const { return r_.get(); }
|
||||
Distribution* phi() const { return phi_.get(); }
|
||||
|
|
@ -83,8 +85,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
Distribution* r() const { return r_.get(); }
|
||||
Distribution* cos_theta() const { return cos_theta_.get(); }
|
||||
|
|
@ -109,8 +111,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
//! Sample the mesh for an element and position within that element
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
|
|
@ -133,8 +135,8 @@ public:
|
|||
|
||||
private:
|
||||
int32_t mesh_idx_ {C_NONE};
|
||||
DiscreteIndex elem_idx_dist_; //!< Distribution of
|
||||
//!< mesh element indices
|
||||
DiscreteIndex elem_idx_dist_; //!< Distribution of mesh element indices
|
||||
vector<double> weight_; //!< Importance weights (empty if unbiased)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -149,12 +151,13 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
std::vector<Position> point_cloud_;
|
||||
DiscreteIndex point_idx_dist_; //!< Distribution of Position indices
|
||||
vector<double> weight_; //!< Importance weights (empty if unbiased)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -167,8 +170,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
// Properties
|
||||
bool only_fissionable() const { return only_fissionable_; }
|
||||
|
|
@ -193,8 +196,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return Sampled (position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
Position r() const { return r_; }
|
||||
|
||||
|
|
|
|||
|
|
@ -217,8 +217,8 @@ public:
|
|||
|
||||
//! Sample a position from the distribution
|
||||
//! \param seed Pseudorandom number seed pointer
|
||||
//! \return Sampled position
|
||||
Position sample(uint64_t* seed) const override;
|
||||
//! \return (sampled position, importance weight)
|
||||
std::pair<Position, double> sample(uint64_t* seed) const override;
|
||||
|
||||
private:
|
||||
int32_t mesh_index_ {C_NONE}; //!< Index in global meshes array
|
||||
|
|
|
|||
|
|
@ -815,7 +815,7 @@ class ParentNuclideFilter(ParticleFilter):
|
|||
|
||||
|
||||
class MeshFilter(Filter):
|
||||
"""Bins tally event locations by mesh elements.
|
||||
r"""Bins tally event locations by mesh elements.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import warnings
|
|||
import openmc
|
||||
import openmc.checkvalue as cv
|
||||
from openmc.checkvalue import PathLike
|
||||
from openmc.stats.multivariate import MeshSpatial
|
||||
from openmc.stats.multivariate import MeshSpatial, Box, PolarAzimuthal, Isotropic
|
||||
from ._xml import clean_indentation, get_elem_list, get_text
|
||||
from .mesh import _read_meshes, RegularMesh, MeshBase
|
||||
from .source import SourceBase, MeshSource, IndependentSource
|
||||
|
|
@ -1877,7 +1877,11 @@ class Settings:
|
|||
for key, value in self._random_ray.items():
|
||||
if key == 'ray_source' and isinstance(value, SourceBase):
|
||||
source_element = value.to_xml_element()
|
||||
if source_element.find('bias') is not None:
|
||||
raise RuntimeError(
|
||||
"Ray source distributions should not be biased.")
|
||||
element.append(source_element)
|
||||
|
||||
elif key == 'source_region_meshes':
|
||||
subelement = ET.SubElement(element, 'source_region_meshes')
|
||||
for mesh, domains in value:
|
||||
|
|
@ -2324,6 +2328,9 @@ class Settings:
|
|||
self.random_ray[child.tag] = float(child.text)
|
||||
elif child.tag == 'source':
|
||||
source = SourceBase.from_xml_element(child)
|
||||
if child.find('bias') is not None:
|
||||
raise RuntimeError(
|
||||
"Ray source distributions should not be biased.")
|
||||
self.random_ray['ray_source'] = source
|
||||
elif child.tag == 'volume_estimator':
|
||||
self.random_ray['volume_estimator'] = child.text
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class PolarAzimuthal(UnitSphere):
|
|||
z-direction.
|
||||
reference_vwu : Iterable of float
|
||||
Direction from which azimuthal angle is measured. Defaults to the positive
|
||||
x-direction.
|
||||
x-direction.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
|
@ -104,7 +104,7 @@ class PolarAzimuthal(UnitSphere):
|
|||
self.phi = phi
|
||||
else:
|
||||
self.phi = Uniform(0., 2*pi)
|
||||
|
||||
|
||||
@property
|
||||
def reference_vwu(self):
|
||||
return self._reference_vwu
|
||||
|
|
@ -114,8 +114,8 @@ class PolarAzimuthal(UnitSphere):
|
|||
cv.check_type('reference v direction', vwu, Iterable, Real)
|
||||
vwu = np.asarray(vwu)
|
||||
uvw = self.reference_uvw
|
||||
cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu))
|
||||
vwu -= vwu.dot(uvw)*uvw
|
||||
cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu))
|
||||
vwu -= vwu.dot(uvw)*uvw
|
||||
cv.check_less_than('reference v direction must be orthogonal to reference u direction', np.abs(vwu.dot(uvw)), 1e-6)
|
||||
self._reference_vwu = vwu/np.linalg.norm(vwu)
|
||||
|
||||
|
|
@ -137,21 +137,30 @@ class PolarAzimuthal(UnitSphere):
|
|||
cv.check_type('azimuthal angle', phi, Univariate)
|
||||
self._phi = phi
|
||||
|
||||
def to_xml_element(self):
|
||||
def to_xml_element(self, element_name: str = None):
|
||||
"""Return XML representation of the angular distribution
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_name : str, optional
|
||||
XML element name
|
||||
|
||||
Returns
|
||||
-------
|
||||
element : lxml.etree._Element
|
||||
XML element containing angular distribution data
|
||||
|
||||
"""
|
||||
element = ET.Element('angle')
|
||||
if element_name is not None:
|
||||
element = ET.Element(element_name)
|
||||
else:
|
||||
element = ET.Element('angle')
|
||||
|
||||
element.set("type", "mu-phi")
|
||||
if self.reference_uvw is not None:
|
||||
element.set("reference_uvw", ' '.join(map(str, self.reference_uvw)))
|
||||
if self.reference_vwu is not None:
|
||||
element.set("reference_vwu", ' '.join(map(str, self.reference_vwu)))
|
||||
element.set("reference_vwu", ' '.join(map(str, self.reference_vwu)))
|
||||
element.append(self.mu.to_xml_element('mu'))
|
||||
element.append(self.phi.to_xml_element('phi'))
|
||||
return element
|
||||
|
|
@ -177,17 +186,48 @@ class PolarAzimuthal(UnitSphere):
|
|||
mu_phi.reference_uvw = uvw
|
||||
vwu = get_elem_list(elem, "reference_vwu", float)
|
||||
if vwu is not None:
|
||||
mu_phi.reference_vwu = vwu
|
||||
mu_phi.reference_vwu = vwu
|
||||
mu_phi.mu = Univariate.from_xml_element(elem.find('mu'))
|
||||
mu_phi.phi = Univariate.from_xml_element(elem.find('phi'))
|
||||
return mu_phi
|
||||
|
||||
|
||||
class Isotropic(UnitSphere):
|
||||
"""Isotropic angular distribution."""
|
||||
"""Isotropic angular distribution.
|
||||
|
||||
def __init__(self):
|
||||
Parameters
|
||||
----------
|
||||
bias : openmc.stats.PolarAzimuthal, optional
|
||||
Distribution for biased sampling.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
bias : openmc.stats.PolarAzimuthal or None
|
||||
Distribution for biased sampling
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bias: PolarAzimuthal | None = None):
|
||||
super().__init__()
|
||||
self.bias = bias
|
||||
|
||||
@property
|
||||
def bias(self):
|
||||
return self._bias
|
||||
|
||||
@bias.setter
|
||||
def bias(self, bias):
|
||||
cv.check_type('Biasing distribution', bias, PolarAzimuthal, none_ok=True)
|
||||
if bias is not None:
|
||||
if (bias.mu.bias is not None) or (bias.phi.bias is not None):
|
||||
raise RuntimeError('Biasing distributions should not have their own bias.')
|
||||
elif (bias.mu.support != (-1., 1.)
|
||||
or not np.all(np.isclose(bias.phi.support, (0., 2*np.pi)))):
|
||||
raise ValueError("Biasing distribution for an isotropic "
|
||||
"distribution should be supported on "
|
||||
"mu=(-1.0,1.0) and phi=(0.0,2*pi).")
|
||||
|
||||
self._bias = bias
|
||||
|
||||
def to_xml_element(self):
|
||||
"""Return XML representation of the isotropic distribution
|
||||
|
|
@ -200,6 +240,15 @@ class Isotropic(UnitSphere):
|
|||
"""
|
||||
element = ET.Element('angle')
|
||||
element.set("type", "isotropic")
|
||||
|
||||
if self.bias is not None:
|
||||
bias_dist = self.bias
|
||||
if (bias_dist.mu.bias is not None) or (bias_dist.phi.bias is not None):
|
||||
raise RuntimeError('Biasing distributions should not have their own bias!')
|
||||
else:
|
||||
bias_elem = self.bias.to_xml_element("bias")
|
||||
element.append(bias_elem)
|
||||
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
|
|
@ -217,7 +266,13 @@ class Isotropic(UnitSphere):
|
|||
Isotropic distribution generated from XML element
|
||||
|
||||
"""
|
||||
return cls()
|
||||
bias_elem = elem.find('bias')
|
||||
if bias_elem is not None:
|
||||
bias_dist = PolarAzimuthal.from_xml_element(bias_elem)
|
||||
return cls(bias=bias_dist)
|
||||
else:
|
||||
return cls()
|
||||
|
||||
|
||||
|
||||
class Monodirectional(UnitSphere):
|
||||
|
|
@ -672,6 +727,9 @@ class MeshSpatial(Spatial):
|
|||
volume_normalized : bool, optional
|
||||
Whether or not the strengths will be multiplied by element volumes at
|
||||
runtime. Default is True.
|
||||
bias : iterable of float, optional
|
||||
An iterable of values giving the selection weights assigned to each
|
||||
element during biased sampling.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
|
@ -682,12 +740,16 @@ class MeshSpatial(Spatial):
|
|||
volume_normalized : bool
|
||||
Whether or not the strengths will be multiplied by element volumes at
|
||||
runtime.
|
||||
bias : numpy.ndarray or None
|
||||
Distribution for biased sampling
|
||||
"""
|
||||
|
||||
def __init__(self, mesh, strengths=None, volume_normalized=True):
|
||||
def __init__(self, mesh, strengths=None, volume_normalized=True,
|
||||
bias: Sequence[float] | None = None):
|
||||
self.mesh = mesh
|
||||
self.strengths = strengths
|
||||
self.volume_normalized = volume_normalized
|
||||
self.bias = bias
|
||||
|
||||
@property
|
||||
def mesh(self):
|
||||
|
|
@ -720,6 +782,23 @@ class MeshSpatial(Spatial):
|
|||
else:
|
||||
self._strengths = None
|
||||
|
||||
@property
|
||||
def bias(self):
|
||||
return self._bias
|
||||
|
||||
@bias.setter
|
||||
def bias(self, given_bias):
|
||||
if given_bias is not None:
|
||||
cv.check_type('Biasing strengths array', given_bias, Iterable, Real)
|
||||
bias_array = np.asarray(given_bias, dtype=float).flatten()
|
||||
if bias_array.size != self.strengths.size:
|
||||
raise ValueError(
|
||||
'Bias strengths array must have same size as strengths array.')
|
||||
else:
|
||||
self._bias = bias_array
|
||||
else:
|
||||
self._bias = None
|
||||
|
||||
@property
|
||||
def num_strength_bins(self):
|
||||
if self.strengths is None:
|
||||
|
|
@ -745,6 +824,9 @@ class MeshSpatial(Spatial):
|
|||
subelement = ET.SubElement(element, 'strengths')
|
||||
subelement.text = ' '.join(str(e) for e in self.strengths)
|
||||
|
||||
if self.bias is not None:
|
||||
Univariate._append_array_bias_to_xml(self, element)
|
||||
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
|
|
@ -774,7 +856,8 @@ class MeshSpatial(Spatial):
|
|||
|
||||
volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true'
|
||||
strengths = get_elem_list(elem, 'strengths', float)
|
||||
return cls(meshes[mesh_id], strengths, volume_normalized)
|
||||
bias_strengths = Univariate._read_array_bias_from_xml(elem)
|
||||
return cls(meshes[mesh_id], strengths, volume_normalized, bias=bias_strengths)
|
||||
|
||||
|
||||
class PointCloud(Spatial):
|
||||
|
|
@ -792,6 +875,9 @@ class PointCloud(Spatial):
|
|||
strengths : iterable of float, optional
|
||||
An iterable of values that represents the relative probabilty of each
|
||||
point.
|
||||
bias : iterable of float, optional
|
||||
An iterable of values representing the relative probability of each
|
||||
point under biased sampling.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
|
@ -799,15 +885,19 @@ class PointCloud(Spatial):
|
|||
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
|
||||
bias : numpy.ndarray or None
|
||||
An array of relative probabilities for biased sampling of mesh points
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
positions: Sequence[Sequence[float]],
|
||||
strengths: Sequence[float] | None = None
|
||||
strengths: Sequence[float] | None = None,
|
||||
bias: Sequence[float] | None = None
|
||||
):
|
||||
self.positions = positions
|
||||
self.strengths = strengths
|
||||
self.bias = bias
|
||||
|
||||
@property
|
||||
def positions(self) -> np.ndarray:
|
||||
|
|
@ -836,6 +926,23 @@ class PointCloud(Spatial):
|
|||
raise ValueError('strengths must have the same length as positions')
|
||||
self._strengths = strengths
|
||||
|
||||
@property
|
||||
def bias(self):
|
||||
return self._bias
|
||||
|
||||
@bias.setter
|
||||
def bias(self, given_bias):
|
||||
if given_bias is not None:
|
||||
cv.check_type('Biasing strengths array', given_bias, Iterable, Real)
|
||||
bias_array = np.asarray(given_bias, dtype=float).flatten()
|
||||
if bias_array.size != self.strengths.size:
|
||||
raise ValueError(
|
||||
'Bias strengths array must have same size as strengths array.')
|
||||
else:
|
||||
self._bias = bias_array
|
||||
else:
|
||||
self._bias = None
|
||||
|
||||
@property
|
||||
def num_strength_bins(self) -> int:
|
||||
if self.strengths is None:
|
||||
|
|
@ -861,6 +968,9 @@ class PointCloud(Spatial):
|
|||
subelement = ET.SubElement(element, 'strengths')
|
||||
subelement.text = ' '.join(str(e) for e in self.strengths)
|
||||
|
||||
if self.bias is not None:
|
||||
Univariate._append_array_bias_to_xml(self, element)
|
||||
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
|
|
@ -883,8 +993,8 @@ class PointCloud(Spatial):
|
|||
positions = np.array(coord_data).reshape((-1, 3))
|
||||
|
||||
strengths = get_elem_list(elem, 'strengths', float)
|
||||
|
||||
return cls(positions, strengths)
|
||||
bias_strengths = Univariate._read_array_bias_from_xml(elem)
|
||||
return cls(positions, strengths, bias=bias_strengths)
|
||||
|
||||
|
||||
class Box(Spatial):
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -70,8 +70,8 @@ ChainNuclide::~ChainNuclide()
|
|||
void DecayPhotonAngleEnergy::sample(
|
||||
double E_in, double& E_out, double& mu, uint64_t* seed) const
|
||||
{
|
||||
E_out = photon_energy_->sample(seed);
|
||||
mu = Uniform(-1., 1.).sample(seed);
|
||||
E_out = photon_energy_->sample(seed).first;
|
||||
mu = Uniform(-1., 1.).sample(seed).first;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <stdexcept> // for runtime_error
|
||||
#include <string> // for string, stod
|
||||
|
||||
#include "openmc/constants.h"
|
||||
#include "openmc/error.h"
|
||||
#include "openmc/math_functions.h"
|
||||
#include "openmc/random_dist.h"
|
||||
|
|
@ -16,6 +17,74 @@
|
|||
|
||||
namespace openmc {
|
||||
|
||||
//==============================================================================
|
||||
// Helper function for computing importance weights from biased sampling
|
||||
//==============================================================================
|
||||
|
||||
vector<double> compute_importance_weights(
|
||||
const vector<double>& p, const vector<double>& b)
|
||||
{
|
||||
std::size_t n = p.size();
|
||||
|
||||
// Normalize original probabilities
|
||||
double sum_p = std::accumulate(p.begin(), p.end(), 0.0);
|
||||
vector<double> p_norm(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
p_norm[i] = p[i] / sum_p;
|
||||
}
|
||||
|
||||
// Normalize bias probabilities
|
||||
double sum_b = std::accumulate(b.begin(), b.end(), 0.0);
|
||||
vector<double> b_norm(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
b_norm[i] = b[i] / sum_b;
|
||||
}
|
||||
|
||||
// Compute importance weights
|
||||
vector<double> weights(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
weights[i] = (b_norm[i] == 0.0) ? INFTY : p_norm[i] / b_norm[i];
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
std::pair<double, double> Distribution::sample(uint64_t* seed) const
|
||||
{
|
||||
if (bias_) {
|
||||
// Sample from the bias distribution and compute importance weight
|
||||
double val = bias_->sample_unbiased(seed);
|
||||
double wgt = this->evaluate(val) / bias_->evaluate(val);
|
||||
return {val, wgt};
|
||||
} else {
|
||||
// Unbiased sampling: return sampled value with weight 1.0
|
||||
double val = sample_unbiased(seed);
|
||||
return {val, 1.0};
|
||||
}
|
||||
}
|
||||
|
||||
// PDF evaluation not supported for all distribution types
|
||||
double Distribution::evaluate(double x) const
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"PDF evaluation not implemented for this distribution type.");
|
||||
}
|
||||
|
||||
void Distribution::read_bias_from_xml(pugi::xml_node node)
|
||||
{
|
||||
if (check_for_node(node, "bias")) {
|
||||
pugi::xml_node bias_node = node.child("bias");
|
||||
|
||||
if (check_for_node(bias_node, "bias")) {
|
||||
openmc::fatal_error(
|
||||
"Distribution has a bias distribution with its own bias distribution. "
|
||||
"Please ensure bias distributions do not have their own bias.");
|
||||
}
|
||||
|
||||
UPtrDist bias = distribution_from_xml(bias_node);
|
||||
this->set_bias(std::move(bias));
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// DiscreteIndex implementation
|
||||
//==============================================================================
|
||||
|
|
@ -36,7 +105,6 @@ DiscreteIndex::DiscreteIndex(span<const double> p)
|
|||
void DiscreteIndex::assign(span<const double> p)
|
||||
{
|
||||
prob_.assign(p.begin(), p.end());
|
||||
|
||||
this->init_alias();
|
||||
}
|
||||
|
||||
|
|
@ -115,24 +183,55 @@ void DiscreteIndex::normalize()
|
|||
// Discrete implementation
|
||||
//==============================================================================
|
||||
|
||||
Discrete::Discrete(pugi::xml_node node) : di_(node)
|
||||
Discrete::Discrete(pugi::xml_node node)
|
||||
{
|
||||
auto params = get_node_array<double>(node, "parameters");
|
||||
|
||||
std::size_t n = params.size() / 2;
|
||||
|
||||
// First half is x values, second half is probabilities
|
||||
x_.assign(params.begin(), params.begin() + n);
|
||||
const double* p = params.data() + n;
|
||||
|
||||
// Check for bias
|
||||
if (check_for_node(node, "bias")) {
|
||||
// Get bias probabilities
|
||||
auto bias_params = get_node_array<double>(node, "bias");
|
||||
if (bias_params.size() != n) {
|
||||
openmc::fatal_error(
|
||||
"Size mismatch: Attempted to bias Discrete distribution with " +
|
||||
std::to_string(n) + " probability entries using a bias with " +
|
||||
std::to_string(bias_params.size()) +
|
||||
" entries. Please ensure distributions have the same size.");
|
||||
}
|
||||
|
||||
// Compute importance weights
|
||||
vector<double> p_vec(p, p + n);
|
||||
weight_ = compute_importance_weights(p_vec, bias_params);
|
||||
|
||||
// Initialize DiscreteIndex with bias probabilities for sampling
|
||||
di_.assign(bias_params);
|
||||
} else {
|
||||
// Unbiased case: weight_ stays empty
|
||||
di_.assign({p, n});
|
||||
}
|
||||
}
|
||||
|
||||
Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n})
|
||||
{
|
||||
|
||||
x_.assign(x, x + n);
|
||||
}
|
||||
|
||||
double Discrete::sample(uint64_t* seed) const
|
||||
std::pair<double, double> Discrete::sample(uint64_t* seed) const
|
||||
{
|
||||
return x_[di_.sample(seed)];
|
||||
size_t idx = di_.sample(seed);
|
||||
double wgt = weight_.empty() ? 1.0 : weight_[idx];
|
||||
return {x_[idx], wgt};
|
||||
}
|
||||
|
||||
double Discrete::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
size_t idx = di_.sample(seed);
|
||||
return x_[idx];
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -149,13 +248,26 @@ Uniform::Uniform(pugi::xml_node node)
|
|||
|
||||
a_ = params.at(0);
|
||||
b_ = params.at(1);
|
||||
|
||||
read_bias_from_xml(node);
|
||||
}
|
||||
|
||||
double Uniform::sample(uint64_t* seed) const
|
||||
double Uniform::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
return a_ + prn(seed) * (b_ - a_);
|
||||
}
|
||||
|
||||
double Uniform::evaluate(double x) const
|
||||
{
|
||||
if (x <= a()) {
|
||||
return 0.0;
|
||||
} else if (x >= b()) {
|
||||
return 0.0;
|
||||
} else {
|
||||
return 1 / (b() - a());
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// PowerLaw implementation
|
||||
//==============================================================================
|
||||
|
|
@ -175,9 +287,24 @@ PowerLaw::PowerLaw(pugi::xml_node node)
|
|||
offset_ = std::pow(a, n + 1);
|
||||
span_ = std::pow(b, n + 1) - offset_;
|
||||
ninv_ = 1 / (n + 1);
|
||||
|
||||
read_bias_from_xml(node);
|
||||
}
|
||||
|
||||
double PowerLaw::sample(uint64_t* seed) const
|
||||
double PowerLaw::evaluate(double x) const
|
||||
{
|
||||
if (x <= a()) {
|
||||
return 0.0;
|
||||
} else if (x >= b()) {
|
||||
return 0.0;
|
||||
} else {
|
||||
int pwr = n() + 1;
|
||||
double norm = pwr / span_;
|
||||
return norm * std::pow(std::fabs(x), n());
|
||||
}
|
||||
}
|
||||
|
||||
double PowerLaw::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
return std::pow(offset_ + prn(seed) * span_, ninv_);
|
||||
}
|
||||
|
|
@ -189,13 +316,21 @@ double PowerLaw::sample(uint64_t* seed) const
|
|||
Maxwell::Maxwell(pugi::xml_node node)
|
||||
{
|
||||
theta_ = std::stod(get_node_value(node, "parameters"));
|
||||
|
||||
read_bias_from_xml(node);
|
||||
}
|
||||
|
||||
double Maxwell::sample(uint64_t* seed) const
|
||||
double Maxwell::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
return maxwell_spectrum(theta_, seed);
|
||||
}
|
||||
|
||||
double Maxwell::evaluate(double x) const
|
||||
{
|
||||
double c = (2.0 / SQRT_PI) * std::pow(theta_, -1.5);
|
||||
return c * std::sqrt(x) * std::exp(-x / theta_);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Watt implementation
|
||||
//==============================================================================
|
||||
|
|
@ -209,13 +344,22 @@ Watt::Watt(pugi::xml_node node)
|
|||
|
||||
a_ = params.at(0);
|
||||
b_ = params.at(1);
|
||||
|
||||
read_bias_from_xml(node);
|
||||
}
|
||||
|
||||
double Watt::sample(uint64_t* seed) const
|
||||
double Watt::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
return watt_spectrum(a_, b_, seed);
|
||||
}
|
||||
|
||||
double Watt::evaluate(double x) const
|
||||
{
|
||||
double c =
|
||||
2.0 / (std::sqrt(PI * b_) * std::pow(a_, 1.5) * std::exp(a_ * b_ / 4.0));
|
||||
return c * std::exp(-x / a_) * std::sinh(std::sqrt(b_ * x));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Normal implementation
|
||||
//==============================================================================
|
||||
|
|
@ -229,13 +373,22 @@ Normal::Normal(pugi::xml_node node)
|
|||
|
||||
mean_value_ = params.at(0);
|
||||
std_dev_ = params.at(1);
|
||||
|
||||
read_bias_from_xml(node);
|
||||
}
|
||||
|
||||
double Normal::sample(uint64_t* seed) const
|
||||
double Normal::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
return normal_variate(mean_value_, std_dev_, seed);
|
||||
}
|
||||
|
||||
double Normal::evaluate(double x) const
|
||||
{
|
||||
return (1.0 / (std::sqrt(2.0 / PI) * std_dev_)) *
|
||||
std::exp(-(std::pow((x - mean_value_), 2.0)) /
|
||||
(2.0 * std::pow(std_dev_, 2.0)));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Tabular implementation
|
||||
//==============================================================================
|
||||
|
|
@ -266,6 +419,8 @@ Tabular::Tabular(pugi::xml_node node)
|
|||
const double* x = params.data();
|
||||
const double* p = x + n;
|
||||
init(x, p, n);
|
||||
|
||||
read_bias_from_xml(node);
|
||||
}
|
||||
|
||||
Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp,
|
||||
|
|
@ -314,7 +469,7 @@ void Tabular::init(
|
|||
}
|
||||
}
|
||||
|
||||
double Tabular::sample(uint64_t* seed) const
|
||||
double Tabular::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
// Sample value of CDF
|
||||
double c = prn(seed);
|
||||
|
|
@ -356,11 +511,39 @@ double Tabular::sample(uint64_t* seed) const
|
|||
}
|
||||
}
|
||||
|
||||
double Tabular::evaluate(double x) const
|
||||
{
|
||||
int i;
|
||||
|
||||
if (interp_ == Interpolation::histogram) {
|
||||
i = std::upper_bound(x_.begin(), x_.end(), x) - x_.begin() - 1;
|
||||
if (i < 0 || i >= static_cast<int>(p_.size())) {
|
||||
return 0.0;
|
||||
} else {
|
||||
return p_[i];
|
||||
}
|
||||
} else {
|
||||
i = std::lower_bound(x_.begin(), x_.end(), x) - x_.begin() - 1;
|
||||
|
||||
if (i < 0 || i >= static_cast<int>(p_.size()) - 1) {
|
||||
return 0.0;
|
||||
} else {
|
||||
double x0 = x_[i];
|
||||
double x1 = x_[i + 1];
|
||||
double p0 = p_[i];
|
||||
double p1 = p_[i + 1];
|
||||
|
||||
double t = (x - x0) / (x1 - x0);
|
||||
return (1 - t) * p0 + t * p1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Equiprobable implementation
|
||||
//==============================================================================
|
||||
|
||||
double Equiprobable::sample(uint64_t* seed) const
|
||||
double Equiprobable::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
std::size_t n = x_.size();
|
||||
|
||||
|
|
@ -372,13 +555,27 @@ double Equiprobable::sample(uint64_t* seed) const
|
|||
return xl + ((n - 1) * r - i) * (xr - xl);
|
||||
}
|
||||
|
||||
double Equiprobable::evaluate(double x) const
|
||||
{
|
||||
double x_min = *std::min_element(x_.begin(), x_.end());
|
||||
double x_max = *std::max_element(x_.begin(), x_.end());
|
||||
|
||||
if (x < x_min || x > x_max) {
|
||||
return 0.0;
|
||||
} else {
|
||||
return 1.0 / (x_max - x_min);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Mixture implementation
|
||||
//==============================================================================
|
||||
|
||||
Mixture::Mixture(pugi::xml_node node)
|
||||
{
|
||||
double cumsum = 0.0;
|
||||
vector<double> probabilities;
|
||||
|
||||
// First pass: collect distributions and their probabilities
|
||||
for (pugi::xml_node pair : node.children("pair")) {
|
||||
// Check that required data exists
|
||||
if (!pair.attribute("probability"))
|
||||
|
|
@ -386,39 +583,60 @@ Mixture::Mixture(pugi::xml_node node)
|
|||
if (!pair.child("dist"))
|
||||
fatal_error("Mixture pair element does not have a distribution.");
|
||||
|
||||
// cummulative sum of probabilities
|
||||
// Get probability and distribution
|
||||
double p = std::stod(pair.attribute("probability").value());
|
||||
|
||||
// Save cummulative probability and distribution
|
||||
auto dist = distribution_from_xml(pair.child("dist"));
|
||||
cumsum += p * dist->integral();
|
||||
|
||||
distribution_.push_back(std::make_pair(cumsum, std::move(dist)));
|
||||
// Weight probability by the distribution's integral
|
||||
double weighted_prob = p * dist->integral();
|
||||
probabilities.push_back(weighted_prob);
|
||||
distribution_.push_back(std::move(dist));
|
||||
}
|
||||
|
||||
// Save integral of distribution
|
||||
integral_ = cumsum;
|
||||
// Save sum of weighted probabilities
|
||||
integral_ = std::accumulate(probabilities.begin(), probabilities.end(), 0.0);
|
||||
|
||||
// Normalize cummulative probabilities to 1
|
||||
for (auto& pair : distribution_) {
|
||||
pair.first /= cumsum;
|
||||
std::size_t n = probabilities.size();
|
||||
|
||||
// Check for bias
|
||||
if (check_for_node(node, "bias")) {
|
||||
// Get bias probabilities
|
||||
auto bias_params = get_node_array<double>(node, "bias");
|
||||
if (bias_params.size() != n) {
|
||||
openmc::fatal_error(
|
||||
"Size mismatch: Attempted to bias Mixture distribution with " +
|
||||
std::to_string(n) + " components using a bias with " +
|
||||
std::to_string(bias_params.size()) +
|
||||
" entries. Please ensure distributions have the same size.");
|
||||
}
|
||||
|
||||
// Compute importance weights
|
||||
weight_ = compute_importance_weights(probabilities, bias_params);
|
||||
|
||||
// Initialize DiscreteIndex with bias probabilities for sampling
|
||||
di_.assign(bias_params);
|
||||
} else {
|
||||
// Unbiased case: weight_ stays empty
|
||||
di_.assign(probabilities);
|
||||
}
|
||||
}
|
||||
|
||||
double Mixture::sample(uint64_t* seed) const
|
||||
std::pair<double, double> Mixture::sample(uint64_t* seed) const
|
||||
{
|
||||
// Sample value of CDF
|
||||
const double p = prn(seed);
|
||||
|
||||
// find matching distribution
|
||||
const auto it = std::lower_bound(distribution_.cbegin(), distribution_.cend(),
|
||||
p, [](const DistPair& pair, double p) { return pair.first < p; });
|
||||
|
||||
// This should not happen. Catch it
|
||||
assert(it != distribution_.cend());
|
||||
size_t idx = di_.sample(seed);
|
||||
|
||||
// Sample the chosen distribution
|
||||
return it->second->sample(seed);
|
||||
auto [val, sub_wgt] = distribution_[idx]->sample(seed);
|
||||
|
||||
// Multiply by component selection weight
|
||||
double mix_wgt = weight_.empty() ? 1.0 : weight_[idx];
|
||||
return {val, mix_wgt * sub_wgt};
|
||||
}
|
||||
|
||||
double Mixture::sample_unbiased(uint64_t* seed) const
|
||||
{
|
||||
size_t idx = di_.sample(seed);
|
||||
return distribution_[idx]->sample(seed).first;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ double AngleDistribution::sample(double E, uint64_t* seed) const
|
|||
++i;
|
||||
|
||||
// Sample i-th distribution
|
||||
double mu = distribution_[i]->sample(seed);
|
||||
double mu = distribution_[i]->sample(seed).first;
|
||||
|
||||
// Make sure mu is in range [-1,1] and return
|
||||
if (std::abs(mu) > 1.0)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ unique_ptr<UnitSphereDistribution> UnitSphereDistribution::create(
|
|||
if (check_for_node(node, "type"))
|
||||
type = get_node_value(node, "type", true, true);
|
||||
if (type == "isotropic") {
|
||||
return UPtrAngle {new Isotropic()};
|
||||
return UPtrAngle {new Isotropic(node)};
|
||||
} else if (type == "monodirectional") {
|
||||
return UPtrAngle {new Monodirectional(node)};
|
||||
} else if (type == "mu-phi") {
|
||||
|
|
@ -82,27 +82,63 @@ PolarAzimuthal::PolarAzimuthal(pugi::xml_node node)
|
|||
}
|
||||
}
|
||||
|
||||
Direction PolarAzimuthal::sample(uint64_t* seed) const
|
||||
std::pair<Direction, double> PolarAzimuthal::sample(uint64_t* seed) const
|
||||
{
|
||||
return sample_impl(seed, false);
|
||||
}
|
||||
|
||||
std::pair<Direction, double> PolarAzimuthal::sample_as_bias(
|
||||
uint64_t* seed) const
|
||||
{
|
||||
return sample_impl(seed, true);
|
||||
}
|
||||
|
||||
std::pair<Direction, double> PolarAzimuthal::sample_impl(
|
||||
uint64_t* seed, bool return_pdf) const
|
||||
{
|
||||
// Sample cosine of polar angle
|
||||
double mu = mu_->sample(seed);
|
||||
if (mu == 1.0)
|
||||
return u_ref_;
|
||||
if (mu == -1.0)
|
||||
return -u_ref_;
|
||||
auto [mu, mu_wgt] = mu_->sample(seed);
|
||||
|
||||
// Sample azimuthal angle
|
||||
double phi = phi_->sample(seed);
|
||||
auto [phi, phi_wgt] = phi_->sample(seed);
|
||||
|
||||
// Compute either the PDF value or the importance weight
|
||||
double weight =
|
||||
return_pdf ? (mu_->evaluate(mu) * phi_->evaluate(phi)) : (mu_wgt * phi_wgt);
|
||||
|
||||
if (mu == 1.0)
|
||||
return {u_ref_, weight};
|
||||
if (mu == -1.0)
|
||||
return {-u_ref_, weight};
|
||||
|
||||
double f = std::sqrt(1 - mu * mu);
|
||||
|
||||
return mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_;
|
||||
return {mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_,
|
||||
weight};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Isotropic implementation
|
||||
//==============================================================================
|
||||
|
||||
Isotropic::Isotropic(pugi::xml_node node) : UnitSphereDistribution {node}
|
||||
{
|
||||
if (check_for_node(node, "bias")) {
|
||||
pugi::xml_node bias_node = node.child("bias");
|
||||
std::string bias_type = get_node_value(bias_node, "type", true, true);
|
||||
if (bias_type != "mu-phi") {
|
||||
openmc::fatal_error(
|
||||
"Isotropic distributions may only be biased by a PolarAzimuthal.");
|
||||
}
|
||||
auto bias = std::make_unique<PolarAzimuthal>(bias_node);
|
||||
if (bias->mu()->bias() || bias->phi()->bias()) {
|
||||
openmc::fatal_error(
|
||||
"Attempted to bias Isotropic distribution with a biased PolarAzimuthal "
|
||||
"distribution. Please ensure bias distributions are unbiased.");
|
||||
}
|
||||
this->set_bias(std::move(bias));
|
||||
}
|
||||
}
|
||||
|
||||
Direction isotropic_direction(uint64_t* seed)
|
||||
{
|
||||
double phi = uniform_distribution(0., 2.0 * PI, seed);
|
||||
|
|
@ -111,18 +147,23 @@ Direction isotropic_direction(uint64_t* seed)
|
|||
std::sqrt(1.0 - mu * mu) * std::sin(phi)};
|
||||
}
|
||||
|
||||
Direction Isotropic::sample(uint64_t* seed) const
|
||||
std::pair<Direction, double> Isotropic::sample(uint64_t* seed) const
|
||||
{
|
||||
return isotropic_direction(seed);
|
||||
if (bias()) {
|
||||
auto [val, eval] = bias()->sample_as_bias(seed);
|
||||
return {val, 1.0 / (4.0 * PI * eval)};
|
||||
} else {
|
||||
return {isotropic_direction(seed), 1.0};
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Monodirectional implementation
|
||||
//==============================================================================
|
||||
|
||||
Direction Monodirectional::sample(uint64_t* seed) const
|
||||
std::pair<Direction, double> Monodirectional::sample(uint64_t* seed) const
|
||||
{
|
||||
return u_ref_;
|
||||
return {u_ref_, 1.0};
|
||||
}
|
||||
|
||||
} // namespace openmc
|
||||
|
|
|
|||
|
|
@ -80,9 +80,13 @@ CartesianIndependent::CartesianIndependent(pugi::xml_node node)
|
|||
}
|
||||
}
|
||||
|
||||
Position CartesianIndependent::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> CartesianIndependent::sample(uint64_t* seed) const
|
||||
{
|
||||
return {x_->sample(seed), y_->sample(seed), z_->sample(seed)};
|
||||
auto [x_val, x_wgt] = x_->sample(seed);
|
||||
auto [y_val, y_wgt] = y_->sample(seed);
|
||||
auto [z_val, z_wgt] = z_->sample(seed);
|
||||
Position xi {x_val, y_val, z_val};
|
||||
return {xi, x_wgt * y_wgt * z_wgt};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -139,14 +143,16 @@ CylindricalIndependent::CylindricalIndependent(pugi::xml_node node)
|
|||
}
|
||||
}
|
||||
|
||||
Position CylindricalIndependent::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> CylindricalIndependent::sample(uint64_t* seed) const
|
||||
{
|
||||
double r = r_->sample(seed);
|
||||
double phi = phi_->sample(seed);
|
||||
auto [r, r_wgt] = r_->sample(seed);
|
||||
auto [phi, phi_wgt] = phi_->sample(seed);
|
||||
auto [z, z_wgt] = z_->sample(seed);
|
||||
double x = r * cos(phi) + origin_.x;
|
||||
double y = r * sin(phi) + origin_.y;
|
||||
double z = z_->sample(seed) + origin_.z;
|
||||
return {x, y, z};
|
||||
z += origin_.z;
|
||||
Position xi {x, y, z};
|
||||
return {xi, r_wgt * phi_wgt * z_wgt};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -203,16 +209,17 @@ SphericalIndependent::SphericalIndependent(pugi::xml_node node)
|
|||
}
|
||||
}
|
||||
|
||||
Position SphericalIndependent::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> SphericalIndependent::sample(uint64_t* seed) const
|
||||
{
|
||||
double r = r_->sample(seed);
|
||||
double cos_theta = cos_theta_->sample(seed);
|
||||
double phi = phi_->sample(seed);
|
||||
auto [r, r_wgt] = r_->sample(seed);
|
||||
auto [cos_theta, cos_theta_wgt] = cos_theta_->sample(seed);
|
||||
auto [phi, phi_wgt] = phi_->sample(seed);
|
||||
// sin(theta) by sin**2 + cos**2 = 1
|
||||
double x = r * std::sqrt(1 - cos_theta * cos_theta) * cos(phi) + origin_.x;
|
||||
double y = r * std::sqrt(1 - cos_theta * cos_theta) * sin(phi) + origin_.y;
|
||||
double z = r * cos_theta + origin_.z;
|
||||
return {x, y, z};
|
||||
Position xi {x, y, z};
|
||||
return {xi, r_wgt * cos_theta_wgt * phi_wgt};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -260,6 +267,37 @@ MeshSpatial::MeshSpatial(pugi::xml_node node)
|
|||
}
|
||||
|
||||
elem_idx_dist_.assign(strengths);
|
||||
|
||||
if (check_for_node(node, "bias")) {
|
||||
pugi::xml_node bias_node = node.child("bias");
|
||||
|
||||
if (check_for_node(bias_node, "strengths")) {
|
||||
std::vector<double> bias_strengths(n_bins, 1.0);
|
||||
bias_strengths = get_node_array<double>(node, "strengths");
|
||||
|
||||
if (bias_strengths.size() != n_bins) {
|
||||
fatal_error(
|
||||
fmt::format("Number of entries in the bias strengths array {} does "
|
||||
"not match the number of entities in mesh {} ({}).",
|
||||
bias_strengths.size(), mesh_id, n_bins));
|
||||
}
|
||||
|
||||
if (get_node_value_bool(node, "volume_normalized")) {
|
||||
for (int i = 0; i < n_bins; i++) {
|
||||
bias_strengths[i] *= this->mesh()->volume(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute importance weights
|
||||
weight_ = compute_importance_weights(strengths, bias_strengths);
|
||||
|
||||
// Re-initialize DiscreteIndex with bias strengths for sampling
|
||||
elem_idx_dist_.assign(bias_strengths);
|
||||
} else {
|
||||
fatal_error(fmt::format(
|
||||
"Bias node for mesh {} found without strengths array.", mesh_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeshSpatial::MeshSpatial(int32_t mesh_idx, span<const double> strengths)
|
||||
|
|
@ -295,9 +333,11 @@ std::pair<int32_t, Position> MeshSpatial::sample_mesh(uint64_t* seed) const
|
|||
return {elem_idx, mesh()->sample_element(elem_idx, seed)};
|
||||
}
|
||||
|
||||
Position MeshSpatial::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> MeshSpatial::sample(uint64_t* seed) const
|
||||
{
|
||||
return this->sample_mesh(seed).second;
|
||||
auto [elem_idx, u] = this->sample_mesh(seed);
|
||||
double wgt = weight_.empty() ? 1.0 : weight_[elem_idx];
|
||||
return {u, wgt};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -328,6 +368,31 @@ PointCloud::PointCloud(pugi::xml_node node)
|
|||
}
|
||||
|
||||
point_idx_dist_.assign(strengths);
|
||||
|
||||
if (check_for_node(node, "bias")) {
|
||||
pugi::xml_node bias_node = node.child("bias");
|
||||
|
||||
if (check_for_node(bias_node, "strengths")) {
|
||||
std::vector<double> bias_strengths(point_cloud_.size(), 1.0);
|
||||
bias_strengths = get_node_array<double>(node, "strengths");
|
||||
|
||||
if (bias_strengths.size() != point_cloud_.size()) {
|
||||
fatal_error(
|
||||
fmt::format("Number of entries in the bias strengths array {} does "
|
||||
"not match the number of spatial points provided {}.",
|
||||
bias_strengths.size(), point_cloud_.size()));
|
||||
}
|
||||
|
||||
// Compute importance weights
|
||||
weight_ = compute_importance_weights(strengths, bias_strengths);
|
||||
|
||||
// Re-initialize DiscreteIndex with bias strengths for sampling
|
||||
point_idx_dist_.assign(bias_strengths);
|
||||
} else {
|
||||
fatal_error(
|
||||
fmt::format("Bias node for PointCloud found without strengths array."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PointCloud::PointCloud(
|
||||
|
|
@ -337,10 +402,11 @@ PointCloud::PointCloud(
|
|||
point_idx_dist_.assign(strengths);
|
||||
}
|
||||
|
||||
Position PointCloud::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> PointCloud::sample(uint64_t* seed) const
|
||||
{
|
||||
int32_t index = point_idx_dist_.sample(seed);
|
||||
return point_cloud_[index];
|
||||
double wgt = weight_.empty() ? 1.0 : weight_[index];
|
||||
return {point_cloud_[index], wgt};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -360,10 +426,10 @@ SpatialBox::SpatialBox(pugi::xml_node node, bool fission)
|
|||
upper_right_ = Position {params[3], params[4], params[5]};
|
||||
}
|
||||
|
||||
Position SpatialBox::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> SpatialBox::sample(uint64_t* seed) const
|
||||
{
|
||||
Position xi {prn(seed), prn(seed), prn(seed)};
|
||||
return lower_left_ + xi * (upper_right_ - lower_left_);
|
||||
return {lower_left_ + xi * (upper_right_ - lower_left_), 1.0};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -382,9 +448,9 @@ SpatialPoint::SpatialPoint(pugi::xml_node node)
|
|||
r_ = Position {params.data()};
|
||||
}
|
||||
|
||||
Position SpatialPoint::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> SpatialPoint::sample(uint64_t* seed) const
|
||||
{
|
||||
return r_;
|
||||
return {r_, 1.0};
|
||||
}
|
||||
|
||||
} // namespace openmc
|
||||
|
|
|
|||
|
|
@ -247,9 +247,9 @@ void CorrelatedAngleEnergy::sample(
|
|||
// Find correlated angular distribution for closest outgoing energy bin
|
||||
if (r1 - c_k < c_k1 - r1 ||
|
||||
distribution_[l].interpolation == Interpolation::histogram) {
|
||||
mu = distribution_[l].angle[k]->sample(seed);
|
||||
mu = distribution_[l].angle[k]->sample(seed).first;
|
||||
} else {
|
||||
mu = distribution_[l].angle[k + 1]->sample(seed);
|
||||
mu = distribution_[l].angle[k + 1]->sample(seed).first;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -356,6 +356,8 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
|
|||
{
|
||||
SourceSite site;
|
||||
site.particle = particle_;
|
||||
double r_wgt = 1.0;
|
||||
double E_wgt = 1.0;
|
||||
|
||||
// Repeat sampling source location until a good site has been accepted
|
||||
bool accepted = false;
|
||||
|
|
@ -365,7 +367,9 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
|
|||
while (!accepted) {
|
||||
|
||||
// Sample spatial distribution
|
||||
site.r = space_->sample(seed);
|
||||
auto [r, r_wgt_temp] = space_->sample(seed);
|
||||
site.r = r;
|
||||
r_wgt = r_wgt_temp;
|
||||
|
||||
// Check if sampled position satisfies spatial constraints
|
||||
accepted = satisfies_spatial_constraints(site.r);
|
||||
|
|
@ -378,7 +382,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
|
|||
}
|
||||
|
||||
// Sample angle
|
||||
site.u = angle_->sample(seed);
|
||||
auto [u, u_wgt] = angle_->sample(seed);
|
||||
site.u = u;
|
||||
|
||||
site.wgt = r_wgt * u_wgt;
|
||||
|
||||
// Sample energy and time for neutron and photon sources
|
||||
if (settings::solver_type != SolverType::RANDOM_RAY) {
|
||||
|
|
@ -395,7 +402,9 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
|
|||
|
||||
while (true) {
|
||||
// Sample energy spectrum
|
||||
site.E = energy_->sample(seed);
|
||||
auto [E, E_wgt_temp] = energy_->sample(seed);
|
||||
site.E = E;
|
||||
E_wgt = E_wgt_temp;
|
||||
|
||||
// Resample if energy falls above maximum particle energy
|
||||
if (site.E < data::energy_max[p] &&
|
||||
|
|
@ -407,7 +416,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
|
|||
}
|
||||
|
||||
// Sample particle creation time
|
||||
site.time = time_->sample(seed);
|
||||
auto [time, time_wgt] = time_->sample(seed);
|
||||
site.time = time;
|
||||
|
||||
site.wgt *= (E_wgt * time_wgt);
|
||||
}
|
||||
|
||||
// Increment number of accepted samples
|
||||
|
|
@ -537,9 +549,9 @@ CompiledSourceWrapper::~CompiledSourceWrapper()
|
|||
// MeshElementSpatial implementation
|
||||
//==============================================================================
|
||||
|
||||
Position MeshElementSpatial::sample(uint64_t* seed) const
|
||||
std::pair<Position, double> MeshElementSpatial::sample(uint64_t* seed) const
|
||||
{
|
||||
return model::meshes[mesh_index_]->sample_element(elem_index_, seed);
|
||||
return {model::meshes[mesh_index_]->sample_element(elem_index_, seed), 1.0};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ TEST_CASE("Test alias method sampling of a discrete distribution")
|
|||
int counter = 0;
|
||||
|
||||
for (size_t i = 0; i < n_samples; i++) {
|
||||
auto sample = dist.sample(&seed);
|
||||
auto sample = dist.sample(&seed).first;
|
||||
std += sample * sample / n_samples;
|
||||
dist_mean += sample;
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ TEST_CASE("Test alias sampling method for pugixml constructor")
|
|||
// Initialize discrete distribution and seed
|
||||
openmc::Discrete dist(energy);
|
||||
uint64_t seed = openmc::init_seed(0, 0);
|
||||
auto sample = dist.sample(&seed);
|
||||
auto sample = dist.sample(&seed).first;
|
||||
|
||||
// Assertions
|
||||
REQUIRE(dist.x().size() == 3);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
k-combined:
|
||||
3.080655E-01 4.837707E-03
|
||||
2.942254E-01 2.571435E-03
|
||||
|
|
|
|||
|
|
@ -14,3 +14,12 @@ def assert_unbounded(obj):
|
|||
ll, ur = obj.bounding_box
|
||||
assert ll == pytest.approx((-np.inf, -np.inf, -np.inf))
|
||||
assert ur == pytest.approx((np.inf, np.inf, np.inf))
|
||||
|
||||
|
||||
def assert_sample_mean(samples, expected_mean):
|
||||
# Calculate sample standard deviation
|
||||
std_dev = samples.std() / np.sqrt(samples.size - 1)
|
||||
|
||||
# Means should agree within 4 sigma 99.993% of the time. Note that this is
|
||||
# expected to fail about 1 out of 16,000 times
|
||||
assert np.abs(expected_mean - samples.mean()) < 4*std_dev
|
||||
|
|
|
|||
276
tests/unit_tests/test_source_biasing.py
Normal file
276
tests/unit_tests/test_source_biasing.py
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
"""Tests for source biasing using C++ sampling routines via openmc.lib
|
||||
|
||||
This test module validates that the C++ distribution sampling implementations
|
||||
correctly handle both unbiased and biased sampling when used in source
|
||||
definitions. Each test:
|
||||
|
||||
1. Creates a minimal model with a source using a specific energy distribution
|
||||
2. Uses model.sample_external_source() to generate samples via openmc.lib
|
||||
3. Extracts energies from the returned particle list
|
||||
4. Validates that:
|
||||
- Unbiased sampling produces the expected mean
|
||||
- Biased sampling with importance weighting produces the expected mean
|
||||
- Weights are correctly applied (non-unity for biased case)
|
||||
|
||||
These tests complement the Python-level tests in test_stats.py by exercising
|
||||
the full C++ sampling codepath that is used during actual simulations.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import openmc
|
||||
|
||||
from tests.unit_tests import assert_sample_mean
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model():
|
||||
"""Create a minimal model for source sampling tests."""
|
||||
sphere = openmc.Sphere(r=100.0, boundary_type='vacuum')
|
||||
cell = openmc.Cell(region=-sphere)
|
||||
geometry = openmc.Geometry([cell])
|
||||
settings = openmc.Settings(particles=100, batches=1)
|
||||
space = openmc.stats.Point()
|
||||
angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0))
|
||||
settings.source = openmc.IndependentSource(space=space, angle=angle)
|
||||
return openmc.Model(geometry=geometry, settings=settings)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_discrete(run_in_tmpdir, model):
|
||||
"""Test Discrete distribution sampling via C++ routines."""
|
||||
vals = np.array([1.0, 2.0, 3.0])
|
||||
probs = np.array([0.1, 0.7, 0.2])
|
||||
exp_mean = (vals * probs).sum()
|
||||
|
||||
# Create source with discrete energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Discrete(vals, probs)
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = np.array([0.2, 0.1, 0.7])
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_uniform(run_in_tmpdir, model):
|
||||
"""Test Uniform distribution sampling via C++ routines."""
|
||||
a, b = 5.0, 10.0
|
||||
exp_mean = 0.5 * (a + b)
|
||||
|
||||
# Create source with uniform energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Uniform(a, b)
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = openmc.stats.PowerLaw(a, b, 2)
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_powerlaw(run_in_tmpdir, model):
|
||||
"""Test PowerLaw distribution sampling via C++ routines."""
|
||||
a, b, n = 1.0, 20.0, 2.0
|
||||
|
||||
# Determine mean of distribution
|
||||
exp_mean = (n+1)*(b**(n+2) - a**(n+2))/((n+2)*(b**(n+1) - a**(n+1)))
|
||||
|
||||
# Create source with powerlaw energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.PowerLaw(a, b, n)
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = openmc.stats.Uniform(a, b)
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_maxwell(run_in_tmpdir, model):
|
||||
"""Test Maxwell distribution sampling via C++ routines."""
|
||||
theta = 1.2895e6
|
||||
exp_mean = 3/2 * theta
|
||||
|
||||
# Create source with Maxwell energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Maxwell(theta)
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = openmc.stats.Maxwell(theta * 1.1)
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_watt(run_in_tmpdir, model):
|
||||
"""Test Watt distribution sampling via C++ routines."""
|
||||
a, b = 0.965e6, 2.29e-6
|
||||
exp_mean = 3/2 * a + a**2 * b / 4
|
||||
|
||||
# Create source with Watt energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Watt(a, b)
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = openmc.stats.Watt(a*1.05, b)
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_tabular(run_in_tmpdir, model):
|
||||
"""Test Tabular distribution sampling via C++ routines."""
|
||||
# Test linear-linear sampling
|
||||
x = np.array([0.0, 5.0, 7.0, 10.0])
|
||||
p = np.array([10.0, 20.0, 5.0, 6.0])
|
||||
|
||||
# Create tabular distribution and normalize to get expected mean
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Tabular(x, p, 'linear-linear')
|
||||
energy_dist.normalize()
|
||||
exp_mean = energy_dist.mean()
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = openmc.stats.Uniform(x[0], x[-1])
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_mixture(run_in_tmpdir, model):
|
||||
"""Test Mixture distribution sampling via C++ routines."""
|
||||
d1 = openmc.stats.Uniform(0, 5)
|
||||
d2 = openmc.stats.Uniform(3, 7)
|
||||
p = [0.5, 0.5]
|
||||
|
||||
# Create mixture energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Mixture(p, [d1, d2])
|
||||
exp_mean = (2.5 + 5.0) / 2
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, exp_mean)
|
||||
|
||||
# Sample using biased sub-distribution
|
||||
energy_dist.distribution[0].bias = openmc.stats.PowerLaw(0, 5, 2)
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, exp_mean)
|
||||
assert np.any(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
def test_normal(run_in_tmpdir, model):
|
||||
"""Test Normal distribution sampling via C++ routines."""
|
||||
mean_val = 25.0
|
||||
std_dev = 2.0
|
||||
|
||||
# Create source with normal energy distribution
|
||||
model.settings.source[0].energy = energy_dist = openmc.stats.Normal(mean_val, std_dev)
|
||||
|
||||
# Sample using C++ routines and extract energies
|
||||
n_samples = 10_000
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
|
||||
# Check unbiased mean
|
||||
assert_sample_mean(energies, mean_val)
|
||||
|
||||
# Sample from biased distribution
|
||||
energy_dist.bias = openmc.stats.Normal(mean_val * 1.1, std_dev)
|
||||
particles = model.sample_external_source(n_samples)
|
||||
energies = np.array([p.E for p in particles])
|
||||
weights = np.array([p.wgt for p in particles])
|
||||
|
||||
# Check biased weighted mean
|
||||
weighted_energies = energies * weights
|
||||
assert_sample_mean(weighted_energies, mean_val)
|
||||
assert np.any(weights != 1.0)
|
||||
|
|
@ -6,14 +6,7 @@ import openmc
|
|||
import openmc.stats
|
||||
from scipy.integrate import trapezoid
|
||||
|
||||
|
||||
def assert_sample_mean(samples, expected_mean):
|
||||
# Calculate sample standard deviation
|
||||
std_dev = samples.std() / np.sqrt(samples.size - 1)
|
||||
|
||||
# Means should agree within 4 sigma 99.993% of the time. Note that this is
|
||||
# expected to fail about 1 out of 16,000 times
|
||||
assert np.abs(expected_mean - samples.mean()) < 4*std_dev
|
||||
from tests.unit_tests import assert_sample_mean
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -47,8 +40,19 @@ def test_discrete():
|
|||
# sample discrete distribution and check that the mean of the samples is
|
||||
# within 4 std. dev. of the expected mean
|
||||
n_samples = 1_000_000
|
||||
samples = d3.sample(n_samples)
|
||||
samples, weights = d3.sample(n_samples)
|
||||
assert_sample_mean(samples, exp_mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# Test biased distribution
|
||||
d3.bias = np.array([0.2, 0.1, 0.7])
|
||||
bias_elem = d3.to_xml_element('distribution')
|
||||
d4 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
np.testing.assert_array_equal(d4.bias, [0.2, 0.1, 0.7])
|
||||
samples, weights = d4.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, exp_mean)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
def test_delta_function():
|
||||
|
|
@ -83,6 +87,28 @@ def test_merge_discrete():
|
|||
assert triple.integral() == pytest.approx(6.0)
|
||||
|
||||
|
||||
def test_merge_discrete_with_bias():
|
||||
# Two discrete distributions with different biases
|
||||
d1 = openmc.stats.Discrete([1.0, 2.0], [0.5, 0.5])
|
||||
d2 = openmc.stats.Discrete([2.0, 3.0], [0.3, 0.7], bias=[0.1, 0.9])
|
||||
|
||||
merged = openmc.stats.Discrete.merge([d1, d2], [0.6, 0.4])
|
||||
exp_mean = 0.6 * d1.mean() + 0.4 * d2.mean()
|
||||
|
||||
# Verify merged distribution has correct x values
|
||||
assert set(merged.x) == {1.0, 2.0, 3.0}
|
||||
|
||||
# Bias should not be changed in original distributions
|
||||
assert d1.bias is None
|
||||
assert np.all(d2.bias == [0.1, 0.9])
|
||||
|
||||
# Sample and verify bias is applied correctly
|
||||
samples, weights = merged.sample(10_000)
|
||||
|
||||
# Verify weighted mean matches expected unbiased mean
|
||||
assert_sample_mean(samples*weights, exp_mean)
|
||||
|
||||
|
||||
def test_clip_discrete():
|
||||
# Create discrete distribution with two points that are not important, one
|
||||
# because the x value is very small, and one because the p value is very
|
||||
|
|
@ -125,8 +151,19 @@ def test_uniform():
|
|||
# std. dev. of the expected mean
|
||||
exp_mean = 0.5 * (a + b)
|
||||
n_samples = 1_000_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, exp_mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# Test biased distribution
|
||||
d.bias = openmc.stats.PowerLaw(a, b, 2)
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d2 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d2.bias, openmc.stats.PowerLaw)
|
||||
samples, weights = d2.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, exp_mean)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -147,8 +184,19 @@ def test_powerlaw():
|
|||
# sample power law distribution and check that the mean of the samples is
|
||||
# within 4 std. dev. of the expected mean
|
||||
n_samples = 1_000_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, exp_mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# Test biased distribution
|
||||
d.bias = openmc.stats.Uniform(a, b)
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d2 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d2.bias, openmc.stats.Uniform)
|
||||
samples, weights = d2.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, exp_mean)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -166,13 +214,25 @@ def test_maxwell():
|
|||
# sample maxwell distribution and check that the mean of the samples is
|
||||
# within 4 std. dev. of the expected mean
|
||||
n_samples = 1_000_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, exp_mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# A second sample starting from a different seed
|
||||
samples_2 = d.sample(n_samples)
|
||||
samples_2, weights_2 = d.sample(n_samples)
|
||||
assert_sample_mean(samples_2, exp_mean)
|
||||
assert samples_2.mean() != samples.mean()
|
||||
assert np.all(weights_2 == 1)
|
||||
|
||||
# Test biased distribution
|
||||
d.bias = openmc.stats.Maxwell((theta * 1.1))
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d2 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d2.bias, openmc.stats.Maxwell)
|
||||
samples, weights = d2.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, exp_mean)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -195,8 +255,19 @@ def test_watt():
|
|||
# sample Watt distribution and check that the mean of the samples is within
|
||||
# 4 std. dev. of the expected mean
|
||||
n_samples = 1_000_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, exp_mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# Test biased distribution with 5 percent higher T_e
|
||||
d.bias = openmc.stats.Watt(a*1.05, b)
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d2 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d2.bias, openmc.stats.Watt)
|
||||
samples, weights = d2.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, exp_mean)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -206,8 +277,9 @@ def test_tabular():
|
|||
p = np.array([10.0, 20.0, 5.0, 6.0])
|
||||
d = openmc.stats.Tabular(x, p, 'linear-linear')
|
||||
n_samples = 100_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, d.mean())
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# test linear-linear normalization
|
||||
d.normalize()
|
||||
|
|
@ -215,8 +287,9 @@ def test_tabular():
|
|||
|
||||
# test histogram sampling
|
||||
d = openmc.stats.Tabular(x, p, interpolation='histogram')
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, d.mean())
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
d.normalize()
|
||||
assert d.integral() == pytest.approx(1.0)
|
||||
|
|
@ -226,7 +299,7 @@ def test_tabular():
|
|||
d = openmc.stats.Tabular(x, p[:-1], interpolation='histogram')
|
||||
d.cdf()
|
||||
d.mean()
|
||||
assert_sample_mean(d.sample(n_samples), d.mean())
|
||||
assert_sample_mean(d.sample(n_samples)[0], d.mean())
|
||||
|
||||
# passing a shorter probability set should raise an error for linear-linear
|
||||
with pytest.raises(ValueError):
|
||||
|
|
@ -238,6 +311,16 @@ def test_tabular():
|
|||
d = openmc.stats.Tabular(x, p, interpolation='linear-linear')
|
||||
d.cdf()
|
||||
|
||||
# Test biased distribution
|
||||
d.bias = openmc.stats.Uniform(x[0], x[-1])
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d2 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d2.bias, openmc.stats.Uniform)
|
||||
samples, weights = d2.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, d2.mean())
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
def test_tabular_from_xml():
|
||||
x = np.array([0.0, 5.0, 7.0, 10.0])
|
||||
|
|
@ -288,8 +371,9 @@ def test_mixture():
|
|||
|
||||
# Sample and make sure sample mean is close to expected mean
|
||||
n_samples = 1_000_000
|
||||
samples = mix.sample(n_samples)
|
||||
samples, weights = mix.sample(n_samples)
|
||||
assert_sample_mean(samples, (2.5 + 5.0)/2)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
elem = mix.to_xml_element('distribution')
|
||||
|
||||
|
|
@ -298,6 +382,26 @@ def test_mixture():
|
|||
assert d.distribution == [d1, d2]
|
||||
assert len(d) == 4
|
||||
|
||||
# Test biased sub-distribution
|
||||
d.distribution[0].bias = openmc.stats.PowerLaw(0, 5, 2)
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d3 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d3.distribution[0].bias, openmc.stats.PowerLaw)
|
||||
samples, weights = d3.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, (2.5 + 5.0)/2)
|
||||
|
||||
# Test biased meta-probability
|
||||
d.distribution[0].bias = None
|
||||
d.bias = [0.25, 0.75]
|
||||
bias_elem_2 = d.to_xml_element('distribution')
|
||||
d4 = openmc.stats.Univariate.from_xml_element(bias_elem_2)
|
||||
assert isinstance (d4.bias, np.ndarray)
|
||||
samples, weights = d4.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, (2.5 + 5.0)/2)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
def test_mixture_clip():
|
||||
# Create mixture distribution containing a discrete distribution with two
|
||||
|
|
@ -330,6 +434,13 @@ def test_mixture_clip():
|
|||
with pytest.warns(UserWarning):
|
||||
mix_clip = mix.clip(1e-6)
|
||||
|
||||
# Make sure warning is raised if a biased Discrete is clipped
|
||||
d3 = openmc.stats.Discrete([1.0, 1.001], [1.0, 0.7e-8])
|
||||
d3.bias = [0.9, 0.1]
|
||||
mix = openmc.stats.Mixture([1.0, 1.0], [d3, d2])
|
||||
with pytest.raises(RuntimeError):
|
||||
mix_clip = mix.clip(1e-6)
|
||||
|
||||
|
||||
def test_polar_azimuthal():
|
||||
# default polar-azimuthal should be uniform in mu and phi
|
||||
|
|
@ -365,12 +476,17 @@ def test_polar_azimuthal():
|
|||
|
||||
def test_isotropic():
|
||||
d = openmc.stats.Isotropic()
|
||||
mu = openmc.stats.Uniform(-1.0, 1.0)
|
||||
phi = openmc.stats.PowerLaw(0., 2*np.pi, 2)
|
||||
d2 = openmc.stats.PolarAzimuthal(mu, phi)
|
||||
d.bias = d2
|
||||
elem = d.to_xml_element()
|
||||
assert elem.tag == 'angle'
|
||||
assert elem.attrib['type'] == 'isotropic'
|
||||
|
||||
d = openmc.stats.Isotropic.from_xml_element(elem)
|
||||
assert isinstance(d, openmc.stats.Isotropic)
|
||||
assert isinstance(d.bias, openmc.stats.PolarAzimuthal)
|
||||
|
||||
|
||||
def test_monodirectional():
|
||||
|
|
@ -387,6 +503,7 @@ def test_cartesian():
|
|||
x = openmc.stats.Uniform(-10., 10.)
|
||||
y = openmc.stats.Uniform(-10., 10.)
|
||||
z = openmc.stats.Uniform(0., 20.)
|
||||
z.bias = openmc.stats.PowerLaw(0., 20., 3)
|
||||
d = openmc.stats.CartesianIndependent(x, y, z)
|
||||
|
||||
elem = d.to_xml_element()
|
||||
|
|
@ -402,6 +519,7 @@ def test_cartesian():
|
|||
|
||||
d = openmc.stats.Spatial.from_xml_element(elem)
|
||||
assert isinstance(d, openmc.stats.CartesianIndependent)
|
||||
assert isinstance (d.z.bias, openmc.stats.PowerLaw)
|
||||
|
||||
|
||||
def test_box():
|
||||
|
|
@ -448,8 +566,19 @@ def test_normal():
|
|||
|
||||
# sample normal distribution
|
||||
n_samples = 100_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# Test biased distribution
|
||||
d.bias = openmc.stats.Normal(10.0, 4.0)
|
||||
bias_elem = d.to_xml_element('distribution')
|
||||
d2 = openmc.stats.Univariate.from_xml_element(bias_elem)
|
||||
assert isinstance (d2.bias, openmc.stats.Normal)
|
||||
samples, weights = d2.sample(n_samples)
|
||||
weighted_sample = samples * weights
|
||||
assert_sample_mean(weighted_sample, mean)
|
||||
assert np.all(weights != 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -468,8 +597,9 @@ def test_muir():
|
|||
|
||||
# sample muir distribution
|
||||
n_samples = 100_000
|
||||
samples = d.sample(n_samples)
|
||||
samples, weights = d.sample(n_samples)
|
||||
assert_sample_mean(samples, mean)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
|
|
@ -514,8 +644,27 @@ def test_combine_distributions():
|
|||
|
||||
# Sample the combined distribution and make sure the sample mean is within
|
||||
# uncertainty of the expected value
|
||||
samples = combined.sample(10_000)
|
||||
samples, weights = combined.sample(10_000)
|
||||
assert_sample_mean(samples, 0.25)
|
||||
assert np.all(weights == 1.0)
|
||||
|
||||
# If biased/unbiased Discrete distributions are combined, unbiased probability
|
||||
# should be conserved and points from both original distributions should be
|
||||
# assigned bias probabilities.
|
||||
x1 = [0.0, 1.0, 10.0]
|
||||
p1 = [0.3, 0.2, 0.5]
|
||||
b1 = [0.2, 0.5, 0.3]
|
||||
d1 = openmc.stats.Discrete(x1, p1, b1)
|
||||
x2 = [0.5, 1.0, 5.0]
|
||||
p2 = [0.4, 0.5, 0.1]
|
||||
d2 = openmc.stats.Discrete(x2, p2)
|
||||
combined = openmc.stats.combine_distributions([d1, d2, t1], [0.25, 0.25, 0.5])
|
||||
|
||||
p3 = [0.075, 0.1, 0.175, 0.025, 0.125]
|
||||
b3 = [0.05, 0.1, 0.25, 0.025, 0.075]
|
||||
assert all(combined.distribution[-1].p == p3)
|
||||
assert all(combined.distribution[-1].bias == b3)
|
||||
|
||||
|
||||
def test_reference_vwu_projection():
|
||||
"""When a non-orthogonal vector is provided, the setter should project out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue