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
103private:
104 /// Build the per-draw Distribution for a (possibly latent) gate_rv leaf,
105 /// resolving wired parameters through evalScalar (so a shared latent
106 /// lands in scalar_cache_ and couples the callers), with the
107 /// parameter-domain guard. Outputs the resolved parameters in p1/p2.
108 std::unique_ptr<Distribution> buildRvDistribution(
109 gate_t leaf, const DistributionTemplate &tmpl, double &p1, double &p2);
110
111 const GenericCircuit &gc_;
112 std::mt19937_64 &rng_;
113 std::unordered_map<gate_t, bool> bool_cache_;
114 std::unordered_map<gate_t, double> scalar_cache_;
115 // Per-gate_rv Distribution, constructed once and reused across iterations
116 // (NOT cleared in resetIteration): sampling then never re-parses the spec
117 // or re-constructs the Distribution per draw.
118 std::unordered_map<gate_t, std::unique_ptr<Distribution>> dist_cache_;
119};
120
121bool Sampler::evalBool(gate_t g)
122{
123 auto it = bool_cache_.find(g);
124 if(it != bool_cache_.end()) return it->second;
125
126 bool result = false;
127 const auto type = gc_.getGateType(g);
128 const auto &wires = gc_.getWires(g);
129
130 switch(type) {
131 case gate_input:
132 case gate_update:
133 {
134 std::uniform_real_distribution<double> u(0.0, 1.0);
135 result = u(rng_) < gc_.getProb(g);
136 break;
137 }
138 case gate_plus:
139 result = false;
140 for(gate_t c : wires) {
141 if(evalBool(c)) { result = true; break; }
142 }
143 break;
144 case gate_times:
145 result = true;
146 for(gate_t c : wires) {
147 if(!evalBool(c)) { result = false; break; }
148 }
149 break;
150 case gate_monus:
151 if(wires.size() != 2)
152 throw CircuitException("gate_monus must have exactly two children");
153 result = evalBool(wires[0]) && !evalBool(wires[1]);
154 break;
155 case gate_zero:
156 result = false;
157 break;
158 case gate_one:
159 result = true;
160 break;
161 case gate_cmp:
162 {
163 if(wires.size() != 2)
164 throw CircuitException("gate_cmp must have exactly two children");
165 bool ok;
166 ComparisonOperator op = cmpOpFromOid(gc_.getInfos(g).first, ok);
167 if(!ok)
168 throw CircuitException(
169 "gate_cmp: unsupported operator OID " +
170 std::to_string(gc_.getInfos(g).first));
171 double l = evalScalar(wires[0]);
172 double r = evalScalar(wires[1]);
173 result = applyCmp(l, op, r);
174 break;
175 }
176 case gate_mulinput:
177 throw CircuitException(
178 "Monte Carlo over circuits containing gate_mulinput "
179 "is not yet supported on the RV path");
180 case gate_delta:
181 // δ-semiring operator: identity on the Boolean semiring, so the
182 // sampled truth value is just the wrapped child's. Showed up
183 // when conditioning on a row's provenance() in an aggregate
184 // query (HAVING / GROUP BY paths can splice δ over the
185 // semimod's k-side).
186 if(wires.size() != 1)
187 throw CircuitException("gate_delta must have exactly one child");
188 result = evalBool(wires[0]);
189 break;
190 case gate_assumed:
191 // Structural Boolean-rewrite marker: identity on the Boolean
192 // semiring, so the sampled truth value is the wrapped child's.
193 // The marker exists to refuse non-Boolean-compat evaluation; MC
194 // sampling for probability is always Boolean-compat.
195 if(wires.size() != 1)
196 throw CircuitException(
197 "gate_assumed must have exactly one child");
198 result = evalBool(wires[0]);
199 break;
200 case gate_annotation:
201 // Transparent annotation wrapper (inversion-free certificate / order
202 // key): identity, so the sampled truth value is the wrapped child's.
203 if(wires.size() != 1)
204 throw CircuitException("gate_annotation must have exactly one child");
205 result = evalBool(wires[0]);
206 break;
207 default:
208 throw CircuitException(
209 "Unsupported gate type in Boolean evaluation: " +
210 std::string(gate_type_name[type]));
211 }
212
213 bool_cache_[g] = result;
214 return result;
215}
216
217double Sampler::evalScalar(gate_t g)
218{
219 auto it = scalar_cache_.find(g);
220 if(it != scalar_cache_.end()) return it->second;
221
222 double result = 0.0;
223 const auto type = gc_.getGateType(g);
224 const auto &wires = gc_.getWires(g);
225
226 switch(type) {
227 case gate_value:
228 result = parseDoubleStrict(gc_.getExtra(g));
229 break;
230 case gate_rv:
231 {
232 // Fast path: an all-literal leaf builds its Distribution once and
233 // reuses it across iterations (dist_cache_ is never reset), so
234 // sampling never re-parses the spec. A latent (parametric) leaf,
235 // whose parameters are wire references resolved per iteration,
236 // bypasses that cache: its distribution changes every draw.
237 auto dit = dist_cache_.find(g);
238 if(dit != dist_cache_.end()) {
239 result = dit->second->sample(rng_);
240 break;
241 }
242 auto tmpl = parse_distribution_template(gc_.getExtra(g));
243 if(!tmpl)
244 throw CircuitException(
245 "Malformed gate_rv extra: " + gc_.getExtra(g));
246 if(!tmpl->parametric()) {
247 DistributionSpec spec{tmpl->family, tmpl->p1.literal, tmpl->p2.literal};
248 dit = dist_cache_.emplace(g, makeDistribution(spec)).first;
249 result = dit->second->sample(rng_);
250 break;
251 }
252 // Parametric leaf: resolve each wired parameter from the gate's
253 // wires via evalScalar (so a latent shared across leaves lands in
254 // scalar_cache_ and couples them), then build the family instance
255 // for this draw's parameters. The (double,double) factory and the
256 // Distribution interface are untouched -- only the parameters'
257 // source changes.
258 double p1, p2;
259 result = buildRvDistribution(g, *tmpl, p1, p2)->sample(rng_);
260 break;
261 }
262 case gate_arith:
263 {
264 if(wires.empty())
265 throw CircuitException("gate_arith must have at least one child");
266 auto op = static_cast<provsql_arith_op>(gc_.getInfos(g).first);
267 switch(op) {
269 result = 0.0;
270 for(gate_t c : wires) result += evalScalar(c);
271 break;
273 result = 1.0;
274 for(gate_t c : wires) result *= evalScalar(c);
275 break;
277 if(wires.size() != 2)
278 throw CircuitException("gate_arith MINUS must be binary");
279 result = evalScalar(wires[0]) - evalScalar(wires[1]);
280 break;
282 if(wires.size() != 2)
283 throw CircuitException("gate_arith DIV must be binary");
284 result = evalScalar(wires[0]) / evalScalar(wires[1]);
285 break;
287 if(wires.size() != 1)
288 throw CircuitException("gate_arith NEG must be unary");
289 result = -evalScalar(wires[0]);
290 break;
292 // n-ary order statistic: max over the sampled children. Shared
293 // base RVs stay coupled through scalar_cache_, so max(x, y) with x,y
294 // over the same leaf draws them jointly (correct correlation).
295 result = evalScalar(wires[0]);
296 for(std::size_t i = 1; i < wires.size(); ++i)
297 result = std::max(result, evalScalar(wires[i]));
298 break;
300 result = evalScalar(wires[0]);
301 for(std::size_t i = 1; i < wires.size(); ++i)
302 result = std::min(result, evalScalar(wires[i]));
303 break;
305 {
306 if(wires.size() != 2)
307 throw CircuitException("gate_arith POW must be binary");
308 const double base = evalScalar(wires[0]);
309 const double expo = evalScalar(wires[1]);
310 result = std::pow(base, expo);
311 // std::pow is real-valued except for a negative base with a
312 // non-integer exponent. A NaN there is a domain violation,
313 // not an undefined world: raise with the fix rather than let
314 // the moment estimators silently drop the draw as a missing
315 // observation (which would report a biased, implicitly
316 // conditioned answer). NaN operands (undefined worlds, e.g.
317 // empty-group aggregates) still propagate as NaN.
318 if(std::isnan(result) && !std::isnan(base) && !std::isnan(expo))
319 throw CircuitException(
320 "pow: negative base drawn with a non-integer exponent ("
321 + std::to_string(base) + " ^ " + std::to_string(expo)
322 + "); restrict the base to be non-negative, e.g. "
323 "pow(greatest(x, 0), p)");
324 break;
325 }
326 case PROVSQL_ARITH_LN:
327 {
328 if(wires.size() != 1)
329 throw CircuitException("gate_arith LN must be unary");
330 const double x = evalScalar(wires[0]);
331 // Same rationale as POW: a negative draw is a domain
332 // violation, raised rather than silently conditioned away.
333 // x = 0 legitimately yields -Infinity (a boundary value of
334 // probability zero for continuous arguments); NaN operands
335 // propagate as undefined worlds.
336 if(x < 0.0)
337 throw CircuitException(
338 "ln: negative draw (" + std::to_string(x)
339 + "); ln is only defined on [0, +Infinity) -- "
340 "restrict the argument's support");
341 result = std::log(x);
342 break;
343 }
345 if(wires.size() != 1)
346 throw CircuitException("gate_arith EXP must be unary");
347 result = std::exp(evalScalar(wires[0]));
348 break;
350 {
351 // Continuous percentile (SQL percentile_cont) over the group's
352 // rows: wires are interleaved [ind_1, x_1, ..., ind_n, x_n],
353 // the fraction is text-encoded in extra. Per draw, the values
354 // whose 0/1 presence indicator draws 1 are sorted and linearly
355 // interpolated at the fraction; a draw with no present row is
356 // NaN (undefined world, skipped by the moment estimators like
357 // an empty-group avg).
358 if(wires.size() < 2 || wires.size() % 2 != 0)
359 throw CircuitException(
360 "gate_arith PERCENTILE must have interleaved "
361 "indicator/value wires");
362 double fraction;
363 try {
364 fraction = std::stod(gc_.getExtra(g));
365 } catch(const std::exception &) {
366 throw CircuitException(
367 "Malformed gate_arith PERCENTILE extra (expected the "
368 "fraction): " + gc_.getExtra(g));
369 }
370 std::vector<double> members;
371 bool has_nan = false;
372 for(std::size_t i = 0; i < wires.size(); i += 2) {
373 if(evalScalar(wires[i]) >= 0.5) {
374 const double x = evalScalar(wires[i + 1]);
375 if(std::isnan(x))
376 has_nan = true;
377 else
378 members.push_back(x);
379 }
380 }
381 if(has_nan || members.empty()) {
382 result = std::numeric_limits<double>::quiet_NaN();
383 break;
384 }
385 std::sort(members.begin(), members.end());
386 const double pos = fraction * (members.size() - 1);
387 const std::size_t lo = static_cast<std::size_t>(pos);
388 const double frac = pos - static_cast<double>(lo);
389 result = (lo + 1 < members.size())
390 ? members[lo] + frac * (members[lo + 1] - members[lo])
391 : members[lo];
392 break;
393 }
394 default:
395 throw CircuitException(
396 "Unknown gate_arith operator tag: " +
397 std::to_string(static_cast<unsigned>(op)));
398 }
399 break;
400 }
401 case gate_agg:
402 {
403 // HAVING-style aggregate evaluated per MC iteration: walk the
404 // gate_semimod children, keep the rows whose k_gate fires in
405 // this world, push their value into a reusable Aggregator,
406 // return the finalised scalar. Closes the priority-4-era gap
407 // that made `WHERE rv > 0 GROUP BY x HAVING count(*) > 1`
408 // structural-only (see continuous_selection.sql section G).
409 //
410 // Type plan: we evaluate every numeric path in float8 to stay
411 // inside evalScalar's return type. COUNT is normalised by
412 // makeAggregator to SumAgg<long>, so each kept row contributes its
413 // value gate cast to long: that gate is 1 for an ordinary row and 0
414 // for a NULL one (count(x) does not count NULLs), so the sum of the
415 // kept values is exactly count(*) / count(x) -- faithful with no
416 // nullability check. SUM / AVG / MIN / MAX consume the value via
417 // evalScalar directly. Empty groups finalise to NONE; what that
418 // means depends on the aggregate and on whether the aggregation is
419 // scalar or grouped -- see the NONE arm below.
421 getAggregationOperator(gc_.getInfos(g).first);
422 std::unique_ptr<Aggregator> agg =
426 if(!agg)
427 throw CircuitException(
428 "gate_agg: makeAggregator returned null for op " +
429 std::to_string(static_cast<int>(op)));
430 for(gate_t child : wires) {
431 if(gc_.getGateType(child) != gate_semimod) continue;
432 const auto &sm = gc_.getWires(child);
433 if(sm.size() != 2) continue;
434 if(!evalBool(sm[0])) continue;
436 agg->add(AggValue(static_cast<long>(evalScalar(sm[1]))));
437 } else {
438 agg->add(AggValue(evalScalar(sm[1])));
439 }
440 }
441 AggValue r = agg->finalize();
442 switch(r.getType()) {
443 case ValueType::INT:
444 result = static_cast<double>(std::get<long>(r.v));
445 break;
446 case ValueType::FLOAT:
447 result = std::get<double>(r.v);
448 break;
449 case ValueType::NONE:
450 // No contributor survived this iteration -- either no row of the
451 // group is present in this world, or every contributed value was
452 // NULL. SUM / AVG / MIN / MAX are then SQL NULL, so they surface
453 // NaN, which compares false under IEEE on any enclosing gate_cmp
454 // (the truth value of a comparison against NULL) and is skipped as
455 // a missing observation by the moment averagers in
456 // Expectation::mc_raw_moment / mc_central_moment, making those
457 // estimators conditional on the worlds where the aggregate is
458 // defined.
459 //
460 // COUNT has no NULL to report -- an empty set genuinely counts 0 --
461 // but 0 is the right answer only where a row exists to carry it.
462 // A scalar aggregation always yields its single row, empty input
463 // included, so 0 it is. For a grouped aggregation an empty group
464 // is no row at all: the possible-world semantics excludes that
465 // world (having_semantics.hpp enumerates from world 1, and
466 // RangeCheck rewrites a trivially-true count comparison to the
467 // group-existence gate rather than to gate_one). Reporting 0
468 // there would let a true-on-zero predicate such as
469 // `count(*) <= k` hold in a world that contributes no row,
470 // inflating the estimate by the probability that the group is
471 // empty; NaN keeps the enclosing comparison false, which is how
472 // this sampler declines a world.
473 result =
475 (gc_.getInfos(g).second & PROVSQL_AGG_SCALAR_FLAG) != 0)
476 ? 0.0
477 : std::numeric_limits<double>::quiet_NaN();
478 break;
479 default:
480 throw CircuitException(
481 "gate_agg: unsupported aggregate result ValueType in MC");
482 }
483 break;
484 }
485 case gate_semimod:
486 {
487 // Bare semimod root (the user pinned one of an agg's per-row
488 // contributions): interpret as a Bernoulli-weighted scalar
489 // value · 1_{k fires}. When the Boolean k child does not fire
490 // in this world, the row contributes nothing -- return 0.0
491 // (the additive identity), which matches the per-iteration
492 // role semimod plays inside gate_agg above. This makes
493 // semimod a legal scalar root for rv_sample / rv_moment /
494 // rv_histogram alongside agg.
495 const auto &wires = gc_.getWires(g);
496 if(wires.size() != 2)
497 throw CircuitException(
498 "gate_semimod must have exactly two children "
499 "[k_gate, value_gate]");
500 result = evalBool(wires[0]) ? evalScalar(wires[1]) : 0.0;
501 break;
502 }
503 case gate_mixture:
504 {
505 // Two shapes of gate_mixture share this case:
506 //
507 // - Classic 3-wire: [p_token, x_token, y_token]. Draw the
508 // Bernoulli via evalBool, which handles gate_input by
509 // sampling uniform(0,1) < get_prob and memoises on
510 // bool_cache_; two mixtures sharing the same p_token
511 // therefore see the same draw, and any unrelated Boolean
512 // parent of p_token stays in sync.
513 //
514 // - Categorical N-wire: [key, mul_1, ..., mul_n]. Built
515 // directly by the @c provsql.categorical SQL constructor;
516 // each mul_i carries its probability in set_prob and its
517 // outcome value in extra.
518 // We draw a single uniform[0,1) per block, walk the
519 // cumulative probabilities to pick a mulinput, and stash the
520 // Boolean truth values into bool_cache_ so any downstream
521 // Boolean consumer of the mulinputs (independentEvaluation,
522 // OR/AND parents) sees a consistent sampled outcome.
523 if(gc_.isCategoricalMixture(g)) {
524 std::uniform_real_distribution<double> u(0.0, 1.0);
525 const double r = u(rng_);
526 double cum = 0.0;
527 // Default to the last mulinput in case floating-point cumulative
528 // sums leave us shy of 1.0 by a few ULPs.
529 std::size_t chosen = wires.size() - 1;
530 for(std::size_t i = 1; i < wires.size(); ++i) {
531 cum += gc_.getProb(wires[i]);
532 if(r < cum) { chosen = i; break; }
533 }
534 for(std::size_t i = 1; i < wires.size(); ++i) {
535 bool_cache_[wires[i]] = (i == chosen);
536 }
537 result = parseDoubleStrict(gc_.getExtra(wires[chosen]));
538 break;
539 }
540 if(wires.size() != 3)
541 throw CircuitException(
542 "gate_mixture must have exactly three children "
543 "[p_token, x_token, y_token]");
544 result = evalBool(wires[0]) ? evalScalar(wires[1])
545 : evalScalar(wires[2]);
546 break;
547 }
548 case gate_case:
549 {
550 // Guarded selection [g_1, v_1, ..., g_k, v_k, default] (2k+1 wires):
551 // first-match on the current draw. Evaluating the guards through
552 // evalBool and the values through evalScalar in the same iteration
553 // keeps shared base RVs coupled (a value and a guard over the same leaf
554 // draw jointly), which is exactly why gate_case beats a mixture-of-
555 // conditioned lowering that would resample and lose the correlation.
556 const auto &wires = gc_.getWires(g);
557 if(wires.empty())
558 throw CircuitException(
559 "gate_case must have at least one child (the default)");
560 const std::size_t k = wires.size() / 2;
561 bool matched = false;
562 for(std::size_t i = 0; i < k; ++i) {
563 if(evalBool(wires[2 * i])) {
564 result = evalScalar(wires[2 * i + 1]);
565 matched = true;
566 break;
567 }
568 }
569 if(!matched)
570 result = evalScalar(wires.back()); // the default value
571 break;
572 }
573 default:
574 throw CircuitException(
575 "Unsupported gate type in scalar evaluation: " +
576 std::string(gate_type_name[type]));
577 }
578
579 scalar_cache_[g] = result;
580 return result;
581}
582
583std::unique_ptr<Distribution> Sampler::buildRvDistribution(
584 gate_t leaf, const DistributionTemplate &tmpl, double &p1, double &p2)
585{
586 const auto &w = gc_.getWires(leaf);
587 auto resolve = [&](const DistributionParam &p) {
588 return p.wire_slot < 0 ? p.literal : evalScalar(w[p.wire_slot]);
589 };
590 p1 = resolve(tmpl.p1);
591 p2 = resolve(tmpl.p2);
592 auto dist = tmpl.family->factory(p1, p2);
593 // Parameter-domain policy: a drawn parameter may fall outside the
594 // family's domain (a sampled scale/rate/shape <= 0). Do NOT silently
595 // drop such a draw -- that implicitly truncates the prior and biases
596 // every downstream moment (same reasoning as the gate_arith POW / LN
597 // guards). integrationRange() returns false exactly when the parameters
598 // are degenerate/out-of-domain, so it is the family-agnostic validity
599 // gate; raise a specific, actionable error.
600 double dlo, dhi;
601 if(!dist->integrationRange(dlo, dhi))
602 throw CircuitException(
603 "gate_rv " + std::string(tmpl.family->name)
604 + ": a parameter drawn outside the family's domain "
605 "(e.g. a scale/rate/shape <= 0: got "
606 + std::to_string(p1) + ", " + std::to_string(p2)
607 + "); put a positive-support prior on it, e.g. "
608 "gamma / lognormal");
609 return dist;
610}
611
612double Sampler::evalWeight(gate_t g)
613{
614 const auto type = gc_.getGateType(g);
615 const auto &wires = gc_.getWires(g);
616 switch(type) {
617 case gate_times: {
618 // Evidence conjunction: the product of the children's weights.
619 // Short-circuit on a zero factor (a rejected Boolean event or a
620 // datum outside a leaf's support) -- the particle is dead.
621 double w = 1.0;
622 for(gate_t c : wires) {
623 w *= evalWeight(c);
624 if(w == 0.0) return 0.0;
625 }
626 return w;
627 }
628 case gate_observe: {
629 // Continuous-density evidence: the observed leaf's pdf at the datum.
630 // Resolving the leaf's (possibly latent) parameters through
631 // evalScalar populates scalar_cache_, so a latent shared with the
632 // queried root couples the weight and the value.
633 if(wires.size() != 1)
634 throw CircuitException(
635 "gate_observe must have exactly one child (the observed leaf)");
636 const gate_t leaf = wires[0];
637 if(gc_.getGateType(leaf) != gate_rv)
638 throw CircuitException(
639 "gate_observe child must be a gate_rv leaf");
640 const double d = parseDoubleStrict(gc_.getExtra(g));
641 auto tmpl = parse_distribution_template(gc_.getExtra(leaf));
642 if(!tmpl)
643 throw CircuitException(
644 "gate_observe: malformed observed gate_rv extra: "
645 + gc_.getExtra(leaf));
646 double p1, p2;
647 return buildRvDistribution(leaf, *tmpl, p1, p2)->pdf(d);
648 }
649 case gate_cmp: {
650 // A point observation "Y = c" on a bare RV leaf is likelihood
651 // evidence (the internal form of observe(Y, c)): weight by the
652 // leaf's density / mass at c -- pdf for a continuous leaf, pmf for a
653 // discrete one (pdf() returns the pmf). Any other cmp (inequality,
654 // non-leaf, non-constant) is an ordinary Boolean event: 0/1 weight.
655 gate_t leaf;
656 double datum;
657 if(matchPointObservationCmp(gc_, g, leaf, datum)) {
658 auto tmpl = parse_distribution_template(gc_.getExtra(leaf));
659 if(tmpl) {
660 double p1, p2;
661 return buildRvDistribution(leaf, *tmpl, p1, p2)->pdf(datum);
662 }
663 }
664 return evalBool(g) ? 1.0 : 0.0;
665 }
666 default:
667 // Any other subtree is a Boolean conditioning event: a 0/1 weight,
668 // which is exactly rejection conditioning -- so a purely Boolean
669 // evidence tree through evalWeight reproduces
670 // monteCarloConditionalScalarSamples.
671 return evalBool(g) ? 1.0 : 0.0;
672 }
673}
674
675} // namespace
676
677double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples)
678{
679 std::mt19937_64 rng = seedRng();
680 Sampler sampler(gc, rng);
681
682 unsigned success = 0;
683 for(unsigned i = 0; i < samples; ++i) {
684 sampler.resetIteration();
685 if(sampler.evalBool(root))
686 ++success;
687
689 throw CircuitException(
690 "Interrupted after " + std::to_string(i + 1) + " samples");
691 }
692 return success * 1.0 / samples;
693}
694
696 double eps, double delta,
697 unsigned long max_samples,
698 unsigned long &samples_used,
699 bool &reached_target)
700{
701 samples_used = 0;
702 reached_target = false;
703 if(max_samples == 0)
704 return 0.;
705
706 // DKLR stopping threshold on the success count -- the S=1 Bernoulli case of
707 // BooleanCircuit::karpLubyStopping: draw whole-circuit worlds until the
708 // success count reaches Y1 and return Y1/N, a relative (eps,delta) estimate
709 // of Pr[root]; N adapts to the true Pr[root] (expected Y1/Pr[root]).
710 const double e = std::exp(1.0);
711 const double Y = 4.0 * (e - 2.0) * std::log(2.0 / delta) / (eps * eps);
712 const double Y1 = 1.0 + (1.0 + eps) * Y;
713
714 std::mt19937_64 rng = seedRng();
715 Sampler sampler(gc, rng);
716
717 unsigned long success = 0;
718 for(unsigned long s = 0; s < max_samples; ++s) {
719 sampler.resetIteration();
720 if(sampler.evalBool(root)) {
721 ++success;
722 if(static_cast<double>(success) >= Y1) {
723 samples_used = s + 1;
724 reached_target = true;
725 return Y1 / static_cast<double>(samples_used);
726 }
727 }
729 throw CircuitException(
730 "Interrupted after " + std::to_string(s + 1) + " samples");
731 }
732
733 // Cap reached before the threshold: the relative target is not met, so return
734 // the plain unbiased mean over the spent budget.
735 samples_used = max_samples;
736 return static_cast<double>(success) / static_cast<double>(max_samples);
737}
738
739std::vector<double> monteCarloJointDistribution(
740 const GenericCircuit &gc,
741 const std::vector<gate_t> &cmps,
742 unsigned samples)
743{
744 const unsigned k = cmps.size();
745 if (k == 0)
746 throw CircuitException(
747 "monteCarloJointDistribution: empty cmps list");
748 if (k > 30)
749 throw CircuitException(
750 "monteCarloJointDistribution: too many cmps in island ("
751 + std::to_string(k) + " > 30)");
752
753 std::mt19937_64 rng = seedRng();
754 Sampler sampler(gc, rng);
755
756 const std::size_t nb_outcomes = std::size_t{1} << k;
757 std::vector<unsigned> counts(nb_outcomes, 0);
758
759 for (unsigned i = 0; i < samples; ++i) {
760 sampler.resetIteration();
761 std::size_t w = 0;
762 for (unsigned j = 0; j < k; ++j) {
763 if (sampler.evalBool(cmps[j])) w |= (std::size_t{1} << j);
764 }
765 ++counts[w];
767 throw CircuitException(
768 "Interrupted after " + std::to_string(i + 1) + " samples");
769 }
770
771 std::vector<double> probs(nb_outcomes);
772 for (std::size_t w = 0; w < nb_outcomes; ++w)
773 probs[w] = counts[w] * 1.0 / samples;
774 return probs;
775}
776
777std::vector<double> monteCarloScalarSamples(
778 const GenericCircuit &gc, gate_t root, unsigned samples)
779{
780 std::mt19937_64 rng = seedRng();
781 Sampler sampler(gc, rng);
782
783 std::vector<double> out;
784 out.reserve(samples);
785 for(unsigned i = 0; i < samples; ++i) {
786 sampler.resetIteration();
787 out.push_back(sampler.evalScalar(root));
788
790 throw CircuitException(
791 "Interrupted after " + std::to_string(i + 1) + " samples");
792 }
793 return out;
794}
795
796std::pair<std::vector<double>, std::vector<double>>
798 gate_t root_b, unsigned samples)
799{
800 std::mt19937_64 rng = seedRng();
801 Sampler sampler(gc, rng);
802
803 std::vector<double> out_a, out_b;
804 out_a.reserve(samples);
805 out_b.reserve(samples);
806 for(unsigned i = 0; i < samples; ++i) {
807 sampler.resetIteration();
808 /* Both roots are evaluated within the same iteration, so a gate_rv /
809 * gate_input leaf reachable from both shares its per-iteration draw:
810 * the pair (a_i, b_i) is a draw from the JOINT distribution, which is
811 * the whole point (mutual information over the marginals alone would
812 * be identically zero). */
813 out_a.push_back(sampler.evalScalar(root_a));
814 out_b.push_back(sampler.evalScalar(root_b));
815
817 throw CircuitException(
818 "Interrupted after " + std::to_string(i + 1) + " samples");
819 }
820 return {std::move(out_a), std::move(out_b)};
821}
822
824 const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
825{
826 std::mt19937_64 rng = seedRng();
827 Sampler sampler(gc, rng);
828
830 out.attempted = 0;
831 out.accepted.reserve(samples);
832
833 for(unsigned i = 0; i < samples; ++i) {
834 sampler.resetIteration();
835 /* Evaluate the indicator FIRST: this populates bool_cache_ AND
836 * scalar_cache_ for every gate_rv / gate_input that the event
837 * touches, so the subsequent evalScalar(root) reads the same
838 * draws. Shared gate_t leaves between root and event_root are
839 * therefore correctly coupled across the indicator and the
840 * value. */
841 if(sampler.evalBool(event_root)) {
842 out.accepted.push_back(sampler.evalScalar(root));
843 }
844 ++out.attempted;
845
847 throw CircuitException(
848 "Interrupted after " + std::to_string(i + 1) + " samples");
849 }
850 return out;
851}
852
854 const GenericCircuit &gc, gate_t root_a, gate_t root_b,
855 gate_t event_root, unsigned samples)
856{
857 std::mt19937_64 rng = seedRng();
858 Sampler sampler(gc, rng);
859
861 out.attempted = 0;
862 out.xs.reserve(samples);
863 out.ys.reserve(samples);
864
865 for(unsigned i = 0; i < samples; ++i) {
866 sampler.resetIteration();
867 /* Indicator first (populating the per-iteration caches), then both
868 * values against the same caches: shared leaves couple across the
869 * event and the two roots, per monteCarloConditionalScalarSamples. */
870 if(sampler.evalBool(event_root)) {
871 out.xs.push_back(sampler.evalScalar(root_a));
872 out.ys.push_back(sampler.evalScalar(root_b));
873 }
874 ++out.attempted;
875
877 throw CircuitException(
878 "Interrupted after " + std::to_string(i + 1) + " samples");
879 }
880 return out;
881}
882
883std::optional<std::vector<double>>
885 gate_t event_root, unsigned n)
886{
887 auto m = matchTruncatedSingleRv(gc, root, event_root);
888 if (!m) return std::nullopt;
889
890 /* Per-family rejection-free scheme (Distribution::sampleTruncated);
891 * a family without one (Erlang: needs the inverse regularised
892 * incomplete gamma) returns nullopt and the MC-rejection fallback
893 * handles it. */
894 std::mt19937_64 rng = seedRng();
895 return makeDistribution(m->spec)->sampleTruncated(rng, m->lo, m->hi, n);
896}
897
899 const GenericCircuit &gc, gate_t root, gate_t evidence, unsigned samples)
900{
901 std::mt19937_64 rng = seedRng();
902 Sampler sampler(gc, rng);
903
905 out.particles.reserve(samples);
906
907 for(unsigned i = 0; i < samples; ++i) {
908 sampler.resetIteration();
909 /* Evaluate the evidence FIRST: this fills scalar_cache_ for every
910 * latent the evidence touches, so the subsequent evalScalar(root)
911 * reads the same latent draw -- coupling the weight and the value. */
912 const double w = sampler.evalWeight(evidence);
913 ++out.attempted;
914 if(w > 0.0) {
915 const double x = sampler.evalScalar(root);
916 out.particles.push_back({x, w});
917 out.weight_sum += w;
918 out.weight_sq_sum += w * w;
919 }
920 /* A zero-weight draw contributes 0 to every weighted sum but still
921 * counts in `attempted`, so evidence() = weight_sum / attempted is the
922 * marginal likelihood P(data). */
923
925 throw CircuitException(
926 "Interrupted after " + std::to_string(i + 1) + " samples");
927 }
928 return out;
929}
930
931double importanceEvidence(const GenericCircuit &gc, gate_t evidence,
932 unsigned samples)
933{
934 if(samples == 0) return 0.0;
935 std::mt19937_64 rng = seedRng();
936 Sampler sampler(gc, rng);
937
938 double sw = 0.0;
939 for(unsigned i = 0; i < samples; ++i) {
940 sampler.resetIteration();
941 sw += sampler.evalWeight(evidence);
943 throw CircuitException(
944 "Interrupted after " + std::to_string(i + 1) + " samples");
945 }
946 return sw / static_cast<double>(samples);
947}
948
949std::vector<double> posteriorResample(const WeightedPosterior &post,
950 unsigned n)
951{
952 std::vector<double> out;
953 if(post.particles.empty() || post.weight_sum <= 0.0) return out;
954 /* Cumulative weights for inverse-CDF resampling. */
955 std::vector<double> cum;
956 cum.reserve(post.particles.size());
957 double c = 0.0;
958 for(const auto &pw : post.particles) { c += pw.second; cum.push_back(c); }
959
960 std::mt19937_64 rng = seedRng();
961 std::uniform_real_distribution<double> u(0.0, c);
962 out.reserve(n);
963 for(unsigned i = 0; i < n; ++i) {
964 const double r = u(rng);
965 auto it = std::lower_bound(cum.begin(), cum.end(), r);
966 std::size_t idx = static_cast<std::size_t>(it - cum.begin());
967 if(idx >= post.particles.size()) idx = post.particles.size() - 1;
968 out.push_back(post.particles[idx].first);
969 }
970 return out;
971}
972
974{
975 std::unordered_set<gate_t> seen;
976 std::stack<gate_t> stack;
977 stack.push(root);
978 while(!stack.empty()) {
979 gate_t g = stack.top();
980 stack.pop();
981 if(!seen.insert(g).second) continue;
982 // Either an explicit gate_observe, or a point observation "Y = c" on a
983 // bare RV leaf (the equality-conditioning form) -- both are density /
984 // mass likelihood evidence the importance-sampling path must weight
985 // rather than reject.
986 gate_t leaf;
987 double datum;
988 if(gc.getGateType(g) == gate_observe
989 || matchPointObservationCmp(gc, g, leaf, datum))
990 return true;
991 for(gate_t c : gc.getWires(g)) stack.push(c);
992 }
993 return false;
994}
995
996bool circuitHasRV(const GenericCircuit &gc, gate_t root)
997{
998 std::unordered_set<gate_t> seen;
999 std::stack<gate_t> stack;
1000 stack.push(root);
1001 while(!stack.empty()) {
1002 gate_t g = stack.top();
1003 stack.pop();
1004 if(!seen.insert(g).second) continue;
1005 auto type = gc.getGateType(g);
1006 // A continuous random variable is signalled by a gate_rv leaf or a
1007 // gate_mixture root. gate_arith is NOT itself an RV marker: it is also
1008 // arithmetic over aggregates (resolved by provsql_having's possible-worlds
1009 // enumeration). A genuine RV arithmetic gate_arith still reaches its
1010 // gate_rv leaves through the child walk below, so it is caught.
1011 if(type == gate_rv || type == gate_mixture)
1012 return true;
1013 for(gate_t c : gc.getWires(g)) stack.push(c);
1014 }
1015 return false;
1016}
1017
1019{
1020 // True iff a gate_agg survives the probability pre-passes AND every surviving
1021 // one is sample-faithful: SUM / AVG / MIN / MAX / COUNT -- all the aggregates
1022 // the sampler's gate_agg arm reproduces exactly. That arm pushes each kept
1023 // contributor's value into the matching Aggregator: the value gate is the
1024 // row's contribution (the summed term for SUM; the 0/1 indicator for COUNT,
1025 // 0 for a NULL row so count(x) does not count NULLs; the compared value for
1026 // AVG / MIN / MAX), so NULL rows are handled and an empty group finalises to
1027 // the value the exact HAVING evaluator uses (0 for SUM / COUNT, NaN ->
1028 // comparison false for AVG / MIN / MAX). An aggregate that bailed the exact
1029 // evaluators (whose threshold-lineage expansion would otherwise not terminate
1030 // for a large-magnitude / large-support aggregate) is then estimated by direct
1031 // world sampling: the apx-safe corner of the HAVING trichotomy (Re & Suciu).
1032 // gate_arith over such aggregates is covered (its gate_agg leaves are reached
1033 // by the walk). The explicit switch rejects any future aggregate operator the
1034 // sampler does not yet handle.
1035 std::unordered_set<gate_t> seen;
1036 std::stack<gate_t> stack;
1037 stack.push(root);
1038 bool any = false;
1039 while(!stack.empty()) {
1040 gate_t g = stack.top();
1041 stack.pop();
1042 if(!seen.insert(g).second) continue;
1043 if(gc.getGateType(g) == gate_agg) {
1044 switch(getAggregationOperator(gc.getInfos(g).first)) {
1050 any = true;
1051 break;
1052 default: // an aggregate the sampler lacks: not routed
1053 return false;
1054 }
1055 }
1056 for(gate_t c : gc.getWires(g)) stack.push(c);
1057 }
1058 return any;
1059}
1060
1061} // 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:92
@ NONE
No value (NULL).
@ FLOAT
Double-precision float.
Definition Aggregation.h:93
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
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.
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::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.
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 –...
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:99
bool provsql_interrupted
Global variable that becomes true if this particular backend received an interrupt signal.
Definition provsql.c:89
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_LN
unary, natural logarithm of child0 (a negative draw raises at evaluation)
@ 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_NEG
unary, -child0
@ PROVSQL_ARITH_MINUS
binary, child0 - child1
@ 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)
@ 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_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.