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 <vector>
43
44#include "GenericCircuit.h"
45
46extern "C" {
47#include "provsql_utils.h"
48}
49
50namespace provsql {
51
52/**
53 * @brief Run Monte Carlo on a circuit that may contain @c gate_rv leaves.
54 *
55 * @param gc The circuit (loaded from the mmap store via
56 * @c CircuitFromMMap).
57 * @param root Gate to evaluate as a Boolean expression.
58 * @param samples Number of independent worlds to sample.
59 * @return Estimated probability that @p root is true.
60 *
61 * @throws CircuitException on malformed circuits (unknown gate kind in
62 * a Boolean position, malformed @c extra, unknown comparison
63 * operator, etc.).
64 */
65double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples);
66
67/**
68 * @brief Whole-circuit @c (eps,delta)-relative probability via the
69 * Dagum-Karp-Luby-Ross stopping rule.
70 *
71 * The general-Bernoulli case of @c BooleanCircuit::karpLubyStopping, driven by
72 * the RV-aware @c Sampler's @c evalBool rather than by DNF coverage trials, so
73 * it applies to ANY circuit the sampler can evaluate (plain Boolean, continuous
74 * @c gate_rv, and HAVING @c gate_cmp / @c gate_agg) -- the universal relative
75 * estimator. Draws whole-circuit worlds until the success count reaches the
76 * threshold @c Y1 = 1 + (1+eps)*4*(e-2)*ln(2/delta)/eps^2, then returns
77 * @c Y1/N: a relative @c (eps,delta) approximation of @c Pr[root]. The sample
78 * count @c N adapts to the true @c Pr[root] (expected @c Y1/Pr[root]), so the
79 * cost is polynomial precisely when @c Pr[root] is at least @c 1/poly.
80 *
81 * Sampling stops early at @p max_samples worlds; @p reached_target is then
82 * @c false and the return is the plain unbiased @c success/N mean over the
83 * spent budget (the relative target was not met -- the caller reports the
84 * weaker, additive guarantee actually achieved).
85 *
86 * @param gc The circuit.
87 * @param root Gate to evaluate as a Boolean event.
88 * @param eps Target relative error (in @c (0,1]).
89 * @param delta Target failure probability (in @c (0,1)).
90 * @param max_samples Hard cap on the number of worlds drawn.
91 * @param samples_used Output: worlds actually drawn.
92 * @param reached_target Output: whether the threshold was reached before the
93 * cap (i.e. the relative guarantee holds).
94 * @return The probability estimate.
95 */
96double monteCarloRVStopping(const GenericCircuit &gc, gate_t root,
97 double eps, double delta,
98 unsigned long max_samples,
99 unsigned long &samples_used,
100 bool &reached_target);
101
102/**
103 * @brief Walk the circuit reachable from @p root looking for any @c gate_rv.
104 *
105 * Used by @c probability_evaluate to dispatch between the existing
106 * @c BooleanCircuit path and the RV-aware sampler in this file.
107 */
108bool circuitHasRV(const GenericCircuit &gc, gate_t root);
109
110/**
111 * @brief Whether a surviving @c gate_agg exists and every one is sample-faithful
112 * (@c SUM / @c AVG / @c MIN / @c MAX / @c COUNT -- every aggregate the
113 * sampler reproduces exactly).
114 *
115 * A @c gate_agg the exact closed-form / marginal-vector pre-passes did not fold
116 * into a Bernoulli @c gate_input marks a HAVING aggregate comparator whose exact
117 * resolution needs @c provsql_having's threshold-lineage expansion -- which does
118 * not terminate in practice for a large-magnitude / large-support aggregate
119 * (the dense @c kMaxSumRange and sparse @c kMaxSumSupport caps exceeded). For
120 * an @c (eps,delta) request @c probability_evaluate uses this to route the
121 * circuit straight to the world-sampler (the @c gate_agg arm of @c evalScalar)
122 * -- a sound FPRAS for the apx-safe corner of the HAVING trichotomy -- instead
123 * of attempting the non-terminating Boolean expansion.
124 *
125 * The sampler's @c gate_agg arm pushes each kept contributor's value into the
126 * matching @c Aggregator, reproducing SQL semantics exactly: the value gate is
127 * the row's contribution (the summed term for @c SUM; the 0/1 indicator for
128 * @c COUNT, 0 for a NULL row so @c count(x) does not count NULLs; the compared
129 * value for @c AVG / @c MIN / @c MAX), so NULL rows are handled and an empty
130 * group finalises to the value the exact evaluator uses (0 for @c SUM / @c COUNT,
131 * NaN -> comparison false for the others), and @c gate_arith over them is covered
132 * too. In practice only @c SUM / @c AVG / @c MIN / @c MAX ever reach here:
133 * @c COUNT's value-support is small (0/1 per row) so it is always resolved
134 * exactly and never bails -- but it is sample-faithful as well, so it is not
135 * excluded.
136 */
137bool circuitHasUnresolvedSampleableAgg(const GenericCircuit &gc, gate_t root);
138
139/**
140 * @brief Estimate the joint distribution of @p cmps via Monte Carlo.
141 *
142 * For each of @p samples worlds, samples the underlying continuous
143 * island once (shared @c gate_rv leaves use the same per-iteration
144 * draw, per @c monteCarloRV's evalScalar) and evaluates each
145 * comparator in @p cmps; the @c k = @p cmps.size() resulting bits
146 * form a single word @c w with bit @c i = result of @c cmps[i]. The
147 * returned vector has size @c 2^k; entry @c w is the empirical
148 * probability that the joint outcome @c w occurred.
149 *
150 * Used by the multi-cmp half of the hybrid evaluator's island
151 * decomposer to inline a categorical distribution over the @c k cmps
152 * that share an island; @p cmps must all sit over a continuous
153 * island whose scalar evaluation reuses common @c gate_rv leaves so
154 * the cmp draws are correctly correlated.
155 *
156 * @c k is capped at 30 (the result vector size is @c 2^30) to keep
157 * memory bounded; the decomposer enforces a much tighter cap
158 * (@c k_max in @c HybridEvaluator.cpp) so this is purely a safety
159 * limit. Throws @c CircuitException above the cap.
160 *
161 * @param gc The circuit.
162 * @param cmps The comparators jointly evaluated.
163 * @param samples Number of independent worlds.
164 * @return Vector of joint probabilities, indexed by the bit
165 * word @c w (bit @c i = @c cmps[i] outcome).
166 */
167std::vector<double> monteCarloJointDistribution(
168 const GenericCircuit &gc,
169 const std::vector<gate_t> &cmps,
170 unsigned samples);
171
172/**
173 * @brief Sample a scalar sub-circuit @p samples times and return the draws.
174 *
175 * @p root must yield a scalar (@c gate_value, @c gate_rv, or @c gate_arith
176 * over scalar children); otherwise a @c CircuitException is thrown. Each
177 * iteration uses a fresh per-iteration memo cache so that repeated
178 * occurrences of the same @c gate_rv UUID inside an arithmetic expression
179 * share their draw within an iteration but not across iterations.
180 *
181 * The RNG is seeded from @c provsql.monte_carlo_seed exactly like
182 * @c monteCarloRV; pinning the GUC makes the returned vector reproducible.
183 *
184 * Used as the universal MC fallback by the analytical evaluators
185 * (@c Expectation, @c HybridEvaluator) when structural shortcuts cannot
186 * decide a sub-expression. Returning the raw draws (rather than a
187 * single statistic) lets callers compute any combination of moments
188 * from a single sampling pass.
189 */
190std::vector<double> monteCarloScalarSamples(
191 const GenericCircuit &gc, gate_t root, unsigned samples);
192
193/**
194 * @brief Outcome of a conditional Monte Carlo sampling pass.
195 *
196 * @c accepted holds the @c root values from the iterations where
197 * @c event_root evaluated to @c true (the rest are rejected).
198 * @c attempted is the total number of iterations -- equal to @c samples
199 * unless the pass was interrupted -- so the caller can derive the
200 * empirical acceptance rate as
201 * <tt>accepted.size() / attempted</tt> for diagnostics.
202 */
204 std::vector<double> accepted;
205 unsigned attempted;
206};
207
208/**
209 * @brief Rejection-sample @p root conditioned on @p event_root.
210 *
211 * For each of @p samples iterations, the shared @c Sampler resets its
212 * per-iteration cache, then:
213 * 1. evaluates @p event_root as a Boolean (populating @c bool_cache_
214 * and @c scalar_cache_ for every @c gate_rv / @c gate_input touched);
215 * 2. if the indicator is @c true, evaluates @p root as a scalar
216 * using the SAME caches, so any shared @c gate_t leaf produces
217 * one draw that the indicator and the value both observe;
218 * 3. otherwise rejects the iteration.
219 *
220 * This coupling is the entire point of routing the conditional path
221 * through one joint circuit: a @c gate_rv reachable from both
222 * @p root and @p event_root has the same @c gate_t and therefore
223 * shares its per-iteration draw between the indicator (which decides
224 * acceptance) and the value (which we record). The accepted draws
225 * are samples from the conditional distribution
226 * @f$X \mid A@f$ where @c X = @p root and @c A = @p event_root.
227 *
228 * @param gc Circuit (typically from @c getJointCircuit).
229 * @param root Scalar gate whose value we sample.
230 * @param event_root Boolean gate that the iteration must satisfy.
231 * @param samples Number of iterations to attempt.
232 */
234 const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples);
235
236/**
237 * @brief Try to draw @p n exact samples from the conditional
238 * distribution of @p root @b given @p event_root via closed-form
239 * truncation, bypassing MC rejection.
240 *
241 * Fires only when @p root is a bare @c gate_rv whose family admits a
242 * closed-form truncation (@c Uniform / @c Exponential / @c Normal)
243 * and @c collectRvConstraints can extract a sound interval from
244 * @p event_root. Other shapes (arith composites, mixtures, Erlang,
245 * un-extractable events) return @c std::nullopt so the caller can fall
246 * back to @c monteCarloConditionalScalarSamples.
247 *
248 * Sampling kernels:
249 * - <b>Uniform(a, b)</b>: @c collectRvConstraints already intersects
250 * with @c [a, b], so the draw is a plain @c U(lo, hi) on the
251 * intersected interval. 100% acceptance.
252 * - <b>Exponential(λ)</b>, one-sided @c X > c: memorylessness yields
253 * @c c + Exp(λ). Two-sided @c lo < X < hi: inverse-CDF via
254 * @c std::log1p / @c std::expm1 for numerical accuracy near the
255 * support boundary.
256 * - <b>Normal(μ, σ)</b>: inverse-CDF transform. Forward CDF uses
257 * @c std::erf (matching @c AnalyticEvaluator::cdfAt); inverse uses
258 * the Beasley-Springer-Moro rational approximation (~1e-7 accuracy,
259 * ample for sampling).
260 *
261 * Empty / degenerate truncations (@c lo >= @c hi after intersection)
262 * also return @c std::nullopt so the caller's MC fallback can emit
263 * its usual "accepted 0" diagnostic.
264 *
265 * The RNG is seeded from @c provsql.monte_carlo_seed identically to
266 * @c monteCarloScalarSamples, so a pinned seed gives reproducible
267 * output on either path.
268 */
269std::optional<std::vector<double>>
271 gate_t event_root, unsigned n);
272
273} // namespace provsql
274
275#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.
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.
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...
bool circuitHasRV(const GenericCircuit &gc, gate_t root)
Walk the circuit reachable from root looking for any gate_rv.
Core types, constants, and utilities shared across ProvSQL.
Outcome of a conditional Monte Carlo sampling pass.