ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
lognormal.cpp
Go to the documentation of this file.
1/**
2 * @file lognormal.cpp
3 * @brief LogNormal(μ, σ) family implementation: @c exp of a
4 * Normal(μ, σ), parameterised by the underlying normal.
5 *
6 * The multiplicative counterpart of Normal: products of independent
7 * lognormals are lognormal (parameters add in log space), positive
8 * scalings shift μ by @c ln @c c, and the @c exp / @c ln transforms
9 * bridge the two families -- all registered here as product / transform
10 * / comparator rules, so the simplifier folds
11 * <tt>exp(normal(...))</tt>, <tt>ln(lognormal(...))</tt>, and lognormal
12 * products with no evaluator changes.
13 *
14 * Self-contained family implementation: the class is file-local and
15 * reaches the evaluators only through the registrars at the bottom
16 * (DistributionRegistry descriptor + pairwise / transform rules).
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 LogNormal(μ=p1, σ=p2 of the underlying normal). */
34class LogNormalDistribution final : public BaseDistribution {
35public:
37 const DistributionFamily &family() const override;
38 double mean() const override {
39 return std::exp(p1_ + 0.5 * p2_ * p2_);
40 }
41 double variance() const override {
42 const double s2 = p2_ * p2_;
43 return std::expm1(s2) * std::exp(2.0 * p1_ + s2);
44 }
45 double rawMoment(unsigned k) const override {
46 if (k == 0) return 1.0;
47 // E[X^k] = exp(kμ + k²σ²/2), finite for every k.
48 const double kd = static_cast<double>(k);
49 return std::exp(kd * p1_ + 0.5 * kd * kd * p2_ * p2_);
50 }
51 double pdf(double c) const override {
52 if (!(p2_ > 0.0)) return kNaN;
53 if (c <= 0.0) return 0.0;
54 const double z = (std::log(c) - p1_) / p2_;
55 return std::exp(-0.5 * z * z)
56 / (c * p2_ * std::sqrt(2.0 * M_PI));
57 }
58 double cdf(double c) const override {
59 if (!(p2_ > 0.0)) return kNaN;
60 if (c <= 0.0) return 0.0;
61 return Phi((std::log(c) - p1_) / p2_);
62 }
63 DistSupport support() const override { return {0.0, kInf}; }
64 bool integrationRange(double &lo, double &hi) const override {
65 if (!(p2_ > 0.0)) return false;
66 /* [0, exp(μ + 8σ)] leaves Φ(-8) ≈ 6e-16 of mass outside. The
67 * multiplicative geometry means a uniform quadrature grid over this
68 * window under-resolves the mass core for large σ (the window is
69 * e^{8σ} times the median); the closed forms registered below keep
70 * the common comparisons off that path. */
71 lo = 0.0;
72 hi = std::exp(p1_ + 8.0 * p2_);
73 return true;
74 }
75 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
76 double lo = trunc_lo, hi = trunc_hi;
77 if (!std::isfinite(lo)) lo = 0.0;
78 if (!std::isfinite(hi)) hi = std::exp(p1_ + 3.0 * p2_); /* 99.87% */
79 return {lo, hi};
80 }
81 double sample(std::mt19937_64 &rng) const override {
82 std::lognormal_distribution<double> d(p1_, p2_);
83 return d(rng);
84 }
85 std::optional<double> quantile(double p) const override {
86 if (!(p2_ > 0.0)) return std::nullopt;
87 /* exp of the normal quantile; same BSM + Newton polish. */
88 double z = inv_phi(p);
89 for (int i = 0; i < 2; ++i) {
90 const double d = phi(z);
91 if (!(d > 0.0)) break;
92 z -= (Phi(z) - p) / d;
93 }
94 return std::exp(p1_ + p2_ * z);
95 }
96 std::optional<double> truncatedRawMoment(double lo, double hi,
97 unsigned k) const override {
98 if (!(p2_ > 0.0)) return std::nullopt;
99 /* In log space the truncation is a normal truncation, and
100 * E[X^k · 1(a<X<b)] integrates to the closed form
101 * exp(kμ + k²σ²/2) · (Φ(β - kσ) - Φ(α - kσ)) with α, β the
102 * standardised log bounds. */
103 if (std::isfinite(hi) && hi <= 0.0) return std::nullopt;
104 const double alpha = (std::isfinite(lo) && lo > 0.0)
105 ? (std::log(lo) - p1_) / p2_ : -kInf;
106 const double beta = std::isfinite(hi)
107 ? (std::log(hi) - p1_) / p2_ : kInf;
108 const double Phi_a = std::isfinite(alpha) ? Phi(alpha) : 0.0;
109 const double Phi_b = std::isfinite(beta) ? Phi(beta) : 1.0;
110 const double mass = Phi_b - Phi_a;
111 if (mass < 1e-12) return std::nullopt;
112 if (k == 0) return 1.0;
113 const double kd = static_cast<double>(k);
114 const double shifted_a =
115 std::isfinite(alpha) ? Phi(alpha - kd * p2_) : 0.0;
116 const double shifted_b =
117 std::isfinite(beta) ? Phi(beta - kd * p2_) : 1.0;
118 return std::exp(kd * p1_ + 0.5 * kd * kd * p2_ * p2_)
119 * (shifted_b - shifted_a) / mass;
120 }
121 std::optional<std::vector<double>> sampleTruncated(
122 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
123 if (!(p2_ > 0.0)) return std::nullopt;
124 /* Truncated normal in log space, exponentiated: the same
125 * inverse-CDF transform as the Normal sampler over the
126 * standardised log bounds. */
127 if (std::isfinite(hi) && hi <= 0.0) return std::nullopt;
128 const double alpha = (std::isfinite(lo) && lo > 0.0)
129 ? (std::log(lo) - p1_) / p2_ : -kInf;
130 const double beta = std::isfinite(hi)
131 ? (std::log(hi) - p1_) / p2_ : kInf;
132 const double Phi_a = std::isfinite(alpha) ? Phi(alpha) : 0.0;
133 const double Phi_b = std::isfinite(beta) ? Phi(beta) : 1.0;
134 if (!(Phi_a < Phi_b)) return std::nullopt;
135 static constexpr double EPS = 1e-15;
136 std::uniform_real_distribution<double> U01(0.0, 1.0);
137 std::vector<double> out;
138 out.reserve(n);
139 for (unsigned i = 0; i < n; ++i) {
140 double u = Phi_a + U01(rng) * (Phi_b - Phi_a);
141 if (u < EPS) u = EPS;
142 if (u > 1.0 - EPS) u = 1.0 - EPS;
143 out.push_back(std::exp(p1_ + p2_ * inv_phi(u)));
144 }
145 return out;
146 }
147 std::unique_ptr<Distribution> affine(double a, double b) const override {
148 /* c · LogNormal(μ, σ) = LogNormal(μ + ln c, σ) for c > 0; a
149 * negative scaling flips the support and an offset shifts it,
150 * neither of which is lognormal. */
151 if (!(a > 0.0) || b != 0.0) return nullptr;
152 return std::make_unique<LogNormalDistribution>(p1_ + std::log(a), p2_);
153 }
154 std::string serialise() const override {
155 return "lognormal:" + double_to_text(p1_) + "," + double_to_text(p2_);
156 }
157 std::optional<double> asDirac() const override {
158 if (p2_ == 0.0) return std::exp(p1_);
159 return std::nullopt;
160 }
161};
162
163const DistributionFamily lognormal_family = {
164 "lognormal", 2, "LogN", {"μ", "σ"},
165 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
166 return std::make_unique<LogNormalDistribution>(p1, p2);
167 }};
168
169const DistributionFamily &LogNormalDistribution::family() const
170{
171 return lognormal_family;
172}
173
174/* P(X < Y) for independent lognormals: monotone in log space, so it is
175 * the Normal comparison of the underlying parameters,
176 * Φ((μ_Y - μ_X) / √(σ_X² + σ_Y²)). */
177double lognormalPairLess(const Distribution &X, const Distribution &Y)
178{
179 const double denom =
180 std::sqrt(X.p2() * X.p2() + Y.p2() * Y.p2());
181 return Phi((Y.p1() - X.p1()) / denom);
182}
183
184[[maybe_unused]] const ComparatorRuleRegistrar lognormal_less_rule(
185 "lognormal", "lognormal", &lognormalPairLess);
186
187/* Independent lognormals multiply to a lognormal: parameters add in log
188 * space (Normal's sum closure seen through the exp bridge). */
189std::unique_ptr<Distribution>
190lognormalProductRule(const std::vector<const Distribution *> &factors)
191{
192 double mu = 0.0, var = 0.0;
193 for (const Distribution *f : factors) {
194 mu += f->p1();
195 var += f->p2() * f->p2();
196 }
197 return std::make_unique<LogNormalDistribution>(mu, std::sqrt(var));
198}
199
200[[maybe_unused]] const ProductRuleRegistrar lognormal_product_rule(
201 "lognormal", "lognormal", &lognormalProductRule);
202
203/* The exp / ln bridges between the two families. exp(Normal) is
204 * constructed directly; ln(LogNormal) resolves Normal's factory through
205 * the registry at rule-execution time (never at static init), so there
206 * is no initialisation-order dependency on normal.cpp. */
207std::unique_ptr<Distribution> expOfNormal(const Distribution &x)
208{
209 return std::make_unique<LogNormalDistribution>(x.p1(), x.p2());
210}
211
212std::unique_ptr<Distribution> lnOfLogNormal(const Distribution &x)
213{
214 const DistributionFamily *normal = lookupDistributionFamily("normal");
215 return normal ? normal->factory(x.p1(), x.p2()) : nullptr;
216}
217
218[[maybe_unused]] const TransformRuleRegistrar exp_normal_rule(
219 "exp", "normal", &expOfNormal);
220[[maybe_unused]] const TransformRuleRegistrar ln_lognormal_rule(
221 "ln", "lognormal", &lnOfLogNormal);
222
223/* LogNormal-Normal conjugacy: a LogNormal(θ, σ) observation with the
224 * latent in the log-scale location slot is a Normal(θ, σ) observation of
225 * ln d, so a Normal(μ, σ₀) prior updates by the same precision weighting.
226 * The predictive is LogNormal(μ, √(σ₀² + σ²)): the Normal predictive at
227 * ln d times the 1/d change-of-variables Jacobian. */
228bool lognormalMuConjugateUpdate(double &mu, double &sigma,
229 const DistributionTemplate &lik, double d)
230{
231 const double s = lik.p2.literal;
232 if (!(s > 0.0) || !(sigma > 0.0) || !(d > 0.0)) return false;
233 const double ld = std::log(d);
234 const double tau0 = 1.0 / (sigma * sigma);
235 const double taul = 1.0 / (s * s);
236 mu = (tau0 * mu + taul * ld) / (tau0 + taul);
237 sigma = std::sqrt(1.0 / (tau0 + taul));
238 return true;
239}
240
241double lognormalMuLogPredictive(double mu, double sigma,
242 const DistributionTemplate &lik, double d)
243{
244 const double s = lik.p2.literal;
245 if (!(s > 0.0) || !(sigma > 0.0) || !(d > 0.0)) return kNaN;
246 const double v = sigma * sigma + s * s;
247 const double ld = std::log(d);
248 return -0.5 * ((ld - mu) * (ld - mu) / v + std::log(2.0 * M_PI * v)) - ld;
249}
250
251[[maybe_unused]] const ConjugateRuleRegistrar lognormal_mu_conjugate(
252 "lognormal", 0, "normal",
253 {&lognormalMuConjugateUpdate, &lognormalMuLogPredictive});
254
255[[maybe_unused]] const DistributionFamilyRegistrar lognormal_family_registrar(
256 lognormal_family);
257
258} // namespace
259
260} // 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
const DistributionFamily * lookupDistributionFamily(const std::string &name)
Look up a family by its on-disk name token.
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.