ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
MonteCarloSampler.h
Go to the documentation of this file.
1/**
2 * @file MonteCarloSampler.h
3 * @brief Monte Carlo sampling over a @c GenericCircuit, RV-aware.
4 *
5 * Drop-in replacement for @c BooleanCircuit::monteCarlo for circuits
6 * that contain continuous random variables (@c gate_rv) or arithmetic
7 * over RVs (@c gate_arith). Operates directly on the
8 * @c GenericCircuit produced by @c CircuitFromMMap, so the
9 * BoolExpr-semiring translation that drops non-Boolean gates is not
10 * needed.
11 *
12 * Gate handling:
13 * - @c gate_input (and @c gate_update) – Bernoulli draw at @c getProb,
14 * memoised per iteration (so the same input feeding two children
15 * produces the same draw).
16 * - @c gate_plus / @c gate_times / @c gate_monus – Boolean OR / AND /
17 * AND-NOT.
18 * - @c gate_zero / @c gate_one – false / true.
19 * - @c gate_cmp with scalar (@c gate_rv / @c gate_arith / @c gate_value)
20 * children – compare two scalar samples per the comparison-operator
21 * OID stored in @c info1. Aggregate-vs-constant @c gate_cmp gates
22 * from HAVING semantics are handled by the existing
23 * @c BooleanCircuit path and are not reached here.
24 * - @c gate_value – parse @c extra as @c float8.
25 * - @c gate_rv – fresh draw from the distribution serialised in
26 * @c extra (memoised per iteration so the SAME RV inside an
27 * arithmetic expression uses the same draw, per the thesis's
28 * SampleOne).
29 * - @c gate_arith – recurse on scalar children, combine per the
30 * operator tag in @c info1 (@c provsql_arith_op enum: PLUS / TIMES
31 * are n-ary; MINUS / DIV are binary; NEG is unary).
32 *
33 * The RNG is seeded from the @c provsql.monte_carlo_seed GUC: zero
34 * (default) seeds non-deterministically from @c std::random_device,
35 * any other value is a literal seed shared across the Bernoulli and
36 * continuous paths so a single GUC pins the whole computation.
37 */
38#ifndef PROVSQL_MONTE_CARLO_SAMPLER_H
39#define PROVSQL_MONTE_CARLO_SAMPLER_H
40
41#include <optional>
42#include <random>
43#include <utility>
44#include <vector>
45
46#include "GenericCircuit.h"
47
48extern "C" {
49#include "provsql_utils.h"
50}
51
52namespace provsql {
53
54/**
55 * @brief The shared Monte Carlo generator, seeded from the
56 * @c provsql.monte_carlo_seed GUC (@c -1 = non-deterministic from
57 * @c std::random_device).
58 *
59 * Every sampling entry point in this file seeds through here; exposed so
60 * closed-form paths that still need draws (the conjugate-posterior exact
61 * sampler behind @c rv_sample) share the same pinned-seed reproducibility.
62 */
63std::mt19937_64 seedRng();
64
65/**
66 * @brief Run Monte Carlo on a circuit that may contain @c gate_rv leaves.
67 *
68 * @param gc The circuit (loaded from the mmap store via
69 * @c CircuitFromMMap).
70 * @param root Gate to evaluate as a Boolean expression.
71 * @param samples Number of independent worlds to sample.
72 * @return Estimated probability that @p root is true.
73 *
74 * @throws CircuitException on malformed circuits (unknown gate kind in
75 * a Boolean position, malformed @c extra, unknown comparison
76 * operator, etc.).
77 */
78double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples);
79
80/**
81 * @brief Whole-circuit @c (eps,delta)-relative probability via the
82 * Dagum-Karp-Luby-Ross stopping rule.
83 *
84 * The general-Bernoulli case of @c BooleanCircuit::karpLubyStopping, driven by
85 * the RV-aware @c Sampler's @c evalBool rather than by DNF coverage trials, so
86 * it applies to ANY circuit the sampler can evaluate (plain Boolean, continuous
87 * @c gate_rv, and HAVING @c gate_cmp / @c gate_agg) -- the universal relative
88 * estimator. Draws whole-circuit worlds until the success count reaches the
89 * threshold @c Y1 = 1 + (1+eps)*4*(e-2)*ln(2/delta)/eps^2, then returns
90 * @c Y1/N: a relative @c (eps,delta) approximation of @c Pr[root]. The sample
91 * count @c N adapts to the true @c Pr[root] (expected @c Y1/Pr[root]), so the
92 * cost is polynomial precisely when @c Pr[root] is at least @c 1/poly.
93 *
94 * Sampling stops early at @p max_samples worlds; @p reached_target is then
95 * @c false and the return is the plain unbiased @c success/N mean over the
96 * spent budget (the relative target was not met -- the caller reports the
97 * weaker, additive guarantee actually achieved).
98 *
99 * @param gc The circuit.
100 * @param root Gate to evaluate as a Boolean event.
101 * @param eps Target relative error (in @c (0,1]).
102 * @param delta Target failure probability (in @c (0,1)).
103 * @param max_samples Hard cap on the number of worlds drawn.
104 * @param samples_used Output: worlds actually drawn.
105 * @param reached_target Output: whether the threshold was reached before the
106 * cap (i.e. the relative guarantee holds).
107 * @return The probability estimate.
108 */
109double monteCarloRVStopping(const GenericCircuit &gc, gate_t root,
110 double eps, double delta,
111 unsigned long max_samples,
112 unsigned long &samples_used,
113 bool &reached_target);
114
115/**
116 * @brief Walk the circuit reachable from @p root looking for any @c gate_rv.
117 *
118 * Used by @c probability_evaluate to dispatch between the existing
119 * @c BooleanCircuit path and the RV-aware sampler in this file.
120 */
121bool circuitHasRV(const GenericCircuit &gc, gate_t root);
122
123/**
124 * @brief Whether a surviving @c gate_agg exists and every one is sample-faithful
125 * (@c SUM / @c AVG / @c MIN / @c MAX / @c COUNT -- every aggregate the
126 * sampler reproduces exactly).
127 *
128 * A @c gate_agg the exact closed-form / marginal-vector pre-passes did not fold
129 * into a Bernoulli @c gate_input marks a HAVING aggregate comparator whose exact
130 * resolution needs @c provsql_having's threshold-lineage expansion -- which does
131 * not terminate in practice for a large-magnitude / large-support aggregate
132 * (the dense @c kMaxSumRange and sparse @c kMaxSumSupport caps exceeded). For
133 * an @c (eps,delta) request @c probability_evaluate uses this to route the
134 * circuit straight to the world-sampler (the @c gate_agg arm of @c evalScalar)
135 * -- a sound FPRAS for the apx-safe corner of the HAVING trichotomy -- instead
136 * of attempting the non-terminating Boolean expansion.
137 *
138 * The sampler's @c gate_agg arm pushes each kept contributor's value into the
139 * matching @c Aggregator, reproducing SQL semantics exactly: the value gate is
140 * the row's contribution (the summed term for @c SUM; the 0/1 indicator for
141 * @c COUNT, 0 for a NULL row so @c count(x) does not count NULLs; the compared
142 * value for @c AVG / @c MIN / @c MAX), so NULL rows are handled and a
143 * contributor-free iteration finalises to the value the exact evaluator uses
144 * (0 for a @e scalar @c COUNT, whose single row exists over empty input;
145 * NaN -> comparison false for every other case, SQL's NULL or a grouped
146 * aggregation's absent row), and @c gate_arith over them is covered too.
147 *
148 * @c COUNT reaches this arm only as a scalar aggregation. Its value-support
149 * is small (0/1 per row), so a grouped @c COUNT comparison is resolved before
150 * any sampler runs -- by @c RangeCheck when the bounds decide it, otherwise by
151 * @c provsql_having's world enumeration -- and both of those already exclude
152 * the empty world. The grouped arm below is nonetheless written to decline
153 * such a world rather than report 0, so the aggregate stays correct if the
154 * routing ever sends one here.
155 */
156bool circuitHasUnresolvedSampleableAgg(const GenericCircuit &gc, gate_t root);
157
158/**
159 * @brief Estimate the joint distribution of @p cmps via Monte Carlo.
160 *
161 * For each of @p samples worlds, samples the underlying continuous
162 * island once (shared @c gate_rv leaves use the same per-iteration
163 * draw, per @c monteCarloRV's evalScalar) and evaluates each
164 * comparator in @p cmps; the @c k = @p cmps.size() resulting bits
165 * form a single word @c w with bit @c i = result of @c cmps[i]. The
166 * returned vector has size @c 2^k; entry @c w is the empirical
167 * probability that the joint outcome @c w occurred.
168 *
169 * Used by the multi-cmp half of the hybrid evaluator's island
170 * decomposer to inline a categorical distribution over the @c k cmps
171 * that share an island; @p cmps must all sit over a continuous
172 * island whose scalar evaluation reuses common @c gate_rv leaves so
173 * the cmp draws are correctly correlated.
174 *
175 * @c k is capped at 30 (the result vector size is @c 2^30) to keep
176 * memory bounded; the decomposer enforces a much tighter cap
177 * (@c k_max in @c HybridEvaluator.cpp) so this is purely a safety
178 * limit. Throws @c CircuitException above the cap.
179 *
180 * @param gc The circuit.
181 * @param cmps The comparators jointly evaluated.
182 * @param samples Number of independent worlds.
183 * @return Vector of joint probabilities, indexed by the bit
184 * word @c w (bit @c i = @c cmps[i] outcome).
185 */
186std::vector<double> monteCarloJointDistribution(
187 const GenericCircuit &gc,
188 const std::vector<gate_t> &cmps,
189 unsigned samples);
190
191/**
192 * @brief Sample a scalar sub-circuit @p samples times and return the draws.
193 *
194 * @p root must yield a scalar (@c gate_value, @c gate_rv, or @c gate_arith
195 * over scalar children); otherwise a @c CircuitException is thrown. Each
196 * iteration uses a fresh per-iteration memo cache so that repeated
197 * occurrences of the same @c gate_rv UUID inside an arithmetic expression
198 * share their draw within an iteration but not across iterations.
199 *
200 * The RNG is seeded from @c provsql.monte_carlo_seed exactly like
201 * @c monteCarloRV; pinning the GUC makes the returned vector reproducible.
202 *
203 * Used as the universal MC fallback by the analytical evaluators
204 * (@c Expectation, @c HybridEvaluator) when structural shortcuts cannot
205 * decide a sub-expression. Returning the raw draws (rather than a
206 * single statistic) lets callers compute any combination of moments
207 * from a single sampling pass.
208 */
209std::vector<double> monteCarloScalarSamples(
210 const GenericCircuit &gc, gate_t root, unsigned samples);
211
212/**
213 * @brief Coupled per-iteration draws of two scalar roots.
214 *
215 * Each iteration resets the per-iteration cache once and evaluates both
216 * roots against it, so any stochastic leaf shared between @p root_a and
217 * @p root_b produces a single draw both observe: the returned pairs are
218 * samples from the JOINT distribution of (A, B). Backs the
219 * mutual-information plug-in estimator.
220 */
221std::pair<std::vector<double>, std::vector<double>>
222monteCarloScalarPairSamples(const GenericCircuit &gc, gate_t root_a,
223 gate_t root_b, unsigned samples);
224
225/**
226 * @brief Outcome of a conditional Monte Carlo sampling pass.
227 *
228 * @c accepted holds the @c root values from the iterations where
229 * @c event_root evaluated to @c true (the rest are rejected).
230 * @c attempted is the total number of iterations -- equal to @c samples
231 * unless the pass was interrupted -- so the caller can derive the
232 * empirical acceptance rate as
233 * <tt>accepted.size() / attempted</tt> for diagnostics.
234 */
236 std::vector<double> accepted;
237 unsigned attempted;
238};
239
240/**
241 * @brief Rejection-sample @p root conditioned on @p event_root.
242 *
243 * For each of @p samples iterations, the shared @c Sampler resets its
244 * per-iteration cache, then:
245 * 1. evaluates @p event_root as a Boolean (populating @c bool_cache_
246 * and @c scalar_cache_ for every @c gate_rv / @c gate_input touched);
247 * 2. if the indicator is @c true, evaluates @p root as a scalar
248 * using the SAME caches, so any shared @c gate_t leaf produces
249 * one draw that the indicator and the value both observe;
250 * 3. otherwise rejects the iteration.
251 *
252 * This coupling is the entire point of routing the conditional path
253 * through one joint circuit: a @c gate_rv reachable from both
254 * @p root and @p event_root has the same @c gate_t and therefore
255 * shares its per-iteration draw between the indicator (which decides
256 * acceptance) and the value (which we record). The accepted draws
257 * are samples from the conditional distribution
258 * @f$X \mid A@f$ where @c X = @p root and @c A = @p event_root.
259 *
260 * @param gc Circuit (typically from @c getJointCircuit).
261 * @param root Scalar gate whose value we sample.
262 * @param event_root Boolean gate that the iteration must satisfy.
263 * @param samples Number of iterations to attempt.
264 */
266 const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples);
267
268/**
269 * @brief Outcome of a conditional coupled-pair Monte Carlo pass:
270 * @c xs[i] / @c ys[i] are the two roots' values from the same
271 * accepted iteration. @c attempted as in
272 * @c ConditionalScalarSamples.
273 */
275 std::vector<double> xs;
276 std::vector<double> ys;
277 unsigned attempted;
278};
279
280/**
281 * @brief Rejection-sample the PAIR (@p root_a, @p root_b) conditioned on
282 * @p event_root.
283 *
284 * The pair analogue of @c monteCarloConditionalScalarSamples: per
285 * iteration the indicator is evaluated first, and on acceptance both
286 * scalar roots are evaluated against the SAME per-iteration caches, so
287 * any stochastic leaf shared between the two values and/or the event
288 * produces one draw all three observe. The accepted pairs are samples
289 * from the joint conditional distribution @f$(A, B) \mid E@f$ -- the
290 * input the single-pass covariance / correlation estimators need.
291 */
293 const GenericCircuit &gc, gate_t root_a, gate_t root_b,
294 gate_t event_root, unsigned samples);
295
296/**
297 * @brief Try to draw @p n exact samples from the conditional
298 * distribution of @p root @b given @p event_root via closed-form
299 * truncation, bypassing MC rejection.
300 *
301 * Fires only when @p root is a bare @c gate_rv whose family admits a
302 * closed-form truncation (@c Uniform / @c Exponential / @c Normal)
303 * and @c collectRvConstraints can extract a sound interval from
304 * @p event_root. Other shapes (arith composites, mixtures, Erlang,
305 * un-extractable events) return @c std::nullopt so the caller can fall
306 * back to @c monteCarloConditionalScalarSamples.
307 *
308 * Sampling kernels:
309 * - <b>Uniform(a, b)</b>: @c collectRvConstraints already intersects
310 * with @c [a, b], so the draw is a plain @c U(lo, hi) on the
311 * intersected interval. 100% acceptance.
312 * - <b>Exponential(λ)</b>, one-sided @c X > c: memorylessness yields
313 * @c c + Exp(λ). Two-sided @c lo < X < hi: inverse-CDF via
314 * @c std::log1p / @c std::expm1 for numerical accuracy near the
315 * support boundary.
316 * - <b>Normal(μ, σ)</b>: inverse-CDF transform. Forward CDF uses
317 * @c std::erf (matching @c AnalyticEvaluator::cdfAt); inverse uses
318 * the Beasley-Springer-Moro rational approximation (~1e-7 accuracy,
319 * ample for sampling).
320 *
321 * Empty / degenerate truncations (@c lo >= @c hi after intersection)
322 * also return @c std::nullopt so the caller's MC fallback can emit
323 * its usual "accepted 0" diagnostic.
324 *
325 * The RNG is seeded from @c provsql.monte_carlo_seed identically to
326 * @c monteCarloScalarSamples, so a pinned seed gives reproducible
327 * output on either path.
328 */
329std::optional<std::vector<double>>
331 gate_t event_root, unsigned n);
332
333/**
334 * @brief Outcome of a likelihood-weighting (importance-sampling) pass.
335 *
336 * Latent-variable posterior inference draws latents from the prior via the
337 * forward recursion and weights each draw by the observed leaves' densities
338 * at the data (self-normalised importance sampling; the continuous
339 * generalisation of rejection conditioning, which is the 0/1-weight case).
340 *
341 * @c particles holds one @c (x, w) pair per prior draw with @b positive
342 * weight (@c x = the queried root's value, @c w = the product of the
343 * evidence factors); the caller derives any weighted posterior statistic
344 * (mean, variance, quantile) from them. @c weight_sum / @c weight_sq_sum
345 * accumulate over @b all @c attempted draws (a zero-weight draw contributes
346 * 0), so @c evidence() is the marginal likelihood @c P(data) and
347 * @c effectiveSampleSize() the ESS diagnostic.
348 */
350 std::vector<std::pair<double, double>> particles; ///< (x, w) with w > 0.
351 double weight_sum = 0.0; ///< Sum of w over all attempted draws.
352 double weight_sq_sum = 0.0; ///< Sum of w^2 over all attempted draws.
353 unsigned attempted = 0; ///< Number of prior draws.
354
355 /// Marginal likelihood P(data): the mean raw importance weight.
356 double evidence() const {
357 return attempted ? weight_sum / static_cast<double>(attempted) : 0.0;
358 }
359 /// Effective sample size (Sum w)^2 / (Sum w^2); 0 when all weights are 0.
360 double effectiveSampleSize() const {
361 return weight_sq_sum > 0.0 ? (weight_sum * weight_sum) / weight_sq_sum : 0.0;
362 }
363};
364
365/**
366 * @brief Self-normalised importance sampling of @p root given @p evidence.
367 *
368 * For each of @p samples prior draws the shared @c Sampler resets its
369 * per-iteration caches, then:
370 * 1. evaluates @p evidence to an importance @b weight (@c evalWeight):
371 * a @c gate_observe contributes its leaf's pdf at the datum, a Boolean
372 * conditioning event contributes a 0/1 weight, a @c gate_times
373 * multiplies its children's weights -- populating @c scalar_cache_ for
374 * every latent the evidence touches;
375 * 2. if the weight is positive, evaluates @p root as a scalar using the
376 * SAME caches, so a latent shared between @p root and @p evidence is
377 * drawn once and the weight and the value observe it jointly;
378 * 3. records the @c (value, weight) particle.
379 *
380 * Coupling the weight and the value through one joint circuit
381 * (@c getJointCircuit) is what makes the shared latent a single @c gate_t;
382 * the particles are then draws from the posterior of @c root given the data.
383 *
384 * @param gc Circuit (typically from @c getJointCircuit).
385 * @param root Scalar gate whose posterior we sample.
386 * @param evidence Evidence circuit (an @c and_agg conjunction of
387 * @c gate_observe / Boolean events).
388 * @param samples Number of prior draws.
389 */
390WeightedPosterior importanceSampleConditional(
391 const GenericCircuit &gc, gate_t root, gate_t evidence, unsigned samples);
392
393/**
394 * @brief Marginal likelihood @c P(data) of @p evidence: the mean raw
395 * importance weight over @p samples prior draws.
396 *
397 * The same quantity rejection conditioning computes as @c P(C), now a
398 * product of the observations' densities. Backs @c provsql.evidence.
399 */
400double importanceEvidence(const GenericCircuit &gc, gate_t evidence,
401 unsigned samples);
402
403/**
404 * @brief Sampling-importance-resampling: draw @p n posterior samples from a
405 * weighted particle set (proportional to weight, with replacement).
406 *
407 * Turns the weighted particles of @c importanceSampleConditional into
408 * (approximately) unweighted posterior draws for @c rv_sample. Returns an
409 * empty vector when there is no positive-weight particle. The RNG is
410 * seeded from @c provsql.monte_carlo_seed, like every other sampling path.
411 */
412std::vector<double> posteriorResample(const WeightedPosterior &post,
413 unsigned n);
414
415/**
416 * @brief Whether the circuit reachable from @p root contains a
417 * @c gate_observe -- the signal that a conditioning event is
418 * continuous-density evidence and must be evaluated by importance
419 * sampling rather than the analytic / rejection conditional paths.
420 */
421bool circuitHasObserve(const GenericCircuit &gc, gate_t root);
422
423} // namespace provsql
424
425#endif // PROVSQL_MONTE_CARLO_SAMPLER_H
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Semiring-agnostic in-memory provenance circuit.
In-memory provenance circuit with semiring-generic evaluation.
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...
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::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.
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.
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...
Core types, constants, and utilities shared across ProvSQL.
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.
double evidence() const
Marginal likelihood P(data): the mean raw importance weight.
std::vector< std::pair< double, double > > particles
(x, w) with w > 0.
double effectiveSampleSize() const
Effective sample size (Sum w)^2 / (Sum w^2); 0 when all weights are 0.
double weight_sum
Sum of w over all attempted draws.