Skip to content

Commit c1cbee8

Browse files
committed
ENH: add geninvgauss and genhyperbolic pdf kernels
1 parent 2364d39 commit c1cbee8

3 files changed

Lines changed: 189 additions & 0 deletions

File tree

include/xsf/cpu/stats.h

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,81 @@ namespace cpu {
159159
return std::min(std::max(cdf, 0.0), 1.0);
160160
}
161161

162+
// Logarithm of the generalized inverse Gaussian probability density function.
163+
inline double geninvgauss_logpdf(double x, double p, double b) {
164+
if (x <= 0) {
165+
return -std::numeric_limits<double>::infinity();
166+
}
167+
168+
double z = cyl_bessel_ke(p, b);
169+
if (std::isinf(z)) {
170+
return std::numeric_limits<double>::quiet_NaN();
171+
}
172+
173+
return -std::log(2.0) - std::log(z) + b + (p - 1.0) * std::log(x) - b * (x + 1.0 / x) / 2.0;
174+
}
175+
176+
inline float geninvgauss_logpdf(float x, float p, float b) {
177+
return static_cast<float>(
178+
geninvgauss_logpdf(static_cast<double>(x), static_cast<double>(p), static_cast<double>(b))
179+
);
180+
}
181+
182+
// Generalized inverse Gaussian probability density function.
183+
inline double geninvgauss_pdf(double x, double p, double b) { return std::exp(geninvgauss_logpdf(x, p, b)); }
184+
185+
inline float geninvgauss_pdf(float x, float p, float b) {
186+
return static_cast<float>(
187+
geninvgauss_pdf(static_cast<double>(x), static_cast<double>(p), static_cast<double>(b))
188+
);
189+
}
190+
191+
namespace detail {
192+
193+
inline double genhyperbolic_log_norming_constant(double p, double a, double b) {
194+
double t1, t2, t3, t4, t5, t6;
195+
196+
t1 = (a + b) * (a - b);
197+
t2 = p * 0.5 * std::log(t1);
198+
t3 = 0.5 * std::log(2 * M_PI);
199+
t4 = (p - 0.5) * std::log(a);
200+
t5 = std::sqrt(t1);
201+
t6 = std::log(cyl_bessel_ke(p, t5)) - t5;
202+
203+
return t2 - t3 - t4 - t6;
204+
}
205+
206+
} // namespace detail
207+
208+
// Logarithm of the generalized hyperbolic probability density function.
209+
inline double genhyperbolic_logpdf(double x, double p, double a, double b) {
210+
double t1, t2, t3, t4, t5;
211+
212+
t1 = detail::genhyperbolic_log_norming_constant(p, a, b);
213+
t2 = std::sqrt(1.0 + x * x);
214+
t3 = (p - 0.5) * std::log(t2);
215+
t4 = std::log(cyl_bessel_ke(p - 0.5, a * t2)) - a * t2;
216+
t5 = b * x;
217+
218+
return t1 + t3 + t4 + t5;
219+
}
220+
221+
inline float genhyperbolic_logpdf(float x, float p, float a, float b) {
222+
return static_cast<float>(genhyperbolic_logpdf(
223+
static_cast<double>(x), static_cast<double>(p), static_cast<double>(a), static_cast<double>(b)
224+
));
225+
}
226+
227+
// Generalized hyperbolic probability density function.
228+
inline double genhyperbolic_pdf(double x, double p, double a, double b) {
229+
return std::exp(genhyperbolic_logpdf(x, p, a, b));
230+
}
231+
232+
inline float genhyperbolic_pdf(float x, float p, float a, float b) {
233+
return static_cast<float>(genhyperbolic_pdf(
234+
static_cast<double>(x), static_cast<double>(p), static_cast<double>(a), static_cast<double>(b)
235+
));
236+
}
237+
162238
} // namespace cpu
163239
} // namespace xsf
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#include "../testing_utils.h"
2+
3+
#include <array>
4+
#include <cmath>
5+
#include <limits>
6+
7+
#include <xsf/cpu/stats.h>
8+
9+
TEST_CASE("generalized hyperbolic density", "[genhyperbolic][xsf_tests]") {
10+
// Mirrors https://github.com/scipy/scipy/blob/v1.18.1/scipy/stats/tests/test_distributions.py#L962-L982
11+
constexpr std::array<double, 10> expected_pdf{
12+
2.94895678275316e-13, 1.75746848647696e-10, 9.48149804073045e-08, 4.17862521692026e-05, 0.0103947630463822,
13+
0.240864958986839, 0.162833527161649, 0.0374609592899472, 0.00634894847327781, 0.000941920705790324,
14+
};
15+
constexpr double p = 2.0;
16+
constexpr double a = 3.0;
17+
constexpr double b = 1.5;
18+
constexpr double loc = 0.5;
19+
constexpr double scale = 1.5;
20+
21+
const std::vector<double> x_values = linspace(-10.0, 10.0, expected_pdf.size());
22+
for (int i = 0; i < expected_pdf.size(); ++i) {
23+
double x = x_values[i];
24+
double standardized_x = (x - loc) / scale;
25+
double pdf = xsf::cpu::genhyperbolic_pdf(standardized_x, p, a, b) / scale;
26+
CAPTURE(i, x, standardized_x, pdf, expected_pdf[i]);
27+
REQUIRE(xsf::extended_relative_error(pdf, expected_pdf[i]) <= 1e-13);
28+
}
29+
}
30+
31+
TEST_CASE("generalized hyperbolic Student's t limit", "[genhyperbolic][xsf_tests]") {
32+
// Mirrors https://github.com/scipy/scipy/blob/v1.18.1/scipy/stats/tests/test_distributions.py#L1081-L1097
33+
constexpr std::array<double, 10> lower{
34+
-31.820519750798752, -3.640296435003648, -2.9487520061496664, -2.7322159231076126, -2.6271579957950233,
35+
-2.565225109345393, -2.524412431436311, -2.495498782411984, -2.4739457843399073, -2.4572615423796655,
36+
};
37+
constexpr std::array<double, 10> upper{
38+
31.820140377530542, 3.640296435001152, 2.9487520061496824, 2.732215923107273, 2.627157995794383,
39+
2.5652251093455574, 2.5244124314366156, 2.4954987824114236, 2.473945784340125, 2.4572615423800115,
40+
};
41+
constexpr double alpha_epsilon = std::numeric_limits<float>::epsilon();
42+
43+
const std::vector<double> degrees_of_freedom = linspace(1.0, 30.0, lower.size());
44+
for (int j = 0; j < degrees_of_freedom.size(); ++j) {
45+
double df = degrees_of_freedom[j];
46+
double p = -df / 2.0;
47+
double a = df * df * alpha_epsilon;
48+
double scale = std::sqrt(df);
49+
50+
for (const double x : linspace(lower[j], upper[j], 50)) {
51+
double pdf = xsf::cpu::genhyperbolic_pdf(x / scale, p, a, 0.0) / scale;
52+
double expected_pdf = std::tgamma((df + 1.0) / 2.0) / (std::sqrt(df * M_PI) * std::tgamma(df / 2.0)) *
53+
std::pow(1.0 + x * x / df, -(df + 1.0) / 2.0);
54+
CAPTURE(j, df, x, pdf, expected_pdf);
55+
REQUIRE(xsf::extended_relative_error(pdf, expected_pdf) <= 1e-6);
56+
}
57+
}
58+
}
59+
60+
TEST_CASE("generalized hyperbolic Cauchy limit", "[genhyperbolic][xsf_tests]") {
61+
// Mirrors https://github.com/scipy/scipy/blob/v1.18.1/scipy/stats/tests/test_distributions.py#L1099-L1114
62+
constexpr double p = -0.5;
63+
constexpr double a = std::numeric_limits<float>::epsilon();
64+
constexpr double lower = -31.820519750798752;
65+
constexpr double upper = 31.820140377530542;
66+
67+
for (const double x : linspace(lower, upper, 50)) {
68+
double pdf = xsf::cpu::genhyperbolic_pdf(x, p, a, 0.0);
69+
double expected_pdf = 1.0 / (M_PI * (1.0 + x * x));
70+
CAPTURE(x, pdf, expected_pdf);
71+
REQUIRE(xsf::extended_relative_error(pdf, expected_pdf) <= 1e-6);
72+
}
73+
}
74+
75+
TEST_CASE("generalized hyperbolic Laplace limit", "[genhyperbolic][xsf_tests]") {
76+
// Mirrors https://github.com/scipy/scipy/blob/v1.18.1/scipy/stats/tests/test_distributions.py#L1116-L1135
77+
constexpr double scale = std::numeric_limits<float>::epsilon();
78+
79+
for (const double loc : linspace(-10.0, 10.0, 10)) {
80+
for (const double x : linspace(-20.0, 20.0, 50)) {
81+
double pdf = xsf::cpu::genhyperbolic_pdf((x - loc) / scale, 1.0, scale, 0.0) / scale;
82+
double expected_pdf = 0.5 * std::exp(-std::abs(x - loc));
83+
CAPTURE(loc, x, pdf, expected_pdf);
84+
REQUIRE(xsf::extended_relative_error(pdf, expected_pdf) <= 1e-11);
85+
}
86+
}
87+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include "../testing_utils.h"
2+
3+
#include <array>
4+
#include <cmath>
5+
6+
#include <xsf/cpu/stats.h>
7+
8+
TEST_CASE("generalized inverse Gaussian density", "[geninvgauss][xsf_tests]") {
9+
// Mirrors https://github.com/scipy/scipy/blob/v1.18.1/scipy/stats/tests/test_distributions.py#L941-L950
10+
constexpr std::array<double, 10> expected_pdf{
11+
2.081176820e-21, 4.488660034e-01, 3.747774338e-01, 2.693297528e-01, 1.905637275e-01,
12+
1.351476913e-01, 9.636538981e-02, 6.909040154e-02, 4.978006801e-02, 3.602084467e-02,
13+
};
14+
const std::vector<double> x = linspace(0.01, 5.0, 10);
15+
for (int i = 0; i < x.size(); ++i) {
16+
double pdf = xsf::cpu::geninvgauss_pdf(x[i], 0.5, 1.0);
17+
CAPTURE(i, x[i], pdf, expected_pdf[i]);
18+
REQUIRE(xsf::extended_relative_error(pdf, expected_pdf[i]) <= 1e-8);
19+
}
20+
}
21+
22+
TEST_CASE("generalized inverse Gaussian PDF support", "[geninvgauss][xsf_tests]") {
23+
// Mirrors https://github.com/scipy/scipy/blob/v1.18.1/scipy/stats/tests/test_distributions.py#L952-L957
24+
REQUIRE(xsf::cpu::geninvgauss_pdf(0.0, 0.5, 0.5) == 0.0);
25+
REQUIRE(xsf::cpu::geninvgauss_pdf(2e6, 50.0, 2.0) == 0.0);
26+
}

0 commit comments

Comments
 (0)