ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
logistic.cpp
Go to the documentation of this file.
1/**
2 * @file logistic.cpp
3 * @brief Logistic(μ, s) family implementation.
4 *
5 * The location-scale family whose CDF is the logistic sigmoid
6 * @f$F(x) = \sigma((x-\mu)/s)@f$ and whose quantile is the logit. This is
7 * the natural noise for a latent-utility / logit-link selection model: a
8 * threshold event @c eps @c < @c score with @c eps @c ~ @c Logistic(0,1)
9 * has probability @f$\sigma(\text{score})@f$ exactly (a Normal @c eps would
10 * give the probit @f$\Phi@f$, a different link off by a ~1.6 scale factor).
11 *
12 * Self-contained: the class is file-local and reaches the evaluators only
13 * through the family registrar at the bottom (no comparator / closure rule
14 * -- a difference of logistics is not logistic, so those fall through to the
15 * family-agnostic quadrature / Monte Carlo).
16 */
17#include "DistributionCommon.h"
18
19#include <cmath>
20#include <memory>
21#include <optional>
22#include <random>
23#include <string>
24#include <utility>
25#include <vector>
26
27namespace provsql {
28
29namespace {
30
31/// E[Z^k] for Z ~ standard Logistic(0, 1): 0 for odd k (symmetric), and for
32/// even k = 2n the value (2n)!·[t^{2n}] (πt/sin πt). Tabulated up to k = 6
33/// (covers mean / variance / skewness / kurtosis and a margin); kNaN beyond,
34/// so a higher raw moment declines to Monte Carlo per the family contract.
35double standard_logistic_moment(unsigned k)
36{
37 if (k % 2 == 1) return 0.0;
38 const double pi2 = M_PI * M_PI;
39 switch (k) {
40 case 0: return 1.0;
41 case 2: return pi2 / 3.0; // π²/3
42 case 4: return 7.0 * pi2 * pi2 / 15.0; // 7π⁴/15
43 case 6: return 31.0 * pi2 * pi2 * pi2 / 21.0; // 31π⁶/21
44 default: return kNaN;
45 }
46}
47
48/// Logistic sigmoid σ(z) = 1/(1+e^{-z}), evaluated without overflow.
49double sigmoid(double z)
50{
51 if (z >= 0.0) return 1.0 / (1.0 + std::exp(-z));
52 const double e = std::exp(z);
53 return e / (1.0 + e);
54}
55
56/** @brief Logistic(μ=p1, s=p2). */
57class LogisticDistribution final : public BaseDistribution {
58public:
60 const DistributionFamily &family() const override;
61 double mean() const override { return p1_; }
62 bool meanIsAffine() const override { return true; } // mean = μ
63 double variance() const override {
64 return M_PI * M_PI * p2_ * p2_ / 3.0;
65 }
66 double rawMoment(unsigned k) const override {
67 if (k == 0) return 1.0;
68 if (k == 1) return mean();
69 // E[X^k] = Σ_{i=0}^{k} C(k,i) μ^{k-i} s^i E[Z^i], Z standard logistic.
70 double total = 0.0;
71 for (unsigned i = 0; i <= k; ++i) {
72 const double ez = standard_logistic_moment(i);
73 if (std::isnan(ez)) return kNaN; // beyond the table: decline to MC
74 if (ez == 0.0) continue;
75 total += binomial_coeff(k, i)
76 * std::pow(p1_, static_cast<double>(k - i))
77 * std::pow(p2_, static_cast<double>(i))
78 * ez;
79 }
80 return total;
81 }
82 double pdf(double c) const override {
83 if (!(p2_ > 0.0)) return kNaN;
84 // f(x) = σ(z)(1-σ(z))/s = e^{-|z|} / (s (1+e^{-|z|})²), z = (x-μ)/s.
85 const double z = (c - p1_) / p2_;
86 const double e = std::exp(-std::fabs(z));
87 return e / ((1.0 + e) * (1.0 + e) * p2_);
88 }
89 double cdf(double c) const override {
90 if (!(p2_ > 0.0)) return kNaN;
91 return sigmoid((c - p1_) / p2_);
92 }
93 DistSupport support() const override { return {-kInf, kInf}; }
94 bool integrationRange(double &lo, double &hi) const override {
95 if (!(p2_ > 0.0)) return false;
96 // The logistic tail F(μ-ks) = σ(-k) ≈ e^{-k}; ±30 s leaves < 1e-13.
97 lo = p1_ - 30.0 * p2_;
98 hi = p1_ + 30.0 * p2_;
99 return true;
100 }
101 std::pair<double, double> plotRange(double trunc_lo,
102 double trunc_hi) const override {
103 double lo = trunc_lo, hi = trunc_hi;
104 if (!std::isfinite(lo)) lo = p1_ - 6.0 * p2_;
105 if (!std::isfinite(hi)) hi = p1_ + 6.0 * p2_;
106 return {lo, hi};
107 }
108 double sample(std::mt19937_64 &rng) const override {
109 std::uniform_real_distribution<double> U01(0.0, 1.0);
110 double u = U01(rng);
111 static constexpr double EPS = 1e-15;
112 if (u < EPS) u = EPS;
113 if (u > 1.0 - EPS) u = 1.0 - EPS;
114 return p1_ + p2_ * std::log(u / (1.0 - u)); // μ + s·logit(u)
115 }
116 std::optional<double> quantile(double p) const override {
117 if (!(p2_ > 0.0)) return std::nullopt;
118 return p1_ + p2_ * std::log(p / (1.0 - p)); // μ + s·logit(p)
119 }
120 std::optional<std::vector<double>> sampleTruncated(
121 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
122 if (!(p2_ > 0.0)) return std::nullopt;
123 const double Fa = std::isfinite(lo) ? sigmoid((lo - p1_) / p2_) : 0.0;
124 const double Fb = std::isfinite(hi) ? sigmoid((hi - p1_) / p2_) : 1.0;
125 if (!(Fa < Fb)) return std::nullopt;
126 static constexpr double EPS = 1e-15;
127 std::uniform_real_distribution<double> U01(0.0, 1.0);
128 std::vector<double> out;
129 out.reserve(n);
130 for (unsigned i = 0; i < n; ++i) {
131 double u = Fa + U01(rng) * (Fb - Fa);
132 if (u < EPS) u = EPS;
133 if (u > 1.0 - EPS) u = 1.0 - EPS;
134 out.push_back(p1_ + p2_ * std::log(u / (1.0 - u)));
135 }
136 return out;
137 }
138 std::unique_ptr<Distribution> affine(double a, double b) const override {
139 if (a == 0.0) return nullptr;
140 double mu = a * p1_;
141 if (b != 0.0) mu += b;
142 return std::make_unique<LogisticDistribution>(mu, std::fabs(a) * p2_);
143 }
144 std::string serialise() const override {
145 return "logistic:" + double_to_text(p1_) + "," + double_to_text(p2_);
146 }
147 std::optional<double> asDirac() const override {
148 if (p2_ == 0.0) return p1_;
149 return std::nullopt;
150 }
151};
152
153const DistributionFamily logistic_family = {
154 "logistic", 2, "Lgs", {"μ", "s"},
155 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
156 return std::make_unique<LogisticDistribution>(p1, p2);
157 }};
158
159const DistributionFamily &LogisticDistribution::family() const
160{
161 return logistic_family;
162}
163
164[[maybe_unused]] const DistributionFamilyRegistrar logistic_family_registrar(
165 logistic_family);
166
167} // namespace
168
169} // 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 binomial_coeff(unsigned n, unsigned k)
C(n, k) as a double (exact for the small moment orders used here).
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.