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