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