ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
probability_evaluate.cpp
Go to the documentation of this file.
1/**
2 * @file probability_evaluate.cpp
3 * @brief SQL function @c provsql.probability_evaluate() – probabilistic circuit evaluation.
4 *
5 * Implements @c provsql.probability_evaluate(), which computes the
6 * probability that a provenance circuit evaluates to @c true under the
7 * tuple-independent probabilistic-database model.
8 *
9 * The @p method argument selects the computation algorithm:
10 * - @c "possible-worlds": exact enumeration of all 2^n worlds.
11 * - @c "monte-carlo": approximate via random sampling (fast, inexact).
12 * - @c "weightmc": approximate using the @c weightmc model counter.
13 * - @c "tree-decomposition": exact via tree-decomposition-based d-DNNF.
14 * - @c "independent": exact evaluation for disconnected circuits.
15 * - @c "inversion-free": exact via the structured-d-DNNF builder over the
16 * query-derived order; errors unless the root carries an inversion-free
17 * certificate. The default method also tries it (after @c "independent")
18 * when a certificate is present.
19 * - Any external compiler name (@c "d4", @c "c2d", @c "minic2d", @c "dsharp").
20 *
21 * A SIGINT signal sets a process-local flag that causes the evaluation
22 * to abort and return @c NULL (used when the user cancels a long-running
23 * probability computation).
24 */
25extern "C" {
26#include "postgres.h"
27#include "fmgr.h"
28#include "catalog/pg_type.h"
29#include "miscadmin.h"
30#include "storage/latch.h"
31#include "utils/uuid.h"
32#include "executor/spi.h"
33#include "funcapi.h" // get_call_result_type, BlessTupleDesc
34#include "access/htup_details.h" // heap_form_tuple
35#include "provsql_shmem.h"
36#include "provsql_utils.h"
37#include "utils/guc.h"
38
39PG_FUNCTION_INFO_V1(probability_evaluate);
40PG_FUNCTION_INFO_V1(probability_bounds);
41}
42
43#include "c_cpp_compatibility.h"
44#include <set>
45#include <stack>
46#include <map>
47#include <unordered_map>
48#include <limits>
49#include <chrono>
50#include <vector>
51#include <algorithm>
52#include <cmath>
53#include <csignal>
54#include <string>
55#include <sstream>
56#include <cctype>
57
58#include "BooleanCircuit.h"
59#include "CircuitFromMMap.h"
60#include "GenericCircuit.h"
61#include "AnalyticEvaluator.h"
62#include "CountCmpEvaluator.h"
63#include "MinMaxCmpEvaluator.h"
64#include "SumCmpEvaluator.h"
66#include "HybridEvaluator.h"
67#include "RangeCheck.h"
68#include "MonteCarloSampler.h"
70#include "TreeDecomposition.h"
71#include "StructuredDNNF.h"
72#include "DTree.h"
73#include "ProbabilityMethod.h"
75#include "having_semantics.hpp"
76#include "provsql_mmap.h"
77#include "safe_query_cert.h"
78#include "provsql_utils_cpp.h"
79#include "tool_registry_sync.h"
80#include "semiring/BoolExpr.h"
81#include "semiring/Semiring.h"
82#include "mobius_evaluate.h"
83
84using namespace std;
85
86namespace {
87
88// ---------------------------------------------------------------------------
89// Möbius-route probability sweep (safe-UCQ Möbius-inversion route).
90//
91// The Möbius compiler (mobius_evaluate.cpp) materialises a circuit rooted at a
92// gate_mobius signed combination over certified-independent Boolean islands.
93// Its probability is a single linear sweep: the Boolean islands evaluate
94// read-once (independent OR / AND), and at each gate_mobius the children's
95// probabilities are summed with the stored integer coefficients. gate_mobius
96// nodes nest (an inner MobiusStep is a child of an outer separator's
97// independent-OR), so one unified recursion walks the whole mixed circuit.
98// ---------------------------------------------------------------------------
99double mobiusEvalRec(GenericCircuit &gc, gate_t g, std::map<gate_t,double> &memo)
100{
101 auto it = memo.find(g);
102 if(it != memo.end()) return it->second;
103 CHECK_FOR_INTERRUPTS();
104 double r;
105 switch(gc.getGateType(g)) {
106 case gate_input: r = gc.getProb(g); break;
107 case gate_one: r = 1.0; break;
108 case gate_zero: r = 0.0; break;
109 case gate_times: {
110 r = 1.0;
111 for(gate_t c : gc.getWires(g)) r *= mobiusEvalRec(gc, c, memo);
112 break;
113 }
114 case gate_plus: { // independent OR (read-once by construction)
115 double pn = 1.0;
116 for(gate_t c : gc.getWires(g)) pn *= (1.0 - mobiusEvalRec(gc, c, memo));
117 r = 1.0 - pn;
118 break;
119 }
120 case gate_monus: { // monus(one, x) = NOT x
121 const auto &w = gc.getWires(g);
122 if(w.size()!=2)
123 throw CircuitException("mobius: malformed monus gate");
124 r = mobiusEvalRec(gc, w[0], memo) - mobiusEvalRec(gc, w[1], memo);
125 if(r < 0.) r = 0.;
126 break;
127 }
128 case gate_mobius: { // signed Möbius combination
129 const auto &w = gc.getWires(g);
130 const std::string extra = gc.getExtra(g);
131 // extra is a space-separated list of "uuid:coeff" (coefficients keyed by
132 // child UUID, so wire order / dedup do not matter), plus an optional
133 // "L:<uuid>" naming the literal-lineage child, which the probability
134 // shortcut ignores (it is there only for the non-probability evaluators).
135 std::map<std::string,long> co;
136 std::string lineage;
137 {
138 std::size_t i = 0;
139 while(i < extra.size()) {
140 while(i < extra.size() && (extra[i]==' '||extra[i]=='\t')) ++i;
141 if(i >= extra.size()) break;
142 std::size_t j = i;
143 while(j < extra.size() && extra[j]!=' ' && extra[j]!='\t') ++j;
144 const std::string tok = extra.substr(i, j-i);
145 if(tok.size()>2 && tok[0]=='L' && tok[1]==':')
146 lineage = tok.substr(2);
147 else {
148 const std::size_t colon = tok.rfind(':');
149 if(colon != std::string::npos)
150 co[tok.substr(0,colon)] =
151 std::strtol(tok.substr(colon+1).c_str(), nullptr, 10);
152 }
153 i = j;
154 }
155 }
156 double v = 0.;
157 for(std::size_t i=0;i<w.size();++i) {
158 const std::string u = uuid2string(string2uuid(gc.getUUID(w[i])));
159 if(u == lineage)
160 continue; // the literal lineage: not summed
161 auto cit = co.find(u);
162 if(cit == co.end())
163 throw CircuitException("mobius: a gate_mobius child has no coefficient");
164 v += static_cast<double>(cit->second) * mobiusEvalRec(gc, w[i], memo);
165 }
166 // The true value is a probability; a small fp excursion is folded away,
167 // a larger one means a compiler bug -- warn (a free sanity check).
168 constexpr double tol = 1e-9;
169 if(v < -tol || v > 1.0 + tol)
170 provsql_warning("mobius: signed combination left [0,1] before clamping "
171 "(value %g) -- possible compiler bug", v);
172 if(v < 0.) v = 0.; else if(v > 1.) v = 1.;
173 r = v;
174 break;
175 }
176 default:
177 throw CircuitException("mobius: unsupported gate type in the Möbius-route "
178 "circuit");
179 }
180 memo[g] = r;
181 return r;
182}
183
184/// The literal-lineage child UUID of a gate_mobius-rooted @p token (the "L:"
185/// entry in extra), read from the RAW circuit -- the load-time simplifier may
186/// strip the gate_mobius extra, so a simplified read is unreliable. Empty if
187/// the token carries no lineage (a measure-only build).
188std::string mobiusLineageOf(pg_uuid_t token)
189{
190 const bool s1 = provsql_simplify_on_load;
191 const bool s2 = provsql_boolean_provenance;
192 const bool s3 = provsql_absorptive_provenance;
196 std::string lineage;
197 try {
199 const std::string ex = gc.getExtra(gc.getGate(uuid2string(token)));
200 const std::size_t p = ex.find("L:");
201 if(p != std::string::npos) {
202 const std::size_t e = ex.find(' ', p);
203 lineage = ex.substr(p + 2,
204 e == std::string::npos ? std::string::npos : e - p - 2);
205 }
206 } catch(...) { /* leave empty */ }
210 return lineage;
211}
212
213double mobiusProbabilityImpl(pg_uuid_t token)
214{
215 // Load the circuit RAW: the load-time simplifier (foldSemiringIdentities /
216 // foldBooleanIdentities) rewrites the gate_mobius children's gates, which
217 // would break the coefficient<->child association. The Möbius sweep needs
218 // the circuit exactly as the compiler built it.
219 const bool s1 = provsql_simplify_on_load;
220 const bool s2 = provsql_boolean_provenance;
221 const bool s3 = provsql_absorptive_provenance;
225 double r;
226 try {
228 gate_t root = gc.getGate(uuid2string(token));
229 std::map<gate_t,double> memo;
230 r = mobiusEvalRec(gc, root, memo);
231 } catch(...) {
235 throw;
236 }
240 return r;
241}
242
243
244/// Trim leading/trailing ASCII spaces and tabs.
245string trim_arg(const string &s)
246{
247 size_t a = s.find_first_not_of(" \t");
248 if(a == string::npos)
249 return string();
250 size_t b = s.find_last_not_of(" \t");
251 return s.substr(a, b - a + 1);
252}
253
254/**
255 * @brief Parsed probability-method argument string.
256 *
257 * @c kv holds the @c key=value pairs (key lower-cased, @c eps folded to
258 * @c epsilon); @c positional holds the comma-separated tokens that carry no
259 * @c '=' -- the bare-number (@c monte-carlo) and
260 * @c 'delta;epsilon' / @c 'tool;args' (@c weightmc / @c wmc) shortcuts.
261 */
262struct MethodArgs {
263 map<string, string> kv;
264 vector<string> positional;
265 bool has(const string &k) const { return kv.find(k) != kv.end(); }
266 string get(const string &k) const {
267 auto it = kv.find(k);
268 return it == kv.end() ? string() : it->second;
269 }
270};
271
272/**
273 * @brief Tokenise a probability-method argument string.
274 *
275 * The grammar is shared by every method: a comma-separated list whose
276 * @c key=value items populate @c MethodArgs::kv and whose bare items (no
277 * @c '=') go verbatim into @c MethodArgs::positional. None of these
278 * shortcuts use a comma, so each survives as a single positional token (a
279 * bare integer for @c monte-carlo, @c 'delta;epsilon' for @c weightmc,
280 * @c 'tool;args' for @c wmc), letting the canonical @c key=value form and the
281 * shorthand form coexist.
282 */
283MethodArgs parse_method_args(const string &args)
284{
285 MethodArgs out;
286 stringstream ss(args);
287 string tok;
288 while(getline(ss, tok, ',')) {
289 string t = trim_arg(tok);
290 if(t.empty())
291 continue;
292 auto eq = t.find('=');
293 if(eq == string::npos) {
294 out.positional.push_back(t);
295 } else {
296 string key = trim_arg(t.substr(0, eq));
297 string val = trim_arg(t.substr(eq + 1));
298 transform(key.begin(), key.end(), key.begin(),
299 [](unsigned char c){ return tolower(c); });
300 if(key == "eps")
301 key = "epsilon";
302 out.kv[key] = val;
303 }
304 }
305 return out;
306}
307
308/// Raise if any kv key lies outside @p allowed, naming the method.
309void reject_unknown_keys(const MethodArgs &a, const set<string> &allowed,
310 const char *method)
311{
312 for(const auto &p : a.kv)
313 if(allowed.find(p.first) == allowed.end())
314 provsql_error("method '%s': unknown argument key '%s'",
315 method, p.first.c_str());
316}
317
318/// Parse a non-negative integer that consumes the whole string.
319bool parse_ulong_full(const string &v, unsigned long &out)
320{
321 if(v.empty() || v.find_first_not_of("0123456789") != string::npos)
322 return false;
323 try {
324 size_t pos = 0;
325 out = stoul(v, &pos);
326 return pos == v.size();
327 } catch(const std::exception &) {
328 return false;
329 }
330}
331
332/// Parse a finite double that consumes the whole string.
333bool parse_double_full(const string &v, double &out)
334{
335 if(v.empty())
336 return false;
337 try {
338 size_t pos = 0;
339 out = stod(v, &pos);
340 return pos == v.size();
341 } catch(const std::exception &) {
342 return false;
343 }
344}
345
346/**
347 * @brief Validate and read @c epsilon / @c delta from the parsed args.
348 *
349 * Shared by every approximate method (@c monte-carlo, @c karp-luby,
350 * @c weightmc / @c wmc) so the keys, the @c eps alias, and the
351 * @c epsilon in (0,1] / @c delta in (0,1) range checks are uniform. @p eps and
352 * @p delta carry the caller's defaults on entry and are overwritten only when
353 * the corresponding key is present.
354 */
355void parse_eps_delta(const MethodArgs &a, const char *method,
356 double &eps, double &delta)
357{
358 if(a.has("epsilon") && (!parse_double_full(a.get("epsilon"), eps)
359 || eps <= 0. || eps > 1.))
360 provsql_error("method '%s': epsilon must be in (0, 1]", method);
361 if(a.has("delta") && (!parse_double_full(a.get("delta"), delta)
362 || delta < 0. || delta >= 1.))
363 provsql_error("method '%s': delta must be in [0, 1)", method);
364}
365
366/**
367 * @brief A resolved sampling request: a fixed count, or an @c (eps,delta) target.
368 *
369 * @c fixed selects @c samples; otherwise the caller turns @c (eps,delta) into a
370 * sample count with its own bound (additive for @c monte-carlo, relative for
371 * @c karp-luby).
372 */
373struct SampleSpec {
374 bool fixed = false;
375 unsigned long samples = 0;
376 double eps = 0.1, delta = 0.05;
377 bool has_max = false;
378 unsigned long max_samples = 0;
379};
380
381/**
382 * @brief Parse and validate the argument grammar shared by the sampling methods.
383 *
384 * The grammar is @c samples=N | a bare integer | @c
385 * epsilon=E[,delta=D][,max_samples=M]. This routine only resolves *which* path
386 * the user asked for and validates the keys / ranges; the @c (eps,delta) -> N
387 * conversion is method-specific and applied by the caller. An empty argument
388 * selects the adaptive path with the default @c (eps=0.1, delta=0.05).
389 */
390SampleSpec parse_sample_spec(const MethodArgs &a, const char *method)
391{
392 reject_unknown_keys(a, {"samples", "epsilon", "delta", "max_samples"}, method);
393
394 const bool has_samples = a.has("samples") || !a.positional.empty();
395 const bool has_adaptive = a.has("epsilon") || a.has("delta");
396
397 if(a.positional.size() > 1)
398 provsql_error("method '%s': too many positional arguments", method);
399 if(a.has("samples") && !a.positional.empty())
400 provsql_error("method '%s': give either samples= or a bare integer, "
401 "not both", method);
402 if(has_samples && has_adaptive)
403 provsql_error("method '%s': samples is mutually exclusive with "
404 "epsilon/delta", method);
405 if(a.has("max_samples") && !has_adaptive)
406 provsql_error("method '%s': max_samples applies only to the adaptive "
407 "epsilon/delta path", method);
408 if(a.has("delta") && !a.has("epsilon"))
409 provsql_error("method '%s': delta requires epsilon", method);
410
411 SampleSpec s;
412 if(has_samples) {
413 s.fixed = true;
414 const string v = a.has("samples") ? a.get("samples") : a.positional[0];
415 if(!parse_ulong_full(v, s.samples) || s.samples == 0)
416 provsql_error("method '%s': invalid sample count '%s'", method, v.c_str());
417 return s;
418 }
419
420 parse_eps_delta(a, method, s.eps, s.delta);
421 // delta == 0 is a DETERMINISTIC request: valid only on the tolerance paths
422 // ('relative'/'additive'), which route it to the d-tree / an exact method.
423 // A sampler invoked by name cannot honour it (infinite sample count).
424 if(s.delta == 0. && string(method) != "relative"
425 && string(method) != "additive")
426 provsql_error("method '%s': delta must be in (0, 1); delta = 0 "
427 "(deterministic) is supported only on the 'relative' / "
428 "'additive' paths, which route to the d-tree or an exact "
429 "method", method);
430 if(a.has("max_samples")) {
431 s.has_max = true;
432 if(!parse_ulong_full(a.get("max_samples"), s.max_samples) || s.max_samples == 0)
433 provsql_error("method '%s': invalid max_samples '%s'", method,
434 a.get("max_samples").c_str());
435 }
436 return s;
437}
438
439/// Clamp a real sample count into @c unsigned @c long, honouring @c max_samples.
440unsigned long finalize_adaptive(double nd, const SampleSpec &s)
441{
442 // Clamp only to stay within unsigned long; the count is otherwise honest,
443 // and max_samples / query cancel bound the runtime.
444 unsigned long n = (nd >= 9e18) ? static_cast<unsigned long>(9e18)
445 : static_cast<unsigned long>(nd);
446 if(n == 0)
447 n = 1;
448 if(s.has_max && n > s.max_samples)
449 n = s.max_samples;
450 return n;
451}
452
453/// Format a double compactly for the approximation-guarantee notice.
454string fmt_num(double x)
455{
456 char buf[32];
457 snprintf(buf, sizeof(buf), "%.6g", x);
458 return string(buf);
459}
460
461/**
462 * @brief Emit the machine-readable approximation-guarantee NOTICE (verbose>=5).
463 *
464 * An approximate method's estimate carries an @c (eps,delta) error guarantee;
465 * we surface it as a structured NOTICE that downstream UIs (Studio floors
466 * @c verbose_level at 5 for evaluation) parse and render. @p kind is
467 * @c "additive" (@c |est-p| <= eps) or @c "relative" (@c est within a
468 * @c 1±eps factor of @c p), each holding with probability @c >= 1-delta.
469 * Optional fields are omitted when not applicable: @p delta @c < 0,
470 * @p samples @c == 0, @p clauses @c < 0, @p tool empty. Gated on
471 * @c verbose_level>=5 so plain SQL evaluation (and the regression suite) stay
472 * quiet by default.
473 */
474void emit_guarantee(const char *kind, double eps, double delta,
475 unsigned long samples, long clauses, const char *tool)
476{
477 if(provsql_verbose < 5)
478 return;
479 string msg = "approximation-guarantee: kind=" + string(kind)
480 + " eps=" + fmt_num(eps);
481 if(delta >= 0.)
482 msg += " delta=" + fmt_num(delta);
483 if(samples > 0)
484 msg += " samples=" + std::to_string(samples);
485 if(clauses >= 0)
486 msg += " clauses=" + std::to_string(clauses);
487 if(tool && *tool)
488 msg += " tool=" + string(tool);
489 provsql_notice("%s", msg.c_str());
490}
491
492/// Whether a @c wmc tool is an approximate (multiplicative-guarantee) counter.
493bool is_approx_wmc_tool(const string &tool)
494{
495 return tool == "weightmc" || tool == "approxmc";
496}
497
498/// Extract weightmc's epsilon from a @c wmcCount opt string (@c "delta;epsilon").
499double eps_from_wmc_opt(const string &opt)
500{
501 auto semi = opt.find(';');
502 if(semi == string::npos)
503 return 0.8; // no epsilon field: wmcCount's own default tolerance
504 double e;
505 return (parse_double_full(opt.substr(semi + 1), e) && e > 0.) ? e : 0.8;
506}
507
508/**
509 * @brief Resolve the @c monte-carlo sample count from the parsed args.
510 *
511 * A fixed @c samples=N (or bare integer), or an *additive* @c (eps,delta)
512 * guarantee: the sample mean of the Bernoulli circuit indicator is within
513 * @c eps of the true probability with probability at least @c 1-delta after
514 * @c N = ceil(ln(2/delta) / (2*eps^2)) samples (Hoeffding's inequality, so the
515 * count is independent of the probability being estimated). This is an
516 * *absolute* error bound; @c karp-luby provides the *relative* one needed on
517 * rare-event outputs. Also emits the additive guarantee NOTICE.
518 */
519unsigned long monte_carlo_samples(const MethodArgs &a)
520{
521 SampleSpec s = parse_sample_spec(a, "monte-carlo");
522 unsigned long n = s.fixed
523 ? s.samples
524 : finalize_adaptive(ceil(log(2.0 / s.delta) / (2.0 * s.eps * s.eps)), s);
525 // For a fixed N, report the additive eps achieved at the conventional
526 // delta=0.05; for the adaptive path, report the requested (eps,delta).
527 const double eps = s.fixed
528 ? sqrt(log(2.0 / 0.05) / (2.0 * static_cast<double>(n))) : s.eps;
529 const double delta = s.fixed ? 0.05 : s.delta;
530 emit_guarantee("additive", eps, delta, n, -1, nullptr);
531 return n;
532}
533
534/**
535 * @brief Build the @c wmcCount opt string (@c "delta;epsilon") from the args.
536 *
537 * Canonical form @c epsilon=E[,delta=D], validated through the same
538 * @c parse_eps_delta as the sampling methods; the positional @c 'delta;epsilon'
539 * is accepted as a documented legacy alias (forwarded verbatim). @c wmcCount
540 * reads only @c epsilon (it drives the @c {pivotAC} approximation tolerance), so
541 * @c delta is carried for the legacy two-field order but is presently inert for
542 * the tool.
543 */
544string wmc_opt_from_args(const MethodArgs &a, const char *method)
545{
546 if(!a.positional.empty()) {
547 if(a.has("epsilon") || a.has("delta"))
548 provsql_error("method '%s': give either the legacy 'delta;epsilon' or "
549 "epsilon=/delta=, not both", method);
550 if(a.positional.size() > 1)
551 provsql_error("method '%s': too many positional arguments", method);
552 return a.positional[0];
553 }
554 reject_unknown_keys(a, {"epsilon", "delta"}, method);
555 // weightmc's own epsilon default (0.8) applies when epsilon is omitted, so
556 // validate but emit only the fields the user gave.
557 double eps = 0.8, delta = 0.5;
558 parse_eps_delta(a, method, eps, delta);
559 return (a.has("delta") ? a.get("delta") : string()) + ";"
560 + (a.has("epsilon") ? a.get("epsilon") : string());
561}
562
563/**
564 * @brief Run Karp-Luby on a DNF-shaped circuit and surface its guarantee.
565 *
566 * Two paths, selected by the shared argument grammar:
567 *
568 * - A fixed @c samples=N (or bare integer): the *stratified* fixed-budget
569 * estimator (@c BooleanCircuit::karpLuby). Reports the relative @c eps that
570 * @c N rounds deliver over @p m clauses at the conventional @c delta=0.05.
571 * - An adaptive @c (eps,delta) target (the default @c eps=0.1, @c delta=0.05):
572 * the Dagum-Karp-Luby-Ross self-adjusting *stopping rule*
573 * (@c BooleanCircuit::karpLubyStopping), which samples until the accept
574 * count reaches @c Y1 = 1+(1+eps)*4*(e-2)*ln(2/delta)/eps^2 and reports the
575 * exact @c (eps,delta) over the rounds actually run. The cap defaults to the
576 * fixed worst-case round count @c ceil(Y1*m) (so the adaptive run never costs
577 * more than ~(1+eps) times the old fixed bound) and is overridden by
578 * @c max_samples; hitting it before the target downgrades the guarantee to
579 * the relative @c eps achieved at the spent budget (with a warning).
580 *
581 * Unlike @c monte-carlo's additive bound, this controls the *relative* error,
582 * which is what stays meaningful on rare-event outputs.
583 */
584double evaluate_karp_luby(const BooleanCircuit &c,
585 const std::vector<gate_t> &clauses,
586 const std::vector<std::set<gate_t> > &supports,
587 const MethodArgs &a)
588{
589 const size_t m = clauses.size();
590 const double e = exp(1.0);
591 const double mm = (m == 0) ? 1. : static_cast<double>(m);
592 SampleSpec s = parse_sample_spec(a, "karp-luby");
593
594 if(s.fixed) {
595 double r = c.karpLuby(clauses, supports, s.samples);
596 const double eps =
597 sqrt(4.0 * (e - 2.0) * mm * log(2.0 / 0.05) / static_cast<double>(s.samples));
598 emit_guarantee("relative", eps, 0.05, s.samples, static_cast<long>(m), nullptr);
599 return r;
600 }
601
602 // Adaptive: the self-adjusting stopping rule, capped at ceil(Y1*m) by default.
603 const double Y = 4.0 * (e - 2.0) * log(2.0 / s.delta) / (s.eps * s.eps);
604 const double Y1 = 1.0 + (1.0 + s.eps) * Y;
605 const unsigned long cap =
606 s.has_max ? s.max_samples : finalize_adaptive(ceil(Y1 * mm), s);
607 unsigned long used = 0;
608 bool reached = false;
609 double r = c.karpLubyStopping(clauses, supports, s.eps, s.delta,
610 cap, used, reached);
611 if(reached || used == 0) {
612 emit_guarantee("relative", s.eps, s.delta, used, static_cast<long>(m), nullptr);
613 } else {
614 const double eps =
615 sqrt(4.0 * (e - 2.0) * mm * log(2.0 / 0.05) / static_cast<double>(used));
616 provsql_warning("method 'karp-luby': the stopping rule reached its "
617 "%lu-sample cap before the (epsilon=%g, delta=%g) target; "
618 "reporting the relative guarantee achieved at the samples "
619 "spent", cap, s.eps, s.delta);
620 emit_guarantee("relative", eps, 0.05, used, static_cast<long>(m), nullptr);
621 }
622 return r;
623}
624
625} // anonymous namespace
626
627/// External entry point for the Möbius-route probability sweep (declared in
628/// mobius_evaluate.h, used by the stats SRF in mobius_evaluate.cpp).
630{
631 return mobiusProbabilityImpl(token);
632}
633
634/**
635 * @brief SIGINT handler that sets the global interrupted flag.
636 *
637 * The signal number argument is required by the @c signal() API but is
638 * not used.
639 *
640 * In addition to the @c provsql_interrupted flag polled by the long
641 * Monte-Carlo / possible-worlds evaluation loops, we drive PG's
642 * standard cancel pipeline (@c InterruptPending / @c QueryCancelPending
643 * + @c SetLatch) the same way PG's own @c StatementCancelHandler does.
644 * That makes a SIGINT delivered to the backend (e.g. via
645 * @c pg_cancel_backend) outside of a @c system() wait turn into a
646 * proper 57014 cancel at the next @c CHECK_FOR_INTERRUPTS instead of
647 * being silently absorbed. (The matching case where an external
648 * compiler is running is handled in @c run_external_tool, which runs the
649 * tool in its own process group and @c SIGKILLs that group on a pending
650 * cancel, then lets @c CHECK_FOR_INTERRUPTS raise it.)
651 */
652static void provsql_sigint_handler (int)
653{
654 provsql_interrupted = true;
655
656 if (!proc_exit_inprogress) {
657 InterruptPending = true;
658 QueryCancelPending = true;
659 }
660 SetLatch(MyLatch);
661}
662
663/**
664 * @brief Collect the inversion-free per-input order keys for the structured
665 * builder.
666 *
667 * Walks the @c GenericCircuit @p gc for @c K-prefixed annotation gates whose
668 * child is a @c gate_input (the per-input order markers attached by the planner
669 * on the certified path), parses each key, and maps the wrapped input to its
670 * @c BooleanCircuit variable via @p gc_to_bc. Returns @c true only if every
671 * @c BooleanCircuit input reachable from @p bc_root carries a key (the
672 * structured builder needs a total order over all variables); a missing marker
673 * means the certified markers are absent / incomplete and the caller must not
674 * use the structured path.
675 */
677 const GenericCircuit &gc, gate_t gc_root,
678 const std::unordered_map<gate_t, gate_t> &gc_to_bc,
679 const BooleanCircuit &c, gate_t bc_root,
680 std::map<gate_t, StructuredDNNFBuilder::InputKey> &out)
681{
682 std::set<gate_t> seen;
683 std::stack<gate_t> st;
684 st.push(gc_root);
685 while (!st.empty()) {
686 gate_t g = st.top(); st.pop();
687 if (!seen.insert(g).second) continue;
688 if (gc.getGateType(g) == gate_annotation) {
689 std::string ex = gc.getExtra(g); // must outlive the parse (k points into it)
690 SafeCertKey k;
691 if (safe_cert_key_parse(ex.c_str(), &k)) {
692 const auto &w = gc.getWires(g);
693 if (!w.empty() && gc.getGateType(w[0]) == gate_input) {
694 auto it = gc_to_bc.find(w[0]);
695 if (it != gc_to_bc.end())
696 out[it->second] = StructuredDNNFBuilder::InputKey{
697 std::string(k.root, k.root_len),
698 std::string(k.sec, k.sec_len), k.factor };
699 }
700 }
701 }
702 for (gate_t ch : gc.getWires(g)) st.push(ch);
703 }
704
705 /* every Boolean input must be ordered */
706 std::set<gate_t> bseen;
707 std::stack<gate_t> bst;
708 bst.push(bc_root);
709 while (!bst.empty()) {
710 gate_t g = bst.top(); bst.pop();
711 if (!bseen.insert(g).second) continue;
712 if (c.getGateType(g) == BooleanGate::IN) {
713 if (out.find(g) == out.end())
714 return false;
715 } else {
716 for (gate_t ch : c.getWires(g)) bst.push(ch);
717 }
718 }
719 return true;
720}
721
722/**
723 * @brief Flatten the per-input order keys into a total rank for the structured
724 * builder's order-only constructor.
725 *
726 * Sorts the certified inputs into a Prop. 4.5-consistent order -- root-class
727 * value first (one independent block per value), then secondary-class value
728 * (one tile per value within a block), then the shared self-join guard before
729 * the payloads of its tile, then by factor -- and assigns consecutive ranks.
730 * Ties (two inputs with identical keys) keep a deterministic order via the
731 * input gate id, so distinct variables always get distinct ranks. Unlike the
732 * keyed (factored-sweep) constructor this makes no single-secondary-axis /
733 * one-payload-per-tile assumption, so it certifies every hierarchical
734 * inversion-free lineage, including the self-join-free case.
735 */
736static std::map<gate_t, int> inversion_free_rank(
737 const std::map<gate_t, StructuredDNNFBuilder::InputKey> &keys)
738{
739 std::vector<std::pair<gate_t, StructuredDNNFBuilder::InputKey>> v(
740 keys.begin(), keys.end());
741 std::sort(v.begin(), v.end(), [](const auto &a, const auto &b) {
742 const auto &ka = a.second, &kb = b.second;
743 if (ka.root != kb.root) return ka.root < kb.root;
744 if (ka.sec != kb.sec) return ka.sec < kb.sec;
745 int ga = (ka.factor == StructuredDNNFBuilder::GUARD_FACTOR) ? 0 : 1;
746 int gb = (kb.factor == StructuredDNNFBuilder::GUARD_FACTOR) ? 0 : 1;
747 if (ga != gb) return ga < gb;
748 if (ka.factor != kb.factor) return ka.factor < kb.factor;
749 return a.first < b.first;
750 });
751 std::map<gate_t, int> rank;
752 int r = 0;
753 for (const auto &p : v) rank[p.first] = r++;
754 return rank;
755}
756
758{
759 // Compile a query certified inversion-free to its structured d-DNNF (the
760 // same artefact the 'inversion-free' probability method builds), so the KC
761 // surface can render / measure it. Mirrors the dispatch in
762 // probability_evaluate_internal: the per-input order keys live on the
763 // GenericCircuit's annotation markers, so we go through the generic circuit
764 // rather than getBooleanCircuit(token, ...) directly.
766 gate_t gc_root = gc.getGate(uuid2string(token));
767 std::string ex = gc.getExtra(gc_root);
768 if (ex.empty() || ex[0] != SAFE_CERT_EXTRA_PREFIX_RECIPE)
769 throw CircuitException("compile 'inversion-free': the provenance root "
770 "carries no inversion-free certificate");
771 gate_t root;
772 std::unordered_map<gate_t, gate_t> gc_to_bc;
773 BooleanCircuit c = getBooleanCircuit(gc, token, root, gc_to_bc);
774 std::map<gate_t, StructuredDNNFBuilder::InputKey> keys;
775 if (!collect_inversion_free_keys(gc, gc_root, gc_to_bc, c, root, keys))
776 throw CircuitException("compile 'inversion-free': the certificate's inputs "
777 "lack per-input order markers");
778 return StructuredDNNFBuilder(c, root, inversion_free_rank(keys)).dnnf();
779}
780
781// ---------------------------------------------------------------------------
782// Probability-method catalog (see ProbabilityMethod.h).
783//
784// Each dispatch branch is a ProbabilityMethod object: chooseAndRun runs the
785// independent -> inversion-free -> compilation default ladder, and
786// byName runs each explicit method. The RV+monte-carlo special case and
787// every probability-side pre-pass stay in probability_evaluate_internal.
788// ---------------------------------------------------------------------------
789
790// Forward declaration: the whole-circuit (eps,delta)-relative stopping-rule
791// estimator (defined below probability_evaluate_internal) is delegated to by the
792// portfolio's StoppingRuleMethod, which is defined earlier (in the method-catalog
793// block). It operates on the GenericCircuit so it serves Boolean, RV and HAVING
794// circuits uniformly.
795static void run_stopping_rule(GenericCircuit &gc, gate_t gc_root,
796 const MethodArgs &a, double &result,
797 std::string &actual_method);
798
799namespace provsql {
800
801/// Sanity bound on the reachable-input count for the auto-chosen 2^N
802/// possible-worlds enumeration: above it the method drops out of the portfolio
803/// so it is never *attempted* (its 2^N cost already deprioritises it, but this
804/// guards against a catastrophic last-resort attempt if every cheaper method
805/// failed). The by-name call ignores it (up to possibleWorlds' own 64 limit).
806/// The actual small-N-vs-compile crossover is a cost comparison, not this bound.
807static const size_t kPossibleWorldsSanityMax = 30;
808
809/// Largest clause count for which the auto-chosen sieve (2^m inclusion-exclusion)
810/// is admitted (matches BooleanCircuit::sieve's internal cap). The by-name call
811/// is unaffected.
812static const size_t kSieveSanityMaxClauses = 24;
813
814// ---------------------------------------------------------------------------
815// Cost-function constants. Each method's / feature's estimatedCost models its
816// actual asymptotic complexity (see doc / the complexity table) times one of
817// these constants -- ARBITRARY placeholders for now, to be replaced by measured
818// values in the calibration pass. Parameters (all O(1) on the EvalContext):
819// S = circuit_size (gates), N = n_inputs, m = dnf_num_clauses_, w = tw_proxy_,
820// Delta = tw_max_degree_. Constraints the values must keep (so the lazy chooser
821// stays correct): a cheap method must be tried before acquiring the feature that
822// would only reveal a costlier one, hence
823// C_independent <= C_dnfShape <= C_twProxy
824// (a read-once circuit runs 'independent' before paying for either feature), and
825// C_compilation > C_possibleWorlds (same 2^N, but compilation is the subprocess
826// last resort).
827// Calibrated on this machine so that the value of each cost function is roughly
828// the number of MILLISECONDS the work takes (order-of-magnitude only -- the goal
829// is to know whether a cost is ~1, ~100, ~1e6 ms, not a precise fit). See the
830// calibration instrumentation (provsql.verbose_level >= 50) and doc.
831static const double kCostIndependent = 5e-5; // O(S): ~5e-5 * S
832static const double kCostInversionFree = 5e-5; // O(S + N log N) (~ independent)
833static const double kCostPossibleWorlds = 3e-6; // O(S * 2^N): ~3e-6 * S*2^N
834static const double kCostSieve = 1e-5; // O(S * 2^m): ~1e-5 (rarely optimal)
835// NB: w is the degeneracy LOWER bound, so 2^w under-costs tree-decomposition when
836// the true treewidth exceeds it (a dense, low-degeneracy circuit can run far
837// slower than predicted). The constant is calibrated where the proxy is tight
838// (tree-like circuits, where tree-decomposition is the right pick anyway).
839static const double kCostTreeDecomp = 7e-4; // O(S * (Delta^2+2^w)): ~7e-4 * f
840// compilation is an external knowledge compiler: a fixed subprocess startup
841// plus a compile that, while it exploits structure on easy shapes (~tens of ms
842// here), can struggle badly on others (the worst case is exponential). So we do
843// NOT model it as linear -- that is too optimistic; we use a pessimistic
844// super-linear S^1.5 above a startup floor, keeping it a strong last resort.
845static const double kCostCompilation = 2e-3; // ~2e-3 * S^1.5 ms ...
846static const double kCostCompilationFloor= 40.0; // ... but at least ~40 ms (startup + easy compile)
847static const double kCostDnfShapeFeature = 2e-6; // O(S): ~2e-6 * S
848static const double kCostTwProxyFeature = 3e-4; // O(S): ~3e-4 * S
849// Approximate portfolio members (relative & additive paths). Their cost has the
850// sample-budget term C = ln(2/delta)/eps^2 times per-sample O(S) work, but the
851// CONSTANT is PESSIMISTIC by design: measurement showed the runtimes depend on the
852// result probability p and the clause structure -- NOT static features (the same
853// lesson as the d-tree). karp-luby moved 14x (4 -> 52 ms on one DNF) purely with
854// p; the stopping rule's Dagum 1/p factor (it draws ~1/p worlds) took a rare-event
855// (p~0.06) circuit to 120-470 ms where a p~1 model predicts ~10. A single
856// constant cannot be accurate across p, so these are upper bounds. The chooser
857// then never UNDER-prices a sampler and picks a slow one: it prefers the
858// delta-independent d-tree (no 1/p, deterministic) and exact-when-cheaper, and
859// falls to a sampler only when it is the sole admissible option (non-DNF
860// relative / additive), where it is picked regardless of cost. Net ordering: a
861// cheap exact method (independent ~5e-5*S, a tiny-m sieve) underbids the
862// estimators so the path returns EXACT; a DNF approximation goes to the d-tree;
863// only a hard non-DNF approximation reaches the samplers.
864static const double kCostMonteCarlo = 1e-5; // additive: ~1e-5 * S * C (p-independent, the clean one)
865static const double kCostStoppingRule = 1e-3; // relative univ: ~1e-3 * S * C (pessimistic: covers the 1/p rare-event blow-up)
866static const double kCostKarpLuby = 3e-6; // relative DNF: ~3e-6 * S * m * C (pessimistic: covers the p-dependent slow case)
867static const double kCostDTreeExact = 3e-4; // d-tree exact: ~3e-4 * S * m (memoised Shannon; pessimistic vs tree-decomp on low tw)
868static const double kCostDTreeApprox = 4e-4; // d-tree approx: ~4e-4 * S / eps, DELTA-INDEPENDENT (deterministic -> overtakes samplers as delta shrinks)
869// Speculative-execution budget conversion: ms per d-tree subproblem (recursion
870// entry). The chooser's budget is in ms (the next-best method's cost); the
871// d-tree counts subproblems, so budget_steps = budget_ms / (ms per subproblem).
872// The two recursions have different per-step cost (calibrated on the bench): the
873// monotone-DNF clause path pays an O(m^2) subsumption sweep per node (~1.4e-3
874// ms/step), the general circuit path only a footprint componentise + pivot scan
875// (~5e-4 ms/step). Using the right one keeps the budget honest -- a single
876// (smaller) constant under-charged the DNF path and let it run well past the
877// fallback's cost instead of bailing.
878static const double kCostDTreeMsPerStepDnf = 1.4e-3;
879static const double kCostDTreeMsPerStepGeneral = 5e-4;
880
881/// 2^k with the exponent clamped to keep the cost finite (a clamped exponent
882/// still sorts the method dead last -- it is then a guaranteed fall-through).
883static double pow2_clamped(size_t k)
884{
885 return std::ldexp(1.0, static_cast<int>(std::min<size_t>(k, 60)));
886}
887
888/// Per-evaluation circuit state threaded to a method's evaluate(). The Boolean
889/// view @c c is built once in probability_evaluate_internal; methods that need
890/// the multivalued rewrite trigger it (idempotently) through this context, so
891/// the rewrite fires exactly for the methods that need it.
893 // Generic-circuit state is needed only by the methods that consult the
894 // original circuit (inversion-free, stopping-rule); the d-D-construction
895 // portfolio (chooseAndBuildDD / makeDDAuto) works off the Boolean view alone,
896 // so these are pointers a Boolean-only caller can leave null.
902 std::unordered_map<gate_t, gate_t> *gc_to_bc;
904 const std::string &args;
905 bool explicitly_named; ///< invoked via byName (vs the default chain)
906 size_t n_inputs = 0; ///< input count N (O(1) cost feature)
907 size_t circuit_size = 0; ///< gate count S, the circuit-size parameter (O(1))
908 /// Speculative-execution budget: the estimated cost (in the chooser's ms-ish
909 /// units) of the next-cheapest admissible method. A budget-aware method
910 /// (currently the d-tree) runs until its own work exceeds this and then throws,
911 /// so the chooser drops it and escalates -- bounding wasted work at ~the cost
912 /// of the safe fallback. Infinity = no budget (the method is the last resort,
913 /// or budgeting is off).
914 double cost_budget = std::numeric_limits<double>::infinity();
916 std::string actual_method;
917
920 c.rewriteMultivaluedGates();
922 }
923 }
924
925 // Cached DNF-shape feature: just (is-DNF, clause count) via the cheap
926 // dnfShapeInfo -- O(circuit), NO per-clause supports. The chooser ranks sieve
927 // from this; the supports (potentially O(m*N)) are built only if sieve /
928 // karp-luby actually runs, inside their evaluate().
929 mutable bool dnf_computed_ = false;
930 mutable bool dnf_ok_ = false;
931 mutable std::size_t dnf_num_clauses_ = 0;
932
933 void ensureDnfShape() const {
934 if(!dnf_computed_) {
935 dnf_ok_ = c.dnfShapeInfo(gate, dnf_num_clauses_);
936 dnf_computed_ = true;
937 }
938 }
939
940 // Cached treewidth proxy: a cheap degeneracy lower bound and the max degree
941 // (both from one O(V+E) pass; see TreeDecomposition::degeneracyLowerBound),
942 // computed once when the chooser is about to consider tree-decomposition.
943 mutable bool tw_computed_ = false;
944 mutable unsigned tw_proxy_ = 0;
945 mutable unsigned tw_max_degree_ = 0;
946
953
954 // --- Feature framework (see ProbabilityMethod.h) ---------------------------
955 // The chooser acquires non-free features lazily; these model their cost and
956 // perform the acquisition.
957
958 /// Heuristic acquisition cost of @p f, in the same work units as a method's
959 /// estimatedCost (so the chooser can compare "run this method" against
960 /// "acquire this feature").
961 double featureCost(Feature f) const {
962 switch(f) {
964 return kCostDnfShapeFeature * static_cast<double>(circuit_size); // O(S)
966 return kCostTwProxyFeature * static_cast<double>(circuit_size); // O(S)
967 }
968 return 0.;
969 }
970
971 bool hasFeature(Feature f) const {
972 switch(f) {
973 case Feature::DnfShape: return dnf_computed_;
975 }
976 return true;
977 }
978
980 switch(f) {
981 case Feature::DnfShape: ensureDnfShape(); break;
983 }
984 }
985};
986
987namespace {
988
989/// The planner-time route that produced @p root's circuit, read back from the
990/// route tag the route stamped there (see @c provsql_route). Three rewrites --
991/// the safe-query read-once rewriter, the joint-width UCQ compiler and the
992/// reachability compiler -- replace a query's ordinary lineage with a circuit
993/// of their own, which they then hand to this dispatcher; the tag is what tells
994/// them apart here, since all three are evaluated by the same
995/// @c independentEvaluation sweep and would otherwise all report 'independent'.
996///
997/// Transparent @c gate_annotation wrappers (inversion-free certificate / order
998/// keys) are skipped: they sit *above* the route's root. A tag is only read
999/// off the route's own root gate -- a circuit that combines route output with
1000/// further provenance is no longer that route's circuit and reports as the
1001/// ordinary evaluation it is.
1002provsql_route rootRoute(const GenericCircuit *gc, gate_t root)
1003{
1004 if(gc == nullptr)
1005 return PROVSQL_ROUTE_NONE;
1006
1007 gate_t g = root;
1008 // Bounded walk: transparent annotation wrappers never nest deeply, and the
1009 // bound keeps a malformed circuit from looping here. A wrapper whose text
1010 // names a route ("route:<name>") is the route's own marker on the root it
1011 // produced (see wrapRoute in CertifiedDDMaterialize.cpp).
1012 for(unsigned i = 0; i < 8; ++i) {
1013 if(gc->getGateType(g) != gate_annotation || gc->getWires(g).size() != 1)
1014 break;
1015 const std::string text = gc->getExtra(g);
1016 if(text.rfind("route:", 0) == 0) {
1017 const std::string name = text.substr(6);
1020 if(name == provsql_route_name(r))
1021 return r;
1022 return PROVSQL_ROUTE_NONE;
1023 }
1024 g = gc->getWires(g)[0];
1025 }
1026
1027 // The tag on the gate itself: info1 of a route's gate_assumed wrapper, or,
1028 // for circuits stored before the route went on an annotation wrapper,
1029 // info2 of the materialised root (info1 being its d-D certificate).
1030 const auto [info1, info2] = gc->getInfos(g);
1031 const unsigned tag =
1032 gc->getGateType(g) == gate_assumed ? info1
1033 : (info1 == DNNF_CERT_INFO ? info2 : 0u);
1034
1035 switch(tag) {
1039 default: return PROVSQL_ROUTE_NONE;
1040 }
1041}
1042
1043/// Exact, decomposition of disconnected circuits. Throws when the circuit is
1044/// not independent, which the default ladder catches to fall through.
1045class IndependentMethod : public ProbabilityMethod {
1046public:
1047 std::string name() const override { return "independent"; }
1048 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1049 bool inDefaultChain() const override { return true; }
1050 // Interprets a mulinput (BID) block natively -- summing the mutually-exclusive
1051 // alternatives -- so it must see the raw circuit, not the Boolean rewrite.
1052 bool handlesMultivalued() const override { return true; }
1053 // O(S): one memoised linear pass over the circuit.
1054 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1055 return kCostIndependent * static_cast<double>(ctx.circuit_size);
1056 }
1057 // A route-tagged root is served by that route's own method (same sweep,
1058 // reported under the route's name), so the four stay mutually exclusive in
1059 // the chooser instead of racing on an identical cost. byName ignores this,
1060 // keeping explicit 'independent' available on any circuit as the escape
1061 // hatch that names the computation rather than its producer.
1062 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1063 return rootRoute(ctx.gc, ctx.gc_root) == PROVSQL_ROUTE_NONE;
1064 }
1065 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1066 double r = ctx.c.independentEvaluation(ctx.gate);
1067 ctx.actual_method = "independent";
1068 return r;
1069 }
1070};
1071
1072/**
1073 * @brief Shared base of the three planner-time route methods.
1074 *
1075 * Circuit production and evaluation are separate steps -- the route runs in the
1076 * planner, the evaluation here -- so the circuit itself is the channel between
1077 * them: the route stamps its tag on the root it produces (@c provsql_route) and
1078 * @c applicable() reads it back. This is the @c mobius / @c inversion-free
1079 * pattern: a first-class, by-name-invocable catalog method gated on a feature
1080 * of the provenance root.
1081 *
1082 * All three evaluate by @c independentEvaluation, which is not one algorithm
1083 * but two: a plain read-once sweep for the safe-query rewriter's circuit, and
1084 * the certified-island sweep (@c BooleanCircuit::evaluateCertifiedIsland, over
1085 * gates carrying @c DNNF_CERT_INFO) for the two compiled d-Ds. What the method
1086 * name adds over @c independent is which of the three produced the circuit.
1087 */
1088class RouteMethod : public ProbabilityMethod {
1089public:
1090 RouteMethod(provsql_route route, std::string name, std::string requirement)
1091 : route_(route), name_(std::move(name)),
1092 requirement_(std::move(requirement)) {}
1093
1094 std::string name() const override { return name_; }
1095 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1096 bool inDefaultChain() const override { return true; }
1097 // Same raw-circuit requirement as 'independent': a route's d-D can carry a
1098 // mulinput (BID) block the rewrite would dissolve.
1099 bool handlesMultivalued() const override { return true; }
1100 // O(S), exactly 'independent' -- it IS that sweep.
1101 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1102 return kCostIndependent * static_cast<double>(ctx.circuit_size);
1103 }
1104 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1105 return rootRoute(ctx.gc, ctx.gc_root) == route_;
1106 }
1107 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1108 // Named explicitly on an untagged root: report the missing precondition
1109 // rather than silently evaluating as something else (as 'mobius' and
1110 // 'inversion-free' do).
1111 if(ctx.explicitly_named && rootRoute(ctx.gc, ctx.gc_root) != route_)
1112 provsql_error("method '%s' requires %s", name_.c_str(),
1113 requirement_.c_str());
1114 double r = ctx.c.independentEvaluation(ctx.gate);
1115 ctx.actual_method = name_;
1116 return r;
1117 }
1118
1119private:
1120 const provsql_route route_;
1121 const std::string name_;
1122 const std::string requirement_;
1123};
1124
1125/// Exact, structured d-DNNF over an inversion-free certificate.
1126class InversionFreeMethod : public ProbabilityMethod {
1127public:
1128 std::string name() const override { return "inversion-free"; }
1129 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1130 bool inDefaultChain() const override { return true; }
1131 // O(S + N log N): linear structured-d-DNNF build + sorting the per-input keys.
1132 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1133 const double N = static_cast<double>(ctx.n_inputs);
1134 return kCostInversionFree
1135 * (static_cast<double>(ctx.circuit_size) + N * std::log2(N < 2 ? 2. : N));
1136 }
1137 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1138 // In the default ladder: only when the certificate is present and the
1139 // kill-switch is on. byName ignores applicable() and enforces the explicit
1140 // rules (hard errors) in evaluate().
1141 return ctx.inv_free_cert && provsql_inversion_free;
1142 }
1143 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1144 if(ctx.explicitly_named && !ctx.inv_free_cert)
1145 provsql_error("method 'inversion-free' requires an inversion-free "
1146 "certificate on the provenance root (none is attached "
1147 "to the rows of a join that merges no rows and uses no "
1148 "relation twice: 'independent' applies to them)");
1149 std::map<gate_t, StructuredDNNFBuilder::InputKey> keys;
1150 if(!collect_inversion_free_keys(*ctx.gc, ctx.gc_root, *ctx.gc_to_bc, ctx.c,
1151 ctx.gate, keys)) {
1152 if(ctx.explicitly_named)
1153 provsql_error("method 'inversion-free': the provenance root carries a "
1154 "certificate but its inputs lack per-input order markers");
1155 // Default-ladder mode: fall through to the next method.
1156 throw CircuitException("inversion-free: inputs lack per-input order "
1157 "markers");
1158 }
1159 double r = StructuredDNNFBuilder(ctx.c, ctx.gate, inversion_free_rank(keys))
1160 .probability();
1161 ctx.actual_method = "inversion-free";
1162 return r;
1163 }
1164};
1165
1166/// The safe-UCQ Möbius-inversion route's evaluation method. Modelled on
1167/// 'inversion-free' -- a first-class, by-name-invocable catalog method in the
1168/// default chain, gated by a feature of the provenance root -- rather than a
1169/// terminal special-case. A @c gate_mobius root is a signed combination
1170/// @f$\sum_i c_i\,P(\text{child}_i)@f$ over certified-independent islands,
1171/// evaluated by a single linear sweep (@c mobiusProbabilityImpl). Because that
1172/// root is not a Boolean gate the circuit never becomes a @c BooleanCircuit, so
1173/// the dispatcher routes a @c gate_mobius-rooted token straight here (see
1174/// @c probability_evaluate_internal); @c applicable() also keeps it out of the
1175/// ordinary chain for Boolean circuits.
1176class MobiusMethod : public ProbabilityMethod {
1177public:
1178 std::string name() const override { return "mobius"; }
1179 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1180 bool inDefaultChain() const override { return true; }
1181 // Linear sweep over the certified-independent islands (the per-element
1182 // probabilities are read-once); same order as 'independent'.
1183 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1184 return kCostIndependent * static_cast<double>(ctx.circuit_size);
1185 }
1186 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1187 return ctx.gc != nullptr
1188 && ctx.gc->getGateType(ctx.gc_root) == gate_mobius;
1189 }
1190 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1191 if(ctx.gc == nullptr || ctx.gc->getGateType(ctx.gc_root) != gate_mobius)
1192 provsql_error("method 'mobius' requires a Möbius-route token (a "
1193 "gate_mobius signed-combination root)");
1194 ctx.actual_method = "mobius";
1195 return mobiusProbabilityImpl(ctx.token);
1196 }
1197};
1198
1199// makeDD's internal interpret-as-dd -> tree-decomposition -> compiler ladder is
1200// lifted here into three first-class catalog members, so the chooser can see
1201// and rank the three most cost-divergent exact compilers (linear / treewidth-
1202// bounded / external subprocess) instead of one opaque "compilation" blob, and
1203// last_eval_method reports the route actually taken. makeDD / makeDDByName stay
1204// for their dD-artifact callers (shapley, compile_to_ddnnf, ddnnf_stats).
1205
1206/// Exact, interpret the circuit directly as a d-DNNF and read off the
1207/// probability. By-name only -- deliberately NOT in the default chain: for a
1208/// probability *number* this is redundant with (indeed strictly weaker than)
1209/// 'independent'. interpretAsDD treats OR as independent-OR (De Morgan over a
1210/// decomposable AND), AND as a product, and throws "Not an independent circuit"
1211/// on a shared input -- exactly independentEvaluation's computation -- while
1212/// also rejecting the multivalued inputs and 0/1 constants that 'independent'
1213/// accepts. Since 'independent' runs first in the chain it always wins, so this
1214/// would be dead there. Kept as an explicit method for parity with the
1215/// dD-artifact surfaces / debugging.
1216class InterpretAsDdMethod : public ProbabilityMethod {
1217public:
1218 std::string name() const override { return "interpret-as-dd"; }
1219 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1220 bool producesDD() const override { return true; }
1221 // O(S), the same cost as 'independent' -- for a probability *number* this IS
1222 // independentEvaluation (interpretAsDD treats OR as independent-OR, AND as a
1223 // product, throws on a shared input), which is why it stays out of the
1224 // probability default chain. In the d-D portfolio it is the cheapest route
1225 // and the artifact-producing twin of 'independent' (which yields no d-DNNF),
1226 // so it must carry this cost to be ranked first by chooseAndBuildDD.
1227 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1228 return kCostIndependent * static_cast<double>(ctx.circuit_size);
1229 }
1230 dDNNF buildDD(EvalContext &ctx) const override {
1231 dDNNF dd = ctx.c.interpretAsDD(ctx.gate);
1232 ctx.actual_method = "interpret-as-dd";
1233 return dd;
1234 }
1235 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1236 return buildDD(ctx).probabilityEvaluation();
1237 }
1238};
1239
1240/// Exact d-DNNF via min-fill tree decomposition. Default-chain member (after
1241/// inversion-free) and by-name "tree-decomposition". Throws above the treewidth
1242/// bound: in the chain that falls through to compilation; an explicit call
1243/// errors with the treewidth message (mirroring makeDD).
1244class TreeDecompositionMethod : public ProbabilityMethod {
1245public:
1246 std::string name() const override { return "tree-decomposition"; }
1247 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1248 bool inDefaultChain() const override { return true; }
1249 bool producesDD() const override { return true; }
1250 // Cost/applicability are gated by a cheap degeneracy lower bound on the
1251 // treewidth: if it already exceeds the build's MAX_TREEWIDTH, the bounded
1252 // min-fill build would certainly fail, so the method is ruled out (skipped
1253 // before the costly attempt). Otherwise the d-DNNF cost is exponential in the
1254 // treewidth; tw_proxy_ is a lower bound, so 2^tw_proxy * n is an optimistic
1255 // lower bound on the real cost (the build can still fail if the true treewidth
1256 // turns out above the bound -- the implicit half of the feature).
1257 std::vector<Feature> requiredFeatures() const override {
1258 return {Feature::TreewidthProxy};
1259 }
1260 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1261 return ctx.tw_proxy_ <= static_cast<unsigned>(TreeDecomposition::MAX_TREEWIDTH);
1262 }
1263 // O(S * 2^w): the d-DNNF is exponential in the treewidth (lower-bounded by the
1264 // degeneracy proxy w), and the min-fill build is poly and bounded by the S
1265 // factor. NB an earlier model multiplied in the max degree Delta^2 to charge
1266 // the build's per-step fill-in -- but for a DNF the root OR's fan-in IS the
1267 // clause count, so Delta^2 exploded (a 300-clause DNF -> Delta=300 -> cost
1268 // ~90000x too high) and the chooser fled a 7 ms tree-decomposition for a
1269 // 1900 ms compilation. The build is fast even at high fan-in (measured), so
1270 // Delta is dropped; 2^w (capped at the MAX_TREEWIDTH applicability bound) is
1271 // the real cost driver.
1272 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1273 return kCostTreeDecomp * static_cast<double>(ctx.circuit_size)
1274 * pow2_clamped(ctx.tw_proxy_);
1275 }
1276 dDNNF buildDD(EvalContext &ctx) const override {
1277 try {
1278 TreeDecomposition td(ctx.c);
1279 // Speculative execution: the (poly) min-fill build has now discovered the
1280 // EXACT treewidth, where the cost estimate above used only the degeneracy
1281 // LOWER bound (which under-costs). Before paying the exponential d-DNNF
1282 // build, recompute the real cost from the discovered width; if it exceeds
1283 // the next-best method's cost, bail so the chooser escalates -- the
1284 // build's own MAX_TREEWIDTH cap is the hard ceiling, this is the
1285 // competitive refinement. A by-name call runs unbounded.
1286 if(!ctx.explicitly_named && std::isfinite(ctx.cost_budget)) {
1287 const double real_cost = kCostTreeDecomp
1288 * static_cast<double>(ctx.circuit_size)
1289 * pow2_clamped(td.getTreewidth());
1290 if(real_cost > ctx.cost_budget)
1291 throw CircuitException(
1292 "tree-decomposition: discovered treewidth exceeds the budget");
1293 }
1294 dDNNF dd = dDNNFTreeDecompositionBuilder{ctx.c, ctx.gate, td}.build();
1295 ctx.actual_method = "tree-decomposition";
1296 return dd;
1297 } catch(TreeDecompositionException &) {
1298 if(ctx.explicitly_named)
1299 provsql_error("Treewidth greater than %u",
1301 // Default chain: fall through to the compilation terminal.
1302 throw CircuitException("tree-decomposition: treewidth above the bound");
1303 }
1304 }
1305 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1306 return buildDD(ctx).probabilityEvaluation();
1307 }
1308};
1309
1310/// Exact d-DNNF via an external knowledge compiler (d4 / c2d / minic2d / dsharp,
1311/// or a KCMCP server). Default-chain terminal (after tree-decomposition) and
1312/// by-name "compilation". An empty compiler argument auto-selects the
1313/// highest-preference available compiler (provsql.fallback_compiler / registry);
1314/// a non-empty @c args names the compiler and its options.
1315class CompilationMethod : public ProbabilityMethod {
1316public:
1317 std::string name() const override { return "compilation"; }
1318 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1319 bool inDefaultChain() const override { return true; }
1320 bool producesDD() const override { return true; }
1321 // Subprocess: the compilers exploit structure, so the typical cost is the
1322 // d-DNNF compile (~linear in the serialized circuit) plus a fixed startup, not
1323 // the 2^N worst case. Modelled as max(startup_floor, slope * S) ms. (It is
1324 // still the last resort: cheaper in-process methods, when they apply, undercut
1325 // it; when none does, it is the only candidate and runs regardless.)
1326 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1327 return std::max(kCostCompilationFloor,
1328 kCostCompilation * static_cast<double>(ctx.circuit_size));
1329 }
1330 dDNNF buildDD(EvalContext &ctx) const override {
1331 // On a chooser path (exact / relative / additive) ctx.args carries the
1332 // path's TOLERANCE string (epsilon=...,delta=...), not a compiler name, so
1333 // auto-select the compiler. Only a by-name 'compilation' call passes an
1334 // explicit compiler in ctx.args. (Without this, a relative/additive request
1335 // makes compilation try to use "epsilon=...,delta=..." as a compiler name,
1336 // which throws -- silently dropping compilation from the chooser and sending
1337 // the request to a worse method.)
1338 const std::string compiler =
1339 ctx.explicitly_named ? ctx.args : std::string();
1340 std::string used;
1341 dDNNF dd = ctx.c.compilation(ctx.gate, compiler, &used);
1342 // Report WHICH compiler ran (e.g. "compilation:d4"), not just "compilation":
1343 // on a chooser path the tool is auto-selected, so the bare label hid it.
1344 ctx.actual_method = used.empty() ? "compilation" : "compilation:" + used;
1345 return dd;
1346 }
1347 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1348 return buildDD(ctx).probabilityEvaluation();
1349 }
1350};
1351
1352/// Exact, naive 2^N enumeration. In the default chain for small circuits only
1353/// (cheap exact, preferred over tree-decomposition / compilation when N is
1354/// small); always available by name.
1355class PossibleWorldsMethod : public ProbabilityMethod {
1356public:
1357 std::string name() const override { return "possible-worlds"; }
1358 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1359 bool inDefaultChain() const override { return true; }
1360 // O(S * 2^N): enumerate 2^N worlds, evaluate the circuit (O(S)) in each.
1361 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1362 return kCostPossibleWorlds * static_cast<double>(ctx.circuit_size)
1363 * pow2_clamped(ctx.n_inputs);
1364 }
1365 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1366 return ctx.n_inputs > 0 && ctx.n_inputs <= kPossibleWorldsSanityMax;
1367 }
1368 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1369 // Only flag ignored args for an EXPLICIT `possible-worlds` request; when the
1370 // chooser auto-picks it on a relative/additive path, the args carry the path's
1371 // (eps,delta) tolerance, which an exact method legitimately ignores.
1372 if(ctx.explicitly_named && !ctx.args.empty())
1373 provsql_warning("Argument '%s' ignored for method possible-worlds",
1374 ctx.args.c_str());
1375 double r = ctx.c.possibleWorlds(ctx.gate);
1376 ctx.actual_method = "possible-worlds";
1377 return r;
1378 }
1379};
1380
1381/// Additive Monte Carlo (the non-RV path; the RV path is handled directly on
1382/// the GenericCircuit in probability_evaluate_internal before the catalog).
1383class MonteCarloMethod : public ProbabilityMethod {
1384public:
1385 std::string name() const override { return "monte-carlo"; }
1386 ToleranceKind guaranteeKind() const override { return ToleranceKind::Additive; }
1387 // Additive portfolio member: the universal fixed-sample estimator on the Boolean
1388 // view, serving the 'additive' path (and any-name).
1389 bool inDefaultChain() const override { return true; }
1390 bool isDeterministic() const override { return false; } // (eps,delta) sampler
1391 // O(S / eps^2 * ln(1/delta)) -- Hoeffding, p-independent.
1392 double estimatedCost(const EvalContext &ctx, const Tolerance &tol) const override {
1393 if(tol.epsilon <= 0.) return std::numeric_limits<double>::infinity();
1394 return kCostMonteCarlo * static_cast<double>(ctx.circuit_size)
1395 * std::log(2.0 / tol.delta)
1396 / (tol.epsilon * tol.epsilon);
1397 }
1398 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1399 unsigned long samples = monte_carlo_samples(parse_method_args(ctx.args));
1400 double r = ctx.c.monteCarlo(ctx.gate, static_cast<unsigned>(samples));
1401 ctx.actual_method = "monte-carlo";
1402 return r;
1403 }
1404};
1405
1406/// Karp-Luby FPRAS over a DNF-shaped monotone circuit. Relative portfolio member
1407/// (chosen on DNFs the cheap exact methods do not resolve) and by-name; runs before
1408/// the multivalued rewrite (it rejects multivalued inputs anyway).
1409class KarpLubyMethod : public ProbabilityMethod {
1410public:
1411 std::string name() const override { return "karp-luby"; }
1412 ToleranceKind guaranteeKind() const override { return ToleranceKind::Relative; }
1413 bool inDefaultChain() const override { return true; }
1414 bool isDeterministic() const override { return false; } // (eps,delta) sampler
1415 // Cost / applicability need the DNF-shape feature; the chooser acquires it
1416 // (lazily) before calling them, so a read-once circuit never pays the walk.
1417 std::vector<Feature> requiredFeatures() const override {
1418 return {Feature::DnfShape};
1419 }
1420 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1421 return ctx.dnf_ok_;
1422 }
1423 // O(S*m / eps^2 * ln(1/delta)) -- relative, p-independent (the m clauses
1424 // replace the 1/p of plain MC).
1425 double estimatedCost(const EvalContext &ctx, const Tolerance &tol) const override {
1426 if(tol.epsilon <= 0.) return std::numeric_limits<double>::infinity();
1427 const double m = static_cast<double>(ctx.dnf_num_clauses_ > 0
1428 ? ctx.dnf_num_clauses_ : 1);
1429 return kCostKarpLuby * static_cast<double>(ctx.circuit_size) * m
1430 * std::log(2.0 / tol.delta)
1431 / (tol.epsilon * tol.epsilon);
1432 }
1433 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1434 std::vector<gate_t> clauses;
1435 std::vector<std::set<gate_t> > supports;
1436 if(!ctx.c.dnfShape(ctx.gate, clauses, supports)) {
1437 provsql_warning("method 'karp-luby' applies only to a DNF-shaped circuit "
1438 "(a monotone OR-of-ANDs over input leaves); negation, "
1439 "comparison, aggregation, random-variable and "
1440 "multivalued-input gates are not supported");
1441 provsql_error("method 'karp-luby' requires a DNF-shaped provenance "
1442 "circuit");
1443 }
1444 double r = evaluate_karp_luby(ctx.c, clauses, supports,
1445 parse_method_args(ctx.args));
1446 ctx.actual_method = "karp-luby";
1447 return r;
1448 }
1449};
1450
1451/// Whole-circuit (eps,delta)-RELATIVE estimate via the Dagum-Karp-Luby-Ross stopping
1452/// rule -- the universal relative fallback (plain Boolean / RV / HAVING agg alike).
1453/// Relative portfolio member and by-name ('stopping-rule'). Operates on the
1454/// GenericCircuit (ctx.gc / ctx.gc_root), so it applies to every circuit regardless
1455/// of whether the Boolean view built; delegates to run_stopping_rule for the
1456/// max_samples cap, the relative->additive degradation, and the guarantee NOTICE.
1457class StoppingRuleMethod : public ProbabilityMethod {
1458public:
1459 std::string name() const override { return "stopping-rule"; }
1460 ToleranceKind guaranteeKind() const override { return ToleranceKind::Relative; }
1461 bool inDefaultChain() const override { return true; }
1462 bool isDeterministic() const override { return false; } // (eps,delta) sampler
1463 // O(S / (p*eps^2) * ln(1/delta)); p is not a static feature, modelled
1464 // optimistically at p ~ 1 (see the kCost block). Above karp-luby on DNFs and
1465 // above plain MC, but below the cheap exact methods so "exact when cheaper" wins.
1466 double estimatedCost(const EvalContext &ctx, const Tolerance &tol) const override {
1467 if(tol.epsilon <= 0.) return std::numeric_limits<double>::infinity();
1468 return kCostStoppingRule * static_cast<double>(ctx.circuit_size)
1469 * std::log(2.0 / tol.delta)
1470 / (tol.epsilon * tol.epsilon);
1471 }
1472 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1473 double r = 0.;
1474 run_stopping_rule(*ctx.gc, ctx.gc_root, parse_method_args(ctx.args), r,
1475 ctx.actual_method);
1476 return r;
1477 }
1478};
1479
1480/// Exact inclusion-exclusion over a monotone DNF. Portfolio member (runs before
1481/// the multivalued rewrite, like karp-luby; dnfShape rejects multivalued inputs)
1482/// and by-name. Work-weighted cost N*2^m in the clause count m: the chooser
1483/// picks it over possible-worlds when there are fewer clauses than inputs
1484/// (m < N), and over the compilers when m is small -- yet it stays behind
1485/// linear-exact 'independent' on a read-once DNF.
1486class SieveMethod : public ProbabilityMethod {
1487public:
1488 std::string name() const override { return "sieve"; }
1489 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1490 bool inDefaultChain() const override { return true; }
1491 // Cost / applicability need the DNF-shape feature; the chooser acquires it
1492 // (lazily) before calling them, so a read-once circuit never pays the walk.
1493 std::vector<Feature> requiredFeatures() const override {
1494 return {Feature::DnfShape};
1495 }
1496 bool applicable(const EvalContext &ctx, const Tolerance &) const override {
1497 return ctx.dnf_ok_ && ctx.dnf_num_clauses_ <= kSieveSanityMaxClauses;
1498 }
1499 // O(S * 2^m): inclusion-exclusion over 2^m clause subsets, each a product over
1500 // the union of supports (bounded by the circuit).
1501 double estimatedCost(const EvalContext &ctx, const Tolerance &) const override {
1502 if(!ctx.dnf_ok_)
1503 return std::numeric_limits<double>::infinity();
1504 return kCostSieve * static_cast<double>(ctx.circuit_size)
1505 * pow2_clamped(ctx.dnf_num_clauses_);
1506 }
1507 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1508 // Auto-chosen on a relative/additive path, the args are the path's (eps,delta)
1509 // tolerance, legitimately ignored by an exact method -- only warn when sieve was
1510 // requested explicitly.
1511 if(ctx.explicitly_named && !ctx.args.empty())
1512 provsql_warning("Argument '%s' ignored for method sieve",
1513 ctx.args.c_str());
1514 // Build the full clauses + supports now (only paid because sieve was
1515 // chosen); the cheap feature only validated the shape and counted clauses.
1516 std::vector<gate_t> clauses;
1517 std::vector<std::set<gate_t> > supports;
1518 if(!ctx.c.dnfShape(ctx.gate, clauses, supports)) {
1519 provsql_warning("method 'sieve' applies only to a DNF-shaped circuit "
1520 "(a monotone OR-of-ANDs over input leaves); negation, "
1521 "comparison, aggregation, random-variable and "
1522 "multivalued-input gates are not supported");
1523 provsql_error("method 'sieve' requires a DNF-shaped provenance circuit");
1524 }
1525 double r = ctx.c.sieve(clauses, supports);
1526 ctx.actual_method = "sieve";
1527 return r;
1528 }
1529};
1530
1531/// d-tree: deterministic anytime interval bounds for a monotone DNF (Olteanu-
1532/// Huang). Refines the cheap leaf bound by independent-or decomposition and
1533/// Shannon expansion until the tolerance is met (exact when run to a zero-width
1534/// interval), returning a certified interval -- no failure probability. Phase
1535/// 1: reachable by name only (inDefaultChain() == false), so it does not yet
1536/// perturb the calibrated auto-chooser; toleranceAdmits still lets an explicit
1537/// 'd-tree' serve the exact, relative and additive paths.
1538class DTreeBoundsMethod : public ProbabilityMethod {
1539public:
1540 std::string name() const override { return "d-tree"; }
1541 ToleranceKind guaranteeKind() const override { return ToleranceKind::Exact; }
1542 bool inDefaultChain() const override { return true; }
1543 // isDeterministic() defaults to true: the certified interval carries no
1544 // failure probability. That is the d-tree's reason for being in the chain --
1545 // it is the ONLY non-exact method admissible for a delta == 0 request, and its
1546 // cost is delta-INDEPENDENT, so it overtakes the (eps,delta) samplers as delta
1547 // shrinks. On low treewidth the exact compilers still win on cost; the d-tree
1548 // is auto-selected for deterministic / low-delta approximation and for exact
1549 // where the treewidth exceeds tree-decomposition's cap.
1550 std::vector<Feature> requiredFeatures() const override {
1551 // DnfShape selects the optimised monotone-DNF clause path and supplies the
1552 // clause count for the exact cost; a non-DNF circuit uses the general
1553 // circuit recursion (dtreeBoundsCircuit), so the method is applicable either
1554 // way -- the feature is a hint, not a gate.
1555 return {Feature::DnfShape};
1556 }
1557 bool applicable(const EvalContext &, const Tolerance &) const override {
1558 // Applies to any Boolean circuit. A multivalued (BID) circuit is handled
1559 // too: handlesMultivalued() is false, so the dispatcher rewrites the blocks
1560 // to independent Booleans before evaluate() and the general recursion never
1561 // meets a mulinput (the throw in footprintOf is now only a defensive net).
1562 return true;
1563 }
1564 double estimatedCost(const EvalContext &ctx, const Tolerance &tol) const override {
1565 const double S = static_cast<double>(ctx.circuit_size);
1566 // Approximate (DNF or general circuit): the anytime early stop caps the work
1567 // (and it is delta-independent); grows as eps tightens. This is the d-tree's
1568 // edge on a non-DNF circuit -- it returns certified bounds where the exact
1569 // compilers would do full work. NB the treewidth proxy is NOT used -- it
1570 // mispredicts this engine (cliques collapse fast under Shannon + subsumption,
1571 // low-w cycles do not).
1572 if(tol.kind != ToleranceKind::Exact && tol.epsilon > 0.)
1573 return kCostDTreeApprox * S / tol.epsilon;
1574 // Exact: memoised Shannon compilation, ~S*m. Pessimistic vs tree-
1575 // decomposition (tighter constant) on low treewidth, so it is picked for
1576 // exact only where tree-decomposition bails (treewidth above its cap). Only
1577 // the monotone-DNF fast path competes for exact auto-selection; a non-DNF
1578 // exact request leaves the well-understood compilers (tree-decomposition /
1579 // d4 / possible-worlds) to choose, with the general recursion reachable
1580 // by-name -- generalising the *shape* of the bounds engine without retuning
1581 // the exact cost model (a separate item).
1582 if(!ctx.dnf_ok_)
1583 return std::numeric_limits<double>::infinity();
1584 const double m = static_cast<double>(ctx.dnf_num_clauses_ > 0
1585 ? ctx.dnf_num_clauses_ : 1);
1586 return kCostDTreeExact * S * m;
1587 }
1588 double evaluate(EvalContext &ctx, const Tolerance &tol) const override {
1589 // Monotone-DNF circuits take the optimised clause path; everything else
1590 // (negation / EXCEPT, nested AND/OR, arbitrary sharing) takes the general
1591 // circuit recursion. Both are the same Olteanu-Huang-Koch anytime engine.
1592 std::vector<gate_t> clause_roots;
1593 std::vector<std::set<gate_t> > supports;
1594 const bool is_dnf = ctx.c.dnfShape(ctx.gate, clause_roots, supports);
1595 ctx.actual_method = "d-tree";
1596
1597 // Effective tolerance: a relative/additive PATH supplies it via `tol`; an
1598 // explicit by-name 'd-tree' is exact, unless an epsilon arg is given, which
1599 // is read as an additive interval half-width target.
1600 ToleranceKind kind = tol.kind;
1601 double eps = tol.epsilon;
1602 if(kind == ToleranceKind::Exact) {
1603 MethodArgs a = parse_method_args(ctx.args);
1604 if(a.has("epsilon")) {
1605 double dummy_delta = 0.;
1606 parse_eps_delta(a, "d-tree", eps, dummy_delta); // validates eps in (0,1]
1608 }
1609 }
1610
1611 // Tolerance -> absolute interval-width target for the recursion.
1612 double max_width;
1613 if(kind == ToleranceKind::Additive && eps > 0.) {
1614 // Additive eps: half-width <= eps means |est - p| <= eps.
1615 max_width = 2. * eps;
1616 } else if(kind == ToleranceKind::Relative && eps > 0.) {
1617 // Relative eps: with p >= L (the cheap lower bound), a half-width <=
1618 // eps*L gives |est - p| <= eps*L <= eps*p. The cheap lower bound is
1619 // dnfBounds on the DNF path; on the general path it is the leaf bound the
1620 // recursion returns for a trivially-wide target.
1621 double l0;
1622 if(is_dnf) {
1623 double u0;
1624 ctx.c.dnfBounds(supports, l0, u0);
1625 } else {
1626 l0 = provsql::dtreeBoundsCircuit(ctx.c, ctx.gate, 1.0).lower;
1627 }
1628 max_width = 2. * eps * l0;
1629 } else {
1630 max_width = 0.; // exact
1631 }
1632
1633 // Speculative-execution budget: convert the chooser's ms budget (the
1634 // next-best method's cost) into a subproblem cap, so the d-tree bails (throws,
1635 // chooser escalates) rather than blowing up on a high-treewidth circuit its
1636 // cheap-feature cost estimate mis-rated. An explicit by-name call runs
1637 // unbounded (it is the user's deliberate choice, with no chooser fallback);
1638 // the debug GUC provsql.dtree_max_subproblems imposes an extra hard cap.
1639 unsigned long budget_steps = 0; // 0 = unbounded
1640 if(!ctx.explicitly_named && std::isfinite(ctx.cost_budget)) {
1641 const double ms_per_step = is_dnf ? kCostDTreeMsPerStepDnf
1643 budget_steps = static_cast<unsigned long>(
1644 std::max(1.0, ctx.cost_budget / ms_per_step));
1645 }
1647 unsigned long cap = static_cast<unsigned long>(provsql_dtree_max_subproblems);
1648 budget_steps = (budget_steps == 0) ? cap : std::min(budget_steps, cap);
1649 }
1650
1651 unsigned long steps = 0;
1652 provsql::DTreeInterval iv = is_dnf
1653 ? provsql::dtreeBounds(ctx.c, std::move(supports), max_width, budget_steps, &steps)
1654 : provsql::dtreeBoundsCircuit(ctx.c, ctx.gate, max_width, budget_steps, &steps);
1655 if(provsql_verbose >= 50)
1656 provsql_notice("calibrate kind=dtree path=%s S=%zu N=%zu steps=%lu budget=%lu",
1657 is_dnf ? "dnf" : "circuit", ctx.circuit_size, ctx.n_inputs,
1658 steps, budget_steps);
1659 const double est = 0.5 * (iv.lower + iv.upper);
1660
1661 // Deterministic certificate (delta = 0) whenever an approximation was
1662 // actually returned (the interval did not collapse to a point).
1663 if(iv.upper > iv.lower) {
1664 const double half = 0.5 * (iv.upper - iv.lower);
1665 if(kind == ToleranceKind::Relative && est > 0.)
1666 emit_guarantee("relative", half / est, 0., 0, -1, nullptr);
1667 else
1668 emit_guarantee("additive", half, 0., 0, -1, nullptr);
1669 }
1670 return est;
1671 }
1672};
1673
1674/// weightmc: backward-compatible alias for the weighted-model-counter path.
1675class WeightmcMethod : public ProbabilityMethod {
1676public:
1677 std::string name() const override { return "weightmc"; }
1678 ToleranceKind guaranteeKind() const override { return ToleranceKind::Relative; }
1679 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1680 std::string opt = wmc_opt_from_args(parse_method_args(ctx.args), "weightmc");
1681 emit_guarantee("relative", eps_from_wmc_opt(opt), -1., 0, -1, "weightmc");
1682 double r = ctx.c.wmcCount(ctx.gate, "weightmc", opt);
1683 ctx.actual_method = "weightmc";
1684 return r;
1685 }
1686};
1687
1688/// wmc: any registered weighted model counter, selected by tool=<name>.
1689class WmcMethod : public ProbabilityMethod {
1690public:
1691 std::string name() const override { return "wmc"; }
1692 ToleranceKind guaranteeKind() const override { return ToleranceKind::Relative; }
1693 double evaluate(EvalContext &ctx, const Tolerance &) const override {
1694 MethodArgs a = parse_method_args(ctx.args);
1695 std::string tool, tool_args;
1696 if(a.has("tool") || a.has("epsilon") || a.has("delta")) {
1697 reject_unknown_keys(a, {"tool", "epsilon", "delta"}, "wmc");
1698 tool = a.get("tool");
1699 if(tool.empty() && !a.positional.empty())
1700 tool = a.positional[0];
1701 if(tool.empty())
1702 provsql_error("method 'wmc' requires a tool (tool=<name>)");
1703 if(a.has("epsilon") || a.has("delta")) {
1704 double eps = 0.8, delta = 0.5; // validate ranges uniformly
1705 parse_eps_delta(a, "wmc", eps, delta);
1706 tool_args = a.get("delta") + ";" + a.get("epsilon");
1707 }
1708 } else {
1709 auto sep = ctx.args.find(';');
1710 tool = (sep == std::string::npos) ? ctx.args : ctx.args.substr(0, sep);
1711 tool_args = (sep == std::string::npos) ? std::string()
1712 : ctx.args.substr(sep + 1);
1713 }
1714 if(is_approx_wmc_tool(tool))
1715 emit_guarantee("relative", eps_from_wmc_opt(tool_args), -1., 0, -1,
1716 tool.c_str());
1717 double r = ctx.c.wmcCount(ctx.gate, tool, tool_args);
1718 // Report WHICH counter ran (e.g. "wmc:ganak"), mirroring "compilation:d4".
1719 ctx.actual_method = tool.empty() ? "wmc" : "wmc:" + tool;
1720 return r;
1721 }
1722};
1723
1724} // anonymous namespace
1725
1726void MethodCatalog::registerMethod(std::unique_ptr<ProbabilityMethod> m)
1727{
1728 methods_.push_back(std::move(m));
1729}
1730
1731// Base implementation: only the producesDD() methods override this.
1733{
1734 throw CircuitException("method '" + name()
1735 + "' does not construct a d-DNNF");
1736}
1737
1739{
1740 // The d-D portfolio reads only the Boolean view, so the generic-circuit
1741 // pointers are null (never dereferenced by interpret-as-dd /
1742 // tree-decomposition / compilation) and the request is the exact path.
1743 std::string no_args;
1744 EvalContext ctx{/*gc=*/nullptr, /*gc_root=*/gate_t{}, /*token=*/pg_uuid_t{},
1745 c, g, /*gc_to_bc=*/nullptr, /*inv_free_cert=*/false, no_args,
1746 /*explicitly_named=*/false,
1747 /*n_inputs=*/c.getInputs().size(),
1748 /*circuit_size=*/c.getNbGates()};
1750}
1751
1752const ProbabilityMethod *MethodCatalog::byName(const std::string &n) const
1753{
1754 for(const auto &m : methods_)
1755 if(m->name() == n)
1756 return m.get();
1757 return nullptr;
1758}
1759
1760/// Admissibility of a method's guarantee under a requested tolerance. The paths
1761/// nest Exact ⊂ Relative ⊂ Additive: a method is admissible iff its guarantee is at
1762/// least as tight as the request (an exact method serves any path -- "exact when
1763/// cheaper"; a relative method serves relative & additive; an additive method serves
1764/// only additive). This both widens the relative/additive portfolios to the
1765/// approximate members AND keeps the exact path (which calls chooseAndRun with an
1766/// Exact tolerance) from ever selecting an approximate method.
1767static bool toleranceAdmits(ToleranceKind request, ToleranceKind method)
1768{
1769 switch(request) {
1770 case ToleranceKind::Exact: return method == ToleranceKind::Exact;
1771 case ToleranceKind::Relative: return method != ToleranceKind::Additive;
1772 case ToleranceKind::Additive: return true;
1773 }
1774 return false;
1775}
1776
1777namespace {
1778
1779/// Uniform-cost search shared by chooseAndRun (returns a probability) and
1780/// chooseAndBuildDD (returns a d-DNNF artifact). @p run performs the chosen
1781/// method's work and returns the result of type @c R (calling @c evaluate or
1782/// @c buildDD respectively); everything else -- lazy feature acquisition, the
1783/// cheapest-first ranking, the speculative budget, and dropping a method that
1784/// throws -- is identical for both, so it lives here once.
1785template<class R, class Run>
1786R runPortfolio(EvalContext &ctx, const Tolerance &tol,
1787 std::vector<const ProbabilityMethod *> portfolio, Run run)
1788{
1789 // Each step either RUNS the cheapest ready method or ACQUIRES the cheapest
1790 // pending feature -- and we acquire a feature only when no ready method is
1791 // already cheaper than acquiring it (a feature-gated method then costs at
1792 // least that much anyway). So a circuit the cheap methods resolve never pays
1793 // to compute features (dnfShape, later a treewidth proxy) that could not have
1794 // changed the decision. A method that throws when attempted is dropped (its
1795 // implicit feature -- e.g. 'independent' learning the circuit is not
1796 // independent); the last such error propagates if the portfolio is exhausted.
1797 std::set<Feature> acquired;
1798 std::string last_error;
1799 bool have_last_error = false;
1800
1801 while(true) {
1802 // The cheapest ready (all required features acquired) and applicable method,
1803 // and the set of features still gating the not-ready ones.
1804 const ProbabilityMethod *best = nullptr;
1805 double best_cost = std::numeric_limits<double>::infinity();
1806 double second_cost = std::numeric_limits<double>::infinity();
1807 std::set<Feature> pending;
1808 for(const ProbabilityMethod *m : portfolio) {
1809 bool ready = true;
1810 for(Feature f : m->requiredFeatures())
1811 if(acquired.find(f) == acquired.end()) { ready = false; pending.insert(f); }
1812 if(!ready || !m->applicable(ctx, tol))
1813 continue;
1814 double cost = m->estimatedCost(ctx, tol);
1815 if(cost < best_cost) { second_cost = best_cost; best_cost = cost; best = m; }
1816 else if(cost < second_cost) { second_cost = cost; }
1817 }
1818 // Speculative-execution budget: bound the chosen method's work at the cost of
1819 // the next-cheapest ready alternative (infinity if it is the only one). A
1820 // budget-aware method (the d-tree) bails past this and the catch below drops
1821 // it to that alternative, so wasted work is at most ~the safe fallback's cost.
1822 ctx.cost_budget = second_cost;
1823
1824 // The cheapest feature we could acquire to reveal more method costs.
1825 bool have_pending = false;
1826 Feature cheapest_f = Feature::DnfShape;
1827 double cheapest_fc = std::numeric_limits<double>::infinity();
1828 for(Feature f : pending) {
1829 double fc = ctx.featureCost(f);
1830 if(fc < cheapest_fc) { cheapest_fc = fc; cheapest_f = f; have_pending = true; }
1831 }
1832
1833 if(best != nullptr && (!have_pending || best_cost <= cheapest_fc)) {
1834 // Run the cheapest method: nothing cheaper could be revealed by acquiring
1835 // a feature first. A Boolean-only method (handlesMultivalued() == false)
1836 // gets multivalued / BID blocks rewritten to independent Booleans first;
1837 // this is the single, declarative enforcement point (idempotent, and a
1838 // no-op on a circuit with no mulinput gates).
1839 if(!best->handlesMultivalued())
1840 ctx.ensureMultivaluedRewritten();
1841 try {
1842 // Calibration (provsql.verbose_level >= 50): emit the raw cost parameters
1843 // and elapsed ms so each kCost can be fit so that cost ~ ms.
1844 if(provsql_verbose >= 50) {
1845 auto t0 = std::chrono::steady_clock::now();
1846 R r = run(best);
1847 double ms = std::chrono::duration<double, std::milli>(
1848 std::chrono::steady_clock::now() - t0).count();
1849 provsql_notice("calibrate kind=method which=%s S=%zu N=%zu m=%zu w=%u "
1850 "D=%u cost=%g ms=%g", best->name().c_str(),
1851 ctx.circuit_size, ctx.n_inputs, ctx.dnf_num_clauses_,
1852 ctx.tw_proxy_, ctx.tw_max_degree_, best_cost, ms);
1853 return r;
1854 }
1855 return run(best);
1856 } catch(CircuitException &e) {
1858 throw; // a cancel / timeout -- do not silently try another method
1859 last_error = e.what();
1860 have_last_error = true;
1861 portfolio.erase(std::remove(portfolio.begin(), portfolio.end(), best),
1862 portfolio.end());
1863 }
1864 } else if(have_pending) {
1865 if(provsql_verbose >= 50) {
1866 auto t0 = std::chrono::steady_clock::now();
1867 ctx.acquireFeature(cheapest_f);
1868 double ms = std::chrono::duration<double, std::milli>(
1869 std::chrono::steady_clock::now() - t0).count();
1870 provsql_notice("calibrate kind=feature which=%s S=%zu cost=%g ms=%g",
1871 cheapest_f == Feature::DnfShape ? "DnfShape"
1872 : "TreewidthProxy",
1873 ctx.circuit_size, cheapest_fc, ms);
1874 } else {
1875 ctx.acquireFeature(cheapest_f);
1876 }
1877 acquired.insert(cheapest_f);
1878 } else {
1879 // No ready method and nothing left to acquire: the portfolio is exhausted.
1880 if(have_last_error)
1881 throw CircuitException(last_error);
1882 throw CircuitException("no applicable probability method in the portfolio");
1883 }
1884 }
1885}
1886
1887} // anonymous namespace
1888
1890{
1891 std::vector<const ProbabilityMethod *> portfolio;
1892 for(const auto &m : methods_)
1893 if(m->inDefaultChain() && toleranceAdmits(tol.kind, m->guaranteeKind())
1894 // A delta == 0 ("deterministic") request admits only deterministic
1895 // methods: the (eps,delta) samplers cannot honour delta = 0 (their cost
1896 // model even masks this by falling back to a finite delta), so they must
1897 // be excluded by admissibility, not left to lose on cost.
1898 && (tol.delta > 0. || m->isDeterministic()))
1899 portfolio.push_back(m.get());
1900 return runPortfolio<double>(ctx, tol, std::move(portfolio),
1901 [&](const ProbabilityMethod *m){ return m->evaluate(ctx, tol); });
1902}
1903
1905{
1906 // The d-D portfolio is the producesDD() methods (interpret-as-dd /
1907 // tree-decomposition / compilation): all exact, so no tolerance filtering.
1908 // interpret-as-dd is by-name-only for the probability chain (independent
1909 // subsumes it there) but IS the cheapest artifact route here, so it is
1910 // included via producesDD(), not inDefaultChain().
1911 std::vector<const ProbabilityMethod *> portfolio;
1912 for(const auto &m : methods_)
1913 if(m->producesDD())
1914 portfolio.push_back(m.get());
1915 return runPortfolio<dDNNF>(ctx, tol, std::move(portfolio),
1916 [&](const ProbabilityMethod *m){ return m->buildDD(ctx); });
1917}
1918
1920{
1921 static const MethodCatalog cat = [] {
1922 MethodCatalog c;
1923 // Exact portfolio (registration order is irrelevant -- the chooser sorts by
1924 // estimatedCost): independent, inversion-free, possible-worlds (2^N),
1925 // tree-decomposition, compilation. interpret-as-dd is NOT in the portfolio
1926 // -- it is redundant with independent (see InterpretAsDd).
1927 c.registerMethod(std::make_unique<IndependentMethod>());
1928 // The three planner-time routes: same sweep as 'independent', reported (and
1929 // invocable) under the name of the rewrite that produced the circuit.
1930 // Mutually exclusive with 'independent' and with each other via the root's
1931 // route tag, so the identical cost never has to be tie-broken.
1932 c.registerMethod(std::make_unique<RouteMethod>(
1933 PROVSQL_ROUTE_SQ_REWRITE, "sq-rewrite",
1934 "a provenance root produced by the safe-query "
1935 "(read-once) rewriter"));
1936 c.registerMethod(std::make_unique<RouteMethod>(
1937 PROVSQL_ROUTE_BOUNDED_JW, "bounded-jw",
1938 "a provenance root produced by the joint-width UCQ "
1939 "compiler"));
1940 c.registerMethod(std::make_unique<RouteMethod>(
1941 PROVSQL_ROUTE_REACHABILITY, "reachability",
1942 "a provenance root produced by the reachability "
1943 "compiler"));
1944 c.registerMethod(std::make_unique<InversionFreeMethod>());
1945 c.registerMethod(std::make_unique<MobiusMethod>());
1946 c.registerMethod(std::make_unique<TreeDecompositionMethod>());
1947 c.registerMethod(std::make_unique<CompilationMethod>());
1948 c.registerMethod(std::make_unique<PossibleWorldsMethod>());
1949 c.registerMethod(std::make_unique<SieveMethod>());
1950 c.registerMethod(std::make_unique<DTreeBoundsMethod>());
1951 // Approximate portfolio members. Admissibility (toleranceAdmits) keeps them out
1952 // of the exact path: monte-carlo (additive) serves only 'additive';
1953 // karp-luby / stopping-rule (relative) serve 'relative' and 'additive'.
1954 c.registerMethod(std::make_unique<MonteCarloMethod>());
1955 c.registerMethod(std::make_unique<KarpLubyMethod>());
1956 c.registerMethod(std::make_unique<StoppingRuleMethod>());
1957 // By-name-only methods (out of the auto-chooser): interpret-as-dd is redundant
1958 // with independent; weightmc / wmc are external subprocess counters needing a
1959 // tool argument, so they are not auto-spawned on a relative request.
1960 c.registerMethod(std::make_unique<InterpretAsDdMethod>());
1961 c.registerMethod(std::make_unique<WeightmcMethod>());
1962 c.registerMethod(std::make_unique<WmcMethod>());
1963 return c;
1964 }();
1965 return cat;
1966}
1967
1969 const std::string &method,
1970 const std::string &args,
1971 bool inv_free_cert, const Tolerance &tol,
1972 bool mc_fallback,
1973 std::string *actual_method_out)
1974{
1975 const pg_uuid_t token = string2uuid(gc.getUUID(root));
1976 const bool is_path =
1977 method.empty() || method == "default" || method == "exact";
1978
1979 // Boolean-view portfolio. Skip the build when the circuit carries random
1980 // variables (the BoolExpr translation drops gate_rv and rejects an RV
1981 // gate_cmp), or when a large sampleable HAVING aggregate under an
1982 // approximate request would force provsql_having's non-terminating
1983 // threshold-lineage expansion -- those go straight to the estimators below.
1984 const bool sampleable_agg = circuitHasUnresolvedSampleableAgg(gc, root);
1985 /* A contribution that is itself an aggregate result has no Boolean view at
1986 * all (its value is one per world, not a gate_value the translation can
1987 * read), so such a circuit goes straight to the estimators below rather than
1988 * meeting the semiring as an unresolved comparison. */
1989 const bool nested_agg = circuitHasNestedAggValue(gc, root);
1990 if (!circuitHasRV(gc, root) && !nested_agg &&
1991 !(sampleable_agg && tol.delta > 0.)) {
1992 try {
1993 gate_t gate;
1994 std::unordered_map<gate_t, gate_t> gc_to_bc;
1995 BooleanCircuit c = getBooleanCircuit(gc, token, gate, gc_to_bc);
1996 EvalContext ctx{&gc, root, token, c, gate, &gc_to_bc,
1997 inv_free_cert, args, /*explicitly_named=*/!is_path,
1998 /*n_inputs=*/c.getInputs().size(),
1999 /*circuit_size=*/c.getNbGates()};
2000 double result;
2001 if (is_path) {
2002 // The empty / "default" / "exact" method runs the cost-ordered
2003 // auto-chooser under the requested tolerance.
2004 result = MethodCatalog::instance().chooseAndRun(ctx, tol);
2005 } else {
2007 if (m == nullptr)
2008 provsql_error("Wrong method '%s' for probability evaluation",
2009 method.c_str());
2010 // A Boolean-only method named explicitly gets multivalued / BID
2011 // blocks rewritten first (same declarative point as chooseAndRun).
2012 if (!m->handlesMultivalued())
2014 result = m->evaluate(ctx, tol);
2015 }
2016 if (actual_method_out != nullptr)
2017 *actual_method_out = ctx.actual_method;
2018 return result;
2019 } catch (const semiring::SemiringException &) {
2020 // Boolean translation met a raw RV comparator: fall to the estimators.
2021 if (tol.kind == ToleranceKind::Exact && !mc_fallback) throw;
2022 } catch (const CircuitException &) {
2023 // Portfolio exhausted / Boolean view unbuildable: estimators or raise.
2024 if (tol.kind == ToleranceKind::Exact && !mc_fallback) throw;
2025 }
2026 }
2027
2028 // No Boolean view: the tolerance-appropriate generic-circuit estimator.
2029 if (tol.delta == 0. && tol.kind != ToleranceKind::Exact)
2031 "a deterministic (delta = 0) guarantee is not available for this "
2032 "circuit: it carries random-variable or HAVING-aggregate gates, for "
2033 "which only the (eps,delta) samplers apply -- use delta > 0");
2034 if (tol.kind == ToleranceKind::Relative) {
2035 double result;
2036 std::string am;
2037 run_stopping_rule(gc, root, parse_method_args(args), result, am);
2038 if (actual_method_out != nullptr) *actual_method_out = am;
2039 return result;
2040 }
2041 if (tol.kind == ToleranceKind::Additive) {
2042 if (actual_method_out != nullptr) *actual_method_out = "monte-carlo";
2043 return monteCarloRV(
2044 gc, root, static_cast<int>(monte_carlo_samples(parse_method_args(args))));
2045 }
2046 // Exact request with no Boolean view: the moment path's fixed-sample MC
2047 // over the base RVs (mc_fallback). probability_evaluate's exact arm routes
2048 // its RV circuits to monte-carlo upstream and passes mc_fallback = false,
2049 // so it raises here rather than silently sampling.
2050 if (!mc_fallback || provsql_rv_mc_samples <= 0)
2051 throw CircuitException(
2052 "booleanSubcircuitProbability: a random-variable comparator could not "
2053 "be resolved to a Boolean and no Monte Carlo fallback is available "
2054 "(provsql.rv_mc_samples = 0)");
2055 if (actual_method_out != nullptr) *actual_method_out = "monte-carlo";
2056 return monteCarloRV(gc, root, static_cast<int>(provsql_rv_mc_samples));
2057}
2058
2059} // namespace provsql
2060
2061// ---------------------------------------------------------------------------
2062// Three-path tolerance surface (exact / relative / additive).
2063//
2064// The user grants a tolerance via the method name -- "exact" (alias for the
2065// empty/default method), "relative" (a (1±eps) guarantee), "additive"
2066// (|p̂-p| <= eps) -- and the system picks the mechanism, rather than naming an
2067// algorithm (named methods stay available as the EXPLAIN-level escape hatch).
2068// Admissibility nests exact ⊂ relative ⊂ additive, so every path returns an
2069// EXACT value when one is cheaply available ("exact when cheaper"): an exact
2070// result satisfies any (eps,delta).
2071//
2072// The relative/additive estimators run on the GenericCircuit (RV-aware), so they
2073// live here rather than in the BooleanCircuit catalog; folding them in behind a
2074// lazy Boolean build is the clean follow-up.
2075// ---------------------------------------------------------------------------
2076
2077/// Whole-circuit (eps,delta)-relative estimate via the stopping rule (shared by
2078/// the explicit 'stopping-rule' method and the 'relative' path's estimator).
2079///
2080/// Complexity O(S / (p * eps^2) * ln(1/delta)): the Dagum rule draws ~Y1/p
2081/// whole-circuit worlds, each an O(S) evalBool. The 1/p factor makes the cost
2082/// NOT a priori computable from static features -- a precise cost needs a
2083/// p-lower-bound feature -- which is why this estimator stays a path fallback
2084/// rather than a cost-ranked portfolio member for now.
2085static void run_stopping_rule(GenericCircuit &gc, gate_t gc_root,
2086 const MethodArgs &a, double &result,
2087 std::string &actual_method)
2088{
2089 SampleSpec s = parse_sample_spec(a, "stopping-rule");
2090 if(s.fixed)
2091 provsql_error("the relative / stopping-rule estimator is adaptive: give "
2092 "epsilon=E[,delta=D][,max_samples=M], not a fixed sample "
2093 "count");
2094 const unsigned long cap = s.has_max ? s.max_samples : 10000000UL;
2095 unsigned long used = 0;
2096 bool reached = false;
2097 result = provsql::monteCarloRVStopping(gc, gc_root, s.eps, s.delta, cap, used,
2098 reached);
2099 if(reached || used == 0) {
2100 emit_guarantee("relative", s.eps, s.delta, used, -1, "stopping-rule");
2101 } else {
2102 const double eps_add = sqrt(log(2.0 / 0.05) / (2.0 * used));
2103 provsql_warning("relative estimate: reached the %lu-sample cap before the "
2104 "(epsilon=%g, delta=%g) relative target; reporting the "
2105 "additive guarantee at the samples spent (the event is "
2106 "likely rarer than this budget resolves -- raise "
2107 "max_samples)", cap, s.eps, s.delta);
2108 emit_guarantee("additive", eps_add, 0.05, used, -1, "stopping-rule");
2109 }
2110 actual_method = "stopping-rule";
2111}
2112
2113/// Record the method just used in the @c provsql.last_eval_method GUC
2114/// (comma-separated, deduplicated across calls in the session) so callers can
2115/// inspect which evaluation strategy the default auto-selection settled on.
2116/// Shared by the main dispatch and the early-returning gate_mobius route.
2117static void record_last_eval_method(const std::string &actual_method)
2118{
2119 if(actual_method.empty())
2120 return;
2121 std::string current = provsql_last_eval_method ? provsql_last_eval_method : "";
2122 if(current.find(actual_method) == std::string::npos) {
2123 if(!current.empty()) current += ",";
2124 current += actual_method;
2125 SetConfigOption("provsql.last_eval_method", current.c_str(),
2126 PGC_USERSET, PGC_S_SESSION);
2127 }
2128}
2129
2130/**
2131 * @brief Core implementation of probability evaluation for a circuit token.
2132 * @param token UUID of the root provenance gate.
2133 * @param method Evaluation method name (e.g. "independent", "monte-carlo").
2134 * @param args Additional arguments for the chosen method.
2135 * @param isnull Out-param set to @c true when the result is SQL NULL (a
2136 * conditioned token whose evidence has probability zero); may
2137 * be @c NULL when the caller does not need null-propagation.
2138 * @return Float8 Datum containing the computed probability (undefined
2139 * when @p isnull is set to @c true).
2140 */
2141/// Name of the exact route that enumerates the worlds of the inputs on the
2142/// circuit as it is, comparisons and aggregates evaluated per world.
2143static const char *const kPossibleWorldsAgg = "possible-worlds-aggregates";
2144/// Most inputs it enumerates the 2^n worlds of.
2145static const unsigned kPossibleWorldsAggMaxInputs = 20;
2146
2147/// The most contributions an aggregation gate below @p root has, 0 when
2148/// neither a comparison nor an aggregation is there. The worst case of the
2149/// Boolean view is the enumeration of the subsets of those contributions, so
2150/// this is what the enumeration of the worlds of the inputs competes with.
2151static unsigned maxAggContributions(const GenericCircuit &gc, gate_t root)
2152{
2153 std::unordered_set<gate_t> seen;
2154 std::stack<gate_t> todo;
2155 unsigned most = 0;
2156 bool any = false;
2157 todo.push(root);
2158 while(!todo.empty()) {
2159 gate_t g = todo.top();
2160 todo.pop();
2161 if(!seen.insert(g).second) continue;
2162 const gate_type t = gc.getGateType(g);
2163 if(t == gate_cmp || t == gate_agg) {
2164 any = true;
2165 if(t == gate_agg)
2166 most = std::max(most, static_cast<unsigned>(gc.getWires(g).size()));
2167 }
2168 for(gate_t c : gc.getWires(g)) todo.push(c);
2169 }
2170 return any ? std::max(most, 1u) : 0;
2171}
2172
2174 (pg_uuid_t token, const string &method, const string &args, bool *isnull)
2175{
2176 if(isnull != nullptr)
2177 *isnull = false;
2178 // Load the GenericCircuit once: we need it for the RV-detection
2179 // dispatch below, and getBooleanCircuit() reuses it internally so we
2180 // pay no extra cost compared to the previous flow. Universal
2181 // cmp-resolution passes (RangeCheck) have already been applied
2182 // inside getGenericCircuit when the provsql.simplify_on_load GUC is
2183 // on (the default), so the circuit we receive here is already
2184 // peephole-pruned for any "always true / always false" comparator.
2186 gate_t gc_root = gc.getGate(uuid2string(token));
2187
2188 // Conditioning gate (the | / cond operator, uuid carrier): a terminal
2189 // gate_conditioned with children [target, evidence, joint], joint =
2190 // times(target, evidence). Its probability is the conditional
2191 // P(target ∧ evidence) / P(evidence) = P(joint) / P(evidence). Both
2192 // sub-tokens are ordinary semiring gates already in the store (the joint
2193 // is materialised at construction), so each is evaluated by an ordinary
2194 // recursive call -- correlation between target and evidence is exact
2195 // because content-addressing makes a shared base tuple the same input
2196 // gate in both circuits. Impossible evidence (P(evidence) = 0) yields
2197 // SQL NULL. The gate is terminal: a conditioned token can never be a
2198 // child of a semiring gate (the constructors refuse it), so we only ever
2199 // meet it at the root here.
2200 if(gc.getGateType(gc_root) == gate_conditioned) {
2201 const auto &w = gc.getWires(gc_root);
2202 if(w.size() == 2)
2203 provsql_error("probability_evaluate: this is a conditioned distribution "
2204 "(a random_variable / agg_token X | C), not a Boolean "
2205 "event; query it with expected / variance / moment / "
2206 "support, which report the conditional distribution");
2207 if(w.size() != 3)
2208 provsql_error("probability_evaluate: malformed conditioned gate "
2209 "(expected 3 children [target, evidence, joint], got %zu)",
2210 w.size());
2211 pg_uuid_t evidence = string2uuid(gc.getUUID(w[1]));
2212 pg_uuid_t joint = string2uuid(gc.getUUID(w[2]));
2213 bool ev_null = false, jt_null = false;
2214 double pe = DatumGetFloat8(
2215 probability_evaluate_internal(evidence, method, args, &ev_null));
2216 if(ev_null || pe == 0.) {
2217 if(isnull != nullptr)
2218 *isnull = true;
2219 return (Datum) 0; // impossible (or undefined) evidence -> NULL
2220 }
2221 double pj = DatumGetFloat8(
2222 probability_evaluate_internal(joint, method, args, &jt_null));
2223 if(jt_null) {
2224 if(isnull != nullptr)
2225 *isnull = true;
2226 return (Datum) 0;
2227 }
2228 double r = pj / pe;
2229 if(r > 1.) r = 1.; else if(r < 0.) r = 0.;
2230 PG_RETURN_FLOAT8(r);
2231 }
2232
2233 // Möbius-inversion route (safe-UCQ Möbius cancellation): a gate_mobius root
2234 // is a signed combination Σ_i coeff_i · P(child_i) over certified-independent
2235 // islands -- a probability-only shortcut layered over the normal provenance,
2236 // which it carries as a designated "L:<uuid>" lineage child (modelled on
2237 // 'inversion-free'). The default / exact / empty request and the granted-
2238 // tolerance paths (relative / additive) run the fast Möbius route, as does an
2239 // explicit 'mobius': the route is exact and linear, so it trivially meets any
2240 // tolerance -- "exact when cheap", which is exactly what those paths want, and
2241 // it avoids falling through to an FPRAS on the (#P-hard) literal lineage. Any
2242 // OTHER named method is evaluated on the literal lineage (the same exact
2243 // probability via, e.g., possible-worlds or monte-carlo -- slower, but the
2244 // user keeps every method).
2245 if(gc.getGateType(gc_root) == gate_mobius) {
2246 const bool is_path =
2247 method.empty() || method == "default" || method == "exact"
2248 || method == "relative" || method == "additive";
2249 if(is_path || method == "mobius") {
2250 BooleanCircuit dummy;
2251 gate_t dummygate{};
2252 std::unordered_map<gate_t, gate_t> dummymap;
2253 provsql::EvalContext ctx{&gc, gc_root, token, dummy, dummygate, &dummymap,
2254 /*inv_free_cert=*/false, args,
2255 /*explicitly_named=*/!is_path, 0, 0};
2256 double r = provsql::MethodCatalog::instance().byName("mobius")->evaluate(
2257 ctx, provsql::Tolerance{});
2258 // This route returns early (below the main dispatch's recording block),
2259 // so record the method here -- otherwise last_eval_method stays empty.
2261 PG_RETURN_FLOAT8(r);
2262 }
2263 // Another named method: fall through to the literal lineage (the "L:"
2264 // child, read from the raw circuit) and recurse with the requested method.
2265 const std::string lineage = mobiusLineageOf(token);
2266 if(lineage.empty())
2267 provsql_error("method '%s': this Möbius-route token carries no literal "
2268 "lineage (it was built measure-only); only the default / "
2269 "'mobius' method applies", method.c_str());
2270 return probability_evaluate_internal(string2uuid(lineage), method, args,
2271 isnull);
2272 }
2273
2274 // Inversion-free tractability certificate: the planner wraps the per-row
2275 // provenance root in a transparent annotation gate carrying the serialised
2276 // SafeCert recipe. Its presence routes the default probability chain through
2277 // the structured-d-DNNF builder (after independentEvaluation, before
2278 // tree-decomposition) and is required by the explicit 'inversion-free'
2279 // method. The recipe is read here (early, before the simplifier passes); the
2280 // per-input order keys are collected at the dispatch point, where the
2281 // GenericCircuit->BooleanCircuit mapping is available.
2282 bool inv_free_cert = false;
2283 {
2284 std::string ex = gc.getExtra(gc_root);
2285 if (!ex.empty() && ex[0] == SAFE_CERT_EXTRA_PREFIX_RECIPE) {
2286 SafeCert *cert = safe_cert_parse(ex.c_str());
2287 if (cert != nullptr && cert->kind == CERT_INVERSION_FREE) {
2288 inv_free_cert = true;
2289 // Internal per-evaluation diagnostic (the certificate round-trips from
2290 // the planner), not a result-comprehension message: keep it at the
2291 // detector's debug-trace level (>= 30) so it stays out of the level-5
2292 // floor the Studio eval strip applies.
2293 if (provsql_verbose >= 30)
2294 provsql_notice("inversion-free certificate read back from circuit "
2295 "root: %d atoms, %d classes, root_class=%d",
2296 cert->natoms, cert->nclasses, cert->root_class);
2297 }
2298 }
2299 }
2300
2301 // Resolve every RV / HAVING comparator into Boolean structure via the
2302 // single shared pipeline (value simplifier + island decomposer +
2303 // AnalyticEvaluator + the closed-form HAVING cmp evaluators + the
2304 // always-true rewrite). The probability path runs the full pipeline;
2305 // the scalar-moment path calls the same function with simplify/decompose
2306 // off (see ComparatorResolution.h).
2307 provsql::resolveComparators(gc, gc_root, /*simplify=*/true,
2308 /*decompose=*/true);
2309 /* After every resolution pass has run, any gate_rv left in the
2310 * circuit reaches the BoolExpr translation in getBooleanCircuit
2311 * unchanged; that walk recurses into the surrounding gate_cmp and
2312 * calls semiring.value() on the gate_value side, producing the
2313 * generic "This semiring does not support value gates." error.
2314 * Detect that here and raise a message that names the actual
2315 * root cause: the analytical evaluators couldn't fold the RV
2316 * leaves away, and the MC fallback that would have decided the
2317 * surrounding cmp is either disabled (rv_mc_samples = 0) or
2318 * wasn't able to close the gap. HAVING-style cmps over gate_agg
2319 * don't contain gate_rv, so this check leaves them for
2320 * provsql_having. */
2321 /* The empty / "default" request lets the system choose the mechanism,
2322 * so it may silently fall back to Monte Carlo for comparators the
2323 * analytic pre-passes deliberately leave unresolved. An explicit
2324 * "exact" request is a contract for an exact value and must NOT be
2325 * quietly downgraded to an MC estimate; it raises like a named method. */
2326 const bool mc_default = method.empty() || method == "default";
2327 if (method != "monte-carlo" && method != "stopping-rule"
2328 && method != "relative" && method != "additive"
2329 && provsql::circuitHasRV(gc, gc_root)) {
2330 if (provsql_rv_mc_samples <= 0) {
2332 "probability_evaluate: a comparison over random variables "
2333 "could not be resolved analytically; raise "
2334 "provsql.rv_mc_samples above 0 to enable the Monte Carlo "
2335 "fallback, or call probability_evaluate(..., 'monte-carlo', "
2336 "<n>) directly");
2337 } else if (!mc_default) {
2339 "probability_evaluate: a comparison over random variables "
2340 "could not be resolved analytically and the hybrid evaluator "
2341 "left it unresolved; call probability_evaluate(..., "
2342 "'monte-carlo', <n>) directly for an MC estimate");
2343 }
2344 /* Default request with the MC budget available: the comparators the
2345 * analytic pre-passes deliberately left unresolved (a mixture cmp
2346 * whose selector is shared / compound, or another intricate
2347 * correlated shape) are handled by the RV-aware whole-circuit Monte
2348 * Carlo sampler, which couples every shared leaf and selector per
2349 * iteration. Fall through; the dispatch below passes
2350 * mc_fallback = mc_default so booleanSubcircuitProbability routes
2351 * to monteCarloRV. */
2352 }
2353
2354 /* A comparison or an aggregation the resolution passes left in the
2355 * circuit: the Boolean view of it (getBooleanCircuit, through
2356 * provsql_having) enumerates the subsets of the comparison's own
2357 * contributors, one Boolean term each -- millions of gates for the rank of
2358 * a row among 21 candidates, where the contributors read the same handful
2359 * of input tuples. With fewer inputs than a comparison has contributions,
2360 * enumerate the worlds of the inputs on the circuit as it is instead:
2361 * exact, and 2^n evaluations of a circuit of its original size. With as
2362 * many inputs or fewer, the resolution is the cheaper of the two (and its
2363 * closed forms cheaper still), so it keeps the circuit. An explicitly
2364 * named method keeps its own route. */
2365 {
2366 const bool named = !(method.empty() || method == "default");
2367 const unsigned contributions = maxAggContributions(gc, gc_root);
2368 /* A contribution that is itself an aggregate result: the resolution passes
2369 * decline it whatever the number of contributions, so the comparison of
2370 * the counts below does not apply and the enumeration is the only exact
2371 * route. It gets the whole bound. */
2372 const bool nested_agg = provsql::circuitHasNestedAggValue(gc, gc_root);
2373 if((!named || method == kPossibleWorldsAgg) && contributions > 0) {
2374 /* At most as many inputs as the resolution would have contributions to
2375 * enumerate subsets of, and never more than the bound */
2376 const unsigned cap =
2377 (named || nested_agg)
2379 : std::min(kPossibleWorldsAggMaxInputs, contributions - 1);
2380 std::optional<double> p;
2381 try {
2382 p = provsql::enumerateBooleanProbability(gc, gc_root, cap);
2383 } catch(const CircuitException &) {
2384 if(named) throw;
2385 p = std::nullopt; /* a gate it cannot evaluate: fall back */
2386 }
2387 if(p) {
2389 return Float8GetDatum(*p);
2390 }
2391 if(named)
2392 provsql_error("probability_evaluate: method %s needs a circuit whose "
2393 "only random sources are at most %u input tuples",
2395 }
2396 }
2397
2398 double result;
2399 // Records which probability method actually produced the result, so it can
2400 // be exposed through the provsql.last_eval_method GUC (useful when method
2401 // is left empty and the default auto-selection picks one).
2402 string actual_method;
2403
2404 provsql_interrupted = false;
2405
2406 void (*prev_sigint_handler)(int);
2407 prev_sigint_handler = signal(SIGINT, provsql_sigint_handler);
2408
2409 try {
2410 // GenericCircuit-level estimators (the relative / additive paths and their
2411 // explicit-method aliases) run before the BoolExpr translation in
2412 // getBooleanCircuit (which drops gate_rv and rejects RV gate_cmp), so they
2413 // sit at the top here rather than in the BooleanCircuit method catalog.
2414 //
2415 // 'relative' / 'stopping-rule': whole-circuit (eps,delta)-RELATIVE
2416 // probability via the Dagum-Karp-Luby-Ross stopping rule (the universal
2417 // relative estimator -- plain Boolean / RV / HAVING agg). The 'relative'
2418 // path first tries an exact result when one is cheaply available.
2419 if(method == "relative" || method == "additive") {
2420 // Three-path tolerance request, routed through the SAME cost chooser as the
2421 // exact path -- just with a wider admissible set (toleranceAdmits): a
2422 // 'relative' request's portfolio is the exact methods (exact when cheaper) +
2423 // the relative estimators (karp-luby on a DNF, the universal stopping rule);
2424 // 'additive' additionally admits fixed-sample monte-carlo. The chooser picks
2425 // the cheapest, so a tuple-independent circuit resolves exactly via
2426 // 'independent', a small DNF exactly via 'sieve', and a hard/large circuit
2427 // falls to the bounded-cost estimator -- generalising the old
2428 // independent-only "exact when cheaper" to the whole exact portfolio.
2429 const provsql::ToleranceKind tk = (method == "relative")
2432 SampleSpec s = parse_sample_spec(parse_method_args(args), method.c_str());
2433 provsql::Tolerance tol{tk, s.eps, s.delta};
2434
2435 // Delegate to the single Boolean-probability entry point under this
2436 // tolerance: it runs the exact portfolio through the Boolean view when
2437 // one can be built (a tuple-independent circuit resolves exactly via
2438 // 'independent', a small DNF via 'sieve'), and the (eps,delta)
2439 // generic-circuit estimator (stopping rule for 'relative', fixed-sample
2440 // monteCarloRV for 'additive') on an RV / large-aggregate circuit.
2442 gc, gc_root, /*method=*/"", args, inv_free_cert, tol,
2443 /*mc_fallback=*/false, &actual_method);
2444 } else if(method == "stopping-rule") {
2445 run_stopping_rule(gc, gc_root, parse_method_args(args), result,
2446 actual_method);
2447 } else if(method == "monte-carlo"
2448 && (provsql::circuitHasRV(gc, gc_root)
2450 // RV-aware (fixed-sample, additive) Monte Carlo. Also the route for a
2451 // surviving sample-faithful HAVING comparator (any aggregate -- the
2452 // apx-safe corner): the sampler evaluates the gate_agg directly, so a
2453 // large-magnitude aggregate is estimated without the non-terminating
2454 // threshold-lineage expansion.
2455 unsigned long samples = monte_carlo_samples(parse_method_args(args));
2456 result = provsql::monteCarloRV(gc, gc_root, static_cast<int>(samples));
2457 } else if(mc_default && provsql::circuitHasRV(gc, gc_root)) {
2458 // Default request over a circuit that still carries random-variable
2459 // comparators after the analytic pre-passes: those are the shapes
2460 // the pre-passes deliberately declined to marginalise (a mixture cmp
2461 // whose selector is shared with the rest of the circuit, or another
2462 // intricate correlated form). The RV-aware whole-circuit Monte Carlo
2463 // sampler is the correct, general evaluator -- it couples every
2464 // shared base RV and mixture selector per iteration. Only the empty
2465 // / "default" request falls back here; an explicit "exact" request
2466 // was already refused above rather than silently downgraded to MC.
2467 result = provsql::monteCarloRV(
2468 gc, gc_root, static_cast<int>(provsql_rv_mc_samples));
2469 actual_method = "monte-carlo";
2470 } else {
2471 // Boolean-circuit path: the single central entry point builds the
2472 // Boolean view (HAVING semantics + BoolExpr translation) and runs the
2473 // method portfolio (chooseAndRun for the empty / "default" / "exact"
2474 // aliases, byName otherwise). mc_fallback is false here so an
2475 // unresolved RV comparator surfaces its diagnostic rather than
2476 // silently sampling (the moment path opts into the MC fallback).
2478 gc, gc_root, method, args, inv_free_cert, provsql::Tolerance{},
2479 /*mc_fallback=*/false, &actual_method);
2480 }
2481 } catch(CircuitException &e) {
2482 // If the exception was raised because a query cancel or statement
2483 // timeout is pending (the in-process loops throw "Interrupted" off the
2484 // provsql_interrupted flag rather than longjmp through their C++ stack),
2485 // let PG report its native 57014 with the specific reason instead of the
2486 // generic "Interrupted". For any other CircuitException no cancel is
2487 // pending, so CHECK_FOR_INTERRUPTS is a no-op and we report it as-is.
2488 CHECK_FOR_INTERRUPTS();
2489 provsql_error("%s", e.what());
2490 }
2491
2492 // Record the method just used (see record_last_eval_method) so callers can
2493 // inspect which evaluation strategy the default auto-selection settled on.
2494 record_last_eval_method(actual_method);
2495
2496 provsql_interrupted = false;
2497 signal (SIGINT, prev_sigint_handler);
2498
2499 // Avoid rounding errors that make probability outside of [0,1]
2500 if(result>1.)
2501 result=1.;
2502 else if(result<0.)
2503 result=0.;
2504
2505 PG_RETURN_FLOAT8(result);
2506}
2507
2508/** @brief PostgreSQL-callable wrapper for probability_evaluate(). */
2509Datum probability_evaluate(PG_FUNCTION_ARGS)
2510{
2511 provsql_sync_tool_registry(); // honour persisted tool-registry overrides
2512 try {
2513 Datum token = PG_GETARG_DATUM(0);
2514 string method;
2515 string args;
2516
2517 if(PG_ARGISNULL(0))
2518 PG_RETURN_NULL();
2519
2520 if(!PG_ARGISNULL(1)) {
2521 text *t = PG_GETARG_TEXT_P(1);
2522 method = string(VARDATA(t),VARSIZE(t)-VARHDRSZ);
2523 }
2524
2525 if(!PG_ARGISNULL(2)) {
2526 text *t = PG_GETARG_TEXT_P(2);
2527 args = string(VARDATA(t),VARSIZE(t)-VARHDRSZ);
2528 }
2529
2530 bool isnull = false;
2531 Datum result =
2532 probability_evaluate_internal(*DatumGetUUIDP(token), method, args, &isnull);
2533 if(isnull)
2534 PG_RETURN_NULL();
2535 return result;
2536 } catch(const semiring::SemiringGateException &e) {
2537 /* A gate the resolution passes left for the semiring itself: a
2538 * comparison of aggregate results none of them could resolve, whose
2539 * value gates the Boolean translation then meets. A shape ProvSQL does
2540 * not support, not an internal error. */
2541 provsql_unsupported(PROVSQL_GAP, "comparison-unresolved", "probability_evaluate: a comparison ProvSQL could not "
2542 "resolve remains in the circuit (%s): supported are a "
2543 "comparison of an aggregate with a constant, between "
2544 "numeric aggregates, and between two array_agg results",
2545 e.what());
2546 } catch(const std::exception &e) {
2547 provsql_error("probability_evaluate: %s", e.what());
2548 } catch(...) {
2549 provsql_error("probability_evaluate: Unknown exception");
2550 }
2551
2552 PG_RETURN_NULL();
2553}
2554
2555/**
2556 * @brief PostgreSQL-callable wrapper for the d-tree leaf bound:
2557 * @c probability_bounds(token uuid, OUT lower float8, OUT upper float8).
2558 *
2559 * Returns a cheap certified interval @c [lower,upper] with @c lower ≤ Pr ≤ upper
2560 * for the probability of the DNF-shaped circuit rooted at @p token, via
2561 * @c BooleanCircuit::dnfBounds (Olteanu-Huang-Koch Fig. 3). Errors when the circuit
2562 * is not a monotone DNF over input leaves (the leaf-bound heuristic is
2563 * DNF-specific); the future d-tree engine will recurse on non-DNF roots.
2564 */
2565Datum probability_bounds(PG_FUNCTION_ARGS)
2566{
2568 try {
2569 if(PG_ARGISNULL(0))
2570 PG_RETURN_NULL();
2571 pg_uuid_t token = *DatumGetUUIDP(PG_GETARG_DATUM(0));
2572
2573 gate_t root;
2574 BooleanCircuit c = getBooleanCircuit(token, root);
2575
2576 std::vector<gate_t> clause_roots;
2577 std::vector<std::set<gate_t> > supports;
2578 if(!c.dnfShape(root, clause_roots, supports))
2579 provsql_error("probability_bounds applies only to a DNF-shaped circuit "
2580 "(a monotone OR-of-ANDs over input leaves); negation, "
2581 "comparison, aggregation, random-variable and "
2582 "multivalued-input gates are not supported");
2583
2584 double lower, upper;
2585 c.dnfBounds(supports, lower, upper);
2586
2587 TupleDesc tupdesc;
2588 if(get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
2589 provsql_error("probability_bounds: expected composite return type");
2590 tupdesc = BlessTupleDesc(tupdesc);
2591
2592 Datum values[2] = { Float8GetDatum(lower), Float8GetDatum(upper) };
2593 bool nulls[2] = { false, false };
2594 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
2595 } catch(const std::exception &e) {
2596 provsql_error("probability_bounds: %s", e.what());
2597 } catch(...) {
2598 provsql_error("probability_bounds: Unknown exception");
2599 }
2600
2601 PG_RETURN_NULL();
2602}
Exact closed-form HAVING COUNT(*) op C probability over safe-join lineage – the recursive marginal-ve...
Closed-form CDF resolution for trivial gate_cmp shapes.
Boolean-expression (lineage formula) semiring.
Boolean provenance circuit with support for knowledge compilation.
constexpr unsigned DNNF_CERT_INFO
d-DNNF certificate value for the (gate-type-specific) per-gate info field.
@ IN
Input (variable) gate representing a base tuple.
BooleanCircuit getBooleanCircuit(GenericCircuit &gc, pg_uuid_t token, gate_t &gate, std::unordered_map< gate_t, gate_t > &gc_to_bc)
Build a BooleanCircuit from an already-loaded GenericCircuit.
GenericCircuit getGenericCircuit(pg_uuid_t token)
Build a GenericCircuit from the mmap store rooted at token.
Build in-memory circuits from the mmap-backed persistent store.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
The single comparator-resolution pipeline and the single Boolean-subcircuit probability entry point,...
Closed-form Poisson-binomial CDF resolution for HAVING COUNT(*) op C gate_cmps.
Anytime interval-bounds probability for monotone DNFs (d-trees).
Semiring-agnostic in-memory provenance circuit.
Peephole simplifier for continuous gate_arith sub-circuits.
Closed-form probability resolution for HAVING MIN(a) op C and MAX(a) op C gate_cmps.
Monte Carlo sampling over a GenericCircuit, RV-aware.
Catalog of probability-evaluation methods (Strategy + registry).
Support-based bound check for continuous-RV comparators.
Abstract semiring interface for provenance evaluation.
In-process structured-d-DNNF construction over a query-derived variable order, for the inversion-free...
Closed-form probability resolution for HAVING SUM(a) op C gate_cmps via a weighted-sum DP.
Tree decomposition of a Boolean circuit for knowledge compilation.
Fix macro conflicts between PostgreSQL headers and the C++ STL/Boost.
Boolean circuit for provenance formula evaluation.
double karpLubyStopping(const std::vector< gate_t > &clauses, const std::vector< std::set< gate_t > > &supports, double eps, double delta, unsigned long max_samples, unsigned long &samples_used, bool &reached_target) const
Karp-Luby FPRAS with the self-adjusting stopping rule (adaptive sample count for a relative (eps,...
const std::set< gate_t > & getInputs() const
Return the set of input (IN) gate IDs.
double karpLuby(const std::vector< gate_t > &clauses, const std::vector< std::set< gate_t > > &supports, unsigned long samples) const
Karp-Luby FPRAS estimate of a DNF-shaped circuit's probability (fixed sample budget,...
dDNNF compilation(gate_t g, std::string compiler, std::string *resolved=nullptr) const
Compile the sub-circuit rooted at g to a dDNNF via an external tool.
void dnfBounds(const std::vector< std::set< gate_t > > &clauses, double &lower, double &upper) const
Cheap certified probability interval [lower,upper] of a monotone DNF, without compiling it (Olteanu-H...
bool dnfShape(gate_t g, std::vector< gate_t > &clauses, std::vector< std::set< gate_t > > &supports) const
Detect the DNF shape the Karp-Luby FPRAS requires.
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
virtual char const * what() const noexcept
Return the error message as a C-string.
Definition Circuit.h:220
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:140
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
uuid getUUID(gate_t g) const
Return the UUID string associated with gate g.
Definition Circuit.hpp:46
gate_t getGate(const uuid &u)
Return (or create) the gate associated with UUID u.
Definition Circuit.hpp:33
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
In-memory provenance circuit with semiring-generic evaluation.
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.
Top-down structured-d-DNNF builder over a query-derived variable order.
const dDNNF & dnnf() const
The constructed d-DNNF (root set, simplified).
static unsigned degeneracyLowerBound(const BooleanCircuit &bc, unsigned &max_degree)
Cheap degeneracy lower bound on the treewidth of bc's primal graph.
static constexpr int MAX_TREEWIDTH
Maximum supported treewidth.
A d-DNNF circuit supporting exact probabilistic and game-theoretic evaluation.
Definition dDNNF.h:71
double probabilityEvaluation() const
Compute the exact probability of the d-DNNF being true.
Definition dDNNF.cpp:141
Registry of ProbabilityMethod objects.
const ProbabilityMethod * byName(const std::string &name) const
Exact match on name(); nullptr if absent.
std::vector< std::unique_ptr< ProbabilityMethod > > methods_
double chooseAndRun(EvalContext &ctx, const Tolerance &tol) const
Run the auto-chooser for tol: the portfolio methods admissible for the tolerance and applicable,...
void registerMethod(std::unique_ptr< ProbabilityMethod > m)
static const MethodCatalog & instance()
The process-wide catalog, lazily populated with the built-in methods.
dDNNF chooseAndBuildDD(EvalContext &ctx, const Tolerance &tol) const
The d-DNNF analogue of chooseAndRun: cost-select among the producesDD() portfolio (interpret-as-dd / ...
Strategy interface: one concrete subclass per probability method.
virtual dDNNF buildDD(EvalContext &ctx) const
Build the d-DNNF this method constructs (only when producesDD()).
virtual double evaluate(EvalContext &ctx, const Tolerance &tol) const =0
Run the method, returning the probability.
virtual bool handlesMultivalued() const
True iff the method evaluates the raw circuit, including multivalued (BID / gate_mulinput) gates,...
virtual std::string name() const =0
Stable identifier used for byName lookup and the provsql.last_eval_method report.
Exception thrown when a semiring operation is not supported.
Definition Semiring.h:55
virtual char const * what() const noexcept
Return the error message as a C-string.
Definition Semiring.h:69
A gate kind this semiring does not interpret (a comparison, a semimodule multiplication,...
Definition Semiring.h:84
Constructs a d-DNNF from a Boolean circuit and its tree decomposition.
Provenance evaluation helper for HAVING-clause circuits.
Shared declaration for the Möbius-route probability sweep.
static const double kCostDnfShapeFeature
static const double kCostSieve
static const size_t kPossibleWorldsSanityMax
Sanity bound on the reachable-input count for the auto-chosen 2^N possible-worlds enumeration: above ...
double booleanSubcircuitProbability(GenericCircuit &gc, gate_t root, const std::string &method, const std::string &args, bool inv_free_cert, const Tolerance &tol, bool mc_fallback, std::string *actual_method_out)
Probability of the Boolean function rooted at root in gc – THE single entry point over the method por...
static const double kCostKarpLuby
std::optional< double > enumerateBooleanProbability(const GenericCircuit &gc, gate_t root, unsigned max_inputs)
The probability of root, exactly, over the possible worlds of the circuit as it is: the alternative t...
static const double kCostDTreeExact
static const double kCostTwProxyFeature
bool circuitHasNestedAggValue(const GenericCircuit &gc, gate_t root)
Whether a contribution of an aggregate is itself an aggregate result (a gate_semimod whose M side is ...
static const double kCostDTreeMsPerStepGeneral
static const double kCostCompilationFloor
static bool toleranceAdmits(ToleranceKind request, ToleranceKind method)
Admissibility of a method's guarantee under a requested tolerance.
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.
static const double kCostStoppingRule
static const double kCostPossibleWorlds
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 –...
Feature
A circuit feature a method's cost/applicability estimate depends on, but that is not free to acquire.
static const double kCostDTreeApprox
static const double kCostInversionFree
ToleranceKind
The contract the user grants – the "path".
static const double kCostTreeDecomp
static const double kCostDTreeMsPerStepDnf
static double pow2_clamped(size_t k)
2^k with the exponent clamped to keep the cost finite (a clamped exponent still sorts the method dead...
static const double kCostMonteCarlo
static const double kCostCompilation
double monteCarloRV(const GenericCircuit &gc, gate_t root, unsigned samples)
Run Monte Carlo on a circuit that may contain gate_rv leaves.
bool circuitHasRV(const GenericCircuit &gc, gate_t root)
Walk the circuit reachable from root looking for any gate_rv.
DTreeInterval dtreeBounds(const BooleanCircuit &c, Clauses clauses, double max_width, unsigned long budget, unsigned long *steps_out)
Definition DTree.cpp:272
dDNNF makeDDAuto(BooleanCircuit &c, gate_t g)
Cost-select a d-DNNF construction route for gate g of Boolean circuit c and build it – the default ma...
DTreeInterval dtreeBoundsCircuit(const BooleanCircuit &c, gate_t root, double max_width, unsigned long budget, unsigned long *steps_out)
Certified probability interval of an arbitrary Boolean circuit, refined to a target width (the d-tree...
Definition DTree.cpp:651
void resolveComparators(GenericCircuit &gc, gate_t root, bool simplify, bool decompose)
Run the comparator-resolution pipeline on gc, rewriting every gate_cmp (RV comparison,...
static const size_t kSieveSanityMaxClauses
Largest clause count for which the auto-chosen sieve (2^m inclusion-exclusion) is admitted (matches B...
static const double kCostIndependent
Datum probability_evaluate(PG_FUNCTION_ARGS)
PostgreSQL-callable wrapper for probability_evaluate().
static Datum probability_evaluate_internal(pg_uuid_t token, const string &method, const string &args, bool *isnull)
static std::map< gate_t, int > inversion_free_rank(const std::map< gate_t, StructuredDNNFBuilder::InputKey > &keys)
Flatten the per-input order keys into a total rank for the structured builder's order-only constructo...
static const char *const kPossibleWorldsAgg
Core implementation of probability evaluation for a circuit token.
double mobius_probability_of(pg_uuid_t token)
External entry point for the Möbius-route probability sweep (declared in mobius_evaluate....
Datum probability_bounds(PG_FUNCTION_ARGS)
PostgreSQL-callable wrapper for the d-tree leaf bound: probability_bounds(token uuid,...
static const unsigned kPossibleWorldsAggMaxInputs
Most inputs it enumerates the 2^n worlds of.
static void record_last_eval_method(const std::string &actual_method)
Record the method just used in the provsql.last_eval_method GUC (comma-separated, deduplicated across...
dDNNF buildInversionFreeDDNNF(pg_uuid_t token)
Compile a query certified inversion-free to its structured d-DNNF.
static bool collect_inversion_free_keys(const GenericCircuit &gc, gate_t gc_root, const std::unordered_map< gate_t, gate_t > &gc_to_bc, const BooleanCircuit &c, gate_t bc_root, std::map< gate_t, StructuredDNNFBuilder::InputKey > &out)
Collect the inversion-free per-input order keys for the structured builder.
static void run_stopping_rule(GenericCircuit &gc, gate_t gc_root, const MethodArgs &a, double &result, std::string &actual_method)
Whole-circuit (eps,delta)-relative estimate via the stopping rule (shared by the explicit 'stopping-r...
static void provsql_sigint_handler(int)
SIGINT handler that sets the global interrupted flag.
static unsigned maxAggContributions(const GenericCircuit &gc, gate_t root)
The most contributions an aggregation gate below root has, 0 when neither a comparison nor an aggrega...
bool provsql_absorptive_provenance
Derived flag: the session's provenance class is 'absorptive' or 'boolean' – licenses constructions so...
Definition provsql.c:139
int provsql_verbose
Verbosity level; controlled by the provsql.verbose_level GUC.
Definition provsql.c:113
bool provsql_simplify_on_load
Run universal cmp-resolution passes when getGenericCircuit returns; controlled by the provsql....
Definition provsql.c:134
bool provsql_inversion_free
Insert the inversion-free structured-d-DNNF path into the default probability chain (after independen...
Definition provsql.c:137
char * provsql_last_eval_method
Last probability evaluation method(s) used; exposed via provsql.last_eval_method.
Definition provsql.c:117
int provsql_rv_mc_samples
Default sample count for analytical-evaluator MC fallbacks; 0 disables fallback (callers raise instea...
Definition provsql.c:124
int provsql_dtree_max_subproblems
Debug/safety hard cap on d-tree subproblems before it bails (0 = off; the chooser auto-budgets at the...
Definition provsql.c:126
bool provsql_interrupted
Global variable that becomes true if this particular backend received an interrupt signal.
Definition provsql.c:109
bool provsql_boolean_provenance
Derived flag: the session's provenance class is 'boolean' – enables the Boolean-only machinery (safe-...
Definition provsql.c:138
#define PROVSQL_GAP
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
#define provsql_unsupported(scope, tag, fmt,...)
Refuse a query ProvSQL cannot track, and abort the transaction.
#define provsql_warning(fmt,...)
Emit a ProvSQL warning message (execution continues).
#define provsql_notice(fmt,...)
Emit a ProvSQL informational notice (execution continues).
Background worker and IPC primitives for mmap-backed circuit storage.
Shared-memory segment and inter-process pipe management.
Core types, constants, and utilities shared across ProvSQL.
@ gate_annotation
Transparent single-child wrapper carrying a query-level annotation in extra (inversion-free certifica...
@ gate_mobius
Signed Möbius combination: a MEASURE-only gate carrying one integer coefficient per child (in extra,...
@ gate_conditioned
Conditioning marker with two children [target, evidence]: measure-only, probability_evaluate returns ...
@ gate_assumed
Structural marker over a single child whose sub-circuit was computed under a Boolean-provenance assum...
provsql_route
Tags identifying the planner-time route that produced a circuit.
@ PROVSQL_ROUTE_BOUNDED_JW
Joint-width UCQ compiler (src/UCQJointCompiler.h).
@ PROVSQL_ROUTE_SQ_REWRITE
Hierarchical-CQ read-once rewrite (src/safe_query.c).
@ PROVSQL_ROUTE_NONE
No route rewrite: ordinary lineage.
@ PROVSQL_ROUTE_REACHABILITY
Recursive-reachability compiler (src/reachability_evaluate.cpp).
static const char * provsql_route_name(provsql_route route)
The name of a route, as the probability dispatcher reports it and as the annotation wrapper of a mate...
pg_uuid_t string2uuid(const string &source)
Parse a UUID string into a pg_uuid_t.
string uuid2string(pg_uuid_t uuid)
Format a pg_uuid_t as a std::string.
C++ utility functions for UUID manipulation.
SafeCert * safe_cert_parse(const char *str)
Parse a C-prefixed recipe string (as produced by safe_cert_serialise and read back from an annotation...
bool safe_cert_key_parse(const char *str, SafeCertKey *out)
Parse a K-prefixed order-key string into out.
Tractability certificate for the inversion-free UCQ(OBDD) path.
@ CERT_INVERSION_FREE
Inversion-free UCQ(OBDD) over TID inputs.
#define SAFE_CERT_EXTRA_PREFIX_RECIPE
Discriminator prefixes for the annotation gate's extra payload.
const char * sec
const char * root
Query-derived order recipe for the structured-d-DNNF builder.
int nclasses
Number of (compacted) equivalence classes.
int root_class
Compacted id of the root class (touches every atom).
int natoms
Number of atoms (range-table entries).
SafeCertKind kind
Structured per-input order key carried by the planner's markers.
Per-evaluation circuit state threaded to a method's evaluate().
double cost_budget
Speculative-execution budget: the estimated cost (in the chooser's ms-ish units) of the next-cheapest...
bool hasFeature(Feature f) const
double featureCost(Feature f) const
Heuristic acquisition cost of f, in the same work units as a method's estimatedCost (so the chooser c...
size_t n_inputs
input count N (O(1) cost feature)
std::unordered_map< gate_t, gate_t > * gc_to_bc
bool explicitly_named
invoked via byName (vs the default chain)
size_t circuit_size
gate count S, the circuit-size parameter (O(1))
bool evaluate(const std::vector< long > &values, const std::vector< bool > &mask, long constant, ComparisonOperator op, std::unique_ptr< Aggregator > aggregator)
Evaluate whether the aggregation of values masked by mask satisfies op constant.
Definition subset.cpp:339
void provsql_sync_tool_registry()
Rebuild the in-memory registry as "compiled seed overlaid with the provsql.tool_overrides rows"...
Reload the in-memory external-tool registry from its persistent overrides.