ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
Expectation.cpp
Go to the documentation of this file.
1/**
2 * @file Expectation.cpp
3 * @brief Implementation of the analytical expectation / variance / moment
4 * evaluator over scalar RV sub-circuits.
5 */
6#include "Expectation.h"
7
8#include "AggMarginalEvaluator.h" // aggAvgRawMomentExact
9#include "AnalyticEvaluator.h"
10#include "Aggregation.h" // ComparisonOperator + cmpOpFromOid
11#include "BooleanCircuit.h"
12#include "Circuit.h"
13#include "CircuitFromMMap.h"
14#include "CollapsedAggMoment.h" // collapsedConditionalMoment
15#include "ComparatorResolution.h" // resolveComparators
16#include "ConjugatePosterior.h" // conjugatePosterior / conjugateLogEvidence
17#include "ProbabilityMethod.h" // booleanSubcircuitProbability
18#include "MonteCarloSampler.h"
19#include "RandomVariable.h"
20#include "distributions/Distribution.h" // makeDistribution -> integrationRange
21#include "PivotIntegration.h" // simpsonIntegrate / binomial / centralFromRaw
22#include "RangeCheck.h"
23#include "provsql_utils_cpp.h"
24#include "semiring/BoolExpr.h"
25
26extern "C" {
27#include "postgres.h"
28#include "fmgr.h"
29#include "utils/uuid.h"
30#include "provsql_utils.h"
31#include "provsql_error.h"
32
33PG_FUNCTION_INFO_V1(rv_moment);
34PG_FUNCTION_INFO_V1(rv_quantile);
35PG_FUNCTION_INFO_V1(rv_evidence);
36PG_FUNCTION_INFO_V1(agg_avg_moment_exact);
37}
38
39#include <algorithm>
40#include <cmath>
41#include <set>
42#include <stack>
43#include <string>
44#include <unordered_map>
45#include <unordered_set>
46#include <vector>
47
48namespace provsql {
49
50namespace {
51/// Copy the subtree rooted at @p root of @p gc into a fresh GenericCircuit
52/// @p sub (mirrors @c MMappedCircuit::createGenericCircuit, but from an
53/// in-memory circuit). Lets the moment path resolve a mixture selector's
54/// comparators in isolation -- without mutating the surrounding moment
55/// circuit, whose gate_case guards and native RV comparisons must keep
56/// their raw comparator structure.
57void extractSubcircuit(const GenericCircuit &gc, gate_t root,
58 GenericCircuit &sub, gate_t &sub_root)
59{
60 std::unordered_set<gate_t> seen;
61 std::stack<gate_t> stk;
62 stk.push(root);
63 while (!stk.empty()) {
64 gate_t g = stk.top(); stk.pop();
65 if (!seen.insert(g).second) continue;
66 const std::string u = gc.getUUID(g);
67 const gate_type t = gc.getGateType(g);
68 const gate_t id = sub.setGate(u, t);
69 const double pr = gc.getProb(g);
70 if (!std::isnan(pr)) sub.setProb(id, pr);
71 if (t == gate_mulinput || t == gate_eq || t == gate_agg || t == gate_cmp
72 || t == gate_arith) {
73 const auto infos = gc.getInfos(g);
74 sub.setInfos(id, infos.first, infos.second);
75 } else if (t == gate_plus || t == gate_times) {
76 const auto infos = gc.getInfos(g);
77 if (infos.first != 0 || infos.second != 0)
78 sub.setInfos(id, infos.first, infos.second);
79 }
80 if (t == gate_project || t == gate_value || t == gate_agg || t == gate_rv
81 || t == gate_mulinput || t == gate_annotation || t == gate_assumed
82 || t == gate_mobius || t == gate_arith || t == gate_observe)
83 sub.setExtra(id, gc.getExtra(g));
84 for (gate_t c : gc.getWires(g)) {
85 sub.addWire(id, sub.getGate(gc.getUUID(c)));
86 stk.push(c);
87 }
88 }
89 sub_root = sub.getGate(gc.getUUID(root));
90}
91} // namespace
92
94{
95 // Route through the single central Boolean-probability entry point
96 // (getBooleanCircuit + MethodCatalog::chooseAndRun, MC fallback), rather
97 // than a hand-rolled Boolean build + independentEvaluation. Any RV
98 // comparator in this Boolean function is resolved on an extracted COPY of
99 // the boolRoot subtree, so the resolution never touches the surrounding
100 // moment circuit (whose gate_case guards / native RV comparisons must
101 // keep their raw comparator for the correlation-aware value moment).
102 GenericCircuit sub;
103 gate_t sub_root;
104 extractSubcircuit(gc, boolRoot, sub, sub_root);
105 resolveComparators(sub, sub_root, /*simplify=*/false, /*decompose=*/false);
106 return booleanSubcircuitProbability(sub, sub_root);
107}
108
109namespace {
110
111using RvSet = std::set<gate_t>;
112
113/// Mixing weight π = P(p = true) for a mixture's Bernoulli wire.
114/// For a bare @c gate_input, the probability is the leaf's pinned
115/// @c set_prob; for any compound Boolean gate, defer to
116/// @c evaluateBooleanProbability.
117double mixturePi(const GenericCircuit &gc, gate_t p)
118{
119 return (gc.getGateType(p) == gate_input)
120 ? gc.getProb(p)
122}
123
124/// True iff @p g is a @c gate_rv whose distribution has a wired (token)
125/// parameter -- a latent / compound leaf with no constant-parameter
126/// closed form, so every analytic path must fall through to Monte Carlo.
127bool rvIsParametric(const GenericCircuit &gc, gate_t g)
128{
129 if (gc.getGateType(g) != gate_rv) return false;
130 auto tmpl = parse_distribution_template(gc.getExtra(g));
131 return tmpl && tmpl->parametric();
132}
133
134/// Whether the family of @p tmpl has a mean affine in its parameters, so the
135/// compound-leaf expectation is exact via @c mean(E[θ]) (see the
136/// @c meanIsAffine() doc). The flag is authoritative, per-family.
137bool familyMeanIsAffine(const DistributionTemplate &tmpl)
138{
139 return tmpl.family->factory(0.0, 0.0)->meanIsAffine();
140}
141
142/// Cache of the base-@c gate_rv UUID footprints reachable below each
143/// scalar gate, used as the structural-independence witness. Two
144/// children of an arithmetic gate are independent iff their footprints
145/// are disjoint -- and therefore the variance and TIMES expectation
146/// shortcuts apply.
147class FootprintCache {
148public:
149 explicit FootprintCache(const GenericCircuit &gc) : gc_(gc) {}
150
151 const RvSet &of(gate_t g) {
152 auto it = cache_.find(g);
153 if (it != cache_.end()) return it->second;
154 RvSet s;
155 auto type = gc_.getGateType(g);
156 if (type == gate_rv) {
157 // The leaf itself is a distinct random source (two independent
158 // normal(M,1) leaves share M but draw independently given it), so
159 // its own gate_t is always in the footprint. A latent (parametric)
160 // leaf ALSO carries the footprints of its parameter wires, so two
161 // leaves sharing a latent M overlap on M and are correctly flagged
162 // dependent -- defeating the closed-form independence shortcuts and
163 // routing the whole expression to the MC path that couples them.
164 // A non-parametric leaf has no wires, so this is a no-op there.
165 s.insert(g);
166 for (gate_t c : gc_.getWires(g)) {
167 const auto &cs = of(c);
168 s.insert(cs.begin(), cs.end());
169 }
170 } else if (type == gate_value) {
171 // empty -- no RV reached
172 } else if (type == gate_arith || type == gate_case) {
173 // gate_arith: all children are scalar operands. gate_case: the
174 // selected value depends on both the guards (which RVs they compare)
175 // and the value branches, so the footprint is the union over every
176 // wire -- a value and a guard sharing a leaf make the selection
177 // correlated, which must defeat the independence shortcuts.
178 for (gate_t c : gc_.getWires(g)) {
179 const auto &cs = of(c);
180 s.insert(cs.begin(), cs.end());
181 }
182 } else if (type == gate_mixture) {
183 const auto &wires = gc_.getWires(g);
184 if (gc_.isCategoricalMixture(g)) {
185 // Categorical-form mixture. Footprint = union of the
186 // mulinputs' footprints (each contributes {self, key} via the
187 // mulinput branch below), so two categoricals sharing a key
188 // overlap on it and are correctly flagged dependent.
189 for (std::size_t i = 1; i < wires.size(); ++i) {
190 const auto &fm = of(wires[i]);
191 s.insert(fm.begin(), fm.end());
192 }
193 } else if (wires.size() == 3) {
194 // Classic 3-wire mixture. Footprint = footprint(p) ∪
195 // footprint(x) ∪ footprint(y). The Boolean wire is included
196 // as a discrete random source so two mixtures whose p's share
197 // an atom are correctly recognised as dependent (their branch
198 // selection is correlated), bypassing the closed-form
199 // independence shortcut. Recursing into wires[0] (rather than
200 // inserting its gate_t directly) generalises that recognition
201 // from bare-input Bernoullis to compound Boolean wires.
202 const auto &fp = of(wires[0]);
203 s.insert(fp.begin(), fp.end());
204 const auto &fx = of(wires[1]);
205 s.insert(fx.begin(), fx.end());
206 const auto &fy = of(wires[2]);
207 s.insert(fy.begin(), fy.end());
208 }
209 } else if (type == gate_mulinput) {
210 // A mulinput is a state-carrying atom (so its own gate_t is in
211 // its footprint -- two mulinputs of the same group are distinct
212 // atoms even though they share a key) *and* references a shared
213 // key gate at wires[0] (whose footprint is added so the shared
214 // key makes the two mulinputs overlap on it, flagging them as
215 // dependent for pairwise_disjoint).
216 s.insert(g);
217 const auto &wires = gc_.getWires(g);
218 if (!wires.empty()) {
219 const auto &fk = of(wires[0]);
220 s.insert(fk.begin(), fk.end());
221 }
222 } else if (type == gate_input) {
223 // Atomic Boolean leaf. Use the gate's own UUID as its footprint
224 // so two Boolean expressions sharing this input collide on it.
225 s.insert(g);
226 } else if (type == gate_plus || type == gate_times || type == gate_monus
227 || type == gate_project || type == gate_eq
228 || type == gate_cmp || type == gate_update
229 || type == gate_annotation || type == gate_conditioned) {
230 // Boolean gates: footprint is the union of children's footprints.
231 // Lets a compound `p` wire feeding a mixture propagate its atom
232 // dependencies to FootprintCache for the disjoint-children
233 // shortcuts in rec_expectation / rec_variance / rec_raw_moment.
234 for (gate_t c : gc_.getWires(g)) {
235 const auto &cs = of(c);
236 s.insert(cs.begin(), cs.end());
237 }
238 } else if (type == gate_zero || type == gate_one) {
239 // Empty footprint -- a constant-true / constant-false Boolean
240 // contributes no shared atoms.
241 } else {
242 // Unknown scalar gate type: return an empty footprint. Callers
243 // will trip the analytical-decomposition switch and route the
244 // gate to the MC fallback (or raise if the fallback is disabled),
245 // which is the right behaviour for any unanticipated leaf shape.
246 }
247 return cache_.emplace(g, std::move(s)).first->second;
248 }
249
250private:
251 const GenericCircuit &gc_;
252 std::unordered_map<gate_t, RvSet> cache_;
253};
254
255bool pairwise_disjoint(FootprintCache &fp, const std::vector<gate_t> &children)
256{
257 RvSet seen;
258 for (gate_t c : children) {
259 const auto &fpc = fp.of(c);
260 for (gate_t r : fpc) {
261 if (!seen.insert(r).second) return false;
262 }
263 }
264 return true;
265}
266
267/* Closed-form E[max] / E[min] of a gate_arith MAX/MIN whose children are
268 * independent bare gate_rv leaves of the SAME family with the SAME parameters
269 * (the i.i.d. order statistic). Returns std::nullopt when the shape is not
270 * i.i.d. bare-RV (mixture-wrapped aggregate children, mixed families, shared
271 * leaves, or a family without an elementary order-statistic mean) so the
272 * caller falls back to Monte Carlo.
273 *
274 * The per-family closed forms live in @c Distribution::iidOrderStatMean
275 * (Uniform, Exponential; Normal and Erlang i.i.d. maxima have no elementary
276 * closed form -- they need the 1-D order-statistic quadrature -- so they
277 * decline there).
278 */
279std::optional<double>
280iidOrderStatMean(const GenericCircuit &gc, gate_t g, bool isMax,
281 FootprintCache &fp)
282{
283 const auto &raw_wires = gc.getWires(g);
284 if (raw_wires.empty())
285 return std::nullopt;
286 /* Idempotence: max / min ignore repeats, so a child appearing more than
287 * once (the same gate) counts once. De-duplicating here generalises the
288 * closed form to any MAX/MIN gate -- without it a repeated child would
289 * collide with itself in pairwise_disjoint and silently drop to MC. */
290 std::vector<gate_t> wires;
291 wires.reserve(raw_wires.size());
292 {
293 std::set<gate_t> seen;
294 for (gate_t c : raw_wires)
295 if (seen.insert(c).second)
296 wires.push_back(c);
297 }
298 if (!pairwise_disjoint(fp, wires))
299 return std::nullopt; /* shared leaves -> correlated -> MC */
300
301 std::vector<DistributionSpec> specs;
302 specs.reserve(wires.size());
303 for (gate_t c : wires) {
304 if (gc.getGateType(c) != gate_rv)
305 return std::nullopt; /* not a bare RV (e.g. a mixture wrap) */
306 auto s = parse_distribution_spec(gc.getExtra(c));
307 if (!s)
308 return std::nullopt;
309 specs.push_back(*s);
310 }
311
312 /* i.i.d.: identical family and parameters across all children (the
313 * family descriptor is interned, so pointer comparison is family
314 * identity). */
315 for (std::size_t i = 1; i < specs.size(); ++i)
316 if (specs[i].family != specs[0].family ||
317 specs[i].p1 != specs[0].p1 || specs[i].p2 != specs[0].p2)
318 return std::nullopt;
319
320 return makeDistribution(specs[0])->iidOrderStatMean(specs.size(), isMax);
321}
322
323/* Closed-form (quadrature) E[max] / E[min] of a gate_arith MAX/MIN whose
324 * children are independent bare gate_rv leaves of *any* families (mixed or
325 * not-identical), generalising @c iidOrderStatMean. Uses the layer-cake
326 * identity over a window [lo, hi] covering every child's support:
327 * E[max] = lo + ∫ (1 − ∏ F_i(t)) dt, E[min] = lo + ∫ ∏ (1 − F_i(t)) dt.
328 * Composite Simpson. std::nullopt on shared leaves, a non-bare-RV child, or
329 * a distribution whose CDF is undefined (e.g. non-integer Erlang), so the
330 * caller falls back to Monte Carlo. */
331std::optional<double>
332mixedOrderStatMean(const GenericCircuit &gc, gate_t g, bool isMax,
333 FootprintCache &fp)
334{
335 const auto &raw_wires = gc.getWires(g);
336 if (raw_wires.empty())
337 return std::nullopt;
338 std::vector<gate_t> wires;
339 {
340 std::set<gate_t> seen;
341 for (gate_t c : raw_wires)
342 if (seen.insert(c).second)
343 wires.push_back(c);
344 }
345 if (!pairwise_disjoint(fp, wires))
346 return std::nullopt;
347
348 // Construct each child's Distribution once; the Simpson loop below calls
349 // cdf on them per point (never re-constructing per point).
350 std::vector<std::unique_ptr<Distribution>> dists;
351 double lo = 0.0, hi = 0.0;
352 bool first = true;
353 for (gate_t c : wires) {
354 if (gc.getGateType(c) != gate_rv)
355 return std::nullopt;
356 auto s = parse_distribution_spec(gc.getExtra(c));
357 if (!s)
358 return std::nullopt;
359 auto d = makeDistribution(*s);
360 double clo, chi;
361 if (!d->integrationRange(clo, chi))
362 return std::nullopt;
363 if (first) { lo = clo; hi = chi; first = false; }
364 else { lo = std::min(lo, clo); hi = std::max(hi, chi); }
365 dists.push_back(std::move(d));
366 }
367 if (!(hi > lo))
368 return std::nullopt;
369
370 const double integral = simpsonIntegrate(lo, hi, kSimpsonPanels,
371 [&](double t) {
372 if (isMax) {
373 double prodF = 1.0; /* ∏ F_i(t) = P(max ≤ t) */
374 for (const auto &d : dists) {
375 const double F = d->cdf(t);
376 if (std::isnan(F)) return std::numeric_limits<double>::quiet_NaN();
377 prodF *= F;
378 }
379 return 1.0 - prodF; /* P(max > t) */
380 }
381 double prod1mF = 1.0; /* ∏ (1 − F_i(t)) = P(min > t) */
382 for (const auto &d : dists) {
383 const double F = d->cdf(t);
384 if (std::isnan(F)) return std::numeric_limits<double>::quiet_NaN();
385 prod1mF *= (1.0 - F);
386 }
387 return prod1mF;
388 });
389 if (std::isnan(integral))
390 return std::nullopt;
391 return lo + integral;
392}
393
394unsigned mc_samples_or_throw(const std::string &what)
395{
396 const int n = provsql_rv_mc_samples;
397 if (n <= 0) {
398 throw CircuitException(
399 what + " could not be decomposed analytically and "
400 "provsql.rv_mc_samples = 0 disables the Monte Carlo fallback");
401 }
402 // Transparency: the analytic moment surface is about to return a Monte Carlo
403 // ESTIMATE, not a closed-form moment. Signal it (at the same verbose>=5
404 // evaluation tier as the probability-side approximation NOTICEs, so Studio
405 // and verbose users can tell an estimate from an exact value) -- the
406 // continuous-RV surface is approximate by nature, but never *silently* so.
407 // Set provsql.rv_mc_samples = 0 to require an exact result instead.
408 if (provsql_verbose >= 5)
410 "%s: no closed form found; estimating by Monte Carlo over %d samples "
411 "(an approximation, not an exact moment) -- set provsql.rv_mc_samples = 0 "
412 "to require an exact result instead", what.c_str(), n);
413 return static_cast<unsigned>(n);
414}
415
416/// Most Boolean inputs whose 2^n possible worlds the moment fallbacks
417/// enumerate, for an exact moment, before sampling.
418constexpr unsigned kEnumerateMaxInputs = 20;
419
420/**
421 * @brief The moment of order @p k of @p g, about @p mu (0 for a raw
422 * moment), given @p event, over the possible worlds of the circuit
423 * (@c enumerateScalarWorlds), or @c std::nullopt when they cannot be
424 * enumerated.
425 *
426 * The worlds where @p g is undefined (NaN: an aggregate over no row) are left
427 * out, as the samplers leave out such draws. NaN when it is never defined.
428 */
429std::optional<double> enumerated_moment(const GenericCircuit &gc, gate_t g,
430 unsigned k, double mu,
431 std::optional<gate_t> event,
432 const std::string &what)
433{
434 auto worlds = enumerateScalarWorlds(gc, g, event, kEnumerateMaxInputs);
435 if (!worlds) return std::nullopt;
436 if (event && worlds->empty())
437 throw CircuitException(what + ": conditioning event is infeasible");
438 double total = 0.0, mass = 0.0;
439 for (const auto &w : *worlds) {
440 if (std::isnan(w.second)) continue;
441 total += w.first * std::pow(w.second - mu, static_cast<double>(k));
442 mass += w.first;
443 }
444 if (!(mass > 0.0)) return std::numeric_limits<double>::quiet_NaN();
445 return total / mass;
446}
447
448double mc_raw_moment(const GenericCircuit &gc, gate_t g, unsigned k,
449 const std::string &what)
450{
451 if (auto m = enumerated_moment(gc, g, k, 0.0, std::nullopt, what))
452 return *m;
453 auto samples = monteCarloScalarSamples(gc, g, mc_samples_or_throw(what));
454 if (samples.empty()) return 0.0;
455 // NaN samples come from sampling-undefined worlds, e.g. an
456 // agg(SUM/AVG/MIN/MAX) over an empty group (SQL NULL). Treat them
457 // as missing observations of the moment rather than poisoning the
458 // mean; only return NaN if every sample was undefined.
459 double total = 0.0;
460 std::size_t finite_count = 0;
461 for (double x : samples) {
462 if (std::isnan(x)) continue;
463 total += std::pow(x, static_cast<double>(k));
464 ++finite_count;
465 }
466 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
467 return total / static_cast<double>(finite_count);
468}
469
470double mc_central_moment(const GenericCircuit &gc, gate_t g, unsigned k,
471 double mu, const std::string &what)
472{
473 if (auto m = enumerated_moment(gc, g, k, mu, std::nullopt, what))
474 return *m;
475 auto samples = monteCarloScalarSamples(gc, g, mc_samples_or_throw(what));
476 if (samples.empty()) return 0.0;
477 double total = 0.0;
478 std::size_t finite_count = 0;
479 for (double x : samples) {
480 if (std::isnan(x)) continue;
481 const double d = x - mu;
482 total += std::pow(d, static_cast<double>(k));
483 ++finite_count;
484 }
485 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
486 return total / static_cast<double>(finite_count);
487}
488
489/// Minimum accepted-sample count for conditional MC moments. Below
490/// this floor we'd be reporting a moment from a handful of accepted
491/// draws and the variance of the estimator would be enormous; raise
492/// rather than silently return a noisy number.
493unsigned min_accepted_floor(unsigned attempted)
494{
495 unsigned floor = attempted / 1000;
496 return floor < 5 ? 5 : floor;
497}
498
499void check_acceptance_or_throw(const ConditionalScalarSamples &cs,
500 const std::string &what)
501{
502 if (cs.accepted.empty()) {
503 /* 0-of-N accepted is the unmistakable signature of an infeasible
504 * conditioning event: raising rv_mc_samples cannot help (the
505 * acceptance probability is exactly 0). Surface that directly
506 * rather than the generic "raise samples or check satisfiability"
507 * advice that applies to merely under-sampled events. */
508 throw CircuitException(
509 what + ": conditioning event is infeasible (0 of " +
510 std::to_string(cs.attempted) +
511 " Monte Carlo samples satisfied it)");
512 }
513 const unsigned floor = min_accepted_floor(cs.attempted);
514 if (cs.accepted.size() < floor) {
515 throw CircuitException(
516 what + ": conditional MC accepted only " +
517 std::to_string(cs.accepted.size()) + " out of " +
518 std::to_string(cs.attempted) +
519 " samples (need >= " + std::to_string(floor) +
520 "); raise provsql.rv_mc_samples or tighten the event.");
521 }
522}
523
524double mc_conditional_raw_moment(const GenericCircuit &gc, gate_t g,
525 unsigned k, gate_t event_root,
526 const std::string &what)
527{
528 if (auto m = enumerated_moment(gc, g, k, 0.0, event_root, what))
529 return *m;
531 gc, g, event_root, mc_samples_or_throw(what));
532 check_acceptance_or_throw(cs, what);
533 // Mirror the unconditional path: NaN observations (sampling-
534 // undefined worlds, typically empty-group SQL NULLs from
535 // gate_agg) are excluded from the mean.
536 double total = 0.0;
537 std::size_t finite_count = 0;
538 for (double x : cs.accepted) {
539 if (std::isnan(x)) continue;
540 total += std::pow(x, static_cast<double>(k));
541 ++finite_count;
542 }
543 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
544 return total / static_cast<double>(finite_count);
545}
546
547double mc_conditional_central_moment(const GenericCircuit &gc, gate_t g,
548 unsigned k, double mu,
549 gate_t event_root,
550 const std::string &what)
551{
552 if (auto m = enumerated_moment(gc, g, k, mu, event_root, what))
553 return *m;
555 gc, g, event_root, mc_samples_or_throw(what));
556 check_acceptance_or_throw(cs, what);
557 double total = 0.0;
558 std::size_t finite_count = 0;
559 for (double x : cs.accepted) {
560 if (std::isnan(x)) continue;
561 const double d = x - mu;
562 total += std::pow(d, static_cast<double>(k));
563 ++finite_count;
564 }
565 if (finite_count == 0) return std::numeric_limits<double>::quiet_NaN();
566 return total / static_cast<double>(finite_count);
567}
568
569double rec_expectation(const GenericCircuit &gc, gate_t g, FootprintCache &fp);
570double rec_variance(const GenericCircuit &gc, gate_t g, FootprintCache &fp);
571double rec_raw_moment(const GenericCircuit &gc, gate_t g, unsigned k,
572 FootprintCache &fp);
573
574/* -----------------------------------------------------------------------
575 * Likelihood-weighting (importance-sampling) posterior readouts.
576 *
577 * When the conditioning event is continuous-density evidence (it contains
578 * a gate_observe), the analytic / rejection conditional paths do not apply:
579 * draw latents from the prior and weight each draw by the observations'
580 * densities, then report the weighted posterior statistic. These helpers
581 * turn one importance-sampling pass (WeightedPosterior) into a posterior
582 * raw moment / central moment / quantile.
583 * -------------------------------------------------------------------- */
584
585/* Self-normalised weighted raw moment Σ w x^k / Σ w. NaN particle values
586 * (sampling-undefined worlds, e.g. an empty-group aggregate) are skipped,
587 * mirroring the unconditional MC path. */
588double weightedRawMoment(const WeightedPosterior &post, unsigned k)
589{
590 double sw = 0.0, swx = 0.0;
591 for (const auto &[x, w] : post.particles) {
592 if (std::isnan(x)) continue;
593 sw += w;
594 swx += w * std::pow(x, static_cast<double>(k));
595 }
596 if (sw <= 0.0) return std::numeric_limits<double>::quiet_NaN();
597 return swx / sw;
598}
599
600/* Self-normalised weighted central moment Σ w (x-mu)^k / Σ w. */
601double weightedCentralMoment(const WeightedPosterior &post, unsigned k,
602 double mu)
603{
604 double sw = 0.0, swd = 0.0;
605 for (const auto &[x, w] : post.particles) {
606 if (std::isnan(x)) continue;
607 sw += w;
608 swd += w * std::pow(x - mu, static_cast<double>(k));
609 }
610 if (sw <= 0.0) return std::numeric_limits<double>::quiet_NaN();
611 return swd / sw;
612}
613
614/* Weighted empirical p-quantile: sort particles by value, walk the
615 * cumulative weight, linearly interpolate at p·(Σw) (the percentile_cont
616 * convention generalised to weights). */
617double weightedQuantile(WeightedPosterior post, double p)
618{
619 auto &pts = post.particles;
620 pts.erase(std::remove_if(pts.begin(), pts.end(),
621 [](const std::pair<double, double> &pw) {
622 return std::isnan(pw.first);
623 }),
624 pts.end());
625 if (pts.empty()) return std::numeric_limits<double>::quiet_NaN();
626 std::sort(pts.begin(), pts.end(),
627 [](const auto &a, const auto &b) { return a.first < b.first; });
628 double total = 0.0;
629 for (const auto &pw : pts) total += pw.second;
630 if (!(total > 0.0)) return pts.front().first;
631 const double target = p * total;
632 double cum = 0.0;
633 for (std::size_t i = 0; i < pts.size(); ++i) {
634 const double prev = cum;
635 cum += pts[i].second;
636 if (cum >= target) {
637 if (i == 0) return pts[0].first;
638 /* Linear interpolation between the two straddling particles at their
639 * cumulative-weight midpoints (matches the empirical MC quantile). */
640 const double frac = (pts[i].second > 0.0)
641 ? (target - prev) / pts[i].second
642 : 0.0;
643 return pts[i - 1].first + frac * (pts[i].first - pts[i - 1].first);
644 }
645 }
646 return pts.back().first;
647}
648
649/* Guard a posterior pass: raise on infeasible (no positive-weight draw)
650 * evidence, and warn when the effective sample size is degenerating. */
651void checkPosteriorOrThrow(const WeightedPosterior &post,
652 const std::string &what)
653{
654 if (post.particles.empty() || post.weight_sum <= 0.0) {
655 throw CircuitException(
656 what + ": evidence is infeasible (no positive-weight draw among " +
657 std::to_string(post.attempted) +
658 " Monte Carlo samples); the observations may contradict the prior, "
659 "or raise provsql.rv_mc_samples");
660 }
661 const double ess = post.effectiveSampleSize();
662 const double nonzero = static_cast<double>(post.particles.size());
663 if (provsql_ess_warn_fraction > 0.0 &&
664 ess < provsql_ess_warn_fraction * nonzero) {
666 "%s: posterior effective sample size low (%.1f of %u accepted); "
667 "likelihood weighting is degenerating -- raise provsql.rv_mc_samples, "
668 "or the model has many observations per latent (defer to SMC)",
669 what.c_str(), ess, static_cast<unsigned>(post.particles.size()));
670 }
671}
672
673/**
674 * @brief Try to evaluate @f$E[X^k \mid A]@f$ in closed form.
675 *
676 * Fires only when @p root is a bare @c gate_rv whose family has a
677 * closed-form truncated moment (@c Distribution::truncatedRawMoment)
678 * and the event walk under @p event_root collects a sound interval
679 * constraint on it.
680 * Otherwise returns @c std::nullopt and the caller falls through to
681 * MC rejection.
682 *
683 * For @p central, returns @f$E[(X - \mu_A)^k \mid A]@f$ where
684 * @f$\mu_A@f$ is the closed-form conditional mean obtained by
685 * recursing on @c k = 1, then binomially expanding the central
686 * moment in terms of the raw moments.
687 */
688std::optional<double>
689try_truncated_closed_form(const GenericCircuit &gc, gate_t root,
690 gate_t event_root, unsigned k, bool central)
691{
692 auto m = matchTruncatedSingleRv(gc, root, event_root);
693 if (!m) return std::nullopt;
694 const double lo = m->lo, hi = m->hi;
695
696 /* Closed-form raw moment of the truncated distribution; constructed
697 * once, then queried per moment order. A family without a closed
698 * form (Erlang: needs the regularised lower incomplete gamma) returns
699 * nullopt and the caller falls through to MC. */
700 const auto dist = makeDistribution(m->spec);
701 auto raw = [&](unsigned q) -> std::optional<double> {
702 if (q == 0) return 1.0;
703 return dist->truncatedRawMoment(lo, hi, q);
704 };
705
706 if (!central) return raw(k);
707 /* Central: E[(X - μ_A)^k | A] via the binomial expansion. */
708 return centralFromRaw(k, raw);
709}
710
711/* A conditioning event of the shape @c "X op Y", where the target @c X and the
712 * other operand @c Y are two independent bare @c gate_rv leaves. */
713struct RvVsRvCond {
714 DistributionSpec targetSpec; /* X (the moment's target) */
715 DistributionSpec otherSpec; /* Y */
716 bool targetGreater; /* true for X > Y, false for X < Y */
717};
718
719/* Match @p event_root as a single @c gate_cmp comparing the target @p root
720 * (a bare RV X) with an independent bare RV Y. Returns std::nullopt for any
721 * other shape (constant threshold -- handled by the truncation path;
722 * conjunctions; agg comparisons; shared operand) so the caller falls through. */
723std::optional<RvVsRvCond>
724matchRvVsRvConditional(const GenericCircuit &gc, gate_t root, gate_t event_root)
725{
726 if (gc.getGateType(root) != gate_rv || gc.getGateType(event_root) != gate_cmp)
727 return std::nullopt;
728 auto specX = parse_distribution_spec(gc.getExtra(root));
729 if (!specX) return std::nullopt;
730
731 const auto &wires = gc.getWires(event_root);
732 if (wires.size() != 2) return std::nullopt;
733 bool ok = false;
734 ComparisonOperator op = cmpOpFromOid(gc.getInfos(event_root).first, ok);
735 if (!ok || op == ComparisonOperator::EQ || op == ComparisonOperator::NE)
736 return std::nullopt;
737
738 gate_t other;
739 bool targetLeft;
740 if (wires[0] == root) { other = wires[1]; targetLeft = true; }
741 else if (wires[1] == root) { other = wires[0]; targetLeft = false; }
742 else return std::nullopt; /* target not an operand */
743 if (other == root || gc.getGateType(other) != gate_rv)
744 return std::nullopt; /* X op X, or Y not a bare RV */
745 auto specY = parse_distribution_spec(gc.getExtra(other));
746 if (!specY) return std::nullopt;
747
748 /* targetGreater: does the event assert X > Y? If X is the left operand,
749 * that is op in {GT,GE}; if X is the right operand (event Y op X), it is
750 * op in {LT,LE}. */
751 const bool greaterOp = (op == ComparisonOperator::GT ||
753 const bool lessOp = (op == ComparisonOperator::LT ||
755 bool targetGreater;
756 if (targetLeft) targetGreater = greaterOp;
757 else targetGreater = lessOp;
758 return RvVsRvCond{*specX, *specY, targetGreater};
759}
760
761/* E[X^k | X op Y] for independent X, Y via a 1-D quadrature:
762 * E[X^k | X>Y] = (∫ x^k f_X(x) F_Y(x) dx) / (∫ f_X(x) F_Y(x) dx),
763 * and the X<Y case swaps F_Y for 1-F_Y. Composite Simpson over X's support;
764 * exact for the Uniform-Uniform case (the integrands are low-degree
765 * polynomials), high-accuracy otherwise. Returns NaN if the event mass is
766 * negligible or a density/CDF is undefined. */
767double rvVsRvConditionalMoment(const DistributionSpec &X,
768 const DistributionSpec &Y,
769 bool targetGreater, unsigned k)
770{
771 // Construct both distributions once; the Simpson loop calls pdf/cdf on
772 // them per point (never re-constructing per point).
773 const auto dX = makeDistribution(X);
774 const auto dY = makeDistribution(Y);
775 double lo, hi;
776 if (!dX->integrationRange(lo, hi))
777 return std::numeric_limits<double>::quiet_NaN();
778
779 auto base = [&](double x) {
780 const double fX = dX->pdf(x);
781 const double FY = dY->cdf(x);
782 if (std::isnan(fX) || std::isnan(FY))
783 return std::numeric_limits<double>::quiet_NaN();
784 const double w = targetGreater ? FY : (1.0 - FY); /* P(Y<x) / P(Y>x) */
785 return fX * w;
786 };
787 const double den = simpsonIntegrate(lo, hi, kSimpsonPanels, base);
788 if (std::isnan(den) || !(den > 1e-12))
789 return std::numeric_limits<double>::quiet_NaN();
790 const double num = simpsonIntegrate(lo, hi, kSimpsonPanels,
791 [&](double x) {
792 return std::pow(x, static_cast<double>(k)) * base(x);
793 });
794 if (std::isnan(num))
795 return std::numeric_limits<double>::quiet_NaN();
796 return num / den;
797}
798
799/* Closed-form (quadrature) E[X^k | X op Y] / central moment for an
800 * RV-vs-RV conditioning event. Mirrors @c try_truncated_closed_form for the
801 * constant-threshold case. */
802std::optional<double>
803try_rvVsRv_conditional_moment(const GenericCircuit &gc, gate_t root,
804 gate_t event_root, unsigned k, bool central)
805{
806 auto m = matchRvVsRvConditional(gc, root, event_root);
807 if (!m) return std::nullopt;
808
809 auto raw = [&](unsigned q) -> std::optional<double> {
810 if (q == 0) return 1.0;
811 double r = rvVsRvConditionalMoment(m->targetSpec, m->otherSpec,
812 m->targetGreater, q);
813 if (std::isnan(r)) return std::nullopt;
814 return r;
815 };
816
817 if (!central) return raw(k);
818 return centralFromRaw(k, raw);
819}
820
821/* One comparison of a pivot RV X against another operand, a factor in the
822 * pivot-conjunction integrand. `other` is an independent bare RV (its CDF
823 * weights the integrand) or, when `isConst`, a constant threshold that clips
824 * the integration window. `pivotGreater` is true when the factor asserts
825 * X > operand. */
826struct PivotFactor {
827 bool isConst;
828 DistributionSpec other; /* valid iff !isConst */
829 double konst; /* valid iff isConst */
830 bool pivotGreater;
831};
832
833/* ∫ x^k f_X(x) Π_j W_j(x) dx over X's support, where each RV factor contributes
834 * W_j(x) = F_{Y_j}(x) for X>Y_j and 1-F_{Y_j}(x) for X<Y_j, and each
835 * constant factor clips the window to {x : x>c} / {x : x<c}. With k=0 this is
836 * the joint probability P(∧_j comparisons). Because the comparisons all share
837 * the single pivot X and the other operands are independent, marginalising each
838 * Y_j analytically collapses the joint to this 1-D integral. Composite Simpson
839 * (exact for the polynomial integrands of the Uniform case). Returns NaN if a
840 * density / CDF is undefined. */
841double pivotConjunctionIntegral(const DistributionSpec &X,
842 const std::vector<PivotFactor> &factors,
843 unsigned k)
844{
845 const auto dX = makeDistribution(X);
846 double lo, hi;
847 if (!dX->integrationRange(lo, hi))
848 return std::numeric_limits<double>::quiet_NaN();
849 for (const auto &f : factors)
850 if (f.isConst) {
851 if (f.pivotGreater) lo = std::max(lo, f.konst);
852 else hi = std::min(hi, f.konst);
853 }
854 if (!(hi > lo)) return 0.0;
855
856 std::vector<std::unique_ptr<Distribution>> others;
857 std::vector<bool> greater;
858 for (const auto &f : factors)
859 if (!f.isConst) {
860 others.push_back(makeDistribution(f.other));
861 greater.push_back(f.pivotGreater);
862 }
863
864 return simpsonIntegrate(lo, hi, kSimpsonPanels, [&](double x) {
865 const double fX = dX->pdf(x);
866 if (std::isnan(fX)) return std::numeric_limits<double>::quiet_NaN();
867 double w = fX;
868 for (std::size_t j = 0; j < others.size(); ++j) {
869 const double FY = others[j]->cdf(x);
870 if (std::isnan(FY)) return std::numeric_limits<double>::quiet_NaN();
871 w *= greater[j] ? FY : (1.0 - FY);
872 }
873 return std::pow(x, static_cast<double>(k)) * w;
874 });
875}
876
877/* A conditioning event that is a conjunction of comparisons all sharing the
878 * target bare RV X, each against an independent bare RV or a constant. */
879struct PivotConjunctionCond {
880 DistributionSpec targetSpec;
881 std::vector<PivotFactor> factors;
882};
883
884/* Match @p event_root as a @c gate_times (AND) of >=2 @c gate_cmp, each
885 * comparing the target @p root (bare RV X) with an independent bare RV or a
886 * constant. Returns nullopt for a single comparison (the truncation / rvVsRv
887 * paths own that), an agg comparison, a cmp not involving X, a non-bare other
888 * operand, or a repeated / shared other operand (would break independence). */
889std::optional<PivotConjunctionCond>
890matchPivotConjunctionConditional(const GenericCircuit &gc, gate_t root,
891 gate_t event_root)
892{
893 if (gc.getGateType(root) != gate_rv ||
894 gc.getGateType(event_root) != gate_times)
895 return std::nullopt;
896 auto specX = parse_distribution_spec(gc.getExtra(root));
897 if (!specX) return std::nullopt;
898
899 const auto &kids = gc.getWires(event_root);
900 if (kids.size() < 2) return std::nullopt;
901 std::vector<PivotFactor> factors;
902 std::set<gate_t> othersSeen;
903 for (gate_t c : kids) {
904 if (gc.getGateType(c) != gate_cmp) return std::nullopt;
905 const auto &w = gc.getWires(c);
906 if (w.size() != 2) return std::nullopt;
907 bool ok = false;
908 ComparisonOperator op = cmpOpFromOid(gc.getInfos(c).first, ok);
909 if (!ok || op == ComparisonOperator::EQ || op == ComparisonOperator::NE)
910 return std::nullopt;
911 gate_t other; bool targetLeft;
912 if (w[0] == root) { other = w[1]; targetLeft = true; }
913 else if (w[1] == root) { other = w[0]; targetLeft = false; }
914 else return std::nullopt;
915 const bool greaterOp = (op == ComparisonOperator::GT ||
917 const bool lessOp = (op == ComparisonOperator::LT ||
919 const bool pivotGreater = targetLeft ? greaterOp : lessOp;
920
921 PivotFactor f;
922 if (gc.getGateType(other) == gate_value) {
923 f = {true, DistributionSpec{}, parseDoubleStrict(gc.getExtra(other)),
924 pivotGreater};
925 } else if (gc.getGateType(other) == gate_rv && other != root) {
926 if (!othersSeen.insert(other).second) return std::nullopt;
927 auto specY = parse_distribution_spec(gc.getExtra(other));
928 if (!specY) return std::nullopt;
929 f = {false, *specY, 0.0, pivotGreater};
930 } else return std::nullopt;
931 factors.push_back(std::move(f));
932 }
933 return PivotConjunctionCond{*specX, std::move(factors)};
934}
935
936/* Closed-form (quadrature) E[X^k | ∧_j (X op Y_j)] for a conjunction of
937 * comparisons sharing the target X. E[X^k | A] = I_k / I_0 with
938 * I_q = ∫ x^q f_X Π_j W_j dx; central via the usual binomial expansion. */
939std::optional<double>
940try_pivotConjunction_conditional_moment(const GenericCircuit &gc, gate_t root,
941 gate_t event_root, unsigned k,
942 bool central)
943{
944 auto m = matchPivotConjunctionConditional(gc, root, event_root);
945 if (!m) return std::nullopt;
946
947 const double den = pivotConjunctionIntegral(m->targetSpec, m->factors, 0);
948 if (std::isnan(den) || !(den > 1e-12))
949 return std::nullopt; /* negligible / undefined event -> infeasible or MC */
950 auto raw = [&](unsigned q) -> std::optional<double> {
951 if (q == 0) return 1.0;
952 const double num = pivotConjunctionIntegral(m->targetSpec, m->factors, q);
953 if (std::isnan(num)) return std::nullopt;
954 return num / den;
955 };
956
957 if (!central) return raw(k);
958 return centralFromRaw(k, raw);
959}
960
961/* Closed-form image of a unary LN / EXP transform over a bare gate_rv
962 * child, via the TransformRuleRegistry (exp(normal) is lognormal,
963 * ln(lognormal) is normal). Read-only: unlike the hybrid simplifier's
964 * fold of the same shape, nothing is rewritten, so no shared-RV
965 * identity can be decoupled -- which is why the moment path may use it
966 * even though it deliberately does not run the simplifier. nullptr
967 * when the child is not a bare parseable rv or no rule covers the
968 * family; the caller falls to MC. */
969std::unique_ptr<Distribution> transform_image(const GenericCircuit &gc,
970 gate_t g,
972{
973 const char *transform = op == PROVSQL_ARITH_LN ? "ln"
974 : op == PROVSQL_ARITH_EXP ? "exp"
975 : nullptr;
976 if (!transform) return nullptr;
977 const auto &wires = gc.getWires(g);
978 if (wires.size() != 1 || gc.getGateType(wires[0]) != gate_rv)
979 return nullptr;
980 auto spec = parse_distribution_spec(gc.getExtra(wires[0]));
981 if (!spec) return nullptr;
982 return closeTransform(transform, *makeDistribution(*spec));
983}
984
985/* Closed-form distribution of a TIMES product over bare, pairwise
986 * distinct gate_rv factors (plus gate_value scalars), via the
987 * ProductRuleRegistry -- read-only, like transform_image, so no shared
988 * identity is disturbed. Quantiles need it (they do not factor the way
989 * the disjoint-product moment shortcuts do); nullptr when the shape or
990 * family combination is outside the registered closures. */
991std::unique_ptr<Distribution> product_image(const GenericCircuit &gc,
992 gate_t g)
993{
994 const auto &wires = gc.getWires(g);
995 if (wires.empty()) return nullptr;
996 double c_total = 1.0;
997 std::vector<std::unique_ptr<Distribution>> dists;
998 std::vector<const Distribution *> factors;
999 std::set<gate_t> seen;
1000 for (gate_t w : wires) {
1001 const auto t = gc.getGateType(w);
1002 if (t == gate_value) {
1003 try { c_total *= parseDoubleStrict(gc.getExtra(w)); }
1004 catch (const CircuitException &) { return nullptr; }
1005 continue;
1006 }
1007 if (t != gate_rv) return nullptr;
1008 if (!seen.insert(w).second) return nullptr; /* dependent */
1009 auto spec = parse_distribution_spec(gc.getExtra(w));
1010 if (!spec) return nullptr;
1011 dists.push_back(makeDistribution(*spec));
1012 factors.push_back(dists.back().get());
1013 }
1014 if (factors.empty()) return nullptr;
1015 /* A single RV with scalar factors is just the affine image; two or
1016 * more dispatch through the product registry. */
1017 std::unique_ptr<Distribution> combined;
1018 if (factors.size() == 1)
1019 combined = std::move(dists.front());
1020 else
1021 combined = closeProductFactors(factors);
1022 if (!combined) return nullptr;
1023 if (c_total != 1.0) return combined->scale(c_total);
1024 return combined;
1025}
1026
1027/* ------------------------------------------------------------------------
1028 * Tier A of the gate_case guard-partition integrator: a CASE that is a
1029 * piecewise function of a SINGLE pivot random variable X. Every guard is a
1030 * bare gate_cmp comparing X against a constant, and every arm value (and the
1031 * default) is affine in X -- a constant, or a*X + b. This is exactly the
1032 * abs / ReLU / clamp piecewise-sugar shape.
1033 *
1034 * Since the whole CASE depends on one RV, E[case^k] is a 1-D integral that
1035 * partitions X's support at the guard thresholds: on each sub-interval a
1036 * single arm fires (first-match, reproducing MonteCarloSampler's order), and
1037 * its affine value's k-th moment over that interval is a binomial combination
1038 * of the truncated raw moments
1039 * ∫_lo^hi x^j f(x) dx = truncatedRawMoment(lo,hi,j) · (F(hi)-F(lo)).
1040 * Returns std::nullopt when the shape is not single-pivot-affine or the family
1041 * lacks a closed-form truncated moment, so the caller falls back to MC. */
1042
1043struct AffineArm { double a, b; }; /* value = a*X + b */
1044
1045std::optional<AffineArm>
1046affineInPivot(const GenericCircuit &gc, gate_t arm, gate_t pivot)
1047{
1048 const auto t = gc.getGateType(arm);
1049 if (t == gate_value)
1050 return AffineArm{0.0, parseDoubleStrict(gc.getExtra(arm))};
1051 if (t == gate_rv)
1052 return (arm == pivot) ? std::optional<AffineArm>(AffineArm{1.0, 0.0})
1053 : std::nullopt;
1054 if (t == gate_arith) {
1055 const auto op = static_cast<provsql_arith_op>(gc.getInfos(arm).first);
1056 const auto &w = gc.getWires(arm);
1057 auto isPivot = [&](gate_t x) {
1058 return gc.getGateType(x) == gate_rv && x == pivot;
1059 };
1060 auto constVal = [&](gate_t x, double &out) {
1061 if (gc.getGateType(x) != gate_value) return false;
1062 out = parseDoubleStrict(gc.getExtra(x));
1063 return true;
1064 };
1065 double c;
1066 if (op == PROVSQL_ARITH_NEG && w.size() == 1 && isPivot(w[0]))
1067 return AffineArm{-1.0, 0.0};
1068 if (op == PROVSQL_ARITH_PLUS && w.size() == 2) {
1069 if (isPivot(w[0]) && constVal(w[1], c)) return AffineArm{1.0, c};
1070 if (isPivot(w[1]) && constVal(w[0], c)) return AffineArm{1.0, c};
1071 }
1072 if (op == PROVSQL_ARITH_MINUS && w.size() == 2) {
1073 if (isPivot(w[0]) && constVal(w[1], c)) return AffineArm{1.0, -c}; /* X - c */
1074 if (isPivot(w[1]) && constVal(w[0], c)) return AffineArm{-1.0, c}; /* c - X */
1075 }
1076 if (op == PROVSQL_ARITH_TIMES && w.size() == 2) {
1077 if (isPivot(w[0]) && constVal(w[1], c)) return AffineArm{c, 0.0};
1078 if (isPivot(w[1]) && constVal(w[0], c)) return AffineArm{c, 0.0};
1079 }
1080 }
1081 return std::nullopt;
1082}
1083
1084std::optional<double>
1085singlePivotCaseRawMoment(const GenericCircuit &gc, gate_t g, unsigned k)
1086{
1087 if (gc.getGateType(g) != gate_case) return std::nullopt;
1088 const auto &wires = gc.getWires(g);
1089 if (wires.size() < 3 || wires.size() % 2 == 0) return std::nullopt;
1090 const std::size_t m = wires.size() / 2; /* guard/value pairs */
1091
1092 /* ---- identify the common pivot RV and each guard's constant threshold ---- */
1093 gate_t pivot{}; bool havePivot = false;
1094 struct Guard { double c; ComparisonOperator op; bool pivotLeft; };
1095 std::vector<Guard> guards;
1096 guards.reserve(m);
1097 for (std::size_t i = 0; i < m; ++i) {
1098 gate_t gd = wires[2 * i];
1099 if (gc.getGateType(gd) != gate_cmp) return std::nullopt;
1100 const auto &gw = gc.getWires(gd);
1101 if (gw.size() != 2) return std::nullopt;
1102 bool ok = false;
1103 ComparisonOperator op = cmpOpFromOid(gc.getInfos(gd).first, ok);
1104 if (!ok || op == ComparisonOperator::EQ || op == ComparisonOperator::NE)
1105 return std::nullopt;
1106 gate_t rvSide, constSide; bool pivotLeft;
1107 if (gc.getGateType(gw[0]) == gate_rv && gc.getGateType(gw[1]) == gate_value) {
1108 rvSide = gw[0]; constSide = gw[1]; pivotLeft = true;
1109 } else if (gc.getGateType(gw[1]) == gate_rv &&
1110 gc.getGateType(gw[0]) == gate_value) {
1111 rvSide = gw[1]; constSide = gw[0]; pivotLeft = false;
1112 } else return std::nullopt;
1113 if (!havePivot) { pivot = rvSide; havePivot = true; }
1114 else if (rvSide != pivot) return std::nullopt; /* multi-pivot -> Tier B */
1115 guards.push_back({parseDoubleStrict(gc.getExtra(constSide)), op, pivotLeft});
1116 }
1117 if (!havePivot) return std::nullopt;
1118
1119 auto spec = parse_distribution_spec(gc.getExtra(pivot));
1120 if (!spec) return std::nullopt;
1121 const auto dist = makeDistribution(*spec);
1122
1123 /* ---- classify each arm (values then default) as affine in the pivot ---- */
1124 std::vector<AffineArm> arms; /* arms[0..m-1] value branches, arms[m] default */
1125 arms.reserve(m + 1);
1126 for (std::size_t i = 0; i < m; ++i) {
1127 auto af = affineInPivot(gc, wires[2 * i + 1], pivot);
1128 if (!af) return std::nullopt;
1129 arms.push_back(*af);
1130 }
1131 {
1132 auto af = affineInPivot(gc, wires.back(), pivot);
1133 if (!af) return std::nullopt;
1134 arms.push_back(*af);
1135 }
1136
1137 /* First-match arm selection at a pivot value x (guards are half-lines, so
1138 * this is constant across the open interior of every partition segment). */
1139 auto guardTrue = [](const Guard &gd, double x) -> bool {
1140 const double lhs = gd.pivotLeft ? x : gd.c;
1141 const double rhs = gd.pivotLeft ? gd.c : x;
1142 switch (gd.op) {
1143 case ComparisonOperator::LT: return lhs < rhs;
1144 case ComparisonOperator::LE: return lhs <= rhs;
1145 case ComparisonOperator::GT: return lhs > rhs;
1146 case ComparisonOperator::GE: return lhs >= rhs;
1147 default: return false;
1148 }
1149 };
1150 auto pickArm = [&](double x) -> const AffineArm & {
1151 for (std::size_t i = 0; i < m; ++i)
1152 if (guardTrue(guards[i], x)) return arms[i];
1153 return arms[m];
1154 };
1155
1156 /* ---- partition the pivot's support at the (sorted, deduped) thresholds ---- */
1157 std::vector<double> cuts;
1158 for (const auto &gd : guards) cuts.push_back(gd.c);
1159 std::sort(cuts.begin(), cuts.end());
1160 cuts.erase(std::unique(cuts.begin(), cuts.end()), cuts.end());
1161
1162 const double NINF = -std::numeric_limits<double>::infinity();
1163 const double PINF = std::numeric_limits<double>::infinity();
1164
1165 double total = 0.0;
1166 const std::size_t nseg = cuts.size() + 1;
1167 for (std::size_t s = 0; s < nseg; ++s) {
1168 const double lo = (s == 0) ? NINF : cuts[s - 1];
1169 const double hi = (s == cuts.size()) ? PINF : cuts[s];
1170 if (lo == hi) continue;
1171
1172 double t; /* interior test point of (lo, hi) */
1173 if (lo == NINF && hi == PINF) t = 0.0;
1174 else if (lo == NINF) t = hi - 1.0;
1175 else if (hi == PINF) t = lo + 1.0;
1176 else t = 0.5 * (lo + hi);
1177 const AffineArm &arm = pickArm(t);
1178
1179 const double dF = dist->cdf(hi) - dist->cdf(lo);
1180 if (std::isnan(dF)) return std::nullopt;
1181 if (dF <= 0.0) continue;
1182
1183 /* E[(aX+b)^k · 1(lo<X<hi)] = Σ_j C(k,j) a^j b^{k-j} ∫ x^j f dx. */
1184 for (unsigned j = 0; j <= k; ++j) {
1185 const double aj = std::pow(arm.a, static_cast<double>(j));
1186 if (aj == 0.0) continue; /* constant arm: only the j=0 term */
1187 const double coef = binomial(k, j) * aj
1188 * std::pow(arm.b, static_cast<double>(k - j));
1189 if (coef == 0.0) continue;
1190 double intj;
1191 if (j == 0) {
1192 intj = dF;
1193 } else {
1194 auto trm = dist->truncatedRawMoment(lo, hi, j);
1195 if (!trm) return std::nullopt; /* family lacks the closed form -> MC */
1196 intj = *trm * dF;
1197 }
1198 total += coef * intj;
1199 }
1200 }
1201 return total;
1202}
1203
1204/* Tier B of the guard-partition integrator: a two-arm CASE
1205 * CASE WHEN (a op b) THEN A ELSE B
1206 * whose single guard compares two DISTINCT independent bare RVs a, b, and whose
1207 * arms A (under the guard) and B (under its complement) are each affine in one
1208 * of a, b. Each arm's contribution is a 1-D integral over its value's pivot
1209 * RV, the other operand marginalised to a CDF weight -- exactly the pivot-
1210 * conjunction integral with a single factor. nullopt for any other shape
1211 * (guard vs a constant is Tier A; >2 arms; an arm not affine in a comparison
1212 * operand; a family without a usable pdf/cdf), so the caller falls to MC. */
1213std::optional<double>
1214twoArmCaseRawMoment(const GenericCircuit &gc, gate_t g, unsigned k)
1215{
1216 if (gc.getGateType(g) != gate_case) return std::nullopt;
1217 const auto &wires = gc.getWires(g);
1218 if (wires.size() != 3) return std::nullopt; /* one guard/value + default */
1219 gate_t guard = wires[0];
1220 if (gc.getGateType(guard) != gate_cmp) return std::nullopt;
1221 const auto &gw = gc.getWires(guard);
1222 if (gw.size() != 2) return std::nullopt;
1223 bool ok = false;
1224 ComparisonOperator op = cmpOpFromOid(gc.getInfos(guard).first, ok);
1225 if (!ok || op == ComparisonOperator::EQ || op == ComparisonOperator::NE)
1226 return std::nullopt;
1227 gate_t a = gw[0], b = gw[1];
1228 if (gc.getGateType(a) != gate_rv || gc.getGateType(b) != gate_rv || a == b)
1229 return std::nullopt;
1230 auto specA = parse_distribution_spec(gc.getExtra(a));
1231 auto specB = parse_distribution_spec(gc.getExtra(b));
1232 if (!specA || !specB) return std::nullopt;
1233 /* op GT/GE with a as the left operand => the guard asserts a > b. */
1234 const bool guard_a_gt_b = (op == ComparisonOperator::GT ||
1236
1237 /* E[value^k · 1(region)] for an arm whose value is affine in a or b, where
1238 * `regionAgtB` says the arm's region is (a > b). */
1239 auto armContribution = [&](gate_t armGate, bool regionAgtB)
1240 -> std::optional<double> {
1241 for (int which = 0; which < 2; ++which) {
1242 gate_t pv = (which == 0) ? a : b;
1243 auto af = affineInPivot(gc, armGate, pv);
1244 if (!af) continue;
1245 const DistributionSpec &pvSpec = (which == 0) ? *specA : *specB;
1246 const DistributionSpec &otSpec = (which == 0) ? *specB : *specA;
1247 /* Weight orientation: with pivot a the region a>b integrates b to F_b(a);
1248 * with pivot b the same region means b<a, i.e. 1-F_a(b). */
1249 const bool pivotGreater = (which == 0) ? regionAgtB : !regionAgtB;
1250 const PivotFactor f{false, otSpec, 0.0, pivotGreater};
1251 double total = 0.0;
1252 for (unsigned j = 0; j <= k; ++j) {
1253 const double aj = std::pow(af->a, static_cast<double>(j));
1254 if (aj == 0.0) continue;
1255 const double coef = binomial(k, j) * aj
1256 * std::pow(af->b, static_cast<double>(k - j));
1257 if (coef == 0.0) continue;
1258 const double I = pivotConjunctionIntegral(pvSpec, {f}, j);
1259 if (std::isnan(I)) return std::nullopt;
1260 total += coef * I;
1261 }
1262 return total;
1263 }
1264 return std::nullopt; /* arm not affine in either comparison operand */
1265 };
1266
1267 auto c0 = armContribution(wires[1], guard_a_gt_b); /* value under guard */
1268 if (!c0) return std::nullopt;
1269 auto c1 = armContribution(wires[2], !guard_a_gt_b); /* default under ¬guard */
1270 if (!c1) return std::nullopt;
1271 return *c0 + *c1;
1272}
1273
1274/* Evaluate a gate_case guard (a bare gate_cmp, or an AND/OR tree of them over
1275 * value RVs) under a strict ordering `rank` of the RVs (higher rank = larger
1276 * value). Returns nullopt if the guard references a constant, an RV outside
1277 * `rank`, or an unsupported gate -- i.e. the CASE is not a pure RV tournament. */
1278std::optional<bool>
1279evalGuardUnderOrder(const GenericCircuit &gc, gate_t guard,
1280 const std::unordered_map<gate_t, int> &rank)
1281{
1282 const auto t = gc.getGateType(guard);
1283 if (t == gate_cmp) {
1284 const auto &w = gc.getWires(guard);
1285 if (w.size() != 2) return std::nullopt;
1286 bool ok = false;
1287 ComparisonOperator op = cmpOpFromOid(gc.getInfos(guard).first, ok);
1288 if (!ok) return std::nullopt;
1289 auto ia = rank.find(w[0]), ib = rank.find(w[1]);
1290 if (ia == rank.end() || ib == rank.end()) return std::nullopt;
1291 const int ra = ia->second, rb = ib->second; /* distinct RVs -> ra != rb */
1292 switch (op) {
1294 case ComparisonOperator::LE: return ra < rb;
1296 case ComparisonOperator::GE: return ra > rb;
1297 case ComparisonOperator::EQ: return false; /* a.s. for continuous RVs */
1298 case ComparisonOperator::NE: return true;
1299 }
1300 return std::nullopt;
1301 }
1302 if (t == gate_times || t == gate_plus) {
1303 const bool isAnd = (t == gate_times);
1304 bool acc = isAnd;
1305 for (gate_t c : gc.getWires(guard)) {
1306 auto v = evalGuardUnderOrder(gc, c, rank);
1307 if (!v) return std::nullopt;
1308 acc = isAnd ? (acc && *v) : (acc || *v);
1309 }
1310 return acc;
1311 }
1312 return std::nullopt;
1313}
1314
1315/* Tier C: a first-match gate_case that computes the max or min of a set of
1316 * independent bare RVs. Recognised by simulating the first-match selection
1317 * over every strict ordering of the value RVs (continuous RVs tie with
1318 * probability 0, so strict orders capture the a.s. behaviour): if the selected
1319 * value is always the maximum (resp. minimum), the CASE is that order
1320 * statistic, whose k-th moment is
1321 * Σ_i ∫ x^k f_{X_i}(x) Π_{j≠i} F_{X_j}(x) dx (max; min flips F to 1-F),
1322 * a sum of pivot-conjunction integrals. Guards may be AND/OR trees of RV-vs-RV
1323 * comparisons among the value RVs; a constant or an outside RV in a guard, a
1324 * non-bare-RV arm, or too many RVs (the n! simulation is capped) decline. */
1325std::optional<double>
1326orderStatCaseRawMoment(const GenericCircuit &gc, gate_t g, unsigned k)
1327{
1328 if (gc.getGateType(g) != gate_case) return std::nullopt;
1329 const auto &wires = gc.getWires(g);
1330 if (wires.size() < 3 || wires.size() % 2 == 0) return std::nullopt;
1331 const std::size_t m = wires.size() / 2;
1332
1333 /* Arms (values + default) must all be bare RVs; collect the distinct set. */
1334 std::vector<gate_t> armRV(m + 1);
1335 std::vector<gate_t> uniq;
1336 std::unordered_map<gate_t, DistributionSpec> specOf;
1337 auto noteRV = [&](gate_t v) -> bool {
1338 if (gc.getGateType(v) != gate_rv) return false;
1339 if (specOf.find(v) == specOf.end()) {
1340 auto sp = parse_distribution_spec(gc.getExtra(v));
1341 if (!sp) return false;
1342 specOf.emplace(v, *sp);
1343 uniq.push_back(v);
1344 }
1345 return true;
1346 };
1347 for (std::size_t i = 0; i < m; ++i) {
1348 armRV[i] = wires[2 * i + 1];
1349 if (!noteRV(armRV[i])) return std::nullopt;
1350 }
1351 armRV[m] = wires.back();
1352 if (!noteRV(armRV[m])) return std::nullopt;
1353
1354 const std::size_t n = uniq.size();
1355 if (n < 2 || n > 7) return std::nullopt; /* n! simulation cap */
1356
1357 /* Guards must reference only the value RVs (checked implicitly by
1358 * evalGuardUnderOrder, which declines on any leaf outside `rank`). */
1359 std::vector<gate_t> guards(m);
1360 for (std::size_t i = 0; i < m; ++i) guards[i] = wires[2 * i];
1361
1362 /* Simulate first-match over every strict ordering of the RVs. */
1363 std::vector<std::size_t> perm(n);
1364 for (std::size_t i = 0; i < n; ++i) perm[i] = i;
1365 bool alwaysMax = true, alwaysMin = true;
1366 do {
1367 std::unordered_map<gate_t, int> rank;
1368 for (std::size_t i = 0; i < n; ++i) rank[uniq[perm[i]]] = static_cast<int>(i);
1369 /* max / min RV under this ordering (highest / lowest rank). */
1370 gate_t maxRV = uniq[perm[n - 1]], minRV = uniq[perm[0]];
1371
1372 gate_t selected = armRV[m]; /* default if no guard fires */
1373 for (std::size_t i = 0; i < m; ++i) {
1374 auto gv = evalGuardUnderOrder(gc, guards[i], rank);
1375 if (!gv) return std::nullopt;
1376 if (*gv) { selected = armRV[i]; break; }
1377 }
1378 if (selected != maxRV) alwaysMax = false;
1379 if (selected != minRV) alwaysMin = false;
1380 if (!alwaysMax && !alwaysMin) return std::nullopt;
1381 } while (std::next_permutation(perm.begin(), perm.end()));
1382
1383 const bool isMax = alwaysMax; /* prefer max if (degenerately) both hold */
1384
1385 /* Σ_i ∫ x^k f_{X_i} Π_{j≠i} (F_{X_j} or 1-F_{X_j}) dx. */
1386 double total = 0.0;
1387 for (std::size_t i = 0; i < n; ++i) {
1388 std::vector<PivotFactor> factors;
1389 factors.reserve(n - 1);
1390 for (std::size_t j = 0; j < n; ++j) {
1391 if (j == i) continue;
1392 factors.push_back({false, specOf.at(uniq[j]), 0.0, isMax});
1393 }
1394 const double I = pivotConjunctionIntegral(specOf.at(uniq[i]), factors, k);
1395 if (std::isnan(I)) return std::nullopt;
1396 total += I;
1397 }
1398 return total;
1399}
1400
1401/* Analytic k-th raw moment of a gate_case, trying each guard-partition tier. */
1402std::optional<double>
1403caseAnalyticRawMoment(const GenericCircuit &gc, gate_t g, unsigned k)
1404{
1405 if (auto v = singlePivotCaseRawMoment(gc, g, k)) return v;
1406 if (auto v = twoArmCaseRawMoment(gc, g, k)) return v;
1407 if (auto v = orderStatCaseRawMoment(gc, g, k)) return v;
1408 return std::nullopt;
1409}
1410
1411double rec_expectation(const GenericCircuit &gc, gate_t g, FootprintCache &fp)
1412{
1413 const auto type = gc.getGateType(g);
1414 switch (type) {
1415 case gate_value:
1416 return parseDoubleStrict(gc.getExtra(g));
1417 case gate_rv: {
1418 // A latent (parametric) leaf -- a parameter is itself a random
1419 // variable -- has no constant-parameter closed form. But the MEAN
1420 // still decomposes exactly when the family's mean is affine in its
1421 // parameters (Normal mean = μ, Uniform mean = (a+b)/2, inverse-
1422 // Gaussian mean = μ): E[X] = E[mean(θ)] = mean(E[θ]) by linearity of
1423 // expectation (no independence assumption), so recurse into the
1424 // parameter wires -- no MC. Nonlinear means (Exponential 1/λ,
1425 // Gamma k/λ, ...) keep meanIsAffine() = false and fall through to MC.
1426 if (rvIsParametric(gc, g)) {
1427 auto tmpl = parse_distribution_template(gc.getExtra(g));
1428 if (tmpl && familyMeanIsAffine(*tmpl)) {
1429 const auto &w = gc.getWires(g);
1430 auto param_mean = [&](const DistributionParam &p) {
1431 return p.wire_slot < 0 ? p.literal
1432 : rec_expectation(gc, w[p.wire_slot], fp);
1433 };
1434 return tmpl->family
1435 ->factory(param_mean(tmpl->p1), param_mean(tmpl->p2))
1436 ->mean();
1437 }
1438 return mc_raw_moment(gc, g, 1, "Expectation of a latent gate_rv");
1439 }
1440 auto spec = parse_distribution_spec(gc.getExtra(g));
1441 if (!spec)
1442 throw CircuitException(
1443 "Expectation: malformed gate_rv extra: " + gc.getExtra(g));
1444 return analytical_mean(*spec);
1445 }
1446 case gate_arith: {
1447 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
1448 const auto &wires = gc.getWires(g);
1449 switch (op) {
1450 case PROVSQL_ARITH_PLUS: {
1451 double s = 0.0;
1452 for (gate_t c : wires) s += rec_expectation(gc, c, fp);
1453 return s;
1454 }
1455 case PROVSQL_ARITH_MINUS: {
1456 if (wires.size() != 2)
1457 throw CircuitException("gate_arith MINUS must be binary");
1458 return rec_expectation(gc, wires[0], fp)
1459 - rec_expectation(gc, wires[1], fp);
1460 }
1461 case PROVSQL_ARITH_NEG: {
1462 if (wires.size() != 1)
1463 throw CircuitException("gate_arith NEG must be unary");
1464 return -rec_expectation(gc, wires[0], fp);
1465 }
1466 case PROVSQL_ARITH_TIMES: {
1467 if (pairwise_disjoint(fp, wires)) {
1468 double p = 1.0;
1469 for (gate_t c : wires) p *= rec_expectation(gc, c, fp);
1470 return p;
1471 }
1472 return mc_raw_moment(gc, g, 1,
1473 "Expectation of gate_arith TIMES with shared random variables");
1474 }
1476 // Truncation has no linearity to push through.
1477 return mc_raw_moment(gc, g, 1,
1478 "Expectation of gate_arith INTDIV (integer division)");
1479 case PROVSQL_ARITH_DIV: {
1480 if (wires.size() != 2)
1481 throw CircuitException("gate_arith DIV must be binary");
1482 if (gc.getGateType(wires[1]) == gate_value) {
1483 const double divisor = parseDoubleStrict(gc.getExtra(wires[1]));
1484 /* A divisor that is zero in every world leaves no world with a
1485 * value, so there is no moment to report: NaN, as for a world an
1486 * aggregate has no contributor in, rather than an infinity. */
1487 if (divisor == 0.0)
1488 return std::numeric_limits<double>::quiet_NaN();
1489 return rec_expectation(gc, wires[0], fp) / divisor;
1490 }
1491 return mc_raw_moment(gc, g, 1,
1492 "Expectation of gate_arith DIV with non-constant divisor");
1493 }
1494 case PROVSQL_ARITH_MAX:
1495 case PROVSQL_ARITH_MIN: {
1496 // Order statistics have no linearity to push through. Exact
1497 // closed form for i.i.d. Uniform / Exponential; the layer-cake
1498 // 1-D quadrature for any other independent bare-RV mix (mixed
1499 // families, non-identical parameters, Normal); Monte Carlo only
1500 // when leaves are shared / correlated.
1501 const bool isMax = (op == PROVSQL_ARITH_MAX);
1502 if (auto v = iidOrderStatMean(gc, g, isMax, fp))
1503 return *v;
1504 if (auto v = mixedOrderStatMean(gc, g, isMax, fp))
1505 return *v;
1506 return mc_raw_moment(gc, g, 1,
1507 "Expectation of gate_arith " + std::string(isMax ? "MAX" : "MIN"));
1508 }
1510 /* The value as double precision reads it, which is the value these
1511 * evaluators already carry: the moment is its child's, exactly. */
1512 if (wires.size() == 1)
1513 return rec_expectation(gc, wires[0], fp);
1514 /* otherwise, the sampled group below */
1515 [[fallthrough]];
1519 case PROVSQL_ARITH_CEIL:
1520 case PROVSQL_ARITH_ABS:
1521 // Rounding and absolute value do not commute with expectation
1522 // either (E[round(X)] is not round(E[X])), and no closed-form image
1523 // is registered for them: the estimate over the worlds is what
1524 // there is.
1525 return mc_raw_moment(gc, g, 1,
1526 "Expectation of a gate_arith rounding or absolute value");
1527 case PROVSQL_ARITH_POW:
1528 case PROVSQL_ARITH_LN:
1529 case PROVSQL_ARITH_EXP:
1530 // Nonlinear transforms: expectation does not commute with the
1531 // map, so there is no linearity to push through. A registered
1532 // closed-form image (exp(normal) is lognormal, ln(lognormal)
1533 // is normal) gives the exact answer; otherwise the empirical
1534 // estimate is the general one.
1535 if (auto image = transform_image(gc, g, op))
1536 return image->mean();
1537 return mc_raw_moment(gc, g, 1,
1538 "Expectation of a gate_arith nonlinear transform");
1540 // Order-statistic aggregate over a random member set: no closed
1541 // form; the sampler sorts and interpolates each draw.
1542 return mc_raw_moment(gc, g, 1,
1543 "Expectation of a gate_arith PERCENTILE");
1544 }
1545 throw CircuitException(
1546 "Expectation: unknown gate_arith op tag: " +
1547 std::to_string(static_cast<unsigned>(op)));
1548 }
1549 case gate_mixture: {
1550 const auto &wires = gc.getWires(g);
1551 if (gc.isCategoricalMixture(g)) {
1552 // Categorical mixture: E[M] = Σ π_i · v_i, where each mulinput
1553 // mul_i carries π_i in set_prob and v_i in extra.
1554 double s = 0.0;
1555 for (std::size_t i = 1; i < wires.size(); ++i) {
1556 s += gc.getProb(wires[i])
1557 * parseDoubleStrict(gc.getExtra(wires[i]));
1558 }
1559 return s;
1560 }
1561 // E[mixture(p, X, Y)] = π·E[X] + (1-π)·E[Y], where π = P(p = true).
1562 // For a bare gate_input p, π is the leaf's pinned set_prob. For
1563 // a compound Boolean p, route through evaluateBooleanProbability
1564 // so π honors the tuple-independent semantics of the Boolean DAG.
1565 if (wires.size() != 3)
1566 throw CircuitException(
1567 "Expectation: gate_mixture must have exactly three children");
1568 const double pi = mixturePi(gc, wires[0]);
1569 return pi * rec_expectation(gc, wires[1], fp)
1570 + (1.0 - pi) * rec_expectation(gc, wires[2], fp);
1571 }
1572 case gate_case:
1573 if (auto v = caseAnalyticRawMoment(gc, g, 1))
1574 return *v;
1575 return mc_raw_moment(gc, g, 1, "Expectation of gate type gate_case");
1576 default:
1577 return mc_raw_moment(gc, g, 1,
1578 "Expectation of gate type " + std::string(gate_type_name[type]));
1579 }
1580}
1581
1582double rec_variance(const GenericCircuit &gc, gate_t g, FootprintCache &fp)
1583{
1584 const auto type = gc.getGateType(g);
1585 switch (type) {
1586 case gate_value:
1587 return 0.0;
1588 case gate_rv: {
1589 // Latent (parametric) leaf: no constant-parameter closed form.
1590 // Var(normal(M,1)) = 1 + Var(M) etc. is exact in expectation under
1591 // MC (the sampler draws the latent then the leaf per iteration).
1592 if (rvIsParametric(gc, g)) {
1593 const std::string what = "Variance of a latent gate_rv";
1594 const double mu = mc_raw_moment(gc, g, 1, what);
1595 return mc_central_moment(gc, g, 2, mu, what);
1596 }
1597 auto spec = parse_distribution_spec(gc.getExtra(g));
1598 if (!spec)
1599 throw CircuitException(
1600 "Variance: malformed gate_rv extra: " + gc.getExtra(g));
1601 return analytical_variance(*spec);
1602 }
1603 case gate_arith: {
1604 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
1605 const auto &wires = gc.getWires(g);
1606 auto mc_var = [&](const std::string &what) {
1607 const double mu = mc_raw_moment(gc, g, 1, what);
1608 return mc_central_moment(gc, g, 2, mu, what);
1609 };
1610 switch (op) {
1611 case PROVSQL_ARITH_PLUS: {
1612 if (pairwise_disjoint(fp, wires)) {
1613 double s = 0.0;
1614 for (gate_t c : wires) s += rec_variance(gc, c, fp);
1615 return s;
1616 }
1617 return mc_var(
1618 "Variance of gate_arith PLUS with shared random variables");
1619 }
1620 case PROVSQL_ARITH_MINUS: {
1621 if (wires.size() != 2)
1622 throw CircuitException("gate_arith MINUS must be binary");
1623 if (pairwise_disjoint(fp, wires)) {
1624 return rec_variance(gc, wires[0], fp)
1625 + rec_variance(gc, wires[1], fp);
1626 }
1627 return mc_var(
1628 "Variance of gate_arith MINUS with shared random variables");
1629 }
1630 case PROVSQL_ARITH_NEG: {
1631 if (wires.size() != 1)
1632 throw CircuitException("gate_arith NEG must be unary");
1633 return rec_variance(gc, wires[0], fp);
1634 }
1635 case PROVSQL_ARITH_TIMES: {
1636 if (pairwise_disjoint(fp, wires)) {
1637 // Var(prod Xi) = prod E[Xi^2] - (prod E[Xi])^2
1638 // = prod (Var[Xi] + E[Xi]^2) - (prod E[Xi])^2
1639 double prod_e2 = 1.0;
1640 double prod_e1 = 1.0;
1641 for (gate_t c : wires) {
1642 const double mu_c = rec_expectation(gc, c, fp);
1643 const double v_c = rec_variance(gc, c, fp);
1644 prod_e2 *= (v_c + mu_c * mu_c);
1645 prod_e1 *= mu_c;
1646 }
1647 return prod_e2 - prod_e1 * prod_e1;
1648 }
1649 return mc_var(
1650 "Variance of gate_arith TIMES with shared random variables");
1651 }
1653 return mc_var("Variance of gate_arith INTDIV (integer division)");
1654 case PROVSQL_ARITH_DIV: {
1655 if (wires.size() != 2)
1656 throw CircuitException("gate_arith DIV must be binary");
1657 if (gc.getGateType(wires[1]) == gate_value) {
1658 const double divisor = parseDoubleStrict(gc.getExtra(wires[1]));
1659 if (divisor == 0.0)
1660 return std::numeric_limits<double>::quiet_NaN();
1661 return rec_variance(gc, wires[0], fp) / (divisor * divisor);
1662 }
1663 return mc_var(
1664 "Variance of gate_arith DIV with non-constant divisor");
1665 }
1666 case PROVSQL_ARITH_MAX:
1667 case PROVSQL_ARITH_MIN:
1668 // No closed-form variance decomposition for order statistics; MC.
1669 return mc_var(
1670 "Variance of gate_arith " +
1671 std::string(op == PROVSQL_ARITH_MAX ? "MAX" : "MIN"));
1673 /* The value as double precision reads it, which is the value these
1674 * evaluators already carry: the moment is its child's, exactly. */
1675 if (wires.size() == 1)
1676 return rec_variance(gc, wires[0], fp);
1677 /* otherwise, the sampled group below */
1678 [[fallthrough]];
1682 case PROVSQL_ARITH_CEIL:
1683 case PROVSQL_ARITH_ABS:
1684 return mc_var(
1685 "Variance of a gate_arith rounding or absolute value");
1686 case PROVSQL_ARITH_POW:
1687 case PROVSQL_ARITH_LN:
1688 case PROVSQL_ARITH_EXP:
1689 if (auto image = transform_image(gc, g, op))
1690 return image->variance();
1691 return mc_var("Variance of a gate_arith nonlinear transform");
1693 return mc_var("Variance of a gate_arith PERCENTILE");
1694 }
1695 throw CircuitException(
1696 "Variance: unknown gate_arith op tag: " +
1697 std::to_string(static_cast<unsigned>(op)));
1698 }
1699 case gate_mixture: {
1700 const auto &wires = gc.getWires(g);
1701 if (gc.isCategoricalMixture(g)) {
1702 // Categorical mixture: Var(M) = Σ π_i v_i² − (Σ π_i v_i)².
1703 double e1 = 0.0, e2 = 0.0;
1704 for (std::size_t i = 1; i < wires.size(); ++i) {
1705 const double p = gc.getProb(wires[i]);
1706 const double v = parseDoubleStrict(gc.getExtra(wires[i]));
1707 e1 += p * v;
1708 e2 += p * v * v;
1709 }
1710 return e2 - e1 * e1;
1711 }
1712 // Var(M) = π·(Var(X) + E[X]²) + (1-π)·(Var(Y) + E[Y]²) - E[M]²
1713 // (law of total variance specialised to a Bernoulli mixture).
1714 if (wires.size() != 3)
1715 throw CircuitException(
1716 "Variance: gate_mixture must have exactly three children");
1717 const double pi = mixturePi(gc, wires[0]);
1718 const double ex = rec_expectation(gc, wires[1], fp);
1719 const double ey = rec_expectation(gc, wires[2], fp);
1720 const double vx = rec_variance(gc, wires[1], fp);
1721 const double vy = rec_variance(gc, wires[2], fp);
1722 const double em = pi * ex + (1.0 - pi) * ey;
1723 return pi * (vx + ex * ex)
1724 + (1.0 - pi) * (vy + ey * ey)
1725 - em * em;
1726 }
1727 case gate_case: {
1728 if (auto m2 = caseAnalyticRawMoment(gc, g, 2))
1729 if (auto m1 = caseAnalyticRawMoment(gc, g, 1))
1730 return *m2 - (*m1) * (*m1);
1731 const std::string what = "Variance of gate type gate_case";
1732 const double mu = mc_raw_moment(gc, g, 1, what);
1733 return mc_central_moment(gc, g, 2, mu, what);
1734 }
1735 default: {
1736 const std::string what =
1737 "Variance of gate type " + std::string(gate_type_name[type]);
1738 const double mu = mc_raw_moment(gc, g, 1, what);
1739 return mc_central_moment(gc, g, 2, mu, what);
1740 }
1741 }
1742}
1743
1744double rec_raw_moment(const GenericCircuit &gc, gate_t g, unsigned k,
1745 FootprintCache &fp)
1746{
1747 if (k == 0) return 1.0;
1748 if (k == 1) return rec_expectation(gc, g, fp);
1749
1750 const auto type = gc.getGateType(g);
1751 switch (type) {
1752 case gate_value:
1753 return std::pow(parseDoubleStrict(gc.getExtra(g)),
1754 static_cast<double>(k));
1755 case gate_rv: {
1756 // Latent (parametric) leaf: no constant-parameter closed form.
1757 if (rvIsParametric(gc, g))
1758 return mc_raw_moment(gc, g, k, "Raw moment of a latent gate_rv");
1759 auto spec = parse_distribution_spec(gc.getExtra(g));
1760 if (!spec)
1761 throw CircuitException(
1762 "Moment: malformed gate_rv extra: " + gc.getExtra(g));
1763 return analytical_raw_moment(*spec, k);
1764 }
1765 case gate_arith: {
1766 const auto op = static_cast<provsql_arith_op>(gc.getInfos(g).first);
1767 const auto &wires = gc.getWires(g);
1768 switch (op) {
1769 case PROVSQL_ARITH_NEG: {
1770 if (wires.size() != 1)
1771 throw CircuitException("gate_arith NEG must be unary");
1772 const double v = rec_raw_moment(gc, wires[0], k, fp);
1773 return ((k % 2 == 0) ? 1.0 : -1.0) * v;
1774 }
1775 case PROVSQL_ARITH_PLUS: {
1776 if (pairwise_disjoint(fp, wires)) {
1777 // Fold-left: m_acc[i] holds E[(X1 + ... + Xj)^i] for the
1778 // first j children processed; combining with the next
1779 // independent child Y uses the binomial theorem.
1780 std::vector<double> m_acc(k + 1, 0.0);
1781 for (unsigned i = 0; i <= k; ++i)
1782 m_acc[i] = rec_raw_moment(gc, wires[0], i, fp);
1783 for (size_t w = 1; w < wires.size(); ++w) {
1784 std::vector<double> next(k + 1, 0.0);
1785 std::vector<double> moments_y(k + 1, 0.0);
1786 for (unsigned i = 0; i <= k; ++i)
1787 moments_y[i] = rec_raw_moment(gc, wires[w], i, fp);
1788 for (unsigned kp = 0; kp <= k; ++kp) {
1789 double total = 0.0;
1790 for (unsigned i = 0; i <= kp; ++i) {
1791 total += binomial(kp, i) * m_acc[i] * moments_y[kp - i];
1792 }
1793 next[kp] = total;
1794 }
1795 m_acc = std::move(next);
1796 }
1797 return m_acc[k];
1798 }
1799 return mc_raw_moment(gc, g, k,
1800 "Raw moment of gate_arith PLUS with shared random variables");
1801 }
1802 case PROVSQL_ARITH_MINUS: {
1803 if (wires.size() != 2)
1804 throw CircuitException("gate_arith MINUS must be binary");
1805 if (pairwise_disjoint(fp, wires)) {
1806 double total = 0.0;
1807 for (unsigned i = 0; i <= k; ++i) {
1808 const double sign = ((k - i) % 2 == 0) ? 1.0 : -1.0;
1809 total += binomial(k, i)
1810 * rec_raw_moment(gc, wires[0], i, fp)
1811 * sign
1812 * rec_raw_moment(gc, wires[1], k - i, fp);
1813 }
1814 return total;
1815 }
1816 return mc_raw_moment(gc, g, k,
1817 "Raw moment of gate_arith MINUS with shared random variables");
1818 }
1819 case PROVSQL_ARITH_TIMES: {
1820 if (pairwise_disjoint(fp, wires)) {
1821 // (prod Xi)^k = prod Xi^k; under independence E factors.
1822 double p = 1.0;
1823 for (gate_t c : wires) p *= rec_raw_moment(gc, c, k, fp);
1824 return p;
1825 }
1826 return mc_raw_moment(gc, g, k,
1827 "Raw moment of gate_arith TIMES with shared random variables");
1828 }
1830 return mc_raw_moment(gc, g, k,
1831 "Raw moment of gate_arith INTDIV (integer division)");
1832 case PROVSQL_ARITH_DIV: {
1833 if (wires.size() != 2)
1834 throw CircuitException("gate_arith DIV must be binary");
1835 if (gc.getGateType(wires[1]) == gate_value) {
1836 const double divisor = parseDoubleStrict(gc.getExtra(wires[1]));
1837 if (divisor == 0.0)
1838 return std::numeric_limits<double>::quiet_NaN();
1839 return rec_raw_moment(gc, wires[0], k, fp)
1840 / std::pow(divisor, static_cast<double>(k));
1841 }
1842 return mc_raw_moment(gc, g, k,
1843 "Raw moment of gate_arith DIV with non-constant divisor");
1844 }
1845 case PROVSQL_ARITH_MAX:
1846 case PROVSQL_ARITH_MIN:
1847 // Order-statistic raw moments have no elementary decomposition; MC.
1848 return mc_raw_moment(gc, g, k,
1849 "Raw moment of gate_arith " +
1850 std::string(op == PROVSQL_ARITH_MAX ? "MAX" : "MIN"));
1852 /* The value as double precision reads it, which is the value these
1853 * evaluators already carry: the moment is its child's, exactly. */
1854 if (wires.size() == 1)
1855 return rec_raw_moment(gc, wires[0], k, fp);
1856 /* otherwise, the sampled group below */
1857 [[fallthrough]];
1861 case PROVSQL_ARITH_CEIL:
1862 case PROVSQL_ARITH_ABS:
1863 return mc_raw_moment(gc, g, k,
1864 "Raw moment of a gate_arith rounding or absolute value");
1865 case PROVSQL_ARITH_POW:
1866 case PROVSQL_ARITH_LN:
1867 case PROVSQL_ARITH_EXP:
1868 if (auto image = transform_image(gc, g, op))
1869 return image->rawMoment(k);
1870 return mc_raw_moment(gc, g, k,
1871 "Raw moment of a gate_arith nonlinear transform");
1873 return mc_raw_moment(gc, g, k,
1874 "Raw moment of a gate_arith PERCENTILE");
1875 }
1876 throw CircuitException(
1877 "Moment: unknown gate_arith op tag: " +
1878 std::to_string(static_cast<unsigned>(op)));
1879 }
1880 case gate_mixture: {
1881 const auto &wires = gc.getWires(g);
1882 if (gc.isCategoricalMixture(g)) {
1883 // Categorical mixture: E[M^k] = Σ π_i v_i^k.
1884 double s = 0.0;
1885 for (std::size_t i = 1; i < wires.size(); ++i) {
1886 const double v = parseDoubleStrict(gc.getExtra(wires[i]));
1887 s += gc.getProb(wires[i])
1888 * std::pow(v, static_cast<double>(k));
1889 }
1890 return s;
1891 }
1892 // E[M^k] = π·E[X^k] + (1-π)·E[Y^k].
1893 if (wires.size() != 3)
1894 throw CircuitException(
1895 "Moment: gate_mixture must have exactly three children");
1896 const double pi = mixturePi(gc, wires[0]);
1897 return pi * rec_raw_moment(gc, wires[1], k, fp)
1898 + (1.0 - pi) * rec_raw_moment(gc, wires[2], k, fp);
1899 }
1900 case gate_case:
1901 if (auto v = caseAnalyticRawMoment(gc, g, k))
1902 return *v;
1903 return mc_raw_moment(gc, g, k, "Raw moment of gate type gate_case");
1904 default:
1905 return mc_raw_moment(gc, g, k,
1906 "Raw moment of gate type " + std::string(gate_type_name[type]));
1907 }
1908}
1909
1910} // namespace
1911
1912/* Conditional dispatch helpers: try closed-form first, fall through
1913 * to MC rejection. Used by all four public compute_* entries to keep
1914 * the conditional logic in one place and the unconditional path
1915 * unchanged. */
1916namespace {
1917
1918[[noreturn]] void raise_infeasible_event(const GenericCircuit &gc, gate_t root)
1919{
1920 (void)gc; (void)root;
1921 throw CircuitException(
1922 "conditioning event is infeasible (empty intersection with the "
1923 "random variable's support)");
1924}
1925
1926double conditional_raw_moment(const GenericCircuit &gc, gate_t root,
1927 unsigned k, gate_t event_root)
1928{
1929 if (k == 0) return 1.0;
1930 /* Collapsed exact posterior: a latent conditioned on a discrete rv over it
1931 * equalling a correlated COUNT (Y(R) = C). Rao-Blackwellises the count to a
1932 * pmf by 1-D quadrature over its shared latent, then closes the R posterior
1933 * by a second 1-D quadrature -- replacing the degenerating rejection sampler
1934 * with an exact quadrature. Declines (nullopt) on any shape mismatch. */
1935 if (auto cf = collapsedConditionalMoment(gc, root, event_root, k))
1936 return *cf;
1937 /* Conjugate prior/likelihood shape: the posterior is a first-class
1938 * distribution of the prior's family, so the raw moment is its family
1939 * closed form -- exact, deterministic, works at rv_mc_samples = 0.
1940 * Declines (nullopt) on any shape mismatch. */
1941 if (auto post = conjugatePosterior(gc, root, event_root))
1942 return makeDistribution(*post)->rawMoment(k);
1943 /* Continuous-density evidence (latent-variable posterior): likelihood
1944 * weighting. The closed-form / rejection paths below assume a bare-rv
1945 * truncation event, so they do not apply. */
1946 if (circuitHasObserve(gc, event_root)) {
1947 const std::string what = "Posterior raw moment";
1948 auto post = importanceSampleConditional(
1949 gc, root, event_root, mc_samples_or_throw(what));
1950 checkPosteriorOrThrow(post, what);
1951 return weightedRawMoment(post, k);
1952 }
1953 if (auto cf = try_truncated_closed_form(gc, root, event_root, k, false))
1954 return *cf;
1955 if (auto cf = try_rvVsRv_conditional_moment(gc, root, event_root, k, false))
1956 return *cf;
1957 if (auto cf = try_pivotConjunction_conditional_moment(gc, root, event_root,
1958 k, false))
1959 return *cf;
1960 if (eventIsProvablyInfeasible(gc, root, event_root))
1961 raise_infeasible_event(gc, root);
1962 return mc_conditional_raw_moment(
1963 gc, root, k, event_root,
1964 "Conditional raw moment of gate type " +
1965 std::string(gate_type_name[gc.getGateType(root)]));
1966}
1967
1968double conditional_central_moment(const GenericCircuit &gc, gate_t root,
1969 unsigned k, gate_t event_root)
1970{
1971 if (k == 0) return 1.0;
1972 if (k == 1) return 0.0;
1973 /* Collapsed exact posterior variance from the collapsed raw moments
1974 * (Var = E[R^2|C] - E[R|C]^2); declines together with the mean. */
1975 if (k == 2) {
1976 auto m1 = collapsedConditionalMoment(gc, root, event_root, 1);
1977 auto m2 = collapsedConditionalMoment(gc, root, event_root, 2);
1978 if (m1 && m2) return *m2 - (*m1) * (*m1);
1979 }
1980 /* Conjugate shape: exact central moment of the posterior distribution
1981 * (family variance for k = 2, binomial expansion over the family raw
1982 * moments above that). */
1983 if (auto post = conjugatePosterior(gc, root, event_root)) {
1984 auto dist = makeDistribution(*post);
1985 if (k == 2) return dist->variance();
1986 const double mu = dist->mean();
1987 double total = 0.0;
1988 for (unsigned i = 0; i <= k; ++i) {
1989 const double mu_pow = std::pow(-mu, static_cast<double>(k - i));
1990 total += binomial(k, i) * mu_pow * dist->rawMoment(i);
1991 }
1992 return total;
1993 }
1994 /* Continuous-density evidence: one importance-sampling pass yields both
1995 * the posterior mean and the central moment (no resampling). */
1996 if (circuitHasObserve(gc, event_root)) {
1997 const std::string what = "Posterior central moment";
1998 auto post = importanceSampleConditional(
1999 gc, root, event_root, mc_samples_or_throw(what));
2000 checkPosteriorOrThrow(post, what);
2001 const double mu = weightedRawMoment(post, 1);
2002 return weightedCentralMoment(post, k, mu);
2003 }
2004 if (auto cf = try_truncated_closed_form(gc, root, event_root, k, true))
2005 return *cf;
2006 if (auto cf = try_rvVsRv_conditional_moment(gc, root, event_root, k, true))
2007 return *cf;
2008 if (auto cf = try_pivotConjunction_conditional_moment(gc, root, event_root,
2009 k, true))
2010 return *cf;
2011 if (eventIsProvablyInfeasible(gc, root, event_root))
2012 raise_infeasible_event(gc, root);
2013 /* MC central: need μ_A first. */
2014 const double mu = conditional_raw_moment(gc, root, 1, event_root);
2015 return mc_conditional_central_moment(
2016 gc, root, k, mu, event_root,
2017 "Conditional central moment of gate type " +
2018 std::string(gate_type_name[gc.getGateType(root)]));
2019}
2020
2021} // namespace
2022
2024 std::optional<gate_t> event_root)
2025{
2026 if (event_root.has_value())
2027 return conditional_raw_moment(gc, root, 1, *event_root);
2028 FootprintCache fp(gc);
2029 return rec_expectation(gc, root, fp);
2030}
2031
2032double compute_raw_moment(const GenericCircuit &gc, gate_t root, unsigned k,
2033 std::optional<gate_t> event_root)
2034{
2035 if (event_root.has_value())
2036 return conditional_raw_moment(gc, root, k, *event_root);
2037 FootprintCache fp(gc);
2038 return rec_raw_moment(gc, root, k, fp);
2039}
2040
2041double compute_central_moment(const GenericCircuit &gc, gate_t root, unsigned k,
2042 std::optional<gate_t> event_root)
2043{
2044 if (event_root.has_value())
2045 return conditional_central_moment(gc, root, k, *event_root);
2046 if (k == 0) return 1.0;
2047 if (k == 1) return 0.0;
2048 FootprintCache fp(gc);
2049 if (k == 2) return rec_variance(gc, root, fp);
2050 // E[(X - mu)^k] = sum_{i=0}^{k} C(k, i) (-mu)^(k-i) E[X^i]
2051 const double mu = rec_expectation(gc, root, fp);
2052 double total = 0.0;
2053 for (unsigned i = 0; i <= k; ++i) {
2054 const double mu_pow = std::pow(-mu, static_cast<double>(k - i));
2055 total += binomial(k, i) * mu_pow * rec_raw_moment(gc, root, i, fp);
2056 }
2057 return total;
2058}
2059
2060/* ─────────────────────── quantiles (§B.1) ─────────────────────── */
2061
2062namespace {
2063
2064/* Empirical p-quantile with the linear-interpolation convention
2065 * PostgreSQL's percentile_cont uses (type 7: h = p·(n-1)). NaN
2066 * observations (sampling-undefined worlds, e.g. empty-group SQL NULLs
2067 * from gate_agg) are dropped like the MC moment estimators do; NaN if
2068 * every sample was undefined. */
2069double empirical_quantile(std::vector<double> xs, double p)
2070{
2071 xs.erase(std::remove_if(xs.begin(), xs.end(),
2072 [](double x) { return std::isnan(x); }),
2073 xs.end());
2074 if (xs.empty()) return std::numeric_limits<double>::quiet_NaN();
2075 std::sort(xs.begin(), xs.end());
2076 if (p <= 0.0) return xs.front();
2077 if (p >= 1.0) return xs.back();
2078 const double h = p * static_cast<double>(xs.size() - 1);
2079 const std::size_t i = static_cast<std::size_t>(h);
2080 if (i + 1 >= xs.size()) return xs.back();
2081 const double frac = h - static_cast<double>(i);
2082 return xs[i] + frac * (xs[i + 1] - xs[i]);
2083}
2084
2085/* Exact quantile of a categorical-form gate_mixture: the generalised
2086 * inverse F⁻¹(p) = min{v : F(v) >= p} over the (value, mass) outcomes.
2087 * nullopt if an outcome's value fails to parse (falls to MC). */
2088std::optional<double> categorical_quantile(const GenericCircuit &gc,
2089 gate_t mix, double p)
2090{
2091 const auto &wires = gc.getWires(mix);
2092 std::vector<std::pair<double, double>> outcomes;
2093 outcomes.reserve(wires.size());
2094 for (std::size_t i = 1; i < wires.size(); ++i) {
2095 double v;
2096 try { v = parseDoubleStrict(gc.getExtra(wires[i])); }
2097 catch (const CircuitException &) { return std::nullopt; }
2098 outcomes.emplace_back(v, gc.getProb(wires[i]));
2099 }
2100 if (outcomes.empty()) return std::nullopt;
2101 std::sort(outcomes.begin(), outcomes.end());
2102 double cum = 0.0;
2103 for (const auto &vp : outcomes) {
2104 cum += vp.second;
2105 if (cum >= p && cum > 0.0) return vp.first;
2106 }
2107 return outcomes.back().first; /* p ≈ 1 vs. mass-sum roundoff */
2108}
2109
2110/* Closed-form (or numerically inverted) quantile of a bare gate_rv,
2111 * optionally truncated to [lo, hi] by a conditioning event: the
2112 * truncated quantile is Q(F(lo) + p·(F(hi) − F(lo))). Tries the
2113 * family's elementary inverse CDF first, then the generic monotone
2114 * CDF bisection (Erlang / Gamma); nullopt when neither decides, so
2115 * the caller falls to MC. */
2116std::optional<double> analytic_dist_quantile(const Distribution &dist,
2117 double p, double lo, double hi);
2118
2119std::optional<double> analytic_rv_quantile(const DistributionSpec &spec,
2120 double p, double lo, double hi)
2121{
2122 return analytic_dist_quantile(*makeDistribution(spec), p, lo, hi);
2123}
2124
2125std::optional<double> analytic_dist_quantile(const Distribution &dist,
2126 double p, double lo, double hi)
2127{
2128 if (p <= 0.0 || p >= 1.0) {
2129 /* Quantile limits are the (truncated) support edges. */
2130 const auto sup = dist.support();
2131 return (p <= 0.0) ? std::max(sup.lo, lo) : std::min(sup.hi, hi);
2132 }
2133 double u = p;
2134 if (std::isfinite(lo) || std::isfinite(hi)) {
2135 const double f_lo = std::isfinite(lo) ? dist.cdf(lo) : 0.0;
2136 const double f_hi = std::isfinite(hi) ? dist.cdf(hi) : 1.0;
2137 if (std::isnan(f_lo) || std::isnan(f_hi)) return std::nullopt;
2138 const double mass = f_hi - f_lo;
2139 if (mass < 1e-12) return std::nullopt; /* vanishing mass: MC's call */
2140 u = f_lo + p * mass;
2141 }
2142 double q = std::numeric_limits<double>::quiet_NaN();
2143 if (auto cf = dist.quantile(u)) q = *cf;
2144 if (std::isnan(q)) q = numericQuantile(dist, u);
2145 if (std::isnan(q)) return std::nullopt;
2146 /* Clamp defensively into the truncation interval (roundoff in u). */
2147 if (q < lo) q = lo;
2148 if (q > hi) q = hi;
2149 return q;
2150}
2151
2152} // namespace
2153
2154double compute_quantile(const GenericCircuit &gc, gate_t root, double p,
2155 std::optional<gate_t> event_root)
2156{
2157 const double inf = std::numeric_limits<double>::infinity();
2158
2159 if (event_root.has_value()) {
2160 /* Conjugate shape: exact quantile of the posterior distribution
2161 * (elementary inverse CDF or the monotone CDF bisection). */
2162 if (auto post = conjugatePosterior(gc, root, *event_root))
2163 if (auto q = analytic_rv_quantile(*post, p, -inf, inf))
2164 return *q;
2165 /* Continuous-density evidence: weighted empirical posterior quantile. */
2166 if (circuitHasObserve(gc, *event_root)) {
2167 const std::string what = "Posterior quantile";
2168 auto post = importanceSampleConditional(
2169 gc, root, *event_root, mc_samples_or_throw(what));
2170 checkPosteriorOrThrow(post, what);
2171 return weightedQuantile(std::move(post), p);
2172 }
2173 /* Bare RV under an interval event: exact truncated quantile. */
2174 if (auto m = matchTruncatedSingleRv(gc, root, *event_root)) {
2175 if (auto q = analytic_rv_quantile(m->spec, p, m->lo, m->hi))
2176 return *q;
2177 }
2178 if (eventIsProvablyInfeasible(gc, root, *event_root))
2179 raise_infeasible_event(gc, root);
2181 gc, root, *event_root,
2182 mc_samples_or_throw("Conditional quantile"));
2183 check_acceptance_or_throw(cs, "Conditional quantile");
2184 return empirical_quantile(std::move(cs.accepted), p);
2185 }
2186
2187 const auto type = gc.getGateType(root);
2188 if (type == gate_value) {
2189 /* Dirac at c: every quantile is c. */
2190 try { return parseDoubleStrict(gc.getExtra(root)); }
2191 catch (const CircuitException &) { /* fall through to MC */ }
2192 } else if (type == gate_rv) {
2193 if (auto spec = parse_distribution_spec(gc.getExtra(root)))
2194 if (auto q = analytic_rv_quantile(*spec, p, -inf, inf))
2195 return *q;
2196 } else if (type == gate_mixture && gc.isCategoricalMixture(root)) {
2197 if (auto q = categorical_quantile(gc, root, p))
2198 return *q;
2199 } else if (type == gate_arith) {
2200 /* A unary LN / EXP transform, or a product of independent factors,
2201 * with a registered closed-form image (exp(normal) is lognormal,
2202 * lognormal products are lognormal, ...) has an exact quantile
2203 * through the image distribution. */
2204 const auto op = static_cast<provsql_arith_op>(gc.getInfos(root).first);
2205 std::unique_ptr<Distribution> image =
2206 (op == PROVSQL_ARITH_TIMES) ? product_image(gc, root)
2207 : transform_image(gc, root, op);
2208 if (image)
2209 if (auto q = analytic_dist_quantile(*image, p, -inf, inf))
2210 return *q;
2211 }
2212
2213 /* Compound scalar circuits (arith trees, Bernoulli mixtures, ...):
2214 * quantiles do not decompose like moments, so estimate from the
2215 * empirical distribution at the rv_mc_samples budget. */
2216 return empirical_quantile(
2217 monteCarloScalarSamples(gc, root, mc_samples_or_throw("Quantile")),
2218 p);
2219}
2220
2221/**
2222 * @brief Lift conditioning out of a scalar arithmetic expression.
2223 *
2224 * Implements @c "f(X|A, Y|B, …) = f(X, Y, …) | (A ∧ B ∧ …)": walks the scalar
2225 * tree rooted at @p root, replaces every nested @c gate_conditioned by a
2226 * transparent passthrough to its target (so the tree becomes the plain
2227 * arithmetic over the unconditioned distributions), collects the evidence
2228 * children, and conjoins them -- together with any pre-existing @p event_opt
2229 * -- into a single conditioning event. The conjunction is built as an
2230 * in-memory @c gate_times over the evidence gates, all of which already live
2231 * in the (joint) circuit, so a base @c gate_rv shared between a value and its
2232 * evidence keeps a single draw under the MC sampler. A conditioned ROOT is
2233 * peeled to its bare target (returned), so a stored "X | C" reaching any
2234 * low-level RV entry point keeps the closed-form scalar path; the (possibly
2235 * new) root is returned. Leaves @p event_opt untouched and returns @p root
2236 * unchanged when the expression carries no conditioning.
2237 */
2239 std::optional<gate_t> &event_opt)
2240{
2241 std::vector<gate_t> evidences;
2242
2243 // 1. Peel a conditioned ROOT to its bare target. A root has no parent
2244 // wires, so it is replaced by its target directly rather than the
2245 // single-child gate_arith passthrough the buried case below needs;
2246 // keeping a bare gate_rv root preserves the closed-form truncation
2247 // path for "X | (X > c)". Handles the 2-child rv/agg carrier
2248 // [target, condition] and (defensively) the 3-child uuid carrier
2249 // [target, evidence, joint]; iterates in case of nested conditioning.
2250 while (gc.getGateType(root) == gate_conditioned) {
2251 const auto &w = gc.getWires(root);
2252 if (w.size() < 2)
2253 throw CircuitException("malformed conditioned gate in scalar expression");
2254 evidences.push_back(w[1]);
2255 root = w[0];
2256 }
2257
2258 // 2. Replace every BURIED gate_conditioned by an arith passthrough to its
2259 // target, collecting evidence as well.
2260 std::set<gate_t> seen;
2261 std::vector<gate_t> stack{root};
2262 while (!stack.empty()) {
2263 gate_t g = stack.back();
2264 stack.pop_back();
2265 if (!seen.insert(g).second) continue;
2266 if (gc.getGateType(g) == gate_conditioned) {
2267 const auto &w = gc.getWires(g);
2268 if (w.size() < 2)
2269 throw CircuitException("malformed conditioned gate in scalar expression");
2270 gate_t target = w[0];
2271 evidences.push_back(w[1]);
2272 gc.liftConditionedToTarget(g, target); // g becomes arith PLUS [target]
2273 stack.push_back(target);
2274 } else {
2275 for (gate_t c : gc.getWires(g))
2276 stack.push_back(c);
2277 }
2278 }
2279 if (evidences.empty())
2280 return root;
2281 if (event_opt.has_value())
2282 evidences.push_back(*event_opt);
2283 gate_t cond;
2284 if (evidences.size() == 1)
2285 cond = evidences[0];
2286 else {
2287 cond = gc.setGate(gate_times); // AND of all evidence (and prior event)
2288 auto &cw = gc.getWires(cond);
2289 for (gate_t e : evidences)
2290 cw.push_back(e);
2291 }
2292 event_opt = cond;
2293 return root;
2294}
2295
2296} // namespace provsql
2297
2298extern "C" {
2299
2300/**
2301 * @brief SQL: rv_moment(token uuid, k integer, central boolean,
2302 * prov uuid DEFAULT gate_one()) -> float8
2303 *
2304 * Single C entry point shared by the @c expected / @c variance /
2305 * @c moment / @c central_moment SQL functions. The SQL wrappers
2306 * select the (k, central) pair that matches their semantics:
2307 * - @c expected(rv, prov): k=1, central=false.
2308 * - @c variance(rv, prov): k=2, central=true.
2309 * - @c moment(rv, k, prov): central=false.
2310 * - @c central_moment(rv, k, prov): central=true.
2311 *
2312 * The @p prov argument carries the conditioning event: typically the
2313 * row's @c provenance() gate after a @c WHERE predicate folded a
2314 * @c gate_cmp into it. When @p prov resolves to @c gate_one (the
2315 * default, or the load-time simplification of any always-true
2316 * sub-circuit) the unconditional path runs unchanged. Otherwise we
2317 * load a JOINT circuit reaching both roots, so shared @c gate_rv
2318 * leaves collapse to a single @c gate_t -- the property the
2319 * conditional MC sampler relies on to couple the indicator's draw
2320 * with the value's draw.
2321 */
2322/**
2323 * @brief SQL: agg_avg_moment_exact(token uuid, k integer) -> float8
2324 *
2325 * The exact independent-rows arm behind @c agg_raw_moment's @c avg
2326 * dispatch: E[AVG^k | COUNT >= 1] from the joint (sum, count) PMF over
2327 * pairwise leaf-disjoint contributors (@c aggAvgRawMomentExact).
2328 * Returns NULL when the shape is out of scope -- shared leaves, compound
2329 * contributors, unset probabilities -- and the SQL caller falls back to
2330 * the Monte-Carlo scalar path.
2331 */
2332Datum agg_avg_moment_exact(PG_FUNCTION_ARGS)
2333{
2334 try {
2335 pg_uuid_t *token = PG_GETARG_UUID_P(0);
2336 const int32 k_signed = PG_GETARG_INT32(1);
2337
2338 if (k_signed < 0)
2339 provsql_error("agg_avg_moment_exact: k must be non-negative (got %d)",
2340 k_signed);
2341
2342 auto gc = getGenericCircuit(*token);
2343 gate_t root = gc.getGate(uuid2string(*token));
2344 bool ok = false;
2345 const double r = provsql::aggAvgRawMomentExact(
2346 gc, root, static_cast<unsigned>(k_signed), ok);
2347 if (!ok)
2348 PG_RETURN_NULL();
2349 return Float8GetDatum(r);
2350 } catch (const std::exception &e) {
2351 provsql_error("agg_avg_moment_exact: %s", e.what());
2352 } catch (...) {
2353 provsql_error("agg_avg_moment_exact: unknown exception");
2354 }
2355 PG_RETURN_NULL();
2356}
2357
2358Datum rv_moment(PG_FUNCTION_ARGS)
2359{
2360 try {
2361 pg_uuid_t *token = PG_GETARG_UUID_P(0);
2362 const int32 k_signed = PG_GETARG_INT32(1);
2363 const bool central = PG_GETARG_BOOL(2);
2364 pg_uuid_t *prov = PG_GETARG_UUID_P(3);
2365
2366 if (k_signed < 0)
2367 provsql_error("rv_moment: k must be non-negative (got %d)", k_signed);
2368 const unsigned k = static_cast<unsigned>(k_signed);
2369
2370 gate_t root_gate, event_gate;
2371 auto gc = getJointCircuit(*token, *prov, root_gate, event_gate);
2372
2373 /* gate_one event = unconditional after load-time simplification. */
2374 std::optional<gate_t> event_opt;
2375 if (gc.getGateType(event_gate) != gate_one)
2376 event_opt = event_gate;
2377
2378 /* Arithmetic over conditioned distributions: peel a conditioned ROOT to
2379 * its bare target (keeping the closed-form truncation path for the
2380 * bare-rv case) and lift any nested gate_conditioned out of the scalar
2381 * expression, folding its evidence into the conditioning event
2382 * (f(X|A, Y|B) = f(X, Y) | (A ∧ B)). Works whether the token arrives
2383 * already unpacked by the SQL dispatcher or as a raw conditioned root
2384 * (Studio's distribution panel calls this low-level binding directly). */
2385 root_gate = provsql::lift_conditioning(gc, root_gate, event_opt);
2386
2387 double result;
2388 if (central)
2389 result = provsql::compute_central_moment(gc, root_gate, k, event_opt);
2390 else if (k == 1)
2391 result = provsql::compute_expectation(gc, root_gate, event_opt);
2392 else
2393 result = provsql::compute_raw_moment(gc, root_gate, k, event_opt);
2394 return Float8GetDatum(result);
2395 } catch (const std::exception &e) {
2396 provsql_error("rv_moment: %s", e.what());
2397 } catch (...) {
2398 provsql_error("rv_moment: unknown exception");
2399 }
2400 PG_RETURN_NULL();
2401}
2402
2403/**
2404 * @brief SQL: rv_quantile(token uuid, p float8,
2405 * prov uuid DEFAULT gate_one()) -> float8
2406 *
2407 * C entry point behind the polymorphic @c quantile SQL dispatcher.
2408 * Same conditioning plumbing as @c rv_moment (joint circuit, nested
2409 * @c gate_conditioned lifting); the evaluation itself is
2410 * @c compute_quantile: closed-form / numerically inverted CDF for a
2411 * (possibly truncated) bare @c gate_rv, exact generalised inverse for
2412 * a categorical mixture, empirical MC quantile for compound circuits.
2413 */
2414Datum rv_quantile(PG_FUNCTION_ARGS)
2415{
2416 try {
2417 pg_uuid_t *token = PG_GETARG_UUID_P(0);
2418 const double p = PG_GETARG_FLOAT8(1);
2419 pg_uuid_t *prov = PG_GETARG_UUID_P(2);
2420
2421 if (std::isnan(p) || p < 0.0 || p > 1.0)
2422 provsql_error("rv_quantile: p must be in [0, 1] (got %g)", p);
2423
2424 gate_t root_gate, event_gate;
2425 auto gc = getJointCircuit(*token, *prov, root_gate, event_gate);
2426
2427 /* gate_one event = unconditional after load-time simplification. */
2428 std::optional<gate_t> event_opt;
2429 if (gc.getGateType(event_gate) != gate_one)
2430 event_opt = event_gate;
2431
2432 root_gate = provsql::lift_conditioning(gc, root_gate, event_opt);
2433
2434 return Float8GetDatum(
2435 provsql::compute_quantile(gc, root_gate, p, event_opt));
2436 } catch (const std::exception &e) {
2437 provsql_error("rv_quantile: %s", e.what());
2438 } catch (...) {
2439 provsql_error("rv_quantile: unknown exception");
2440 }
2441 PG_RETURN_NULL();
2442}
2443
2444/**
2445 * @brief SQL: rv_evidence(evidence uuid) -> float8
2446 *
2447 * The marginal likelihood @c P(data) of an evidence circuit: the mean raw
2448 * importance weight over @c provsql.rv_mc_samples prior draws (the same
2449 * quantity rejection conditioning computes as @c P(C), now a product of the
2450 * observations' densities). Backs @c provsql.evidence.
2451 */
2452Datum rv_evidence(PG_FUNCTION_ARGS)
2453{
2454 try {
2455 pg_uuid_t *token = PG_GETARG_UUID_P(0);
2456 auto gc = getGenericCircuit(*token);
2457 gate_t root = gc.getGate(uuid2string(*token));
2458 /* Conjugate shape with predictive densities registered for every
2459 * observation in the fold: the marginal likelihood is the exact
2460 * product of the sequential predictives (chain rule), accumulated
2461 * in log space. */
2462 if (auto le = provsql::conjugateLogEvidence(gc, root))
2463 return Float8GetDatum(std::exp(*le));
2464 if (provsql_rv_mc_samples == 0)
2466 "rv_evidence: provsql.rv_mc_samples is 0 (the marginal likelihood is "
2467 "estimated by Monte Carlo); set it to a positive sample budget");
2468 const double e = provsql::importanceEvidence(
2469 gc, root, static_cast<unsigned>(provsql_rv_mc_samples));
2470 return Float8GetDatum(e);
2471 } catch (const std::exception &ex) {
2472 provsql_error("rv_evidence: %s", ex.what());
2473 } catch (...) {
2474 provsql_error("rv_evidence: unknown exception");
2475 }
2476 PG_RETURN_NULL();
2477}
2478
2479} // extern "C"
Exact closed-form HAVING COUNT(*) op C probability over safe-join lineage – the recursive marginal-ve...
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
Typed aggregation value, operator, and aggregator abstractions.
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
Closed-form CDF resolution for trivial gate_cmp shapes.
Boolean-expression (lineage formula) semiring.
Boolean provenance circuit with support for knowledge compilation.
GenericCircuit getJointCircuit(const std::vector< pg_uuid_t > &tokens, std::vector< gate_t > &gates)
Multi-root variant of getJointCircuit.
GenericCircuit getGenericCircuit(pg_uuid_t token)
Build a GenericCircuit from the mmap store rooted at token.
Build in-memory circuits from the mmap-backed persistent store.
Generic directed-acyclic-graph circuit template and gate identifier.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Rao-Blackwellised (collapsed) evaluation of a correlated COUNT / SUM and of a latent conditioned on s...
The single comparator-resolution pipeline and the single Boolean-subcircuit probability entry point,...
Exact conjugate-prior posteriors for observe-evidence circuits.
Per-family polymorphic view over a continuous gate_rv distribution (§F.1 class hierarchy).
Datum rv_quantile(PG_FUNCTION_ARGS)
SQL: rv_quantile(token uuid, p float8, prov uuid DEFAULT gate_one()) -> float8...
Datum rv_moment(PG_FUNCTION_ARGS)
Datum agg_avg_moment_exact(PG_FUNCTION_ARGS)
SQL: rv_moment(token uuid, k integer, central boolean, prov uuid DEFAULT gate_on...
Datum rv_evidence(PG_FUNCTION_ARGS)
SQL: rv_evidence(evidence uuid) -> float8.
Analytical expectation / variance / moment evaluator over RV circuits.
Monte Carlo sampling over a GenericCircuit, RV-aware.
Shared 1-D quadrature core for the pivot-conjunction and order-statistic closed forms.
Catalog of probability-evaluation methods (Strategy + registry).
Continuous random-variable helpers (distribution parsing, moments).
Support-based bound check for continuous-RV comparators.
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:140
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
void addWire(gate_t f, gate_t t)
Add a directed wire from gate f (parent) to gate t (child).
Definition Circuit.hpp:81
uuid getUUID(gate_t g) const
Return the UUID string associated with gate g.
Definition Circuit.hpp:46
gate_t getGate(const uuid &u)
Return (or create) the gate associated with UUID u.
Definition Circuit.hpp:33
In-memory provenance circuit with semiring-generic evaluation.
bool isCategoricalMixture(gate_t g) const
Test whether g is a categorical-form gate_mixture (the explicit provsql.categorical output).
void setInfos(gate_t g, unsigned info1, unsigned info2)
Set the integer annotation pair for gate g.
std::string getExtra(gate_t g) const
Return the string extra for gate g.
gate_t setGate(gate_type type) override
Allocate a new gate with type type and no UUID.
double getProb(gate_t g) const
Return the probability for gate g.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
void liftConditionedToTarget(gate_t g, gate_t target)
Replace a gate_conditioned g by a transparent passthrough to its target child (a single-child gate_ar...
void setExtra(gate_t g, const std::string &ex)
Attach a string extra to gate g.
void setProb(gate_t g, double p)
Set the probability for gate g.
Abstract per-family continuous distribution.
double compute_raw_moment(const GenericCircuit &gc, gate_t root, unsigned k, std::optional< gate_t > event_root)
Compute the raw moment (or if event_root is set) for k >= 0.
std::optional< double > conjugateLogEvidence(const GenericCircuit &gc, gate_t evidence)
The exact log marginal likelihood of a conjugate-shaped evidence circuit; std::nullopt on any shape ...
double compute_quantile(const GenericCircuit &gc, gate_t root, double p, std::optional< gate_t > event_root)
Compute the p-quantile of the scalar rooted at root (of the truncated distribution if event_root is ...
std::optional< std::vector< std::pair< double, double > > > enumerateScalarWorlds(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event, unsigned max_inputs)
The possible worlds of a circuit whose only random sources are Boolean inputs: the exact alternative ...
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.
gate_t lift_conditioning(GenericCircuit &gc, gate_t root, std::optional< gate_t > &event_opt)
Lift conditioning out of a scalar arithmetic expression.
double analytical_variance(const DistributionSpec &d)
Closed-form variance Var(X) for a basic distribution.
double importanceEvidence(const GenericCircuit &gc, gate_t evidence, unsigned samples)
Marginal likelihood P(data) of evidence: the mean raw importance weight over samples prior draws.
double booleanSubcircuitProbability(GenericCircuit &gc, gate_t root, const std::string &method, const std::string &args, bool inv_free_cert, const Tolerance &tol, bool mc_fallback, std::string *actual_method_out)
Probability of the Boolean function rooted at root in gc – THE single entry point over the method por...
std::unique_ptr< Distribution > closeTransform(const char *transform, const Distribution &x)
The image distribution of transform applied to x, when a registered rule covers x's family; nullptr o...
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
std::optional< double > centralFromRaw(unsigned k, Raw &&raw)
Central moment of order k from a raw-moment closure: .
bool eventIsProvablyInfeasible(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
True iff the conditioning event is provably infeasible for a bare gate_rv root.
std::unique_ptr< Distribution > makeDistribution(const DistributionSpec &spec)
Construct the per-family Distribution for a parsed spec.
double compute_central_moment(const GenericCircuit &gc, gate_t root, unsigned k, std::optional< gate_t > event_root)
Compute the central moment (or if event_root is set).
double simpsonIntegrate(double lo, double hi, int N, F &&f)
Composite-Simpson with N panels.
ConditionalScalarSamples monteCarloConditionalScalarSamples(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
Rejection-sample root conditioned on event_root.
double binomial(unsigned n, unsigned k)
Binomial coefficient as a double (exact for the small orders the moment expansions use).
std::optional< double > collapsedConditionalMoment(const GenericCircuit &gc, gate_t target, gate_t event, unsigned k)
Collapsed exact posterior raw moment E[R^k | Y = C] for a latent target R conditioned (through the eq...
std::unique_ptr< Distribution > closeProductFactors(const std::vector< const Distribution * > &factors)
Fold a product of independent factors into a single distribution when a registered closure covers eve...
double evaluateBooleanProbability(const GenericCircuit &gc, gate_t boolRoot)
Probability that the Boolean subcircuit rooted at boolRoot evaluates to true under the tuple-independ...
std::optional< DistributionSpec > conjugatePosterior(const GenericCircuit &gc, gate_t target, gate_t evidence)
The exact posterior of target given evidence, as a resolved distribution spec, when the circuit match...
std::vector< double > monteCarloScalarSamples(const GenericCircuit &gc, gate_t root, unsigned samples)
Sample a scalar sub-circuit samples times and return the draws.
std::optional< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
std::optional< DistributionTemplate > parse_distribution_template(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution, keeping wired (token) parameters as wire r...
double numericQuantile(const Distribution &d, double p)
Numeric inverse CDF: monotone bisection of cdf() over the family's integration window.
double analytical_mean(const DistributionSpec &d)
Closed-form expectation E[X] for a basic distribution.
constexpr int kSimpsonPanels
Panel count shared by every composite-Simpson quadrature over a distribution's integration range: exa...
WeightedPosterior importanceSampleConditional(const GenericCircuit &gc, gate_t root, gate_t evidence, unsigned samples)
Self-normalised importance sampling of root given evidence.
std::optional< TruncatedSingleRv > matchTruncatedSingleRv(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
Detect a closed-form, optionally-truncated single-RV shape.
double compute_expectation(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
Compute (or if event_root is set) over the scalar sub-circuit rooted at root.
double analytical_raw_moment(const DistributionSpec &d, unsigned k)
Closed-form raw moment for a basic distribution.
bool circuitHasObserve(const GenericCircuit &gc, gate_t root)
Whether the circuit reachable from root contains a gate_observe – the signal that a conditioning even...
void resolveComparators(GenericCircuit &gc, gate_t root, bool simplify, bool decompose)
Run the comparator-resolution pipeline on gc, rewriting every gate_cmp (RV comparison,...
int provsql_verbose
Verbosity level; controlled by the provsql.verbose_level GUC.
Definition provsql.c:113
double provsql_ess_warn_fraction
Effective-sample-size warning threshold for likelihood weighting: warn when the posterior ESS falls b...
Definition provsql.c:125
int provsql_rv_mc_samples
Default sample count for analytical-evaluator MC fallbacks; 0 disables fallback (callers raise instea...
Definition provsql.c:124
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
#define provsql_warning(fmt,...)
Emit a ProvSQL warning message (execution continues).
#define provsql_notice(fmt,...)
Emit a ProvSQL informational notice (execution continues).
const char * gate_type_name[]
Names of gate types.
Core types, constants, and utilities shared across ProvSQL.
provsql_arith_op
Arithmetic operator tags used by gate_arith.
@ PROVSQL_ARITH_PERCENTILE
continuous percentile (order-statistic aggregate): wires are interleaved [ind_1, x_1,...
@ PROVSQL_ARITH_DIV
binary, child0 / child1
@ PROVSQL_ARITH_ASFLOAT8
unary, child0 as double precision
@ PROVSQL_ARITH_LN
unary, natural logarithm of child0 (a negative draw raises at evaluation)
@ PROVSQL_ARITH_ROUND
child0 rounded half away from zero, to child1 decimal digits where a second child is given (SQL round...
@ PROVSQL_ARITH_PLUS
n-ary, sum of children
@ PROVSQL_ARITH_POW
binary, child0 ^ child1 (real branch only: a negative base drawn with a non-integer exponent raises a...
@ PROVSQL_ARITH_ABS
unary, |child0|
@ PROVSQL_ARITH_FLOOR
unary, greatest integer <= child0
@ PROVSQL_ARITH_NEG
unary, -child0
@ PROVSQL_ARITH_INTDIV
binary, child0 / child1 truncated toward zero: SQL's division of two integers
@ PROVSQL_ARITH_MINUS
binary, child0 - child1
@ PROVSQL_ARITH_CEIL
unary, least integer >= child0
@ PROVSQL_ARITH_EXP
unary, e^child0
@ PROVSQL_ARITH_TIMES
n-ary, product of children
@ PROVSQL_ARITH_MIN
n-ary, min of children (order statistic; least / min aggregate)
@ PROVSQL_ARITH_MAX
n-ary, max of children (order statistic; greatest / max aggregate)
@ PROVSQL_ARITH_ASFLOAT4
unary, child0 as real reads it
@ gate_observe
Latent-variable observation (likelihood-weighting evidence): one wire → an observed bare gate_rv leaf...
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
@ gate_case
N-ary guarded selection over scalar (RV) children: wires are [guard_1, value_1, .....
@ gate_annotation
Transparent single-child wrapper carrying a query-level annotation in extra (inversion-free certifica...
@ gate_mobius
Signed Möbius combination: a MEASURE-only gate carrying one integer coefficient per child (in extra,...
@ gate_conditioned
Conditioning marker with two children [target, evidence]: measure-only, probability_evaluate returns ...
@ gate_mixture
Probabilistic mixture: three wires [p_token (gate_input Bernoulli), x_token, y_token]; samples x when...
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
@ gate_assumed
Structural marker over a single child whose sub-circuit was computed under a Boolean-provenance assum...
string uuid2string(pg_uuid_t uuid)
Format a pg_uuid_t as a std::string.
C++ utility functions for UUID manipulation.
Outcome of a conditional Monte Carlo sampling pass.
One parameter slot of a gate_rv, either a literal or a wire.
Parsed distribution spec (family + up to two parameters).
A gate_rv distribution spec that may carry wired (token) parameters – the parse-time counterpart of D...
Outcome of a likelihood-weighting (importance-sampling) pass.