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