ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
geometric.cpp
Go to the documentation of this file.
1/**
2 * @file geometric.cpp
3 * @brief Geometric(p) family implementation (discrete).
4 *
5 * Number of trials until the first success, support @c {1, 2, 3, ...},
6 * @c pmf(k) = (1-p)^{k-1} p -- the same convention as the literal
7 * @c provsql.geometric constructor (which enumerates a categorical). Only
8 * ever instantiated for a @b latent leaf @c provsql.geometric(random_variable);
9 * reached by @c sample(), by @c observe (weight = pmf at the datum), and the
10 * moment readouts. Self-contained: registered at the bottom.
11 */
12#include "DistributionCommon.h"
13
14#include <cmath>
15#include <memory>
16#include <optional>
17#include <random>
18#include <string>
19#include <utility>
20
21namespace provsql {
22
23namespace {
24
25/** @brief Geometric(p=p1) on {1, 2, ...}. p2 unused. */
26class GeometricDistribution final : public BaseDistribution {
27public:
29 const DistributionFamily &family() const override;
30
31 bool valid() const { return p1_ > 0.0 && p1_ <= 1.0; }
32
33 double mean() const override { return 1.0 / p1_; } // 1/p
34 bool isDiscrete() const override { return true; }
35 double variance() const override { return (1.0 - p1_) / (p1_ * p1_); }
36 double rawMoment(unsigned k) const override {
37 if (k == 0) return 1.0;
38 // Σ_{j>=1} j^k (1-p)^{j-1} p, truncated once the surviving tail mass
39 // (1-p)^j is negligible; the cap bounds a pathologically small p.
40 if (!valid()) return kNaN;
41 const double q = 1.0 - p1_;
42 double total = 0.0, term = p1_; // term = pmf(j), starting j=1
43 for (long j = 1; j <= 100000000L; ++j) {
44 total += std::pow(static_cast<double>(j), static_cast<double>(k)) * term;
45 term *= q; // pmf(j+1) = pmf(j)·(1-p)
46 if (term < 1e-18) break;
47 }
48 return total;
49 }
50 double pdf(double c) const override { // pmf at an integer >= 1
51 if (!valid()) return kNaN;
52 const double r = std::nearbyint(c);
53 if (std::fabs(c - r) > 1e-9 || r < 1.0) return 0.0;
54 return std::pow(1.0 - p1_, r - 1.0) * p1_;
55 }
56 double cdf(double c) const override {
57 if (!valid()) return kNaN;
58 if (c < 1.0) return 0.0;
59 const double kf = std::floor(c);
60 return 1.0 - std::pow(1.0 - p1_, kf); // P(X <= k) = 1 - (1-p)^k
61 }
62 DistSupport support() const override { return {1.0, kInf}; }
63 bool integrationRange(double &lo, double &hi) const override {
64 if (!valid()) return false;
65 lo = 1.0;
66 // Smallest k with (1-p)^k < 1e-13 (all but a vanishing tail).
67 hi = (p1_ >= 1.0) ? 1.0
68 : 1.0 + std::ceil(std::log(1e-13) / std::log(1.0 - p1_));
69 return true;
70 }
71 std::pair<double, double> plotRange(double trunc_lo,
72 double trunc_hi) const override {
73 double lo = trunc_lo, hi = trunc_hi;
74 if (!std::isfinite(lo)) lo = 1.0;
75 if (!std::isfinite(hi)) {
76 // integrationRange leaves its outputs untouched and returns false on an
77 // invalid distribution; fall back to the (degenerate) lower bound then.
78 double a = lo, b = lo;
79 integrationRange(a, b);
80 hi = b;
81 }
82 return {lo, hi};
83 }
84 double sample(std::mt19937_64 &rng) const override {
85 // std::geometric_distribution counts failures on {0, 1, ...}; the trial
86 // count (first success) is that plus one.
87 std::geometric_distribution<long> d(p1_);
88 return static_cast<double>(d(rng) + 1);
89 }
90 std::optional<double> quantile(double p) const override {
91 if (!valid()) return std::nullopt;
92 if (p <= 0.0) return 1.0;
93 if (p1_ >= 1.0) return 1.0;
94 // Smallest k with 1 - (1-p)^k >= p <=> k >= ln(1-p)/ln(1-p1).
95 const double k = std::ceil(std::log(1.0 - p) / std::log(1.0 - p1_));
96 return k < 1.0 ? 1.0 : k;
97 }
98 std::unique_ptr<Distribution> affine(double, double) const override {
99 return nullptr; // discrete: not closed under a·X + b
100 }
101 std::string serialise() const override {
102 return "geometric:" + double_to_text(p1_);
103 }
104};
105
106const DistributionFamily geometric_family = {
107 "geometric", 1, "Geo", {"p", nullptr},
108 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
109 return std::make_unique<GeometricDistribution>(p1, p2);
110 }};
111
112const DistributionFamily &GeometricDistribution::family() const
113{
114 return geometric_family;
115}
116
117/* Beta-Geometric conjugacy, in the family's TRIALS convention (support
118 * {1, 2, ...}, pmf p(1−p)^{d−1}): one observation of d trials updates a
119 * Beta(α, β) prior to Beta(α+1, β+d−1). The predictive is
120 * m(d) = B(α+1, β+d−1) / B(α, β). */
121bool geometricPConjugateUpdate(double &alpha, double &beta,
122 const DistributionTemplate &, double d)
123{
124 if (!(alpha > 0.0) || !(beta > 0.0)) return false;
125 const double r = std::nearbyint(d);
126 if (std::fabs(d - r) > 1e-9 || r < 1.0) return false;
127 alpha += 1.0;
128 beta += r - 1.0;
129 return true;
130}
131
132double geometricPLogPredictive(double alpha, double beta,
133 const DistributionTemplate &, double d)
134{
135 if (!(alpha > 0.0) || !(beta > 0.0)) return kNaN;
136 const double r = std::nearbyint(d);
137 if (std::fabs(d - r) > 1e-9 || r < 1.0) return kNaN;
138 return lbeta(alpha + 1.0, beta + r - 1.0) - lbeta(alpha, beta);
139}
140
141[[maybe_unused]] const ConjugateRuleRegistrar geometric_p_conjugate(
142 "geometric", 0, "beta",
143 {&geometricPConjugateUpdate, &geometricPLogPredictive});
144
145[[maybe_unused]] const DistributionFamilyRegistrar geometric_family_registrar(
146 geometric_family);
147
148} // namespace
149
150} // 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 lbeta(double a, double b)
ln B(a, b) = lnΓ(a) + lnΓ(b) − lnΓ(a+b), for a, b > 0.
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.