ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
RvCovariance.cpp
Go to the documentation of this file.
1/**
2 * @file RvCovariance.cpp
3 * @brief Single-pass covariance / correlation readouts over RV circuits.
4 *
5 * Backs the SQL @c covariance(x, y[, prov]) and @c correlation(x, y[, prov])
6 * readouts. The definitional decomposition
7 * @f$Cov(X, Y) = E[XY] - E[X]\,E[Y]@f$ is kept for the exact cases (every
8 * factor decomposes analytically, so the subtraction is closed-form), but
9 * the Monte Carlo fallback deliberately does NOT go through it: estimating
10 * @c E[XY] and the two means from three independent sampling passes and
11 * subtracting puts the estimator's noise on the scale of
12 * @f$E[X]\,E[Y]@f$ -- the product of the means -- rather than of the
13 * covariance itself, a catastrophic cancellation when the means dominate
14 * the coupling. Instead a single coupled pass draws @f$(x_i, y_i)@f$ pairs
15 * from the joint circuit (shared stochastic leaves produce one draw per
16 * iteration that both roots observe) and returns the plain sample
17 * covariance @f$\tfrac1n \sum (x_i - \bar x)(y_i - \bar y)@f$, whose noise
18 * scales with @f$\sqrt{(\sigma_X^2 \sigma_Y^2 + Cov^2)/n}@f$.
19 *
20 * @c correlation reads @f$Cov@f$, @f$\sigma_X@f$ and @f$\sigma_Y@f$ off the
21 * SAME pass, instead of stacking five independent MC estimates.
22 */
23#include "CircuitFromMMap.h"
24#include "Expectation.h"
25#include "MonteCarloSampler.h"
26#include "provsql_utils_cpp.h"
27
28extern "C" {
29#include "postgres.h"
30#include "fmgr.h"
31#include "utils/uuid.h"
32#include "provsql_utils.h"
33#include "provsql_error.h"
34
35PG_FUNCTION_INFO_V1(rv_covariance);
36PG_FUNCTION_INFO_V1(rv_correlation);
37}
38
39#include <cmath>
40#include <limits>
41#include <optional>
42#include <set>
43#include <string>
44#include <vector>
45
46namespace provsql {
47
48namespace {
49
50/* Stochastic leaves reachable from g, as in InformationTheory.cpp's
51 * structural-independence shortcut: gate_value constants are shared by
52 * v5-keying and carry no randomness, so they are not collected. */
53void collectStochasticLeaves(const GenericCircuit &gc, gate_t g,
54 std::set<gate_t> &out, std::set<gate_t> &seen)
55{
56 if (!seen.insert(g).second) return;
57 const auto t = gc.getGateType(g);
58 if (t == gate_rv || t == gate_input || t == gate_mulinput)
59 out.insert(g);
60 for (gate_t c : gc.getWires(g))
61 collectStochasticLeaves(gc, c, out, seen);
62}
63
64std::set<gate_t> stochasticLeaves(const GenericCircuit &gc, gate_t g)
65{
66 std::set<gate_t> out, seen;
67 collectStochasticLeaves(gc, g, out, seen);
68 return out;
69}
70
71bool disjointFrom(const std::set<gate_t> &a, const std::set<gate_t> &b)
72{
73 for (gate_t l : a)
74 if (b.count(l)) return false;
75 return true;
76}
77
78/* Scoped MC-fallback disable for the analytic attempt: with
79 * provsql.rv_mc_samples forced to 0 every evaluator below throws instead
80 * of sampling, so "no CircuitException" == "the result is closed-form". */
81struct McDisabledGuard {
82 int saved;
83 McDisabledGuard() : saved(provsql_rv_mc_samples) { provsql_rv_mc_samples = 0; }
84 ~McDisabledGuard() { provsql_rv_mc_samples = saved; }
85};
86
87/* In-memory product gate over the two roots, for the exact
88 * E[XY] - E[X]E[Y] path. The persistent store is never mutated. */
89gate_t makeProductGate(GenericCircuit &gc, gate_t a, gate_t b)
90{
92 auto &w = gc.getWires(g);
93 w.push_back(a);
94 w.push_back(b);
96 return g;
97}
98
99/* Mirrors Expectation.cpp's mc_samples_or_throw: throw when the GUC
100 * disables the fallback, emit the approximation-transparency NOTICE at
101 * the same verbosity tier otherwise. */
102unsigned mc_samples_or_throw(const std::string &what)
103{
104 const int n = provsql_rv_mc_samples;
105 if (n <= 0) {
106 throw CircuitException(
107 what + " could not be decomposed analytically and "
108 "provsql.rv_mc_samples = 0 disables the Monte Carlo fallback");
109 }
110 if (provsql_verbose >= 5)
112 "%s: no closed form found; estimating by Monte Carlo over %d samples "
113 "(an approximation, not an exact moment) -- set provsql.rv_mc_samples = 0 "
114 "to require an exact result instead", what.c_str(), n);
115 return static_cast<unsigned>(n);
116}
117
118/* Acceptance guard for the conditional pass, mirroring Expectation.cpp's
119 * check_acceptance_or_throw (0-of-N = infeasible event, otherwise a
120 * max(attempted/1000, 5) floor against enormous-variance estimates). */
121void check_acceptance_or_throw(std::size_t accepted, unsigned attempted,
122 const std::string &what)
123{
124 if (accepted == 0) {
125 throw CircuitException(
126 what + ": conditioning event is infeasible (0 of " +
127 std::to_string(attempted) + " Monte Carlo samples satisfied it)");
128 }
129 unsigned floor = attempted / 1000;
130 if (floor < 5) floor = 5;
131 if (accepted < floor) {
132 throw CircuitException(
133 what + ": conditional MC accepted only " + std::to_string(accepted) +
134 " out of " + std::to_string(attempted) +
135 " samples (need >= " + std::to_string(floor) +
136 "); raise provsql.rv_mc_samples or tighten the event.");
137 }
138}
139
140/* Sample covariance and the two sample variances from one coupled pass.
141 * Population (1/n) normalisation, matching the central-moment MC
142 * convention in Expectation.cpp. Pairs with a NaN member (sampling-
143 * undefined worlds, e.g. empty-group aggregates) are excluded; an
144 * all-NaN pass yields NaN throughout. */
145struct CovStats {
146 double cov;
147 double var_x;
148 double var_y;
149};
150
151CovStats mcCovStats(const GenericCircuit &gc, gate_t x, gate_t y,
152 std::optional<gate_t> event, const std::string &what)
153{
154 const unsigned n = mc_samples_or_throw(what);
155
156 std::vector<double> xs, ys;
157 if (event) {
158 auto cs = monteCarloConditionalScalarPairSamples(gc, x, y, *event, n);
159 check_acceptance_or_throw(cs.xs.size(), cs.attempted, what);
160 xs = std::move(cs.xs);
161 ys = std::move(cs.ys);
162 } else {
163 std::tie(xs, ys) = monteCarloScalarPairSamples(gc, x, y, n);
164 }
165
166 double sum_x = 0.0, sum_y = 0.0;
167 std::size_t m = 0;
168 for (std::size_t i = 0; i < xs.size(); ++i) {
169 if (std::isnan(xs[i]) || std::isnan(ys[i])) continue;
170 sum_x += xs[i];
171 sum_y += ys[i];
172 ++m;
173 }
174 if (m == 0) {
175 const double nan = std::numeric_limits<double>::quiet_NaN();
176 return {nan, nan, nan};
177 }
178 const double mean_x = sum_x / static_cast<double>(m);
179 const double mean_y = sum_y / static_cast<double>(m);
180
181 double cov = 0.0, var_x = 0.0, var_y = 0.0;
182 for (std::size_t i = 0; i < xs.size(); ++i) {
183 if (std::isnan(xs[i]) || std::isnan(ys[i])) continue;
184 const double dx = xs[i] - mean_x;
185 const double dy = ys[i] - mean_y;
186 cov += dx * dy;
187 var_x += dx * dx;
188 var_y += dy * dy;
189 }
190 const double dm = static_cast<double>(m);
191 return {cov / dm, var_x / dm, var_y / dm};
192}
193
194/* Exact E[XY] - E[X]E[Y] (and, when @p need_vars, the two variances),
195 * attempted with the MC fallback disabled: std::nullopt means some factor
196 * has no closed form and the caller should take the single coupled pass
197 * instead of stacking independent estimates. */
198std::optional<CovStats> tryAnalyticCovStats(GenericCircuit &gc, gate_t x,
199 gate_t y,
200 std::optional<gate_t> event,
201 bool need_vars)
202{
203 McDisabledGuard guard;
204 try {
205 CovStats s{0.0, 0.0, 0.0};
206 const double ex = compute_expectation(gc, x, event);
207 const double ey = compute_expectation(gc, y, event);
208 const gate_t prod = makeProductGate(gc, x, y);
209 const double exy = compute_expectation(gc, prod, event);
210 s.cov = exy - ex * ey;
211 if (need_vars) {
212 s.var_x = compute_central_moment(gc, x, 2, event);
213 s.var_y = compute_central_moment(gc, y, 2, event);
214 }
215 return s;
216 } catch (const CircuitException &) {
217 return std::nullopt;
218 }
219}
220
221} // namespace
222
223/**
224 * @brief @f$Cov(X, Y)@f$ (or @f$Cov(X, Y \mid E)@f$) over a joint circuit.
225 *
226 * Exact tiers first: a variance readout when the two roots coincide, an
227 * exact @c 0 when the roots' stochastic-leaf footprints are structurally
228 * independent (given the event), and the closed-form
229 * @f$E[XY] - E[X]\,E[Y]@f$ when every factor decomposes analytically.
230 * Otherwise one coupled Monte Carlo pass at the @c provsql.rv_mc_samples
231 * budget.
232 */
234 std::optional<gate_t> event)
235{
236 x = lift_conditioning(gc, x, event);
237 y = lift_conditioning(gc, y, event);
238
239 /* Cov(X, X) = Var(X): route to the central-moment evaluator (closed
240 * form where the variance has one; single-root MC otherwise) instead
241 * of a product gate over a shared root, which never decomposes. */
242 if (x == y)
243 return compute_central_moment(gc, x, 2, event);
244
245 /* Structural independence: X independent of (Y, E) -- or symmetrically
246 * Y of (X, E) -- gives Cov(X, Y | E) = 0 exactly, whatever the
247 * factors' moments look like. */
248 {
249 auto lx = stochasticLeaves(gc, x);
250 auto ly = stochasticLeaves(gc, y);
251 std::set<gate_t> le;
252 if (event) le = stochasticLeaves(gc, *event);
253 const bool x_indep = disjointFrom(lx, ly) && disjointFrom(lx, le);
254 const bool y_indep = disjointFrom(ly, lx) && disjointFrom(ly, le);
255 if (x_indep || y_indep)
256 return 0.0;
257 }
258
259 if (auto s = tryAnalyticCovStats(gc, x, y, event, false))
260 return s->cov;
261
262 return mcCovStats(gc, x, y, event, "covariance").cov;
263}
264
265/**
266 * @brief Pearson @f$\rho(X, Y) = Cov(X, Y) / (\sigma_X \sigma_Y)@f$
267 * (conditioned on @p event when set).
268 *
269 * Same exact tiers as @c computeCovariance; on the Monte Carlo path the
270 * covariance and BOTH standard deviations are read off one coupled pass.
271 * @c std::nullopt when either variance is @c 0 (or the statistics are
272 * undefined): correlation with a degenerate variable has no value, and
273 * the SQL binding maps this to @c NULL.
274 */
275std::optional<double> computeCorrelation(GenericCircuit &gc, gate_t x,
276 gate_t y,
277 std::optional<gate_t> event)
278{
279 x = lift_conditioning(gc, x, event);
280 y = lift_conditioning(gc, y, event);
281
282 if (x == y) {
283 const double var = compute_central_moment(gc, x, 2, event);
284 if (std::isnan(var) || var <= 0.0) return std::nullopt;
285 return 1.0;
286 }
287
288 {
289 auto lx = stochasticLeaves(gc, x);
290 auto ly = stochasticLeaves(gc, y);
291 std::set<gate_t> le;
292 if (event) le = stochasticLeaves(gc, *event);
293 const bool x_indep = disjointFrom(lx, ly) && disjointFrom(lx, le);
294 const bool y_indep = disjointFrom(ly, lx) && disjointFrom(ly, le);
295 if (x_indep || y_indep) {
296 const double var_x = compute_central_moment(gc, x, 2, event);
297 const double var_y = compute_central_moment(gc, y, 2, event);
298 if (std::isnan(var_x) || std::isnan(var_y) ||
299 var_x <= 0.0 || var_y <= 0.0)
300 return std::nullopt;
301 return 0.0;
302 }
303 }
304
305 CovStats s{};
306 if (auto exact = tryAnalyticCovStats(gc, x, y, event, true))
307 s = *exact;
308 else
309 s = mcCovStats(gc, x, y, event, "correlation");
310
311 if (std::isnan(s.var_x) || std::isnan(s.var_y) ||
312 s.var_x <= 0.0 || s.var_y <= 0.0)
313 return std::nullopt;
314 return s.cov / std::sqrt(s.var_x * s.var_y);
315}
316
317} // namespace provsql
318
319/* ------------------------------------------------------------------
320 * SQL-callable entry points, mirroring rv_moment's plumbing.
321 * ------------------------------------------------------------------ */
322
323/**
324 * @brief SQL: rv_covariance(x uuid, y uuid, prov uuid) -> float8
325 */
326Datum rv_covariance(PG_FUNCTION_ARGS)
327{
328 try {
329 pg_uuid_t *x = PG_GETARG_UUID_P(0);
330 pg_uuid_t *y = PG_GETARG_UUID_P(1);
331 pg_uuid_t *prov = PG_GETARG_UUID_P(2);
332
333 std::vector<gate_t> gates;
334 auto gc = getJointCircuit({*x, *y, *prov}, gates);
335
336 /* gate_one event = unconditional after load-time simplification. */
337 std::optional<gate_t> event_opt;
338 if (gc.getGateType(gates[2]) != gate_one)
339 event_opt = gates[2];
340
341 return Float8GetDatum(
342 provsql::computeCovariance(gc, gates[0], gates[1], event_opt));
343 } catch (const std::exception &e) {
344 provsql_error("rv_covariance: %s", e.what());
345 } catch (...) {
346 provsql_error("rv_covariance: unknown exception");
347 }
348 PG_RETURN_NULL();
349}
350
351/**
352 * @brief SQL: rv_correlation(x uuid, y uuid, prov uuid) -> float8
353 *
354 * Returns SQL @c NULL for a degenerate (zero-variance) argument, matching
355 * the @c NULLIF convention of the former SQL-level definition.
356 */
357Datum rv_correlation(PG_FUNCTION_ARGS)
358{
359 try {
360 pg_uuid_t *x = PG_GETARG_UUID_P(0);
361 pg_uuid_t *y = PG_GETARG_UUID_P(1);
362 pg_uuid_t *prov = PG_GETARG_UUID_P(2);
363
364 std::vector<gate_t> gates;
365 auto gc = getJointCircuit({*x, *y, *prov}, gates);
366
367 std::optional<gate_t> event_opt;
368 if (gc.getGateType(gates[2]) != gate_one)
369 event_opt = gates[2];
370
371 auto rho =
372 provsql::computeCorrelation(gc, gates[0], gates[1], event_opt);
373 if (!rho)
374 PG_RETURN_NULL();
375 return Float8GetDatum(*rho);
376 } catch (const std::exception &e) {
377 provsql_error("rv_correlation: %s", e.what());
378 } catch (...) {
379 provsql_error("rv_correlation: unknown exception");
380 }
381 PG_RETURN_NULL();
382}
GenericCircuit getJointCircuit(const std::vector< pg_uuid_t > &tokens, std::vector< gate_t > &gates)
Multi-root variant of getJointCircuit.
Build in-memory circuits from the mmap-backed persistent store.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Analytical expectation / variance / moment evaluator over RV circuits.
Monte Carlo sampling over a GenericCircuit, RV-aware.
Datum rv_covariance(PG_FUNCTION_ARGS)
SQL: rv_covariance(x uuid, y uuid, prov uuid) -> float8.
Datum rv_correlation(PG_FUNCTION_ARGS)
SQL: rv_correlation(x uuid, y uuid, prov uuid) -> float8.
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:140
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
In-memory provenance circuit with semiring-generic evaluation.
void setInfos(gate_t g, unsigned info1, unsigned info2)
Set the integer annotation pair for gate g.
gate_t setGate(gate_type type) override
Allocate a new gate with type type and no UUID.
gate_t lift_conditioning(GenericCircuit &gc, gate_t root, std::optional< gate_t > &event_opt)
Lift conditioning out of a scalar arithmetic expression.
std::pair< std::vector< double >, std::vector< double > > monteCarloScalarPairSamples(const GenericCircuit &gc, gate_t root_a, gate_t root_b, unsigned samples)
Coupled per-iteration draws of two scalar roots.
double computeCovariance(GenericCircuit &gc, gate_t x, gate_t y, std::optional< gate_t > event)
(or ) over a joint circuit.
std::optional< double > computeCorrelation(GenericCircuit &gc, gate_t x, gate_t y, std::optional< gate_t > event)
Pearson (conditioned on event when set).
double compute_central_moment(const GenericCircuit &gc, gate_t root, unsigned k, std::optional< gate_t > event_root)
Compute the central moment (or if event_root is set).
ConditionalScalarPairSamples monteCarloConditionalScalarPairSamples(const GenericCircuit &gc, gate_t root_a, gate_t root_b, gate_t event_root, unsigned samples)
Rejection-sample the PAIR (root_a, root_b) conditioned on event_root.
double compute_expectation(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
Compute (or if event_root is set) over the scalar sub-circuit rooted at root.
int provsql_verbose
Verbosity level; controlled by the provsql.verbose_level GUC.
Definition provsql.c:93
int provsql_rv_mc_samples
Default sample count for analytical-evaluator MC fallbacks; 0 disables fallback (callers raise instea...
Definition provsql.c:100
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
#define provsql_notice(fmt,...)
Emit a ProvSQL informational notice (execution continues).
Core types, constants, and utilities shared across ProvSQL.
@ PROVSQL_ARITH_TIMES
n-ary, product of children
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
C++ utility functions for UUID manipulation.
UUID structure.