ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
BooleanCircuit.cpp
Go to the documentation of this file.
1/**
2 * @file BooleanCircuit.cpp
3 * @brief Boolean circuit implementation and evaluation algorithms.
4 *
5 * Implements the methods declared in @c BooleanCircuit.h, including:
6 * - Gate management (@c addGate, @c setGate, @c setInfo, @c setProb).
7 * - Probability evaluation algorithms: possible worlds, Monte Carlo,
8 * WeightMC, independent evaluation.
9 * - Knowledge compilation: @c compilation() (external tools),
10 * @c interpretAsDD() (direct from circuit structure),
11 * @c makeDD() (dispatcher).
12 * - @c rewriteMultivaluedGates(): replaces MULVAR/MULIN clusters with
13 * standard AND/OR/NOT circuits.
14 * - @c TseytinCNF(): DIMACS/weighted CNF generation for model counters.
15 * - @c exportCircuit(): serialisation in the @c tdkc text format.
16 * - @c toString(): human-readable gate description.
17 *
18 * In the standalone @c tdkc build (when @c TDKC is defined) a lightweight
19 * @c elog() stub replaces the PostgreSQL error-reporting function.
20 */
21#include "BooleanCircuit.h"
22#include "Circuit.hpp"
23#include <type_traits>
24
25extern "C" {
26#include <unistd.h>
27#include <sys/wait.h>
28#include <math.h>
29}
30
31#include <cassert>
32#include <cstdint>
33#include <string>
34#include <fstream>
35#include <sstream>
36#include <cstdlib>
37#include <iostream>
38#include <random>
39#include <vector>
40#include <stack>
41#include <functional>
42#include <algorithm>
43
44#include <boost/archive/text_oarchive.hpp>
45#include <boost/archive/text_iarchive.hpp>
46
48#include "external_tool.h"
49// The tool registry drives external-tool selection and invocation, all of
50// which lives in #ifndef TDKC blocks (tdkc invokes no external tool), so the
51// registry is needed only in the extension build.
52#ifndef TDKC
53#include "ToolRegistry.h"
54#include "kcmcp_client.h"
55#endif
56
57// "provsql_utils.h"
58#ifdef TDKC
59constexpr bool provsql_interrupted = false;
60constexpr int provsql_verbose = 0;
61constexpr int provsql_monte_carlo_seed = -1;
62// makeDD's final fallback uses this GUC in the extension build; the
63// standalone tdkc tool has no GUC layer, so default it to "d4".
64constexpr const char *provsql_fallback_compiler = "d4";
65enum levels {ERROR, NOTICE};
66#define elog(level, ...) {fprintf(stderr, __VA_ARGS__); if(level==ERROR) exit(EXIT_FAILURE);}
67#define CHECK_FOR_INTERRUPTS() ((void)0)
68// The standalone tool has no PostgreSQL stack-depth governor; its deep
69// recursions (tree decomposition, d-DNNF) already run on heap stacks.
70#define check_stack_depth() ((void)0)
71#else
72extern "C" {
73#include "provsql_utils.h"
74#include "utils/elog.h"
75#include "miscadmin.h"
76}
77#endif
78#include "provsql_error.h"
79#include "scoped_tempdir.h"
81
82namespace {
83
84/**
85 * @brief Best-effort parse of a model counter's "result line".
86 *
87 * Ganak, SharpSAT-TD, and DPMC each emit their final count on a
88 * line like @c "c s exact arb float 0.4350..." or
89 * @c "c s exact arb int 12345" (and DPMC also accepts the older
90 * @c "s wmc N" shape). Keeping the right-most whitespace-separated
91 * token and feeding it to @c std::stod would rely on the value always
92 * being last on the line, which the DPMC source itself notes is not
93 * stable across versions (a trailing @c "(cputime ...)" suffix would
94 * silently corrupt the parse).
95 *
96 * This helper scans tokens right-to-left and returns the
97 * right-most one that parses as a @c double or as a
98 * @c <num>/<den> rational. Throws a clear
99 * @c "<tool>: could not parse '<line>'" instead of letting
100 * @c std::stod's @c std::invalid_argument leak through.
101 */
102#ifndef TDKC // external model-counter output parser; tdkc runs no counter
103double parse_wmc_value(const std::string &line, const char *tool) {
104 std::vector<std::string> tokens;
105 std::stringstream ss(line);
106 std::string tok;
107 while(ss >> tok) tokens.push_back(tok);
108
109 for(auto it = tokens.rbegin(); it != tokens.rend(); ++it) {
110 const std::string &t = *it;
111 try {
112 auto slash = t.find('/');
113 if(slash != std::string::npos) {
114 size_t pn = 0, pd = 0;
115 double num = std::stod(t.substr(0, slash), &pn);
116 double den = std::stod(t.substr(slash + 1), &pd);
117 if(pn != slash || pd != t.size() - slash - 1) continue;
118 return (den == 0.0) ? 0.0 : num / den;
119 }
120 size_t p = 0;
121 double v = std::stod(t, &p);
122 if(p == t.size()) return v;
123 } catch(const std::exception &) {
124 // Not a number; try the next token to the left.
125 }
126 }
127 throw CircuitException(std::string(tool) + ": could not parse '" + line + "'");
128}
129#endif
130
131} // anonymous namespace
132
134{
135 auto id = Circuit::setGate(type);
136 if(type == BooleanGate::IN) {
137 setProb(id,1.);
138 inputs.insert(id);
139 } else if(type == BooleanGate::MULIN) {
140 mulinputs.insert(id);
141 }
142 return id;
143}
144
146{
147 auto id = Circuit::setGate(u, type);
148 if(type == BooleanGate::IN) {
149 setProb(id,1.);
150 inputs.insert(id);
151 } else if(type == BooleanGate::MULIN) {
152 mulinputs.insert(id);
153 }
154 return id;
155}
156
158{
159 auto id = setGate(u, type);
160 if(std::isnan(p))
161 p=1.;
162 setProb(id,p);
163 return id;
164}
165
167{
168 auto id = setGate(type);
169 if(std::isnan(p))
170 p=1.;
171 setProb(id,p);
172 return id;
173}
174
176{
177 auto id=Circuit::addGate();
178 prob.push_back(1);
179 return id;
180}
181
183{
184 return toStringHelper(g, BooleanGate::UNDETERMINED, nullptr);
185}
186
188 gate_t g,
189 const std::unordered_map<gate_t, std::string> &labels) const
190{
191 return toStringHelper(g, BooleanGate::UNDETERMINED, &labels);
192}
193
195 gate_t g,
196 BooleanGate parent,
197 const std::unordered_map<gate_t, std::string> *labels) const
198{
199 std::string op;
200 std::string result;
201 auto gtype = getGateType(g);
202
203 switch(gtype) {
204 case BooleanGate::IN:
205 if(labels) {
206 auto it = labels->find(g);
207 if(it != labels->end())
208 return it->second;
209 }
210 return "x"+to_string(g);
212 if(labels) {
213 auto it = labels->find(g);
214 if(it != labels->end())
215 return it->second + "[" + std::to_string(getProb(g)) + "]";
216 }
217 return "{" + to_string(*getWires(g).begin()) + "=" + std::to_string(getInfo(g)) + "}[" + std::to_string(getProb(g)) + "]";
218 case BooleanGate::NOT:
219 op="¬";
220 break;
222 op="?";
223 break;
224 case BooleanGate::AND:
225 op="∧";
226 break;
227 case BooleanGate::OR:
228 op="∨";
229 break;
231 ; // already dealt with in MULIN
232 }
233
234 if(getWires(g).empty()) {
235 if(gtype==BooleanGate::AND)
236 return "⊤";
237 else if(gtype==BooleanGate::OR)
238 return "⊥";
239 else return op;
240 }
241
242 for(auto s: getWires(g)) {
243 if(gtype==BooleanGate::NOT)
244 result = op;
245 else if(!result.empty())
246 result+=" "+op+" ";
247 result+=toStringHelper(s, gtype, labels);
248 }
249
250 // Parenthesis elision:
251 // * single-wire AND/OR: the join carries no information, drop the wrap.
252 // * root call (parent = UNDETERMINED): no enclosing context, drop the wrap.
253 // * same-op nesting (parent == gtype, AND/OR only): associative, drop the wrap.
254 bool single_join = (gtype==BooleanGate::AND || gtype==BooleanGate::OR)
255 && getWires(g).size()==1;
256 bool same_op_assoc = (gtype==BooleanGate::AND || gtype==BooleanGate::OR)
257 && parent==gtype;
258 if(single_join || parent==BooleanGate::UNDETERMINED || same_op_assoc)
259 return result;
260 return "("+result+")";
261}
262
264{
265 std::stringstream ss;
266
267 std::unordered_set<gate_t> processed;
268 std::stack<gate_t> to_process;
269 to_process.push(root);
270
271 while(!to_process.empty()) {
272 auto g=to_process.top();
273 to_process.pop();
274
275 if(processed.find(g)!=processed.end())
276 continue;
277
278 ss << g << " ";
279
280 switch(getGateType(g)) {
281 case BooleanGate::IN:
282 ss << "IN " << getProb(g);
283 break;
284
285 case BooleanGate::NOT:
286 ss << "NOT " << getWires(g)[0];
287 break;
288
289 case BooleanGate::AND:
290 ss << "AND";
291
292 for(auto s:getWires(g))
293 ss << " " << s;
294 break;
295
296 case BooleanGate::OR:
297 ss << "OR";
298
299 for(auto s:getWires(g))
300 ss << " " << s;
301 break;
302
306 assert(false); // not done
307 }
308
309 ss << "\n";
310
311 for(auto s: getWires(g)) {
312 if(processed.find(s)==processed.end())
313 to_process.push(s);
314 }
315
316 processed.insert(g);
317 }
318
319 return ss.str();
320}
321
322bool BooleanCircuit::evaluate(gate_t g, const std::unordered_set<gate_t> &sampled) const
323{
324 check_stack_depth(); // recurses on wires; guard deep circuits (see GenericCircuit::evaluate)
325 bool disjunction=false;
326
327 switch(getGateType(g)) {
328 case BooleanGate::IN:
329 return sampled.find(g)!=sampled.end();
332 throw CircuitException("Monte-Carlo sampling not implemented on multivalued inputs");
333 case BooleanGate::NOT:
334 return !evaluate(*(getWires(g).begin()), sampled);
335 case BooleanGate::AND:
336 disjunction = false;
337 break;
338 case BooleanGate::OR:
339 disjunction = true;
340 break;
342 throw CircuitException("Incorrect gate type");
343 }
344
345 for(auto s: getWires(g)) {
346 bool e = evaluate(s, sampled);
347 if(disjunction && e)
348 return true;
349 if(!disjunction && !e)
350 return false;
351 }
352
353 if(disjunction)
354 return false;
355 else
356 return true;
357}
358
359double BooleanCircuit::monteCarlo(gate_t g, unsigned samples) const
360{
361 // Seed mt19937_64 from the provsql.monte_carlo_seed GUC: -1 (the
362 // default) means non-deterministic via std::random_device, any other
363 // value (including 0) is a literal seed so regression tests can pin
364 // sampling for reproducibility.
365 std::mt19937_64 rng;
366 if(provsql_monte_carlo_seed != -1) {
367 rng.seed(static_cast<uint64_t>(provsql_monte_carlo_seed));
368 } else {
369 std::random_device rd;
370 rng.seed((static_cast<uint64_t>(rd()) << 32) | rd());
371 }
372 std::uniform_real_distribution<double> uniform01(0.0, 1.0);
373
374 auto success{0u};
375
376 for(unsigned i=0; i<samples; ++i) {
377 std::unordered_set<gate_t> sampled;
378 for(auto in: inputs) {
379 if(uniform01(rng) < getProb(in)) {
380 sampled.insert(in);
381 }
382 }
383
384 if(evaluate(g, sampled))
385 ++success;
386
388 throw CircuitException("Interrupted after "+std::to_string(i+1)+" samples");
389 }
390
391 return success*1./samples;
392}
393
395 gate_t g,
396 std::vector<gate_t> &clauses,
397 std::vector<std::set<gate_t> > &supports) const
398{
399 clauses.clear();
400 supports.clear();
401
402 // A top-level OR exposes one clause per child; anything else is a single
403 // clause rooted at g itself (regime (a): a bare AND-of-leaves or a lone
404 // input).
405 std::vector<gate_t> clause_roots;
407 for(auto c: getWires(g))
408 clause_roots.push_back(c);
409 } else {
410 clause_roots.push_back(g);
411 }
412
413 for(auto root: clause_roots) {
414 // Sweep the AND-only stratum below this clause root, collecting the
415 // reachable input leaves. Any OR (nested disjunction), NOT
416 // (negation), or multivalued input below the root takes the circuit
417 // out of regimes (a)/(b): bail.
418 std::set<gate_t> support;
419 std::unordered_set<gate_t> seen;
420 std::stack<gate_t> st;
421 st.push(root);
422 while(!st.empty()) {
423 gate_t cur = st.top();
424 st.pop();
425 if(!seen.insert(cur).second)
426 continue;
427 switch(getGateType(cur)) {
428 case BooleanGate::IN:
429 support.insert(cur);
430 break;
431 case BooleanGate::AND:
432 for(auto s: getWires(cur))
433 st.push(s);
434 break;
435 default:
436 return false;
437 }
438 }
439 clauses.push_back(root);
440 supports.push_back(std::move(support));
441 }
442
443 return true;
444}
445
446bool BooleanCircuit::dnfShapeInfo(gate_t g, std::size_t &num_clauses) const
447{
448 // Clause count: children of a top-level OR, else a single clause rooted at g.
449 std::vector<gate_t> clause_roots;
451 for(auto c: getWires(g))
452 clause_roots.push_back(c);
453 else
454 clause_roots.push_back(g);
455 num_clauses = clause_roots.size();
456
457 // Validate the AND-only strata below every clause root with ONE global
458 // visited-set (each gate's type is path-independent), so a shared subgraph is
459 // checked once and no per-clause supports are materialised. O(circuit).
460 std::unordered_set<gate_t> seen;
461 std::stack<gate_t> st;
462 for(auto r: clause_roots)
463 st.push(r);
464 while(!st.empty()) {
465 gate_t cur = st.top();
466 st.pop();
467 if(!seen.insert(cur).second)
468 continue;
469 switch(getGateType(cur)) {
470 case BooleanGate::IN:
471 break;
472 case BooleanGate::AND:
473 for(auto s: getWires(cur))
474 st.push(s);
475 break;
476 default:
477 return false;
478 }
479 }
480 return true;
481}
482
483namespace {
484
485/**
486 * Shared Karp-Luby sampler state derived from the per-clause supports:
487 * the per-clause probability @c p_i = product of its support-leaf marginals,
488 * the prefix sums for the O(log m) categorical clause draw, the union-bound
489 * total @c S = sum p_i (with @c Pr[F] <= S <= m*Pr[F]), and the set of leaves
490 * that can affect clause membership (only those need to be drawn each round).
491 */
492struct KarpLubyState {
493 std::vector<double> p;
494 std::vector<double> cumulative;
495 double S = 0.;
496 std::vector<gate_t> relevant;
497};
498
499KarpLubyState karpLubyState(
500 const BooleanCircuit &c,
501 const std::vector<std::set<gate_t> > &supports)
502{
503 KarpLubyState st;
504 const size_t m = supports.size();
505 st.p.resize(m);
506 st.cumulative.resize(m);
507 std::set<gate_t> rel;
508 for(size_t i=0; i<m; ++i) {
509 double pi = 1.;
510 for(gate_t leaf: supports[i]) {
511 pi *= c.getProb(leaf);
512 rel.insert(leaf);
513 }
514 st.p[i] = pi;
515 st.S += pi;
516 st.cumulative[i] = st.S;
517 }
518 st.relevant.assign(rel.begin(), rel.end());
519 return st;
520}
521
522/// Seed mt19937_64 from provsql.monte_carlo_seed exactly as monteCarlo, so a
523/// pinned seed makes the estimate reproducible for the regression tests.
524std::mt19937_64 karpLubySeededRNG()
525{
526 std::mt19937_64 rng;
527 if(provsql_monte_carlo_seed != -1) {
528 rng.seed(static_cast<uint64_t>(provsql_monte_carlo_seed));
529 } else {
530 std::random_device rd;
531 rng.seed((static_cast<uint64_t>(rd()) << 32) | rd());
532 }
533 return rng;
534}
535
536/// Draw a clause index with probability @c p_i / S using the prefix sums.
537size_t karpLubyDrawClause(const KarpLubyState &st,
538 std::mt19937_64 &rng,
539 std::uniform_real_distribution<double> &u01)
540{
541 double u = u01(rng) * st.S;
542 size_t i = static_cast<size_t>(
543 std::upper_bound(st.cumulative.begin(), st.cumulative.end(), u)
544 - st.cumulative.begin());
545 if(i >= st.cumulative.size())
546 i = st.cumulative.size() - 1; // guard against u == S from rounding
547 return i;
548}
549
550/**
551 * One Karp-Luby coverage trial in clause @p i: sample an assignment of
552 * @c C_i (its support forced true, every other relevant leaf drawn from its
553 * marginal), then return whether @p i is the smallest-index clause the
554 * assignment satisfies -- the coverage rejection that divides the over-count
555 * @c S by the number of clauses covering each sampled world. @p trueLeaves is
556 * reused across calls to avoid reallocating.
557 */
558bool karpLubyCovers(
559 const BooleanCircuit &c,
560 const std::vector<std::set<gate_t> > &supports,
561 const KarpLubyState &st, size_t i,
562 std::mt19937_64 &rng,
563 std::uniform_real_distribution<double> &u01,
564 std::unordered_set<gate_t> &trueLeaves)
565{
566 trueLeaves.clear();
567 for(gate_t leaf: st.relevant) {
568 if(supports[i].count(leaf) || u01(rng) < c.getProb(leaf))
569 trueLeaves.insert(leaf);
570 }
571 const size_t m = supports.size();
572 for(size_t j=0; j<m; ++j) {
573 bool sat = true;
574 for(gate_t leaf: supports[j]) {
575 if(trueLeaves.find(leaf)==trueLeaves.end()) { sat = false; break; }
576 }
577 if(sat)
578 return j==i;
579 }
580 return false; // unreachable: clause i always covers its own forced support
581}
582
583} // anonymous namespace
584
586 const std::vector<gate_t> &clauses,
587 const std::vector<std::set<gate_t> > &supports,
588 unsigned long samples) const
589{
590 const size_t m = clauses.size();
591 if(m==0 || samples==0)
592 return 0.;
593
594 KarpLubyState st = karpLubyState(*this, supports);
595 if(st.S<=0.)
596 return 0.;
597
598 std::mt19937_64 rng = karpLubySeededRNG();
599 std::uniform_real_distribution<double> u01(0.0, 1.0);
600 std::unordered_set<gate_t> trueLeaves;
601
602 // Fewer rounds than clauses: too few to stratify (every clause needs at
603 // least one sample for its per-clause acceptance rate to be defined), so
604 // fall back to the unstratified categorical-draw estimator -- S times the
605 // overall acceptance ratio, still unbiased for any budget.
606 if(samples < m) {
607 unsigned long accepts = 0;
608 for(unsigned long s=0; s<samples; ++s) {
609 size_t i = karpLubyDrawClause(st, rng, u01);
610 if(karpLubyCovers(*this, supports, st, i, rng, u01, trueLeaves))
611 ++accepts;
613 throw CircuitException("Interrupted after "+std::to_string(s+1)+" samples");
614 }
615 return st.S * accepts / static_cast<double>(samples);
616 }
617
618 // Stratified allocation: n_i = 1 + proportional share of (samples - m) by
619 // p_i / S, with the leftover rounds handed to the largest fractional parts
620 // (largest-remainder rounding) so the n_i stay proportional and sum to
621 // exactly `samples`. Estimating each clause's acceptance rate separately
622 // and combining sum_i p_i * rate_i removes the categorical-draw
623 // (between-strata) variance of the textbook estimator, tightening the
624 // estimate at the same budget by up to a factor m.
625 std::vector<unsigned long> n(m, 1);
626 const unsigned long rest = samples - m;
627 std::vector<double> frac(m);
628 unsigned long base_sum = 0;
629 for(size_t i=0; i<m; ++i) {
630 double want = static_cast<double>(rest) * st.p[i] / st.S;
631 unsigned long fl = static_cast<unsigned long>(want);
632 n[i] += fl;
633 base_sum += fl;
634 frac[i] = want - static_cast<double>(fl);
635 }
636 unsigned long leftover = rest - base_sum;
637 if(leftover > 0) {
638 std::vector<size_t> idx(m);
639 for(size_t i=0; i<m; ++i) idx[i] = i;
640 std::partial_sort(idx.begin(), idx.begin()+leftover, idx.end(),
641 [&](size_t a, size_t b){ return frac[a] > frac[b]; });
642 for(unsigned long k=0; k<leftover; ++k)
643 ++n[idx[k]];
644 }
645
646 double est = 0.;
647 for(size_t i=0; i<m; ++i) {
648 unsigned long accepts = 0;
649 for(unsigned long k=0; k<n[i]; ++k) {
650 if(karpLubyCovers(*this, supports, st, i, rng, u01, trueLeaves))
651 ++accepts;
653 throw CircuitException("Interrupted while sampling clause "
654 +std::to_string(i));
655 }
656 est += st.p[i] * static_cast<double>(accepts) / static_cast<double>(n[i]);
657 }
658 return est;
659}
660
662 const std::vector<gate_t> &clauses,
663 const std::vector<std::set<gate_t> > &supports,
664 double eps, double delta,
665 unsigned long max_samples,
666 unsigned long &samples_used,
667 bool &reached_target) const
668{
669 samples_used = 0;
670 reached_target = false;
671 const size_t m = clauses.size();
672 if(m==0 || max_samples==0)
673 return 0.;
674
675 KarpLubyState st = karpLubyState(*this, supports);
676 if(st.S<=0.)
677 return 0.;
678
679 // DKLR stopping threshold on the accept count: Y1 = 1 + (1+eps)*Y with
680 // Y = 4*(e-2)*ln(2/delta)/eps^2. Sample coverage trials until the accept
681 // count reaches Y1 and return S*Y1/N (a relative (eps,delta) estimate of
682 // Pr[F]); the number of rounds N then adapts to the true acceptance
683 // probability Pr[F]/S in [1/m, 1] -- up to m times fewer rounds than the
684 // fixed bound when the clauses barely overlap.
685 const double e = exp(1.0);
686 const double Y = 4.0 * (e - 2.0) * log(2.0/delta) / (eps*eps);
687 const double Y1 = 1.0 + (1.0 + eps) * Y;
688
689 std::mt19937_64 rng = karpLubySeededRNG();
690 std::uniform_real_distribution<double> u01(0.0, 1.0);
691 std::unordered_set<gate_t> trueLeaves;
692
693 unsigned long accepts = 0;
694 for(unsigned long s=0; s<max_samples; ++s) {
695 size_t i = karpLubyDrawClause(st, rng, u01);
696 if(karpLubyCovers(*this, supports, st, i, rng, u01, trueLeaves)) {
697 ++accepts;
698 if(static_cast<double>(accepts) >= Y1) {
699 samples_used = s + 1;
700 reached_target = true;
701 return st.S * Y1 / static_cast<double>(samples_used);
702 }
703 }
705 throw CircuitException("Interrupted after "+std::to_string(s+1)+" samples");
706 }
707
708 // Cap reached before the threshold: the (eps,delta) target is not met, so
709 // return the plain unbiased S*accepts/N estimate over the spent budget (the
710 // caller reports the weaker guarantee actually achieved).
711 samples_used = max_samples;
712 return st.S * static_cast<double>(accepts) / static_cast<double>(max_samples);
713}
714
715/// Largest clause count for which the 2^m sieve enumeration is admitted.
716static const size_t kSieveMaxClauses = 24;
717
719 const std::vector<gate_t> &clauses,
720 const std::vector<std::set<gate_t> > &supports) const
721{
722 const size_t m = clauses.size();
723 if(m == 0)
724 return 0.;
725 if(m > kSieveMaxClauses)
726 throw CircuitException(
727 "sieve: too many clauses (" + std::to_string(m) + " > "
728 + std::to_string(kSieveMaxClauses)
729 + "); inclusion-exclusion is 2^m -- use another method");
730
731 // Pr[∨ c_i] = Σ_{∅≠S} (-1)^{|S|+1} ∏_{leaf ∈ ∪supports(S)} getProb(leaf).
732 double total = 0.;
733 std::unordered_set<gate_t> u;
734 for(unsigned long long s = 1; s < (1ULL << m); ++s) {
735 u.clear();
736 int bits = 0;
737 for(size_t i = 0; i < m; ++i)
738 if(s & (1ULL << i)) {
739 ++bits;
740 for(gate_t leaf : supports[i])
741 u.insert(leaf);
742 }
743 double p = 1.;
744 for(gate_t leaf : u)
745 p *= getProb(leaf);
746 if(bits & 1) total += p; else total -= p;
747
749 throw CircuitException("Interrupted");
750 }
751 return total;
752}
753
755 const std::vector<std::set<gate_t> > &clauses,
756 double &lower, double &upper) const
757{
758 const size_t m = clauses.size();
759 if(m == 0) {
760 lower = upper = 0.;
761 return;
762 }
763
764 // Per-clause probability P(d) = ∏_{leaf ∈ clauses[d]} getProb(leaf) (an empty
765 // support is a constant-true clause, product over the empty set = 1).
766 std::vector<double> clause_prob(m);
767 for(size_t i = 0; i < m; ++i) {
768 double p = 1.;
769 for(gate_t leaf : clauses[i])
770 p *= getProb(leaf);
771 clause_prob[i] = p;
772 }
773
774 // Greedy partition into buckets of pairwise-independent clauses, clauses taken
775 // in descending marginal-probability order (the paper's improved heuristic).
776 std::vector<size_t> order(m);
777 for(size_t i = 0; i < m; ++i)
778 order[i] = i;
779 std::sort(order.begin(), order.end(),
780 [&](size_t a, size_t b) {
781 return clause_prob[a] > clause_prob[b];
782 });
783
784 // For each bucket: the union of its clauses' supports (to test independence in
785 // O(|support|) against the whole bucket at once -- disjoint from the union iff
786 // independent of every clause already in it) and its running independent-or
787 // probability 1 - ∏(1 - P(d)).
788 std::vector<std::set<gate_t> > bucket_support;
789 std::vector<double> bucket_prob;
790 for(size_t idx : order) {
791 const std::set<gate_t> &sup = clauses[idx];
792 size_t target = bucket_support.size(); // default: open a new bucket
793 for(size_t b = 0; b < bucket_support.size(); ++b) {
794 bool disjoint = true;
795 for(gate_t leaf : sup)
796 if(bucket_support[b].count(leaf)) {
797 disjoint = false;
798 break;
799 }
800 if(disjoint) {
801 target = b;
802 break;
803 }
804 }
805 if(target == bucket_support.size()) {
806 bucket_support.emplace_back();
807 bucket_prob.push_back(0.);
808 }
809 bucket_prob[target] =
810 1. - (1. - bucket_prob[target]) * (1. - clause_prob[idx]);
811 bucket_support[target].insert(sup.begin(), sup.end());
812
814 throw CircuitException("Interrupted");
815 }
816
817 // lower = max bucket probability (each bucket is a sub-disjunction of Φ);
818 // upper = min(1, Σ bucket probabilities) (union bound over the buckets).
819 double L = 0., U = 0.;
820 for(double bp : bucket_prob) {
821 if(bp > L)
822 L = bp;
823 U += bp;
824 }
825 lower = L;
826 upper = (U > 1.) ? 1. : U;
827}
828
830{
831 /* Enumerate only the inputs reachable from g. An input the root does
832 * not reach factors out of every world (its two branches sum to 1), so
833 * restricting the enumeration returns the identical probability -- and
834 * the circuit object routinely carries gates the evaluated root no
835 * longer reaches (e.g. a categorical mulinput block whose comparison
836 * the analytic pre-pass collapsed to a single Bernoulli), which would
837 * otherwise inflate the enumeration exponentially for nothing. */
838 std::set<gate_t> rinputs;
839 {
840 std::unordered_set<gate_t> seen;
841 std::stack<gate_t> stk;
842 stk.push(g);
843 while(!stk.empty()) {
844 gate_t u = stk.top(); stk.pop();
845 if(!seen.insert(u).second) continue;
846 if(getGateType(u) == BooleanGate::IN) rinputs.insert(u);
847 for(gate_t w: getWires(u)) stk.push(w);
848 }
849 }
850
851 if(rinputs.size()>=8*sizeof(unsigned long long))
852 throw CircuitException("Too many possible worlds to iterate over");
853
854 unsigned long long nb=(1ULL<<rinputs.size());
855 double totalp=0.;
856
857 for(unsigned long long i=0; i < nb; ++i) {
858 std::unordered_set<gate_t> s;
859 double p = 1;
860
861 unsigned j=0;
862 for(gate_t in : rinputs) {
863 if(i & (1ULL << j)) {
864 s.insert(in);
865 p*=getProb(in);
866 } else {
867 p*=1-getProb(in);
868 }
869 ++j;
870 }
871
872 if(evaluate(g, s))
873 totalp+=p;
874
876 throw CircuitException("Interrupted");
877 }
878
879 return totalp;
880}
881
882std::string BooleanCircuit::TseytinCNF(gate_t g, bool display_prob, bool mapping) const {
883 std::vector<std::vector<int> > clauses;
884
885 // Tseytin transformation
886 for(gate_t i{0}; i<gates.size(); ++i) {
887 switch(getGateType(i)) {
888 case BooleanGate::AND:
889 {
890 int id{static_cast<int>(i)+1};
891 std::vector<int> c = {id};
892 for(auto s: getWires(i)) {
893 clauses.push_back({-id, static_cast<int>(s)+1});
894 c.push_back(-static_cast<int>(s)-1);
895 }
896 clauses.push_back(c);
897 break;
898 }
899
900 case BooleanGate::OR:
901 {
902 int id{static_cast<int>(i)+1};
903 std::vector<int> c = {-id};
904 for(auto s: getWires(i)) {
905 clauses.push_back({id, -static_cast<int>(s)-1});
906 c.push_back(static_cast<int>(s)+1);
907 }
908 clauses.push_back(c);
909 }
910 break;
911
912 case BooleanGate::NOT:
913 {
914 int id=static_cast<int>(i)+1;
915 auto s=*getWires(i).begin();
916 clauses.push_back({-id,-static_cast<int>(s)-1});
917 clauses.push_back({id,static_cast<int>(s)+1});
918 break;
919 }
920
922 throw CircuitException("Multivalued inputs should have been removed by then.");
924 case BooleanGate::IN:
926 ;
927 }
928 }
929 clauses.push_back({(int)g+1});
930
931 std::ostringstream oss;
932 // Optional self-documenting mapping, emitted as DIMACS comments
933 // before the problem line so a saved CNF records which provenance
934 // input each variable stands for. Comments are ignored by every
935 // model counter / compiler, so the file stays valid DIMACS.
936 if(mapping) {
937 for(const auto &m : tseytinVariableMapping()) {
938 oss << "c input " << m.variable << " "
939 << (m.uuid.empty() ? "?" : m.uuid) << " "
940 << m.probability << "\n";
941 }
942 }
943 oss << "p cnf " << gates.size() << " " << clauses.size() << "\n";
944 for(unsigned i=0; i<clauses.size(); ++i) {
945 for(int x : clauses[i]) {
946 oss << x << " ";
947 }
948 oss << "0\n";
949 }
950 if(display_prob) {
951 for(gate_t in: inputs) {
952 oss << "w " << (static_cast<std::underlying_type<gate_t>::type>(in)+1) << " " << getProb(in) << "\n";
953 oss << "w -" << (static_cast<std::underlying_type<gate_t>::type>(in)+1) << " " << (1. - getProb(in)) << "\n";
954 }
955 }
956 return oss.str();
957}
958
959std::vector<BooleanCircuit::CNFInputMapping>
961 std::vector<CNFInputMapping> mapping;
962 // `inputs` is a std::set<gate_t>, so iteration is in gate-id order
963 // and the variable indices (id + 1) come out sorted and stable.
964 for(gate_t in : inputs) {
965 auto id = static_cast<std::underlying_type<gate_t>::type>(in);
966 std::string u;
967 auto it = id2uuid.find(in);
968 if(it != id2uuid.end())
969 u = it->second;
970 mapping.push_back({static_cast<int>(id) + 1, u, getProb(in)});
971 }
972 return mapping;
973}
974
975std::string BooleanCircuit::BCS12(gate_t g, std::vector<gate_t> &inputOrder) const {
976 inputOrder.clear();
977 auto idOf = [](gate_t x) {
978 return static_cast<std::underlying_type<gate_t>::type>(x);
979 };
980
981 std::set<gate_t> seenInputs;
982 std::set<gate_t> internalGates; // AND/OR gates to emit, ordered by id
983
984 // Resolve a wire to a BC-S1.2 literal, inlining NOT chains as sign flips.
985 std::function<std::string(gate_t)> lit = [&](gate_t w) -> std::string {
986 switch(getGateType(w)) {
987 case BooleanGate::IN:
988 return "in" + std::to_string(idOf(w));
989 case BooleanGate::AND:
990 case BooleanGate::OR:
991 return "g" + std::to_string(idOf(w));
992 case BooleanGate::NOT: {
993 std::string inner = lit(*getWires(w).begin());
994 return inner[0]=='-' ? inner.substr(1) : "-"+inner;
995 }
996 default:
997 throw CircuitException("BC-S1.2 export: unsupported gate type");
998 }
999 };
1000
1001 // DFS collecting input gates (in first-seen order, fixing their d4
1002 // variable numbers) and the AND/OR gates to define; NOT gates are
1003 // traversed but never named.
1004 std::function<void(gate_t)> collect = [&](gate_t w) {
1005 switch(getGateType(w)) {
1006 case BooleanGate::IN:
1007 if(seenInputs.insert(w).second)
1008 inputOrder.push_back(w);
1009 break;
1010 case BooleanGate::NOT:
1011 collect(*getWires(w).begin());
1012 break;
1013 case BooleanGate::AND:
1014 case BooleanGate::OR:
1015 if(internalGates.insert(w).second)
1016 for(gate_t c : getWires(w))
1017 collect(c);
1018 break;
1019 default:
1020 throw CircuitException("BC-S1.2 export: unsupported gate type");
1021 }
1022 };
1023 collect(g);
1024
1025 std::ostringstream oss;
1026 oss << "c BC-S1.2\n";
1027 // Inputs first: d4 numbers them 1..k in this order (see header doc).
1028 for(gate_t in : inputOrder)
1029 oss << "I in" << idOf(in) << "\n";
1030 for(gate_t w : internalGates) {
1031 const auto &ch = getWires(w);
1032 if(ch.empty())
1033 throw CircuitException("BC-S1.2 export: nullary gate");
1034 oss << "G g" << idOf(w) << " := ";
1035 // BC-S1.2 requires >= 2 literals for A/O; a unary AND/OR is the identity.
1036 if(ch.size()==1)
1037 oss << "I";
1038 else
1039 oss << (getGateType(w)==BooleanGate::AND ? "A" : "O");
1040 for(gate_t c : ch)
1041 oss << " " << lit(c);
1042 oss << "\n";
1043 }
1044 oss << "T " << lit(g) << "\n";
1045 return oss.str();
1046}
1047
1048// ---------------------------------------------------------------------------
1049// External-tool knowledge compilation and weighted model counting.
1050//
1051// The standalone tdkc tool deliberately invokes NO external tool (it compiles
1052// purely via tree decomposition), so this whole block -- the knowledge
1053// compilers, the Panini wrapper, the weighted model counters, and the
1054// makeDD/makeDDByName dispatchers that fall back to them -- is excluded from
1055// the tdkc build. The registry is therefore used unconditionally here.
1056// ---------------------------------------------------------------------------
1057#ifndef TDKC
1058
1059// Parse a Panini (KCBox) DD output file into a d-DNNF: this is the
1060// `panini-dd` output parser, selected by compilation() for the panini-*
1061// records (which run the generic compile path -- write a Tseytin CNF, run the
1062// record's argtpl -- and differ only in this parse-back). Panini emits its
1063// own DD format (sequential node ids; "F"/"T" terminals; "C"/"D" decomposable
1064// conjunctions; decision nodes), not the c2d/d4 NNF the `nnf` parser reads.
1065// R2-D2 and CCDD emit "K" (kernelize) nodes that break decomposability, so
1066// ProvSQL does not register the variants that produce them.
1067dDNNF BooleanCircuit::parsePaniniDD(const std::string &outfilename) const {
1068 std::ifstream ifs(outfilename.c_str());
1069 if (!ifs)
1070 throw CircuitException("Cannot open Panini output: " + outfilename);
1071
1072 // Skip Panini's preamble ("Variable order: ...", "Maximum variable: ...",
1073 // "Number of nodes: ...") and stop at the first data line, which always
1074 // starts with "0:".
1075 std::string line;
1076 bool found_data = false;
1077 while (std::getline(ifs, line)) {
1078 if (line.rfind("0:", 0) == 0) { found_data = true; break; }
1079 }
1080 if (!found_data)
1081 throw CircuitException("Panini output: no data lines found");
1082
1083 dDNNF dnnf;
1084 // Panini node ids are sequential 0, 1, 2, ... Highest id is the
1085 // root of the compilation.
1086 std::vector<gate_t> id_to_gate;
1087
1088 do {
1089 if (line.empty()) continue;
1090 auto colon_pos = line.find(':');
1091 if (colon_pos == std::string::npos) continue;
1092
1093 // Sanity-check the leading id matches the size of id_to_gate so far
1094 // (the file should be in monotonically increasing id order).
1095 int panini_id = std::stoi(line.substr(0, colon_pos));
1096 if (static_cast<size_t>(panini_id) != id_to_gate.size())
1097 throw CircuitException(
1098 "Panini output: out-of-order node id "
1099 + std::to_string(panini_id));
1100
1101 std::stringstream ss(line.substr(colon_pos + 1));
1102 std::string first;
1103 ss >> first;
1104
1105 gate_t this_gate;
1106 if (first == "F") {
1107 // FALSE terminal: empty OR.
1108 this_gate = dnnf.setGate(BooleanGate::OR);
1109 } else if (first == "T") {
1110 // TRUE terminal: empty AND.
1111 this_gate = dnnf.setGate(BooleanGate::AND);
1112 } else if (first == "C" || first == "D") {
1113 // C (CONJOIN), D (DECOMPOSE) are decomposable conjunctions in
1114 // Panini's CDD format; OR is only ever expressed implicitly by
1115 // the (v ? t : f) decision nodes. K (KERNELIZE) nodes encode
1116 // literal-equivalence constraints over a shared kernel
1117 // variable and break decomposability; we refuse the only two
1118 // target languages that emit them (R2-D2 and CCDD) upstream,
1119 // so seeing K here is an upstream-Panini surprise.
1120 this_gate = dnnf.setGate(BooleanGate::AND);
1121 int child;
1122 while (ss >> child) {
1123 if (child == 0) break;
1124 if (child < 0 || static_cast<size_t>(child) >= id_to_gate.size())
1125 throw CircuitException(
1126 "Panini output: forward / invalid child reference "
1127 + std::to_string(child));
1128 dnnf.addWire(this_gate, id_to_gate[child]);
1129 }
1130 } else if (first == "K") {
1131 throw CircuitException(
1132 "Panini output: unexpected K (kernelize) node; ProvSQL "
1133 "does not support Panini target languages that emit K "
1134 "nodes (R2-D2, CCDD).");
1135 } else {
1136 // Decision node: <var> <false_child> <true_child> [0]
1137 // (Panini's CDD::Display emits children in ch[0]/ch[1] order;
1138 // CDD.cpp's DOT writer maps ch[0] to the dotted/false edge and
1139 // ch[1] to the solid/true edge.)
1140 int var = std::stoi(first);
1141 int f_child, t_child;
1142 if (!(ss >> f_child >> t_child))
1143 throw CircuitException(
1144 "Panini output: malformed decision line at id "
1145 + std::to_string(panini_id));
1146 if (t_child < 0 || f_child < 0
1147 || static_cast<size_t>(t_child) >= id_to_gate.size()
1148 || static_cast<size_t>(f_child) >= id_to_gate.size())
1149 throw CircuitException(
1150 "Panini output: forward / invalid decision child at id "
1151 + std::to_string(panini_id));
1152 gate_t t_gate = id_to_gate[t_child];
1153 gate_t f_gate = id_to_gate[f_child];
1154
1155 // Translate the decision. Two cases:
1156 // (a) v is an input gate: keep the literal in the structure,
1157 // OR(AND(v, t'), AND(NOT(v), f')).
1158 // (b) v is a Tseytin auxiliary: aux vars are functionally
1159 // determined by inputs, so under WMC we want literal
1160 // weights w(v) = w(NOT v) = 1 (not (p, 1-p)). With those
1161 // weights the AND wrappers contribute 1 to either branch
1162 // and we can drop them entirely, emitting just
1163 // OR(t', f'). The OR is not determinism-preserving on
1164 // the variable v, but the input-projection of its two
1165 // arms is still disjoint by Tseytin determinism so
1166 // @c dDNNF::probabilityEvaluation() still returns the
1167 // correct weighted model count.
1168 size_t var_idx = static_cast<size_t>(var) - 1;
1169 if (var_idx < gates.size() && gates[var_idx] == BooleanGate::IN) {
1170 gate_t pos_lit = dnnf.setGate(
1171 getUUID(static_cast<gate_t>(var_idx)),
1172 BooleanGate::IN, prob[var_idx]);
1173 gate_t neg_lit = dnnf.setGate(BooleanGate::NOT);
1174 dnnf.addWire(neg_lit, pos_lit);
1175 gate_t and_t = dnnf.setGate(BooleanGate::AND);
1176 dnnf.addWire(and_t, pos_lit);
1177 dnnf.addWire(and_t, t_gate);
1178 gate_t and_f = dnnf.setGate(BooleanGate::AND);
1179 dnnf.addWire(and_f, neg_lit);
1180 dnnf.addWire(and_f, f_gate);
1181 this_gate = dnnf.setGate(BooleanGate::OR);
1182 dnnf.addWire(this_gate, and_t);
1183 dnnf.addWire(this_gate, and_f);
1184 } else {
1185 this_gate = dnnf.setGate(BooleanGate::OR);
1186 dnnf.addWire(this_gate, t_gate);
1187 dnnf.addWire(this_gate, f_gate);
1188 }
1189 }
1190 id_to_gate.push_back(this_gate);
1191 } while (std::getline(ifs, line));
1192
1193 ifs.close();
1194
1195 if (id_to_gate.empty())
1196 throw CircuitException("Panini output produced no nodes");
1197
1198 // The root of a Panini DD is the highest-id node.
1199 dnnf.setRoot(id_to_gate.back());
1200
1201 dnnf.simplify();
1202 return dnnf;
1203}
1204
1205// Preference-ranked tool selection for @p operation: honour the explicitly
1206// @p preferred tool when it is enabled, advertises the operation, and is
1207// available; otherwise return the highest-preference enabled tool advertising
1208// the operation whose binary and dependencies resolve on PATH. Returns "" if
1209// none is available, so a dispatcher can fall back or raise a clear error.
1210// This replaces the old "if d4 else c2d ..." chains: an admin reorders or
1211// disables tools (or bumps provsql.fallback_compiler, honoured via @p
1212// preferred) and selection follows.
1213static std::string selectTool(const std::string &operation,
1214 const std::string &preferred = "") {
1215 if(!preferred.empty()) {
1216 const provsql::ToolRecord *r = provsql::tool_registry().find(preferred);
1217 if(r != nullptr && r->enabled && r->hasOperation(operation)
1218 && toolAvailable(*r))
1219 return preferred;
1220 }
1221 for(const provsql::ToolRecord *r : provsql::tool_registry().byOperation(operation))
1222 if(toolAvailable(*r))
1223 return r->name;
1224 return "";
1225}
1226
1227// The compiler for makeDD's last-resort fallback route: prefer
1228// provsql.fallback_compiler (default "d4") when available, otherwise the
1229// highest-preference compiler whose binary resolves on PATH. Falls back to
1230// the configured name when nothing is available, so compilation() raises its
1231// actionable error. (The GUC governs only this fallback route; an explicit
1232// no-argument compilation() request just takes the best available compiler --
1233// symmetric with the no-tool wmc path.)
1234static std::string chooseCompiler() {
1235 const char *fb = (provsql_fallback_compiler != NULL
1236 && provsql_fallback_compiler[0] != '\0')
1238 std::string chosen = selectTool("compile", fb);
1239 return chosen.empty() ? std::string(fb) : chosen;
1240}
1241
1243 std::string *resolved) const {
1244 // No compiler named: pick the highest-preference available one (symmetric
1245 // with the no-tool wmc path). provsql.fallback_compiler is deliberately
1246 // not consulted here -- it governs only makeDD's last-resort fallback route.
1247 if(compiler.empty()) {
1248 compiler = selectTool("compile");
1249 if(compiler.empty())
1250 throw CircuitException(
1251 "no knowledge compiler is available; install one (d4, d4v2, "
1252 "c2d, minic2d, dsharp) or add its directory to "
1253 "provsql.tool_search_path");
1254 }
1255
1256 // Validate the compiler against the registry before any temp-dir or CNF
1257 // work. A name that is not a registered, enabled 'compile' tool is
1258 // rejected here. compilation() implements two output parsers: the tolerant
1259 // `nnf` reader (both the d4-family header-less NNF and the c2d-style header
1260 // form) and the `panini-dd` reader (Panini's own DD format); a compile tool
1261 // advertising any other parser is something we cannot read back, so reject
1262 // it rather than mis-parse.
1263 const provsql::ToolRecord *rec = provsql::tool_registry().find(compiler);
1264 if(rec == nullptr || !rec->hasOperation("compile"))
1265 throw CircuitException("Unknown compiler '"+compiler+"'");
1266 if(!rec->enabled)
1267 throw CircuitException(
1268 "Compiler '"+compiler+"' is disabled in the tool registry");
1269 if(rec->parser != "nnf" && rec->parser != "panini-dd")
1270 throw CircuitException(
1271 "Compiler '"+compiler+"' uses output parser '"+rec->parser
1272 +"', which compilation() does not implement");
1273 const std::string compiler_binary = rec->binary;
1274 if(resolved)
1275 *resolved = compiler; // the validated tool actually used (CLI or KCMCP)
1276
1277 // KCMCP backend: compile over a warm socket server instead of spawning a
1278 // CLI tool. The problem is sent as a native BC-S1.2 circuit when the record
1279 // advertises that input, else as a Tseytin CNF; the RESULT's d-DNNF text is
1280 // parsed by the same parseDDNNF() the CLI path uses. Any failure (connect,
1281 // protocol, server ERROR) raises, so makeDD's fallback can try another tool.
1282 if(rec->kind == "kcmcp") {
1283 std::vector<gate_t> inputOrder;
1284 std::string content;
1285 uint8_t input_format = 0; // dimacs-cnf
1286 if(rec->acceptsInput("circuit-bcs12")) {
1287 try {
1288 content = BCS12(g, inputOrder);
1289 input_format = 1; // circuit-bcs12
1290 } catch(const CircuitException &) {
1291 inputOrder.clear();
1292 content.clear();
1293 }
1294 }
1295 if(content.empty())
1296 content = TseytinCNF(g, false); // inputOrder stays empty => CNF mode
1297 // Resolve the server address: a literal 'managed' endpoint defers to the
1298 // live address the supervisor worker published in shared memory; anything
1299 // else is a fixed endpoint (unix:/path or host:port).
1300 std::string endpoint = rec->endpoint;
1301 if(endpoint == "managed")
1302 endpoint = provsql_kcmcp_managed_endpoint();
1303 if(endpoint.empty())
1304 throw CircuitException(
1305 "KCMCP tool '"+compiler+"' has no endpoint (managed server not "
1306 "running, or provsql.kcmcp_server unset)");
1307 try {
1308 std::string nnf = provsql::kcmcp_compile(endpoint, input_format, content);
1309 std::istringstream iss(nnf);
1310 return parseDDNNF(iss, inputOrder);
1311 } catch(const CircuitException &) {
1312 throw;
1313 } catch(const std::exception &e) {
1314 throw CircuitException(std::string("KCMCP compile via '")+compiler
1315 +"' failed: "+e.what());
1316 }
1317 }
1318
1319 // A compiler that advertises the BC-S1.2 circuit input (KCMCP
1320 // "circuit-bcs12") is driven with native circuit input: the Boolean circuit
1321 // is sent directly instead of a Tseytin CNF, skipping the CNF transform and
1322 // the aux-variable reconciliation in the parse-back. Requires a parser
1323 // that honours `I` declarations; on a gate shape BC-S1.2 cannot express, we
1324 // fall back to the Tseytin CNF path below. (Today only d4v2 advertises it.)
1325 bool circuit_input = rec->acceptsInput("circuit-bcs12")
1326 && !rec->argtpl_circuit.empty();
1327 if(find_external_tool(compiler_binary).empty())
1328 throw CircuitException(
1329 compiler_binary + " not found on PATH; install it or add its "
1330 "directory to provsql.tool_search_path");
1331
1332 ScopedTempDir tmp;
1333 std::string filename = tmp.file("input");
1334 std::string outfilename = tmp.file("input.nnf");
1335 // In circuit mode, inputOrder[v-1] is the IN gate for d4 variable v
1336 // (1-based); empty in CNF mode.
1337 std::vector<gate_t> inputOrder;
1338 std::string content;
1339 if(circuit_input) {
1340 try {
1341 content = BCS12(g, inputOrder);
1342 } catch(const CircuitException &) {
1343 // A gate shape BC-S1.2 cannot express: fall back to the CNF path.
1344 circuit_input = false;
1345 inputOrder.clear();
1346 }
1347 }
1348 if(!circuit_input)
1349 content = TseytinCNF(g, false);
1350 {
1351 std::ofstream ofs(filename);
1352 ofs << content;
1353 }
1354
1355 if(provsql_verbose>=20) {
1356 provsql_notice("Tseytin circuit in %s", filename.c_str());
1357 }
1358
1359 // The command line is the registry argtpl with {in}/{out} (and {binary})
1360 // substituted -- so a newly-registered compiler runs with its own
1361 // invocation, no per-name branch here. When the BC-S1.2 circuit input is
1362 // in use, the record's argtpl_circuit is used instead (it pairs with the
1363 // circuit input written above and the circuit-mode variable resolution in
1364 // the parse-back).
1365 std::string cmdline;
1366 if(circuit_input)
1368 compiler_binary, filename, outfilename);
1369 else
1370 cmdline = rec->buildCommand(filename, outfilename, compiler_binary);
1371
1372 int retvalue=run_external_tool(cmdline);
1373
1374 // run_external_tool runs the compiler in its own process group and
1375 // raises any pending cancel/terminate itself (after killing the child),
1376 // so a statement_timeout / pg_cancel_backend surfaces as 57014 here
1377 // rather than being masked by the "killed by signal" throw.
1378 //
1379 // (An older d4 CLI without `-dDNNF` is not handled as a compiled-in
1380 // special case: a deployment using it can register a tool with the
1381 // appropriate argtpl, e.g. register_tool('d4-old', argtpl => '{in} -out={out}', ...).)
1382 CHECK_FOR_INTERRUPTS();
1383
1384 if(retvalue)
1385 throw CircuitException(format_external_tool_status(retvalue, compiler));
1386
1387 // Read the result back with the parser the record advertises. Panini's DD
1388 // format has its own reader; everything else is the tolerant NNF parser.
1389 if(rec->parser == "panini-dd")
1390 return parsePaniniDD(outfilename);
1391
1392 std::ifstream ifs(outfilename.c_str());
1393 dDNNF dnnf = parseDDNNF(ifs, inputOrder);
1394 ifs.close();
1395
1396 if(provsql_verbose>=20) {
1397 tmp.keep();
1398 provsql_notice("Compiled d-DNNF in %s", outfilename.c_str());
1399 }
1400
1401 return dnnf;
1402}
1403
1404// Parse a c2d/d4 NNF stream into a dDNNF over this circuit's input gates.
1405// Shared by the CLI compilation() path and the KCMCP client; see the header.
1407 const std::vector<gate_t> &inputOrder) const {
1408 const bool circuit_input = !inputOrder.empty();
1409
1410 std::string line;
1411 getline(in,line);
1412
1413 // Tolerant NNF detection (the single `nnf` parser): the classic c2d/d4
1414 // form opens with an "nnf <nodes> <edges> <vars>" magic line and roots at
1415 // the last node; the d4-family form has no magic line and roots at gate
1416 // "1". A classic compiler emits the header iff satisfiable, so a missing
1417 // header on an empty file is an unsatisfiable formula; a missing header on
1418 // a non-empty file is the d4-family form. (The d4v2 native-circuit path
1419 // also produces the header-less d4 form.)
1420 bool new_d4;
1421 if(line.rfind("nnf", 0) != 0) {
1422 if(line.empty()) {
1423 // unsatisfiable formula (empty output)
1424 return dDNNF();
1425 }
1426 new_d4 = true;
1427 } else {
1428 new_d4 = false;
1429 std::string nnf;
1430 unsigned nb_nodes, nb_edges, nb_variables;
1431
1432 std::stringstream ss(line);
1433 ss >> nnf >> nb_nodes >> nb_edges >> nb_variables;
1434
1435 if(nb_variables!=gates.size())
1436 throw CircuitException("Unreadable d-DNNF (wrong number of variables: " + std::to_string(nb_variables) +" vs " + std::to_string(gates.size()) + ")");
1437
1438 getline(in,line);
1439 }
1440
1441 dDNNF dnnf;
1442
1443 // Map a d-DNNF literal's variable to the IN gate it stands for, if any.
1444 // CNF mode: variable = gate id + 1, real only for IN gates (every other
1445 // variable is a Tseytin auxiliary to skip). Circuit mode: variables 1..k
1446 // are the inputs in BCS12's declaration order, everything above k is an
1447 // internal-gate variable to skip.
1448 size_t k = inputOrder.size();
1449 auto resolveVar = [&](int v) -> std::pair<bool, gate_t> {
1450 unsigned idx = static_cast<unsigned>(abs(v));
1451 if(circuit_input) {
1452 if(idx>=1 && idx<=k)
1453 return {true, inputOrder[idx-1]};
1454 return {false, gate_t{}};
1455 }
1456 if(idx>=1 && (idx-1) < gates.size() && gates[idx-1]==BooleanGate::IN)
1457 return {true, static_cast<gate_t>(idx-1)};
1458 return {false, gate_t{}};
1459 };
1460
1461 unsigned i=0;
1462 do {
1463 std::stringstream ss(line);
1464
1465 std::string c;
1466 ss >> c;
1467
1468 if(c=="O") {
1469 int var, args;
1470 ss >> var >> args;
1471 auto id=dnnf.getGate(std::to_string(i));
1472 dnnf.setGate(std::to_string(i), BooleanGate::OR);
1473 int g;
1474 while(ss >> g) {
1475 auto id2=dnnf.getGate(std::to_string(g));
1476 dnnf.addWire(id,id2);
1477 }
1478 } else if(c=="A") {
1479 int args;
1480 ss >> args;
1481 auto id=dnnf.getGate(std::to_string(i));
1482 dnnf.setGate(std::to_string(i), BooleanGate::AND);
1483 int g;
1484 while(ss >> g) {
1485 auto id2=dnnf.getGate(std::to_string(g));
1486 dnnf.addWire(id,id2);
1487 }
1488 } else if(c=="L") {
1489 int leaf;
1490 ss >> leaf;
1491 auto and_gate=dnnf.setGate(std::to_string(i), BooleanGate::AND);
1492 auto [is_in, in_gate] = resolveVar(leaf);
1493 if(is_in) {
1494 auto pid = static_cast<std::underlying_type<gate_t>::type>(in_gate);
1495 auto leaf_gate = dnnf.setGate(getUUID(in_gate), BooleanGate::IN, prob[pid]);
1496 if(leaf<0) {
1497 auto not_gate = dnnf.setGate(BooleanGate::NOT);
1498 dnnf.addWire(not_gate, leaf_gate);
1499 dnnf.addWire(and_gate, not_gate);
1500 } else {
1501 dnnf.addWire(and_gate, leaf_gate);
1502 }
1503 } else {
1504 ; // Do nothing, TRUE gate
1505 }
1506 } else if(c=="f" || c=="o") {
1507 // d4 extended format
1508 // A FALSE gate is an OR gate without wires
1509 int var;
1510 ss >> var;
1511 dnnf.setGate(std::to_string(var), BooleanGate::OR);
1512 } else if(c=="t" || c=="a") {
1513 // d4 extended format
1514 // A TRUE gate is an AND gate without wires
1515 int var;
1516 ss >> var;
1517 dnnf.setGate(std::to_string(var), BooleanGate::AND);
1518 } else if(dnnf.hasGate(c)) {
1519 // d4 extended format
1520 int var;
1521 ss >> var;
1522 auto id2=dnnf.getGate(std::to_string(var));
1523
1524 std::vector<int> decisions;
1525 int decision;
1526 while(ss >> decision) {
1527 if(decision==0)
1528 break;
1529 // Edges carry decision literals over both real inputs and internal
1530 // variables (Tseytin auxiliaries in CNF mode, gate variables in
1531 // circuit mode). Keep only the input literals; the rest are
1532 // functionally determined and projected out (sound for probability).
1533 if(resolveVar(decision).first)
1534 decisions.push_back(decision);
1535 }
1536
1537 if(decisions.empty()) {
1538 dnnf.addWire(dnnf.getGate(c), id2);
1539 } else {
1540 auto and_gate = dnnf.setGate(BooleanGate::AND);
1541 dnnf.addWire(dnnf.getGate(c), and_gate);
1542 dnnf.addWire(and_gate, id2);
1543 for(auto leaf : decisions) {
1544 auto in_gate = resolveVar(leaf).second;
1545 auto pid = static_cast<std::underlying_type<gate_t>::type>(in_gate);
1546 auto leaf_gate = dnnf.setGate(getUUID(in_gate), BooleanGate::IN, prob[pid]);
1547 if(leaf<0) {
1548 auto not_gate = dnnf.setGate(BooleanGate::NOT);
1549 dnnf.addWire(not_gate, leaf_gate);
1550 dnnf.addWire(and_gate, not_gate);
1551 } else {
1552 dnnf.addWire(and_gate, leaf_gate);
1553 }
1554 }
1555 }
1556 } else
1557 throw CircuitException(std::string("Unreadable d-DNNF (unknown node type: ")+c+")");
1558
1559 ++i;
1560 } while(getline(in, line));
1561
1562 dnnf.setRoot(dnnf.getGate(new_d4?"1":std::to_string(i-1)));
1563
1564 // External NNF writers (c2d, minic2d, dsharp) leave TRUE constants
1565 // (empty AND gates) and FALSE constants (empty OR gates) embedded in
1566 // the structure, because their target formats (Decision-DNNF, SDD)
1567 // require every variable to be "covered" even when its value is
1568 // forced by the CNF. Run the standard peephole so the d-DNNF returned
1569 // to callers is in canonical form, matching the tree-decomposition
1570 // builder which already simplifies.
1571 dnnf.simplify();
1572
1573 return dnnf;
1574}
1575
1576// Generic weighted-model-counting runner. Selects the counter from the
1577// registry by logical name, checks its binary and dependencies resolve,
1578// writes the weighted CNF in the convention its `parser` implies, runs the
1579// record's argtpl and reads the count back the same way -- so the four
1580// per-counter methods (ganak, sharpsat-td, dpmc, weightmc) share one
1581// runner, and a wmc tool speaking a known convention is registrable without
1582// code. The two conventions, keyed by `parser`:
1583// wmc-line MCC-2024 weighted DIMACS in ("c t wmc" + "c p weight" lines);
1584// the count on a "c s exact" / "s wmc" line out.
1585// weightmc weightmc's own weighted DIMACS in; a "mantissa x 2^exp" out.
1586double BooleanCircuit::wmcCount(gate_t g, const std::string &requested,
1587 const std::string &opt) const {
1588 // An empty tool name means "pick the best available counter" (highest
1589 // preference whose binary + dependencies resolve on PATH).
1590 std::string tool = requested;
1591 if(tool.empty()) {
1592 tool = selectTool("wmc");
1593 if(tool.empty())
1594 throw CircuitException(
1595 "no weighted model counter is available; install one (ganak, "
1596 "sharpsat-td, dpmc, weightmc) or add its directory to "
1597 "provsql.tool_search_path");
1598 }
1599
1600 const provsql::ToolRecord *rec = provsql::tool_registry().find(tool);
1601 if(rec == nullptr || !rec->hasOperation("wmc"))
1602 throw CircuitException("Unknown wmc tool '" + tool + "'");
1603 if(!rec->enabled)
1604 throw CircuitException("Tool '" + tool + "' is disabled in the tool registry");
1605
1606 // The binary (when the tool has one of its own) and every dependency must
1607 // resolve on PATH. dpmc has no binary of its own -- it is the htb | dmc
1608 // pipeline named entirely in its template -- so its components are its
1609 // dependencies.
1610 if(!rec->binary.empty() && find_external_tool(rec->binary).empty())
1611 throw CircuitException(
1612 rec->binary + " not found on PATH; install it or add its "
1613 "directory to provsql.tool_search_path");
1614 for(const std::string &dep : rec->dependencies)
1615 if(find_external_tool(dep).empty())
1616 throw CircuitException(
1617 tool + " needs '" + dep + "' on PATH; install it or add its "
1618 "directory to provsql.tool_search_path");
1619
1620 const bool weightmc_io = (rec->parser == "weightmc");
1621
1622 ScopedTempDir tmp;
1623 const std::string &dirname = tmp.path();
1624 std::string filename = tmp.file("input");
1625 std::string outfilename = tmp.file("input.out");
1626 {
1627 std::ofstream ofs(filename);
1628 if(weightmc_io) {
1629 // weightmc reads weights inline, in its own weighted-DIMACS dialect.
1630 ofs << TseytinCNF(g, true);
1631 } else {
1632 // MCC 2024 weighted DIMACS: a plain CNF plus per-input weight lines.
1633 ofs << "c t wmc\n";
1634 ofs << TseytinCNF(g, false);
1635 for(gate_t in : inputs) {
1636 int id = static_cast<int>(in) + 1;
1637 ofs << "c p weight " << id << ' ' << getProb(in) << " 0\n";
1638 ofs << "c p weight -" << id << ' ' << (1.0 - getProb(in)) << " 0\n";
1639 }
1640 }
1641 }
1642
1643 // {tmpdir} (sharpsat-td's flowcutter scratch) and {pivotAC} (weightmc's
1644 // approximation tolerance, from opt='delta;epsilon') are offered as
1645 // template placeholders; a tool that does not reference one ignores it.
1646 double epsilon = 0.8;
1647 {
1648 std::stringstream ssopt(opt);
1649 std::string delta_s, epsilon_s;
1650 getline(ssopt, delta_s, ';');
1651 getline(ssopt, epsilon_s, ';');
1652 try { double e = stod(epsilon_s); if(e != 0) epsilon = e; }
1653 catch(const std::exception &) {}
1654 }
1655 const double pivotAC = 2*ceil(exp(3./2)*(1+1/epsilon)*(1+1/epsilon));
1656
1657 std::string cmdline = rec->buildCommand(
1658 filename, outfilename, rec->binary,
1659 {{"tmpdir", dirname}, {"pivotAC", std::to_string(pivotAC)}});
1660
1661 int retvalue = run_external_tool(cmdline);
1662 CHECK_FOR_INTERRUPTS();
1663 if(retvalue)
1664 throw CircuitException(format_external_tool_status(retvalue, tool));
1665
1666 std::ifstream ifs(outfilename.c_str());
1667 double ret;
1668 if(weightmc_io) {
1669 // weightmc prints the count as "<mantissa> x 2^<exp>" on its last line.
1670 std::string line, prev_line;
1671 while(getline(ifs, line)) prev_line = line;
1672 std::stringstream ss(prev_line);
1673 std::string result;
1674 ss >> result >> result >> result >> result >> result;
1675 std::istringstream iss(result);
1676 std::string val, exp;
1677 getline(iss, val, 'x');
1678 getline(iss, exp);
1679 if(exp.size() < 2)
1680 throw CircuitException("weightmc: could not parse '" + prev_line + "'");
1681 double value = stod(val);
1682 double exponent = stod(exp.substr(2));
1683 ret = value * pow(2.0, exponent);
1684 } else {
1685 // The count is on the last "c s exact ..." (or "s wmc ...") line;
1686 // parse_wmc_value tolerates the per-tool token layout on that line.
1687 std::string line, matched;
1688 while(getline(ifs, line))
1689 if(line.rfind("c s exact", 0) == 0 || line.rfind("s wmc", 0) == 0)
1690 matched = line;
1691 if(matched.empty())
1692 throw CircuitException(tool + ": could not find a count line in output");
1693 ret = parse_wmc_value(matched, tool.c_str());
1694 }
1695
1696 if(provsql_verbose >= 20)
1697 tmp.keep();
1698 return ret;
1699}
1700
1701#endif // external-tool compilation / counting (excluded from tdkc)
1702
1704 gate_t g, std::set<gate_t> &seen,
1705 std::unordered_map<gate_t, double> &memo) const
1706{
1707 check_stack_depth(); // recurses on wires; guard deep circuits (see GenericCircuit::evaluate)
1708 // Memoised gates are variable-free (constant-only) -- returning the cached
1709 // value is sound (it touched nothing in `seen`) and avoids re-traversing a
1710 // shared constant subgraph. A variable-bearing gate is never cached, so a
1711 // second visit re-enters its subtree and throws on the repeated variable.
1712 {
1713 auto it = memo.find(g);
1714 if(it != memo.end())
1715 return it->second;
1716 }
1717
1718 // A certified gate (see DNNF_CERT_INFO) opens a maximal island, walked
1719 // iteratively (certified circuits can be as deep as the data). A
1720 // certified gate reached a second time -- from another island or from
1721 // the uncertified region -- is re-walked: its variables then hit `seen`
1722 // and the evaluation throws, so entanglement across islands is
1723 // conservatively rejected, exactly like a read-once violation.
1724 if(isDNNFCertified(g))
1725 return evaluateCertifiedIsland(g, seen, memo);
1726
1727 const std::size_t seen_before = seen.size();
1728
1729 double result=1.;
1730
1731 switch(getGateType(g)) {
1732 case BooleanGate::AND:
1733 for(const auto &c: getWires(g)) {
1734 result*=independentEvaluationInternal(c, seen, memo);
1735 }
1736 break;
1737
1738 case BooleanGate::OR:
1739 {
1740 // We collect probability among each group of children, where we
1741 // group MULIN gates with the same key var together
1742 std::map<gate_t, double> groups;
1743 std::set<gate_t> local_mulins;
1744 std::set<std::pair<gate_t, unsigned> > mulin_seen;
1745
1746 for(const auto &c: getWires(g)) {
1747 auto group = c;
1749 group = *getWires(c).begin();
1750 if(local_mulins.find(group)==local_mulins.end()) {
1751 if(seen.find(group)!=seen.end())
1752 throw CircuitException("Not an independent circuit");
1753 else
1754 seen.insert(group);
1755 local_mulins.insert(group);
1756 }
1757 auto p = std::make_pair(group, getInfo(c));
1758 if(mulin_seen.find(p)==mulin_seen.end()) {
1759 groups[group] += getProb(c);
1760 mulin_seen.insert(p);
1761 }
1762 } else
1763 groups[group] = independentEvaluationInternal(c, seen, memo);
1764 }
1765
1766 for(const auto [k, v]: groups)
1767 result *= 1-v;
1768 result = 1-result;
1769 }
1770 break;
1771
1772 case BooleanGate::NOT:
1773 result=1-independentEvaluationInternal(*getWires(g).begin(), seen, memo);
1774 break;
1775
1776 case BooleanGate::IN:
1777 {
1778 /* A leaf with probability 0 or 1 is a constant : it carries no
1779 * Boolean variable that can collide with another occurrence of
1780 * itself. Skip the seen-set bookkeeping so circuits where the
1781 * shared subgraphs are all constants (e.g. RangeCheck-resolved
1782 * comparators flowing through a non-tree structure, or
1783 * user-flipped Bernoullis pinned to 0 / 1) stay evaluable under
1784 * the read-once `independent` method. Anything strictly between
1785 * 0 and 1 is a real Bernoulli variable and must remain
1786 * read-once. */
1787 const double p = getProb(g);
1788 if (p == 0.0 || p == 1.0) {
1789 result = p;
1790 break;
1791 }
1792 if(seen.find(g)!=seen.end())
1793 throw CircuitException("Not an independent circuit");
1794 seen.insert(g);
1795 result=p;
1796 }
1797 break;
1798
1799 case BooleanGate::MULIN:
1800 {
1801 auto child = *getWires(g).begin();
1802 if(seen.find(child)!=seen.end())
1803 throw CircuitException("Not an independent circuit");
1804 seen.insert(child);
1805 result=getProb(g);
1806 }
1807 break;
1808
1811 throw CircuitException("Bad gate");
1812 }
1813
1814 // Cache only if this gate consumed no variable (constant-only subgraph).
1815 if(seen.size() == seen_before)
1816 memo[g] = result;
1817 return result;
1818}
1819
1821 gate_t root, std::set<gate_t> &seen,
1822 std::unordered_map<gate_t, double> &memo) const
1823{
1824 const std::size_t seen_before = seen.size();
1825 // Island-local values: within the island every gate is computed once
1826 // (sharing is licensed by the certificate), and the explicit post-order
1827 // stack keeps the walk safe on circuits as deep as the data.
1828 std::unordered_map<gate_t, double> val;
1829 // Key variables of MULIN gates already registered within this island: a
1830 // BID block's alternatives share their key variable by design (they are
1831 // mutually exclusive outcomes appearing under deterministic ORs), so
1832 // only the first registers globally.
1833 std::unordered_set<gate_t> island_mulvars;
1834 std::vector<gate_t> stack{root};
1835
1836 while(!stack.empty()) {
1837 const gate_t g = stack.back();
1838 if(val.find(g) != val.end()) {
1839 stack.pop_back();
1840 continue;
1841 }
1842
1843 const auto t = getGateType(g);
1844
1845 if(t == BooleanGate::IN) {
1846 // Same constant-leaf exemption as the read-once walk: a 0/1 leaf
1847 // carries no Boolean variable.
1848 const double p = getProb(g);
1849 if(p != 0.0 && p != 1.0) {
1850 if(seen.find(g) != seen.end())
1851 throw CircuitException("Not an independent circuit");
1852 seen.insert(g);
1853 }
1854 val[g] = p;
1855 stack.pop_back();
1856 continue;
1857 }
1858 if(t == BooleanGate::MULIN) {
1859 auto child = *getWires(g).begin();
1860 if(island_mulvars.insert(child).second) {
1861 if(seen.find(child) != seen.end())
1862 throw CircuitException("Not an independent circuit");
1863 seen.insert(child);
1864 }
1865 val[g] = getProb(g);
1866 stack.pop_back();
1867 continue;
1868 }
1869 if(t != BooleanGate::NOT && !isDNNFCertified(g)) {
1870 // Uncertified gate inside the island: standard read-once rules (its
1871 // own certified descendants open fresh sub-islands).
1872 val[g] = independentEvaluationInternal(g, seen, memo);
1873 stack.pop_back();
1874 continue;
1875 }
1876
1877 bool ready = true;
1878 for(const auto &c: getWires(g))
1879 if(val.find(c) == val.end()) {
1880 stack.push_back(c);
1881 ready = false;
1882 }
1883 if(!ready)
1884 continue;
1885
1886 double result;
1887 if(t == BooleanGate::NOT)
1888 result = 1 - val[getWires(g)[0]];
1889 else if(t == BooleanGate::AND) {
1890 // Certified decomposable AND: product.
1891 result = 1.;
1892 for(const auto &c: getWires(g))
1893 result *= val[c];
1894 } else {
1895 // Certified deterministic OR: plain sum (mutual exclusivity).
1896 result = 0.;
1897 for(const auto &c: getWires(g))
1898 result += val[c];
1899 }
1900 val[g] = result;
1901 stack.pop_back();
1902 }
1903
1904 // Same global-memo rule as the recursive walk: cache only when the
1905 // island consumed no variable.
1906 if(seen.size() == seen_before)
1907 memo[root] = val[root];
1908 return val[root];
1909}
1910
1912{
1913 std::set<gate_t> seen;
1914 std::unordered_map<gate_t, double> memo;
1915 return independentEvaluationInternal(g, seen, memo);
1916}
1917
1918void BooleanCircuit::setInfo(gate_t g, unsigned int i)
1919{
1920 info[g] = i;
1921}
1922
1924{
1925 auto it = info.find(g);
1926
1927 if(it==info.end())
1928 return 0;
1929 else
1930 return it->second;
1931}
1932
1934 const std::vector<gate_t> &muls,
1935 const std::vector<double> &cumulated_probs,
1936 unsigned start,
1937 unsigned end,
1938 std::vector<gate_t> &prefix)
1939{
1940 if(start==end) {
1941 getWires(muls[start]) = prefix;
1942 return;
1943 }
1944
1945 unsigned mid = (start+end)/2;
1946 // cumulated_probs is an *inclusive* prefix sum (cumulated_probs[i] =
1947 // p[0]+...+p[i]). The conditional probability of being in the left
1948 // half [start..mid] given the range [start..end] is therefore
1949 // (cum[mid] - cum[start-1]) / (cum[end] - cum[start-1])
1950 // with cum[-1] treated as 0 when start==0.
1951 double prev_start = (start == 0) ? 0. : cumulated_probs[start - 1];
1952 auto g = setGate(
1954 (cumulated_probs[mid] - prev_start) /
1955 (cumulated_probs[end] - prev_start));
1956 auto not_g = setGate(BooleanGate::NOT);
1957 getWires(not_g).push_back(g);
1958
1959 prefix.push_back(g);
1960 rewriteMultivaluedGatesRec(muls, cumulated_probs, start, mid, prefix);
1961 prefix.pop_back();
1962 prefix.push_back(not_g);
1963 rewriteMultivaluedGatesRec(muls, cumulated_probs, mid+1, end, prefix);
1964 prefix.pop_back();
1965}
1966
1967/**
1968 * @brief Check whether two double values are approximately equal.
1969 * @param a First value.
1970 * @param b Second value.
1971 * @return @c true if @p a and @p b differ by less than 10× machine epsilon.
1972 */
1973static constexpr bool almost_equals(double a, double b)
1974{
1975 double diff = a - b;
1976 constexpr double epsilon = std::numeric_limits<double>::epsilon() * 10;
1977
1978 return (diff < epsilon && diff > -epsilon);
1979}
1980
1982{
1983 std::map<gate_t,std::vector<gate_t> > var2mulinput;
1984 for(auto mul: mulinputs) {
1985 var2mulinput[*getWires(mul).begin()].push_back(mul);
1986 }
1987 mulinputs.clear();
1988
1989 for(const auto &[var, muls]: var2mulinput)
1990 {
1991 const unsigned n = muls.size();
1992 std::vector<double> cumulated_probs(n);
1993 double cumulated_prob=0.;
1994
1995 for(unsigned i=0; i<n; ++i) {
1996 cumulated_prob += getProb(muls[i]);
1997 cumulated_probs[i] = cumulated_prob;
1998 gates[static_cast<std::underlying_type<gate_t>::type>(muls[i])] = BooleanGate::AND;
1999 getWires(muls[i]).clear();
2000 }
2001
2002 std::vector<gate_t> prefix;
2003 prefix.reserve(static_cast<unsigned>(log(n)/log(2)+2));
2004 if(!almost_equals(cumulated_probs[n-1],1.)) {
2005 prefix.push_back(setGate(BooleanGate::IN, cumulated_probs[n-1]));
2006 }
2007 rewriteMultivaluedGatesRec(muls, cumulated_probs, 0, n-1, prefix);
2008 }
2009}
2010
2011gate_t BooleanCircuit::interpretAsDDInternal(gate_t g, std::set<gate_t> &seen, dDNNF &dd) const {
2012 check_stack_depth(); // recurses on wires; guard deep circuits (see GenericCircuit::evaluate)
2013
2014 // A certified gate (see DNNF_CERT_INFO) opens a maximal island, copied
2015 // iteratively with native deterministic ORs; the recursion only walks
2016 // the uncertified region.
2017 if(isDNNFCertified(g))
2018 return interpretCertifiedIsland(g, seen, dd);
2019
2020 gate_t dg{0};
2021
2022 switch(getGateType(g)) {
2023 case BooleanGate::AND:
2024 {
2025 dg = dd.setGate(BooleanGate::AND);
2026 for(const auto &c: getWires(g)) {
2027 auto dc = interpretAsDDInternal(c, seen, dd);
2028 dd.addWire(dg, dc);
2029 }
2030 }
2031 break;
2032
2033 case BooleanGate::OR:
2034 {
2035 dg = dd.setGate(BooleanGate::NOT);
2036 auto dng = dd.setGate(BooleanGate::AND);
2037 dd.addWire(dg, dng);
2038 for(const auto &c: getWires(g)) {
2039 auto dc = interpretAsDDInternal(c, seen, dd);
2040 auto dnc = dd.setGate(BooleanGate::NOT);
2041 dd.addWire(dnc, dc);
2042 dd.addWire(dng, dnc);
2043 }
2044 }
2045 break;
2046
2047 case BooleanGate::NOT:
2048 {
2049 dg = dd.setGate(BooleanGate::NOT);
2050 auto dc = interpretAsDDInternal(getWires(g)[0], seen, dd);
2051 dd.addWire(dg, dc);
2052 }
2053 break;
2054
2055 case BooleanGate::IN:
2056 if(seen.find(g)!=seen.end())
2057 throw CircuitException("Not an independent circuit");
2058 seen.insert(g);
2059 if(getUUID(g).empty())
2060 dg = dd.setGate(BooleanGate::IN, getProb(g));
2061 else
2062 dg = dd.setGate(getUUID(g), BooleanGate::IN, getProb(g));
2063 break;
2064
2065 case BooleanGate::MULIN:
2068 throw CircuitException("Unsupported gate in interpretAsDD");
2069 }
2070
2071 return dg;
2072}
2073
2075 std::set<gate_t> &seen,
2076 dDNNF &dd) const
2077{
2078 // Same iterative island walk as evaluateCertifiedIsland, building dd
2079 // gates instead of probabilities; shared sub-circuits map to shared dd
2080 // gates, and the copied gates keep their certificate so the produced
2081 // artefact remains self-describing.
2082 std::unordered_map<gate_t, gate_t> val;
2083 std::vector<gate_t> stack{root};
2084
2085 while(!stack.empty()) {
2086 const gate_t g = stack.back();
2087 if(val.find(g) != val.end()) {
2088 stack.pop_back();
2089 continue;
2090 }
2091
2092 const auto t = getGateType(g);
2093
2094 if(t == BooleanGate::IN) {
2095 if(seen.find(g) != seen.end())
2096 throw CircuitException("Not an independent circuit");
2097 seen.insert(g);
2098 val[g] = getUUID(g).empty()
2100 : dd.setGate(getUUID(g), BooleanGate::IN, getProb(g));
2101 stack.pop_back();
2102 continue;
2103 }
2104 if(t != BooleanGate::NOT && !isDNNFCertified(g)) {
2105 // Uncertified gate inside the island: standard rules (its own
2106 // certified descendants open fresh sub-islands).
2107 val[g] = interpretAsDDInternal(g, seen, dd);
2108 stack.pop_back();
2109 continue;
2110 }
2111
2112 bool ready = true;
2113 for(const auto &c: getWires(g))
2114 if(val.find(c) == val.end()) {
2115 stack.push_back(c);
2116 ready = false;
2117 }
2118 if(!ready)
2119 continue;
2120
2121 gate_t dg;
2122 if(t == BooleanGate::NOT) {
2123 dg = dd.setGate(BooleanGate::NOT);
2124 dd.addWire(dg, val[getWires(g)[0]]);
2125 } else {
2126 dg = dd.setGate(t);
2127 dd.setInfo(dg, DNNF_CERT_INFO);
2128 for(const auto &c: getWires(g))
2129 dd.addWire(dg, val[c]);
2130 }
2131 val[g] = dg;
2132 stack.pop_back();
2133 }
2134
2135 return val[root];
2136}
2137
2139{
2140 dDNNF dd;
2141 std::set<gate_t> seen;
2142
2143 dd.setRoot(interpretAsDDInternal(g, seen, dd));
2144
2145 // The OR-as-NOT(AND(NOT, ...)) De Morgan rewriting above introduces
2146 // many redundant NOT-NOT pairs and single-child AND/OR gates that the
2147 // canonical simplify pass folds away; matches what the external-KC
2148 // and tree-decomposition paths already do.
2149 dd.simplify();
2150
2151 return dd;
2152}
2153
2154// makeDD / makeDDByName fall back to the external compilers, so they too are
2155// excluded from the external-tool-free tdkc build.
2156#ifndef TDKC
2157dDNNF BooleanCircuit::makeDD(gate_t g, const std::string &method, const std::string &args) const
2158{
2159 if(method=="compilation") {
2160 return compilation(g, args);
2161 } else if(method=="tree-decomposition") {
2162 try {
2163 TreeDecomposition td(*this);
2165 *this, g, td}.build();
2166 } catch(TreeDecompositionException &) {
2167 provsql_error("Treewidth greater than %u", TreeDecomposition::MAX_TREEWIDTH);
2168 }
2169 } else if(method=="interpret-as-dd") {
2170 return interpretAsDD(g);
2171 } else {
2172 dDNNF dd;
2173 try {
2174 dd = interpretAsDD(g);
2175 if(provsql_verbose>=20)
2176 provsql_notice("Circuit interpreted as dD, %ld gates", dd.getNbGates());
2177 } catch(CircuitException &) {
2178 try {
2179 TreeDecomposition td(*this);
2181 *this, g, td}.build();
2182 if(provsql_verbose>=25)
2183 provsql_notice("dD obtained by tree decomposition, %ld gates", dd.getNbGates());
2184 } catch(TreeDecompositionException &) {
2185 // Last-resort fallback: chooseCompiler() prefers
2186 // provsql.fallback_compiler when available, else the highest-
2187 // preference compiler on PATH.
2188 std::string chosen = chooseCompiler();
2189 dd = compilation(g, chosen);
2190 if(provsql_verbose>=20)
2191 provsql_notice("dD obtained by compilation using %s, %ld gates",
2192 chosen.c_str(), dd.getNbGates());
2193 }
2194 }
2195
2196 return dd;
2197 }
2198}
2199
2200dDNNF BooleanCircuit::makeDDByName(gate_t g, const std::string &name) const
2201{
2202 // In-process meta-routes are dispatched through makeDD: "default" runs the
2203 // whole fallback chain, "tree-decomposition" / "interpret-as-dd" the single
2204 // route. Everything else goes to compilation(); the empty string there
2205 // means "pick the highest-preference available compiler" (so a no-compiler
2206 // request uses the registry instead of a hardcoded d4).
2207 if(name=="default" || name=="tree-decomposition" || name=="interpret-as-dd")
2208 return makeDD(g, name=="default" ? std::string() : name, "");
2209 return compilation(g, name);
2210}
2211#endif // makeDD / makeDDByName (excluded from tdkc)
static std::string selectTool(const std::string &operation, const std::string &preferred="")
static const size_t kSieveMaxClauses
Largest clause count for which the 2^m sieve enumeration is admitted.
static std::string chooseCompiler()
static constexpr bool almost_equals(double a, double b)
Check whether two double values are approximately equal.
Boolean provenance circuit with support for knowledge compilation.
constexpr unsigned DNNF_CERT_INFO
d-DNNF certificate value for the (gate-type-specific) per-gate info field.
BooleanGate
Gate types for a Boolean provenance circuit.
@ MULVAR
Auxiliary gate grouping all MULIN siblings.
@ NOT
Logical negation of a single child gate.
@ OR
Logical disjunction of child gates.
@ AND
Logical conjunction of child gates.
@ IN
Input (variable) gate representing a base tuple.
@ UNDETERMINED
Placeholder gate whose type has not been set yet.
@ MULIN
Multivalued-input gate (one of several options).
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
std::string to_string(gate_t g)
Convert a gate_t to its decimal string representation.
Definition Circuit.h:250
Out-of-line template method implementations for Circuit<gateType>.
In-memory catalog of the external tools ProvSQL can invoke.
Boolean circuit for provenance formula evaluation.
std::vector< double > prob
Per-gate probability (for IN gates).
bool evaluate(gate_t g, const std::unordered_set< gate_t > &sampled) const
Evaluate the sub-circuit at g on one sampled world.
dDNNF interpretAsDD(gate_t g) const
Build a dDNNF directly from the Boolean circuit's structure.
double independentEvaluationInternal(gate_t g, std::set< gate_t > &seen, std::unordered_map< gate_t, double > &memo) const
Recursive helper for independentEvaluation().
double sieve(const std::vector< gate_t > &clauses, const std::vector< std::set< gate_t > > &supports) const
Exact probability of a monotone DNF by inclusion-exclusion (sieve).
double possibleWorlds(gate_t g) const
Compute the probability by exact enumeration of all possible worlds.
std::vector< CNFInputMapping > tseytinVariableMapping() const
Map each input gate to its DIMACS variable, UUID, probability.
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,...
void setProb(gate_t g, double p)
Set the probability for gate g and mark the circuit as probabilistic.
dDNNF parseDDNNF(std::istream &in, const std::vector< gate_t > &inputOrder) const
Parse a c2d/d4 NNF stream into a dDNNF over this circuit's input gates.
std::set< gate_t > inputs
Set of IN (input) gate IDs.
bool isDNNFCertified(gate_t g) const
Is gate g certified by the d-DNNF per-gate marking?
void rewriteMultivaluedGatesRec(const std::vector< gate_t > &muls, const std::vector< double > &cumulated_probs, unsigned start, unsigned end, std::vector< gate_t > &prefix)
Recursive helper for rewriteMultivaluedGates().
double evaluateCertifiedIsland(gate_t root, std::set< gate_t > &seen, std::unordered_map< gate_t, double > &memo) const
Iteratively evaluate a certified d-DNNF island.
std::string exportCircuit(gate_t g) const
Export the circuit in the textual format expected by external compilers.
std::string BCS12(gate_t g, std::vector< gate_t > &inputOrder) const
Serialise the sub-circuit at g in d4's BC-S1.2 circuit format.
std::string TseytinCNF(gate_t g, bool display_prob, bool mapping=false) const
Return the Tseytin transformation of the sub-circuit at g as a DIMACS string.
dDNNF parsePaniniDD(const std::string &outfilename) const
Parse a Panini (KCBox) DD output file into a ProvSQL d-DNNF.
std::string toStringHelper(gate_t g, BooleanGate parent, const std::unordered_map< gate_t, std::string > *labels) const
Internal recursive helper for the two toString() variants.
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.
dDNNF makeDD(gate_t g, const std::string &method, const std::string &args) const
Dispatch to the appropriate d-DNNF construction method.
gate_t setGate(BooleanGate type) override
Allocate a new gate with type type and no UUID.
unsigned getInfo(gate_t g) const
Return the integer annotation for gate g.
friend class dDNNFTreeDecompositionBuilder
double wmcCount(gate_t g, const std::string &tool, const std::string &opt) const
Weighted model counting through a registered external counter.
gate_t addGate() override
Allocate a new gate with a default-initialised type.
double monteCarlo(gate_t g, unsigned samples) const
Estimate the probability via Monte Carlo sampling.
void rewriteMultivaluedGates()
Rewrite all MULVAR/MULIN gate clusters into standard AND/OR/NOT circuits.
double getProb(gate_t g) const
Return the probability stored for gate g.
bool dnfShapeInfo(gate_t g, std::size_t &num_clauses) const
Cheap shape test: is the circuit DNF-shaped, and how many clauses?
void setInfo(gate_t g, unsigned info)
Store an integer annotation on gate g.
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...
virtual std::string toString(gate_t g) const override
Return a textual description of gate g for debugging.
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.
std::map< gate_t, unsigned > info
Per-gate integer info (for MULIN gates).
gate_t interpretCertifiedIsland(gate_t root, std::set< gate_t > &seen, dDNNF &dd) const
Iteratively copy a certified island into dd.
dDNNF makeDDByName(gate_t g, const std::string &name) const
Build a dDNNF from a single compiler/route name.
gate_t interpretAsDDInternal(gate_t g, std::set< gate_t > &seen, dDNNF &dd) const
Recursive helper for interpretAsDD().
double independentEvaluation(gate_t g) const
Compute the probability exactly when inputs are independent.
std::set< gate_t > mulinputs
Set of MULVAR gate IDs.
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
std::string uuid
Definition Circuit.h:65
std::vector< gate_t > & getWires(gate_t g)
Definition Circuit.h:140
BooleanGate getGateType(gate_t g) const
Definition Circuit.h:130
std::unordered_map< gate_t, uuid > id2uuid
Definition Circuit.h:69
virtual gate_t setGate(const uuid &u, gateType type)
Create or update the gate associated with UUID u.
Definition Circuit.hpp:73
void addWire(gate_t f, gate_t t)
Add a directed wire from gate f (parent) to gate t (child).
Definition Circuit.hpp:81
std::vector< BooleanGate > gates
Definition Circuit.h:71
uuid getUUID(gate_t g) const
Definition Circuit.hpp:46
gate_t getGate(const uuid &u)
Return (or create) the gate associated with UUID u.
Definition Circuit.hpp:33
bool hasGate(const uuid &u) const
Test whether a gate with UUID u exists.
Definition Circuit.hpp:27
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
virtual gate_t addGate()
Allocate a new gate with a default-initialised type.
Definition Circuit.hpp:56
Exception thrown when a tree decomposition cannot be constructed.
Tree decomposition of a Boolean circuit'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
void setRoot(gate_t g)
Set the root gate.
Definition dDNNF.h:127
void simplify()
Simplify the d-DNNF by removing redundant constants.
Definition dDNNF.cpp:604
RAII guard around a freshly mkdtemp'd /tmp directory.
std::string file(const std::string &basename)
Build a path under the temp dir and register it for cleanup.
const std::string & path() const
void keep()
Leave the directory on disk; cleanup is skipped at scope exit.
const ToolRecord * find(const std::string &name) const
Find a record by logical name, or nullptr if none is registered.
Constructs a d-DNNF from a Boolean circuit and its tree decomposition.
int run_external_tool(const std::string &cmdline)
Run a shell command line in its own process group, optionally extending PATH, interruptible by query ...
std::string format_external_tool_status(int rv, const std::string &tool)
Decode a system() return value into a human-readable message.
bool toolAvailable(const provsql::ToolRecord &rec)
True iff a registry tool can currently be used.
std::string find_external_tool(const std::string &name)
Locate an external tool by name.
Helpers for invoking external command-line tools.
In-extension KCMCP client: compile a Boolean problem on a warm, socket-attached knowledge compiler in...
const char * provsql_kcmcp_managed_endpoint(void)
Read the live endpoint of the managed KCMCP server from shared memory (e.g.
std::string kcmcp_compile(const std::string &endpoint, uint8_t input_format, const std::string &problem)
Compile problem on a KCMCP server and return its d-DNNF NNF text.
ToolRegistry & tool_registry()
Shorthand for ToolRegistry::instance().
std::string expandCommandTemplate(const std::string &tpl, const std::string &binary, const std::string &in, const std::string &out, const std::vector< std::pair< std::string, std::string > > &extra={})
Expand a command template into a runnable shell command line.
int provsql_verbose
Verbosity level; controlled by the provsql.verbose_level GUC.
Definition provsql.c:93
int provsql_monte_carlo_seed
Seed for the Monte Carlo sampler; -1 means non-deterministic (std::random_device); controlled by the ...
Definition provsql.c:99
bool provsql_interrupted
Global variable that becomes true if this particular backend received an interrupt signal.
Definition provsql.c:89
char * provsql_fallback_compiler
Compiler used by BooleanCircuit::makeDD as the final fallback after interpretAsDD and tree-decomposit...
Definition provsql.c:97
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
#define provsql_notice(fmt,...)
Emit a ProvSQL informational notice (execution continues).
Core types, constants, and utilities shared across ProvSQL.
One registered external tool.
bool acceptsInput(const std::string &fmt) const
std::vector< std::string > dependencies
std::string argtpl_circuit
std::string kind
"cli" (spawn a binary) or "kcmcp" (talk to a socket server at endpoint).
std::string endpoint
KCMCP server address for kind "kcmcp": "unix:/path" or "host:port".
bool hasOperation(const std::string &op) const
std::string buildCommand(const std::string &in, const std::string &out, const std::string &binary_override, const std::vector< std::pair< std::string, std::string > > &extra={}) const
Build the command line for this tool.