ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
uniform.cpp
Go to the documentation of this file.
1/**
2 * @file uniform.cpp
3 * @brief Uniform on [a, b] 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/**
26 * @brief Raw moments of @c X ~ Uniform(p1, p2) truncated to @c [a, b].
27 *
28 * The intersection @c [a', b'] = [max(p1,a), min(p2,b)] is uniform;
29 * its k-th raw moment is @c (b'^{k+1} - a'^{k+1}) / ((k+1)(b' - a')).
30 */
31double truncated_uniform_raw_moment(double p1, double p2, double a, double b,
32 unsigned k)
33{
34 const double lo = std::max(p1, a);
35 const double hi = std::min(p2, b);
36 if (hi <= lo) return kNaN;
37 if (k == 0) return 1.0;
38 return (std::pow(hi, static_cast<double>(k + 1))
39 - std::pow(lo, static_cast<double>(k + 1)))
40 / ((k + 1) * (hi - lo));
41}
42
43/** @brief Uniform on [a=p1, b=p2]. */
44class UniformDistribution final : public BaseDistribution {
45public:
47 const DistributionFamily &family() const override;
48 double mean() const override { return 0.5 * (p1_ + p2_); }
49 bool meanIsAffine() const override { return true; } // mean = (a+b)/2
50 double variance() const override {
51 const double w = p2_ - p1_;
52 return (w * w) / 12.0;
53 }
54 double rawMoment(unsigned k) const override {
55 if (k == 0) return 1.0;
56 if (k == 1) return mean();
57 const double a = p1_, b = p2_, kp1 = static_cast<double>(k + 1);
58 return (std::pow(b, kp1) - std::pow(a, kp1)) / (kp1 * (b - a));
59 }
60 double pdf(double c) const override {
61 if (!(p2_ > p1_)) return kNaN;
62 return (c < p1_ || c > p2_) ? 0.0 : 1.0 / (p2_ - p1_);
63 }
64 double cdf(double c) const override {
65 if (c <= p1_) return 0.0;
66 if (c >= p2_) return 1.0;
67 return (c - p1_) / (p2_ - p1_);
68 }
69 DistSupport support() const override { return {p1_, p2_}; }
70 bool integrationRange(double &lo, double &hi) const override {
71 lo = p1_;
72 hi = p2_;
73 return hi > lo;
74 }
75 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
76 double lo = trunc_lo, hi = trunc_hi;
77 const double pad = 0.15 * (p2_ - p1_);
78 lo = std::isfinite(lo) ? std::max(lo, p1_ - pad) : p1_ - pad;
79 hi = std::isfinite(hi) ? std::min(hi, p2_ + pad) : p2_ + pad;
80 return {lo, hi};
81 }
82 double sample(std::mt19937_64 &rng) const override {
83 std::uniform_real_distribution<double> d(p1_, p2_);
84 return d(rng);
85 }
86 std::optional<double> quantile(double p) const override {
87 if (!(p2_ > p1_)) return std::nullopt;
88 return p1_ + p * (p2_ - p1_);
89 }
90 std::optional<double> truncatedRawMoment(double lo, double hi,
91 unsigned k) const override {
92 const double r = truncated_uniform_raw_moment(p1_, p2_, lo, hi, k);
93 if (std::isnan(r)) return std::nullopt;
94 return r;
95 }
96 std::optional<std::vector<double>> sampleTruncated(
97 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
98 /* The caller (matchTruncatedSingleRv) already intersected the
99 * event's interval with the RV's natural [a, b] support, so a plain
100 * uniform draw on [lo, hi] is the conditional distribution. */
101 std::uniform_real_distribution<double> U(lo, hi);
102 std::vector<double> out;
103 out.reserve(n);
104 for (unsigned i = 0; i < n; ++i) out.push_back(U(rng));
105 return out;
106 }
107 std::optional<double> iidOrderStatMean(std::size_t n,
108 bool isMax) const override {
109 const double a = p1_, b = p2_;
110 const double frac = isMax ? static_cast<double>(n) / (n + 1)
111 : 1.0 / (n + 1);
112 return a + (b - a) * frac;
113 }
114 std::unique_ptr<Distribution> affine(double a, double b) const override {
115 if (a == 0.0) return nullptr;
116 /* A negative coefficient flips the bounds. */
117 double lo = (a > 0.0) ? a * p1_ : a * p2_;
118 double hi = (a > 0.0) ? a * p2_ : a * p1_;
119 if (b != 0.0) { lo += b; hi += b; }
120 return std::make_unique<UniformDistribution>(lo, hi);
121 }
122 std::string serialise() const override {
123 return "uniform:" + double_to_text(p1_) + "," + double_to_text(p2_);
124 }
125};
126
127const DistributionFamily uniform_family = {
128 "uniform", 2, "U", {"a", "b"},
129 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
130 return std::make_unique<UniformDistribution>(p1, p2);
131 }};
132
133const DistributionFamily &UniformDistribution::family() const
134{
135 return uniform_family;
136}
137
138/* (Uniform, +, Uniform): NOT closed for two distinct uniforms (the sum
139 * has a triangular / trapezoidal density), which the rule expresses by
140 * declining a second Uniform term. A single (possibly scaled / negated)
141 * Uniform plus any number of constants is just the affine transform
142 * a·U + Σb, with a negative coefficient flipping the bounds. */
143std::unique_ptr<Distribution>
144uniformSumRule(const std::vector<ClosureTerm> &terms)
145{
146 const ClosureTerm *uniform = nullptr;
147 for (const auto &t : terms) {
148 if (!t.dist) continue;
149 if (uniform) return nullptr; /* second Uniform term */
150 uniform = &t;
151 }
152 /* Dispatch guarantees at least one RV term. Every wire's additive
153 * offset folds into the global one: (a·U + b_term) + offsets =
154 * a·U + (b_term + offsets). */
155 double b_total = 0.0;
156 for (const auto &t : terms) b_total += t.b;
157 const double a = uniform->a;
158 const double p1 = uniform->dist->p1();
159 const double p2 = uniform->dist->p2();
160 const double new_lo = (a > 0.0) ? a * p1 + b_total : a * p2 + b_total;
161 const double new_hi = (a > 0.0) ? a * p2 + b_total : a * p1 + b_total;
162 return std::make_unique<UniformDistribution>(new_lo, new_hi);
163}
164
165[[maybe_unused]] const ClosureRuleRegistrar uniform_sum_rule(
166 "uniform", "uniform", &uniformSumRule);
167
168/* ∫_c^d F_X(y) dy for X ~ Uniform(a, b) (b > a), i.e. the integral of the
169 * clamped ramp clamp((y-a)/(b-a), 0, 1) over [c, d]. Used by the
170 * Uniform-Uniform comparison closed form P(X < Y) = E_Y[F_X(Y)]. */
171double integralUniformCdf(double a, double b, double c, double d)
172{
173 double total = 0.0;
174 /* y in [a, b]: integrand (y-a)/(b-a) */
175 const double lo = std::max(c, a), hi = std::min(d, b);
176 if (hi > lo)
177 total += ((hi - a) * (hi - a) - (lo - a) * (lo - a)) / (2.0 * (b - a));
178 /* y >= b: integrand 1 */
179 const double lo2 = std::max(c, b);
180 if (d > lo2)
181 total += d - lo2;
182 /* y <= a: integrand 0 (contributes nothing) */
183 return total;
184}
185
186/* P(X < Y) = E_Y[F_X(Y)] = (1/(d-c)) ∫_c^d F_X(y) dy, geometric. */
187double uniformPairLess(const Distribution &X, const Distribution &Y)
188{
189 const double a = X.p1(), b = X.p2(), c = Y.p1(), d = Y.p2();
190 if (!(b > a && d > c)) return kNaN;
191 return integralUniformCdf(a, b, c, d) / (d - c);
192}
193
194[[maybe_unused]] const ComparatorRuleRegistrar uniform_less_rule(
195 "uniform", "uniform", &uniformPairLess);
196
197/* Pareto-Uniform conjugacy: a Uniform(0, θ) observation with the latent
198 * in the UPPER-bound slot updates a Pareto(xₘ, α) prior on θ to
199 * Pareto(max(xₘ, d), α+1). Exact only for a zero lower bound: the
200 * likelihood kernel 1/(θ − a₀)·1{θ ≥ d} is Pareto-shaped in θ itself
201 * only when a₀ = 0, so a nonzero literal lower bound declines to
202 * importance sampling. The predictive is
203 * m(d) = α xₘ^α / ((α+1) max(xₘ, d)^{α+1}). */
204bool uniformUpperConjugateUpdate(double &xm, double &alpha,
205 const DistributionTemplate &lik, double d)
206{
207 if (lik.p1.literal != 0.0) return false;
208 if (!(xm > 0.0) || !(alpha > 0.0) || !(d >= 0.0)) return false;
209 if (d > xm) xm = d;
210 alpha += 1.0;
211 return true;
212}
213
214double uniformUpperLogPredictive(double xm, double alpha,
215 const DistributionTemplate &lik, double d)
216{
217 if (lik.p1.literal != 0.0) return kNaN;
218 if (!(xm > 0.0) || !(alpha > 0.0) || !(d >= 0.0)) return kNaN;
219 const double m = std::max(xm, d);
220 return std::log(alpha) + alpha * std::log(xm)
221 - std::log(alpha + 1.0) - (alpha + 1.0) * std::log(m);
222}
223
224[[maybe_unused]] const ConjugateRuleRegistrar uniform_upper_conjugate(
225 "uniform", 1, "pareto",
226 {&uniformUpperConjugateUpdate, &uniformUpperLogPredictive});
227
228[[maybe_unused]] const DistributionFamilyRegistrar uniform_family_registrar(
229 uniform_family);
230
231} // namespace
232
233} // 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)
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.