ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
exponential.cpp
Go to the documentation of this file.
1/**
2 * @file exponential.cpp
3 * @brief Exponential(λ) family implementation.
4 *
5 * Self-contained family implementation: the class is file-local and
6 * reaches the evaluators only through the registrars at the bottom
7 * (DistributionRegistry descriptor + pairwise comparator / closure
8 * rules).
9 */
10#include "DistributionCommon.h"
11
12#include <algorithm>
13#include <cmath>
14#include <memory>
15#include <optional>
16#include <random>
17#include <string>
18#include <utility>
19#include <vector>
20
21namespace provsql {
22
23namespace {
24
25double factorial(unsigned k)
26{
27 double r = 1.0;
28 for (unsigned i = 2; i <= k; ++i) r *= static_cast<double>(i);
29 return r;
30}
31
32/**
33 * @brief Raw moments of @c X ~ Exp(λ) truncated to @c [a, b].
34 *
35 * Decomposes via change of variable Y = X - max(a,0):
36 * - left endpoint @c a > 0, right endpoint @c b = +∞: by
37 * memorylessness @c X | X>a is distributed as @c a + Exp(λ), so
38 * @c E[X^k|X>a] = Σ_{i=0..k} C(k,i) a^{k-i} · i!/λ^i.
39 * - finite @c [a, b] (with @c a ≥ 0, @c b < ∞): integrate
40 * @c x^k λ e^{-λx} dx by parts and divide by the truncation mass
41 * @c e^{-λa} - e^{-λb}. Uses the recurrence
42 * @c I_k = k I_{k-1} / λ - (b^k e^{-λb} - a^k e^{-λa}) / λ
43 * with @c I_0 = e^{-λa} - e^{-λb}.
44 */
45double truncated_exponential_raw_moment(double lambda, double a, double b,
46 unsigned k)
47{
48 const double aa = std::max(a, 0.0); /* Exp support is [0, +∞) */
49 if (std::isfinite(b)) {
50 if (b <= aa) return kNaN;
51 /* Finite-interval recurrence on I_k = ∫_{aa}^{b} x^k λ e^{-λx} dx. */
52 const double e_a = std::exp(-lambda * aa);
53 const double e_b = std::exp(-lambda * b);
54 const double Z = e_a - e_b; /* P(aa < X < b) */
55 if (Z < 1e-12) return kNaN;
56 if (k == 0) return 1.0;
57 /* Integration by parts: ∫ x^k λ e^{-λx} dx = -x^k e^{-λx} + k ∫ x^{k-1} e^{-λx} dx
58 * so I_k (with λ factor folded into the e^{-λx}·λ dx term) follows:
59 * I_k = [aa^k e^{-λaa} - b^k e^{-λb}] + (k/λ) · I_{k-1}_no_lambda
60 * where I_{k-1}_no_lambda = ∫ x^{k-1} e^{-λx} dx = I_{k-1}/λ.
61 * Cleaner: compute J_k = ∫_{aa}^{b} x^k e^{-λx} dx via
62 * J_0 = Z/λ; J_k = (aa^k e^{-λaa} - b^k e^{-λb})/λ + (k/λ) J_{k-1}.
63 * Then E[X^k|aa<X<b] = λ J_k / Z. */
64 std::vector<double> J(k + 1, 0.0);
65 J[0] = Z / lambda;
66 for (unsigned m = 1; m <= k; ++m) {
67 const double endpoint = std::pow(aa, static_cast<double>(m)) * e_a
68 - std::pow(b, static_cast<double>(m)) * e_b;
69 J[m] = endpoint / lambda + (m / lambda) * J[m - 1];
70 }
71 return lambda * J[k] / Z;
72 }
73 /* Semi-infinite right tail [aa, +∞): memorylessness. */
74 double total = 0.0;
75 double fact_i = 1.0;
76 for (unsigned i = 0; i <= k; ++i) {
77 total += binomial_coeff(k, i)
78 * std::pow(aa, static_cast<double>(k - i))
79 * fact_i / std::pow(lambda, static_cast<double>(i));
80 fact_i *= (i + 1);
81 }
82 return total;
83}
84
85/** @brief Exponential(λ=p1). p2 unused. */
86class ExponentialDistribution final : public BaseDistribution {
87public:
89 const DistributionFamily &family() const override;
90 double mean() const override { return 1.0 / p1_; }
91 double variance() const override { return 1.0 / (p1_ * p1_); }
92 double rawMoment(unsigned k) const override {
93 if (k == 0) return 1.0;
94 if (k == 1) return mean();
95 return factorial(k) / std::pow(p1_, static_cast<double>(k));
96 }
97 double pdf(double c) const override {
98 if (!(p1_ > 0.0)) return kNaN;
99 return (c < 0.0) ? 0.0 : p1_ * std::exp(-p1_ * c);
100 }
101 double cdf(double c) const override {
102 return (c <= 0.0) ? 0.0 : -std::expm1(-p1_ * c); // 1 - exp(-λc)
103 }
104 DistSupport support() const override { return {0.0, kInf}; }
105 bool integrationRange(double &lo, double &hi) const override {
106 if (!(p1_ > 0.0)) return false;
107 lo = 0.0;
108 hi = 40.0 / p1_;
109 return true;
110 }
111 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
112 double lo = trunc_lo, hi = trunc_hi;
113 if (!std::isfinite(lo)) lo = 0.0;
114 if (!std::isfinite(hi)) hi = 6.0 / p1_;
115 return {lo, hi};
116 }
117 double sample(std::mt19937_64 &rng) const override {
118 std::exponential_distribution<double> d(p1_);
119 return d(rng);
120 }
121 std::optional<double> quantile(double p) const override {
122 if (!(p1_ > 0.0)) return std::nullopt;
123 /* -log1p(-p)/λ for accuracy as p approaches 1. */
124 return -std::log1p(-p) / p1_;
125 }
126 std::optional<double> truncatedRawMoment(double lo, double hi,
127 unsigned k) const override {
128 const double r = truncated_exponential_raw_moment(p1_, lo, hi, k);
129 if (std::isnan(r)) return std::nullopt;
130 return r;
131 }
132 std::optional<std::vector<double>> sampleTruncated(
133 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
134 const double lambda = p1_;
135 if (!(lambda > 0.0)) return std::nullopt;
136 std::vector<double> out;
137 out.reserve(n);
138 if (std::isinf(hi)) {
139 /* X | X > lo = lo + Exp(λ) by memorylessness. Numerically
140 * stable for arbitrarily large @c lo, where the inverse-CDF
141 * would underflow on @c 1 - exp(-λ·lo). */
142 std::exponential_distribution<double> E(lambda);
143 for (unsigned i = 0; i < n; ++i) out.push_back(lo + E(rng));
144 return out;
145 }
146 /* Two-sided truncation: inverse-CDF on @c [F(lo), F(hi)] with
147 * @c F(x) = -expm1(-λx) for accuracy near 0, and @c x =
148 * -log1p(-u)/λ for accuracy as @c u approaches 1. */
149 const double F_lo = -std::expm1(-lambda * lo);
150 const double F_hi = -std::expm1(-lambda * hi);
151 if (!(F_lo < F_hi)) return std::nullopt;
152 std::uniform_real_distribution<double> U01(0.0, 1.0);
153 for (unsigned i = 0; i < n; ++i) {
154 const double u = F_lo + U01(rng) * (F_hi - F_lo);
155 out.push_back(-std::log1p(-u) / lambda);
156 }
157 return out;
158 }
159 std::optional<double> iidOrderStatMean(std::size_t n,
160 bool isMax) const override {
161 const double lambda = p1_;
162 if (!(lambda > 0.0))
163 return std::nullopt;
164 if (!isMax)
165 return 1.0 / (static_cast<double>(n) * lambda); /* min of exps */
166 double H = 0.0; /* max: harmonic */
167 for (std::size_t k = 1; k <= n; ++k)
168 H += 1.0 / static_cast<double>(k);
169 return H / lambda;
170 }
171 std::unique_ptr<Distribution> affine(double a, double b) const override {
172 /* Closed under positive scaling only: a negative coefficient flips
173 * the support to (-∞, 0] and an offset shifts it, neither of which
174 * is exponential. */
175 if (!(a > 0.0) || b != 0.0) return nullptr;
176 return std::make_unique<ExponentialDistribution>(p1_ / a, 0.0);
177 }
178 std::string serialise() const override {
179 return "exponential:" + double_to_text(p1_);
180 }
181};
182
183const DistributionFamily exponential_family = {
184 "exponential", 1, "Exp", {"λ", nullptr},
185 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
186 return std::make_unique<ExponentialDistribution>(p1, p2);
187 }};
188
189const DistributionFamily &ExponentialDistribution::family() const
190{
191 return exponential_family;
192}
193
194/* P(X < Y) = λ_X / (λ_X + λ_Y) for independent exponentials. */
195double exponentialPairLess(const Distribution &X, const Distribution &Y)
196{
197 const double lx = X.p1(), ly = Y.p1();
198 if (!(lx > 0.0 && ly > 0.0)) return kNaN;
199 return lx / (lx + ly);
200}
201
202[[maybe_unused]] const ComparatorRuleRegistrar exponential_less_rule(
203 "exponential", "exponential", &exponentialPairLess);
204
205/* Gamma-Exponential conjugacy: an Exponential(θ) observation of a
206 * Gamma(k, λ) prior on the rate updates to Gamma(k+1, λ+d). The
207 * predictive is the Lomax (Pareto type II) density
208 * m(d) = k λ^k / (λ + d)^{k+1}. */
209bool exponentialRateConjugateUpdate(double &k, double &lambda,
210 const DistributionTemplate &, double d)
211{
212 if (!(k > 0.0) || !(lambda > 0.0) || !(d >= 0.0)) return false;
213 k += 1.0;
214 lambda += d;
215 return true;
216}
217
218double exponentialRateLogPredictive(double k, double lambda,
219 const DistributionTemplate &, double d)
220{
221 if (!(k > 0.0) || !(lambda > 0.0) || !(d >= 0.0)) return kNaN;
222 return std::log(k) + k * std::log(lambda)
223 - (k + 1.0) * std::log(lambda + d);
224}
225
226[[maybe_unused]] const ConjugateRuleRegistrar exponential_rate_conjugate(
227 "exponential", 0, "gamma",
228 {&exponentialRateConjugateUpdate, &exponentialRateLogPredictive});
229
230[[maybe_unused]] const DistributionFamilyRegistrar exponential_family_registrar(
231 exponential_family);
232
233} // namespace
234
235} // 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)
constexpr double kInf
double binomial_coeff(unsigned n, unsigned k)
C(n, k) as a double (exact for the small moment orders used here).
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.