ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
gamma.cpp
Go to the documentation of this file.
1/**
2 * @file gamma.cpp
3 * @brief Gamma(k, λ) family implementation (shape k any positive real,
4 * rate λ).
5 *
6 * The general-shape counterpart of Erlang: the SQL constructor routes
7 * integer shapes through @c provsql.erlang, so a @c gamma leaf always
8 * carries a non-integer (or sub-1) shape here. The CDF is the
9 * regularised lower incomplete gamma @f$P(k, \lambda x)@f$ (series /
10 * continued fraction); Chi-squared is the SQL-level sugar
11 * <tt>gamma(k/2, 1/2)</tt>.
12 *
13 * Self-contained family implementation: the class is file-local and
14 * reaches the evaluators only through the registrars at the bottom
15 * (DistributionRegistry factory + closure rule). First family added
16 * under the per-family layout -- no evaluator or parser code changes.
17 */
18#include "DistributionCommon.h"
19
20#include <algorithm>
21#include <cmath>
22#include <memory>
23#include <optional>
24#include <random>
25#include <string>
26#include <utility>
27#include <vector>
28
29namespace provsql {
30
31namespace {
32
33/** @brief Gamma(k=p1 shape > 0, λ=p2 rate > 0). */
34class GammaDistribution final : public BaseDistribution {
35public:
37 const DistributionFamily &family() const override;
38 double mean() const override { return p1_ / p2_; }
39 double variance() const override { return p1_ / (p2_ * p2_); }
40 double rawMoment(unsigned k) const override {
41 if (k == 0) return 1.0;
42 if (k == 1) return mean();
43 // Γ(s+k)/(Γ(s) λ^k) = s(s+1)...(s+k-1)/λ^k, valid for any real s > 0.
44 const double s = p1_;
45 double rising = 1.0;
46 for (unsigned i = 0; i < k; ++i) rising *= (s + static_cast<double>(i));
47 return rising / std::pow(p2_, static_cast<double>(k));
48 }
49 double pdf(double c) const override {
50 const double s = p1_, lambda = p2_;
51 if (!(s > 0.0) || !(lambda > 0.0)) return kNaN;
52 if (c < 0.0) return 0.0;
53 if (c == 0.0) {
54 /* The density diverges at 0 for shape < 1 (integrable
55 * singularity). Report NaN rather than +∞ so the Simpson
56 * quadratures -- which cannot resolve a singular endpoint on a
57 * uniform grid -- decline and fall back to Monte Carlo instead
58 * of integrating garbage. */
59 if (s < 1.0) return kNaN;
60 return (s == 1.0) ? lambda : 0.0;
61 }
62 return std::exp(s * std::log(lambda) + (s - 1.0) * std::log(c)
63 - lambda * c - std::lgamma(s));
64 }
65 double cdf(double c) const override {
66 const double s = p1_, lambda = p2_;
67 if (!(s > 0.0) || !(lambda > 0.0)) return kNaN;
68 if (c <= 0.0) return 0.0;
69 return gammaP(s, lambda * c);
70 }
71 DistSupport support() const override { return {0.0, kInf}; }
72 bool integrationRange(double &lo, double &hi) const override {
73 if (!(p1_ > 0.0 && p2_ > 0.0)) return false;
74 /* Same k + 12√k window as Erlang (mean + 12 standard deviations
75 * in units of 1/λ). */
76 lo = 0.0;
77 hi = (p1_ + 12.0 * std::sqrt(p1_)) / p2_;
78 return true;
79 }
80 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
81 double lo = trunc_lo, hi = trunc_hi;
82 if (!std::isfinite(lo)) lo = 0.0;
83 if (!std::isfinite(hi)) hi = std::max(2.0 * p1_ / p2_, 6.0 / p2_);
84 /* Shape < 1: the density has an integrable singularity at 0 (pdf(0)
85 * is NaN by design, so the quadratures decline). Nudge the window's
86 * left edge just inside so a uniform plot grid never samples the
87 * singular endpoint -- otherwise every curve payload for a sub-1
88 * shape would be all-NaN-poisoned and dropped. */
89 if (p1_ < 1.0 && lo <= 0.0) lo = 0.005 * hi;
90 return {lo, hi};
91 }
92 double sample(std::mt19937_64 &rng) const override {
93 // Gamma(shape=k, scale=1/λ) in the std parameterisation.
94 std::gamma_distribution<double> d(p1_, 1.0 / p2_);
95 return d(rng);
96 }
97 std::unique_ptr<Distribution> affine(double a, double b) const override {
98 /* Same positive-scaling-only closure as Erlang / Exponential. */
99 if (!(a > 0.0) || b != 0.0) return nullptr;
100 if (!(p1_ > 0.0)) return nullptr;
101 return std::make_unique<GammaDistribution>(p1_, p2_ / a);
102 }
103 std::string serialise() const override {
104 return "gamma:" + double_to_text(p1_) + "," + double_to_text(p2_);
105 }
106};
107
108const DistributionFamily gamma_family = {
109 "gamma", 2, "Γ", {"k", "λ"},
110 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
111 return std::make_unique<GammaDistribution>(p1, p2);
112 }};
113
114const DistributionFamily &GammaDistribution::family() const
115{
116 return gamma_family;
117}
118
119/* (Gamma, +, Gamma), same rate: Gamma(Σkᵢ, λ) for independent terms.
120 * Strict shape like the Erlang rule: unscaled, unshifted, no additive
121 * constants, rates exactly equal. Mixed Gamma + Exponential / Erlang
122 * sums are NOT registered (the closure-rule dispatch requires one rule
123 * per family combination and those pairs belong to the Erlang-sum
124 * rule); they stay unfolded and Monte Carlo handles them, which is
125 * sound -- only the closed form is missed. */
126std::unique_ptr<Distribution>
127gammaSumRule(const std::vector<ClosureTerm> &terms)
128{
129 double lambda = kNaN;
130 double total_shape = 0.0;
131 for (const auto &t : terms) {
132 if (!t.dist) return nullptr; /* additive constant */
133 if (t.a != 1.0 || t.b != 0.0) return nullptr; /* scaled / shifted */
134 if (!(t.dist->p1() > 0.0)) return nullptr;
135 const double w_lambda = t.dist->p2();
136 if (std::isnan(lambda)) lambda = w_lambda;
137 else if (lambda != w_lambda) return nullptr; /* different rate */
138 total_shape += t.dist->p1();
139 }
140 return std::make_unique<GammaDistribution>(total_shape, lambda);
141}
142
143[[maybe_unused]] const ClosureRuleRegistrar gamma_sum_rule(
144 "gamma", "gamma", &gammaSumRule);
145
146/* Gamma-Gamma conjugacy: a Gamma(k₀, θ) observation with the latent in
147 * the RATE slot (k₀ a known literal shape) updates a Gamma(k, λ) prior
148 * to Gamma(k+k₀, λ+d). The predictive is the compound-gamma
149 * (scaled beta-prime) density
150 * m(d) = Γ(k+k₀)/(Γ(k)Γ(k₀)) · λ^k d^{k₀−1} / (λ+d)^{k+k₀}. */
151bool gammaRateConjugateUpdate(double &k, double &lambda,
152 const DistributionTemplate &lik, double d)
153{
154 const double k0 = lik.p1.literal;
155 if (!(k > 0.0) || !(lambda > 0.0) || !(k0 > 0.0) || !(d > 0.0))
156 return false;
157 k += k0;
158 lambda += d;
159 return true;
160}
161
162double gammaRateLogPredictive(double k, double lambda,
163 const DistributionTemplate &lik, double d)
164{
165 const double k0 = lik.p1.literal;
166 if (!(k > 0.0) || !(lambda > 0.0) || !(k0 > 0.0) || !(d > 0.0))
167 return kNaN;
168 return std::lgamma(k + k0) - std::lgamma(k) - std::lgamma(k0)
169 + k * std::log(lambda) + (k0 - 1.0) * std::log(d)
170 - (k + k0) * std::log(lambda + d);
171}
172
173[[maybe_unused]] const ConjugateRuleRegistrar gamma_rate_conjugate(
174 "gamma", 1, "gamma",
175 {&gammaRateConjugateUpdate, &gammaRateLogPredictive});
176
177[[maybe_unused]] const DistributionFamilyRegistrar gamma_family_registrar(
178 gamma_family);
179
180} // namespace
181
182} // 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 gammaP(double a, double x)
Regularised lower incomplete gamma for a > 0, x >= 0.
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.