ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
weibull.cpp
Go to the documentation of this file.
1/**
2 * @file weibull.cpp
3 * @brief Weibull(k, λ) family implementation: shape @c k > 0, scale
4 * @c λ > 0 (the 63.2% quantile -- NOT a rate; @c k = 1 is
5 * Exponential with rate @c 1/λ, and the SQL constructor routes
6 * that case through @c exponential).
7 *
8 * The workhorse of reliability / survival analysis: @c k tunes the
9 * hazard (infant mortality for @c k < 1, wear-out for @c k > 1).
10 * @f$(X/\lambda)^k@f$ is a unit exponential, which gives an exact
11 * quantile, a same-shape comparator closed form, closed-form truncated
12 * moments through the regularised incomplete gamma, and a min-stability
13 * order statistic (the min of @c n i.i.d. Weibulls is Weibull at scale
14 * @f$\lambda n^{-1/k}@f$).
15 *
16 * Self-contained family implementation: the class is file-local and
17 * reaches the evaluators only through the registrars at the bottom.
18 */
19#include "DistributionCommon.h"
20
21#include <algorithm>
22#include <cmath>
23#include <memory>
24#include <optional>
25#include <random>
26#include <string>
27#include <utility>
28#include <vector>
29
30namespace provsql {
31
32namespace {
33
34/** @brief Weibull(k=p1 shape > 0, λ=p2 scale > 0). */
35class WeibullDistribution final : public BaseDistribution {
36public:
38 const DistributionFamily &family() const override;
39 double mean() const override {
40 return p2_ * std::tgamma(1.0 + 1.0 / p1_);
41 }
42 double variance() const override {
43 const double g1 = std::tgamma(1.0 + 1.0 / p1_);
44 return p2_ * p2_ * (std::tgamma(1.0 + 2.0 / p1_) - g1 * g1);
45 }
46 double rawMoment(unsigned k) const override {
47 if (k == 0) return 1.0;
48 // E[X^j] = λ^j Γ(1 + j/k), finite for every j.
49 const double j = static_cast<double>(k);
50 return std::pow(p2_, j) * std::tgamma(1.0 + j / p1_);
51 }
52 double pdf(double c) const override {
53 const double k = p1_, lambda = p2_;
54 if (!(k > 0.0) || !(lambda > 0.0)) return kNaN;
55 if (c < 0.0) return 0.0;
56 if (c == 0.0) {
57 /* Same integrable-singularity convention as Gamma: shape < 1
58 * diverges at 0 and reports NaN so the uniform-grid quadratures
59 * decline to MC rather than integrate a singular endpoint. */
60 if (k < 1.0) return kNaN;
61 return (k == 1.0) ? 1.0 / lambda : 0.0;
62 }
63 const double u = std::pow(c / lambda, k);
64 return (k / lambda) * std::pow(c / lambda, k - 1.0) * std::exp(-u);
65 }
66 double cdf(double c) const override {
67 const double k = p1_, lambda = p2_;
68 if (!(k > 0.0) || !(lambda > 0.0)) return kNaN;
69 if (c <= 0.0) return 0.0;
70 return -std::expm1(-std::pow(c / lambda, k));
71 }
72 DistSupport support() const override { return {0.0, kInf}; }
73 bool integrationRange(double &lo, double &hi) const override {
74 if (!(p1_ > 0.0 && p2_ > 0.0)) return false;
75 /* quantile(1 - e^{-36}): mass beyond is ~2e-16. */
76 lo = 0.0;
77 hi = p2_ * std::pow(36.0, 1.0 / p1_);
78 return true;
79 }
80 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
81 double lo = trunc_lo, hi = trunc_hi;
82 if (!std::isfinite(lo)) lo = 0.0;
83 if (!std::isfinite(hi))
84 hi = p2_ * std::pow(6.907755278982137, 1.0 / p1_); /* q(0.999) */
85 return {lo, hi};
86 }
87 double sample(std::mt19937_64 &rng) const override {
88 std::weibull_distribution<double> d(p1_, p2_);
89 return d(rng);
90 }
91 std::optional<double> quantile(double p) const override {
92 if (!(p1_ > 0.0) || !(p2_ > 0.0)) return std::nullopt;
93 /* Exact inversion: λ·(-ln(1-p))^{1/k}, stable near p = 0 via
94 * log1p. */
95 return p2_ * std::pow(-std::log1p(-p), 1.0 / p1_);
96 }
97 std::optional<double> truncatedRawMoment(double lo, double hi,
98 unsigned j) const override {
99 const double k = p1_, lambda = p2_;
100 if (!(k > 0.0) || !(lambda > 0.0)) return std::nullopt;
101 /* Substituting u = (x/λ)^k turns the truncated moment into an
102 * incomplete-gamma difference:
103 * E[X^j · 1(a<X<b)] = λ^j Γ(1+j/k) (P(1+j/k, u_b) - P(1+j/k, u_a)). */
104 const double u_a = (std::isfinite(lo) && lo > 0.0)
105 ? std::pow(lo / lambda, k) : 0.0;
106 const double u_b = std::isfinite(hi)
107 ? (hi > 0.0 ? std::pow(hi / lambda, k) : 0.0) : kInf;
108 const double mass = std::exp(-u_a) - std::exp(-u_b);
109 if (mass < 1e-12) return std::nullopt;
110 if (j == 0) return 1.0;
111 const double a = 1.0 + static_cast<double>(j) / k;
112 const double P_a = u_a > 0.0 ? gammaP(a, u_a) : 0.0;
113 const double P_b = std::isfinite(u_b) ? gammaP(a, u_b) : 1.0;
114 if (std::isnan(P_a) || std::isnan(P_b)) return std::nullopt;
115 return std::pow(lambda, static_cast<double>(j)) * std::tgamma(a)
116 * (P_b - P_a) / mass;
117 }
118 std::optional<std::vector<double>> sampleTruncated(
119 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
120 const double k = p1_, lambda = p2_;
121 if (!(k > 0.0) || !(lambda > 0.0)) return std::nullopt;
122 std::vector<double> out;
123 out.reserve(n);
124 const double u_lo = (std::isfinite(lo) && lo > 0.0)
125 ? std::pow(lo / lambda, k) : 0.0;
126 if (std::isinf(hi)) {
127 /* One-sided: (X/λ)^k is a unit exponential, which is memoryless,
128 * so u | u > u_lo = u_lo + Exp(1) -- numerically stable for
129 * arbitrarily deep tails. */
130 std::exponential_distribution<double> E(1.0);
131 for (unsigned i = 0; i < n; ++i)
132 out.push_back(lambda * std::pow(u_lo + E(rng), 1.0 / k));
133 return out;
134 }
135 /* Two-sided: exact inverse CDF on [F(lo), F(hi)]. */
136 const double F_lo = -std::expm1(-u_lo);
137 const double F_hi = hi > 0.0
138 ? -std::expm1(-std::pow(hi / lambda, k)) : 0.0;
139 if (!(F_lo < F_hi)) return std::nullopt;
140 std::uniform_real_distribution<double> U01(0.0, 1.0);
141 for (unsigned i = 0; i < n; ++i) {
142 const double u = F_lo + U01(rng) * (F_hi - F_lo);
143 out.push_back(lambda * std::pow(-std::log1p(-u), 1.0 / k));
144 }
145 return out;
146 }
147 std::optional<double> iidOrderStatMean(std::size_t n,
148 bool isMax) const override {
149 if (!(p1_ > 0.0) || !(p2_ > 0.0)) return std::nullopt;
150 if (isMax) return std::nullopt; /* no elementary max closed form */
151 /* Min-stability: min of n i.i.d. Weibull(k, λ) is
152 * Weibull(k, λ n^{-1/k}). */
153 const double scale =
154 p2_ * std::pow(static_cast<double>(n), -1.0 / p1_);
155 return scale * std::tgamma(1.0 + 1.0 / p1_);
156 }
157 std::unique_ptr<Distribution> affine(double a, double b) const override {
158 /* Positive scalings rescale λ; negations / offsets leave the
159 * family. */
160 if (!(a > 0.0) || b != 0.0) return nullptr;
161 return std::make_unique<WeibullDistribution>(p1_, a * p2_);
162 }
163 std::string serialise() const override {
164 return "weibull:" + double_to_text(p1_) + "," + double_to_text(p2_);
165 }
166};
167
168const DistributionFamily weibull_family = {
169 "weibull", 2, "Wbl", {"k", "λ"},
170 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
171 return std::make_unique<WeibullDistribution>(p1, p2);
172 }};
173
174const DistributionFamily &WeibullDistribution::family() const
175{
176 return weibull_family;
177}
178
179/* P(X < Y) for independent same-shape Weibulls: (X/1)^k maps both to
180 * exponentials, so P(X < Y) = λ_Y^k / (λ_X^k + λ_Y^k). Different
181 * shapes have no elementary form; NaN falls to the quadrature. */
182double weibullPairLess(const Distribution &X, const Distribution &Y)
183{
184 if (X.p1() != Y.p1()) return kNaN;
185 const double ax = std::pow(X.p2(), X.p1());
186 const double ay = std::pow(Y.p2(), Y.p1());
187 if (!(ax > 0.0) || !(ay > 0.0)) return kNaN;
188 return ay / (ax + ay);
189}
190
191[[maybe_unused]] const ComparatorRuleRegistrar weibull_less_rule(
192 "weibull", "weibull", &weibullPairLess);
193
194[[maybe_unused]] const DistributionFamilyRegistrar weibull_family_registrar(
195 weibull_family);
196
197} // namespace
198
199} // 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 gammaP(double a, double x)
Regularised lower incomplete gamma for a > 0, x >= 0.
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.