ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
erlang.cpp
Go to the documentation of this file.
1/**
2 * @file erlang.cpp
3 * @brief Erlang(k, λ) family implementation (integer shape).
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 <cstring>
15#include <memory>
16#include <optional>
17#include <random>
18#include <string>
19#include <utility>
20#include <vector>
21
22namespace provsql {
23
24namespace {
25
26/** @brief Erlang(k=p1 integer≥1, λ=p2). */
27class ErlangDistribution final : public BaseDistribution {
28public:
30 const DistributionFamily &family() const override;
31 double mean() const override { return p1_ / p2_; }
32 double variance() const override { return p1_ / (p2_ * p2_); }
33 double rawMoment(unsigned k) const override {
34 if (k == 0) return 1.0;
35 if (k == 1) return mean();
36 // s(s+1)...(s+k-1) / lambda^k for integer shape s.
37 const double s = p1_;
38 double rising = 1.0;
39 for (unsigned i = 0; i < k; ++i) rising *= (s + static_cast<double>(i));
40 return rising / std::pow(p2_, static_cast<double>(k));
41 }
42 double pdf(double c) const override {
43 const double s = p1_, lambda = p2_;
44 if (s < 1.0 || s != std::floor(s) || !(lambda > 0.0)) return kNaN;
45 if (c < 0.0) return 0.0;
46 const unsigned long k = static_cast<unsigned long>(s);
47 double fact = 1.0; // (k-1)!
48 for (unsigned long i = 2; i < k; ++i) fact *= static_cast<double>(i);
49 return std::pow(lambda, static_cast<double>(k))
50 * std::pow(c, static_cast<double>(k - 1))
51 * std::exp(-lambda * c) / fact;
52 }
53 double cdf(double c) const override {
54 const double s = p1_, lambda = p2_;
55 if (s < 1.0 || s != std::floor(s)) return kNaN;
56 if (c <= 0.0) return 0.0;
57 const unsigned long k = static_cast<unsigned long>(s);
58 const double lc = lambda * c;
59 double term = 1.0, sum = 1.0; // 1 - e^{-λc} Σ_{n=0..k-1}(λc)^n/n!
60 for (unsigned long n = 1; n < k; ++n) {
61 term *= lc / static_cast<double>(n);
62 sum += term;
63 }
64 return 1.0 - std::exp(-lc) * sum;
65 }
66 DistSupport support() const override { return {0.0, kInf}; }
67 bool integrationRange(double &lo, double &hi) const override {
68 if (!(p1_ >= 1.0 && p2_ > 0.0)) return false;
69 lo = 0.0;
70 hi = (p1_ + 12.0 * std::sqrt(p1_)) / p2_;
71 return true;
72 }
73 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
74 double lo = trunc_lo, hi = trunc_hi;
75 if (!std::isfinite(lo)) lo = 0.0;
76 if (!std::isfinite(hi)) hi = std::max(2.0 * p1_ / p2_, 6.0 / p2_);
77 return {lo, hi};
78 }
79 double sample(std::mt19937_64 &rng) const override {
80 // Gamma(shape=k, scale=1/λ) samples Erlang(k, λ) directly.
81 std::gamma_distribution<double> d(p1_, 1.0 / p2_);
82 return d(rng);
83 }
84 std::unique_ptr<Distribution> affine(double a, double b) const override {
85 /* Same positive-scaling-only closure as Exponential. */
86 if (!(a > 0.0) || b != 0.0) return nullptr;
87 /* Integer k stored in p1; non-integer is rejected upstream by the
88 * SQL constructor, but guard defensively. */
89 if (p1_ < 1.0 || p1_ != std::floor(p1_)) return nullptr;
90 return std::make_unique<ErlangDistribution>(p1_, p2_ / a);
91 }
92 std::string serialise() const override {
93 /* The integer shape prints without a decimal point / exponent
94 * whatever its magnitude, matching the constructor's encoding. */
95 if (p1_ >= 1.0 && p1_ == std::floor(p1_))
96 return "erlang:" + std::to_string(static_cast<unsigned long>(p1_))
97 + "," + double_to_text(p2_);
98 return "erlang:" + double_to_text(p1_) + "," + double_to_text(p2_);
99 }
100};
101
102const DistributionFamily erlang_family = {
103 "erlang", 2, "Erl", {"k", "λ"},
104 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
105 return std::make_unique<ErlangDistribution>(p1, p2);
106 }};
107
108const DistributionFamily &ErlangDistribution::family() const
109{
110 return erlang_family;
111}
112
113/* (Exp|Erlang, +, Exp|Erlang), same rate: Erlang(Σkᵢ, λ) with
114 * Exp ≡ Erlang(1, λ). Strict shape: unscaled, unshifted, no additive
115 * constants (any of those leaves the family; hypoexponential /
116 * different-rate sums are the sampler's job), rates exactly equal. */
117std::unique_ptr<Distribution>
118erlangSumRule(const std::vector<ClosureTerm> &terms)
119{
120 double lambda = kNaN;
121 unsigned long total_shape = 0;
122 for (const auto &t : terms) {
123 if (!t.dist) return nullptr; /* additive constant */
124 if (t.a != 1.0 || t.b != 0.0) return nullptr; /* scaled / shifted */
125 double w_lambda;
126 unsigned long w_shape;
127 if (std::strcmp(t.dist->family().name, "exponential") == 0) {
128 w_lambda = t.dist->p1();
129 w_shape = 1;
130 } else {
131 /* Integer k stored in p1; guard defensively so a corrupted extra
132 * cannot trigger an invalid shape sum. */
133 if (t.dist->p1() < 1.0 || t.dist->p1() != std::floor(t.dist->p1()))
134 return nullptr;
135 w_lambda = t.dist->p2();
136 w_shape = static_cast<unsigned long>(t.dist->p1());
137 }
138 if (std::isnan(lambda)) lambda = w_lambda;
139 else if (lambda != w_lambda) return nullptr; /* different rate */
140 total_shape += w_shape;
141 }
142 return std::make_unique<ErlangDistribution>(
143 static_cast<double>(total_shape), lambda);
144}
145
146[[maybe_unused]] const ClosureRuleRegistrar erlang_sum_rules[] = {
147 {"exponential", "exponential", &erlangSumRule},
148 {"exponential", "erlang", &erlangSumRule},
149 {"erlang", "exponential", &erlangSumRule},
150 {"erlang", "erlang", &erlangSumRule},
151};
152
153/* Gamma-Erlang conjugacy: the integer-shape case of the Gamma-Gamma
154 * rate rule (gamma.cpp) -- an Erlang(k₀, θ) observation with the latent
155 * in the rate slot updates a Gamma(k, λ) prior to Gamma(k+k₀, λ+d),
156 * with the same compound-gamma predictive. Guards the integer shape
157 * the family's own pdf requires (a non-integer k₀ makes the Erlang
158 * likelihood undefined, so declining matches the sampler's NaN). */
159bool erlangRateConjugateUpdate(double &k, double &lambda,
160 const DistributionTemplate &lik, double d)
161{
162 const double k0 = lik.p1.literal;
163 if (k0 < 1.0 || k0 != std::floor(k0)) return false;
164 if (!(k > 0.0) || !(lambda > 0.0) || !(d > 0.0)) return false;
165 k += k0;
166 lambda += d;
167 return true;
168}
169
170double erlangRateLogPredictive(double k, double lambda,
171 const DistributionTemplate &lik, double d)
172{
173 const double k0 = lik.p1.literal;
174 if (k0 < 1.0 || k0 != std::floor(k0)) return kNaN;
175 if (!(k > 0.0) || !(lambda > 0.0) || !(d > 0.0)) return kNaN;
176 return std::lgamma(k + k0) - std::lgamma(k) - std::lgamma(k0)
177 + k * std::log(lambda) + (k0 - 1.0) * std::log(d)
178 - (k + k0) * std::log(lambda + d);
179}
180
181[[maybe_unused]] const ConjugateRuleRegistrar erlang_rate_conjugate(
182 "erlang", 1, "gamma",
183 {&erlangRateConjugateUpdate, &erlangRateLogPredictive});
184
185[[maybe_unused]] const DistributionFamilyRegistrar erlang_family_registrar(
186 erlang_family);
187
188} // namespace
189
190} // 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
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.