ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
Expectation.cpp
Go to the documentation of this file.
1/**
2 * @file Expectation.cpp
3 * @brief Implementation of the analytical expectation / variance / moment
4 * evaluator over scalar RV sub-circuits.
5 */
6#include "Expectation.h"
7
8#include "AnalyticEvaluator.h"
9#include "BooleanCircuit.h"
10#include "Circuit.h"
11#include "CircuitFromMMap.h"
12#include "MonteCarloSampler.h"
13#include "RandomVariable.h"
14#include "RangeCheck.h"
15#include "provsql_utils_cpp.h"
16#include "semiring/BoolExpr.h"
17
18extern "C" {
19#include "postgres.h"
20#include "fmgr.h"
21#include "utils/uuid.h"
22#include "provsql_utils.h"
23#include "provsql_error.h"
24
25PG_FUNCTION_INFO_V1(rv_moment);
26}
27
28#include <cmath>
29#include <set>
30#include <string>
31#include <unordered_map>
32#include <vector>
33
34namespace provsql {
35
37{
39 std::unordered_map<gate_t, gate_t> gc_to_bc;
40 for (gate_t u : gc.getInputs()) {
41 gc_to_bc[u] = c.setGate(gc.getUUID(u), BooleanGate::IN, gc.getProb(u));
42 }
43 for (size_t i = 0; i < gc.getNbGates(); ++i) {
44 auto u = static_cast<gate_t>(i);
45 if (gc.getGateType(u) == gate_mulinput) {
46 gc_to_bc[u] = c.setGate(gc.getUUID(u), BooleanGate::MULIN, gc.getProb(u));
47 c.setInfo(gc_to_bc[u], gc.getInfos(u).first);
48 c.addWire(gc_to_bc[u], gc_to_bc[gc.getWires(u)[0]]);
49 }
50 }
52 gate_t bcRoot = gc.evaluate(boolRoot, gc_to_bc, semiring);
53 propagateDNNFCertificate(gc, gc_to_bc, c);
54
55 try {
56 return c.independentEvaluation(bcRoot);
57 } catch (CircuitException &) {
58 if (provsql_rv_mc_samples <= 0) {
59 throw CircuitException(
60 "evaluateBooleanProbability: subcircuit could not be evaluated "
61 "independently and provsql.rv_mc_samples = 0 disables the "
62 "Monte Carlo fallback");
63 }
65 return c.monteCarlo(bcRoot,
66 static_cast<unsigned>(provsql_rv_mc_samples));
67 }
68}
69
70namespace {
71
72using RvSet = std::set<gate_t>;
73
74/// Mixing weight π = P(p = true) for a mixture's Bernoulli wire.
75/// For a bare @c gate_input, the probability is the leaf's pinned
76/// @c set_prob; for any compound Boolean gate, defer to
77/// @c evaluateBooleanProbability.
78double mixturePi(const GenericCircuit &gc, gate_t p)
79{
80 return (gc.getGateType(p) == gate_input)
81 ? gc.getProb(p)
83}
84
85/// Cache of the base-@c gate_rv UUID footprints reachable below each
86/// scalar gate, used as the structural-independence witness. Two
87/// children of an arithmetic gate are independent iff their footprints
88/// are disjoint -- and therefore the variance and TIMES expectation
89/// shortcuts apply.
90class FootprintCache {
91public:
92 explicit FootprintCache(const GenericCircuit &gc) : gc_(gc) {}
93
94 const RvSet &of(gate_t g) {
95 auto it = cache_.find(g);
96 if (it != cache_.end()) return it->second;
97 RvSet s;
98 auto type = gc_.getGateType(g);
99 if (type == gate_rv) {
100 s.insert(g);
101 } else if (type == gate_value) {
102 // empty -- no RV reached
103 } else if (type == gate_arith) {
104 for (gate_t c : gc_.getWires(g)) {
105 const auto &cs = of(c);
106 s.insert(cs.begin(), cs.end());
107 }
108 } else if (type == gate_mixture) {
109 const auto &wires = gc_.getWires(g);
110 if (gc_.isCategoricalMixture(g)) {
111 // Categorical-form mixture. Footprint = union of the
112 // mulinputs' footprints (each contributes {self, key} via the
113 // mulinput branch below), so two categoricals sharing a key
114 // overlap on it and are correctly flagged dependent.
115 for (std::size_t i = 1; i < wires.size(); ++i) {
116 const auto &fm = of(wires[i]);
117 s.insert(fm.begin(), fm.end());
118 }
119 } else if (wires.size() == 3) {
120 // Classic 3-wire mixture. Footprint = footprint(p) ∪
121 // footprint(x) ∪ footprint(y). The Boolean wire is included
122 // as a discrete random source so two mixtures whose p's share
123 // an atom are correctly recognised as dependent (their branch
124 // selection is correlated), bypassing the closed-form
125 // independence shortcut. Recursing into wires[0] (rather than
126 // inserting its gate_t directly) generalises that recognition
127 // from bare-input Bernoullis to compound Boolean wires.
128 const auto &fp = of(wires[0]);
129 s.insert(fp.begin(), fp.end());
130 const auto &fx = of(wires[1]);
131 s.insert(fx.begin(), fx.end());
132 const auto &fy = of(wires[2]);
133 s.insert(fy.begin(), fy.end());
134 }
135 } else if (type == gate_mulinput) {
136 // A mulinput is a state-carrying atom (so its own gate_t is in
137 // its footprint -- two mulinputs of the same group are distinct
138 // atoms even though they share a key) *and* references a shared
139 // key gate at wires[0] (whose footprint is added so the shared
140 // key makes the two mulinputs overlap on it, flagging them as
141 // dependent for pairwise_disjoint).
142 s.insert(g);
143 const auto &wires = gc_.getWires(g);
144 if (!wires.empty()) {
145 const auto &fk = of(wires[0]);
146 s.insert(fk.begin(), fk.end());
147 }
148 } else if (type == gate_input) {
149 // Atomic Boolean leaf. Use the gate's own UUID as its footprint
150 // so two Boolean expressions sharing this input collide on it.
151 s.insert(g);
152 } else if (type == gate_plus || type == gate_times || type == gate_monus
153 || type == gate_project || type == gate_eq
154 || type == gate_cmp || type == gate_update
155 || type == gate_annotation || type == gate_conditioned) {
156 // Boolean gates: footprint is the union of children's footprints.
157 // Lets a compound `p` wire feeding a mixture propagate its atom
158 // dependencies to FootprintCache for the disjoint-children
159 // shortcuts in rec_expectation / rec_variance / rec_raw_moment.
160 for (gate_t c : gc_.getWires(g)) {
161 const auto &cs = of(c);
162 s.insert(cs.begin(), cs.end());
163 }
164 } else if (type == gate_zero || type == gate_one) {
165 // Empty footprint -- a constant-true / constant-false Boolean
166 // contributes no shared atoms.
167 } else {
168 // Unknown scalar gate type: return an empty footprint. Callers
169 // will trip the analytical-decomposition switch and route the
170 // gate to the MC fallback (or raise if the fallback is disabled),
171 // which is the right behaviour for any unanticipated leaf shape.
172 }
173 return cache_.emplace(g, std::move(s)).first->second;
174 }
175
176private:
177 const GenericCircuit &gc_;
178 std::unordered_map<gate_t, RvSet> cache_;
179};
180
181bool pairwise_disjoint(FootprintCache &fp, const std::vector<gate_t> &children)
182{
183 RvSet seen;
184 for (gate_t c : children) {
185 const auto &fpc = fp.of(c);
186 for (gate_t r : fpc) {
187 if (!seen.insert(r).second) return false;
188 }
189 }
190 return true;
191}
192
193unsigned mc_samples_or_throw(const std::string &what)
194{
195 const int n = provsql_rv_mc_samples;
196 if (n <= 0) {
197 throw CircuitException(
198 what + " could not be decomposed analytically and "
199 "provsql.rv_mc_samples = 0 disables the Monte Carlo fallback");
200 }
201 // Transparency: the analytic moment surface is about to return a Monte Carlo
202 // ESTIMATE, not a closed-form moment. Signal it (at the same verbose>=5
203 // evaluation tier as the probability-side approximation NOTICEs, so Studio
204 // and verbose users can tell an estimate from an exact value) -- the
205 // continuous-RV surface is approximate by nature, but never *silently* so.
206 // Set provsql.rv_mc_samples = 0 to require an exact result instead.
207 if (provsql_verbose >= 5)
209 "%s: no closed form found; estimating by Monte Carlo over %d samples "
210 "(an approximation, not an exact moment) -- set provsql.rv_mc_samples = 0 "
211 "to require an exact result instead", what.c_str(), n);
212 return static_cast<unsigned>(n);
213}
214
215double mc_raw_moment(const GenericCircuit &gc, gate_t g, unsigned k,
216 const std::string &what)
217{
218 auto samples = monteCarloScalarSamples(gc, g, mc_samples_or_throw(what));
219 if (samples.empty()) return 0.0;
220 // NaN samples come from sampling-undefined worlds, e.g. an
221 // agg(SUM/AVG/MIN/MAX) over an empty group (SQL NULL). Treat them
222 // as missing observations of the moment rather than poisoning the
223 // mean; only return NaN if every sample was undefined.
224 double total = 0.0;
225 std::size_t finite_count = 0;
226 for (double x : samples) {
227 if (std::isnan(x)) continue;
228 total += std::pow(x, static_cast<double>(k));
229 ++finite_count;
230 }
231 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
232 return total / static_cast<double>(finite_count);
233}
234
235double mc_central_moment(const GenericCircuit &gc, gate_t g, unsigned k,
236 double mu, const std::string &what)
237{
238 auto samples = monteCarloScalarSamples(gc, g, mc_samples_or_throw(what));
239 if (samples.empty()) return 0.0;
240 double total = 0.0;
241 std::size_t finite_count = 0;
242 for (double x : samples) {
243 if (std::isnan(x)) continue;
244 const double d = x - mu;
245 total += std::pow(d, static_cast<double>(k));
246 ++finite_count;
247 }
248 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
249 return total / static_cast<double>(finite_count);
250}
251
252/// Minimum accepted-sample count for conditional MC moments. Below
253/// this floor we'd be reporting a moment from a handful of accepted
254/// draws and the variance of the estimator would be enormous; raise
255/// rather than silently return a noisy number.
256unsigned min_accepted_floor(unsigned attempted)
257{
258 unsigned floor = attempted / 1000;
259 return floor < 5 ? 5 : floor;
260}
261
262void check_acceptance_or_throw(const ConditionalScalarSamples &cs,
263 const std::string &what)
264{
265 if (cs.accepted.empty()) {
266 /* 0-of-N accepted is the unmistakable signature of an infeasible
267 * conditioning event: raising rv_mc_samples cannot help (the
268 * acceptance probability is exactly 0). Surface that directly
269 * rather than the generic "raise samples or check satisfiability"
270 * advice that applies to merely under-sampled events. */
271 throw CircuitException(
272 what + ": conditioning event is infeasible (0 of " +
273 std::to_string(cs.attempted) +
274 " Monte Carlo samples satisfied it)");
275 }
276 const unsigned floor = min_accepted_floor(cs.attempted);
277 if (cs.accepted.size() < floor) {
278 throw CircuitException(
279 what + ": conditional MC accepted only " +
280 std::to_string(cs.accepted.size()) + " out of " +
281 std::to_string(cs.attempted) +
282 " samples (need >= " + std::to_string(floor) +
283 "); raise provsql.rv_mc_samples or tighten the event.");
284 }
285}
286
287double mc_conditional_raw_moment(const GenericCircuit &gc, gate_t g,
288 unsigned k, gate_t event_root,
289 const std::string &what)
290{
292 gc, g, event_root, mc_samples_or_throw(what));
293 check_acceptance_or_throw(cs, what);
294 // Mirror the unconditional path: NaN observations (sampling-
295 // undefined worlds, typically empty-group SQL NULLs from
296 // gate_agg) are excluded from the mean.
297 double total = 0.0;
298 std::size_t finite_count = 0;
299 for (double x : cs.accepted) {
300 if (std::isnan(x)) continue;
301 total += std::pow(x, static_cast<double>(k));
302 ++finite_count;
303 }
304 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
305 return total / static_cast<double>(finite_count);
306}
307
308double mc_conditional_central_moment(const GenericCircuit &gc, gate_t g,
309 unsigned k, double mu,
310 gate_t event_root,
311 const std::string &what)
312{
314 gc, g, event_root, mc_samples_or_throw(what));
315 check_acceptance_or_throw(cs, what);
316 double total = 0.0;
317 std::size_t finite_count = 0;
318 for (double x : cs.accepted) {
319 if (std::isnan(x)) continue;
320 const double d = x - mu;
321 total += std::pow(d, static_cast<double>(k));
322 ++finite_count;
323 }
324 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
325 return total / static_cast<double>(finite_count);
326}
327
328double binomial(unsigned n, unsigned k)
329{
330 if (k > n) return 0.0;
331 if (k > n - k) k = n - k;
332 double r = 1.0;
333 for (unsigned i = 1; i <= k; ++i) {
334 r *= static_cast<double>(n - i + 1);
335 r /= static_cast<double>(i);
336 }
337 return r;
338}
339
340double rec_expectation(const GenericCircuit &gc, gate_t g, FootprintCache &fp);
341double rec_variance(const GenericCircuit &gc, gate_t g, FootprintCache &fp);
342double rec_raw_moment(const GenericCircuit &gc, gate_t g, unsigned k,
343 FootprintCache &fp);
344
345/// Standard normal pdf φ(z) = exp(-z²/2)/√(2π).
346double phi(double z)
347{
348 static const double INV_SQRT_2PI = 1.0 / std::sqrt(2.0 * M_PI);
349 return INV_SQRT_2PI * std::exp(-0.5 * z * z);
350}
351
352/// Standard normal CDF Φ(z) = ½(1 + erf(z/√2)). Mirrors the
353/// AnalyticEvaluator::cdfAt Normal branch so the truncation formulas
354/// here use the same numerical convention.
355double Phi(double z)
356{
357 static const double SQRT2 = std::sqrt(2.0);
358 return 0.5 * (1.0 + std::erf(z / SQRT2));
359}
360
361/**
362 * @brief Raw moments of @c X ~ Normal(μ, σ) truncated to @c [a, b].
363 *
364 * Closed form via the integration-by-parts recurrence on the
365 * standardised variable Z = (X - μ)/σ:
366 * E[Z^{k}|α<Z<β] = (k-1) E[Z^{k-2}|α<Z<β]
367 * + (α^{k-1}φ(α) − β^{k-1}φ(β)) / (Φ(β) − Φ(α))
368 * with E[Z^0|…] = 1 and E[Z^1|…] = (φ(α) − φ(β)) / (Φ(β) − Φ(α))
369 * (Greene, "Econometric Analysis", 5e, App. F). Then expand
370 * E[X^k] = E[(μ + σZ)^k] binomially.
371 *
372 * @c α = -∞ corresponds to @p a = -INFINITY (semi-infinite left tail);
373 * @c β = +∞ to @p b = +INFINITY. Returns @c NaN if @c P(α<Z<β) is
374 * below a numerical floor (so the caller falls through to MC).
375 */
376double truncated_normal_raw_moment(double mu, double sigma, double a, double b,
377 unsigned k)
378{
379 const double alpha = std::isfinite(a) ? (a - mu) / sigma
380 : -std::numeric_limits<double>::infinity();
381 const double beta = std::isfinite(b) ? (b - mu) / sigma
382 : +std::numeric_limits<double>::infinity();
383 const double Phi_alpha = std::isfinite(alpha) ? Phi(alpha) : 0.0;
384 const double Phi_beta = std::isfinite(beta) ? Phi(beta) : 1.0;
385 const double Z = Phi_beta - Phi_alpha;
386 if (Z < 1e-12) return std::numeric_limits<double>::quiet_NaN();
387
388 const double phi_alpha = std::isfinite(alpha) ? phi(alpha) : 0.0;
389 const double phi_beta = std::isfinite(beta) ? phi(beta) : 0.0;
390
391 /* E[Z^k | α<Z<β] via recurrence; store all moments up to k. */
392 std::vector<double> M(k + 1, 0.0);
393 M[0] = 1.0;
394 if (k >= 1) M[1] = (phi_alpha - phi_beta) / Z;
395 for (unsigned m = 2; m <= k; ++m) {
396 /* α^{m-1}·φ(α) and β^{m-1}·φ(β); take 0 when the endpoint is
397 * infinite (the φ factor vanishes faster than any polynomial). */
398 double end_term = 0.0;
399 if (std::isfinite(alpha))
400 end_term += std::pow(alpha, static_cast<double>(m - 1)) * phi_alpha;
401 if (std::isfinite(beta))
402 end_term -= std::pow(beta, static_cast<double>(m - 1)) * phi_beta;
403 M[m] = (m - 1) * M[m - 2] + end_term / Z;
404 }
405
406 /* E[X^k] = E[(μ + σZ)^k] = Σ_{i=0..k} C(k,i) μ^{k-i} σ^i E[Z^i|…]. */
407 double total = 0.0;
408 for (unsigned i = 0; i <= k; ++i) {
409 total += binomial(k, i)
410 * std::pow(mu, static_cast<double>(k - i))
411 * std::pow(sigma, static_cast<double>(i))
412 * M[i];
413 }
414 return total;
415}
416
417/**
418 * @brief Raw moments of @c X ~ Uniform(p1, p2) truncated to @c [a, b].
419 *
420 * The intersection @c [a', b'] = [max(p1,a), min(p2,b)] is uniform;
421 * its k-th raw moment is @c (b'^{k+1} - a'^{k+1}) / ((k+1)(b' - a')).
422 */
423double truncated_uniform_raw_moment(double p1, double p2, double a, double b,
424 unsigned k)
425{
426 const double lo = std::max(p1, a);
427 const double hi = std::min(p2, b);
428 if (hi <= lo) return std::numeric_limits<double>::quiet_NaN();
429 if (k == 0) return 1.0;
430 return (std::pow(hi, static_cast<double>(k + 1))
431 - std::pow(lo, static_cast<double>(k + 1)))
432 / ((k + 1) * (hi - lo));
433}
434
435/**
436 * @brief Raw moments of @c X ~ Exp(λ) truncated to @c [a, b].
437 *
438 * Decomposes via change of variable Y = X - max(a,0):
439 * - left endpoint @c a > 0, right endpoint @c b = +∞: by
440 * memorylessness @c X | X>a is distributed as @c a + Exp(λ), so
441 * @c E[X^k|X>a] = Σ_{i=0..k} C(k,i) a^{k-i} · i!/λ^i.
442 * - finite @c [a, b] (with @c a ≥ 0, @c b < ∞): integrate
443 * @c x^k λ e^{-λx} dx by parts and divide by the truncation mass
444 * @c e^{-λa} - e^{-λb}. Uses the recurrence
445 * @c I_k = k I_{k-1} / λ - (b^k e^{-λb} - a^k e^{-λa}) / λ
446 * with @c I_0 = e^{-λa} - e^{-λb}.
447 */
448double truncated_exponential_raw_moment(double lambda, double a, double b,
449 unsigned k)
450{
451 const double aa = std::max(a, 0.0); /* Exp support is [0, +∞) */
452 if (std::isfinite(b)) {
453 if (b <= aa) return std::numeric_limits<double>::quiet_NaN();
454 /* Finite-interval recurrence on I_k = ∫_{aa}^{b} x^k λ e^{-λx} dx. */
455 const double e_a = std::exp(-lambda * aa);
456 const double e_b = std::exp(-lambda * b);
457 const double Z = e_a - e_b; /* P(aa < X < b) */
458 if (Z < 1e-12) return std::numeric_limits<double>::quiet_NaN();
459 if (k == 0) return 1.0;
460 /* Integration by parts: ∫ x^k λ e^{-λx} dx = -x^k e^{-λx} + k ∫ x^{k-1} e^{-λx} dx
461 * so I_k (with λ factor folded into the e^{-λx}·λ dx term) follows:
462 * I_k = [aa^k e^{-λaa} - b^k e^{-λb}] + (k/λ) · I_{k-1}_no_lambda
463 * where I_{k-1}_no_lambda = ∫ x^{k-1} e^{-λx} dx = I_{k-1}/λ.
464 * Cleaner: compute J_k = ∫_{aa}^{b} x^k e^{-λx} dx via
465 * J_0 = Z/λ; J_k = (aa^k e^{-λaa} - b^k e^{-λb})/λ + (k/λ) J_{k-1}.
466 * Then E[X^k|aa<X<b] = λ J_k / Z. */
467 std::vector<double> J(k + 1, 0.0);
468 J[0] = Z / lambda;
469 for (unsigned m = 1; m <= k; ++m) {
470 const double endpoint = std::pow(aa, static_cast<double>(m)) * e_a
471 - std::pow(b, static_cast<double>(m)) * e_b;
472 J[m] = endpoint / lambda + (m / lambda) * J[m - 1];
473 }
474 return lambda * J[k] / Z;
475 }
476 /* Semi-infinite right tail [aa, +∞): memorylessness. */
477 double total = 0.0;
478 double fact_i = 1.0;
479 for (unsigned i = 0; i <= k; ++i) {
480 total += binomial(k, i)
481 * std::pow(aa, static_cast<double>(k - i))
482 * fact_i / std::pow(lambda, static_cast<double>(i));
483 fact_i *= (i + 1);
484 }
485 return total;
486}
487
488/**
489 * @brief Try to evaluate @f$E[X^k \mid A]@f$ in closed form.
490 *
491 * Fires only when @p root is a bare @c gate_rv of a recognised kind
492 * (Normal / Uniform / Exponential) and the event walk under
493 * @p event_root collects a sound interval constraint on it.
494 * Otherwise returns @c std::nullopt and the caller falls through to
495 * MC rejection.
496 *
497 * For @p central, returns @f$E[(X - \mu_A)^k \mid A]@f$ where
498 * @f$\mu_A@f$ is the closed-form conditional mean obtained by
499 * recursing on @c k = 1, then binomially expanding the central
500 * moment in terms of the raw moments.
501 */
502std::optional<double>
503try_truncated_closed_form(const GenericCircuit &gc, gate_t root,
504 gate_t event_root, unsigned k, bool central)
505{
506 auto m = matchTruncatedSingleRv(gc, root, event_root);
507 if (!m) return std::nullopt;
508 const DistributionSpec &spec = m->spec;
509 const double lo = m->lo, hi = m->hi;
510
511 /* Closed-form raw moment of the truncated distribution. */
512 auto raw = [&](unsigned q) -> std::optional<double> {
513 if (q == 0) return 1.0;
514 double r = std::numeric_limits<double>::quiet_NaN();
515 switch (spec.kind) {
516 case DistKind::Normal:
517 r = truncated_normal_raw_moment(spec.p1, spec.p2, lo, hi, q);
518 break;
520 r = truncated_uniform_raw_moment(spec.p1, spec.p2, lo, hi, q);
521 break;
523 r = truncated_exponential_raw_moment(spec.p1, lo, hi, q);
524 break;
525 case DistKind::Erlang:
526 /* Truncated Erlang moments require the regularised lower
527 * incomplete gamma; out of scope for v1. Fall through to MC. */
528 return std::nullopt;
529 }
530 if (std::isnan(r)) return std::nullopt;
531 return r;
532 };
533
534 if (!central) return raw(k);
535
536 /* Central: E[(X - μ_A)^k | A] = Σ_{i=0..k} C(k,i) (-μ_A)^{k-i} E[X^i | A]. */
537 auto mu_opt = raw(1);
538 if (!mu_opt) return std::nullopt;
539 const double mu = *mu_opt;
540 if (k == 1) return 0.0;
541 double total = 0.0;
542 for (unsigned i = 0; i <= k; ++i) {
543 auto m_i = raw(i);
544 if (!m_i) return std::nullopt;
545 total += binomial(k, i)
546 * std::pow(-mu, static_cast<double>(k - i)) * (*m_i);
547 }
548 return total;
549}
550
551double rec_expectation(const GenericCircuit &gc, gate_t g, FootprintCache &fp)
552{
553 const auto type = gc.getGateType(g);
554 switch (type) {
555 case gate_value:
556 return parseDoubleStrict(gc.getExtra(g));
557 case gate_rv: {
558 auto spec = parse_distribution_spec(gc.getExtra(g));
559 if (!spec)
560 throw CircuitException(
561 "Expectation: malformed gate_rv extra: " + gc.getExtra(g));
562 return analytical_mean(*spec);
563 }
564 case gate_arith: {
565 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
566 const auto &wires = gc.getWires(g);
567 switch (op) {
568 case PROVSQL_ARITH_PLUS: {
569 double s = 0.0;
570 for (gate_t c : wires) s += rec_expectation(gc, c, fp);
571 return s;
572 }
573 case PROVSQL_ARITH_MINUS: {
574 if (wires.size() != 2)
575 throw CircuitException("gate_arith MINUS must be binary");
576 return rec_expectation(gc, wires[0], fp)
577 - rec_expectation(gc, wires[1], fp);
578 }
579 case PROVSQL_ARITH_NEG: {
580 if (wires.size() != 1)
581 throw CircuitException("gate_arith NEG must be unary");
582 return -rec_expectation(gc, wires[0], fp);
583 }
584 case PROVSQL_ARITH_TIMES: {
585 if (pairwise_disjoint(fp, wires)) {
586 double p = 1.0;
587 for (gate_t c : wires) p *= rec_expectation(gc, c, fp);
588 return p;
589 }
590 return mc_raw_moment(gc, g, 1,
591 "Expectation of gate_arith TIMES with shared random variables");
592 }
593 case PROVSQL_ARITH_DIV: {
594 if (wires.size() != 2)
595 throw CircuitException("gate_arith DIV must be binary");
596 if (gc.getGateType(wires[1]) == gate_value) {
597 const double divisor = parseDoubleStrict(gc.getExtra(wires[1]));
598 return rec_expectation(gc, wires[0], fp) / divisor;
599 }
600 return mc_raw_moment(gc, g, 1,
601 "Expectation of gate_arith DIV with non-constant divisor");
602 }
603 }
604 throw CircuitException(
605 "Expectation: unknown gate_arith op tag: " +
606 std::to_string(static_cast<unsigned>(op)));
607 }
608 case gate_mixture: {
609 const auto &wires = gc.getWires(g);
610 if (gc.isCategoricalMixture(g)) {
611 // Categorical mixture: E[M] = Σ π_i · v_i, where each mulinput
612 // mul_i carries π_i in set_prob and v_i in extra.
613 double s = 0.0;
614 for (std::size_t i = 1; i < wires.size(); ++i) {
615 s += gc.getProb(wires[i])
616 * parseDoubleStrict(gc.getExtra(wires[i]));
617 }
618 return s;
619 }
620 // E[mixture(p, X, Y)] = π·E[X] + (1-π)·E[Y], where π = P(p = true).
621 // For a bare gate_input p, π is the leaf's pinned set_prob. For
622 // a compound Boolean p, route through evaluateBooleanProbability
623 // so π honors the tuple-independent semantics of the Boolean DAG.
624 if (wires.size() != 3)
625 throw CircuitException(
626 "Expectation: gate_mixture must have exactly three children");
627 const double pi = mixturePi(gc, wires[0]);
628 return pi * rec_expectation(gc, wires[1], fp)
629 + (1.0 - pi) * rec_expectation(gc, wires[2], fp);
630 }
631 default:
632 return mc_raw_moment(gc, g, 1,
633 "Expectation of gate type " + std::string(gate_type_name[type]));
634 }
635}
636
637double rec_variance(const GenericCircuit &gc, gate_t g, FootprintCache &fp)
638{
639 const auto type = gc.getGateType(g);
640 switch (type) {
641 case gate_value:
642 return 0.0;
643 case gate_rv: {
644 auto spec = parse_distribution_spec(gc.getExtra(g));
645 if (!spec)
646 throw CircuitException(
647 "Variance: malformed gate_rv extra: " + gc.getExtra(g));
648 return analytical_variance(*spec);
649 }
650 case gate_arith: {
651 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
652 const auto &wires = gc.getWires(g);
653 auto mc_var = [&](const std::string &what) {
654 const double mu = mc_raw_moment(gc, g, 1, what);
655 return mc_central_moment(gc, g, 2, mu, what);
656 };
657 switch (op) {
658 case PROVSQL_ARITH_PLUS: {
659 if (pairwise_disjoint(fp, wires)) {
660 double s = 0.0;
661 for (gate_t c : wires) s += rec_variance(gc, c, fp);
662 return s;
663 }
664 return mc_var(
665 "Variance of gate_arith PLUS with shared random variables");
666 }
667 case PROVSQL_ARITH_MINUS: {
668 if (wires.size() != 2)
669 throw CircuitException("gate_arith MINUS must be binary");
670 if (pairwise_disjoint(fp, wires)) {
671 return rec_variance(gc, wires[0], fp)
672 + rec_variance(gc, wires[1], fp);
673 }
674 return mc_var(
675 "Variance of gate_arith MINUS with shared random variables");
676 }
677 case PROVSQL_ARITH_NEG: {
678 if (wires.size() != 1)
679 throw CircuitException("gate_arith NEG must be unary");
680 return rec_variance(gc, wires[0], fp);
681 }
682 case PROVSQL_ARITH_TIMES: {
683 if (pairwise_disjoint(fp, wires)) {
684 // Var(prod Xi) = prod E[Xi^2] - (prod E[Xi])^2
685 // = prod (Var[Xi] + E[Xi]^2) - (prod E[Xi])^2
686 double prod_e2 = 1.0;
687 double prod_e1 = 1.0;
688 for (gate_t c : wires) {
689 const double mu_c = rec_expectation(gc, c, fp);
690 const double v_c = rec_variance(gc, c, fp);
691 prod_e2 *= (v_c + mu_c * mu_c);
692 prod_e1 *= mu_c;
693 }
694 return prod_e2 - prod_e1 * prod_e1;
695 }
696 return mc_var(
697 "Variance of gate_arith TIMES with shared random variables");
698 }
699 case PROVSQL_ARITH_DIV: {
700 if (wires.size() != 2)
701 throw CircuitException("gate_arith DIV must be binary");
702 if (gc.getGateType(wires[1]) == gate_value) {
703 const double divisor = parseDoubleStrict(gc.getExtra(wires[1]));
704 return rec_variance(gc, wires[0], fp) / (divisor * divisor);
705 }
706 return mc_var(
707 "Variance of gate_arith DIV with non-constant divisor");
708 }
709 }
710 throw CircuitException(
711 "Variance: unknown gate_arith op tag: " +
712 std::to_string(static_cast<unsigned>(op)));
713 }
714 case gate_mixture: {
715 const auto &wires = gc.getWires(g);
716 if (gc.isCategoricalMixture(g)) {
717 // Categorical mixture: Var(M) = Σ π_i v_i² − (Σ π_i v_i)².
718 double e1 = 0.0, e2 = 0.0;
719 for (std::size_t i = 1; i < wires.size(); ++i) {
720 const double p = gc.getProb(wires[i]);
721 const double v = parseDoubleStrict(gc.getExtra(wires[i]));
722 e1 += p * v;
723 e2 += p * v * v;
724 }
725 return e2 - e1 * e1;
726 }
727 // Var(M) = π·(Var(X) + E[X]²) + (1-π)·(Var(Y) + E[Y]²) - E[M]²
728 // (law of total variance specialised to a Bernoulli mixture).
729 if (wires.size() != 3)
730 throw CircuitException(
731 "Variance: gate_mixture must have exactly three children");
732 const double pi = mixturePi(gc, wires[0]);
733 const double ex = rec_expectation(gc, wires[1], fp);
734 const double ey = rec_expectation(gc, wires[2], fp);
735 const double vx = rec_variance(gc, wires[1], fp);
736 const double vy = rec_variance(gc, wires[2], fp);
737 const double em = pi * ex + (1.0 - pi) * ey;
738 return pi * (vx + ex * ex)
739 + (1.0 - pi) * (vy + ey * ey)
740 - em * em;
741 }
742 default: {
743 const std::string what =
744 "Variance of gate type " + std::string(gate_type_name[type]);
745 const double mu = mc_raw_moment(gc, g, 1, what);
746 return mc_central_moment(gc, g, 2, mu, what);
747 }
748 }
749}
750
751double rec_raw_moment(const GenericCircuit &gc, gate_t g, unsigned k,
752 FootprintCache &fp)
753{
754 if (k == 0) return 1.0;
755 if (k == 1) return rec_expectation(gc, g, fp);
756
757 const auto type = gc.getGateType(g);
758 switch (type) {
759 case gate_value:
760 return std::pow(parseDoubleStrict(gc.getExtra(g)),
761 static_cast<double>(k));
762 case gate_rv: {
763 auto spec = parse_distribution_spec(gc.getExtra(g));
764 if (!spec)
765 throw CircuitException(
766 "Moment: malformed gate_rv extra: " + gc.getExtra(g));
767 return analytical_raw_moment(*spec, k);
768 }
769 case gate_arith: {
770 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
771 const auto &wires = gc.getWires(g);
772 switch (op) {
773 case PROVSQL_ARITH_NEG: {
774 if (wires.size() != 1)
775 throw CircuitException("gate_arith NEG must be unary");
776 const double v = rec_raw_moment(gc, wires[0], k, fp);
777 return ((k % 2 == 0) ? 1.0 : -1.0) * v;
778 }
779 case PROVSQL_ARITH_PLUS: {
780 if (pairwise_disjoint(fp, wires)) {
781 // Fold-left: m_acc[i] holds E[(X1 + ... + Xj)^i] for the
782 // first j children processed; combining with the next
783 // independent child Y uses the binomial theorem.
784 std::vector<double> m_acc(k + 1, 0.0);
785 for (unsigned i = 0; i <= k; ++i)
786 m_acc[i] = rec_raw_moment(gc, wires[0], i, fp);
787 for (size_t w = 1; w < wires.size(); ++w) {
788 std::vector<double> next(k + 1, 0.0);
789 std::vector<double> moments_y(k + 1, 0.0);
790 for (unsigned i = 0; i <= k; ++i)
791 moments_y[i] = rec_raw_moment(gc, wires[w], i, fp);
792 for (unsigned kp = 0; kp <= k; ++kp) {
793 double total = 0.0;
794 for (unsigned i = 0; i <= kp; ++i) {
795 total += binomial(kp, i) * m_acc[i] * moments_y[kp - i];
796 }
797 next[kp] = total;
798 }
799 m_acc = std::move(next);
800 }
801 return m_acc[k];
802 }
803 return mc_raw_moment(gc, g, k,
804 "Raw moment of gate_arith PLUS with shared random variables");
805 }
806 case PROVSQL_ARITH_MINUS: {
807 if (wires.size() != 2)
808 throw CircuitException("gate_arith MINUS must be binary");
809 if (pairwise_disjoint(fp, wires)) {
810 double total = 0.0;
811 for (unsigned i = 0; i <= k; ++i) {
812 const double sign = ((k - i) % 2 == 0) ? 1.0 : -1.0;
813 total += binomial(k, i)
814 * rec_raw_moment(gc, wires[0], i, fp)
815 * sign
816 * rec_raw_moment(gc, wires[1], k - i, fp);
817 }
818 return total;
819 }
820 return mc_raw_moment(gc, g, k,
821 "Raw moment of gate_arith MINUS with shared random variables");
822 }
823 case PROVSQL_ARITH_TIMES: {
824 if (pairwise_disjoint(fp, wires)) {
825 // (prod Xi)^k = prod Xi^k; under independence E factors.
826 double p = 1.0;
827 for (gate_t c : wires) p *= rec_raw_moment(gc, c, k, fp);
828 return p;
829 }
830 return mc_raw_moment(gc, g, k,
831 "Raw moment of gate_arith TIMES with shared random variables");
832 }
833 case PROVSQL_ARITH_DIV: {
834 if (wires.size() != 2)
835 throw CircuitException("gate_arith DIV must be binary");
836 if (gc.getGateType(wires[1]) == gate_value) {
837 const double divisor = parseDoubleStrict(gc.getExtra(wires[1]));
838 return rec_raw_moment(gc, wires[0], k, fp)
839 / std::pow(divisor, static_cast<double>(k));
840 }
841 return mc_raw_moment(gc, g, k,
842 "Raw moment of gate_arith DIV with non-constant divisor");
843 }
844 }
845 throw CircuitException(
846 "Moment: unknown gate_arith op tag: " +
847 std::to_string(static_cast<unsigned>(op)));
848 }
849 case gate_mixture: {
850 const auto &wires = gc.getWires(g);
851 if (gc.isCategoricalMixture(g)) {
852 // Categorical mixture: E[M^k] = Σ π_i v_i^k.
853 double s = 0.0;
854 for (std::size_t i = 1; i < wires.size(); ++i) {
855 const double v = parseDoubleStrict(gc.getExtra(wires[i]));
856 s += gc.getProb(wires[i])
857 * std::pow(v, static_cast<double>(k));
858 }
859 return s;
860 }
861 // E[M^k] = π·E[X^k] + (1-π)·E[Y^k].
862 if (wires.size() != 3)
863 throw CircuitException(
864 "Moment: gate_mixture must have exactly three children");
865 const double pi = mixturePi(gc, wires[0]);
866 return pi * rec_raw_moment(gc, wires[1], k, fp)
867 + (1.0 - pi) * rec_raw_moment(gc, wires[2], k, fp);
868 }
869 default:
870 return mc_raw_moment(gc, g, k,
871 "Raw moment of gate type " + std::string(gate_type_name[type]));
872 }
873}
874
875} // namespace
876
877/* Conditional dispatch helpers: try closed-form first, fall through
878 * to MC rejection. Used by all four public compute_* entries to keep
879 * the conditional logic in one place and the unconditional path
880 * unchanged. */
881namespace {
882
883[[noreturn]] void raise_infeasible_event(const GenericCircuit &gc, gate_t root)
884{
885 (void)gc; (void)root;
886 throw CircuitException(
887 "conditioning event is infeasible (empty intersection with the "
888 "random variable's support)");
889}
890
891double conditional_raw_moment(const GenericCircuit &gc, gate_t root,
892 unsigned k, gate_t event_root)
893{
894 if (k == 0) return 1.0;
895 if (auto cf = try_truncated_closed_form(gc, root, event_root, k, false))
896 return *cf;
897 if (eventIsProvablyInfeasible(gc, root, event_root))
898 raise_infeasible_event(gc, root);
899 return mc_conditional_raw_moment(
900 gc, root, k, event_root,
901 "Conditional raw moment of gate type " +
902 std::string(gate_type_name[gc.getGateType(root)]));
903}
904
905double conditional_central_moment(const GenericCircuit &gc, gate_t root,
906 unsigned k, gate_t event_root)
907{
908 if (k == 0) return 1.0;
909 if (k == 1) return 0.0;
910 if (auto cf = try_truncated_closed_form(gc, root, event_root, k, true))
911 return *cf;
912 if (eventIsProvablyInfeasible(gc, root, event_root))
913 raise_infeasible_event(gc, root);
914 /* MC central: need μ_A first. */
915 const double mu = conditional_raw_moment(gc, root, 1, event_root);
916 return mc_conditional_central_moment(
917 gc, root, k, mu, event_root,
918 "Conditional central moment of gate type " +
919 std::string(gate_type_name[gc.getGateType(root)]));
920}
921
922} // namespace
923
925 std::optional<gate_t> event_root)
926{
927 if (event_root.has_value())
928 return conditional_raw_moment(gc, root, 1, *event_root);
929 FootprintCache fp(gc);
930 return rec_expectation(gc, root, fp);
931}
932
933double compute_raw_moment(const GenericCircuit &gc, gate_t root, unsigned k,
934 std::optional<gate_t> event_root)
935{
936 if (event_root.has_value())
937 return conditional_raw_moment(gc, root, k, *event_root);
938 FootprintCache fp(gc);
939 return rec_raw_moment(gc, root, k, fp);
940}
941
942double compute_central_moment(const GenericCircuit &gc, gate_t root, unsigned k,
943 std::optional<gate_t> event_root)
944{
945 if (event_root.has_value())
946 return conditional_central_moment(gc, root, k, *event_root);
947 if (k == 0) return 1.0;
948 if (k == 1) return 0.0;
949 FootprintCache fp(gc);
950 if (k == 2) return rec_variance(gc, root, fp);
951 // E[(X - mu)^k] = sum_{i=0}^{k} C(k, i) (-mu)^(k-i) E[X^i]
952 const double mu = rec_expectation(gc, root, fp);
953 double total = 0.0;
954 for (unsigned i = 0; i <= k; ++i) {
955 const double mu_pow = std::pow(-mu, static_cast<double>(k - i));
956 total += binomial(k, i) * mu_pow * rec_raw_moment(gc, root, i, fp);
957 }
958 return total;
959}
960
961/**
962 * @brief Lift conditioning out of a scalar arithmetic expression.
963 *
964 * Implements @c "f(X|A, Y|B, …) = f(X, Y, …) | (A ∧ B ∧ …)": walks the scalar
965 * tree rooted at @p root, replaces every nested @c gate_conditioned by a
966 * transparent passthrough to its target (so the tree becomes the plain
967 * arithmetic over the unconditioned distributions), collects the evidence
968 * children, and conjoins them -- together with any pre-existing @p event_opt
969 * -- into a single conditioning event. The conjunction is built as an
970 * in-memory @c gate_times over the evidence gates, all of which already live
971 * in the (joint) circuit, so a base @c gate_rv shared between a value and its
972 * evidence keeps a single draw under the MC sampler. A conditioned ROOT is
973 * peeled to its bare target (returned), so a stored "X | C" reaching any
974 * low-level RV entry point keeps the closed-form scalar path; the (possibly
975 * new) root is returned. Leaves @p event_opt untouched and returns @p root
976 * unchanged when the expression carries no conditioning.
977 */
979 std::optional<gate_t> &event_opt)
980{
981 std::vector<gate_t> evidences;
982
983 // 1. Peel a conditioned ROOT to its bare target. A root has no parent
984 // wires, so it is replaced by its target directly rather than the
985 // single-child gate_arith passthrough the buried case below needs;
986 // keeping a bare gate_rv root preserves the closed-form truncation
987 // path for "X | (X > c)". Handles the 2-child rv/agg carrier
988 // [target, condition] and (defensively) the 3-child uuid carrier
989 // [target, evidence, joint]; iterates in case of nested conditioning.
990 while (gc.getGateType(root) == gate_conditioned) {
991 const auto &w = gc.getWires(root);
992 if (w.size() < 2)
993 throw CircuitException("malformed conditioned gate in scalar expression");
994 evidences.push_back(w[1]);
995 root = w[0];
996 }
997
998 // 2. Replace every BURIED gate_conditioned by an arith passthrough to its
999 // target, collecting evidence as well.
1000 std::set<gate_t> seen;
1001 std::vector<gate_t> stack{root};
1002 while (!stack.empty()) {
1003 gate_t g = stack.back();
1004 stack.pop_back();
1005 if (!seen.insert(g).second) continue;
1006 if (gc.getGateType(g) == gate_conditioned) {
1007 const auto &w = gc.getWires(g);
1008 if (w.size() < 2)
1009 throw CircuitException("malformed conditioned gate in scalar expression");
1010 gate_t target = w[0];
1011 evidences.push_back(w[1]);
1012 gc.liftConditionedToTarget(g, target); // g becomes arith PLUS [target]
1013 stack.push_back(target);
1014 } else {
1015 for (gate_t c : gc.getWires(g))
1016 stack.push_back(c);
1017 }
1018 }
1019 if (evidences.empty())
1020 return root;
1021 if (event_opt.has_value())
1022 evidences.push_back(*event_opt);
1023 gate_t cond;
1024 if (evidences.size() == 1)
1025 cond = evidences[0];
1026 else {
1027 cond = gc.setGate(gate_times); // AND of all evidence (and prior event)
1028 auto &cw = gc.getWires(cond);
1029 for (gate_t e : evidences)
1030 cw.push_back(e);
1031 }
1032 event_opt = cond;
1033 return root;
1034}
1035
1036} // namespace provsql
1037
1038extern "C" {
1039
1040/**
1041 * @brief SQL: rv_moment(token uuid, k integer, central boolean,
1042 * prov uuid DEFAULT gate_one()) -> float8
1043 *
1044 * Single C entry point shared by the @c expected / @c variance /
1045 * @c moment / @c central_moment SQL functions. The SQL wrappers
1046 * select the (k, central) pair that matches their semantics:
1047 * - @c expected(rv, prov): k=1, central=false.
1048 * - @c variance(rv, prov): k=2, central=true.
1049 * - @c moment(rv, k, prov): central=false.
1050 * - @c central_moment(rv, k, prov): central=true.
1051 *
1052 * The @p prov argument carries the conditioning event: typically the
1053 * row's @c provenance() gate after a @c WHERE predicate folded a
1054 * @c gate_cmp into it. When @p prov resolves to @c gate_one (the
1055 * default, or the load-time simplification of any always-true
1056 * sub-circuit) the unconditional path runs unchanged. Otherwise we
1057 * load a JOINT circuit reaching both roots, so shared @c gate_rv
1058 * leaves collapse to a single @c gate_t -- the property the
1059 * conditional MC sampler relies on to couple the indicator's draw
1060 * with the value's draw.
1061 */
1062Datum rv_moment(PG_FUNCTION_ARGS)
1063{
1064 try {
1065 pg_uuid_t *token = PG_GETARG_UUID_P(0);
1066 const int32 k_signed = PG_GETARG_INT32(1);
1067 const bool central = PG_GETARG_BOOL(2);
1068 pg_uuid_t *prov = PG_GETARG_UUID_P(3);
1069
1070 if (k_signed < 0)
1071 provsql_error("rv_moment: k must be non-negative (got %d)", k_signed);
1072 const unsigned k = static_cast<unsigned>(k_signed);
1073
1074 gate_t root_gate, event_gate;
1075 auto gc = getJointCircuit(*token, *prov, root_gate, event_gate);
1076
1077 /* gate_one event = unconditional after load-time simplification. */
1078 std::optional<gate_t> event_opt;
1079 if (gc.getGateType(event_gate) != gate_one)
1080 event_opt = event_gate;
1081
1082 /* Arithmetic over conditioned distributions: peel a conditioned ROOT to
1083 * its bare target (keeping the closed-form truncation path for the
1084 * bare-rv case) and lift any nested gate_conditioned out of the scalar
1085 * expression, folding its evidence into the conditioning event
1086 * (f(X|A, Y|B) = f(X, Y) | (A ∧ B)). Works whether the token arrives
1087 * already unpacked by the SQL dispatcher or as a raw conditioned root
1088 * (Studio's distribution panel calls this low-level binding directly). */
1089 root_gate = provsql::lift_conditioning(gc, root_gate, event_opt);
1090
1091 double result;
1092 if (central)
1093 result = provsql::compute_central_moment(gc, root_gate, k, event_opt);
1094 else if (k == 1)
1095 result = provsql::compute_expectation(gc, root_gate, event_opt);
1096 else
1097 result = provsql::compute_raw_moment(gc, root_gate, k, event_opt);
1098 return Float8GetDatum(result);
1099 } catch (const std::exception &e) {
1100 provsql_error("rv_moment: %s", e.what());
1101 } catch (...) {
1102 provsql_error("rv_moment: unknown exception");
1103 }
1104 PG_RETURN_NULL();
1105}
1106
1107} // extern "C"
Closed-form CDF resolution for trivial gate_cmp shapes.
Boolean-expression (lineage formula) semiring.
Boolean provenance circuit with support for knowledge compilation.
@ IN
Input (variable) gate representing a base tuple.
@ MULIN
Multivalued-input gate (one of several options).
void propagateDNNFCertificate(const GenericCircuit &gc, const std::unordered_map< gate_t, gate_t > &gc_to_bc, BooleanCircuit &c)
Propagate the per-gate d-DNNF certificate from gc to c.
GenericCircuit getJointCircuit(pg_uuid_t root_token, pg_uuid_t event_token, gate_t &root_gate, gate_t &event_gate)
Build a GenericCircuit containing the closures of two roots, with shared subgraphs unified.
Build in-memory circuits from the mmap-backed persistent store.
Generic directed-acyclic-graph circuit template and gate identifier.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Datum rv_moment(PG_FUNCTION_ARGS)
SQL: rv_moment(token uuid, k integer, central boolean, prov uuid DEFAULT gate_on...
Analytical expectation / variance / moment evaluator over RV circuits.
Monte Carlo sampling over a GenericCircuit, RV-aware.
Continuous random-variable helpers (distribution parsing, moments).
Support-based bound check for continuous-RV comparators.
Boolean circuit for provenance formula evaluation.
gate_t setGate(BooleanGate type) override
Allocate a new gate with type type and no UUID.
double monteCarlo(gate_t g, unsigned samples) const
Estimate the probability via Monte Carlo sampling.
void rewriteMultivaluedGates()
Rewrite all MULVAR/MULIN gate clusters into standard AND/OR/NOT circuits.
void setInfo(gate_t g, unsigned info)
Store an integer annotation on gate g.
double independentEvaluation(gate_t g) const
Compute the probability exactly when inputs are independent.
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
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
void addWire(gate_t f, gate_t t)
Add a directed wire from gate f (parent) to gate t (child).
Definition Circuit.hpp:81
uuid getUUID(gate_t g) const
Return the UUID string associated with gate g.
Definition Circuit.hpp:46
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
In-memory provenance circuit with semiring-generic evaluation.
S::value_type evaluate(gate_t g, std::unordered_map< gate_t, typename S::value_type > &provenance_mapping, S semiring) const
Evaluate the sub-circuit rooted at gate g over semiring semiring.
bool isCategoricalMixture(gate_t g) const
Test whether g is a categorical-form gate_mixture (the explicit provsql.categorical output).
std::string getExtra(gate_t g) const
Return the string extra for gate g.
gate_t setGate(gate_type type) override
Allocate a new gate with type type and no UUID.
double getProb(gate_t g) const
Return the probability for gate g.
const std::set< gate_t > & getInputs() const
Return the set of input (leaf) gates.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
void liftConditionedToTarget(gate_t g, gate_t target)
Replace a gate_conditioned g by a transparent passthrough to its target child (a single-child gate_ar...
Provenance-as-Boolean-circuit semiring.
Definition BoolExpr.h:48
@ Normal
Normal (Gaussian): p1=μ, p2=σ
@ Exponential
Exponential: p1=λ, p2 unused.
@ Uniform
Uniform on [a,b]: p1=a, p2=b.
@ Erlang
Erlang: p1=k (positive integer), p2=λ.
double compute_raw_moment(const GenericCircuit &gc, gate_t root, unsigned k, std::optional< gate_t > event_root)
Compute the raw moment (or if event_root is set) for k >= 0.
gate_t lift_conditioning(GenericCircuit &gc, gate_t root, std::optional< gate_t > &event_opt)
Lift conditioning out of a scalar arithmetic expression.
double analytical_variance(const DistributionSpec &d)
Closed-form variance Var(X) for a basic distribution.
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
bool eventIsProvablyInfeasible(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
True iff the conditioning event is provably infeasible for a bare gate_rv root.
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).
ConditionalScalarSamples monteCarloConditionalScalarSamples(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
Rejection-sample root conditioned on event_root.
double evaluateBooleanProbability(const GenericCircuit &gc, gate_t boolRoot)
Probability that the Boolean subcircuit rooted at boolRoot evaluates to true under the tuple-independ...
std::vector< double > monteCarloScalarSamples(const GenericCircuit &gc, gate_t root, unsigned samples)
Sample a scalar sub-circuit samples times and return the draws.
std::optional< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
double analytical_mean(const DistributionSpec &d)
Closed-form expectation E[X] for a basic distribution.
std::optional< TruncatedSingleRv > matchTruncatedSingleRv(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
Detect a closed-form, optionally-truncated single-RV shape.
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.
double analytical_raw_moment(const DistributionSpec &d, unsigned k)
Closed-form raw moment for a basic distribution.
int provsql_verbose
Verbosity level; controlled by the provsql.verbose_level GUC.
Definition provsql.c:89
int provsql_rv_mc_samples
Default sample count for analytical-evaluator MC fallbacks; 0 disables fallback (callers raise instea...
Definition provsql.c:96
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).
const char * gate_type_name[]
Names of gate types.
Core types, constants, and utilities shared across ProvSQL.
provsql_arith_op
Arithmetic operator tags used by gate_arith.
@ PROVSQL_ARITH_DIV
binary, child0 / child1
@ PROVSQL_ARITH_PLUS
n-ary, sum of children
@ PROVSQL_ARITH_NEG
unary, -child0
@ PROVSQL_ARITH_MINUS
binary, child0 - child1
@ PROVSQL_ARITH_TIMES
n-ary, product of children
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
@ gate_annotation
Transparent single-child wrapper carrying a query-level annotation in extra (inversion-free certifica...
@ gate_conditioned
Conditioning marker with two children [target, evidence]: measure-only, probability_evaluate returns ...
@ gate_mixture
Probabilistic mixture: three wires [p_token (gate_input Bernoulli), x_token, y_token]; samples x when...
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
C++ utility functions for UUID manipulation.
UUID structure.
Outcome of a conditional Monte Carlo sampling pass.
Parsed distribution spec (kind + up to two parameters).