Skip to content

More interpolation types in Tabular. #3413

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,8 @@ list(APPEND libopenmc_SOURCES
# Add bundled external dependencies
list(APPEND libopenmc_SOURCES
src/external/quartic_solver.cpp
src/external/Faddeeva.cc)
src/external/Faddeeva.cc
src/external/LambertW.cpp)

# For Visual Studio compilers
if(MSVC)
Expand Down
40 changes: 40 additions & 0 deletions include/openmc/external/LambertW.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
MIT License

Copyright (c) 2025 GuySten

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Available at https://github.com/GuySten/LambertW
*/

#ifndef LAMBERTW_H
#define LAMBERTW_H

namespace LambertW {

// Compute principal branch of the lambert-w function
extern double lambert_w0(double x);

// Compute secondary negative branch of the lambert-w function
extern double lambert_wm1(double x);

} // namespace LambertW

#endif
24 changes: 24 additions & 0 deletions include/openmc/math_functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,29 @@ std::complex<double> faddeeva(std::complex<double> z);
//! \return Derivative of Faddeeva function evaluated at z
std::complex<double> w_derivative(std::complex<double> z, int order);

//! Evaluate relative exponential function
//!
//! \param x Real argument
//! \return (exp(x)-1)/x without loss of precision near 0
double exprel(double x);

//! Evaluate relative logarithm function
//!
//! \param x Real argument
//! \return log(1+x)/x without loss of precision near 0
double log1prel(double x);

//! Evaluate principal branch of lambert_w function
//!
//! \param x Real argument
//! \return principal branch of lambert_w function
double lambert_w0(double x);

//! Evaluate secondary branch of lambert_w function
//!
//! \param x Real argument
//! \return secondary branch of lambert_w function
double lambert_wm1(double x);

} // namespace openmc
#endif // OPENMC_MATH_FUNCTIONS_H
173 changes: 139 additions & 34 deletions openmc/stats/univariate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import lxml.etree as ET
import numpy as np
from scipy.integrate import trapezoid
from scipy.special import exprel, hyp1f1, lambertw

import openmc.checkvalue as cv
from .._xml import get_text
Expand All @@ -24,6 +25,16 @@
}


def exprel2(x):
"""Evaluate 2*(exp(x)-1-x)/x^2 without loss of precision near 0"""
return hyp1f1(1, 3, x)


def log1prel(x):
"""Evaluate log(1+x)/x without loss of precision near 0"""
return np.where(np.abs(x) < 1e-16, 1.0, np.log1p(x) / x)


class Univariate(EqualityMixin, ABC):
"""Probability distribution of a single random variable.

Expand Down Expand Up @@ -971,44 +982,76 @@ def cdf(self):
c[1:] = p[:x.size-1] * np.diff(x)
elif self.interpolation == 'linear-linear':
c[1:] = 0.5 * (p[:-1] + p[1:]) * np.diff(x)
elif self.interpolation == "linear-log":
m = np.diff(p) / np.diff(np.log(x))
c[1:] = p[:-1] * np.diff(x) + m * (
x[1:] * (np.diff(np.log(x)) - 1.0) + x[:-1]
)
elif self.interpolation == "log-linear":
m = np.diff(np.log(p)) / np.diff(x)
c[1:] = p[:-1] * np.diff(x) * exprel(m * np.diff(x))
elif self.interpolation == "log-log":
m = np.diff(np.log(x * p)) / np.diff(np.log(x))
c[1:] = (x * p)[:-1] * np.diff(np.log(x)) * exprel(m * np.diff(np.log(x)))
else:
raise NotImplementedError('Can only generate CDFs for tabular '
'distributions using histogram or '
'linear-linear interpolation')

raise NotImplementedError(
f"Cannot generate CDFs for tabular "
f"distributions using {self.interpolation} 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[:self.x.size-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()

# use normalized probabilities when computing mean
p = self.p / self.cdf().max()
x = self.x
x_min = x[:-1]
x_max = x[1:]
p_min = p[: x.size - 1]
p_max = p[1:]

if self.interpolation == "linear-linear":
m = np.diff(p) / np.diff(x)
mean = (
(1.0 / 3.0) * m * np.diff(x**3)
+ 0.5 * (p_min - m * x_min) * np.diff(x**2)
).sum()
elif self.interpolation == "linear-log":
m = np.diff(p) / np.diff(np.log(x))
mean = (
(1.0 / 4.0)
* m
* x_min**2
* ((x_max / x_min) ** 2 * (2 * np.diff(np.log(x)) - 1) + 1)
+ +0.5 * p_min * np.diff(x**2)
).sum()
elif self.interpolation == "log-linear":
m = np.diff(np.log(p)) / np.diff(x)
mean = (
p_min
* (
np.diff(x) ** 2
* ((0.5 * exprel2(m * np.diff(x)) * (m * np.diff(x) - 1) + 1))
+ np.diff(x) * x_min * exprel(m * np.diff(x))
)
).sum()
elif self.interpolation == "log-log":
m = np.diff(np.log(p)) / np.diff(np.log(x))
mean = (
p_min
* x_min**2
* np.diff(np.log(x))
* exprel((m + 2) * np.diff(np.log(x)))
).sum()
elif self.interpolation == "histogram":
mean = (0.5 * (x_min + x_max) * np.diff(x) * p_min).sum()
else:
raise NotImplementedError(
f"Cannot compute mean for tabular "
f"distributions using {self.interpolation} interpolation"
)
return mean

def normalize(self):
Expand Down Expand Up @@ -1067,11 +1110,56 @@ def sample(self, n_samples: int = 1, seed: int | None = None):
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
elif self.interpolation == "linear-log":
# 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) / np.log(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]

positive = m > 0
negative = m < 0
a = p_i / m - 1
m[positive] = (
x_i
* ((xi - c_i) / (m * x_i) + a)
/ np.real(lambertw((((xi - c_i) / (m * x_i) + a)) * np.exp(a)))
)[positive]
m[negative] = (
x_i
* ((xi - c_i) / (m * x_i) + a)
/ np.real(lambertw((((xi - c_i) / (m * x_i) + a)) * np.exp(a), -1.0))
)[negative]
samples_out = m
elif self.interpolation == "log-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 = np.log(p_i1 / p_i) / (x_i1 - x_i)
f = (xi - c_i) / p_i

samples_out = x_i + f * log1prel(m * f)
elif self.interpolation == "log-log":
# 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 = np.log((x_i1 * p_i1) / (x_i * p_i)) / np.log(x_i1 / x_i)
f = (xi - c_i) / (x_i * p_i)

samples_out = x_i * np.exp(f * log1prel(m * f))
else:
raise NotImplementedError('Can only sample tabular distributions '
'using histogram or '
'linear-linear interpolation')
raise NotImplementedError(
f"Cannot sample tabular distributions "
f"for {self.inteprolation} interpolation "
)

assert all(samples_out < self.x[-1])
return samples_out
Expand Down Expand Up @@ -1135,6 +1223,23 @@ def integral(self):
return np.sum(np.diff(self.x) * self.p[:self.x.size-1])
elif self.interpolation == 'linear-linear':
return trapezoid(self.p, self.x)
elif self.interpolation == "linear-log":
m = np.diff(self.p) / np.diff(np.log(self.x))
return np.sum(
self.p[:-1] * np.diff(self.x)
+ m * (self.x[1:] * (np.diff(np.log(self.x)) - 1.0) + self.x[:-1])
)
elif self.interpolation == "log-linear":
m = np.diff(np.log(self.p)) / np.diff(self.x)
return np.sum(self.p[:-1] * np.diff(self.x) * exprel(m * np.diff(self.x)))
elif self.interpolation == "log-log":
m = np.diff(np.log(self.p)) / np.diff(np.log(self.x))
return np.sum(
self.p[:-1]
* self.x[:-1]
* np.diff(np.log(self.x))
* exprel((m + 1) * np.diff(np.log(self.x)))
)
else:
raise NotImplementedError(
f'integral() not supported for {self.inteprolation} interpolation')
Expand Down
65 changes: 57 additions & 8 deletions src/distribution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ Tabular::Tabular(pugi::xml_node node)
interp_ = Interpolation::histogram;
} else if (temp == "linear-linear") {
interp_ = Interpolation::lin_lin;
} else if (temp == "log-linear") {
interp_ = Interpolation::log_lin;
} else if (temp == "linear-log") {
interp_ = Interpolation::lin_log;
} else if (temp == "log-log") {
interp_ = Interpolation::log_log;
} else {
openmc::fatal_error(
"Unsupported interpolation type for distribution: " + temp);
Expand Down Expand Up @@ -282,13 +288,6 @@ void Tabular::init(
std::copy(x, x + n, std::back_inserter(x_));
std::copy(p, p + n, std::back_inserter(p_));

// Check interpolation parameter
if (interp_ != Interpolation::histogram &&
interp_ != Interpolation::lin_lin) {
openmc::fatal_error("Only histogram and linear-linear interpolation "
"for tabular distribution is supported.");
}

// Calculate cumulative distribution function
if (c) {
std::copy(c, c + n, std::back_inserter(c_));
Expand All @@ -300,6 +299,22 @@ void Tabular::init(
c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]);
} else if (interp_ == Interpolation::lin_lin) {
c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]);
} else if (interp_ == Interpolation::lin_log) {
double m = (p_[i] - p_[i - 1]) / std::log(x_[i] / x_[i - 1]);
c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]) +
m * (x_[i] * (std::log(x_[i] / x_[i - 1]) - 1.0) + x_[i - 1]);
} else if (interp_ == Interpolation::log_lin) {
double m = std::log(p_[i] / p_[i - 1]) / (x_[i] - x_[i - 1]);
c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]) *
exprel(m * (x_[i] - x_[i - 1]));
} else if (interp_ == Interpolation::log_log) {
double m = std::log((x_[i] * p_[i]) / (x_[i - 1] * p_[i - 1])) /
std::log(x_[i] / x_[i - 1]);
c_[i] = c_[i - 1] + x_[i - 1] * p_[i - 1] *
std::log(x_[i] / x_[i - 1]) *
exprel(m * std::log(x_[i] / x_[i - 1]));
} else {
UNREACHABLE();
}
}
}
Expand Down Expand Up @@ -340,7 +355,7 @@ double Tabular::sample(uint64_t* seed) const
} else {
return x_i;
}
} else {
} else if (interp_ == Interpolation::lin_lin) {
// Linear-linear interpolation
double x_i1 = x_[i + 1];
double p_i1 = p_[i + 1];
Expand All @@ -353,6 +368,40 @@ double Tabular::sample(uint64_t* seed) const
(std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) /
m;
}
} else if (interp_ == Interpolation::lin_log) {
// Linear-Log interpolation
double x_i1 = x_[i + 1];
double p_i1 = p_[i + 1];

double m = (p_i1 - p_i) / std::log(x_i1 / x_i);
double a = p_i / m - 1;
if (m > 0.0) {
return x_i * ((c - c_i) / (m * x_i) + a) /
lambert_w0(((c - c_i) / (m * x_i) + a) * std::exp(a));
} else if (m < 0.0) {
return x_i * ((c - c_i) / (m * x_i) + a) /
lambert_wm1(((c - c_i) / (m * x_i) + a) * std::exp(a));
} else {
return x_i + (c - c_i) / p_i;
}
} else if (interp_ == Interpolation::log_lin) {
// Log-linear interpolation
double x_i1 = x_[i + 1];
double p_i1 = p_[i + 1];

double m = std::log(p_i1 / p_i) / (x_i1 - x_i);
double f = (c - c_i) / p_i;
return x_i + f * log1prel(m * f);
} else if (interp_ == Interpolation::log_log) {
// Log-Log interpolation
double x_i1 = x_[i + 1];
double p_i1 = p_[i + 1];

double m = std::log((x_i1 * p_i1) / (x_i * p_i)) / std::log(x_i1 / x_i);
double f = (c - c_i) / (p_i * x_i);
return x_i * std::exp(f * log1prel(m * f));
} else {
UNREACHABLE();
}
}

Expand Down
Loading