ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
CollapsedAggMoment.cpp
Go to the documentation of this file.
1/**
2 * @file CollapsedAggMoment.cpp
3 * @brief Rao-Blackwellised (collapsed) moments of a correlated COUNT / SUM,
4 * and the exact posterior of a latent conditioned on such a count.
5 *
6 * The recurring shape in latent-variable relational models is an aggregate
7 * over probabilistically-selected rows whose selection events are coupled by
8 * a small shared latent:
9 *
10 * C = COUNT/SUM over rows i of [ eps_i op g_i(b) ]
11 *
12 * where @c eps_i is a per-row independent random-variable leaf, @c b is a
13 * shared continuous latent that every row references, and @c g_i(b) is an
14 * expression in @c b (plus per-row constants). The exact moment path
15 * enumerates @c n^k row tuples and evaluates a joint probability for each --
16 * @c O(n^2) pair-probabilities for the variance, which does not scale.
17 *
18 * Conditional on @c b, the indicators are INDEPENDENT (the only coupling is
19 * through @c b), so this collapses to a 1-D quadrature over @c b: at each node
20 * the per-row success probability @c q_i(b) = Pr[eps_i op g_i(b)] is a
21 * closed-form CDF, and the count's conditional law is the elementary
22 * sum-of-independent-Bernoulli (Poisson-binomial) distribution. Cost
23 * @c O(G·n) (moments) / @c O(G·n^2) (pmf) -- milliseconds where the exact path
24 * took minutes -- and exact up to the quadrature (no sampling).
25 *
26 * The same collapse yields the EXACT posterior of a latent @c R conditioned on
27 * a discrete rv @c Y(R) equalling the count: @c P(C=j) is the collapsed pmf,
28 * and @c E[R^k|C] is a 1-D quadrature over @c R weighted by the likelihood
29 * @c L(r)=Σ_j P(C=j) pmf_Y(j;θ(r)) -- replacing a degenerating importance
30 * sampler with a closed quadrature.
31 *
32 * Scope (returns @c std::nullopt, i.e. "fall back", on any mismatch): a
33 * @c COUNT / @c SUM @c gate_agg whose per-row indicators reduce to
34 * @c "(constant Bernoullis) AND (one gate_cmp)", the cmp comparing a bare
35 * per-row @c gate_rv leaf against an expression over exactly ONE shared
36 * continuous latent leaf. Everything else declines.
37 */
38#include "CollapsedAggMoment.h"
39
40#include "CircuitFromMMap.h"
41#include "RandomVariable.h"
42#include "Aggregation.h" // AggregationOperator, ComparisonOperator
43#include "distributions/Distribution.h" // makeDistribution
44#include "Circuit.h"
45#include "provsql_utils_cpp.h" // uuid2string
46
47extern "C" {
48#include "postgres.h"
49#include "fmgr.h"
50#include "utils/uuid.h"
51#include "utils/array.h"
52#include "catalog/pg_type.h"
53#include "provsql_utils.h"
54#include "provsql_error.h"
55}
56
57#include <cmath>
58#include <limits>
59#include <memory>
60#include <optional>
61#include <unordered_map>
62#include <unordered_set>
63#include <vector>
64
65namespace provsql {
66
67namespace {
68
69constexpr double kNaN = std::numeric_limits<double>::quiet_NaN();
70constexpr int kGrid = 400; ///< quadrature nodes (shared latent and posterior)
71
72/// Collect the @c gate_rv leaves reachable from @p g (the RV footprint).
73void collectRvLeaves(const GenericCircuit &gc, gate_t g,
74 std::unordered_set<gate_t> &out)
75{
76 std::unordered_set<gate_t> seen;
77 std::vector<gate_t> stack{g};
78 while (!stack.empty()) {
79 gate_t x = stack.back(); stack.pop_back();
80 if (!seen.insert(x).second) continue;
81 if (gc.getGateType(x) == gate_rv) { out.insert(x); continue; }
82 for (gate_t c : gc.getWires(x)) stack.push_back(c);
83 }
84}
85
86/// Evaluate a deterministic scalar expression at @p bval, where the only
87/// random leaf allowed is @p var (set to @p bval). Returns NaN on any
88/// unexpected gate, which aborts the collapse. Used both for the shared-side
89/// link expression g_i(b) and for a latent leaf's parameter expression θ(r).
90double evalWithVar(const GenericCircuit &gc, gate_t g, gate_t var, double val)
91{
92 switch (gc.getGateType(g)) {
93 case gate_rv:
94 return (g == var) ? val : kNaN;
95 case gate_value:
96 try { return parseDoubleStrict(gc.getExtra(g)); }
97 catch (const CircuitException &) { return kNaN; }
98 case gate_arith: {
99 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
100 const auto &w = gc.getWires(g);
101 auto ev = [&](gate_t c) { return evalWithVar(gc, c, var, val); };
102 switch (op) {
103 case PROVSQL_ARITH_PLUS: {
104 double s = 0.0; for (gate_t c : w) s += ev(c); return s;
105 }
106 case PROVSQL_ARITH_TIMES: {
107 double p = 1.0; for (gate_t c : w) p *= ev(c); return p;
108 }
110 return (w.size() == 2) ? ev(w[0]) - ev(w[1]) : kNaN;
112 return (w.size() == 2) ? ev(w[0]) / ev(w[1]) : kNaN;
114 return (w.size() == 1) ? -ev(w[0]) : kNaN;
115 default:
116 return kNaN;
117 }
118 }
119 default:
120 return kNaN;
121 }
122}
123
124/// Resolve @p g to an affine form `slope·var + intercept` over the same ops
125/// @c evalWithVar handles, or std::nullopt when it is not affine in @p var
126/// (a genuinely non-linear b-dependence, or an unsupported gate). A structural
127/// (not sampled) decomposition, so it never mis-classifies a non-linear link.
128/// In a shared-side link expression the only rv leaf present is @p var, so a
129/// @c gate_rv other than @p var cannot occur (it declines defensively anyway).
130std::optional<std::pair<double, double>>
131affineInVar(const GenericCircuit &gc, gate_t g, gate_t var)
132{
133 using R = std::optional<std::pair<double, double>>;
134 switch (gc.getGateType(g)) {
135 case gate_rv:
136 return (g == var) ? R({1.0, 0.0}) : std::nullopt;
137 case gate_value:
138 try { return R({0.0, parseDoubleStrict(gc.getExtra(g))}); }
139 catch (const CircuitException &) { return std::nullopt; }
140 case gate_arith: {
141 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
142 const auto &w = gc.getWires(g);
143 switch (op) {
144 case PROVSQL_ARITH_PLUS: {
145 double s = 0.0, c = 0.0;
146 for (gate_t ch : w) {
147 auto a = affineInVar(gc, ch, var);
148 if (!a) return std::nullopt;
149 s += a->first; c += a->second;
150 }
151 return R({s, c});
152 }
153 case PROVSQL_ARITH_TIMES: {
154 double s = 0.0, c = 1.0; // running affine product (starts at 1)
155 for (gate_t ch : w) {
156 auto a = affineInVar(gc, ch, var);
157 if (!a) return std::nullopt;
158 // (s·x + c)·(a.s·x + a.c) is affine only if a factor is constant.
159 if (s != 0.0 && a->first != 0.0) return std::nullopt;
160 const double ns = s * a->second + c * a->first;
161 c = c * a->second;
162 s = ns;
163 }
164 return R({s, c});
165 }
166 case PROVSQL_ARITH_MINUS: {
167 if (w.size() != 2) return std::nullopt;
168 auto a = affineInVar(gc, w[0], var), b = affineInVar(gc, w[1], var);
169 if (!a || !b) return std::nullopt;
170 return R({a->first - b->first, a->second - b->second});
171 }
172 case PROVSQL_ARITH_NEG: {
173 if (w.size() != 1) return std::nullopt;
174 auto a = affineInVar(gc, w[0], var);
175 if (!a) return std::nullopt;
176 return R({-a->first, -a->second});
177 }
178 case PROVSQL_ARITH_DIV: {
179 if (w.size() != 2) return std::nullopt;
180 auto a = affineInVar(gc, w[0], var), b = affineInVar(gc, w[1], var);
181 if (!a || !b) return std::nullopt;
182 if (b->first != 0.0 || b->second == 0.0) return std::nullopt;
183 return R({a->first / b->second, a->second / b->second});
184 }
185 default:
186 return std::nullopt;
187 }
188 }
189 default:
190 return std::nullopt;
191 }
192}
193
194/// One recognised per-row term: q_i(b) = bern · Pr[eps_i op_i g_i(b)].
195struct Term {
196 const DistributionFamily *family; ///< eps_i family (for the CDF)
197 double p1, p2; ///< eps_i parameters
198 gate_t shared_side; ///< g_i(b) expression gate
199 ComparisonOperator op; ///< comparison, oriented "eps_i op g_i"
200 double bern; ///< product of the constant Bernoulli inputs
201 double value; ///< v_i (1 for COUNT; the summed value for SUM)
202
203 // Precomputed, b-invariant, so the O(G·n) grid loop touches no gate: the
204 // eps_i distribution is built once (its parameters are constant), and the
205 // shared-side link g_i(b) is resolved to an affine form slope·b + intercept
206 // whenever it is affine in the shared latent (the common alpha_i + b link),
207 // sparing the per-node evalWithVar walk and its std::map gate lookups.
208 std::shared_ptr<Distribution> dist; ///< family->factory(p1, p2), cached
209 bool affine = false; ///< g_i(b) = slope·b + intercept
210 double slope = 0.0, intercept = 0.0; ///< valid iff affine
211};
212
213/// The recognised collapse: the shared latent, its distribution and quadrature
214/// window, and the oriented per-row terms.
215struct CollapsePlan {
217 gate_t shared;
218 std::unique_ptr<Distribution> shared_dist;
219 double lo, hi;
220 std::vector<Term> terms;
221};
222
223/// Reduce an indicator k-gate to (Bernoulli product, the single gate_cmp).
224/// Returns false if it is not "(constant inputs) AND (one cmp)".
225bool reduceIndicator(const GenericCircuit &gc, gate_t k_gate,
226 double &bern, gate_t &cmp_out)
227{
228 bern = 1.0;
229 bool have_cmp = false;
230 std::vector<gate_t> stack{k_gate};
231 while (!stack.empty()) {
232 gate_t g = stack.back(); stack.pop_back();
233 switch (gc.getGateType(g)) {
234 case gate_times:
235 for (gate_t c : gc.getWires(g)) stack.push_back(c);
236 break;
237 case gate_one:
238 break;
239 case gate_input:
240 case gate_update: {
241 const double p = gc.getProb(g);
242 if (std::isnan(p)) return false;
243 bern *= p;
244 break;
245 }
246 case gate_cmp:
247 if (have_cmp) return false; // only a single comparison supported
248 cmp_out = g;
249 have_cmp = true;
250 break;
251 default:
252 return false;
253 }
254 }
255 return have_cmp;
256}
257
259{
260 switch (op) {
265 default: return op; // EQ / NE symmetric
266 }
267}
268
269/// Recognise the shared-latent COUNT / SUM shape at @p agg and build the plan.
270std::optional<CollapsePlan>
271buildCollapsePlan(const GenericCircuit &gc, gate_t agg)
272{
273 if (gc.getGateType(agg) != gate_agg) return std::nullopt;
274 const AggregationOperator aop = getAggregationOperator(gc.getInfos(agg).first);
276 return std::nullopt;
277
278 const auto &children = gc.getWires(agg);
279 if (children.empty()) return std::nullopt;
280
281 struct Raw {
282 double bern, value;
283 gate_t a, b;
285 };
286 std::vector<Raw> raws;
287 raws.reserve(children.size());
288 std::unordered_map<gate_t, unsigned> leaf_rows;
289
290 for (gate_t sm : children) {
291 if (gc.getGateType(sm) != gate_semimod) return std::nullopt;
292 const auto &smw = gc.getWires(sm);
293 if (smw.size() != 2) return std::nullopt;
294 if (gc.getGateType(smw[1]) != gate_value) return std::nullopt;
295 double value;
296 try { value = parseDoubleStrict(gc.getExtra(smw[1])); }
297 catch (const CircuitException &) { return std::nullopt; }
298
299 double bern;
300 gate_t cmp = static_cast<gate_t>(0);
301 if (!reduceIndicator(gc, smw[0], bern, cmp)) return std::nullopt;
302 const auto &cw = gc.getWires(cmp);
303 if (cw.size() != 2) return std::nullopt;
304 bool ok = false;
305 ComparisonOperator op = cmpOpFromOid(gc.getInfos(cmp).first, ok);
306 if (!ok) return std::nullopt;
307
308 raws.push_back({bern, value, cw[0], cw[1], op});
309
310 std::unordered_set<gate_t> rowleaves;
311 collectRvLeaves(gc, cmp, rowleaves);
312 for (gate_t l : rowleaves) ++leaf_rows[l];
313 }
314
315 gate_t shared = static_cast<gate_t>(0);
316 unsigned nb_shared = 0;
317 for (const auto &[leaf, rows] : leaf_rows)
318 if (rows > 1) { shared = leaf; ++nb_shared; }
319 if (nb_shared != 1) return std::nullopt;
320
321 auto shared_spec = parse_distribution_spec(gc.getExtra(shared));
322 if (!shared_spec) return std::nullopt;
323 auto shared_dist = makeDistribution(*shared_spec);
324 double lo, hi;
325 if (!shared_dist->integrationRange(lo, hi) || !(hi > lo)) return std::nullopt;
326
327 std::vector<Term> terms;
328 terms.reserve(raws.size());
329 for (const auto &r : raws) {
330 std::unordered_set<gate_t> fa, fb;
331 collectRvLeaves(gc, r.a, fa);
332 collectRvLeaves(gc, r.b, fb);
333
334 gate_t per_row = static_cast<gate_t>(0),
335 shared_side = static_cast<gate_t>(0);
336 ComparisonOperator op = r.op;
337 auto is_per_row = [&](gate_t side, const std::unordered_set<gate_t> &f) {
338 return gc.getGateType(side) == gate_rv && side != shared && f.size() == 1;
339 };
340 auto is_shared_side = [&](const std::unordered_set<gate_t> &f) {
341 return f.empty() || (f.size() == 1 && f.count(shared));
342 };
343 if (is_per_row(r.a, fa) && is_shared_side(fb)) {
344 per_row = r.a; shared_side = r.b;
345 } else if (is_per_row(r.b, fb) && is_shared_side(fa)) {
346 per_row = r.b; shared_side = r.a; op = flipOp(op);
347 } else {
348 return std::nullopt;
349 }
351 return std::nullopt;
352
353 auto spec = parse_distribution_spec(gc.getExtra(per_row));
354 if (!spec) return std::nullopt;
355 Term term{spec->family, spec->p1, spec->p2, shared_side, op,
356 r.bern, r.value};
357 term.dist = std::shared_ptr<Distribution>(
358 spec->family->factory(spec->p1, spec->p2));
359 if (auto aff = affineInVar(gc, shared_side, shared)) {
360 term.affine = true;
361 term.slope = aff->first;
362 term.intercept = aff->second;
363 }
364 terms.push_back(std::move(term));
365 }
366
367 CollapsePlan plan;
368 plan.aop = aop;
369 plan.shared = shared;
370 plan.shared_dist = std::move(shared_dist);
371 plan.lo = lo;
372 plan.hi = hi;
373 plan.terms = std::move(terms);
374 return plan;
375}
376
377/// The trapezoidal quadrature grid over a latent's distribution, weighted by
378/// its pdf and renormalised to unit mass (removing discretisation bias).
379struct Grid { std::vector<double> node, weight; };
380std::optional<Grid> pdfGrid(const Distribution &d, double lo, double hi)
381{
382 if (!(hi > lo)) return std::nullopt;
383 const double dx = (hi - lo) / (kGrid - 1);
384 Grid grid;
385 grid.node.resize(kGrid);
386 grid.weight.resize(kGrid);
387 double wsum = 0.0;
388 for (int gi = 0; gi < kGrid; ++gi) {
389 grid.node[gi] = lo + gi * dx;
390 double w = d.pdf(grid.node[gi]);
391 if (std::isnan(w) || w < 0.0) w = 0.0;
392 if (gi == 0 || gi == kGrid - 1) w *= 0.5;
393 grid.weight[gi] = w;
394 wsum += w;
395 }
396 if (!(wsum > 0.0)) return std::nullopt;
397 for (double &w : grid.weight) w /= wsum;
398 return grid;
399}
400
401/// Per-row success probabilities q_i(b) at a shared-latent value @p b.
402/// Returns false if any term's link expression / CDF is undefined.
403bool perNodeProbs(const GenericCircuit &gc, const CollapsePlan &plan,
404 double b, std::vector<double> &q)
405{
406 q.clear();
407 q.reserve(plan.terms.size());
408 for (const auto &t : plan.terms) {
409 // Affine links resolve with no gate access; the rest keep the walk.
410 const double c = t.affine ? std::fma(t.slope, b, t.intercept)
411 : evalWithVar(gc, t.shared_side, plan.shared, b);
412 if (std::isnan(c)) return false;
413 double F = t.dist->cdf(c);
414 if (std::isnan(F)) return false;
415 if (F < 0.0) F = 0.0; else if (F > 1.0) F = 1.0;
416 double qi;
417 switch (t.op) {
418 case ComparisonOperator::LT:
419 case ComparisonOperator::LE: qi = F; break;
420 case ComparisonOperator::GT:
421 case ComparisonOperator::GE: qi = 1.0 - F; break;
422 default: return false;
423 }
424 qi *= t.bern;
425 if (qi < 0.0) qi = 0.0; else if (qi > 1.0) qi = 1.0;
426 q.push_back(qi);
427 }
428 return true;
429}
430
431/// Poisson-binomial pmf of Σ Bernoulli(q_i) on {0, ..., q.size()}.
432std::vector<double> poissonBinomialPmf(const std::vector<double> &q)
433{
434 std::vector<double> pmf(q.size() + 1, 0.0);
435 pmf[0] = 1.0;
436 std::size_t filled = 0;
437 for (double qi : q) {
438 // Fold in Bernoulli(qi): pmf'[j] = pmf[j](1-qi) + pmf[j-1] qi, updated in
439 // place descending so each pmf[j-1] is still the pre-fold value.
440 for (std::size_t j = filled + 1; j >= 1; --j)
441 pmf[j] = pmf[j] * (1.0 - qi) + pmf[j - 1] * qi;
442 pmf[0] *= (1.0 - qi);
443 ++filled;
444 }
445 return pmf;
446}
447
448/// The count pmf P(C=j) via the collapse: 1-D quadrature over the shared
449/// latent of the conditional Poisson-binomial. COUNT only (unit per-row
450/// values), so the sum is a plain count.
451std::optional<std::vector<double>>
452aggCollapsedPmf(const GenericCircuit &gc, gate_t agg)
453{
454 auto plan = buildCollapsePlan(gc, agg);
455 if (!plan) return std::nullopt;
456 // A count is a COUNT agg, or equivalently a SUM whose per-row values are all
457 // 1 (the count-lift shape). Either way the per-tuple contribution is 1, so
458 // the Poisson-binomial pmf is the count distribution.
459 for (const auto &t : plan->terms)
460 if (t.value != 1.0) return std::nullopt;
461
462 auto grid = pdfGrid(*plan->shared_dist, plan->lo, plan->hi);
463 if (!grid) return std::nullopt;
464
465 const std::size_t n = plan->terms.size();
466 std::vector<double> pmf(n + 1, 0.0);
467 std::vector<double> q;
468 for (int gi = 0; gi < kGrid; ++gi) {
469 if (!perNodeProbs(gc, *plan, grid->node[gi], q)) return std::nullopt;
470 const std::vector<double> pb = poissonBinomialPmf(q);
471 const double w = grid->weight[gi];
472 for (std::size_t j = 0; j <= n; ++j) pmf[j] += w * pb[j];
473 }
474 return pmf;
475}
476
477} // namespace
478
479std::optional<std::pair<double, double>>
481{
482 auto plan = buildCollapsePlan(gc, agg);
483 if (!plan) return std::nullopt;
484
485 auto grid = pdfGrid(*plan->shared_dist, plan->lo, plan->hi);
486 if (!grid) return std::nullopt;
487
488 double m1 = 0.0, m2 = 0.0;
489 std::vector<double> q;
490 for (int gi = 0; gi < kGrid; ++gi) {
491 if (!perNodeProbs(gc, *plan, grid->node[gi], q)) return std::nullopt;
492 double mean_b = 0.0, var_b = 0.0; // E[C|b], Var[C|b] (independent given b)
493 for (std::size_t i = 0; i < plan->terms.size(); ++i) {
494 const double v = plan->terms[i].value, qi = q[i];
495 mean_b += v * qi;
496 var_b += v * v * qi * (1.0 - qi);
497 }
498 const double w = grid->weight[gi];
499 m1 += w * mean_b;
500 m2 += w * (var_b + mean_b * mean_b);
501 }
502 return std::make_pair(m1, m2);
503}
504
505std::optional<double>
506aggCollapsedRawMoment(const GenericCircuit &gc, gate_t agg, unsigned k)
507{
508 if (k == 0) return 1.0;
509 if (k > 2) return std::nullopt;
510 // Both moments share the whole load / plan / grid pass; variance() consumes
511 // them together through agg_collapsed_moments so a readout that needs E[C]
512 // and E[C^2] pays for one traversal, not two.
513 auto m = aggCollapsedRawMoments(gc, agg);
514 if (!m) return std::nullopt;
515 return (k == 1) ? m->first : m->second;
516}
517
518std::optional<double>
520 gate_t event, unsigned k)
521{
522 if (k == 0) return 1.0;
523 if (k > 2) return std::nullopt;
524 if (gc.getGateType(target) != gate_rv) return std::nullopt;
525 if (gc.getGateType(event) != gate_cmp) return std::nullopt;
526
527 bool ok = false;
528 const ComparisonOperator eop = cmpOpFromOid(gc.getInfos(event).first, ok);
529 if (!ok || eop != ComparisonOperator::EQ) return std::nullopt;
530 const auto &ew = gc.getWires(event);
531 if (ew.size() != 2) return std::nullopt;
532
533 // Identify the discrete rv Y(target) side and the count agg C side.
534 gate_t ygate = static_cast<gate_t>(0), cagg = static_cast<gate_t>(0);
535 for (int i = 0; i < 2; ++i) {
536 const gate_t s = ew[i], o = ew[1 - i];
537 if (gc.getGateType(s) == gate_rv && gc.getGateType(o) == gate_agg) {
538 ygate = s; cagg = o; break;
539 }
540 }
541 if (ygate == static_cast<gate_t>(0)) return std::nullopt;
542
543 // Y must be a discrete family whose parameter subtree references target.
544 auto ytmpl = parse_distribution_template(gc.getExtra(ygate));
545 if (!ytmpl) return std::nullopt;
546 if (!ytmpl->family->factory(1.0, 1.0)->isDiscrete()) return std::nullopt;
547 // Y's parameter subtrees are its wires; descend into them (collectRvLeaves
548 // stops at the gate_rv Y itself) to confirm the parameter references target.
549 std::unordered_set<gate_t> yfoot;
550 for (gate_t w : gc.getWires(ygate)) collectRvLeaves(gc, w, yfoot);
551 if (!yfoot.count(target)) return std::nullopt;
552
553 // The correlated count's pmf via the collapse.
554 auto Cpmf = aggCollapsedPmf(gc, cagg);
555 if (!Cpmf) return std::nullopt;
556
557 // R's prior and quadrature window.
558 auto rspec = parse_distribution_spec(gc.getExtra(target));
559 if (!rspec) return std::nullopt;
560 auto rdist = makeDistribution(*rspec);
561 double rlo, rhi;
562 if (!rdist->integrationRange(rlo, rhi)) return std::nullopt;
563 auto rgrid = pdfGrid(*rdist, rlo, rhi);
564 if (!rgrid) return std::nullopt;
565
566 const auto &ywires = gc.getWires(ygate);
567 auto resolveParam = [&](const DistributionParam &p, double r) -> double {
568 if (p.wire_slot < 0) return p.literal;
569 if (static_cast<std::size_t>(p.wire_slot) >= ywires.size()) return kNaN;
570 return evalWithVar(gc, ywires[p.wire_slot], target, r);
571 };
572
573 // Posterior quadrature over R: E[R^k|C] = ∫ r^k f_R L / ∫ f_R L,
574 // L(r) = Σ_j P(C=j) pmf_Y(j; θ(r)).
575 const std::size_t J = Cpmf->size();
576 double Z = 0.0, N1 = 0.0, N2 = 0.0;
577 for (int gi = 0; gi < kGrid; ++gi) {
578 const double r = rgrid->node[gi];
579 const double p1v = resolveParam(ytmpl->p1, r);
580 const double p2v = resolveParam(ytmpl->p2, r);
581 if (std::isnan(p1v)) return std::nullopt;
582 auto ydist = ytmpl->family->factory(p1v, std::isnan(p2v) ? 0.0 : p2v);
583 double L = 0.0;
584 bool degenerate = false;
585 for (std::size_t j = 0; j < J; ++j) {
586 const double pj = (*Cpmf)[j];
587 if (pj <= 0.0) continue;
588 const double pmfy = ydist->pdf(static_cast<double>(j));
589 if (std::isnan(pmfy)) { degenerate = true; break; }
590 L += pj * pmfy;
591 }
592 // A degenerate Y at this node (e.g. a boundary parameter such as
593 // Poisson(0) at R = 0) contributes nothing: such nodes sit at the
594 // prior's support edge, where f_R already vanishes, so skipping them
595 // leaves the quadrature exact.
596 if (degenerate) continue;
597 const double w = rgrid->weight[gi];
598 Z += w * L;
599 N1 += w * r * L;
600 N2 += w * r * r * L;
601 }
602 if (!(Z > 0.0)) return std::nullopt;
603 return (k == 1) ? (N1 / Z) : (N2 / Z);
604}
605
606} // namespace provsql
607
608extern "C" {
609
610PG_FUNCTION_INFO_V1(agg_collapsed_moment);
611
612/**
613 * @brief SQL: agg_collapsed_moment(token uuid, k integer) -> float8
614 *
615 * The collapsed raw moment @c E[C^k] of a correlated COUNT / SUM, or @c NULL
616 * when the circuit does not match the recognised shared-latent pattern (the
617 * caller then falls back to the exact enumeration). @c k in @c {1, 2}.
618 */
619Datum agg_collapsed_moment(PG_FUNCTION_ARGS)
620{
621 try {
622 pg_uuid_t *token = PG_GETARG_UUID_P(0);
623 const int32 k = PG_GETARG_INT32(1);
624 if (k < 0)
625 provsql_error("agg_collapsed_moment: k must be non-negative (got %d)", k);
626 auto gc = getGenericCircuit(*token);
627 gate_t root = gc.getGate(uuid2string(*token));
628 auto r = provsql::aggCollapsedRawMoment(gc, root,
629 static_cast<unsigned>(k));
630 if (!r.has_value())
631 PG_RETURN_NULL();
632 return Float8GetDatum(*r);
633 } catch (const std::exception &e) {
634 provsql_error("agg_collapsed_moment: %s", e.what());
635 } catch (...) {
636 provsql_error("agg_collapsed_moment: unknown exception");
637 }
638 PG_RETURN_NULL();
639}
640
641PG_FUNCTION_INFO_V1(agg_collapsed_moments);
642
643/**
644 * @brief SQL: agg_collapsed_moments(token uuid) -> float8[] {E[C], E[C^2]}
645 *
646 * Both collapsed raw moments from a single circuit load and plan build, or
647 * @c NULL when the shared-latent pattern does not match. @c variance() calls
648 * this so a mean+variance readout traverses the circuit once, not twice (the
649 * load and plan build dominate once the grid loop is arithmetic-only).
650 */
651Datum agg_collapsed_moments(PG_FUNCTION_ARGS)
652{
653 try {
654 pg_uuid_t *token = PG_GETARG_UUID_P(0);
655 auto gc = getGenericCircuit(*token);
656 gate_t root = gc.getGate(uuid2string(*token));
657 auto m = provsql::aggCollapsedRawMoments(gc, root);
658 if (!m.has_value())
659 PG_RETURN_NULL();
660 Datum elems[2] = { Float8GetDatum(m->first), Float8GetDatum(m->second) };
661 ArrayType *arr = construct_array(elems, 2, FLOAT8OID,
662 sizeof(float8), FLOAT8PASSBYVAL, 'd');
663 PG_RETURN_ARRAYTYPE_P(arr);
664 } catch (const std::exception &e) {
665 provsql_error("agg_collapsed_moments: %s", e.what());
666 } catch (...) {
667 provsql_error("agg_collapsed_moments: unknown exception");
668 }
669 PG_RETURN_NULL();
670}
671
672} // extern "C"
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
AggregationOperator getAggregationOperator(Oid oid)
Map a PostgreSQL aggregate function OID to an AggregationOperator.
Typed aggregation value, operator, and aggregator abstractions.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
@ COUNT
COUNT(*) or COUNT(expr) → integer.
Definition Aggregation.h:52
@ SUM
SUM → integer or float.
Definition Aggregation.h:53
ComparisonOperator
SQL comparison operators used in gate_cmp circuit gates.
Definition Aggregation.h:39
@ LT
Less than (<).
Definition Aggregation.h:43
@ GT
Greater than (>).
Definition Aggregation.h:45
@ LE
Less than or equal (<=).
Definition Aggregation.h:42
@ NE
Not equal (<>).
Definition Aggregation.h:41
@ GE
Greater than or equal (>=).
Definition Aggregation.h:44
GenericCircuit getGenericCircuit(pg_uuid_t token)
Build a GenericCircuit from the mmap store rooted at token.
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 agg_collapsed_moments(PG_FUNCTION_ARGS)
SQL: agg_collapsed_moments(token uuid) -> float8[] {E[C], E[C^2]}.
Datum agg_collapsed_moment(PG_FUNCTION_ARGS)
SQL: agg_collapsed_moment(token uuid, k integer) -> float8.
Rao-Blackwellised (collapsed) evaluation of a correlated COUNT / SUM and of a latent conditioned on s...
Per-family polymorphic view over a continuous gate_rv distribution (§F.1 class hierarchy).
Continuous random-variable helpers (distribution parsing, moments).
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
gate_t getGate(const uuid &u)
Return (or create) the gate associated with UUID u.
Definition Circuit.hpp:33
In-memory provenance circuit with semiring-generic evaluation.
std::string getExtra(gate_t g) const
Return the string extra for gate g.
double getProb(gate_t g) const
Return the probability for gate g.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
Abstract per-family continuous distribution.
std::optional< double > aggCollapsedRawMoment(const GenericCircuit &gc, gate_t agg, unsigned k)
Collapsed raw moment E[C^k] of a correlated COUNT / SUM agg, or std::nullopt when the circuit does no...
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
std::optional< std::pair< double, double > > aggCollapsedRawMoments(const GenericCircuit &gc, gate_t agg)
std::unique_ptr< Distribution > makeDistribution(const DistributionSpec &spec)
Construct the per-family Distribution for a parsed spec.
std::optional< double > collapsedConditionalMoment(const GenericCircuit &gc, gate_t target, gate_t event, unsigned k)
Collapsed exact posterior raw moment E[R^k | Y = C] for a latent target R conditioned (through the eq...
std::optional< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
std::optional< DistributionTemplate > parse_distribution_template(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution, keeping wired (token) parameters as wire r...
constexpr double kNaN
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
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_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
string uuid2string(pg_uuid_t uuid)
Format a pg_uuid_t as a std::string.
C++ utility functions for UUID manipulation.
UUID structure.
One parameter slot of a gate_rv, either a literal or a wire.
double literal
Value when wire_slot < 0.
int wire_slot
>= 0: resolve from the gate's wires[wire_slot].