ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
AggMarginalEvaluator.cpp
Go to the documentation of this file.
1/**
2 * @file AggMarginalEvaluator.cpp
3 * @brief Implementation of the safe-join aggregate marginal-vector pre-pass
4 * (COUNT / SUM / MIN / MAX). See @c AggMarginalEvaluator.h for the
5 * full docstring and the soundness argument.
6 */
8
9#include <algorithm>
10#include <climits>
11#include <cmath>
12#include <cstdint>
13#include <map>
14#include <numeric>
15#include <set>
16#include <type_traits>
17#include <vector>
18
19#include "Aggregation.h" // AggregationOperator + ComparisonOperator
20#include "CmpEvaluatorCommon.h" // matchAggCmp, computeRefCounts
21#include "RandomVariable.h" // parseDoubleStrict
22
23extern "C" {
24#include "provsql_utils.h" // gate_type enum
25}
26
27namespace provsql {
28
29namespace {
30
31/* ------------------------------------------------------------------ *
32 * Contributor parsing
33 * ------------------------------------------------------------------ *
34 * A contributor (the K side of a semimod) is in scope iff it is a
35 * conjunction of @c gate_input leaves: a bare @c gate_input, a
36 * @c gate_one (deterministically-present, empty leaf set), or a
37 * @c gate_times -- recursively, so a *nested* product
38 * @c times(times(r,s),t) (e.g. an SPJ subquery / view whose tuple
39 * provenance feeds an outer join) flattens to the same leaf set
40 * @c {r,s,t} as the flat @c times(r,s,t). This is sound on the
41 * probability path: @c times is logical AND there, so it is
42 * associative and the nesting does not change the conjunction's
43 * probability (the non-commutativity of @c times matters only to the
44 * symbolic semirings, which this pass never touches). Any other shape
45 * (gate_plus from a UNION, gate_monus, gate_mulinput) makes the cmp
46 * bail. A leaf repeated within the contributor also bails (the
47 * read-once-within check below), since p^2 != p.
48 *
49 * On success @p out holds the contributor's leaf set (sorted, unique).
50 */
51static bool collectProductLeaves(GenericCircuit &gc, gate_t k,
52 std::vector<gate_t> &out)
53{
54 switch (gc.getGateType(k)) {
55 case gate_one:
56 return true; /* identity factor: contributes nothing */
57 case gate_input:
58 out.push_back(k);
59 return true;
60 case gate_times:
61 for (gate_t c : gc.getWires(k))
62 if (!collectProductLeaves(gc, c, out)) return false;
63 return true;
64 default:
65 return false; /* non-product factor: out of scope */
66 }
67}
68
69static bool parseProductContributor(GenericCircuit &gc, gate_t k,
70 std::vector<gate_t> &out)
71{
72 out.clear();
73 if (!collectProductLeaves(gc, k, out))
74 return false;
75 /* Read-once within the contributor: a leaf used twice would make the
76 * product probability wrong (p^2 vs the leaf's single mass). */
77 std::sort(out.begin(), out.end());
78 if (std::adjacent_find(out.begin(), out.end()) != out.end())
79 return false;
80 return true;
81}
82
83/* ------------------------------------------------------------------ *
84 * Privacy of the aggregate subtree
85 * ------------------------------------------------------------------ *
86 * The cmp may be resolved to an independent Bernoulli only if all the
87 * randomness it depends on is private to its own subtree -- i.e. no
88 * gate reachable from the @c gate_agg is also referenced from elsewhere
89 * in the circuit (which would couple the cmp's outcome to that other
90 * use). Walk the subtree rooted at @p agg and require, for every
91 * non-constant gate in it, that its whole-circuit reference count
92 * equals the number of references it receives from *within* the
93 * subtree. This subsumes (and generalises to nested / shared product
94 * gates) the per-leaf @c ref==cnt and per-semimod @c ref==1 checks: a
95 * subquery tuple's @c times(r,s) shared across several contributors is
96 * internal (its internal ref count matches its total), so it passes;
97 * any escape to an outside parent fails. Constants (@c gate_one /
98 * @c gate_zero / @c gate_value) carry no randomness and may be shared
99 * freely, so they are exempt. The caller separately requires
100 * @c ref[agg]==1 (the agg is consumed by this cmp alone). */
101static bool aggSubtreePrivate(GenericCircuit &gc, gate_t agg,
102 const std::vector<unsigned> &ref)
103{
104 std::map<gate_t, unsigned> internalRef;
105 std::set<gate_t> visited;
106 std::vector<gate_t> stk{agg};
107 visited.insert(agg);
108 while (!stk.empty()) {
109 gate_t g = stk.back(); stk.pop_back();
110 for (gate_t c : gc.getWires(g)) {
111 ++internalRef[c];
112 if (visited.insert(c).second) stk.push_back(c);
113 }
114 }
115 for (gate_t g : visited) {
116 if (g == agg) continue;
117 switch (gc.getGateType(g)) {
118 case gate_one: case gate_zero: case gate_value:
119 continue; /* constants: sharing is harmless */
120 default:
121 break;
122 }
123 if (ref[static_cast<std::size_t>(g)] != internalRef[g])
124 return false; /* referenced from outside the subtree */
125 }
126 return true;
127}
128
129/* Brute-force leaf cap for the exact private-contributor marginal. */
130constexpr unsigned kMaxContributorLeaves = 20;
131
132/* Exact marginal probability of a contributor (a semimod K side) that is a
133 * *private* Boolean sub-circuit over @c input leaves -- @c plus / @c times /
134 * @c monus and the @c one / @c zero constants -- even when it is *not*
135 * read-once internally. This is the UNION / EXCEPT-over-a-shared-base-tuple
136 * shape: a contributor @c (r∧s)∨(r∧t) or @c (r∧s)∖(r∧t) repeats the joined
137 * leaf @c r, which @c contributorProb (read-once only) rejects.
138 *
139 * "Private" means every gate in the cone below the root is referenced only
140 * from within the cone (whole-circuit @c ref == the cone-internal reference
141 * count). That single condition gives independence from every *other*
142 * contributor (their footprints are disjoint -- a shared leaf would have an
143 * external reference), so the contributor is an independent event whose exact
144 * probability the caller can treat as a one-alternative BID block. Computed
145 * by brute force over the cone's distinct inputs (the internal sharing is
146 * resolved exactly; capped at @c kMaxContributorLeaves). Returns false --
147 * caller bails to enumeration -- when the cone is not private (shared with
148 * another contributor: the genuinely #P-hard case), too large, or holds an
149 * unsupported gate. */
150static bool contributorExactMarginal(GenericCircuit &gc, gate_t g,
151 const std::vector<unsigned> &ref,
152 double &out)
153{
154 /* Iterative post-order over the cone; count cone-internal references. */
155 std::map<gate_t, unsigned> internalRef;
156 std::set<gate_t> seen;
157 std::vector<gate_t> order; /* children before parents */
158 std::vector<std::pair<gate_t, bool>> stk{{g, false}};
159 while (!stk.empty()) {
160 auto top = stk.back(); stk.pop_back();
161 gate_t x = top.first;
162 const auto t = gc.getGateType(x);
163 if (t != gate_one && t != gate_zero && t != gate_input &&
164 t != gate_times && t != gate_plus && t != gate_monus)
165 return false; /* unsupported gate in cone */
166 if (top.second) { order.push_back(x); continue; }
167 if (!seen.insert(x).second) continue;
168 stk.push_back({x, true});
169 if (t == gate_times || t == gate_plus || t == gate_monus)
170 for (gate_t c : gc.getWires(x)) {
171 ++internalRef[c];
172 stk.push_back({c, false});
173 }
174 }
175
176 /* Privacy: every non-constant cone gate but the root used only inside. */
177 for (gate_t x : seen) {
178 if (x == g) continue; /* root: ref checked by caller */
179 switch (gc.getGateType(x)) {
180 case gate_one: case gate_zero: continue;
181 default: break;
182 }
183 if (ref[static_cast<std::size_t>(x)] != internalRef[x]) return false;
184 }
185
186 /* Compact, pre-resolved representation for the inner loop. */
187 const int N = static_cast<int>(order.size());
188 std::map<gate_t, int> pos;
189 for (int i = 0; i < N; ++i) pos[order[i]] = i;
190 std::vector<gate_type> typ(N);
191 std::vector<std::vector<int>> childIdx(N);
192 std::vector<int> leafbit(N, -1);
193 std::vector<double> leafProb;
194 for (int i = 0; i < N; ++i) {
195 typ[i] = gc.getGateType(order[i]);
196 if (typ[i] == gate_input) {
197 leafbit[i] = static_cast<int>(leafProb.size());
198 leafProb.push_back(gc.getProb(order[i]));
199 } else {
200 for (gate_t c : gc.getWires(order[i])) childIdx[i].push_back(pos[c]);
201 }
202 }
203 const unsigned m = static_cast<unsigned>(leafProb.size());
204 if (m > kMaxContributorLeaves) return false;
205
206 /* Σ over assignments where the root is true of ∏ leaf marginals. */
207 std::vector<char> val(N);
208 double total = 0.0;
209 for (uint32_t mask = 0; mask < (1u << m); ++mask) {
210 for (int i = 0; i < N; ++i) {
211 switch (typ[i]) {
212 case gate_one: val[i] = 1; break;
213 case gate_zero: val[i] = 0; break;
214 case gate_input: val[i] = (mask >> leafbit[i]) & 1u; break;
215 case gate_times: { char v = 1; for (int c : childIdx[i]) v = v && val[c]; val[i] = v; break; }
216 case gate_plus: { char v = 0; for (int c : childIdx[i]) v = v || val[c]; val[i] = v; break; }
217 case gate_monus: val[i] = val[childIdx[i][0]] && !val[childIdx[i][1]]; break;
218 default: return false;
219 }
220 }
221 if (val[N - 1]) { /* root is last in post-order */
222 double pr = 1.0;
223 for (unsigned b = 0; b < m; ++b)
224 pr *= (mask >> b) & 1u ? leafProb[b] : 1.0 - leafProb[b];
225 total += pr;
226 }
227 }
228 out = total;
229 return true;
230}
231
232/* Disjoint-set forest over contributor indices, union by shared leaf. */
233struct UnionFind {
234 std::vector<int> parent;
235 explicit UnionFind(int n) : parent(n) {
236 std::iota(parent.begin(), parent.end(), 0);
237 }
238 int find(int x) {
239 while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
240 return x;
241 }
242 void unite(int a, int b) { parent[find(a)] = find(b); }
243};
244
245/* Partition contributor indices into independence blocks: two
246 * contributors are in the same block iff they (transitively) share a
247 * leaf. Independent blocks are combined by the aggregate's monoid; the
248 * sharing inside a block is resolved by recursion. */
249static std::vector<std::vector<int>> independenceBlocks(
250 const std::vector<std::vector<gate_t>> &contribs)
251{
252 const std::size_t n = contribs.size();
253 UnionFind uf(static_cast<int>(n));
254 std::map<gate_t, int> first_owner;
255 for (std::size_t i = 0; i < n; ++i)
256 for (gate_t l : contribs[i]) {
257 auto it = first_owner.find(l);
258 if (it == first_owner.end()) first_owner[l] = static_cast<int>(i);
259 else uf.unite(static_cast<int>(i), it->second);
260 }
261 std::map<int, std::vector<int>> bmap;
262 for (std::size_t i = 0; i < n; ++i)
263 bmap[uf.find(static_cast<int>(i))].push_back(static_cast<int>(i));
264 std::vector<std::vector<int>> blocks;
265 blocks.reserve(bmap.size());
266 for (auto &kv : bmap) blocks.push_back(std::move(kv.second));
267 return blocks;
268}
269
270/* Leaves common to *every* member of a block (this hierarchy level's
271 * shared root event); empty when the members have no leaf in common,
272 * which marks a non-laminar (non-hierarchical) structure. */
273static std::vector<gate_t> commonLeaves(
274 const std::vector<std::vector<gate_t>> &contribs,
275 const std::vector<int> &members)
276{
277 std::vector<gate_t> common = contribs[members[0]];
278 for (std::size_t mi = 1; mi < members.size() && !common.empty(); ++mi) {
279 std::vector<gate_t> tmp;
280 std::set_intersection(common.begin(), common.end(),
281 contribs[members[mi]].begin(),
282 contribs[members[mi]].end(),
283 std::back_inserter(tmp));
284 common.swap(tmp);
285 }
286 return common;
287}
288
289/* Per-member residual leaf sets after removing this level's common root
290 * leaves -- the structure one hierarchy level deeper. */
291static std::vector<std::vector<gate_t>> residualsOf(
292 const std::vector<std::vector<gate_t>> &contribs,
293 const std::vector<int> &members, const std::vector<gate_t> &common)
294{
295 std::vector<std::vector<gate_t>> residuals;
296 residuals.reserve(members.size());
297 for (int m : members) {
298 std::vector<gate_t> r;
299 std::set_difference(contribs[m].begin(), contribs[m].end(),
300 common.begin(), common.end(), std::back_inserter(r));
301 residuals.push_back(std::move(r));
302 }
303 return residuals;
304}
305
306/* Convolution of two count PMFs (independent sum of the two counts). */
307static std::vector<double> convolve(const std::vector<double> &a,
308 const std::vector<double> &b)
309{
310 if (a.empty()) return b;
311 if (b.empty()) return a;
312 std::vector<double> r(a.size() + b.size() - 1, 0.0);
313 for (std::size_t i = 0; i < a.size(); ++i) {
314 if (a[i] == 0.0) continue;
315 for (std::size_t j = 0; j < b.size(); ++j)
316 r[i + j] += a[i] * b[j];
317 }
318 return r;
319}
320
321/* Distribution of the product of two independent non-negative integer
322 * counts: r[a*b] += A[a]*B[b]. Combines the per-factor count PMFs of a
323 * Cartesian-product block (count = N_1 · N_2 · ...). */
324static std::vector<double> productConvolve(const std::vector<double> &a,
325 const std::vector<double> &b)
326{
327 if (a.empty() || b.empty()) return {};
328 const std::size_t amax = a.size() - 1, bmax = b.size() - 1;
329 std::vector<double> r(amax * bmax + 1, 0.0);
330 for (std::size_t i = 0; i <= amax; ++i) {
331 if (a[i] == 0.0) continue;
332 for (std::size_t j = 0; j <= bmax; ++j)
333 r[i * j] += a[i] * b[j];
334 }
335 return r;
336}
337
338/* Result of a Cartesian-product decomposition (see @c decomposeProduct). */
339struct ProductDecomp {
340 bool ok = false; /* a complete leaf-disjoint product? */
341 std::map<gate_t, int> leafFactor; /* leaf -> factor index */
342 std::vector<std::vector<std::vector<gate_t>>> parts; /* per factor: distinct parts */
343};
344
345/* Try to decompose a connected, common-less block into independent
346 * Cartesian-product factors. Two leaves share a factor iff they NEVER
347 * co-occur in a contributor (united below); the factors are those
348 * classes. On success (@c ok) the block's contributors are exactly the
349 * complete Cartesian product of the per-factor distinct parts, so the
350 * block count is the product of the per-factor counts. @c ok is false
351 * when the block is not a complete leaf-disjoint product.
352 *
353 * This is what separates the safe cross-product (R(a),S(a,b),T(a,c) →
354 * count = N_S·N_T) from the #P-hard h0 / triangle: h0 carries a private
355 * "middle" leaf (the S(x,y) tuple, in exactly one contributor) that makes
356 * leaves of different branches never co-occur, collapsing the factor
357 * partition to one class and/or breaking |contributors| = ∏|parts|. The
358 * cross-product has no middle relation, so its branch leaves always
359 * co-occur (completeness) and stay in separate factors.
360 *
361 * Soundness is a circuit-level fact independent of the query: a complete
362 * leaf-disjoint product means each contributor is one part per factor,
363 * present iff all its parts are; parts of distinct factors are
364 * leaf-disjoint hence independent, so count = ∏ N_i exactly. */
365static ProductDecomp decomposeProduct(
366 const std::vector<std::vector<gate_t>> &contribs,
367 const std::vector<int> &members)
368{
369 ProductDecomp out;
370
371 /* Index the block's leaves. */
372 std::vector<gate_t> L;
373 for (int m : members) for (gate_t l : contribs[m]) L.push_back(l);
374 std::sort(L.begin(), L.end());
375 L.erase(std::unique(L.begin(), L.end()), L.end());
376 std::map<gate_t, int> idx;
377 for (std::size_t i = 0; i < L.size(); ++i) idx[L[i]] = static_cast<int>(i);
378 const int nl = static_cast<int>(L.size());
379
380 /* Co-occurrence: cooc[u][v] iff some member contains both leaves. */
381 std::vector<std::vector<char>> cooc(nl, std::vector<char>(nl, 0));
382 for (int m : members) {
383 const auto &cl = contribs[m];
384 for (std::size_t i = 0; i < cl.size(); ++i)
385 for (std::size_t j = i + 1; j < cl.size(); ++j) {
386 int a = idx[cl[i]], b = idx[cl[j]];
387 cooc[a][b] = cooc[b][a] = 1;
388 }
389 }
390
391 /* Factors = connected components under "never co-occur". */
392 UnionFind uf(nl);
393 for (int u = 0; u < nl; ++u)
394 for (int v = u + 1; v < nl; ++v)
395 if (!cooc[u][v]) uf.unite(u, v);
396 std::map<int, int> factorId;
397 for (int u = 0; u < nl; ++u)
398 factorId.emplace(uf.find(u), static_cast<int>(factorId.size()));
399 const int nf = static_cast<int>(factorId.size());
400 if (nf < 2) return out; /* single class: not a product */
401
402 for (gate_t l : L) out.leafFactor[l] = factorId[uf.find(idx[l])];
403
404 /* Project each member onto each factor; collect distinct parts. A
405 * member missing a factor is not a clean product. */
406 std::vector<std::set<std::vector<gate_t>>> parts(nf);
407 for (int m : members) {
408 std::vector<std::vector<gate_t>> proj(nf);
409 for (gate_t l : contribs[m]) /* member leaves are sorted */
410 proj[out.leafFactor[l]].push_back(l);
411 for (int f = 0; f < nf; ++f) {
412 if (proj[f].empty()) return out;
413 parts[f].insert(std::move(proj[f]));
414 }
415 }
416
417 /* Completeness: |contributors| == product of per-factor part counts.
418 * With the projection map injective (member = union of its parts), this
419 * forces a bijection onto the full Cartesian product. */
420 std::size_t prod = 1;
421 for (int f = 0; f < nf; ++f) prod *= parts[f].size();
422 if (prod != members.size()) return out;
423
424 out.parts.resize(nf);
425 for (int f = 0; f < nf; ++f)
426 out.parts[f].assign(parts[f].begin(), parts[f].end());
427 out.ok = true;
428 return out;
429}
430
431/* Recursive count distribution over a set of product-of-leaves
432 * contributors coupled only through a laminar (hierarchical) leaf-sharing
433 * structure. Returns the PMF @c m[c] = Pr(exactly c contributors present),
434 * or clears @p ok (returning {}) when the sharing is non-laminar -- a
435 * multi-member independence block with no leaf common to every member
436 * (e.g. the triangle) -- which is outside the exact safe-plan class.
437 *
438 * This is the marginal-vector safe-plan engine, handling arbitrary
439 * hierarchical depth:
440 * - partition the contributors into independent blocks by shared leaf
441 * (union-find); independent blocks combine by convolution (the ⊛^+
442 * combinator);
443 * - a singleton block is a Bernoulli over the product of its leaves;
444 * - a multi-member block with a leaf common to EVERY member factors out
445 * that shared root event: the block count is the disjoint mixture
446 * (1-p_root)·δ_0 + p_root·inner (the ⊥ combinator), with @c inner the
447 * recursion on the per-member residual leaf sets (one level deeper);
448 * - a multi-member block with no common leaf is either a Cartesian
449 * product of independent factors (the join node: count = ∏ N_i,
450 * @c tryProductFactors + @c productConvolve) or a genuinely non-laminar
451 * tangle (h0 / triangle), which clears @p ok and bails.
452 * Each recursion strips at least the common leaves, so the total leaf
453 * count strictly decreases and the recursion terminates. Depth-1 fan-out
454 * is the case where every residual is a single leaf (inner becomes the
455 * Poisson-binomial); deeper nesting (e.g. orders→items under a user)
456 * recurses further. */
457static std::vector<double> countPMF(GenericCircuit &gc,
458 std::vector<std::vector<gate_t>> contribs,
459 bool &ok)
460{
461 const std::size_t n = contribs.size();
462 if (n == 0) return std::vector<double>{1.0}; /* δ_0 */
463
464 /* Independence blocks: contributors sharing any leaf are coupled. */
465 UnionFind uf(static_cast<int>(n));
466 {
467 std::map<gate_t, int> first_owner;
468 for (std::size_t i = 0; i < n; ++i)
469 for (gate_t l : contribs[i]) {
470 auto it = first_owner.find(l);
471 if (it == first_owner.end()) first_owner[l] = static_cast<int>(i);
472 else uf.unite(static_cast<int>(i), it->second);
473 }
474 }
475 std::map<int, std::vector<int>> blocks;
476 for (std::size_t i = 0; i < n; ++i)
477 blocks[uf.find(static_cast<int>(i))].push_back(static_cast<int>(i));
478
479 std::vector<double> total{1.0}; /* δ_0, convolution identity */
480 for (const auto &be : blocks) {
481 const std::vector<int> &members = be.second;
482 std::vector<double> blockPMF;
483
484 if (members.size() == 1) {
485 /* One contributor: present iff all its leaves are -- a Bernoulli
486 * over the product of the (independent) leaf marginals. */
487 double q = 1.0;
488 for (gate_t l : contribs[members[0]]) q *= gc.getProb(l);
489 blockPMF = std::vector<double>{1.0 - q, q};
490 } else {
491 std::vector<gate_t> common = commonLeaves(contribs, members);
492 if (!common.empty()) {
493 /* Laminar: factor this level's shared root (the ⊥ mixture) and
494 * recurse on the per-member residuals one level deeper. */
495 double p_root = 1.0;
496 for (gate_t l : common) p_root *= gc.getProb(l);
497 std::vector<double> inner =
498 countPMF(gc, residualsOf(contribs, members, common), ok);
499 if (!ok) return {};
500 blockPMF = std::move(inner);
501 for (double &x : blockPMF) x *= p_root; /* root-present arm */
502 blockPMF[0] += (1.0 - p_root); /* root-absent: count 0 */
503 } else {
504 /* No shared root: the block is either a Cartesian product of
505 * independent factors (the join node, count = ∏ N_i) or a
506 * genuinely non-laminar tangle (h0 / triangle).
507 * decomposeProduct distinguishes them on the circuit. */
508 ProductDecomp pd = decomposeProduct(contribs, members);
509 if (!pd.ok) { ok = false; return {}; } /* non-hierarchical */
510 std::vector<double> acc;
511 for (std::size_t f = 0; f < pd.parts.size(); ++f) {
512 std::vector<double> fp = countPMF(gc, std::move(pd.parts[f]), ok);
513 if (!ok) return {};
514 acc = (f == 0) ? std::move(fp) : productConvolve(acc, fp);
515 }
516 blockPMF = std::move(acc);
517 }
518 }
519 total = convolve(total, blockPMF);
520 }
521 return total;
522}
523
524/* Tail-sum over the final count PMF under SQL HAVING semantics: sum the
525 * mass of every count @c c with @c c >= 1 (empty group excluded) and
526 * @c c op C true. Mirrors CountCmpEvaluator::cdfForOperator exactly,
527 * but driven by the materialised PMF rather than a Poisson-binomial.
528 *
529 * For a scalar aggregation (@p is_scalar) the empty input is a real world
530 * (one row, count 0), so the sum starts at @c c = 0 and @c pmf[0] is
531 * included when @c 0 op C holds. */
532static double prFromPMF(const std::vector<double> &pmf,
533 ComparisonOperator op, long C, bool is_scalar)
534{
535 double pr = 0.0;
536 for (int c = is_scalar ? 0 : 1; c < static_cast<int>(pmf.size()); ++c) {
537 bool sat = false;
538 switch (op) {
539 case ComparisonOperator::GE: sat = (c >= C); break;
540 case ComparisonOperator::GT: sat = (c > C); break;
541 case ComparisonOperator::LE: sat = (c <= C); break;
542 case ComparisonOperator::LT: sat = (c < C); break;
543 case ComparisonOperator::EQ: sat = (c == C); break;
544 case ComparisonOperator::NE: sat = (c != C); break;
545 }
546 if (sat) pr += pmf[c];
547 }
548 return pr;
549}
550
551/* ------------------------------------------------------------------ *
552 * MIN / MAX
553 * ------------------------------------------------------------------ *
554 * P(every contributor's lineage is false) over a hierarchical set --
555 * the scalar version of countPMF[0]. Independent blocks multiply; a
556 * singleton block contributes (1 - product of its leaves); a
557 * multi-member block is absent iff its shared root is absent, or the
558 * root is present and all residuals are absent. Clears @p ok on a
559 * non-laminar block (no common leaf). Every MIN/MAX HAVING predicate
560 * reduces to a few calls of this on value-thresholded subsets, which is
561 * the hierarchical generalisation of MinMaxCmpEvaluator's @c qprod
562 * (product of @c 1-p_i over the matching independent children). */
563static double pAllAbsent(GenericCircuit &gc,
564 std::vector<std::vector<gate_t>> contribs, bool &ok)
565{
566 if (contribs.empty()) return 1.0;
567 double result = 1.0;
568 for (const auto &members : independenceBlocks(contribs)) {
569 double block_absent;
570 if (members.size() == 1) {
571 double q = 1.0;
572 for (gate_t l : contribs[members[0]]) q *= gc.getProb(l);
573 block_absent = 1.0 - q;
574 } else {
575 std::vector<gate_t> common = commonLeaves(contribs, members);
576 if (!common.empty()) {
577 double p_root = 1.0;
578 for (gate_t l : common) p_root *= gc.getProb(l);
579 double inner = pAllAbsent(gc, residualsOf(contribs, members, common), ok);
580 if (!ok) return 0.0;
581 block_absent = (1.0 - p_root) + p_root * inner;
582 } else {
583 /* Cartesian product: all contributors absent iff some factor is
584 * entirely absent. P = 1 - ∏_f (1 - pAllAbsent(factor_f)). (When
585 * a value-thresholded subset from minMaxProb is a sub-product this
586 * is exactly the right combine; a non-product subset fails
587 * decomposeProduct and bails, which is the sound action for the
588 * #P-hard bipartite case.) */
589 ProductDecomp pd = decomposeProduct(contribs, members);
590 if (!pd.ok) { ok = false; return 0.0; } /* non-hierarchical */
591 double prodPresent = 1.0;
592 for (auto &fparts : pd.parts) {
593 double fa = pAllAbsent(gc, fparts, ok);
594 if (!ok) return 0.0;
595 prodPresent *= (1.0 - fa);
596 }
597 block_absent = 1.0 - prodPresent;
598 }
599 }
600 result *= block_absent;
601 }
602 return result;
603}
604
605/* P(MIN/MAX(value) op C) over a hierarchical contributor set, empty
606 * group excluded (a group with no present contributor has no min/max).
607 * Each operator is a small combination of pAllAbsent over the subset of
608 * contributors whose value satisfies a threshold predicate -- exactly
609 * the decomposition in MinMaxCmpEvaluator, but with pAllAbsent in place
610 * of the independent-only qprod, so it is exact on safe joins too. */
611static double minMaxProb(GenericCircuit &gc,
612 const std::vector<std::vector<gate_t>> &leaves,
613 const std::vector<long> &vals,
614 const std::vector<std::vector<std::pair<long, double>>> &blocks,
616 long C, bool &ok)
617{
618 /* P(all contributors whose value satisfies @p pred are absent). The TID
619 * part goes through the hierarchical pAllAbsent; each independent BID block
620 * contributes (1 - Σ_{alt: pred} p_alt) -- the probability its (single)
621 * present alternative is not one whose value satisfies @p pred (mutual
622 * exclusion: the matching subset is all-absent iff the chosen one, if any,
623 * lies outside it). */
624 auto pAbsentWhere = [&](auto pred) -> double {
625 std::vector<std::vector<gate_t>> sub;
626 for (std::size_t i = 0; i < leaves.size(); ++i)
627 if (pred(vals[i])) sub.push_back(leaves[i]);
628 double r = pAllAbsent(gc, std::move(sub), ok);
629 for (const auto &blk : blocks) {
630 double s = 0.0;
631 for (const auto &alt : blk) if (pred(alt.first)) s += alt.second;
632 r *= 1.0 - (s > 1.0 ? 1.0 : s);
633 }
634 return r;
635 };
636
637 const double allAbsent = pAbsentWhere([](int) { return true; });
638 double pr = 0.0;
639
640 if (agg == AggregationOperator::MAX) {
641 switch (op) {
642 case ComparisonOperator::GE: pr = 1.0 - pAbsentWhere([&](long v){return v >= C;}); break;
643 case ComparisonOperator::GT: pr = 1.0 - pAbsentWhere([&](long v){return v > C;}); break;
644 case ComparisonOperator::LE: pr = pAbsentWhere([&](long v){return v > C;}) - allAbsent; break;
645 case ComparisonOperator::LT: pr = pAbsentWhere([&](long v){return v >= C;}) - allAbsent; break;
646 case ComparisonOperator::EQ: pr = pAbsentWhere([&](long v){return v > C;})
647 - pAbsentWhere([&](long v){return v >= C;}); break;
648 case ComparisonOperator::NE: pr = (1.0 - allAbsent)
649 - (pAbsentWhere([&](long v){return v > C;})
650 - pAbsentWhere([&](long v){return v >= C;})); break;
651 }
652 } else { /* MIN */
653 switch (op) {
654 case ComparisonOperator::LE: pr = 1.0 - pAbsentWhere([&](long v){return v <= C;}); break;
655 case ComparisonOperator::LT: pr = 1.0 - pAbsentWhere([&](long v){return v < C;}); break;
656 case ComparisonOperator::GE: pr = pAbsentWhere([&](long v){return v < C;}) - allAbsent; break;
657 case ComparisonOperator::GT: pr = pAbsentWhere([&](long v){return v <= C;}) - allAbsent; break;
658 case ComparisonOperator::EQ: pr = pAbsentWhere([&](long v){return v < C;})
659 - pAbsentWhere([&](long v){return v <= C;}); break;
660 case ComparisonOperator::NE: pr = (1.0 - allAbsent)
661 - (pAbsentWhere([&](long v){return v < C;})
662 - pAbsentWhere([&](long v){return v <= C;})); break;
663 }
664 }
665 return pr;
666}
667
668/* ------------------------------------------------------------------ *
669 * SUM
670 * ------------------------------------------------------------------ *
671 * Reachable-sum support cap (Remark 3 pseudo-polynomial caveat): bail
672 * when the sparse sum distribution would exceed this many distinct
673 * values. */
674constexpr std::size_t kMaxSumSupport = 1u << 20;
675
676/* Does integer sum @p s satisfy @p s op C ? Mirrors SumCmpEvaluator. */
677static bool sumSatisfies(long s, ComparisonOperator op, long C)
678{
679 switch (op) {
680 case ComparisonOperator::EQ: return s == C;
681 case ComparisonOperator::NE: return s != C;
682 case ComparisonOperator::LE: return s <= C;
683 case ComparisonOperator::LT: return s < C;
684 case ComparisonOperator::GE: return s >= C;
685 case ComparisonOperator::GT: return s > C;
686 }
687 return false;
688}
689
690/* Joint (sum, count) distribution over a contributor set, as a sparse map
691 * (sum, count) -> probability. Generalises @c countPMF / @c sumPMF to
692 * track both coordinates at once. This is what the *branch-spanning* SUM
693 * needs: when an additively-separable value spans several product factors,
694 * the block sum is Σ_f sum_f · ∏_{g≠f} cnt_g, which couples each factor's
695 * weighted sum to the others' counts -- so neither marginal alone carries
696 * enough information and the per-factor *joint* must be folded. Same
697 * laminar recursion as @c sumPMF; clears @p ok on a non-laminar block, a
698 * non-separable product value, or a support overflow.
699 *
700 * Templated on the weight (sum-coordinate) type: the HAVING cmp path
701 * instantiates @c long (its constants and grid arithmetic are integer);
702 * the AVG moment instantiates @c double (arbitrary numeric row values).
703 * The singleton and laminar-shared-root branches are weight-agnostic;
704 * only the additive-separation recovery over a Cartesian-product block
705 * is genuinely integer arithmetic and is compiled for the integral
706 * instantiation alone (the double instantiation self-gates to the
707 * caller's fallback there). */
708template <typename W>
709using JointPMFT = std::map<std::pair<W, long>, double>;
710using JointPMF = JointPMFT<long>;
711
712/* Recover an additive separation of a product block's weights across its
713 * factors: find per-factor part values @p partVals (aligned to
714 * @c pd.parts[f]) with weights[m] == Σ_f partVals[f][part_f(m)], the
715 * constant folded into factor 0. Uses the reference-axis construction on
716 * the complete grid (h_f(p) = w(ref but p at f) - w(ref)); verifies the
717 * separation reproduces every member. Returns false -- caller bails --
718 * when the value is not additively separable (it genuinely couples factors
719 * and may be #P-hard, e.g. a product of two branches). */
720static bool recoverAdditiveSeparation(
721 const std::vector<std::vector<gate_t>> &contribs,
722 const std::vector<int> &members, const std::vector<long> &weights,
723 const ProductDecomp &pd, std::vector<std::vector<long>> &partVals)
724{
725 const int nf = static_cast<int>(pd.parts.size());
726
727 auto partOf = [&](int m, int f) {
728 std::vector<gate_t> p;
729 for (gate_t l : contribs[m]) /* contribs[m] already sorted */
730 if (pd.leafFactor.at(l) == f) p.push_back(l);
731 return p;
732 };
733
734 /* Complete-grid lookup: full part-tuple -> weight. */
735 std::map<std::vector<std::vector<gate_t>>, long> grid;
736 for (int m : members) {
737 std::vector<std::vector<gate_t>> key(nf);
738 for (int f = 0; f < nf; ++f) key[f] = partOf(m, f);
739 grid[key] = weights[m];
740 }
741
742 const int m0 = members[0];
743 const long W0 = weights[m0];
744 std::vector<std::vector<gate_t>> ref(nf);
745 for (int f = 0; f < nf; ++f) ref[f] = partOf(m0, f);
746
747 /* h_f(p) = w(ref, but part p at factor f) - W0 (so h_f(ref_f) = 0). */
748 std::vector<std::map<std::vector<gate_t>, long>> h(nf);
749 for (int f = 0; f < nf; ++f)
750 for (const auto &p : pd.parts[f]) {
751 std::vector<std::vector<gate_t>> key = ref;
752 key[f] = p;
753 auto it = grid.find(key);
754 if (it == grid.end()) return false; /* incomplete grid */
755 h[f][p] = it->second - W0;
756 }
757
758 /* The separation must reproduce every member's weight. */
759 for (int m : members) {
760 long acc = W0;
761 for (int f = 0; f < nf; ++f) acc += h[f].at(partOf(m, f));
762 if (acc != weights[m]) return false; /* not additively separable */
763 }
764
765 partVals.assign(nf, {});
766 for (int f = 0; f < nf; ++f) {
767 partVals[f].reserve(pd.parts[f].size());
768 for (const auto &p : pd.parts[f])
769 partVals[f].push_back(h[f].at(p) + (f == 0 ? W0 : 0)); /* fold W0 into f=0 */
770 }
771 return true;
772}
773
774template <typename W>
775static JointPMFT<W> sumCountPMF(GenericCircuit &gc,
776 std::vector<std::vector<gate_t>> contribs,
777 std::vector<W> weights, bool &ok)
778{
779 JointPMFT<W> total;
780 total[{0, 0}] = 1.0; /* δ_(0,0) */
781 if (contribs.empty()) return total;
782
783 for (const auto &members : independenceBlocks(contribs)) {
784 JointPMFT<W> blockPMF;
785 if (members.size() == 1) {
786 double q = 1.0;
787 for (gate_t l : contribs[members[0]]) q *= gc.getProb(l);
788 blockPMF[{0, 0}] += 1.0 - q; /* absent: (0,0) */
789 blockPMF[{weights[members[0]], 1}] += q; /* present: (w,1) */
790 } else {
791 std::vector<gate_t> common = commonLeaves(contribs, members);
792 if (!common.empty()) {
793 /* Laminar shared root: disjoint mixture, recurse on residuals. */
794 double p_root = 1.0;
795 for (gate_t l : common) p_root *= gc.getProb(l);
796 std::vector<W> rweights;
797 rweights.reserve(members.size());
798 for (int m : members) rweights.push_back(weights[m]);
799 JointPMFT<W> inner = sumCountPMF(
800 gc, residualsOf(contribs, members, common), std::move(rweights), ok);
801 if (!ok) return {};
802 for (const auto &kv : inner) blockPMF[kv.first] += p_root * kv.second;
803 blockPMF[{0, 0}] += 1.0 - p_root; /* root absent: (0,0) */
804 } else if constexpr (std::is_integral_v<W>) {
805 /* Cartesian product of independent factors. An additively
806 * separable value folds per-factor joints with the product
807 * combinator (S,N) ⊗ (s,n) = (S·n + s·N, N·n), identity (0,1):
808 * count multiplies, sum picks up each factor's weighted sum times
809 * the others' counts. This is exactly Σ_f sum_f · ∏_{g≠f} cnt_g.
810 * The separation recovery is exact integer grid arithmetic, hence
811 * integral instantiations only. */
812 ProductDecomp pd = decomposeProduct(contribs, members);
813 if (!pd.ok) { ok = false; return {}; }
814 std::vector<std::vector<long>> partVals;
815 if (!recoverAdditiveSeparation(contribs, members, weights, pd,
816 partVals)) {
817 ok = false; return {}; /* value couples factors */
818 }
819 JointPMFT<W> acc;
820 acc[{0, 1}] = 1.0; /* empty product: (0,1) */
821 for (std::size_t f = 0; f < pd.parts.size(); ++f) {
822 JointPMFT<W> Jf = sumCountPMF(gc, pd.parts[f], partVals[f], ok);
823 if (!ok) return {};
824 JointPMFT<W> nacc;
825 for (const auto &a : acc)
826 for (const auto &b : Jf)
827 nacc[{a.first.first * b.first.second
828 + b.first.first * a.first.second,
829 a.first.second * b.first.second}] += a.second * b.second;
830 if (nacc.size() > kMaxSumSupport) { ok = false; return {}; }
831 acc.swap(nacc);
832 }
833 blockPMF = std::move(acc);
834 } else {
835 /* Non-laminar product block under a non-integral weight type:
836 * out of the double instantiation's scope. */
837 ok = false; return {};
838 }
839 }
840 /* Independent blocks: sums and counts add. */
841 JointPMFT<W> ntotal;
842 for (const auto &a : total)
843 for (const auto &b : blockPMF)
844 ntotal[{a.first.first + b.first.first,
845 a.first.second + b.first.second}] += a.second * b.second;
846 if (ntotal.size() > kMaxSumSupport) { ok = false; return {}; }
847 total.swap(ntotal);
848 }
849 return total;
850}
851
852static std::map<long, double> sumPMF(GenericCircuit &gc,
853 std::vector<std::vector<gate_t>> contribs,
854 std::vector<long> weights, bool &ok);
855
856/* Overflow-checked 128-bit multiply with magnitude headroom: @c false on
857 * wraparound or a result past @c LIM (so the caller bails to enumeration,
858 * still correct). Used by the multiplicative-separable fold, whose
859 * intermediate products of per-factor sums can be large. */
860static bool i128_mul(__int128 a, __int128 b, __int128 &out)
861{
862 constexpr __int128 LIM = static_cast<__int128>(1) << 120;
863 if (a == 0 || b == 0) { out = 0; return true; }
864 __int128 r = a * b;
865 if (r / b != a) return false; /* wrapped */
866 __int128 ar = r < 0 ? -r : r;
867 if (ar > LIM) return false; /* keep headroom for later ops */
868 out = r;
869 return true;
870}
871
872/* SUM distribution of a Cartesian-product block whose value is
873 * *multiplicatively* separable across the factors, w_m = ∏_f v_f(part_f):
874 * then SUM = ∏_f sum_f with sum_f = Σ_{present p} v_f(p), so the block sum
875 * is a product of independent per-factor weighted sums. No explicit
876 * factorisation is needed -- with a nonzero pivot weight @c D at reference
877 * parts and the grid's axis entries A^f_p = w(ref but p at f), the identity
878 * block sum = ∏_f (Σ_{present p} A^f_p) / D^{nf-1}
879 * holds (each axis sum carries a spurious factor D/v_f(ref_f), and the nf
880 * of them divide back to D^{nf-1}). The A^f_p are grid entries (integers),
881 * so per-factor @c sumPMF gives Σ A^f_p exactly; the per-factor sum PMFs are
882 * product-convolved (in 128-bit, guarded) and divided by D^{nf-1} (exact in
883 * every world, since each world's block sum is integral). Clears @p ok --
884 * caller bails to enumeration -- when the value is not multiplicatively
885 * separable, on overflow, or on a within-factor non-laminar bail. */
886static std::map<long, double> mulSeparableSumPMF(
887 GenericCircuit &gc,
888 const std::vector<std::vector<gate_t>> &contribs,
889 const std::vector<int> &members, const std::vector<long> &weights,
890 const ProductDecomp &pd, bool &ok)
891{
892 const int nf = static_cast<int>(pd.parts.size());
893
894 auto partOf = [&](int m, int f) {
895 std::vector<gate_t> p;
896 for (gate_t l : contribs[m])
897 if (pd.leafFactor.at(l) == f) p.push_back(l);
898 return p;
899 };
900
901 std::map<std::vector<std::vector<gate_t>>, long> grid;
902 for (int m : members) {
903 std::vector<std::vector<gate_t>> key(nf);
904 for (int f = 0; f < nf; ++f) key[f] = partOf(m, f);
905 grid[key] = weights[m];
906 }
907
908 int piv = -1;
909 for (int m : members) if (weights[m] != 0) { piv = m; break; }
910 if (piv < 0) { ok = false; return {}; } /* all zero: additive handled it */
911 const long D = weights[piv];
912 std::vector<std::vector<gate_t>> ref(nf);
913 for (int f = 0; f < nf; ++f) ref[f] = partOf(piv, f);
914
915 /* Axis values A^f (aligned to pd.parts[f]) and a part -> index map. */
916 std::vector<std::vector<long>> A(nf);
917 std::vector<std::map<std::vector<gate_t>, int>> partIdx(nf);
918 for (int f = 0; f < nf; ++f)
919 for (const auto &p : pd.parts[f]) {
920 partIdx[f][p] = static_cast<int>(A[f].size());
921 std::vector<std::vector<gate_t>> key = ref;
922 key[f] = p;
923 auto it = grid.find(key);
924 if (it == grid.end()) { ok = false; return {}; }
925 A[f].push_back(it->second);
926 }
927
928 /* D^{nf-1}. */
929 __int128 Dk1 = 1;
930 for (int t = 0; t < nf - 1; ++t)
931 if (!i128_mul(Dk1, static_cast<__int128>(D), Dk1)) { ok = false; return {}; }
932
933 /* Verify multiplicative separability: ∏_f A^f_{p_f} == w_m · D^{nf-1}. */
934 for (int m : members) {
935 __int128 prod = 1;
936 for (int f = 0; f < nf; ++f) {
937 long a = A[f][partIdx[f].at(partOf(m, f))];
938 if (!i128_mul(prod, static_cast<__int128>(a), prod)) { ok = false; return {}; }
939 }
940 __int128 rhs;
941 if (!i128_mul(static_cast<__int128>(weights[m]), Dk1, rhs)) { ok = false; return {}; }
942 if (prod != rhs) { ok = false; return {}; } /* not multiplicatively separable */
943 }
944
945 /* Per-factor sum PMFs (over the axis weights), product-convolved. */
946 std::map<__int128, double> run;
947 run[1] = 1.0; /* multiplicative identity */
948 for (int f = 0; f < nf; ++f) {
949 std::map<long, double> Pf = sumPMF(gc, pd.parts[f], A[f], ok);
950 if (!ok) return {};
951 std::map<__int128, double> nxt;
952 for (const auto &rk : run)
953 for (const auto &sk : Pf) {
954 __int128 prod;
955 if (!i128_mul(rk.first, static_cast<__int128>(sk.first), prod)) {
956 ok = false; return {};
957 }
958 nxt[prod] += rk.second * sk.second;
959 }
960 if (nxt.size() > kMaxSumSupport) { ok = false; return {}; }
961 run.swap(nxt);
962 }
963
964 /* Divide each product by D^{nf-1} (exact per world) and downcast. */
965 std::map<long, double> out;
966 for (const auto &kv : run) {
967 if (kv.first % Dk1 != 0) { ok = false; return {}; } /* defensive */
968 __int128 bs = kv.first / Dk1;
969 if (bs > static_cast<__int128>(LONG_MAX) ||
970 bs < static_cast<__int128>(LONG_MIN)) { ok = false; return {}; }
971 out[static_cast<long>(bs)] += kv.second;
972 }
973 return out;
974}
975
976/* Distribution of SUM(value) over a hierarchical contributor set, as a
977 * sparse map sum -> probability. Same recursion as countPMF, but a
978 * present contributor adds its weight @p weights[i] (not 1), so blocks
979 * combine by additive convolution over the (possibly negative) integer
980 * sum domain. COUNT is the all-weights-1 instance; this carries the
981 * weighted case. Clears @p ok on a non-laminar block or when the
982 * support exceeds @c kMaxSumSupport. */
983static std::map<long, double> sumPMF(GenericCircuit &gc,
984 std::vector<std::vector<gate_t>> contribs,
985 std::vector<long> weights, bool &ok)
986{
987 std::map<long, double> total;
988 total[0] = 1.0; /* δ_0 */
989 if (contribs.empty()) return total;
990
991 for (const auto &members : independenceBlocks(contribs)) {
992 std::map<long, double> blockPMF;
993 if (members.size() == 1) {
994 double q = 1.0;
995 for (gate_t l : contribs[members[0]]) q *= gc.getProb(l);
996 blockPMF[0] += 1.0 - q; /* absent: contributes 0 */
997 blockPMF[weights[members[0]]] += q; /* present: contributes w */
998 } else {
999 std::vector<gate_t> common = commonLeaves(contribs, members);
1000 if (!common.empty()) {
1001 double p_root = 1.0;
1002 for (gate_t l : common) p_root *= gc.getProb(l);
1003
1004 std::vector<std::vector<gate_t>> residuals =
1005 residualsOf(contribs, members, common);
1006 std::vector<long> rweights;
1007 rweights.reserve(members.size());
1008 for (int m : members) rweights.push_back(weights[m]);
1009
1010 std::map<long, double> inner =
1011 sumPMF(gc, std::move(residuals), std::move(rweights), ok);
1012 if (!ok) return {};
1013 for (const auto &kv : inner) blockPMF[kv.first] += p_root * kv.second;
1014 blockPMF[0] += (1.0 - p_root); /* root absent: sum 0 */
1015 } else {
1016 /* Cartesian product. Tractable cases: (1) the value depends on a
1017 * single factor f -- SUM = S_f · M, with S_f the weighted sum over
1018 * factor f and M = ∏_{i≠f} N_i the count-product of the others (the
1019 * fast path below, detected by a weight constant within each f-part
1020 * group); (2) a branch-spanning but *additively separable* value
1021 * (sum(b+c)) -- per-factor joint (sum,count) distributions in
1022 * @c sumCountPMF; (3) a *multiplicatively separable* value (sum(b*c))
1023 * -- product of per-factor weighted sums in @c mulSeparableSumPMF
1024 * (both in the else arm). A value that is none of these couples the
1025 * factors (may be #P-hard), so it bails. */
1026 ProductDecomp pd = decomposeProduct(contribs, members);
1027 if (!pd.ok) { ok = false; return {}; }
1028 const int nf = static_cast<int>(pd.parts.size());
1029
1030 int chosen = -1;
1031 std::map<std::vector<gate_t>, long> partVal;
1032 for (int f = 0; f < nf && chosen < 0; ++f) {
1033 std::map<std::vector<gate_t>, long> pv;
1034 bool consistent = true;
1035 for (int m : members) {
1036 std::vector<gate_t> partf;
1037 for (gate_t l : contribs[m])
1038 if (pd.leafFactor[l] == f) partf.push_back(l);
1039 auto it = pv.find(partf);
1040 if (it == pv.end()) pv[partf] = weights[m];
1041 else if (it->second != weights[m]) { consistent = false; break; }
1042 }
1043 if (consistent) { chosen = f; partVal = std::move(pv); }
1044 }
1045 if (chosen >= 0) {
1046 /* Single-factor value: SUM = S_f · M (the other factors
1047 * contribute only their count). */
1048
1049 /* S_f: weighted-sum distribution over the chosen factor's parts. */
1050 std::vector<long> partValues;
1051 partValues.reserve(pd.parts[chosen].size());
1052 for (const auto &part : pd.parts[chosen])
1053 partValues.push_back(partVal[part]);
1054 std::map<long, double> Sf =
1055 sumPMF(gc, pd.parts[chosen], std::move(partValues), ok);
1056 if (!ok) return {};
1057
1058 /* M: count-product distribution over the other factors. */
1059 std::vector<double> M;
1060 for (int f = 0; f < nf; ++f) {
1061 if (f == chosen) continue;
1062 std::vector<double> cf = countPMF(gc, pd.parts[f], ok);
1063 if (!ok) return {};
1064 M = M.empty() ? std::move(cf) : productConvolve(M, cf);
1065 }
1066
1067 /* blockPMF = distribution of S_f · M (independent factors). */
1068 for (const auto &skv : Sf)
1069 for (std::size_t mm = 0; mm < M.size(); ++mm)
1070 if (M[mm] != 0.0)
1071 blockPMF[skv.first * static_cast<long>(mm)] += skv.second * M[mm];
1072 if (blockPMF.size() > kMaxSumSupport) { ok = false; return {}; }
1073 } else {
1074 /* Branch-spanning value. Two tractable shapes: *additively*
1075 * separable (sum(b+c)) -> fold the per-factor joint (sum,count)
1076 * distributions (sumCountPMF) and read off the sum marginal;
1077 * *multiplicatively* separable (sum(b*c)) -> product of the
1078 * per-factor weighted sums (mulSeparableSumPMF). Try additive
1079 * first (a value that is both is constant, handled there); a value
1080 * that is neither couples the factors and bails. */
1081 std::vector<std::vector<long>> sep;
1082 if (recoverAdditiveSeparation(contribs, members, weights, pd, sep)) {
1083 std::vector<std::vector<gate_t>> bc;
1084 std::vector<long> bw;
1085 bc.reserve(members.size());
1086 bw.reserve(members.size());
1087 for (int m : members) {
1088 bc.push_back(contribs[m]);
1089 bw.push_back(weights[m]);
1090 }
1091 JointPMF j = sumCountPMF(gc, std::move(bc), std::move(bw), ok);
1092 if (!ok) return {};
1093 for (const auto &kv : j) blockPMF[kv.first.first] += kv.second;
1094 } else {
1095 blockPMF = mulSeparableSumPMF(gc, contribs, members, weights, pd, ok);
1096 if (!ok) return {}; /* neither separable: bail */
1097 }
1098 if (blockPMF.size() > kMaxSumSupport) { ok = false; return {}; }
1099 }
1100 }
1101 }
1102 std::map<long, double> ntotal;
1103 for (const auto &a : total)
1104 for (const auto &b : blockPMF)
1105 ntotal[a.first + b.first] += a.second * b.second;
1106 if (ntotal.size() > kMaxSumSupport) { ok = false; return {}; }
1107 total.swap(ntotal);
1108 }
1109 return total;
1110}
1111
1112} // namespace
1113
1115{
1116 unsigned resolved = 0;
1117 const auto nb = gc.getNbGates();
1118
1119 std::vector<gate_t> cmps;
1120 for (std::size_t i = 0; i < nb; ++i) {
1121 auto g = static_cast<gate_t>(i);
1122 if (gc.getGateType(g) == gate_cmp)
1123 cmps.push_back(g);
1124 }
1125 if (cmps.empty()) return 0;
1126
1127 auto ref = computeRefCounts(gc);
1128
1129 for (gate_t cmp : cmps) {
1130 if (gc.getGateType(cmp) != gate_cmp) continue; /* resolved meanwhile */
1131
1132 AggCmpMatch match;
1133 if (!matchAggCmp(gc, cmp, match))
1134 continue;
1135 const AggregationOperator agg_kind = match.agg_kind;
1136 if (agg_kind != AggregationOperator::COUNT &&
1137 agg_kind != AggregationOperator::SUM &&
1138 agg_kind != AggregationOperator::AVG &&
1139 agg_kind != AggregationOperator::MIN &&
1140 agg_kind != AggregationOperator::MAX)
1141 continue; /* other aggregates: out of scope */
1142
1143 const gate_t agg = match.agg;
1144 const auto &ks = match.ks;
1145 const std::size_t n = ks.size();
1146
1147 /* The aggregate must be consumed by this cmp alone: a shared agg
1148 * would couple two HAVING comparators over the same aggregate. */
1149 if (ref[static_cast<std::size_t>(agg)] != 1) continue;
1150 bool ok = true;
1151
1152 /* Parse every contributor: either a plain product of independent
1153 * @c gate_input leaves (TID, fed to the laminar / product recursion) or a
1154 * single @c gate_mulinput -- one alternative of a mutually-exclusive BID
1155 * block (e.g. @c repair_key), identified by its shared block-key child.
1156 * A contributor mixing the two (a join onto a BID row) or holding several
1157 * mulinputs is out of scope and bails to enumeration. */
1158 std::vector<std::vector<gate_t>> leaves; /* TID contributor leaf sets */
1159 std::vector<long> tid_vals; /* per-TID value (match.ms, aligned) */
1160 /* block key -> alternatives (prob, value). A BID block is a categorical:
1161 * at most one alternative present (Σp_i ≤ 1), the null arm contributes 0. */
1162 std::map<gate_t, std::vector<std::pair<double, long>>> blocks;
1163 for (std::size_t i = 0; i < n && ok; ++i) {
1164 if (gc.getGateType(ks[i]) == gate_mulinput) {
1165 const auto &ch = gc.getWires(ks[i]);
1166 if (ch.size() != 1) { ok = false; break; } /* not a block alternative */
1167 blocks[ch[0]].push_back({gc.getProb(ks[i]),
1168 static_cast<long>(match.ms[i])});
1169 } else {
1170 std::vector<gate_t> ls;
1171 if (parseProductContributor(gc, ks[i], ls)) {
1172 leaves.push_back(std::move(ls));
1173 tid_vals.push_back(static_cast<long>(match.ms[i]));
1174 } else {
1175 /* Not a product (a UNION/EXCEPT contributor: gate_plus / gate_monus,
1176 * non-read-once on a shared base tuple). Exact iff its footprint is
1177 * private -- then it is an independent event, modelled as a
1178 * one-alternative BID block of its exact marginal. */
1179 double pi;
1180 if (!contributorExactMarginal(gc, ks[i], ref, pi)) { ok = false; break; }
1181 blocks[ks[i]].push_back({pi, static_cast<long>(match.ms[i])});
1182 }
1183 }
1184 }
1185 if (!ok) continue;
1186
1187 /* Independence guard: a block key (shared by its alternatives) must not
1188 * also surface as a TID leaf, which would couple the block to an
1189 * independent contributor. Distinct repair_key blocks already get
1190 * distinct keys; cross-group sharing is caught by aggSubtreePrivate. */
1191 {
1192 std::set<gate_t> tidset;
1193 for (const auto &ls : leaves) tidset.insert(ls.begin(), ls.end());
1194 bool clash = false;
1195 for (const auto &b : blocks)
1196 if (tidset.count(b.first)) { clash = true; break; }
1197 if (clash) continue;
1198 }
1199
1200 /* The cmp's randomness must be private to its agg subtree -- no gate
1201 * reachable from the agg referenced from outside it -- the soundness
1202 * precondition for resolving the cmp to an independent Bernoulli.
1203 * Subsumes the per-semimod ref==1 and per-leaf ref==cnt checks and
1204 * extends them to nested / shared product gates (subquery tuples). */
1205 if (!aggSubtreePrivate(gc, agg, ref)) continue;
1206
1207 /* Σ_i p_i of a BID block (clamped). */
1208 auto blockMass = [](const std::vector<std::pair<double, long>> &alts) {
1209 double psum = 0.0;
1210 for (const auto &alt : alts) psum += alt.first;
1211 return psum > 1.0 ? 1.0 : psum;
1212 };
1213
1214 /* Dispatch on the aggregate; each arm computes the exact probability
1215 * over the hierarchical (laminar) contributor structure, recursing
1216 * through shared root events. A non-laminar shape clears @c ok and
1217 * the cmp falls back to exact enumeration. */
1218 double pr;
1219 if (agg_kind == AggregationOperator::COUNT) {
1220 /* countPMF treats every contributor as +1 (cardinality), correct only for
1221 * count(*) / count(col) with no NULLs. A count(col) with NULL-valued
1222 * contributors carries per-row 0/1 values (match.ms), so cardinality would
1223 * over-count; defer it to the value-aware generic enumeration
1224 * (having_semantics -> sum_dp), which also keeps the scalar empty world. */
1225 bool all_one = true;
1226 for (int m : match.ms) if (m != 1) { all_one = false; break; }
1227 if (!all_one) continue;
1228 std::vector<double> total = countPMF(gc, leaves, ok);
1229 if (!ok) continue;
1230 /* Each BID block adds 0 or 1 to the count (mutual exclusion): present
1231 * w.p. Σp_i, absent w.p. 1-Σp_i; independent of the rest. */
1232 for (const auto &b : blocks) {
1233 double psum = blockMass(b.second);
1234 total = convolve(total, std::vector<double>{1.0 - psum, psum});
1235 }
1236 const bool is_scalar =
1237 (gc.getInfos(agg).second & PROVSQL_AGG_SCALAR_FLAG) != 0;
1238 pr = prFromPMF(total, match.op, match.C, is_scalar);
1239 } else if (agg_kind == AggregationOperator::SUM ||
1240 agg_kind == AggregationOperator::AVG) {
1241 /* SUM(v) θ C directly; AVG(v) θ C ⟺ SUM(v_i − C) θ 0 (multiply the
1242 * average by the positive group count; the empty group has no
1243 * average and is excluded, exactly as the empty group is for SUM).
1244 * Both reduce to the weighted-sum distribution, so AVG inherits the
1245 * laminar / product machinery for free. Only integer thresholds
1246 * reach here -- a fractional HAVING-AVG constant is rejected upstream
1247 * before the cmp is even built. */
1248 const bool is_avg = (agg_kind == AggregationOperator::AVG);
1249 auto shift = [&](long m) { return is_avg ? m - match.C : m; };
1250 std::vector<long> weights;
1251 weights.reserve(tid_vals.size());
1252 long lo = 0, hi = 0;
1253 for (long m : tid_vals) {
1254 long w = shift(m);
1255 weights.push_back(w);
1256 if (w < 0) lo += w; else hi += w;
1257 }
1258 for (const auto &b : blocks)
1259 for (const auto &alt : b.second) {
1260 long w = shift(alt.second);
1261 if (w < 0) lo += w; else hi += w;
1262 }
1263 /* Reachable-sum range cap (Remark 3 pseudo-polynomial caveat). */
1264 if (hi - lo + 1 > static_cast<long>(kMaxSumSupport)) continue;
1265 const long thr = is_avg ? 0 : match.C;
1266
1267 std::map<long, double> dist = sumPMF(gc, leaves, std::move(weights), ok);
1268 if (!ok) continue;
1269 /* Convolve each BID block's categorical (shifted) sum distribution. */
1270 for (const auto &b : blocks) {
1271 std::map<long, double> bpmf;
1272 for (const auto &alt : b.second) bpmf[shift(alt.second)] += alt.first;
1273 bpmf[0] += 1.0 - blockMass(b.second); /* null outcome: sum 0 */
1274 std::map<long, double> nd;
1275 for (const auto &a : dist)
1276 for (const auto &c : bpmf)
1277 nd[a.first + c.first] += a.second * c.second;
1278 if (nd.size() > kMaxSumSupport) { ok = false; break; }
1279 dist.swap(nd);
1280 }
1281 if (!ok) continue;
1282 pr = 0.0;
1283 for (const auto &kv : dist)
1284 if (sumSatisfies(kv.first, match.op, thr)) pr += kv.second;
1285 /* Exclude the empty group: its (shifted) sum is 0, so subtract its
1286 * mass when 0 satisfies the predicate (a non-empty group that
1287 * happens to sum to the threshold stays). The empty world is all TID
1288 * contributors absent AND every block in its null outcome. */
1289 if (sumSatisfies(0, match.op, thr)) {
1290 double emptyMass = pAllAbsent(gc, leaves, ok);
1291 if (!ok) continue;
1292 for (const auto &b : blocks) emptyMass *= 1.0 - blockMass(b.second);
1293 pr -= emptyMass;
1294 }
1295 } else { /* MIN or MAX */
1296 /* MIN/MAX over the TID part (laminar pAllAbsent) and the BID blocks
1297 * (each an independent categorical; a value-thresholded subset of a
1298 * block is all-absent w.p. 1-Σp over its matching alternatives). */
1299 std::vector<std::vector<std::pair<long, double>>> blockvec;
1300 blockvec.reserve(blocks.size());
1301 for (const auto &b : blocks) {
1302 std::vector<std::pair<long, double>> alts;
1303 alts.reserve(b.second.size());
1304 for (const auto &alt : b.second) alts.push_back({alt.second, alt.first});
1305 blockvec.push_back(std::move(alts));
1306 }
1307 pr = minMaxProb(gc, leaves, tid_vals, blockvec, agg_kind, match.op,
1308 match.C, ok);
1309 if (!ok) continue;
1310 }
1311
1312 if (pr < 0.0) pr = 0.0;
1313 if (pr > 1.0) pr = 1.0;
1314
1315 gc.resolveCmpToBernoulli(cmp, pr);
1316 ++resolved;
1317 }
1318
1319 return resolved;
1320}
1321
1323 bool &ok)
1324{
1325 ok = false;
1326 if (gc.getGateType(g) != gate_agg) return 0.0;
1327
1328 /* Per-row (contributor leaf set, value) pairs from the semimod
1329 * children -- the same contributor parse the HAVING cmp path uses. */
1330 std::vector<std::vector<gate_t>> contribs;
1331 std::vector<double> values;
1332 for (gate_t sm : gc.getWires(g)) {
1333 if (gc.getGateType(sm) != gate_semimod) return 0.0;
1334 const auto &w = gc.getWires(sm);
1335 if (w.size() != 2) return 0.0;
1336 double v;
1337 try {
1338 v = parseDoubleStrict(gc.getExtra(w[1]));
1339 } catch (const CircuitException &) {
1340 return 0.0; /* non-numeric value: decline */
1341 }
1342 std::vector<gate_t> leaves;
1343 if (!parseProductContributor(gc, w[0], leaves))
1344 return 0.0; /* not a private product: decline */
1345 contribs.push_back(std::move(leaves));
1346 values.push_back(v);
1347 }
1348 for (const auto &c : contribs)
1349 for (gate_t l : c)
1350 if (std::isnan(gc.getProb(l))) return 0.0; /* unset prob: decline */
1351
1352 /* Joint (sum, count) distribution via the shared HAVING machinery
1353 * (double instantiation: independent rows fold directly, laminar
1354 * shared-root groups recurse; a non-laminar product block self-gates). */
1355 bool pmf_ok = true;
1356 JointPMFT<double> pmf = sumCountPMF(gc, std::move(contribs),
1357 std::move(values), pmf_ok);
1358 if (!pmf_ok) return 0.0;
1359
1360 /* E[AVG^k | COUNT >= 1]: AVG over the empty world is NULL, so the
1361 * moment conditions on the aggregate being defined -- the same
1362 * convention as the MIN / MAX arms of agg_raw_moment. */
1363 double num = 0.0, den = 0.0;
1364 for (const auto &kv : pmf) {
1365 if (kv.first.second < 1) continue;
1366 den += kv.second;
1367 num += kv.second * std::pow(kv.first.first
1368 / static_cast<double>(kv.first.second),
1369 static_cast<double>(k));
1370 }
1371 if (!(den > 1e-12)) return 0.0; /* never defined: decline (the MC
1372 fallback then reports the same
1373 undefined answer) */
1374 ok = true;
1375 return num / den;
1376}
1377
1378} // namespace provsql
Exact closed-form HAVING COUNT(*) op C probability over safe-join lineage – the recursive marginal-ve...
Typed aggregation value, operator, and aggregator abstractions.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
@ MAX
MAX → input type.
Definition Aggregation.h:55
@ COUNT
COUNT(*) or COUNT(expr) → integer.
Definition Aggregation.h:52
@ SUM
SUM → integer or float.
Definition Aggregation.h:53
@ MIN
MIN → input type.
Definition Aggregation.h:54
@ AVG
AVG → float.
Definition Aggregation.h:56
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
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Shared machinery for the closed-form HAVING gate_cmp probability evaluators (Poisson-binomial COUNT,...
Continuous random-variable helpers (distribution parsing, moments).
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
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.
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.
void resolveCmpToBernoulli(gate_t g, double p)
Replace a gate_cmp by a constant Boolean leaf (gate_one for p == 1, gate_zero for p == 0) or by a Ber...
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
double aggAvgRawMomentExact(GenericCircuit &gc, gate_t g, unsigned k, bool &ok)
Exact k-th raw moment of AVG = SUM/COUNT over independent rows, conditional on COUNT >= 1.
unsigned runAggMarginalEvaluator(GenericCircuit &gc)
Run the safe-join aggregate marginal-vector pre-pass over gc.
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
std::vector< unsigned > computeRefCounts(const GenericCircuit &gc)
Reference count of every gate as a wire-target across the whole circuit.
bool matchAggCmp(GenericCircuit &gc, gate_t cmp, AggCmpMatch &out)
Try to match cmp against gate_cmp(gate_agg(α, semimod_i(K_i, m_i)*), gate_value(C)).
Core types, constants, and utilities shared across ProvSQL.
#define PROVSQL_AGG_SCALAR_FLAG
Scalar-aggregation flag, stored in the upper bit of a gate_agg's info2 (whose low 31 bits hold the ag...
Result of matching a gate_cmp against the canonical HAVING aggregate-comparison shape.
gate_t agg
the gate_agg operand of the cmp
long C
the constant threshold, on the same integer grid as ms
std::vector< gate_t > ks
the K side of each semimod (contributor root)
std::vector< long > ms
the M side of each semimod (per-row value), scaled to a common integer grid (numeric / decimal-float ...
AggregationOperator agg_kind
effective aggregate (SUM-of-1s remapped to COUNT)
ComparisonOperator op
comparator, flipped if the agg sits on the right