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 "RangeCheck.h" // collectRvConstraints
9#include "Circuit.h"
10
11#include <algorithm>
12#include <cmath>
13#include <cstdint>
14#include <limits>
15#include <memory>
16#include <optional>
17#include <random>
18#include <stack>
19#include <stdexcept>
20#include <string>
21#include <unordered_map>
22#include <unordered_set>
23#include <variant>
24#include <vector>
25
26namespace provsql {
27
28namespace {
29
30/// Seed an mt19937_64 from the provsql.monte_carlo_seed GUC.
31std::mt19937_64 seedRng()
32{
33 std::mt19937_64 rng;
34 if(provsql_monte_carlo_seed != -1) {
35 rng.seed(static_cast<uint64_t>(provsql_monte_carlo_seed));
36 } else {
37 std::random_device rd;
38 rng.seed((static_cast<uint64_t>(rd()) << 32) | rd());
39 }
40 return rng;
41}
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/// Per-iteration sampler state shared between the Boolean and scalar
58/// recursions.
59class Sampler {
60public:
61 Sampler(const GenericCircuit &gc, std::mt19937_64 &rng)
62 : gc_(gc), rng_(rng) {}
63
64 /// Reset per-iteration memo caches.
65 void resetIteration() {
66 bool_cache_.clear();
67 scalar_cache_.clear();
68 }
69
70 bool evalBool(gate_t g);
71 double evalScalar(gate_t g);
72
73private:
74 const GenericCircuit &gc_;
75 std::mt19937_64 &rng_;
76 std::unordered_map<gate_t, bool> bool_cache_;
77 std::unordered_map<gate_t, double> scalar_cache_;
78};
79
80bool Sampler::evalBool(gate_t g)
81{
82 auto it = bool_cache_.find(g);
83 if(it != bool_cache_.end()) return it->second;
84
85 bool result = false;
86 const auto type = gc_.getGateType(g);
87 const auto &wires = gc_.getWires(g);
88
89 switch(type) {
90 case gate_input:
91 case gate_update:
92 {
93 std::uniform_real_distribution<double> u(0.0, 1.0);
94 result = u(rng_) < gc_.getProb(g);
95 break;
96 }
97 case gate_plus:
98 result = false;
99 for(gate_t c : wires) {
100 if(evalBool(c)) { result = true; break; }
101 }
102 break;
103 case gate_times:
104 result = true;
105 for(gate_t c : wires) {
106 if(!evalBool(c)) { result = false; break; }
107 }
108 break;
109 case gate_monus:
110 if(wires.size() != 2)
111 throw CircuitException("gate_monus must have exactly two children");
112 result = evalBool(wires[0]) && !evalBool(wires[1]);
113 break;
114 case gate_zero:
115 result = false;
116 break;
117 case gate_one:
118 result = true;
119 break;
120 case gate_cmp:
121 {
122 if(wires.size() != 2)
123 throw CircuitException("gate_cmp must have exactly two children");
124 bool ok;
125 ComparisonOperator op = cmpOpFromOid(gc_.getInfos(g).first, ok);
126 if(!ok)
127 throw CircuitException(
128 "gate_cmp: unsupported operator OID " +
129 std::to_string(gc_.getInfos(g).first));
130 double l = evalScalar(wires[0]);
131 double r = evalScalar(wires[1]);
132 result = applyCmp(l, op, r);
133 break;
134 }
135 case gate_mulinput:
136 throw CircuitException(
137 "Monte Carlo over circuits containing gate_mulinput "
138 "is not yet supported on the RV path");
139 case gate_delta:
140 // δ-semiring operator: identity on the Boolean semiring, so the
141 // sampled truth value is just the wrapped child's. Showed up
142 // when conditioning on a row's provenance() in an aggregate
143 // query (HAVING / GROUP BY paths can splice δ over the
144 // semimod's k-side).
145 if(wires.size() != 1)
146 throw CircuitException("gate_delta must have exactly one child");
147 result = evalBool(wires[0]);
148 break;
149 case gate_assumed:
150 // Structural Boolean-rewrite marker: identity on the Boolean
151 // semiring, so the sampled truth value is the wrapped child's.
152 // The marker exists to refuse non-Boolean-compat evaluation; MC
153 // sampling for probability is always Boolean-compat.
154 if(wires.size() != 1)
155 throw CircuitException(
156 "gate_assumed must have exactly one child");
157 result = evalBool(wires[0]);
158 break;
159 case gate_annotation:
160 // Transparent annotation wrapper (inversion-free certificate / order
161 // key): identity, so the sampled truth value is the wrapped child's.
162 if(wires.size() != 1)
163 throw CircuitException("gate_annotation must have exactly one child");
164 result = evalBool(wires[0]);
165 break;
166 default:
167 throw CircuitException(
168 "Unsupported gate type in Boolean evaluation: " +
169 std::string(gate_type_name[type]));
170 }
171
172 bool_cache_[g] = result;
173 return result;
174}
175
176double Sampler::evalScalar(gate_t g)
177{
178 auto it = scalar_cache_.find(g);
179 if(it != scalar_cache_.end()) return it->second;
180
181 double result = 0.0;
182 const auto type = gc_.getGateType(g);
183 const auto &wires = gc_.getWires(g);
184
185 switch(type) {
186 case gate_value:
187 result = parseDoubleStrict(gc_.getExtra(g));
188 break;
189 case gate_rv:
190 {
191 auto spec = parse_distribution_spec(gc_.getExtra(g));
192 if(!spec)
193 throw CircuitException(
194 "Malformed gate_rv extra: " + gc_.getExtra(g));
195 switch(spec->kind) {
196 case DistKind::Normal: {
197 std::normal_distribution<double> d(spec->p1, spec->p2);
198 result = d(rng_);
199 break;
200 }
201 case DistKind::Uniform: {
202 std::uniform_real_distribution<double> d(spec->p1, spec->p2);
203 result = d(rng_);
204 break;
205 }
206 case DistKind::Exponential: {
207 std::exponential_distribution<double> d(spec->p1);
208 result = d(rng_);
209 break;
210 }
211 case DistKind::Erlang: {
212 /* Gamma(shape, scale) with integer shape k and scale 1/λ
213 * samples Erlang(k, λ) directly. std::gamma_distribution
214 * uses the rate's inverse as its scale parameter. */
215 std::gamma_distribution<double> d(spec->p1, 1.0 / spec->p2);
216 result = d(rng_);
217 break;
218 }
219 }
220 break;
221 }
222 case gate_arith:
223 {
224 if(wires.empty())
225 throw CircuitException("gate_arith must have at least one child");
226 auto op = static_cast<provsql_arith_op>(gc_.getInfos(g).first);
227 switch(op) {
229 result = 0.0;
230 for(gate_t c : wires) result += evalScalar(c);
231 break;
233 result = 1.0;
234 for(gate_t c : wires) result *= evalScalar(c);
235 break;
237 if(wires.size() != 2)
238 throw CircuitException("gate_arith MINUS must be binary");
239 result = evalScalar(wires[0]) - evalScalar(wires[1]);
240 break;
242 if(wires.size() != 2)
243 throw CircuitException("gate_arith DIV must be binary");
244 result = evalScalar(wires[0]) / evalScalar(wires[1]);
245 break;
247 if(wires.size() != 1)
248 throw CircuitException("gate_arith NEG must be unary");
249 result = -evalScalar(wires[0]);
250 break;
251 default:
252 throw CircuitException(
253 "Unknown gate_arith operator tag: " +
254 std::to_string(static_cast<unsigned>(op)));
255 }
256 break;
257 }
258 case gate_agg:
259 {
260 // HAVING-style aggregate evaluated per MC iteration: walk the
261 // gate_semimod children, keep the rows whose k_gate fires in
262 // this world, push their value into a reusable Aggregator,
263 // return the finalised scalar. Closes the priority-4-era gap
264 // that made `WHERE rv > 0 GROUP BY x HAVING count(*) > 1`
265 // structural-only (see continuous_selection.sql section G).
266 //
267 // Type plan: we evaluate every numeric path in float8 to stay
268 // inside evalScalar's return type. COUNT is normalised by
269 // makeAggregator to SumAgg<long>, so each kept row contributes its
270 // value gate cast to long: that gate is 1 for an ordinary row and 0
271 // for a NULL one (count(x) does not count NULLs), so the sum of the
272 // kept values is exactly count(*) / count(x) -- faithful with no
273 // nullability check. SUM / AVG / MIN / MAX consume the value via
274 // evalScalar directly. Empty groups finalise to NONE; SUM / COUNT
275 // surface 0 (the additive identity, the ProvSQL empty-group
276 // convention) and AVG / MIN / MAX surface NaN, which compares false
277 // under IEEE on any subsequent gate_cmp -- the SQL convention for
278 // HAVING on an empty group.
280 getAggregationOperator(gc_.getInfos(g).first);
281 std::unique_ptr<Aggregator> agg =
285 if(!agg)
286 throw CircuitException(
287 "gate_agg: makeAggregator returned null for op " +
288 std::to_string(static_cast<int>(op)));
289 for(gate_t child : wires) {
290 if(gc_.getGateType(child) != gate_semimod) continue;
291 const auto &sm = gc_.getWires(child);
292 if(sm.size() != 2) continue;
293 if(!evalBool(sm[0])) continue;
295 agg->add(AggValue(static_cast<long>(evalScalar(sm[1]))));
296 } else {
297 agg->add(AggValue(evalScalar(sm[1])));
298 }
299 }
300 AggValue r = agg->finalize();
301 switch(r.getType()) {
302 case ValueType::INT:
303 result = static_cast<double>(std::get<long>(r.v));
304 break;
305 case ValueType::FLOAT:
306 result = std::get<double>(r.v);
307 break;
308 case ValueType::NONE:
309 // ProvSQL convention diverges from standard SQL here: COUNT
310 // and SUM over an empty group both yield the additive
311 // identity 0 (see test/sql/continuous_aggregation.sql §5 --
312 // empty-group SUM returns as_random(0) as a deterministic
313 // Dirac). MC sampling mirrors that so an iteration where
314 // every semimod's Boolean filter was false produces 0.0
315 // rather than poisoning the moment estimator with NaN.
316 // MIN / MAX / AVG over empty groups stay NaN: there is no
317 // natural identity for those, and the moment averagers in
318 // Expectation::mc_raw_moment / mc_central_moment skip NaN
319 // samples so the estimator is conditional on non-empty
320 // worlds.
321 result = (op == AggregationOperator::COUNT
323 ? 0.0
324 : std::numeric_limits<double>::quiet_NaN();
325 break;
326 default:
327 throw CircuitException(
328 "gate_agg: unsupported aggregate result ValueType in MC");
329 }
330 break;
331 }
332 case gate_semimod:
333 {
334 // Bare semimod root (the user pinned one of an agg's per-row
335 // contributions): interpret as a Bernoulli-weighted scalar
336 // value · 1_{k fires}. When the Boolean k child does not fire
337 // in this world, the row contributes nothing -- return 0.0
338 // (the additive identity), which matches the per-iteration
339 // role semimod plays inside gate_agg above. This makes
340 // semimod a legal scalar root for rv_sample / rv_moment /
341 // rv_histogram alongside agg.
342 const auto &wires = gc_.getWires(g);
343 if(wires.size() != 2)
344 throw CircuitException(
345 "gate_semimod must have exactly two children "
346 "[k_gate, value_gate]");
347 result = evalBool(wires[0]) ? evalScalar(wires[1]) : 0.0;
348 break;
349 }
350 case gate_mixture:
351 {
352 // Two shapes of gate_mixture share this case:
353 //
354 // - Classic 3-wire: [p_token, x_token, y_token]. Draw the
355 // Bernoulli via evalBool, which handles gate_input by
356 // sampling uniform(0,1) < get_prob and memoises on
357 // bool_cache_; two mixtures sharing the same p_token
358 // therefore see the same draw, and any unrelated Boolean
359 // parent of p_token stays in sync.
360 //
361 // - Categorical N-wire: [key, mul_1, ..., mul_n]. Built
362 // directly by the @c provsql.categorical SQL constructor;
363 // each mul_i carries its probability in set_prob and its
364 // outcome value in extra.
365 // We draw a single uniform[0,1) per block, walk the
366 // cumulative probabilities to pick a mulinput, and stash the
367 // Boolean truth values into bool_cache_ so any downstream
368 // Boolean consumer of the mulinputs (independentEvaluation,
369 // OR/AND parents) sees a consistent sampled outcome.
370 if(gc_.isCategoricalMixture(g)) {
371 std::uniform_real_distribution<double> u(0.0, 1.0);
372 const double r = u(rng_);
373 double cum = 0.0;
374 // Default to the last mulinput in case floating-point cumulative
375 // sums leave us shy of 1.0 by a few ULPs.
376 std::size_t chosen = wires.size() - 1;
377 for(std::size_t i = 1; i < wires.size(); ++i) {
378 cum += gc_.getProb(wires[i]);
379 if(r < cum) { chosen = i; break; }
380 }
381 for(std::size_t i = 1; i < wires.size(); ++i) {
382 bool_cache_[wires[i]] = (i == chosen);
383 }
384 result = parseDoubleStrict(gc_.getExtra(wires[chosen]));
385 break;
386 }
387 if(wires.size() != 3)
388 throw CircuitException(
389 "gate_mixture must have exactly three children "
390 "[p_token, x_token, y_token]");
391 result = evalBool(wires[0]) ? evalScalar(wires[1])
392 : evalScalar(wires[2]);
393 break;
394 }
395 default:
396 throw CircuitException(
397 "Unsupported gate type in scalar evaluation: " +
398 std::string(gate_type_name[type]));
399 }
400
401 scalar_cache_[g] = result;
402 return result;
403}
404
405} // namespace
406
407double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples)
408{
409 std::mt19937_64 rng = seedRng();
410 Sampler sampler(gc, rng);
411
412 unsigned success = 0;
413 for(unsigned i = 0; i < samples; ++i) {
414 sampler.resetIteration();
415 if(sampler.evalBool(root))
416 ++success;
417
419 throw CircuitException(
420 "Interrupted after " + std::to_string(i + 1) + " samples");
421 }
422 return success * 1.0 / samples;
423}
424
426 double eps, double delta,
427 unsigned long max_samples,
428 unsigned long &samples_used,
429 bool &reached_target)
430{
431 samples_used = 0;
432 reached_target = false;
433 if(max_samples == 0)
434 return 0.;
435
436 // DKLR stopping threshold on the success count -- the S=1 Bernoulli case of
437 // BooleanCircuit::karpLubyStopping: draw whole-circuit worlds until the
438 // success count reaches Y1 and return Y1/N, a relative (eps,delta) estimate
439 // of Pr[root]; N adapts to the true Pr[root] (expected Y1/Pr[root]).
440 const double e = std::exp(1.0);
441 const double Y = 4.0 * (e - 2.0) * std::log(2.0 / delta) / (eps * eps);
442 const double Y1 = 1.0 + (1.0 + eps) * Y;
443
444 std::mt19937_64 rng = seedRng();
445 Sampler sampler(gc, rng);
446
447 unsigned long success = 0;
448 for(unsigned long s = 0; s < max_samples; ++s) {
449 sampler.resetIteration();
450 if(sampler.evalBool(root)) {
451 ++success;
452 if(static_cast<double>(success) >= Y1) {
453 samples_used = s + 1;
454 reached_target = true;
455 return Y1 / static_cast<double>(samples_used);
456 }
457 }
459 throw CircuitException(
460 "Interrupted after " + std::to_string(s + 1) + " samples");
461 }
462
463 // Cap reached before the threshold: the relative target is not met, so return
464 // the plain unbiased mean over the spent budget.
465 samples_used = max_samples;
466 return static_cast<double>(success) / static_cast<double>(max_samples);
467}
468
469std::vector<double> monteCarloJointDistribution(
470 const GenericCircuit &gc,
471 const std::vector<gate_t> &cmps,
472 unsigned samples)
473{
474 const unsigned k = cmps.size();
475 if (k == 0)
476 throw CircuitException(
477 "monteCarloJointDistribution: empty cmps list");
478 if (k > 30)
479 throw CircuitException(
480 "monteCarloJointDistribution: too many cmps in island ("
481 + std::to_string(k) + " > 30)");
482
483 std::mt19937_64 rng = seedRng();
484 Sampler sampler(gc, rng);
485
486 const std::size_t nb_outcomes = std::size_t{1} << k;
487 std::vector<unsigned> counts(nb_outcomes, 0);
488
489 for (unsigned i = 0; i < samples; ++i) {
490 sampler.resetIteration();
491 std::size_t w = 0;
492 for (unsigned j = 0; j < k; ++j) {
493 if (sampler.evalBool(cmps[j])) w |= (std::size_t{1} << j);
494 }
495 ++counts[w];
497 throw CircuitException(
498 "Interrupted after " + std::to_string(i + 1) + " samples");
499 }
500
501 std::vector<double> probs(nb_outcomes);
502 for (std::size_t w = 0; w < nb_outcomes; ++w)
503 probs[w] = counts[w] * 1.0 / samples;
504 return probs;
505}
506
507std::vector<double> monteCarloScalarSamples(
508 const GenericCircuit &gc, gate_t root, unsigned samples)
509{
510 std::mt19937_64 rng = seedRng();
511 Sampler sampler(gc, rng);
512
513 std::vector<double> out;
514 out.reserve(samples);
515 for(unsigned i = 0; i < samples; ++i) {
516 sampler.resetIteration();
517 out.push_back(sampler.evalScalar(root));
518
520 throw CircuitException(
521 "Interrupted after " + std::to_string(i + 1) + " samples");
522 }
523 return out;
524}
525
527 const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
528{
529 std::mt19937_64 rng = seedRng();
530 Sampler sampler(gc, rng);
531
533 out.attempted = 0;
534 out.accepted.reserve(samples);
535
536 for(unsigned i = 0; i < samples; ++i) {
537 sampler.resetIteration();
538 /* Evaluate the indicator FIRST: this populates bool_cache_ AND
539 * scalar_cache_ for every gate_rv / gate_input that the event
540 * touches, so the subsequent evalScalar(root) reads the same
541 * draws. Shared gate_t leaves between root and event_root are
542 * therefore correctly coupled across the indicator and the
543 * value. */
544 if(sampler.evalBool(event_root)) {
545 out.accepted.push_back(sampler.evalScalar(root));
546 }
547 ++out.attempted;
548
550 throw CircuitException(
551 "Interrupted after " + std::to_string(i + 1) + " samples");
552 }
553 return out;
554}
555
556namespace {
557
558/**
559 * @brief Inverse standard-normal CDF, Beasley-Springer-Moro (1995).
560 *
561 * Returns @c z such that @f$\Phi(z) = p@f$. Accurate to about
562 * @c 1e-7 over @c p ∈ [0.02425, 1 - 0.02425], with a tail
563 * rational fallback for the rest of @c (0, 1). Callers must clamp
564 * @c p strictly inside @c (0, 1) since the function diverges at the
565 * endpoints; the truncated-normal sampler below clamps to
566 * @c [1e-15, 1 - 1e-15] before each call.
567 *
568 * Used by @c try_truncated_closed_form_sample to invert the normal
569 * CDF for inverse-CDF transform sampling. The Beasley-Springer-Moro
570 * routine is in widespread library use (NumPy/SciPy 'norminv', etc.)
571 * and its accuracy is several orders of magnitude tighter than the
572 * sampling noise the tests can detect at 10k draws, so it's a
573 * comfortable margin.
574 */
575double inv_phi(double p)
576{
577 static const double a[] = {
578 -3.969683028665376e+01, 2.209460984245205e+02,
579 -2.759285104469687e+02, 1.383577518672690e+02,
580 -3.066479806614716e+01, 2.506628277459239e+00
581 };
582 static const double b[] = {
583 -5.447609879822406e+01, 1.615858368580409e+02,
584 -1.556989798598866e+02, 6.680131188771972e+01,
585 -1.328068155288572e+01
586 };
587 static const double c_arr[] = {
588 -7.784894002430293e-03, -3.223964580411365e-01,
589 -2.400758277161838e+00, -2.549732539343734e+00,
590 4.374664141464968e+00, 2.938163982698783e+00
591 };
592 static const double d[] = {
593 7.784695709041462e-03, 3.224671290700398e-01,
594 2.445134137142996e+00, 3.754408661907416e+00
595 };
596 static const double p_low = 0.02425;
597 static const double p_high = 1.0 - p_low;
598
599 if (p < p_low) {
600 const double q = std::sqrt(-2.0 * std::log(p));
601 return (((((c_arr[0]*q + c_arr[1])*q + c_arr[2])*q
602 + c_arr[3])*q + c_arr[4])*q + c_arr[5])
603 / ((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1.0);
604 }
605 if (p <= p_high) {
606 const double q = p - 0.5;
607 const double r = q * q;
608 return (((((a[0]*r + a[1])*r + a[2])*r + a[3])*r + a[4])*r + a[5]) * q
609 / (((((b[0]*r + b[1])*r + b[2])*r + b[3])*r + b[4])*r + 1.0);
610 }
611 const double q = std::sqrt(-2.0 * std::log(1.0 - p));
612 return -(((((c_arr[0]*q + c_arr[1])*q + c_arr[2])*q
613 + c_arr[3])*q + c_arr[4])*q + c_arr[5])
614 / ((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1.0);
615}
616
617} // namespace
618
619std::optional<std::vector<double>>
621 gate_t event_root, unsigned n)
622{
623 auto m = matchTruncatedSingleRv(gc, root, event_root);
624 if (!m) return std::nullopt;
625 const DistributionSpec &spec = m->spec;
626 const double lo = m->lo, hi = m->hi;
627
628 std::mt19937_64 rng = seedRng();
629 std::uniform_real_distribution<double> U01(0.0, 1.0);
630
631 std::vector<double> out;
632 out.reserve(n);
633
634 switch (spec.kind) {
635 case DistKind::Uniform: {
636 /* @c matchTruncatedSingleRv already intersected the event's
637 * interval with the RV's natural [a, b] support, so a plain
638 * uniform draw on [lo, hi] is the conditional distribution. */
639 std::uniform_real_distribution<double> U(lo, hi);
640 for (unsigned i = 0; i < n; ++i) out.push_back(U(rng));
641 return out;
642 }
644 const double lambda = spec.p1;
645 if (!(lambda > 0.0)) return std::nullopt;
646 if (std::isinf(hi)) {
647 /* X | X > lo = lo + Exp(λ) by memorylessness. Numerically
648 * stable for arbitrarily large @c lo, where the inverse-CDF
649 * would underflow on @c 1 - exp(-λ·lo). */
650 std::exponential_distribution<double> E(lambda);
651 for (unsigned i = 0; i < n; ++i) out.push_back(lo + E(rng));
652 return out;
653 }
654 /* Two-sided truncation: inverse-CDF on @c [F(lo), F(hi)] with
655 * @c F(x) = -expm1(-λx) for accuracy near 0, and @c x =
656 * -log1p(-u)/λ for accuracy as @c u approaches 1. */
657 const double F_lo = -std::expm1(-lambda * lo);
658 const double F_hi = -std::expm1(-lambda * hi);
659 if (!(F_lo < F_hi)) return std::nullopt;
660 for (unsigned i = 0; i < n; ++i) {
661 const double u = F_lo + U01(rng) * (F_hi - F_lo);
662 out.push_back(-std::log1p(-u) / lambda);
663 }
664 return out;
665 }
666 case DistKind::Normal: {
667 const double mu = spec.p1;
668 const double sigma = spec.p2;
669 if (!(sigma > 0.0)) return std::nullopt;
670 const double sqrt2 = std::sqrt(2.0);
671 const double alpha = (lo - mu) / sigma;
672 const double beta = (hi - mu) / sigma;
673 const double Phi_a = std::isfinite(alpha)
674 ? 0.5 * (1.0 + std::erf(alpha / sqrt2))
675 : (alpha < 0 ? 0.0 : 1.0);
676 const double Phi_b = std::isfinite(beta)
677 ? 0.5 * (1.0 + std::erf(beta / sqrt2))
678 : (beta < 0 ? 0.0 : 1.0);
679 if (!(Phi_a < Phi_b)) return std::nullopt;
680 /* Clamp the target probability strictly inside (0, 1) so
681 * @c inv_phi does not diverge near the asymptotes. The 1e-15
682 * margin is well below the BSM approximation's intrinsic
683 * accuracy floor (~1e-7), so it's a safe sentinel. */
684 static constexpr double EPS = 1e-15;
685 for (unsigned i = 0; i < n; ++i) {
686 double u = Phi_a + U01(rng) * (Phi_b - Phi_a);
687 if (u < EPS) u = EPS;
688 if (u > 1.0 - EPS) u = 1.0 - EPS;
689 const double z = inv_phi(u);
690 out.push_back(mu + sigma * z);
691 }
692 return out;
693 }
694 case DistKind::Erlang:
695 /* Truncated Erlang requires the regularised lower incomplete
696 * gamma's inverse. Out of scope for v1; MC fallback handles it. */
697 return std::nullopt;
698 }
699 return std::nullopt;
700}
701
702bool circuitHasRV(const GenericCircuit &gc, gate_t root)
703{
704 std::unordered_set<gate_t> seen;
705 std::stack<gate_t> stack;
706 stack.push(root);
707 while(!stack.empty()) {
708 gate_t g = stack.top();
709 stack.pop();
710 if(!seen.insert(g).second) continue;
711 auto type = gc.getGateType(g);
712 // A continuous random variable is signalled by a gate_rv leaf or a
713 // gate_mixture root. gate_arith is NOT itself an RV marker: it is also
714 // arithmetic over aggregates (resolved by provsql_having's possible-worlds
715 // enumeration). A genuine RV arithmetic gate_arith still reaches its
716 // gate_rv leaves through the child walk below, so it is caught.
717 if(type == gate_rv || type == gate_mixture)
718 return true;
719 for(gate_t c : gc.getWires(g)) stack.push(c);
720 }
721 return false;
722}
723
725{
726 // True iff a gate_agg survives the probability pre-passes AND every surviving
727 // one is sample-faithful: SUM / AVG / MIN / MAX / COUNT -- all the aggregates
728 // the sampler's gate_agg arm reproduces exactly. That arm pushes each kept
729 // contributor's value into the matching Aggregator: the value gate is the
730 // row's contribution (the summed term for SUM; the 0/1 indicator for COUNT,
731 // 0 for a NULL row so count(x) does not count NULLs; the compared value for
732 // AVG / MIN / MAX), so NULL rows are handled and an empty group finalises to
733 // the value the exact HAVING evaluator uses (0 for SUM / COUNT, NaN ->
734 // comparison false for AVG / MIN / MAX). An aggregate that bailed the exact
735 // evaluators (whose threshold-lineage expansion would otherwise not terminate
736 // for a large-magnitude / large-support aggregate) is then estimated by direct
737 // world sampling: the apx-safe corner of the HAVING trichotomy (Re & Suciu).
738 // gate_arith over such aggregates is covered (its gate_agg leaves are reached
739 // by the walk). The explicit switch rejects any future aggregate operator the
740 // sampler does not yet handle.
741 std::unordered_set<gate_t> seen;
742 std::stack<gate_t> stack;
743 stack.push(root);
744 bool any = false;
745 while(!stack.empty()) {
746 gate_t g = stack.top();
747 stack.pop();
748 if(!seen.insert(g).second) continue;
749 if(gc.getGateType(g) == gate_agg) {
750 switch(getAggregationOperator(gc.getInfos(g).first)) {
756 any = true;
757 break;
758 default: // an aggregate the sampler lacks: not routed
759 return false;
760 }
761 }
762 for(gate_t c : gc.getWires(g)) stack.push(c);
763 }
764 return any;
765}
766
767} // 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:68
@ NONE
No value (NULL).
Definition Aggregation.h:76
@ FLOAT
Double-precision float.
Definition Aggregation.h:69
Generic directed-acyclic-graph circuit template and gate identifier.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
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.
@ Normal
Normal (Gaussian): p1=μ, p2=σ
@ Exponential
Exponential: p1=λ, p2 unused.
@ Uniform
Uniform on [a,b]: p1=a, p2=b.
@ Erlang
Erlang: p1=k (positive integer), p2=λ.
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.
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< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
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...
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.
int provsql_monte_carlo_seed
Seed for the Monte Carlo sampler; -1 means non-deterministic (std::random_device); controlled by the ...
Definition provsql.c:95
bool provsql_interrupted
Global variable that becomes true if this particular backend received an interrupt signal.
Definition provsql.c:85
const char * gate_type_name[]
Names of gate types.
provsql_arith_op
Arithmetic operator tags used by gate_arith.
@ PROVSQL_ARITH_DIV
binary, child0 / child1
@ PROVSQL_ARITH_PLUS
n-ary, sum of children
@ PROVSQL_ARITH_NEG
unary, -child0
@ PROVSQL_ARITH_MINUS
binary, child0 - child1
@ PROVSQL_ARITH_TIMES
n-ary, product of children
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
@ 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...
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.
Definition Aggregation.h:93
Outcome of a conditional Monte Carlo sampling pass.
Parsed distribution spec (kind + up to two parameters).
double p2
Second parameter (σ or b; unused for Exponential).
double p1
First parameter (μ, a, or λ).