ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
normal.cpp
Go to the documentation of this file.
1/**
2 * @file normal.cpp
3 * @brief Normal(μ, σ) 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
25// (j-1)!! with the empty-product convention (-1)!! = 1.
26double double_factorial_minus_one(unsigned j)
27{
28 if (j == 0) return 1.0;
29 double r = 1.0;
30 for (unsigned i = 1; i < j; i += 2) r *= static_cast<double>(i);
31 return r;
32}
33
34/**
35 * @brief Raw moments of @c X ~ Normal(μ, σ) truncated to @c [a, b].
36 *
37 * Closed form via the integration-by-parts recurrence on the
38 * standardised variable Z = (X - μ)/σ:
39 * E[Z^{k}|α<Z<β] = (k-1) E[Z^{k-2}|α<Z<β]
40 * + (α^{k-1}φ(α) − β^{k-1}φ(β)) / (Φ(β) − Φ(α))
41 * with E[Z^0|…] = 1 and E[Z^1|…] = (φ(α) − φ(β)) / (Φ(β) − Φ(α))
42 * (Greene, "Econometric Analysis", 5e, App. F). Then expand
43 * E[X^k] = E[(μ + σZ)^k] binomially.
44 *
45 * @c α = -∞ corresponds to @p a = -INFINITY (semi-infinite left tail);
46 * @c β = +∞ to @p b = +INFINITY. Returns @c NaN if @c P(α<Z<β) is
47 * below a numerical floor (so the caller falls through to MC).
48 */
49double truncated_normal_raw_moment(double mu, double sigma, double a, double b,
50 unsigned k)
51{
52 const double alpha = std::isfinite(a) ? (a - mu) / sigma : -kInf;
53 const double beta = std::isfinite(b) ? (b - mu) / sigma : +kInf;
54 const double Phi_alpha = std::isfinite(alpha) ? Phi(alpha) : 0.0;
55 const double Phi_beta = std::isfinite(beta) ? Phi(beta) : 1.0;
56 const double Z = Phi_beta - Phi_alpha;
57 if (Z < 1e-12) return kNaN;
58
59 const double phi_alpha = std::isfinite(alpha) ? phi(alpha) : 0.0;
60 const double phi_beta = std::isfinite(beta) ? phi(beta) : 0.0;
61
62 /* E[Z^k | α<Z<β] via recurrence; store all moments up to k. */
63 std::vector<double> M(k + 1, 0.0);
64 M[0] = 1.0;
65 if (k >= 1) M[1] = (phi_alpha - phi_beta) / Z;
66 for (unsigned m = 2; m <= k; ++m) {
67 /* α^{m-1}·φ(α) and β^{m-1}·φ(β); take 0 when the endpoint is
68 * infinite (the φ factor vanishes faster than any polynomial). */
69 double end_term = 0.0;
70 if (std::isfinite(alpha))
71 end_term += std::pow(alpha, static_cast<double>(m - 1)) * phi_alpha;
72 if (std::isfinite(beta))
73 end_term -= std::pow(beta, static_cast<double>(m - 1)) * phi_beta;
74 M[m] = (m - 1) * M[m - 2] + end_term / Z;
75 }
76
77 /* E[X^k] = E[(μ + σZ)^k] = Σ_{i=0..k} C(k,i) μ^{k-i} σ^i E[Z^i|…]. */
78 double total = 0.0;
79 for (unsigned i = 0; i <= k; ++i) {
80 total += binomial_coeff(k, i)
81 * std::pow(mu, static_cast<double>(k - i))
82 * std::pow(sigma, static_cast<double>(i))
83 * M[i];
84 }
85 return total;
86}
87
88/** @brief Normal(μ=p1, σ=p2). */
89class NormalDistribution final : public BaseDistribution {
90public:
92 const DistributionFamily &family() const override;
93 double mean() const override { return p1_; }
94 bool meanIsAffine() const override { return true; } // mean = μ
95 double variance() const override { return p2_ * p2_; }
96 double rawMoment(unsigned k) const override {
97 if (k == 0) return 1.0;
98 if (k == 1) return mean();
99 // E[X^k] = sum_{j=0,2,...}^{k} C(k,j) mu^(k-j) sigma^j (j-1)!!
100 const double mu = p1_, sigma = p2_;
101 double total = 0.0;
102 for (unsigned j = 0; j <= k; j += 2) {
103 total += binomial_coeff(k, j)
104 * std::pow(mu, static_cast<double>(k - j))
105 * std::pow(sigma, static_cast<double>(j))
106 * double_factorial_minus_one(j);
107 }
108 return total;
109 }
110 double pdf(double c) const override {
111 if (!(p2_ > 0.0)) return kNaN;
112 const double SQRT_2PI = std::sqrt(2.0 * M_PI);
113 const double z = (c - p1_) / p2_;
114 return std::exp(-0.5 * z * z) / (p2_ * SQRT_2PI);
115 }
116 double cdf(double c) const override {
117 const double SQRT2 = std::sqrt(2.0);
118 const double z = (c - p1_) / p2_;
119 return 0.5 * (1.0 + std::erf(z / SQRT2));
120 }
121 DistSupport support() const override { return {-kInf, kInf}; }
122 bool integrationRange(double &lo, double &hi) const override {
123 if (!(p2_ > 0.0)) return false;
124 lo = p1_ - 12.0 * p2_;
125 hi = p1_ + 12.0 * p2_;
126 return true;
127 }
128 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
129 double lo = trunc_lo, hi = trunc_hi;
130 if (!std::isfinite(lo)) lo = p1_ - 4.0 * p2_;
131 if (!std::isfinite(hi)) hi = p1_ + 4.0 * p2_;
132 return {lo, hi};
133 }
134 double sample(std::mt19937_64 &rng) const override {
135 std::normal_distribution<double> d(p1_, p2_);
136 return d(rng);
137 }
138 std::optional<double> quantile(double p) const override {
139 if (!(p2_ > 0.0)) return std::nullopt;
140 /* Beasley-Springer-Moro start (~1e-7), polished to machine
141 * precision by two Newton steps on Φ(z) - p (φ(z) > 0 everywhere,
142 * so the iteration is well-defined; the first step already lands
143 * within ~1e-14). */
144 double z = inv_phi(p);
145 for (int i = 0; i < 2; ++i) {
146 const double d = phi(z);
147 if (!(d > 0.0)) break; /* far tail: φ underflowed, keep BSM */
148 z -= (Phi(z) - p) / d;
149 }
150 return p1_ + p2_ * z;
151 }
152 std::optional<double> truncatedRawMoment(double lo, double hi,
153 unsigned k) const override {
154 const double r = truncated_normal_raw_moment(p1_, p2_, lo, hi, k);
155 if (std::isnan(r)) return std::nullopt;
156 return r;
157 }
158 std::optional<std::vector<double>> sampleTruncated(
159 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
160 const double mu = p1_, sigma = p2_;
161 if (!(sigma > 0.0)) return std::nullopt;
162 const double sqrt2 = std::sqrt(2.0);
163 const double alpha = (lo - mu) / sigma;
164 const double beta = (hi - mu) / sigma;
165 const double Phi_a = std::isfinite(alpha)
166 ? 0.5 * (1.0 + std::erf(alpha / sqrt2))
167 : (alpha < 0 ? 0.0 : 1.0);
168 const double Phi_b = std::isfinite(beta)
169 ? 0.5 * (1.0 + std::erf(beta / sqrt2))
170 : (beta < 0 ? 0.0 : 1.0);
171 if (!(Phi_a < Phi_b)) return std::nullopt;
172 /* Inverse-CDF transform. Clamp the target probability strictly
173 * inside (0, 1) so @c inv_phi does not diverge near the asymptotes.
174 * The 1e-15 margin is well below the BSM approximation's intrinsic
175 * accuracy floor (~1e-7), so it's a safe sentinel. */
176 static constexpr double EPS = 1e-15;
177 std::uniform_real_distribution<double> U01(0.0, 1.0);
178 std::vector<double> out;
179 out.reserve(n);
180 for (unsigned i = 0; i < n; ++i) {
181 double u = Phi_a + U01(rng) * (Phi_b - Phi_a);
182 if (u < EPS) u = EPS;
183 if (u > 1.0 - EPS) u = 1.0 - EPS;
184 const double z = inv_phi(u);
185 out.push_back(mu + sigma * z);
186 }
187 return out;
188 }
189 std::unique_ptr<Distribution> affine(double a, double b) const override {
190 if (a == 0.0) return nullptr;
191 double mu = a * p1_;
192 if (b != 0.0) mu += b;
193 return std::make_unique<NormalDistribution>(mu, std::fabs(a) * p2_);
194 }
195 std::string serialise() const override {
196 return "normal:" + double_to_text(p1_) + "," + double_to_text(p2_);
197 }
198 std::optional<double> asDirac() const override {
199 if (p2_ == 0.0) return p1_;
200 return std::nullopt;
201 }
202};
203
204const DistributionFamily normal_family = {
205 "normal", 2, "N", {"μ", "σ"},
206 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
207 return std::make_unique<NormalDistribution>(p1, p2);
208 }};
209
210const DistributionFamily &NormalDistribution::family() const
211{
212 return normal_family;
213}
214
215/* (Normal, +, Normal): every term a·Zᵢ + bᵢ over independent normals
216 * sums to Normal(Σ(aᵢμᵢ + bᵢ), √(Σ aᵢ²σᵢ²)). A degenerate total
217 * variance would make the closure a Dirac at the total mean; decline and
218 * leave that to other passes (in practice unreachable: σ=0 normals are
219 * constructed as gate_value by provsql.normal, so total_var > 0 whenever
220 * the closure fires). */
221std::unique_ptr<Distribution>
222normalSumRule(const std::vector<ClosureTerm> &terms)
223{
224 double total_mean = 0.0;
225 double total_var = 0.0;
226 for (const auto &t : terms) {
227 total_mean += t.b;
228 if (!t.dist) continue;
229 const double mu = t.dist->p1();
230 const double sigma = t.dist->p2();
231 total_mean += t.a * mu;
232 total_var += t.a * t.a * sigma * sigma;
233 }
234 if (total_var <= 0.0) return nullptr;
235 return std::make_unique<NormalDistribution>(total_mean,
236 std::sqrt(total_var));
237}
238
239[[maybe_unused]] const ClosureRuleRegistrar normal_sum_rule(
240 "normal", "normal", &normalSumRule);
241
242/* P(X < Y) for X, Y independent normals. Reduces to P(X - Y < 0) with
243 * X - Y ~ N(μ_X - μ_Y, √(σ_X² + σ_Y²)). */
244double normalPairLess(const Distribution &X, const Distribution &Y)
245{
246 return NormalDistribution(
247 X.p1() - Y.p1(),
248 std::sqrt(X.p2() * X.p2() + Y.p2() * Y.p2())).cdf(0.0);
249}
250
251[[maybe_unused]] const ComparatorRuleRegistrar normal_less_rule(
252 "normal", "normal", &normalPairLess);
253
254/* Normal-Normal conjugacy: a Normal(θ, σ) observation with the latent in
255 * the mean slot updates a Normal(μ, σ₀) prior by precision weighting:
256 * τ ← 1/σ₀² + 1/σ², μ ← (μ/σ₀² + d/σ²)/τ. The predictive of the datum
257 * before the update is Normal(μ, √(σ₀² + σ²)) -- the marginal of
258 * d = θ + noise. */
259bool normalMeanConjugateUpdate(double &mu, double &sigma,
260 const DistributionTemplate &lik, double d)
261{
262 const double s = lik.p2.literal;
263 if (!(s > 0.0) || !(sigma > 0.0)) return false;
264 const double tau0 = 1.0 / (sigma * sigma);
265 const double taul = 1.0 / (s * s);
266 mu = (tau0 * mu + taul * d) / (tau0 + taul);
267 sigma = std::sqrt(1.0 / (tau0 + taul));
268 return true;
269}
270
271double normalMeanLogPredictive(double mu, double sigma,
272 const DistributionTemplate &lik, double d)
273{
274 const double s = lik.p2.literal;
275 if (!(s > 0.0) || !(sigma > 0.0)) return kNaN;
276 const double v = sigma * sigma + s * s;
277 return -0.5 * ((d - mu) * (d - mu) / v + std::log(2.0 * M_PI * v));
278}
279
280[[maybe_unused]] const ConjugateRuleRegistrar normal_mean_conjugate(
281 "normal", 0, "normal",
282 {&normalMeanConjugateUpdate, &normalMeanLogPredictive});
283
284[[maybe_unused]] const DistributionFamilyRegistrar normal_family_registrar(
285 normal_family);
286
287} // namespace
288
289} // 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
double binomial_coeff(unsigned n, unsigned k)
C(n, k) as a double (exact for the small moment orders used here).
double inv_phi(double p)
Inverse standard-normal CDF, Beasley-Springer-Moro (1995).
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.