ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
inverse_gaussian.cpp
Go to the documentation of this file.
1/**
2 * @file inverse_gaussian.cpp
3 * @brief InverseGaussian(μ, λ) / Wald family implementation: mean
4 * @c μ > 0, shape @c λ > 0.
5 *
6 * The first-passage time of Brownian motion with drift: a positive,
7 * right-skewed family widely used for reliability and reaction-time
8 * models. Its CDF has a closed form in terms of the standard normal
9 * @c Φ, so @f$P(X < c)@f$ and the numeric quantile are analytic; all raw
10 * moments are finite (a finite-sum closed form). Positive scalings map
11 * @c c·IG(μ, λ) @c = @c IG(cμ, cλ), and a sum of independent inverse
12 * Gaussians that share the ratio @c λ/μ² folds to a single inverse
13 * Gaussian in the simplifier (registered as a closure rule below).
14 *
15 * Self-contained family implementation: the class is file-local and
16 * reaches the evaluators only through the registrars at the bottom
17 * (DistributionRegistry factory + sum-closure rule). No evaluator or
18 * parser code changes.
19 */
20#include "DistributionCommon.h"
21
22#include <algorithm>
23#include <cmath>
24#include <memory>
25#include <optional>
26#include <random>
27#include <string>
28#include <utility>
29#include <vector>
30
31namespace provsql {
32
33namespace {
34
35/** @brief n! as a double (exact for the small moment orders used here). */
36double factorial(unsigned n)
37{
38 double r = 1.0;
39 for (unsigned i = 2; i <= n; ++i) r *= static_cast<double>(i);
40 return r;
41}
42
43/** @brief InverseGaussian(μ=p1 mean > 0, λ=p2 shape > 0). */
44class InverseGaussianDistribution final : public BaseDistribution {
45public:
47 const DistributionFamily &family() const override;
48 double mean() const override { return p1_; }
49 bool meanIsAffine() const override { return true; } // mean = μ
50 double variance() const override { return p1_ * p1_ * p1_ / p2_; }
51 double rawMoment(unsigned n) const override {
52 if (n == 0) return 1.0;
53 if (n == 1) return p1_;
54 const double mu = p1_, lambda = p2_;
55 /* E[X^n] = μ^n Σ_{s=0}^{n-1} (n-1+s)!/(s!(n-1-s)!) · (μ/2λ)^s. */
56 const double r = mu / (2.0 * lambda);
57 double sum = 0.0, rpow = 1.0;
58 for (unsigned s = 0; s < n; ++s) {
59 const double coef =
60 factorial(n - 1 + s) / (factorial(s) * factorial(n - 1 - s));
61 sum += coef * rpow;
62 rpow *= r;
63 }
64 return std::pow(mu, static_cast<double>(n)) * sum;
65 }
66 double pdf(double c) const override {
67 const double mu = p1_, lambda = p2_;
68 if (!(mu > 0.0) || !(lambda > 0.0)) return kNaN;
69 if (c <= 0.0) return 0.0;
70 const double d = c - mu;
71 return std::sqrt(lambda / (2.0 * M_PI * c * c * c))
72 * std::exp(-lambda * d * d / (2.0 * mu * mu * c));
73 }
74 double cdf(double c) const override {
75 const double mu = p1_, lambda = p2_;
76 if (!(mu > 0.0) || !(lambda > 0.0)) return kNaN;
77 if (c <= 0.0) return 0.0;
78 const double s = std::sqrt(lambda / c);
79 const double term1 = Phi(s * (c / mu - 1.0));
80 /* exp(2λ/μ) · Φ(-√(λ/c)(c/μ + 1)): the exponential overflows for
81 * large λ/μ while its Φ factor underflows; their product tends to 0,
82 * so guard the inf·0 with a finite-check. */
83 double term2 = std::exp(2.0 * lambda / mu) * Phi(-s * (c / mu + 1.0));
84 if (!std::isfinite(term2)) term2 = 0.0;
85 return term1 + term2;
86 }
87 DistSupport support() const override { return {0.0, kInf}; }
88 bool integrationRange(double &lo, double &hi) const override {
89 if (!(p1_ > 0.0 && p2_ > 0.0)) return false;
90 /* Mean μ + 12 standard deviations √(μ³/λ). */
91 lo = 0.0;
92 hi = p1_ + 12.0 * std::sqrt(p1_ * p1_ * p1_ / p2_);
93 return true;
94 }
95 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
96 double lo = trunc_lo, hi = trunc_hi;
97 if (!std::isfinite(lo)) lo = 0.0;
98 if (!std::isfinite(hi))
99 hi = p1_ + 4.0 * std::sqrt(p1_ * p1_ * p1_ / p2_);
100 return {lo, hi};
101 }
102 double sample(std::mt19937_64 &rng) const override {
103 /* Michael-Schucany-Haas (1976): one chi-square draw + a Bernoulli
104 * accept/reflect on the two roots. */
105 const double mu = p1_, lambda = p2_;
106 std::normal_distribution<double> N01(0.0, 1.0);
107 std::uniform_real_distribution<double> U01(0.0, 1.0);
108 const double nu = N01(rng);
109 const double y = nu * nu;
110 const double x = mu + (mu * mu * y) / (2.0 * lambda)
111 - (mu / (2.0 * lambda))
112 * std::sqrt(4.0 * mu * lambda * y + mu * mu * y * y);
113 if (U01(rng) <= mu / (mu + x)) return x;
114 return mu * mu / x;
115 }
116 std::unique_ptr<Distribution> affine(double a, double b) const override {
117 /* c · IG(μ, λ) = IG(cμ, cλ) for c > 0; negations flip the support
118 * and offsets shift it, neither of which is inverse-Gaussian. */
119 if (!(a > 0.0) || b != 0.0) return nullptr;
120 return std::make_unique<InverseGaussianDistribution>(a * p1_, a * p2_);
121 }
122 std::string serialise() const override {
123 return "inverse_gaussian:" + double_to_text(p1_) + ","
124 + double_to_text(p2_);
125 }
126};
127
128const DistributionFamily inverse_gaussian_family = {
129 "inverse_gaussian", 2, "IG", {"μ", "λ"},
130 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
131 return std::make_unique<InverseGaussianDistribution>(p1, p2);
132 }};
133
134const DistributionFamily &InverseGaussianDistribution::family() const
135{
136 return inverse_gaussian_family;
137}
138
139/* (IG, +, IG): a sum of independent inverse Gaussians that all share the
140 * ratio φ = λ/μ² folds to IG(Σμ, φ·(Σμ)²). Strict shape like the
141 * Gamma-sum rule: unscaled, unshifted, no additive constants, ratios
142 * exactly equal. A mismatch (or a scaled / shifted term) declines, and
143 * the sum stays unfolded for Monte Carlo -- only the closed form is
144 * missed. */
145std::unique_ptr<Distribution>
146inverseGaussianSumRule(const std::vector<ClosureTerm> &terms)
147{
148 double phi = kNaN;
149 double total_mu = 0.0;
150 for (const auto &t : terms) {
151 if (!t.dist) return nullptr; /* additive constant */
152 if (t.a != 1.0 || t.b != 0.0) return nullptr; /* scaled / shifted */
153 const double mu = t.dist->p1(), lambda = t.dist->p2();
154 if (!(mu > 0.0) || !(lambda > 0.0)) return nullptr;
155 const double w_phi = lambda / (mu * mu);
156 if (std::isnan(phi)) phi = w_phi;
157 else if (phi != w_phi) return nullptr; /* different ratio */
158 total_mu += mu;
159 }
160 if (!(total_mu > 0.0) || std::isnan(phi)) return nullptr;
161 return std::make_unique<InverseGaussianDistribution>(
162 total_mu, phi * total_mu * total_mu);
163}
164
165[[maybe_unused]] const ClosureRuleRegistrar inverse_gaussian_sum_rule(
166 "inverse_gaussian", "inverse_gaussian", &inverseGaussianSumRule);
167
168[[maybe_unused]] const DistributionFamilyRegistrar inverse_gaussian_family_registrar(
169 inverse_gaussian_family);
170
171} // namespace
172
173} // namespace provsql
Internal helpers shared by the per-family Distribution implementations under src/distributions/.
Base holding the two parameters; subclasses add closed forms.
BaseDistribution(double p1, double p2)
double phi(double z)
Standard normal pdf φ(z) = exp(-z²/2)/√(2π).
double Phi(double z)
Standard normal CDF Φ(z) = ½(1 + erf(z/√2)).
constexpr double kInf
std::string double_to_text(double v)
Format a double back into the canonical text form used by gate_value extras and gate_rv distribution ...
constexpr double kNaN
A registered family's descriptor: its complete identity.