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