ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
MonteCarloSampler.cpp
Go to the documentation of this file.
1/**
2 * @file MonteCarloSampler.cpp
3 * @brief Implementation of the RV-aware Monte Carlo sampler.
4 */
5#include "MonteCarloSampler.h"
6#include "Aggregation.h"
7#include "RandomVariable.h"
8#include "distributions/Distribution.h" // makeDistribution -> per-family sample()
9#include "RangeCheck.h" // collectRvConstraints
10#include "Circuit.h"
11
12#include <algorithm>
13#include <cmath>
14#include <cstdint>
15#include <limits>
16#include <memory>
17#include <optional>
18#include <random>
19#include <stack>
20#include <stdexcept>
21#include <string>
22#include <unordered_map>
23#include <unordered_set>
24#include <variant>
25#include <vector>
26
27namespace provsql {
28
29std::mt19937_64 seedRng()
30{
31 std::mt19937_64 rng;
32 if(provsql_monte_carlo_seed != -1) {
33 rng.seed(static_cast<uint64_t>(provsql_monte_carlo_seed));
34 } else {
35 std::random_device rd;
36 rng.seed((static_cast<uint64_t>(rd()) << 32) | rd());
37 }
38 return rng;
39}
40
41namespace {
42
43bool applyCmp(double l, ComparisonOperator op, double r)
44{
45 // IEEE 754 semantics: any comparison involving NaN is false except !=.
46 switch(op) {
47 case ComparisonOperator::LT: return l < r;
48 case ComparisonOperator::LE: return l <= r;
49 case ComparisonOperator::EQ: return l == r;
50 case ComparisonOperator::NE: return l != r;
51 case ComparisonOperator::GE: return l >= r;
52 case ComparisonOperator::GT: return l > r;
53 }
54 return false;
55}
56
57/// Recognise a point observation @c "Y = c": a @c gate_cmp with the @c EQ
58/// operator, one wire a bare @c gate_rv leaf @p leaf_out, the other a
59/// constant @c gate_value @p datum_out. This is the internal form of
60/// @c observe(Y, c) -- the conditioning-evidence interpretation of the
61/// equality, a likelihood weight by the leaf's density/mass at @c c (a
62/// continuous point event is measure-zero as a rejection interval, so this
63/// is the only meaningful reading of it as evidence). Returns @c false for
64/// an inequality, a non-leaf scalar, or a non-constant right side (those
65/// stay ordinary Boolean events).
66bool matchPointObservationCmp(const GenericCircuit &gc, gate_t g,
67 gate_t &leaf_out, double &datum_out)
68{
69 if(gc.getGateType(g) != gate_cmp) return false;
70 const auto &wires = gc.getWires(g);
71 if(wires.size() != 2) return false;
72 bool ok = false;
73 ComparisonOperator op = cmpOpFromOid(gc.getInfos(g).first, ok);
74 if(!ok || op != ComparisonOperator::EQ) return false;
75 auto try_side = [&](gate_t rv_side, gate_t const_side) {
76 if(gc.getGateType(rv_side) != gate_rv) return false;
77 if(gc.getGateType(const_side) != gate_value) return false;
78 try { datum_out = parseDoubleStrict(gc.getExtra(const_side)); }
79 catch(const CircuitException &) { return false; }
80 leaf_out = rv_side;
81 return true;
82 };
83 return try_side(wires[0], wires[1]) || try_side(wires[1], wires[0]);
84}
85
86/// Per-iteration sampler state shared between the Boolean and scalar
87/// recursions.
88class Sampler {
89public:
90 Sampler(const GenericCircuit &gc, std::mt19937_64 &rng)
91 : gc_(gc), rng_(rng) {}
92
93 /// Reset per-iteration memo caches.
94 void resetIteration() {
95 bool_cache_.clear();
96 scalar_cache_.clear();
97 }
98
99 bool evalBool(gate_t g);
100 double evalScalar(gate_t g);
101 double evalWeight(gate_t g);
102
103 /// Read the inputs from @p world instead of drawing them (enumeration
104 /// of the possible worlds); @c nullptr draws them again.
105 void fixInputs(const std::unordered_map<gate_t, bool> *world) {
106 fixed_ = world;
107 }
108
109private:
110 /// Build the per-draw Distribution for a (possibly latent) gate_rv leaf,
111 /// resolving wired parameters through evalScalar (so a shared latent
112 /// lands in scalar_cache_ and couples the callers), with the
113 /// parameter-domain guard. Outputs the resolved parameters in p1/p2.
114 std::unique_ptr<Distribution> buildRvDistribution(
115 gate_t leaf, const DistributionTemplate &tmpl, double &p1, double &p2);
116
117 const GenericCircuit &gc_;
118 std::mt19937_64 &rng_;
119 std::unordered_map<gate_t, bool> bool_cache_;
120 std::unordered_map<gate_t, double> scalar_cache_;
121 const std::unordered_map<gate_t, bool> *fixed_ = nullptr;
122 // Per-gate_rv Distribution, constructed once and reused across iterations
123 // (NOT cleared in resetIteration): sampling then never re-parses the spec
124 // or re-constructs the Distribution per draw.
125 std::unordered_map<gate_t, std::unique_ptr<Distribution>> dist_cache_;
126};
127
128bool Sampler::evalBool(gate_t g)
129{
130 auto it = bool_cache_.find(g);
131 if(it != bool_cache_.end()) return it->second;
132
133 bool result = false;
134 const auto type = gc_.getGateType(g);
135 const auto &wires = gc_.getWires(g);
136
137 switch(type) {
138 case gate_input:
139 case gate_update:
140 {
141 if(fixed_ != nullptr) {
142 result = fixed_->at(g);
143 break;
144 }
145 std::uniform_real_distribution<double> u(0.0, 1.0);
146 result = u(rng_) < gc_.getProb(g);
147 break;
148 }
149 case gate_plus:
150 result = false;
151 for(gate_t c : wires) {
152 if(evalBool(c)) { result = true; break; }
153 }
154 break;
155 case gate_times:
156 result = true;
157 for(gate_t c : wires) {
158 if(!evalBool(c)) { result = false; break; }
159 }
160 break;
161 case gate_monus:
162 if(wires.size() != 2)
163 throw CircuitException("gate_monus must have exactly two children");
164 result = evalBool(wires[0]) && !evalBool(wires[1]);
165 break;
166 case gate_zero:
167 result = false;
168 break;
169 case gate_one:
170 result = true;
171 break;
172 case gate_cmp:
173 {
174 if(wires.size() != 2)
175 throw CircuitException("gate_cmp must have exactly two children");
176 bool ok;
177 ComparisonOperator op = cmpOpFromOid(gc_.getInfos(g).first, ok);
178 if(!ok)
179 throw CircuitException(
180 "gate_cmp: unsupported operator OID " +
181 std::to_string(gc_.getInfos(g).first));
182 double l = evalScalar(wires[0]);
183 double r = evalScalar(wires[1]);
184 result = applyCmp(l, op, r);
185 break;
186 }
187 case gate_mulinput:
188 throw CircuitException(
189 "Monte Carlo over circuits containing gate_mulinput "
190 "is not yet supported on the RV path");
191 case gate_delta:
192 // δ-semiring operator: identity on the Boolean semiring, so the
193 // sampled truth value is just the wrapped child's. Showed up
194 // when conditioning on a row's provenance() in an aggregate
195 // query (HAVING / GROUP BY paths can splice δ over the
196 // semimod's k-side).
197 if(wires.size() != 1)
198 throw CircuitException("gate_delta must have exactly one child");
199 result = evalBool(wires[0]);
200 break;
201 case gate_assumed:
202 // Structural Boolean-rewrite marker: identity on the Boolean
203 // semiring, so the sampled truth value is the wrapped child's.
204 // The marker exists to refuse non-Boolean-compat evaluation; MC
205 // sampling for probability is always Boolean-compat.
206 if(wires.size() != 1)
207 throw CircuitException(
208 "gate_assumed must have exactly one child");
209 result = evalBool(wires[0]);
210 break;
211 case gate_annotation:
212 // Transparent annotation wrapper (inversion-free certificate / order
213 // key): identity, so the sampled truth value is the wrapped child's.
214 if(wires.size() != 1)
215 throw CircuitException("gate_annotation must have exactly one child");
216 result = evalBool(wires[0]);
217 break;
218 default:
219 throw CircuitException(
220 "Unsupported gate type in Boolean evaluation: " +
221 std::string(gate_type_name[type]));
222 }
223
224 bool_cache_[g] = result;
225 return result;
226}
227
228double Sampler::evalScalar(gate_t g)
229{
230 auto it = scalar_cache_.find(g);
231 if(it != scalar_cache_.end()) return it->second;
232
233 double result = 0.0;
234 const auto type = gc_.getGateType(g);
235 const auto &wires = gc_.getWires(g);
236
237 switch(type) {
238 case gate_value:
239 /* The NULL value (a NULL branch of a CASE): undefined, as an aggregate
240 * over no row */
241 if(gc_.getUUID(g) == PROVSQL_GATE_NULL_UUID)
242 result = std::numeric_limits<double>::quiet_NaN();
243 else
244 result = parseDoubleStrict(gc_.getExtra(g));
245 break;
246 case gate_rv:
247 {
248 // Fast path: an all-literal leaf builds its Distribution once and
249 // reuses it across iterations (dist_cache_ is never reset), so
250 // sampling never re-parses the spec. A latent (parametric) leaf,
251 // whose parameters are wire references resolved per iteration,
252 // bypasses that cache: its distribution changes every draw.
253 auto dit = dist_cache_.find(g);
254 if(dit != dist_cache_.end()) {
255 result = dit->second->sample(rng_);
256 break;
257 }
258 auto tmpl = parse_distribution_template(gc_.getExtra(g));
259 if(!tmpl)
260 throw CircuitException(
261 "Malformed gate_rv extra: " + gc_.getExtra(g));
262 if(!tmpl->parametric()) {
263 DistributionSpec spec{tmpl->family, tmpl->p1.literal, tmpl->p2.literal};
264 dit = dist_cache_.emplace(g, makeDistribution(spec)).first;
265 result = dit->second->sample(rng_);
266 break;
267 }
268 // Parametric leaf: resolve each wired parameter from the gate's
269 // wires via evalScalar (so a latent shared across leaves lands in
270 // scalar_cache_ and couples them), then build the family instance
271 // for this draw's parameters. The (double,double) factory and the
272 // Distribution interface are untouched -- only the parameters'
273 // source changes.
274 double p1, p2;
275 result = buildRvDistribution(g, *tmpl, p1, p2)->sample(rng_);
276 break;
277 }
278 case gate_arith:
279 {
280 if(wires.empty())
281 throw CircuitException("gate_arith must have at least one child");
282 auto op = static_cast<provsql_arith_op>(gc_.getInfos(g).first);
283 switch(op) {
285 result = 0.0;
286 for(gate_t c : wires) result += evalScalar(c);
287 break;
289 result = 1.0;
290 for(gate_t c : wires) result *= evalScalar(c);
291 break;
293 if(wires.size() != 2)
294 throw CircuitException("gate_arith MINUS must be binary");
295 result = evalScalar(wires[0]) - evalScalar(wires[1]);
296 break;
299 {
300 if(wires.size() != 2)
301 throw CircuitException(
302 std::string("gate_arith ")
303 + (op == PROVSQL_ARITH_DIV ? "DIV" : "INTDIV")
304 + " must be binary");
305 const double num = evalScalar(wires[0]);
306 const double den = evalScalar(wires[1]);
307 // A divisor that is zero in THIS world leaves the world without a
308 // value, as SQL leaves the query without a result there: the draw is
309 // a missing observation and the moment is taken over the worlds that
310 // do have one, exactly as for an aggregate with no contributing row.
311 // Dividing anyway gave an infinity that swallowed the estimate --
312 // expected(sum(a)/sum(b)) came back Infinity for data whose divisor
313 // cancels in one world only, where the same division under a HAVING
314 // answers over the other worlds (having_semantics declines this one).
315 // NaN is this sampler's marker for such a world (see POW below), and
316 // the moment estimators skip those draws.
317 if(den == 0.0)
318 result = std::numeric_limits<double>::quiet_NaN();
319 else if(op == PROVSQL_ARITH_INTDIV)
320 result = std::trunc(num / den);
321 else
322 result = num / den;
323 break;
324 }
326 if(wires.size() != 1)
327 throw CircuitException("gate_arith NEG must be unary");
328 result = -evalScalar(wires[0]);
329 break;
331 // n-ary order statistic: max over the sampled children. Shared
332 // base RVs stay coupled through scalar_cache_, so max(x, y) with x,y
333 // over the same leaf draws them jointly (correct correlation).
334 result = evalScalar(wires[0]);
335 for(std::size_t i = 1; i < wires.size(); ++i)
336 result = std::max(result, evalScalar(wires[i]));
337 break;
339 result = evalScalar(wires[0]);
340 for(std::size_t i = 1; i < wires.size(); ++i)
341 result = std::min(result, evalScalar(wires[i]));
342 break;
344 {
345 if(wires.size() != 2)
346 throw CircuitException("gate_arith POW must be binary");
347 const double base = evalScalar(wires[0]);
348 const double expo = evalScalar(wires[1]);
349 result = std::pow(base, expo);
350 // std::pow is real-valued except for a negative base with a
351 // non-integer exponent. A NaN there is a domain violation,
352 // not an undefined world: raise with the fix rather than let
353 // the moment estimators silently drop the draw as a missing
354 // observation (which would report a biased, implicitly
355 // conditioned answer). NaN operands (undefined worlds, e.g.
356 // empty-group aggregates) still propagate as NaN.
357 if(std::isnan(result) && !std::isnan(base) && !std::isnan(expo))
358 throw CircuitException(
359 "pow: negative base drawn with a non-integer exponent ("
360 + std::to_string(base) + " ^ " + std::to_string(expo)
361 + "); restrict the base to be non-negative, e.g. "
362 "pow(greatest(x, 0), p)");
363 break;
364 }
365 case PROVSQL_ARITH_LN:
366 {
367 if(wires.size() != 1)
368 throw CircuitException("gate_arith LN must be unary");
369 const double x = evalScalar(wires[0]);
370 // Same rationale as POW: a negative draw is a domain
371 // violation, raised rather than silently conditioned away.
372 // x = 0 legitimately yields -Infinity (a boundary value of
373 // probability zero for continuous arguments); NaN operands
374 // propagate as undefined worlds.
375 if(x < 0.0)
376 throw CircuitException(
377 "ln: negative draw (" + std::to_string(x)
378 + "); ln is only defined on [0, +Infinity) -- "
379 "restrict the argument's support");
380 result = std::log(x);
381 break;
382 }
384 if(wires.size() != 1)
385 throw CircuitException("gate_arith EXP must be unary");
386 result = std::exp(evalScalar(wires[0]));
387 break;
389 {
390 // SQL round(v) / round(v, d): half away from zero, as numeric
391 // rounding is, and to d digits where a second wire gives d. The
392 // value is read per world, so the rounding applies to the value the
393 // world has, never to an expectation (rounding does not commute
394 // with it, which is why the moment evaluators sample it).
395 if(wires.empty() || wires.size() > 2)
396 throw CircuitException("gate_arith ROUND takes one or two wires");
397 const double x = evalScalar(wires[0]);
398 if(wires.size() == 1) {
399 result = std::round(x);
400 } else {
401 const double d = evalScalar(wires[1]);
402 if(std::isnan(x) || std::isnan(d)) { result = x + d; break; }
403 const double f = std::pow(10.0, d);
404 result = std::round(x * f) / f;
405 }
406 break;
407 }
409 if(wires.size() != 1)
410 throw CircuitException("gate_arith FLOOR must be unary");
411 result = std::floor(evalScalar(wires[0]));
412 break;
414 if(wires.size() != 1)
415 throw CircuitException("gate_arith CEIL must be unary");
416 result = std::ceil(evalScalar(wires[0]));
417 break;
419 if(wires.size() != 1)
420 throw CircuitException("gate_arith ABS must be unary");
421 result = std::fabs(evalScalar(wires[0]));
422 break;
424 if(wires.size() != 1)
425 throw CircuitException("gate_arith ASFLOAT8 must be unary");
426 result = evalScalar(wires[0]); /* already a double */
427 break;
429 if(wires.size() != 1)
430 throw CircuitException("gate_arith ASFLOAT4 must be unary");
431 result = static_cast<double>(
432 static_cast<float>(evalScalar(wires[0])));
433 break;
435 {
436 // Continuous percentile (SQL percentile_cont) over the group's
437 // rows: wires are interleaved [ind_1, x_1, ..., ind_n, x_n],
438 // the fraction is text-encoded in extra. Per draw, the values
439 // whose 0/1 presence indicator draws 1 are sorted and linearly
440 // interpolated at the fraction; a draw with no present row is
441 // NaN (undefined world, skipped by the moment estimators like
442 // an empty-group avg).
443 if(wires.size() < 2 || wires.size() % 2 != 0)
444 throw CircuitException(
445 "gate_arith PERCENTILE must have interleaved "
446 "indicator/value wires");
447 double fraction;
448 try {
449 fraction = std::stod(gc_.getExtra(g));
450 } catch(const std::exception &) {
451 throw CircuitException(
452 "Malformed gate_arith PERCENTILE extra (expected the "
453 "fraction): " + gc_.getExtra(g));
454 }
455 std::vector<double> members;
456 bool has_nan = false;
457 for(std::size_t i = 0; i < wires.size(); i += 2) {
458 if(evalScalar(wires[i]) >= 0.5) {
459 const double x = evalScalar(wires[i + 1]);
460 if(std::isnan(x))
461 has_nan = true;
462 else
463 members.push_back(x);
464 }
465 }
466 if(has_nan || members.empty()) {
467 result = std::numeric_limits<double>::quiet_NaN();
468 break;
469 }
470 std::sort(members.begin(), members.end());
471 const double pos = fraction * (members.size() - 1);
472 const std::size_t lo = static_cast<std::size_t>(pos);
473 const double frac = pos - static_cast<double>(lo);
474 result = (lo + 1 < members.size())
475 ? members[lo] + frac * (members[lo + 1] - members[lo])
476 : members[lo];
477 break;
478 }
479 default:
480 throw CircuitException(
481 "Unknown gate_arith operator tag: " +
482 std::to_string(static_cast<unsigned>(op)));
483 }
484 break;
485 }
486 case gate_agg:
487 {
488 // HAVING-style aggregate evaluated per MC iteration: walk the
489 // gate_semimod children, keep the rows whose k_gate fires in
490 // this world, push their value into a reusable Aggregator,
491 // return the finalised scalar. Closes the priority-4-era gap
492 // that made `WHERE rv > 0 GROUP BY x HAVING count(*) > 1`
493 // structural-only (see continuous_selection.sql section G).
494 //
495 // Type plan: we evaluate every numeric path in float8 to stay
496 // inside evalScalar's return type. COUNT is normalised by
497 // makeAggregator to SumAgg<long>, so each kept row contributes its
498 // value gate cast to long: that gate is 1 for an ordinary row and 0
499 // for a NULL one (count(x) does not count NULLs), so the sum of the
500 // kept values is exactly count(*) / count(x) -- faithful with no
501 // nullability check. SUM / AVG / MIN / MAX consume the value via
502 // evalScalar directly. Empty groups finalise to NONE; what that
503 // means depends on the aggregate and on whether the aggregation is
504 // scalar or grouped -- see the NONE arm below.
506 getAggregationOperator(gc_.getInfos(g).first);
507 std::unique_ptr<Aggregator> agg =
511 if(!agg)
512 throw CircuitException(
513 "gate_agg: makeAggregator returned null for op " +
514 std::to_string(static_cast<int>(op)));
515 for(gate_t child : wires) {
516 if(gc_.getGateType(child) != gate_semimod) continue;
517 const auto &sm = gc_.getWires(child);
518 if(sm.size() != 2) continue;
519 if(!evalBool(sm[0])) continue;
521 agg->add(AggValue(static_cast<long>(evalScalar(sm[1]))));
522 } else {
523 agg->add(AggValue(evalScalar(sm[1])));
524 }
525 }
526 AggValue r = agg->finalize();
527 switch(r.getType()) {
528 case ValueType::INT:
529 result = static_cast<double>(std::get<long>(r.v));
530 break;
531 case ValueType::FLOAT:
532 result = std::get<double>(r.v);
533 break;
534 case ValueType::NONE:
535 // No contributor survived this iteration -- either no row of the
536 // group is present in this world, or every contributed value was
537 // NULL. SUM / AVG / MIN / MAX are then SQL NULL, so they surface
538 // NaN, which compares false under IEEE on any enclosing gate_cmp
539 // (the truth value of a comparison against NULL) and is skipped as
540 // a missing observation by the moment averagers in
541 // Expectation::mc_raw_moment / mc_central_moment, making those
542 // estimators conditional on the worlds where the aggregate is
543 // defined.
544 //
545 // COUNT has no NULL to report -- an empty set genuinely counts 0 --
546 // but 0 is the right answer only where a row exists to carry it.
547 // A scalar aggregation always yields its single row, empty input
548 // included, so 0 it is. For a grouped aggregation an empty group
549 // is no row at all: the possible-world semantics excludes that
550 // world (having_semantics.hpp enumerates from world 1, and
551 // RangeCheck rewrites a trivially-true count comparison to the
552 // group-existence gate rather than to gate_one). Reporting 0
553 // there would let a true-on-zero predicate such as
554 // `count(*) <= k` hold in a world that contributes no row,
555 // inflating the estimate by the probability that the group is
556 // empty; NaN keeps the enclosing comparison false, which is how
557 // this sampler declines a world.
558 result =
560 (gc_.getInfos(g).second & PROVSQL_AGG_SCALAR_FLAG) != 0)
561 ? 0.0
562 : std::numeric_limits<double>::quiet_NaN();
563 break;
564 default:
565 throw CircuitException(
566 "gate_agg: unsupported aggregate result ValueType in MC");
567 }
568 break;
569 }
570 case gate_semimod:
571 {
572 // Bare semimod root (the user pinned one of an agg's per-row
573 // contributions): interpret as a Bernoulli-weighted scalar
574 // value · 1_{k fires}. When the Boolean k child does not fire
575 // in this world, the row contributes nothing -- return 0.0
576 // (the additive identity), which matches the per-iteration
577 // role semimod plays inside gate_agg above. This makes
578 // semimod a legal scalar root for rv_sample / rv_moment /
579 // rv_histogram alongside agg.
580 const auto &wires = gc_.getWires(g);
581 if(wires.size() != 2)
582 throw CircuitException(
583 "gate_semimod must have exactly two children "
584 "[k_gate, value_gate]");
585 result = evalBool(wires[0]) ? evalScalar(wires[1]) : 0.0;
586 break;
587 }
588 case gate_mixture:
589 {
590 // Two shapes of gate_mixture share this case:
591 //
592 // - Classic 3-wire: [p_token, x_token, y_token]. Draw the
593 // Bernoulli via evalBool, which handles gate_input by
594 // sampling uniform(0,1) < get_prob and memoises on
595 // bool_cache_; two mixtures sharing the same p_token
596 // therefore see the same draw, and any unrelated Boolean
597 // parent of p_token stays in sync.
598 //
599 // - Categorical N-wire: [key, mul_1, ..., mul_n]. Built
600 // directly by the @c provsql.categorical SQL constructor;
601 // each mul_i carries its probability in set_prob and its
602 // outcome value in extra.
603 // We draw a single uniform[0,1) per block, walk the
604 // cumulative probabilities to pick a mulinput, and stash the
605 // Boolean truth values into bool_cache_ so any downstream
606 // Boolean consumer of the mulinputs (independentEvaluation,
607 // OR/AND parents) sees a consistent sampled outcome.
608 if(gc_.isCategoricalMixture(g)) {
609 std::uniform_real_distribution<double> u(0.0, 1.0);
610 const double r = u(rng_);
611 double cum = 0.0;
612 // Default to the last mulinput in case floating-point cumulative
613 // sums leave us shy of 1.0 by a few ULPs.
614 std::size_t chosen = wires.size() - 1;
615 for(std::size_t i = 1; i < wires.size(); ++i) {
616 cum += gc_.getProb(wires[i]);
617 if(r < cum) { chosen = i; break; }
618 }
619 for(std::size_t i = 1; i < wires.size(); ++i) {
620 bool_cache_[wires[i]] = (i == chosen);
621 }
622 result = parseDoubleStrict(gc_.getExtra(wires[chosen]));
623 break;
624 }
625 if(wires.size() != 3)
626 throw CircuitException(
627 "gate_mixture must have exactly three children "
628 "[p_token, x_token, y_token]");
629 result = evalBool(wires[0]) ? evalScalar(wires[1])
630 : evalScalar(wires[2]);
631 break;
632 }
633 case gate_case:
634 {
635 // Guarded selection [g_1, v_1, ..., g_k, v_k, default] (2k+1 wires):
636 // first-match on the current draw. Evaluating the guards through
637 // evalBool and the values through evalScalar in the same iteration
638 // keeps shared base RVs coupled (a value and a guard over the same leaf
639 // draw jointly), which is exactly why gate_case beats a mixture-of-
640 // conditioned lowering that would resample and lose the correlation.
641 const auto &wires = gc_.getWires(g);
642 if(wires.empty())
643 throw CircuitException(
644 "gate_case must have at least one child (the default)");
645 const std::size_t k = wires.size() / 2;
646 bool matched = false;
647 for(std::size_t i = 0; i < k; ++i) {
648 if(evalBool(wires[2 * i])) {
649 result = evalScalar(wires[2 * i + 1]);
650 matched = true;
651 break;
652 }
653 }
654 if(!matched)
655 result = evalScalar(wires.back()); // the default value
656 break;
657 }
658 default:
659 throw CircuitException(
660 "Unsupported gate type in scalar evaluation: " +
661 std::string(gate_type_name[type]));
662 }
663
664 scalar_cache_[g] = result;
665 return result;
666}
667
668std::unique_ptr<Distribution> Sampler::buildRvDistribution(
669 gate_t leaf, const DistributionTemplate &tmpl, double &p1, double &p2)
670{
671 const auto &w = gc_.getWires(leaf);
672 auto resolve = [&](const DistributionParam &p) {
673 return p.wire_slot < 0 ? p.literal : evalScalar(w[p.wire_slot]);
674 };
675 p1 = resolve(tmpl.p1);
676 p2 = resolve(tmpl.p2);
677 auto dist = tmpl.family->factory(p1, p2);
678 // Parameter-domain policy: a drawn parameter may fall outside the
679 // family's domain (a sampled scale/rate/shape <= 0). Do NOT silently
680 // drop such a draw -- that implicitly truncates the prior and biases
681 // every downstream moment (same reasoning as the gate_arith POW / LN
682 // guards). integrationRange() returns false exactly when the parameters
683 // are degenerate/out-of-domain, so it is the family-agnostic validity
684 // gate; raise a specific, actionable error.
685 double dlo, dhi;
686 if(!dist->integrationRange(dlo, dhi))
687 throw CircuitException(
688 "gate_rv " + std::string(tmpl.family->name)
689 + ": a parameter drawn outside the family's domain "
690 "(e.g. a scale/rate/shape <= 0: got "
691 + std::to_string(p1) + ", " + std::to_string(p2)
692 + "); put a positive-support prior on it, e.g. "
693 "gamma / lognormal");
694 return dist;
695}
696
697double Sampler::evalWeight(gate_t g)
698{
699 const auto type = gc_.getGateType(g);
700 const auto &wires = gc_.getWires(g);
701 switch(type) {
702 case gate_times: {
703 // Evidence conjunction: the product of the children's weights.
704 // Short-circuit on a zero factor (a rejected Boolean event or a
705 // datum outside a leaf's support) -- the particle is dead.
706 double w = 1.0;
707 for(gate_t c : wires) {
708 w *= evalWeight(c);
709 if(w == 0.0) return 0.0;
710 }
711 return w;
712 }
713 case gate_observe: {
714 // Continuous-density evidence: the observed leaf's pdf at the datum.
715 // Resolving the leaf's (possibly latent) parameters through
716 // evalScalar populates scalar_cache_, so a latent shared with the
717 // queried root couples the weight and the value.
718 if(wires.size() != 1)
719 throw CircuitException(
720 "gate_observe must have exactly one child (the observed leaf)");
721 const gate_t leaf = wires[0];
722 if(gc_.getGateType(leaf) != gate_rv)
723 throw CircuitException(
724 "gate_observe child must be a gate_rv leaf");
725 const double d = parseDoubleStrict(gc_.getExtra(g));
726 auto tmpl = parse_distribution_template(gc_.getExtra(leaf));
727 if(!tmpl)
728 throw CircuitException(
729 "gate_observe: malformed observed gate_rv extra: "
730 + gc_.getExtra(leaf));
731 double p1, p2;
732 return buildRvDistribution(leaf, *tmpl, p1, p2)->pdf(d);
733 }
734 case gate_cmp: {
735 // A point observation "Y = c" on a bare RV leaf is likelihood
736 // evidence (the internal form of observe(Y, c)): weight by the
737 // leaf's density / mass at c -- pdf for a continuous leaf, pmf for a
738 // discrete one (pdf() returns the pmf). Any other cmp (inequality,
739 // non-leaf, non-constant) is an ordinary Boolean event: 0/1 weight.
740 gate_t leaf;
741 double datum;
742 if(matchPointObservationCmp(gc_, g, leaf, datum)) {
743 auto tmpl = parse_distribution_template(gc_.getExtra(leaf));
744 if(tmpl) {
745 double p1, p2;
746 return buildRvDistribution(leaf, *tmpl, p1, p2)->pdf(datum);
747 }
748 }
749 return evalBool(g) ? 1.0 : 0.0;
750 }
751 default:
752 // Any other subtree is a Boolean conditioning event: a 0/1 weight,
753 // which is exactly rejection conditioning -- so a purely Boolean
754 // evidence tree through evalWeight reproduces
755 // monteCarloConditionalScalarSamples.
756 return evalBool(g) ? 1.0 : 0.0;
757 }
758}
759
760} // namespace
761
762double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples)
763{
764 std::mt19937_64 rng = seedRng();
765 Sampler sampler(gc, rng);
766
767 unsigned success = 0;
768 for(unsigned i = 0; i < samples; ++i) {
769 sampler.resetIteration();
770 if(sampler.evalBool(root))
771 ++success;
772
774 throw CircuitException(
775 "Interrupted after " + std::to_string(i + 1) + " samples");
776 }
777 return success * 1.0 / samples;
778}
779
781 double eps, double delta,
782 unsigned long max_samples,
783 unsigned long &samples_used,
784 bool &reached_target)
785{
786 samples_used = 0;
787 reached_target = false;
788 if(max_samples == 0)
789 return 0.;
790
791 // DKLR stopping threshold on the success count -- the S=1 Bernoulli case of
792 // BooleanCircuit::karpLubyStopping: draw whole-circuit worlds until the
793 // success count reaches Y1 and return Y1/N, a relative (eps,delta) estimate
794 // of Pr[root]; N adapts to the true Pr[root] (expected Y1/Pr[root]).
795 const double e = std::exp(1.0);
796 const double Y = 4.0 * (e - 2.0) * std::log(2.0 / delta) / (eps * eps);
797 const double Y1 = 1.0 + (1.0 + eps) * Y;
798
799 std::mt19937_64 rng = seedRng();
800 Sampler sampler(gc, rng);
801
802 unsigned long success = 0;
803 for(unsigned long s = 0; s < max_samples; ++s) {
804 sampler.resetIteration();
805 if(sampler.evalBool(root)) {
806 ++success;
807 if(static_cast<double>(success) >= Y1) {
808 samples_used = s + 1;
809 reached_target = true;
810 return Y1 / static_cast<double>(samples_used);
811 }
812 }
814 throw CircuitException(
815 "Interrupted after " + std::to_string(s + 1) + " samples");
816 }
817
818 // Cap reached before the threshold: the relative target is not met, so return
819 // the plain unbiased mean over the spent budget.
820 samples_used = max_samples;
821 return static_cast<double>(success) / static_cast<double>(max_samples);
822}
823
824std::vector<double> monteCarloJointDistribution(
825 const GenericCircuit &gc,
826 const std::vector<gate_t> &cmps,
827 unsigned samples)
828{
829 const unsigned k = cmps.size();
830 if (k == 0)
831 throw CircuitException(
832 "monteCarloJointDistribution: empty cmps list");
833 if (k > 30)
834 throw CircuitException(
835 "monteCarloJointDistribution: too many cmps in island ("
836 + std::to_string(k) + " > 30)");
837
838 std::mt19937_64 rng = seedRng();
839 Sampler sampler(gc, rng);
840
841 const std::size_t nb_outcomes = std::size_t{1} << k;
842 std::vector<unsigned> counts(nb_outcomes, 0);
843
844 for (unsigned i = 0; i < samples; ++i) {
845 sampler.resetIteration();
846 std::size_t w = 0;
847 for (unsigned j = 0; j < k; ++j) {
848 if (sampler.evalBool(cmps[j])) w |= (std::size_t{1} << j);
849 }
850 ++counts[w];
852 throw CircuitException(
853 "Interrupted after " + std::to_string(i + 1) + " samples");
854 }
855
856 std::vector<double> probs(nb_outcomes);
857 for (std::size_t w = 0; w < nb_outcomes; ++w)
858 probs[w] = counts[w] * 1.0 / samples;
859 return probs;
860}
861
862std::vector<double> monteCarloScalarSamples(
863 const GenericCircuit &gc, gate_t root, unsigned samples)
864{
865 std::mt19937_64 rng = seedRng();
866 Sampler sampler(gc, rng);
867
868 std::vector<double> out;
869 out.reserve(samples);
870 for(unsigned i = 0; i < samples; ++i) {
871 sampler.resetIteration();
872 out.push_back(sampler.evalScalar(root));
873
875 throw CircuitException(
876 "Interrupted after " + std::to_string(i + 1) + " samples");
877 }
878 return out;
879}
880
881/** @brief The Boolean inputs the worlds of @p root (and @p event) range over,
882 * or @c nullopt when another random source, an input without a probability,
883 * or more than @p max_inputs inputs make the enumeration inapplicable. */
884static std::optional<std::vector<gate_t>>
886 std::optional<gate_t> event, unsigned max_inputs)
887{
888 std::vector<gate_t> inputs;
889 std::unordered_set<gate_t> seen;
890 std::stack<gate_t> todo;
891 todo.push(root);
892 if(event) todo.push(*event);
893 while(!todo.empty()) {
894 gate_t g = todo.top();
895 todo.pop();
896 if(!seen.insert(g).second) continue;
897 switch(gc.getGateType(g)) {
898 case gate_input:
899 case gate_update:
900 if(std::isnan(gc.getProb(g))) return std::nullopt;
901 inputs.push_back(g);
902 break;
903 case gate_rv:
904 case gate_mixture:
905 case gate_observe:
906 case gate_mulinput:
907 return std::nullopt;
908 default:
909 break;
910 }
911 if(inputs.size() > max_inputs) return std::nullopt;
912 for(gate_t c : gc.getWires(g)) todo.push(c);
913 }
914 return inputs;
915}
916
917std::optional<double>
919 unsigned max_inputs)
920{
921 auto inputs_opt = enumerableInputs(gc, root, std::nullopt, max_inputs);
922 if(!inputs_opt) return std::nullopt;
923 const std::vector<gate_t> &inputs = *inputs_opt;
924
925 std::mt19937_64 rng; /* unused: every input is fixed */
926 Sampler sampler(gc, rng);
927 std::unordered_map<gate_t, bool> world;
928 sampler.fixInputs(&world);
929 double total = 0.0;
930 const std::uint64_t n_worlds = std::uint64_t(1) << inputs.size();
931 for(std::uint64_t w = 0; w < n_worlds; ++w) {
932 double p = 1.0;
933 for(std::size_t i = 0; i < inputs.size(); ++i) {
934 const bool present = (w >> i) & 1;
935 const double pi = gc.getProb(inputs[i]);
936 world[inputs[i]] = present;
937 p *= present ? pi : 1.0 - pi;
938 }
939 if(p == 0.0) continue;
940 sampler.resetIteration();
941 if(sampler.evalBool(root))
942 total += p;
943
945 throw CircuitException("Interrupted after " + std::to_string(w + 1) +
946 " worlds");
947 }
948 return total;
949}
950
951std::optional<std::vector<std::pair<double, double>>>
953 std::optional<gate_t> event, unsigned max_inputs)
954{
955 auto inputs_opt = enumerableInputs(gc, root, event, max_inputs);
956 if(!inputs_opt) return std::nullopt;
957 const std::vector<gate_t> &inputs = *inputs_opt;
958
959 std::mt19937_64 rng; /* unused: every input is fixed */
960 Sampler sampler(gc, rng);
961 std::unordered_map<gate_t, bool> world;
962 sampler.fixInputs(&world);
963 std::vector<std::pair<double, double>> out;
964 const std::uint64_t n_worlds = std::uint64_t(1) << inputs.size();
965 for(std::uint64_t w = 0; w < n_worlds; ++w) {
966 double p = 1.0;
967 for(std::size_t i = 0; i < inputs.size(); ++i) {
968 const bool present = (w >> i) & 1;
969 const double pi = gc.getProb(inputs[i]);
970 world[inputs[i]] = present;
971 p *= present ? pi : 1.0 - pi;
972 }
973 if(p == 0.0) continue;
974 sampler.resetIteration();
975 if(event && !sampler.evalBool(*event)) continue;
976 out.emplace_back(p, sampler.evalScalar(root));
977
979 throw CircuitException("Interrupted after " + std::to_string(w + 1) +
980 " worlds");
981 }
982 return out;
983}
984
985std::pair<std::vector<double>, std::vector<double>>
987 gate_t root_b, unsigned samples)
988{
989 std::mt19937_64 rng = seedRng();
990 Sampler sampler(gc, rng);
991
992 std::vector<double> out_a, out_b;
993 out_a.reserve(samples);
994 out_b.reserve(samples);
995 for(unsigned i = 0; i < samples; ++i) {
996 sampler.resetIteration();
997 /* Both roots are evaluated within the same iteration, so a gate_rv /
998 * gate_input leaf reachable from both shares its per-iteration draw:
999 * the pair (a_i, b_i) is a draw from the JOINT distribution, which is
1000 * the whole point (mutual information over the marginals alone would
1001 * be identically zero). */
1002 out_a.push_back(sampler.evalScalar(root_a));
1003 out_b.push_back(sampler.evalScalar(root_b));
1004
1006 throw CircuitException(
1007 "Interrupted after " + std::to_string(i + 1) + " samples");
1008 }
1009 return {std::move(out_a), std::move(out_b)};
1010}
1011
1013 const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
1014{
1015 std::mt19937_64 rng = seedRng();
1016 Sampler sampler(gc, rng);
1017
1019 out.attempted = 0;
1020 out.accepted.reserve(samples);
1021
1022 for(unsigned i = 0; i < samples; ++i) {
1023 sampler.resetIteration();
1024 /* Evaluate the indicator FIRST: this populates bool_cache_ AND
1025 * scalar_cache_ for every gate_rv / gate_input that the event
1026 * touches, so the subsequent evalScalar(root) reads the same
1027 * draws. Shared gate_t leaves between root and event_root are
1028 * therefore correctly coupled across the indicator and the
1029 * value. */
1030 if(sampler.evalBool(event_root)) {
1031 out.accepted.push_back(sampler.evalScalar(root));
1032 }
1033 ++out.attempted;
1034
1036 throw CircuitException(
1037 "Interrupted after " + std::to_string(i + 1) + " samples");
1038 }
1039 return out;
1040}
1041
1043 const GenericCircuit &gc, gate_t root_a, gate_t root_b,
1044 gate_t event_root, unsigned samples)
1045{
1046 std::mt19937_64 rng = seedRng();
1047 Sampler sampler(gc, rng);
1048
1050 out.attempted = 0;
1051 out.xs.reserve(samples);
1052 out.ys.reserve(samples);
1053
1054 for(unsigned i = 0; i < samples; ++i) {
1055 sampler.resetIteration();
1056 /* Indicator first (populating the per-iteration caches), then both
1057 * values against the same caches: shared leaves couple across the
1058 * event and the two roots, per monteCarloConditionalScalarSamples. */
1059 if(sampler.evalBool(event_root)) {
1060 out.xs.push_back(sampler.evalScalar(root_a));
1061 out.ys.push_back(sampler.evalScalar(root_b));
1062 }
1063 ++out.attempted;
1064
1066 throw CircuitException(
1067 "Interrupted after " + std::to_string(i + 1) + " samples");
1068 }
1069 return out;
1070}
1071
1072std::optional<std::vector<double>>
1074 gate_t event_root, unsigned n)
1075{
1076 auto m = matchTruncatedSingleRv(gc, root, event_root);
1077 if (!m) return std::nullopt;
1078
1079 /* Per-family rejection-free scheme (Distribution::sampleTruncated);
1080 * a family without one (Erlang: needs the inverse regularised
1081 * incomplete gamma) returns nullopt and the MC-rejection fallback
1082 * handles it. */
1083 std::mt19937_64 rng = seedRng();
1084 return makeDistribution(m->spec)->sampleTruncated(rng, m->lo, m->hi, n);
1085}
1086
1088 const GenericCircuit &gc, gate_t root, gate_t evidence, unsigned samples)
1089{
1090 std::mt19937_64 rng = seedRng();
1091 Sampler sampler(gc, rng);
1092
1094 out.particles.reserve(samples);
1095
1096 for(unsigned i = 0; i < samples; ++i) {
1097 sampler.resetIteration();
1098 /* Evaluate the evidence FIRST: this fills scalar_cache_ for every
1099 * latent the evidence touches, so the subsequent evalScalar(root)
1100 * reads the same latent draw -- coupling the weight and the value. */
1101 const double w = sampler.evalWeight(evidence);
1102 ++out.attempted;
1103 if(w > 0.0) {
1104 const double x = sampler.evalScalar(root);
1105 out.particles.push_back({x, w});
1106 out.weight_sum += w;
1107 out.weight_sq_sum += w * w;
1108 }
1109 /* A zero-weight draw contributes 0 to every weighted sum but still
1110 * counts in `attempted`, so evidence() = weight_sum / attempted is the
1111 * marginal likelihood P(data). */
1112
1114 throw CircuitException(
1115 "Interrupted after " + std::to_string(i + 1) + " samples");
1116 }
1117 return out;
1118}
1119
1120double importanceEvidence(const GenericCircuit &gc, gate_t evidence,
1121 unsigned samples)
1122{
1123 if(samples == 0) return 0.0;
1124 std::mt19937_64 rng = seedRng();
1125 Sampler sampler(gc, rng);
1126
1127 double sw = 0.0;
1128 for(unsigned i = 0; i < samples; ++i) {
1129 sampler.resetIteration();
1130 sw += sampler.evalWeight(evidence);
1132 throw CircuitException(
1133 "Interrupted after " + std::to_string(i + 1) + " samples");
1134 }
1135 return sw / static_cast<double>(samples);
1136}
1137
1138std::vector<double> posteriorResample(const WeightedPosterior &post,
1139 unsigned n)
1140{
1141 std::vector<double> out;
1142 if(post.particles.empty() || post.weight_sum <= 0.0) return out;
1143 /* Cumulative weights for inverse-CDF resampling. */
1144 std::vector<double> cum;
1145 cum.reserve(post.particles.size());
1146 double c = 0.0;
1147 for(const auto &pw : post.particles) { c += pw.second; cum.push_back(c); }
1148
1149 std::mt19937_64 rng = seedRng();
1150 std::uniform_real_distribution<double> u(0.0, c);
1151 out.reserve(n);
1152 for(unsigned i = 0; i < n; ++i) {
1153 const double r = u(rng);
1154 auto it = std::lower_bound(cum.begin(), cum.end(), r);
1155 std::size_t idx = static_cast<std::size_t>(it - cum.begin());
1156 if(idx >= post.particles.size()) idx = post.particles.size() - 1;
1157 out.push_back(post.particles[idx].first);
1158 }
1159 return out;
1160}
1161
1163{
1164 std::unordered_set<gate_t> seen;
1165 std::stack<gate_t> stack;
1166 stack.push(root);
1167 while(!stack.empty()) {
1168 gate_t g = stack.top();
1169 stack.pop();
1170 if(!seen.insert(g).second) continue;
1171 // Either an explicit gate_observe, or a point observation "Y = c" on a
1172 // bare RV leaf (the equality-conditioning form) -- both are density /
1173 // mass likelihood evidence the importance-sampling path must weight
1174 // rather than reject.
1175 gate_t leaf;
1176 double datum;
1177 if(gc.getGateType(g) == gate_observe
1178 || matchPointObservationCmp(gc, g, leaf, datum))
1179 return true;
1180 for(gate_t c : gc.getWires(g)) stack.push(c);
1181 }
1182 return false;
1183}
1184
1186{
1187 std::unordered_set<gate_t> seen;
1188 std::stack<gate_t> stack;
1189 stack.push(root);
1190 while(!stack.empty()) {
1191 gate_t g = stack.top();
1192 stack.pop();
1193 if(!seen.insert(g).second) continue;
1194 auto type = gc.getGateType(g);
1195 // A continuous random variable is signalled by a gate_rv leaf or a
1196 // gate_mixture root. gate_arith is NOT itself an RV marker: it is also
1197 // arithmetic over aggregates (resolved by provsql_having's possible-worlds
1198 // enumeration). A genuine RV arithmetic gate_arith still reaches its
1199 // gate_rv leaves through the child walk below, so it is caught.
1200 if(type == gate_rv || type == gate_mixture)
1201 return true;
1202 for(gate_t c : gc.getWires(g)) stack.push(c);
1203 }
1204 return false;
1205}
1206
1208{
1209 // True iff a gate_agg survives the probability pre-passes AND every surviving
1210 // one is sample-faithful: SUM / AVG / MIN / MAX / COUNT -- all the aggregates
1211 // the sampler's gate_agg arm reproduces exactly. That arm pushes each kept
1212 // contributor's value into the matching Aggregator: the value gate is the
1213 // row's contribution (the summed term for SUM; the 0/1 indicator for COUNT,
1214 // 0 for a NULL row so count(x) does not count NULLs; the compared value for
1215 // AVG / MIN / MAX), so NULL rows are handled and an empty group finalises to
1216 // the value the exact HAVING evaluator uses (0 for SUM / COUNT, NaN ->
1217 // comparison false for AVG / MIN / MAX). An aggregate that bailed the exact
1218 // evaluators (whose threshold-lineage expansion would otherwise not terminate
1219 // for a large-magnitude / large-support aggregate) is then estimated by direct
1220 // world sampling: the apx-safe corner of the HAVING trichotomy (Re & Suciu).
1221 // gate_arith over such aggregates is covered (its gate_agg leaves are reached
1222 // by the walk). The explicit switch rejects any future aggregate operator the
1223 // sampler does not yet handle.
1224 std::unordered_set<gate_t> seen;
1225 std::stack<gate_t> stack;
1226 stack.push(root);
1227 bool any = false;
1228 while(!stack.empty()) {
1229 gate_t g = stack.top();
1230 stack.pop();
1231 if(!seen.insert(g).second) continue;
1232 if(gc.getGateType(g) == gate_agg) {
1233 switch(getAggregationOperator(gc.getInfos(g).first)) {
1239 any = true;
1240 break;
1241 default: // an aggregate the sampler lacks: not routed
1242 return false;
1243 }
1244 }
1245 for(gate_t c : gc.getWires(g)) stack.push(c);
1246 }
1247 return any;
1248}
1249
1251{
1252 // True iff a gate_semimod carries an aggregate-valued M side: the
1253 // contribution of a row whose value is itself an aggregate result (an avg of
1254 // a count, a max of a sum, an aggregate of an arithmetic expression over
1255 // aggregates -- provenance_semimod_nested). Such a value is one per
1256 // possible world, not a constant read off a gate_value, so every closed form
1257 // declines it and the Boolean view of the comparison cannot be built: only
1258 // an evaluator that reads a value per world (this sampler, or the exact
1259 // enumeration of the worlds of the inputs) resolves it.
1260 std::unordered_set<gate_t> seen;
1261 std::stack<gate_t> stack;
1262 stack.push(root);
1263 while(!stack.empty()) {
1264 gate_t g = stack.top();
1265 stack.pop();
1266 if(!seen.insert(g).second) continue;
1267 if(gc.getGateType(g) == gate_semimod) {
1268 const auto &w = gc.getWires(g);
1269 if(w.size() == 2 && gc.getGateType(w[1]) != gate_value)
1270 return true;
1271 }
1272 for(gate_t c : gc.getWires(g)) stack.push(c);
1273 }
1274 return false;
1275}
1276
1277} // namespace provsql
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
AggregationOperator getAggregationOperator(Oid oid)
Map a PostgreSQL aggregate function OID to an AggregationOperator.
std::unique_ptr< Aggregator > makeAggregator(AggregationOperator op, ValueType t)
Create a concrete Aggregator for the given operator and value type.
Typed aggregation value, operator, and aggregator abstractions.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
@ MAX
MAX → input type.
Definition Aggregation.h:55
@ COUNT
COUNT(*) or COUNT(expr) → integer.
Definition Aggregation.h:52
@ SUM
SUM → integer or float.
Definition Aggregation.h:53
@ MIN
MIN → input type.
Definition Aggregation.h:54
@ AVG
AVG → float.
Definition Aggregation.h:56
ComparisonOperator
SQL comparison operators used in gate_cmp circuit gates.
Definition Aggregation.h:39
@ LT
Less than (<).
Definition Aggregation.h:43
@ GT
Greater than (>).
Definition Aggregation.h:45
@ LE
Less than or equal (<=).
Definition Aggregation.h:42
@ NE
Not equal (<>).
Definition Aggregation.h:41
@ GE
Greater than or equal (>=).
Definition Aggregation.h:44
@ INT
Signed 64-bit integer.
Definition Aggregation.h:99
@ NONE
No value (NULL).
@ FLOAT
Double-precision float.
Generic directed-acyclic-graph circuit template and gate identifier.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Per-family polymorphic view over a continuous gate_rv distribution (§F.1 class hierarchy).
Monte Carlo sampling over a GenericCircuit, RV-aware.
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
uuid getUUID(gate_t g) const
Return the UUID string associated with gate g.
Definition Circuit.hpp:46
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).
std::string getExtra(gate_t g) const
Return the string extra for gate g.
double getProb(gate_t g) const
Return the probability for gate g.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
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 importanceEvidence(const GenericCircuit &gc, gate_t evidence, unsigned samples)
Marginal likelihood P(data) of evidence: the mean raw importance weight over samples prior draws.
std::optional< double > enumerateBooleanProbability(const GenericCircuit &gc, gate_t root, unsigned max_inputs)
The probability of root, exactly, over the possible worlds of the circuit as it is: the alternative t...
std::pair< std::vector< double >, std::vector< double > > monteCarloScalarPairSamples(const GenericCircuit &gc, gate_t root_a, gate_t root_b, unsigned samples)
Coupled per-iteration draws of two scalar roots.
std::vector< double > posteriorResample(const WeightedPosterior &post, unsigned n)
Sampling-importance-resampling: draw n posterior samples from a weighted particle set (proportional t...
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
std::vector< double > monteCarloJointDistribution(const GenericCircuit &gc, const std::vector< gate_t > &cmps, unsigned samples)
Estimate the joint distribution of cmps via Monte Carlo.
bool circuitHasNestedAggValue(const GenericCircuit &gc, gate_t root)
Whether a contribution of an aggregate is itself an aggregate result (a gate_semimod whose M side is ...
double monteCarloRVStopping(const GenericCircuit &gc, gate_t root, double eps, double delta, unsigned long max_samples, unsigned long &samples_used, bool &reached_target)
Whole-circuit (eps,delta)-relative probability via the Dagum-Karp-Luby-Ross stopping rule.
std::unique_ptr< Distribution > makeDistribution(const DistributionSpec &spec)
Construct the per-family Distribution for a parsed spec.
std::mt19937_64 seedRng()
The shared Monte Carlo generator, seeded from the provsql.monte_carlo_seed GUC (-1 = non-deterministi...
bool circuitHasUnresolvedSampleableAgg(const GenericCircuit &gc, gate_t root)
Whether a surviving gate_agg exists and every one is sample-faithful (SUM / AVG / MIN / MAX / COUNT –...
static std::optional< std::vector< gate_t > > enumerableInputs(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event, unsigned max_inputs)
The Boolean inputs the worlds of root (and event) range over, or nullopt when another random source,...
ConditionalScalarSamples monteCarloConditionalScalarSamples(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
Rejection-sample root conditioned on event_root.
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< 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...
ConditionalScalarPairSamples monteCarloConditionalScalarPairSamples(const GenericCircuit &gc, gate_t root_a, gate_t root_b, gate_t event_root, unsigned samples)
Rejection-sample the PAIR (root_a, root_b) conditioned on event_root.
double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples)
Run Monte Carlo on a circuit that may contain gate_rv leaves.
std::optional< std::vector< double > > try_truncated_closed_form_sample(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned n)
Try to draw n exact samples from the conditional distribution of root given event_root via closed-for...
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.
bool circuitHasRV(const GenericCircuit &gc, gate_t root)
Walk the circuit reachable from root looking for any gate_rv.
bool circuitHasObserve(const GenericCircuit &gc, gate_t root)
Whether the circuit reachable from root contains a gate_observe – the signal that a conditioning even...
int provsql_monte_carlo_seed
Seed for the Monte Carlo sampler; -1 means non-deterministic (std::random_device); controlled by the ...
Definition provsql.c:123
bool provsql_interrupted
Global variable that becomes true if this particular backend received an interrupt signal.
Definition provsql.c:109
const char * gate_type_name[]
Names of gate types.
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_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...
#define PROVSQL_GATE_NULL_UUID
UUID of the constant value gate standing for the NULL value, the result of gate_null().
#define PROVSQL_AGG_SCALAR_FLAG
Scalar-aggregation flag, stored in the upper bit of a gate_agg's info2 (whose low 31 bits hold the ag...
ValueType getType() const
Return the runtime type tag of this value.
std::variant< long, double, bool, std::string, std::vector< long >, std::vector< double >, std::vector< bool >, std::vector< std::string > > v
The variant holding the actual value.
Outcome of a conditional coupled-pair Monte Carlo pass: xs[i] / ys[i] are the two roots' values from ...
Outcome of a conditional Monte Carlo sampling pass.
Outcome of a likelihood-weighting (importance-sampling) pass.
double weight_sq_sum
Sum of w^2 over all attempted draws.
unsigned attempted
Number of prior draws.
std::vector< std::pair< double, double > > particles
(x, w) with w > 0.
double weight_sum
Sum of w over all attempted draws.