mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-26 13:15:39 -04:00
1367 lines
39 KiB
Python
1367 lines
39 KiB
Python
from __future__ import annotations
|
|
import math
|
|
import typing
|
|
from abc import ABC, abstractmethod
|
|
from collections import defaultdict
|
|
from collections.abc import Iterable
|
|
from copy import deepcopy
|
|
from numbers import Real
|
|
from warnings import warn
|
|
|
|
import lxml.etree as ET
|
|
import numpy as np
|
|
from scipy.integrate import trapezoid
|
|
|
|
import openmc.checkvalue as cv
|
|
from .._xml import get_text
|
|
from ..mixin import EqualityMixin
|
|
|
|
_INTERPOLATION_SCHEMES = [
|
|
'histogram',
|
|
'linear-linear',
|
|
'linear-log',
|
|
'log-linear',
|
|
'log-log'
|
|
]
|
|
|
|
|
|
class Univariate(EqualityMixin, ABC):
|
|
"""Probability distribution of a single random variable.
|
|
|
|
The Univariate class is an abstract class that can be derived to implement a
|
|
specific probability distribution.
|
|
|
|
"""
|
|
@abstractmethod
|
|
def to_xml_element(self, element_name):
|
|
return ''
|
|
|
|
@abstractmethod
|
|
def __len__(self):
|
|
return 0
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def from_xml_element(cls, elem):
|
|
distribution = get_text(elem, 'type')
|
|
if distribution == 'discrete':
|
|
return Discrete.from_xml_element(elem)
|
|
elif distribution == 'uniform':
|
|
return Uniform.from_xml_element(elem)
|
|
elif distribution == 'powerlaw':
|
|
return PowerLaw.from_xml_element(elem)
|
|
elif distribution == 'maxwell':
|
|
return Maxwell.from_xml_element(elem)
|
|
elif distribution == 'watt':
|
|
return Watt.from_xml_element(elem)
|
|
elif distribution == 'normal':
|
|
return Normal.from_xml_element(elem)
|
|
elif distribution == 'muir':
|
|
# Support older files where Muir had its own class
|
|
params = [float(x) for x in get_text(elem, 'parameters').split()]
|
|
return muir(*params)
|
|
elif distribution == 'tabular':
|
|
return Tabular.from_xml_element(elem)
|
|
elif distribution == 'legendre':
|
|
return Legendre.from_xml_element(elem)
|
|
elif distribution == 'mixture':
|
|
return Mixture.from_xml_element(elem)
|
|
|
|
@abstractmethod
|
|
def sample(n_samples: int = 1, seed: typing.Optional[int] = None):
|
|
"""Sample the univariate distribution
|
|
|
|
Parameters
|
|
----------
|
|
n_samples : int
|
|
Number of sampled values to generate
|
|
seed : int or None
|
|
Initial random number seed.
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray
|
|
A 1-D array of sampled values
|
|
"""
|
|
pass
|
|
|
|
def integral(self):
|
|
"""Return integral of distribution
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
Integral of distribution
|
|
"""
|
|
return 1.0
|
|
|
|
|
|
class Discrete(Univariate):
|
|
"""Distribution characterized by a probability mass function.
|
|
|
|
The Discrete distribution assigns probability values to discrete values of a
|
|
random variable, rather than expressing the distribution as a continuous
|
|
random variable.
|
|
|
|
Parameters
|
|
----------
|
|
x : Iterable of float
|
|
Values of the random variable
|
|
p : Iterable of float
|
|
Discrete probability for each value
|
|
|
|
Attributes
|
|
----------
|
|
x : numpy.ndarray
|
|
Values of the random variable
|
|
p : numpy.ndarray
|
|
Discrete probability for each value
|
|
|
|
"""
|
|
|
|
def __init__(self, x, p):
|
|
self.x = x
|
|
self.p = p
|
|
|
|
def __len__(self):
|
|
return len(self.x)
|
|
|
|
@property
|
|
def x(self):
|
|
return self._x
|
|
|
|
@x.setter
|
|
def x(self, x):
|
|
if isinstance(x, Real):
|
|
x = [x]
|
|
cv.check_type('discrete values', x, Iterable, Real)
|
|
self._x = np.array(x, dtype=float)
|
|
|
|
@property
|
|
def p(self):
|
|
return self._p
|
|
|
|
@p.setter
|
|
def p(self, p):
|
|
if isinstance(p, Real):
|
|
p = [p]
|
|
cv.check_type('discrete probabilities', p, Iterable, Real)
|
|
for pk in p:
|
|
cv.check_greater_than('discrete probability', pk, 0.0, True)
|
|
self._p = np.array(p, dtype=float)
|
|
|
|
def cdf(self):
|
|
return np.insert(np.cumsum(self.p), 0, 0.0)
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
p = self.p / self.p.sum()
|
|
return rng.choice(self.x, n_samples, p=p)
|
|
|
|
def normalize(self):
|
|
"""Normalize the probabilities stored on the distribution"""
|
|
norm = sum(self.p)
|
|
self.p = [val / norm for val in self.p]
|
|
|
|
def to_xml_element(self, element_name):
|
|
"""Return XML representation of the discrete distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing discrete distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "discrete")
|
|
|
|
params = ET.SubElement(element, "parameters")
|
|
params.text = ' '.join(map(str, self.x)) + ' ' + ' '.join(map(str, self.p))
|
|
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate discrete distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Discrete
|
|
Discrete distribution generated from XML element
|
|
|
|
"""
|
|
params = [float(x) for x in get_text(elem, 'parameters').split()]
|
|
x = params[:len(params)//2]
|
|
p = params[len(params)//2:]
|
|
return cls(x, p)
|
|
|
|
@classmethod
|
|
def merge(
|
|
cls,
|
|
dists: typing.Sequence[Discrete],
|
|
probs: typing.Sequence[int]
|
|
):
|
|
"""Merge multiple discrete distributions into a single distribution
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
Parameters
|
|
----------
|
|
dists : iterable of openmc.stats.Discrete
|
|
Discrete distributions to combine
|
|
probs : iterable of float
|
|
Probability of each distribution
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Discrete
|
|
Combined discrete distribution
|
|
|
|
"""
|
|
if len(dists) != len(probs):
|
|
raise ValueError("Number of distributions and probabilities must match.")
|
|
|
|
# Combine distributions accounting for duplicate x values
|
|
x_merged = set()
|
|
p_merged = defaultdict(float)
|
|
for dist, p_dist in zip(dists, probs):
|
|
for x, p in zip(dist.x, dist.p):
|
|
x_merged.add(x)
|
|
p_merged[x] += p*p_dist
|
|
|
|
# Create values and probabilities as arrays
|
|
x_arr = np.array(sorted(x_merged))
|
|
p_arr = np.array([p_merged[x] for x in x_arr])
|
|
return cls(x_arr, p_arr)
|
|
|
|
def integral(self):
|
|
"""Return integral of distribution
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
Integral of discrete distribution
|
|
"""
|
|
return np.sum(self.p)
|
|
|
|
def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete:
|
|
r"""Remove low-importance points from discrete distribution.
|
|
|
|
Given a probability mass function :math:`p(x)` with :math:`\{x_1, x_2,
|
|
x_3, \dots\}` the possible values of the random variable with
|
|
corresponding probabilities :math:`\{p_1, p_2, p_3, \dots\}`, this
|
|
function will remove any low-importance points such that :math:`\sum_i
|
|
x_i p_i` is preserved to within some threshold.
|
|
|
|
.. versionadded:: 0.14.0
|
|
|
|
Parameters
|
|
----------
|
|
tolerance : float
|
|
Maximum fraction of :math:`\sum_i x_i p_i` that will be discarded.
|
|
inplace : bool
|
|
Whether to modify the current object in-place or return a new one.
|
|
|
|
Returns
|
|
-------
|
|
Discrete distribution with low-importance points removed
|
|
|
|
"""
|
|
cv.check_less_than("tolerance", tolerance, 1.0, equality=True)
|
|
cv.check_greater_than("tolerance", tolerance, 0.0, equality=True)
|
|
|
|
# Determine (reversed) sorted order of probabilities
|
|
intensity = self.p * self.x
|
|
index_sort = np.argsort(intensity)[::-1]
|
|
|
|
# Get probabilities in above order
|
|
sorted_intensity = intensity[index_sort]
|
|
|
|
# Determine cumulative sum of probabilities
|
|
cumsum = np.cumsum(sorted_intensity)
|
|
cumsum /= cumsum[-1]
|
|
|
|
# Find index which satisfies cutoff
|
|
index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance)
|
|
|
|
# Now get indices up to cutoff
|
|
new_indices = index_sort[:index_cutoff + 1]
|
|
new_indices.sort()
|
|
|
|
# Create new discrete distribution
|
|
if inplace:
|
|
self.x = self.x[new_indices]
|
|
self.p = self.p[new_indices]
|
|
return self
|
|
else:
|
|
new_x = self.x[new_indices]
|
|
new_p = self.p[new_indices]
|
|
return type(self)(new_x, new_p)
|
|
|
|
|
|
class Uniform(Univariate):
|
|
"""Distribution with constant probability over a finite interval [a,b]
|
|
|
|
Parameters
|
|
----------
|
|
a : float, optional
|
|
Lower bound of the sampling interval. Defaults to zero.
|
|
b : float, optional
|
|
Upper bound of the sampling interval. Defaults to unity.
|
|
|
|
Attributes
|
|
----------
|
|
a : float
|
|
Lower bound of the sampling interval
|
|
b : float
|
|
Upper bound of the sampling interval
|
|
|
|
"""
|
|
|
|
def __init__(self, a: float = 0.0, b: float = 1.0):
|
|
self.a = a
|
|
self.b = b
|
|
|
|
def __len__(self):
|
|
return 2
|
|
|
|
@property
|
|
def a(self):
|
|
return self._a
|
|
|
|
@a.setter
|
|
def a(self, a):
|
|
cv.check_type('Uniform a', a, Real)
|
|
self._a = a
|
|
|
|
@property
|
|
def b(self):
|
|
return self._b
|
|
|
|
@b.setter
|
|
def b(self, b):
|
|
cv.check_type('Uniform b', b, Real)
|
|
self._b = b
|
|
|
|
def to_tabular(self):
|
|
prob = 1./(self.b - self.a)
|
|
t = Tabular([self.a, self.b], [prob, prob], 'histogram')
|
|
t.c = [0., 1.]
|
|
return t
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
return rng.uniform(self.a, self.b, n_samples)
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the uniform distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing uniform distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "uniform")
|
|
element.set("parameters", '{} {}'.format(self.a, self.b))
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate uniform distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Uniform
|
|
Uniform distribution generated from XML element
|
|
|
|
"""
|
|
params = get_text(elem, 'parameters').split()
|
|
return cls(*map(float, params))
|
|
|
|
|
|
class PowerLaw(Univariate):
|
|
"""Distribution with power law probability over a finite interval [a,b]
|
|
|
|
The power law distribution has density function :math:`p(x) dx = c x^n dx`.
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
Parameters
|
|
----------
|
|
a : float, optional
|
|
Lower bound of the sampling interval. Defaults to zero.
|
|
b : float, optional
|
|
Upper bound of the sampling interval. Defaults to unity.
|
|
n : float, optional
|
|
Power law exponent. Defaults to zero, which is equivalent to a uniform
|
|
distribution.
|
|
|
|
Attributes
|
|
----------
|
|
a : float
|
|
Lower bound of the sampling interval
|
|
b : float
|
|
Upper bound of the sampling interval
|
|
n : float
|
|
Power law exponent
|
|
|
|
"""
|
|
|
|
def __init__(self, a: float = 0.0, b: float = 1.0, n: float = 0.):
|
|
self.a = a
|
|
self.b = b
|
|
self.n = n
|
|
|
|
def __len__(self):
|
|
return 3
|
|
|
|
@property
|
|
def a(self):
|
|
return self._a
|
|
|
|
@a.setter
|
|
def a(self, a):
|
|
cv.check_type('interval lower bound', a, Real)
|
|
self._a = a
|
|
|
|
@property
|
|
def b(self):
|
|
return self._b
|
|
|
|
@b.setter
|
|
def b(self, b):
|
|
cv.check_type('interval upper bound', b, Real)
|
|
self._b = b
|
|
|
|
@property
|
|
def n(self):
|
|
return self._n
|
|
|
|
@n.setter
|
|
def n(self, n):
|
|
cv.check_type('power law exponent', n, Real)
|
|
self._n = n
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
xi = rng.random(n_samples)
|
|
pwr = self.n + 1
|
|
offset = self.a**pwr
|
|
span = self.b**pwr - offset
|
|
return np.power(offset + xi * span, 1/pwr)
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the power law distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "powerlaw")
|
|
element.set("parameters", f'{self.a} {self.b} {self.n}')
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate power law distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.PowerLaw
|
|
Distribution generated from XML element
|
|
|
|
"""
|
|
params = get_text(elem, 'parameters').split()
|
|
return cls(*map(float, params))
|
|
|
|
|
|
class Maxwell(Univariate):
|
|
r"""Maxwellian distribution in energy.
|
|
|
|
The Maxwellian distribution in energy is characterized by a single parameter
|
|
:math:`\theta` and has a density function :math:`p(E) dE = c \sqrt{E}
|
|
e^{-E/\theta} dE`.
|
|
|
|
Parameters
|
|
----------
|
|
theta : float
|
|
Effective temperature for distribution in eV
|
|
|
|
Attributes
|
|
----------
|
|
theta : float
|
|
Effective temperature for distribution in eV
|
|
|
|
"""
|
|
|
|
def __init__(self, theta):
|
|
self.theta = theta
|
|
|
|
def __len__(self):
|
|
return 1
|
|
|
|
@property
|
|
def theta(self):
|
|
return self._theta
|
|
|
|
@theta.setter
|
|
def theta(self, theta):
|
|
cv.check_type('Maxwell temperature', theta, Real)
|
|
cv.check_greater_than('Maxwell temperature', theta, 0.0)
|
|
self._theta = theta
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
return self.sample_maxwell(self.theta, n_samples, rng=rng)
|
|
|
|
@staticmethod
|
|
def sample_maxwell(t, n_samples: int, rng=None):
|
|
if rng is None:
|
|
rng = np.random.default_rng()
|
|
r1, r2, r3 = rng.random((3, n_samples))
|
|
c = np.cos(0.5 * np.pi * r3)
|
|
return -t * (np.log(r1) + np.log(r2) * c * c)
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the Maxwellian distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing Maxwellian distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "maxwell")
|
|
element.set("parameters", str(self.theta))
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate Maxwellian distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Maxwell
|
|
Maxwellian distribution generated from XML element
|
|
|
|
"""
|
|
theta = float(get_text(elem, 'parameters'))
|
|
return cls(theta)
|
|
|
|
|
|
class Watt(Univariate):
|
|
r"""Watt fission energy spectrum.
|
|
|
|
The Watt fission energy spectrum is characterized by two parameters
|
|
:math:`a` and :math:`b` and has density function :math:`p(E) dE = c e^{-E/a}
|
|
\sinh \sqrt{b \, E} dE`.
|
|
|
|
Parameters
|
|
----------
|
|
a : float
|
|
First parameter of distribution in units of eV
|
|
b : float
|
|
Second parameter of distribution in units of 1/eV
|
|
|
|
Attributes
|
|
----------
|
|
a : float
|
|
First parameter of distribution in units of eV
|
|
b : float
|
|
Second parameter of distribution in units of 1/eV
|
|
|
|
"""
|
|
|
|
def __init__(self, a=0.988e6, b=2.249e-6):
|
|
self.a = a
|
|
self.b = b
|
|
|
|
def __len__(self):
|
|
return 2
|
|
|
|
@property
|
|
def a(self):
|
|
return self._a
|
|
|
|
@a.setter
|
|
def a(self, a):
|
|
cv.check_type('Watt a', a, Real)
|
|
cv.check_greater_than('Watt a', a, 0.0)
|
|
self._a = a
|
|
|
|
@property
|
|
def b(self):
|
|
return self._b
|
|
|
|
@b.setter
|
|
def b(self, b):
|
|
cv.check_type('Watt b', b, Real)
|
|
cv.check_greater_than('Watt b', b, 0.0)
|
|
self._b = b
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
w = Maxwell.sample_maxwell(self.a, n_samples, rng=rng)
|
|
u = rng.uniform(-1., 1., n_samples)
|
|
aab = self.a * self.a * self.b
|
|
return w + 0.25*aab + u*np.sqrt(aab*w)
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the Watt distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing Watt distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "watt")
|
|
element.set("parameters", '{} {}'.format(self.a, self.b))
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate Watt distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Watt
|
|
Watt distribution generated from XML element
|
|
|
|
"""
|
|
params = get_text(elem, 'parameters').split()
|
|
return cls(*map(float, params))
|
|
|
|
|
|
class Normal(Univariate):
|
|
r"""Normally distributed sampling.
|
|
|
|
The Normal Distribution is characterized by two parameters
|
|
:math:`\mu` and :math:`\sigma` and has density function
|
|
:math:`p(X) dX = 1/(\sqrt{2\pi}\sigma) e^{-(X-\mu)^2/(2\sigma^2)}`
|
|
|
|
Parameters
|
|
----------
|
|
mean_value : float
|
|
Mean value of the distribution
|
|
std_dev : float
|
|
Standard deviation of the Normal distribution
|
|
|
|
Attributes
|
|
----------
|
|
mean_value : float
|
|
Mean of the Normal distribution
|
|
std_dev : float
|
|
Standard deviation of the Normal distribution
|
|
"""
|
|
|
|
def __init__(self, mean_value, std_dev):
|
|
self.mean_value = mean_value
|
|
self.std_dev = std_dev
|
|
|
|
def __len__(self):
|
|
return 2
|
|
|
|
@property
|
|
def mean_value(self):
|
|
return self._mean_value
|
|
|
|
@mean_value.setter
|
|
def mean_value(self, mean_value):
|
|
cv.check_type('Normal mean_value', mean_value, Real)
|
|
self._mean_value = mean_value
|
|
|
|
@property
|
|
def std_dev(self):
|
|
return self._std_dev
|
|
|
|
@std_dev.setter
|
|
def std_dev(self, std_dev):
|
|
cv.check_type('Normal std_dev', std_dev, Real)
|
|
cv.check_greater_than('Normal std_dev', std_dev, 0.0)
|
|
self._std_dev = std_dev
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
return rng.normal(self.mean_value, self.std_dev, n_samples)
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the Normal distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing Watt distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "normal")
|
|
element.set("parameters", '{} {}'.format(self.mean_value, self.std_dev))
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate Normal distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Normal
|
|
Normal distribution generated from XML element
|
|
|
|
"""
|
|
params = get_text(elem, 'parameters').split()
|
|
return cls(*map(float, params))
|
|
|
|
|
|
def muir(e0: float, m_rat: float, kt: float):
|
|
"""Generate a Muir energy spectrum
|
|
|
|
The Muir energy spectrum is a normal distribution, but for convenience
|
|
reasons allows the user to specify three parameters to define the
|
|
distribution: the mean energy of particles ``e0``, the mass of reactants
|
|
``m_rat``, and the ion temperature ``kt``.
|
|
|
|
.. versionadded:: 0.13.2
|
|
|
|
Parameters
|
|
----------
|
|
e0 : float
|
|
Mean of the Muir distribution in [eV]
|
|
m_rat : float
|
|
Ratio of the sum of the masses of the reaction inputs to 1 amu
|
|
kt : float
|
|
Ion temperature for the Muir distribution in [eV]
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Normal
|
|
Corresponding normal distribution
|
|
|
|
"""
|
|
# https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-05411-MS
|
|
std_dev = math.sqrt(2 * e0 * kt / m_rat)
|
|
return Normal(e0, std_dev)
|
|
|
|
|
|
# Retain deprecated name for the time being
|
|
def Muir(*args, **kwargs):
|
|
# warn of name change
|
|
warn(
|
|
"The Muir(...) class has been replaced by the muir(...) function and "
|
|
"will be removed in a future version of OpenMC. Use muir(...) instead.",
|
|
FutureWarning
|
|
)
|
|
return muir(*args, **kwargs)
|
|
|
|
|
|
class Tabular(Univariate):
|
|
"""Piecewise continuous probability distribution.
|
|
|
|
This class is used to represent a probability distribution whose density
|
|
function is tabulated at specific values with a specified interpolation
|
|
scheme.
|
|
|
|
Parameters
|
|
----------
|
|
x : Iterable of float
|
|
Tabulated values of the random variable
|
|
p : Iterable of float
|
|
Tabulated probabilities
|
|
interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional
|
|
Indicate whether the density function is constant between tabulated
|
|
points or linearly-interpolated. Defaults to 'linear-linear'.
|
|
ignore_negative : bool
|
|
Ignore negative probabilities
|
|
|
|
Attributes
|
|
----------
|
|
x : numpy.ndarray
|
|
Tabulated values of the random variable
|
|
p : numpy.ndarray
|
|
Tabulated probabilities
|
|
interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional
|
|
Indicate whether the density function is constant between tabulated
|
|
points or linearly-interpolated.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
x: typing.Sequence[float],
|
|
p: typing.Sequence[float],
|
|
interpolation: str = 'linear-linear',
|
|
ignore_negative: bool = False
|
|
):
|
|
self._ignore_negative = ignore_negative
|
|
self.x = x
|
|
self.p = p
|
|
self.interpolation = interpolation
|
|
|
|
def __len__(self):
|
|
return len(self.x)
|
|
|
|
@property
|
|
def x(self):
|
|
return self._x
|
|
|
|
@x.setter
|
|
def x(self, x):
|
|
cv.check_type('tabulated values', x, Iterable, Real)
|
|
self._x = np.array(x, dtype=float)
|
|
|
|
@property
|
|
def p(self):
|
|
return self._p
|
|
|
|
@p.setter
|
|
def p(self, p):
|
|
cv.check_type('tabulated probabilities', p, Iterable, Real)
|
|
if not self._ignore_negative:
|
|
for pk in p:
|
|
cv.check_greater_than('tabulated probability', pk, 0.0, True)
|
|
self._p = np.array(p, dtype=float)
|
|
|
|
@property
|
|
def interpolation(self):
|
|
return self._interpolation
|
|
|
|
@interpolation.setter
|
|
def interpolation(self, interpolation):
|
|
cv.check_value('interpolation', interpolation, _INTERPOLATION_SCHEMES)
|
|
self._interpolation = interpolation
|
|
|
|
def cdf(self):
|
|
c = np.zeros_like(self.x)
|
|
x = self.x
|
|
p = self.p
|
|
|
|
if self.interpolation == 'histogram':
|
|
c[1:] = p[:-1] * np.diff(x)
|
|
elif self.interpolation == 'linear-linear':
|
|
c[1:] = 0.5 * (p[:-1] + p[1:]) * np.diff(x)
|
|
else:
|
|
raise NotImplementedError('Can only generate CDFs for tabular '
|
|
'distributions using histogram or '
|
|
'linear-linear interpolation')
|
|
|
|
|
|
return np.cumsum(c)
|
|
|
|
def mean(self):
|
|
"""Compute the mean of the tabular distribution"""
|
|
if self.interpolation == 'linear-linear':
|
|
mean = 0.0
|
|
for i in range(1, len(self.x)):
|
|
y_min = self.p[i-1]
|
|
y_max = self.p[i]
|
|
x_min = self.x[i-1]
|
|
x_max = self.x[i]
|
|
|
|
m = (y_max - y_min) / (x_max - x_min)
|
|
|
|
exp_val = (1./3.) * m * (x_max**3 - x_min**3)
|
|
exp_val += 0.5 * m * x_min * (x_min**2 - x_max**2)
|
|
exp_val += 0.5 * y_min * (x_max**2 - x_min**2)
|
|
mean += exp_val
|
|
|
|
elif self.interpolation == 'histogram':
|
|
x_l = self.x[:-1]
|
|
x_r = self.x[1:]
|
|
p_l = self.p[:-1]
|
|
mean = (0.5 * (x_l + x_r) * (x_r - x_l) * p_l).sum()
|
|
else:
|
|
raise NotImplementedError('Can only compute mean for tabular '
|
|
'distributions using histogram '
|
|
'or linear-linear interpolation.')
|
|
|
|
# Normalize for when integral of distribution is not 1
|
|
mean /= self.integral()
|
|
|
|
return mean
|
|
|
|
def normalize(self):
|
|
"""Normalize the probabilities stored on the distribution"""
|
|
self.p /= self.cdf().max()
|
|
|
|
def sample(self, n_samples: int = 1, seed: typing.Optional[int] = None):
|
|
rng = np.random.RandomState(seed)
|
|
xi = rng.random(n_samples)
|
|
|
|
# always use normalized probabilities when sampling
|
|
cdf = self.cdf()
|
|
p = self.p / cdf.max()
|
|
cdf /= cdf.max()
|
|
|
|
# get CDF bins that are above the
|
|
# sampled values
|
|
c_i = np.full(n_samples, cdf[0])
|
|
cdf_idx = np.zeros(n_samples, dtype=int)
|
|
for i, val in enumerate(cdf[:-1]):
|
|
mask = xi > val
|
|
c_i[mask] = val
|
|
cdf_idx[mask] = i
|
|
|
|
# get table values at each index where
|
|
# the random number is less than the next cdf
|
|
# entry
|
|
x_i = self.x[cdf_idx]
|
|
p_i = p[cdf_idx]
|
|
|
|
if self.interpolation == 'histogram':
|
|
# mask where probability is greater than zero
|
|
pos_mask = p_i > 0.0
|
|
# probabilities greater than zero are set proportional to the
|
|
# position of the random numebers in relation to the cdf value
|
|
p_i[pos_mask] = x_i[pos_mask] + (xi[pos_mask] - c_i[pos_mask]) \
|
|
/ p_i[pos_mask]
|
|
# probabilities smaller than zero are set to the random number value
|
|
p_i[~pos_mask] = x_i[~pos_mask]
|
|
|
|
samples_out = p_i
|
|
|
|
elif self.interpolation == 'linear-linear':
|
|
# get variable and probability values for the
|
|
# next entry
|
|
x_i1 = self.x[cdf_idx + 1]
|
|
p_i1 = p[cdf_idx + 1]
|
|
# compute slope between entries
|
|
m = (p_i1 - p_i) / (x_i1 - x_i)
|
|
# set values for zero slope
|
|
zero = m == 0.0
|
|
m[zero] = x_i[zero] + (xi[zero] - c_i[zero]) / p_i[zero]
|
|
# set values for non-zero slope
|
|
non_zero = ~zero
|
|
quad = np.power(p_i[non_zero], 2) + 2.0 * m[non_zero] * (xi[non_zero] - c_i[non_zero])
|
|
quad[quad < 0.0] = 0.0
|
|
m[non_zero] = x_i[non_zero] + (np.sqrt(quad) - p_i[non_zero]) / m[non_zero]
|
|
samples_out = m
|
|
|
|
else:
|
|
raise NotImplementedError('Can only sample tabular distributions '
|
|
'using histogram or '
|
|
'linear-linear interpolation')
|
|
|
|
assert all(samples_out < self.x[-1])
|
|
return samples_out
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the tabular distribution
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing tabular distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "tabular")
|
|
element.set("interpolation", self.interpolation)
|
|
|
|
params = ET.SubElement(element, "parameters")
|
|
params.text = ' '.join(map(str, self.x)) + ' ' + ' '.join(map(str, self.p))
|
|
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate tabular distribution from an XML element
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Tabular
|
|
Tabular distribution generated from XML element
|
|
|
|
"""
|
|
interpolation = get_text(elem, 'interpolation')
|
|
params = [float(x) for x in get_text(elem, 'parameters').split()]
|
|
x = params[:len(params)//2]
|
|
p = params[len(params)//2:]
|
|
return cls(x, p, interpolation)
|
|
|
|
def integral(self):
|
|
"""Return integral of distribution
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
Integral of tabular distrbution
|
|
"""
|
|
if self.interpolation == 'histogram':
|
|
return np.sum(np.diff(self.x) * self.p[:-1])
|
|
elif self.interpolation == 'linear-linear':
|
|
return trapezoid(self.p, self.x)
|
|
else:
|
|
raise NotImplementedError(
|
|
f'integral() not supported for {self.inteprolation} interpolation')
|
|
|
|
|
|
class Legendre(Univariate):
|
|
r"""Probability density given by a Legendre polynomial expansion
|
|
:math:`\sum\limits_{\ell=0}^N \frac{2\ell + 1}{2} a_\ell P_\ell(\mu)`.
|
|
|
|
Parameters
|
|
----------
|
|
coefficients : Iterable of Real
|
|
Expansion coefficients :math:`a_\ell`. Note that the :math:`(2\ell +
|
|
1)/2` factor should not be included.
|
|
|
|
Attributes
|
|
----------
|
|
coefficients : Iterable of Real
|
|
Expansion coefficients :math:`a_\ell`. Note that the :math:`(2\ell +
|
|
1)/2` factor should not be included.
|
|
|
|
"""
|
|
|
|
def __init__(self, coefficients: typing.Sequence[float]):
|
|
self.coefficients = coefficients
|
|
self._legendre_poly = None
|
|
|
|
def __call__(self, x):
|
|
# Create Legendre polynomial if we haven't yet
|
|
if self._legendre_poly is None:
|
|
l = np.arange(len(self._coefficients))
|
|
coeffs = (2.*l + 1.)/2. * self._coefficients
|
|
self._legendre_poly = np.polynomial.Legendre(coeffs)
|
|
|
|
return self._legendre_poly(x)
|
|
|
|
def __len__(self):
|
|
return len(self._coefficients)
|
|
|
|
@property
|
|
def coefficients(self):
|
|
return self._coefficients
|
|
|
|
@coefficients.setter
|
|
def coefficients(self, coefficients):
|
|
self._coefficients = np.asarray(coefficients)
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
raise NotImplementedError
|
|
|
|
def to_xml_element(self, element_name):
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem):
|
|
raise NotImplementedError
|
|
|
|
|
|
class Mixture(Univariate):
|
|
"""Probability distribution characterized by a mixture of random variables.
|
|
|
|
Parameters
|
|
----------
|
|
probability : Iterable of Real
|
|
Probability of selecting a particular distribution
|
|
distribution : Iterable of Univariate
|
|
List of distributions with corresponding probabilities
|
|
|
|
Attributes
|
|
----------
|
|
probability : Iterable of Real
|
|
Probability of selecting a particular distribution
|
|
distribution : Iterable of Univariate
|
|
List of distributions with corresponding probabilities
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
probability: typing.Sequence[float],
|
|
distribution: typing.Sequence[Univariate]
|
|
):
|
|
self.probability = probability
|
|
self.distribution = distribution
|
|
|
|
def __len__(self):
|
|
return sum(len(d) for d in self.distribution)
|
|
|
|
@property
|
|
def probability(self):
|
|
return self._probability
|
|
|
|
@probability.setter
|
|
def probability(self, probability):
|
|
cv.check_type('mixture distribution probabilities', probability,
|
|
Iterable, Real)
|
|
for p in probability:
|
|
cv.check_greater_than('mixture distribution probabilities',
|
|
p, 0.0, True)
|
|
self._probability = probability
|
|
|
|
@property
|
|
def distribution(self):
|
|
return self._distribution
|
|
|
|
@distribution.setter
|
|
def distribution(self, distribution):
|
|
cv.check_type('mixture distribution components', distribution,
|
|
Iterable, Univariate)
|
|
self._distribution = distribution
|
|
|
|
def cdf(self):
|
|
return np.insert(np.cumsum(self.probability), 0, 0.0)
|
|
|
|
def sample(self, n_samples=1, seed=None):
|
|
rng = np.random.RandomState(seed)
|
|
|
|
# Get probability of each distribution accounting for its intensity
|
|
p = np.array([prob*dist.integral() for prob, dist in
|
|
zip(self.probability, self.distribution)])
|
|
p /= p.sum()
|
|
|
|
# Sample from the distributions
|
|
idx = rng.choice(range(len(self.distribution)), n_samples, p=p)
|
|
|
|
# Draw samples from the distributions sampled above
|
|
out = np.empty_like(idx, dtype=float)
|
|
for i in np.unique(idx):
|
|
n_dist_samples = np.count_nonzero(idx == i)
|
|
samples = self.distribution[i].sample(n_dist_samples)
|
|
out[idx == i] = samples
|
|
return out
|
|
|
|
def normalize(self):
|
|
"""Normalize the probabilities stored on the distribution"""
|
|
norm = sum(self.probability)
|
|
self.probability = [val / norm for val in self.probability]
|
|
|
|
def to_xml_element(self, element_name: str):
|
|
"""Return XML representation of the mixture distribution
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
Parameters
|
|
----------
|
|
element_name : str
|
|
XML element name
|
|
|
|
Returns
|
|
-------
|
|
element : lxml.etree._Element
|
|
XML element containing mixture distribution data
|
|
|
|
"""
|
|
element = ET.Element(element_name)
|
|
element.set("type", "mixture")
|
|
|
|
for p, d in zip(self.probability, self.distribution):
|
|
data = ET.SubElement(element, "pair")
|
|
data.set("probability", str(p))
|
|
data.append(d.to_xml_element("dist"))
|
|
|
|
return element
|
|
|
|
@classmethod
|
|
def from_xml_element(cls, elem: ET.Element):
|
|
"""Generate mixture distribution from an XML element
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
Parameters
|
|
----------
|
|
elem : lxml.etree._Element
|
|
XML element
|
|
|
|
Returns
|
|
-------
|
|
openmc.stats.Mixture
|
|
Mixture distribution generated from XML element
|
|
|
|
"""
|
|
probability = []
|
|
distribution = []
|
|
for pair in elem.findall('pair'):
|
|
probability.append(float(get_text(pair, 'probability')))
|
|
distribution.append(Univariate.from_xml_element(pair.find("dist")))
|
|
|
|
return cls(probability, distribution)
|
|
|
|
def integral(self):
|
|
"""Return integral of the distribution
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
Integral of the distribution
|
|
"""
|
|
return sum([
|
|
p*dist.integral()
|
|
for p, dist in zip(self.probability, self.distribution)
|
|
])
|
|
|
|
def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture:
|
|
r"""Remove low-importance points from contained discrete distributions.
|
|
|
|
Given a probability mass function :math:`p(x)` with :math:`\{x_1, x_2,
|
|
x_3, \dots\}` the possible values of the random variable with
|
|
corresponding probabilities :math:`\{p_1, p_2, p_3, \dots\}`, this
|
|
function will remove any low-importance points such that :math:`\sum_i
|
|
x_i p_i` is preserved to within some threshold.
|
|
|
|
.. versionadded:: 0.14.0
|
|
|
|
Parameters
|
|
----------
|
|
tolerance : float
|
|
Maximum fraction of :math:`\sum_i x_i p_i` that will be discarded
|
|
for any discrete distributions within the mixture distribution.
|
|
inplace : bool
|
|
Whether to modify the current object in-place or return a new one.
|
|
|
|
Returns
|
|
-------
|
|
Discrete distribution with low-importance points removed
|
|
|
|
"""
|
|
if inplace:
|
|
for dist in self.distribution:
|
|
if isinstance(dist, Discrete):
|
|
dist.clip(tolerance, inplace=True)
|
|
return self
|
|
else:
|
|
distribution = [
|
|
dist.clip(tolerance) if isinstance(dist, Discrete) else dist
|
|
for dist in self.distribution
|
|
]
|
|
return type(self)(self.probability, distribution)
|
|
|
|
|
|
def combine_distributions(
|
|
dists: typing.Sequence[Univariate],
|
|
probs: typing.Sequence[float]
|
|
):
|
|
"""Combine distributions with specified probabilities
|
|
|
|
This function can be used to combine multiple instances of
|
|
:class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular`. Multiple
|
|
discrete distributions are merged into a single distribution and the
|
|
remainder of the distributions are put into a :class:`~openmc.stats.Mixture`
|
|
distribution.
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
Parameters
|
|
----------
|
|
dists : iterable of openmc.stats.Univariate
|
|
Distributions to combine
|
|
probs : iterable of float
|
|
Probability (or intensity) of each distribution
|
|
|
|
"""
|
|
# Get copy of distribution list so as not to modify the argument
|
|
dist_list = deepcopy(dists)
|
|
|
|
# Get list of discrete/continuous distribution indices
|
|
discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)]
|
|
cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)]
|
|
|
|
# Apply probabilites to continuous distributions
|
|
for i in cont_index:
|
|
dist = dist_list[i]
|
|
dist.p *= probs[i]
|
|
|
|
if discrete_index:
|
|
# Create combined discrete distribution
|
|
dist_discrete = [dist_list[i] for i in discrete_index]
|
|
discrete_probs = [probs[i] for i in discrete_index]
|
|
combined_dist = Discrete.merge(dist_discrete, discrete_probs)
|
|
|
|
# Replace multiple discrete distributions with merged
|
|
for idx in reversed(discrete_index):
|
|
dist_list.pop(idx)
|
|
dist_list.append(combined_dist)
|
|
|
|
# Combine discrete and continuous if present
|
|
if len(dist_list) > 1:
|
|
probs = [1.0]*len(dist_list)
|
|
dist_list[:] = [Mixture(probs, dist_list.copy())]
|
|
|
|
return dist_list[0]
|