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 if(inputs.size()>=8*sizeof(unsigned long long))
832 throw CircuitException("Too many possible worlds to iterate over");
833
834 unsigned long long nb=(1ULL<<inputs.size());
835 double totalp=0.;
836
837 for(unsigned long long i=0; i < nb; ++i) {
838 std::unordered_set<gate_t> s;
839 double p = 1;
840
841 unsigned j=0;
842 for(gate_t in : inputs) {
843 if(i & (1ULL << j)) {
844 s.insert(in);
845 p*=getProb(in);
846 } else {
847 p*=1-getProb(in);
848 }
849 ++j;
850 }
851
852 if(evaluate(g, s))
853 totalp+=p;
854
856 throw CircuitException("Interrupted");
857 }
858
859 return totalp;
860}
861
862std::string BooleanCircuit::TseytinCNF(gate_t g, bool display_prob, bool mapping) const {
863 std::vector<std::vector<int> > clauses;
864
865 // Tseytin transformation
866 for(gate_t i{0}; i<gates.size(); ++i) {
867 switch(getGateType(i)) {
868 case BooleanGate::AND:
869 {
870 int id{static_cast<int>(i)+1};
871 std::vector<int> c = {id};
872 for(auto s: getWires(i)) {
873 clauses.push_back({-id, static_cast<int>(s)+1});
874 c.push_back(-static_cast<int>(s)-1);
875 }
876 clauses.push_back(c);
877 break;
878 }
879
880 case BooleanGate::OR:
881 {
882 int id{static_cast<int>(i)+1};
883 std::vector<int> c = {-id};
884 for(auto s: getWires(i)) {
885 clauses.push_back({id, -static_cast<int>(s)-1});
886 c.push_back(static_cast<int>(s)+1);
887 }
888 clauses.push_back(c);
889 }
890 break;
891
892 case BooleanGate::NOT:
893 {
894 int id=static_cast<int>(i)+1;
895 auto s=*getWires(i).begin();
896 clauses.push_back({-id,-static_cast<int>(s)-1});
897 clauses.push_back({id,static_cast<int>(s)+1});
898 break;
899 }
900
902 throw CircuitException("Multivalued inputs should have been removed by then.");
904 case BooleanGate::IN:
906 ;
907 }
908 }
909 clauses.push_back({(int)g+1});
910
911 std::ostringstream oss;
912 // Optional self-documenting mapping, emitted as DIMACS comments
913 // before the problem line so a saved CNF records which provenance
914 // input each variable stands for. Comments are ignored by every
915 // model counter / compiler, so the file stays valid DIMACS.
916 if(mapping) {
917 for(const auto &m : tseytinVariableMapping()) {
918 oss << "c input " << m.variable << " "
919 << (m.uuid.empty() ? "?" : m.uuid) << " "
920 << m.probability << "\n";
921 }
922 }
923 oss << "p cnf " << gates.size() << " " << clauses.size() << "\n";
924 for(unsigned i=0; i<clauses.size(); ++i) {
925 for(int x : clauses[i]) {
926 oss << x << " ";
927 }
928 oss << "0\n";
929 }
930 if(display_prob) {
931 for(gate_t in: inputs) {
932 oss << "w " << (static_cast<std::underlying_type<gate_t>::type>(in)+1) << " " << getProb(in) << "\n";
933 oss << "w -" << (static_cast<std::underlying_type<gate_t>::type>(in)+1) << " " << (1. - getProb(in)) << "\n";
934 }
935 }
936 return oss.str();
937}
938
939std::vector<BooleanCircuit::CNFInputMapping>
941 std::vector<CNFInputMapping> mapping;
942 // `inputs` is a std::set<gate_t>, so iteration is in gate-id order
943 // and the variable indices (id + 1) come out sorted and stable.
944 for(gate_t in : inputs) {
945 auto id = static_cast<std::underlying_type<gate_t>::type>(in);
946 std::string u;
947 auto it = id2uuid.find(in);
948 if(it != id2uuid.end())
949 u = it->second;
950 mapping.push_back({static_cast<int>(id) + 1, u, getProb(in)});
951 }
952 return mapping;
953}
954
955std::string BooleanCircuit::BCS12(gate_t g, std::vector<gate_t> &inputOrder) const {
956 inputOrder.clear();
957 auto idOf = [](gate_t x) {
958 return static_cast<std::underlying_type<gate_t>::type>(x);
959 };
960
961 std::set<gate_t> seenInputs;
962 std::set<gate_t> internalGates; // AND/OR gates to emit, ordered by id
963
964 // Resolve a wire to a BC-S1.2 literal, inlining NOT chains as sign flips.
965 std::function<std::string(gate_t)> lit = [&](gate_t w) -> std::string {
966 switch(getGateType(w)) {
967 case BooleanGate::IN:
968 return "in" + std::to_string(idOf(w));
969 case BooleanGate::AND:
970 case BooleanGate::OR:
971 return "g" + std::to_string(idOf(w));
972 case BooleanGate::NOT: {
973 std::string inner = lit(*getWires(w).begin());
974 return inner[0]=='-' ? inner.substr(1) : "-"+inner;
975 }
976 default:
977 throw CircuitException("BC-S1.2 export: unsupported gate type");
978 }
979 };
980
981 // DFS collecting input gates (in first-seen order, fixing their d4
982 // variable numbers) and the AND/OR gates to define; NOT gates are
983 // traversed but never named.
984 std::function<void(gate_t)> collect = [&](gate_t w) {
985 switch(getGateType(w)) {
986 case BooleanGate::IN:
987 if(seenInputs.insert(w).second)
988 inputOrder.push_back(w);
989 break;
990 case BooleanGate::NOT:
991 collect(*getWires(w).begin());
992 break;
993 case BooleanGate::AND:
994 case BooleanGate::OR:
995 if(internalGates.insert(w).second)
996 for(gate_t c : getWires(w))
997 collect(c);
998 break;
999 default:
1000 throw CircuitException("BC-S1.2 export: unsupported gate type");
1001 }
1002 };
1003 collect(g);
1004
1005 std::ostringstream oss;
1006 oss << "c BC-S1.2\n";
1007 // Inputs first: d4 numbers them 1..k in this order (see header doc).
1008 for(gate_t in : inputOrder)
1009 oss << "I in" << idOf(in) << "\n";
1010 for(gate_t w : internalGates) {
1011 const auto &ch = getWires(w);
1012 if(ch.empty())
1013 throw CircuitException("BC-S1.2 export: nullary gate");
1014 oss << "G g" << idOf(w) << " := ";
1015 // BC-S1.2 requires >= 2 literals for A/O; a unary AND/OR is the identity.
1016 if(ch.size()==1)
1017 oss << "I";
1018 else
1019 oss << (getGateType(w)==BooleanGate::AND ? "A" : "O");
1020 for(gate_t c : ch)
1021 oss << " " << lit(c);
1022 oss << "\n";
1023 }
1024 oss << "T " << lit(g) << "\n";
1025 return oss.str();
1026}
1027
1028// ---------------------------------------------------------------------------
1029// External-tool knowledge compilation and weighted model counting.
1030//
1031// The standalone tdkc tool deliberately invokes NO external tool (it compiles
1032// purely via tree decomposition), so this whole block -- the knowledge
1033// compilers, the Panini wrapper, the weighted model counters, and the
1034// makeDD/makeDDByName dispatchers that fall back to them -- is excluded from
1035// the tdkc build. The registry is therefore used unconditionally here.
1036// ---------------------------------------------------------------------------
1037#ifndef TDKC
1038
1039// Parse a Panini (KCBox) DD output file into a d-DNNF: this is the
1040// `panini-dd` output parser, selected by compilation() for the panini-*
1041// records (which run the generic compile path -- write a Tseytin CNF, run the
1042// record's argtpl -- and differ only in this parse-back). Panini emits its
1043// own DD format (sequential node ids; "F"/"T" terminals; "C"/"D" decomposable
1044// conjunctions; decision nodes), not the c2d/d4 NNF the `nnf` parser reads.
1045// R2-D2 and CCDD emit "K" (kernelize) nodes that break decomposability, so
1046// ProvSQL does not register the variants that produce them.
1047dDNNF BooleanCircuit::parsePaniniDD(const std::string &outfilename) const {
1048 std::ifstream ifs(outfilename.c_str());
1049 if (!ifs)
1050 throw CircuitException("Cannot open Panini output: " + outfilename);
1051
1052 // Skip Panini's preamble ("Variable order: ...", "Maximum variable: ...",
1053 // "Number of nodes: ...") and stop at the first data line, which always
1054 // starts with "0:".
1055 std::string line;
1056 bool found_data = false;
1057 while (std::getline(ifs, line)) {
1058 if (line.rfind("0:", 0) == 0) { found_data = true; break; }
1059 }
1060 if (!found_data)
1061 throw CircuitException("Panini output: no data lines found");
1062
1063 dDNNF dnnf;
1064 // Panini node ids are sequential 0, 1, 2, ... Highest id is the
1065 // root of the compilation.
1066 std::vector<gate_t> id_to_gate;
1067
1068 do {
1069 if (line.empty()) continue;
1070 auto colon_pos = line.find(':');
1071 if (colon_pos == std::string::npos) continue;
1072
1073 // Sanity-check the leading id matches the size of id_to_gate so far
1074 // (the file should be in monotonically increasing id order).
1075 int panini_id = std::stoi(line.substr(0, colon_pos));
1076 if (static_cast<size_t>(panini_id) != id_to_gate.size())
1077 throw CircuitException(
1078 "Panini output: out-of-order node id "
1079 + std::to_string(panini_id));
1080
1081 std::stringstream ss(line.substr(colon_pos + 1));
1082 std::string first;
1083 ss >> first;
1084
1085 gate_t this_gate;
1086 if (first == "F") {
1087 // FALSE terminal: empty OR.
1088 this_gate = dnnf.setGate(BooleanGate::OR);
1089 } else if (first == "T") {
1090 // TRUE terminal: empty AND.
1091 this_gate = dnnf.setGate(BooleanGate::AND);
1092 } else if (first == "C" || first == "D") {
1093 // C (CONJOIN), D (DECOMPOSE) are decomposable conjunctions in
1094 // Panini's CDD format; OR is only ever expressed implicitly by
1095 // the (v ? t : f) decision nodes. K (KERNELIZE) nodes encode
1096 // literal-equivalence constraints over a shared kernel
1097 // variable and break decomposability; we refuse the only two
1098 // target languages that emit them (R2-D2 and CCDD) upstream,
1099 // so seeing K here is an upstream-Panini surprise.
1100 this_gate = dnnf.setGate(BooleanGate::AND);
1101 int child;
1102 while (ss >> child) {
1103 if (child == 0) break;
1104 if (child < 0 || static_cast<size_t>(child) >= id_to_gate.size())
1105 throw CircuitException(
1106 "Panini output: forward / invalid child reference "
1107 + std::to_string(child));
1108 dnnf.addWire(this_gate, id_to_gate[child]);
1109 }
1110 } else if (first == "K") {
1111 throw CircuitException(
1112 "Panini output: unexpected K (kernelize) node; ProvSQL "
1113 "does not support Panini target languages that emit K "
1114 "nodes (R2-D2, CCDD).");
1115 } else {
1116 // Decision node: <var> <false_child> <true_child> [0]
1117 // (Panini's CDD::Display emits children in ch[0]/ch[1] order;
1118 // CDD.cpp's DOT writer maps ch[0] to the dotted/false edge and
1119 // ch[1] to the solid/true edge.)
1120 int var = std::stoi(first);
1121 int f_child, t_child;
1122 if (!(ss >> f_child >> t_child))
1123 throw CircuitException(
1124 "Panini output: malformed decision line at id "
1125 + std::to_string(panini_id));
1126 if (t_child < 0 || f_child < 0
1127 || static_cast<size_t>(t_child) >= id_to_gate.size()
1128 || static_cast<size_t>(f_child) >= id_to_gate.size())
1129 throw CircuitException(
1130 "Panini output: forward / invalid decision child at id "
1131 + std::to_string(panini_id));
1132 gate_t t_gate = id_to_gate[t_child];
1133 gate_t f_gate = id_to_gate[f_child];
1134
1135 // Translate the decision. Two cases:
1136 // (a) v is an input gate: keep the literal in the structure,
1137 // OR(AND(v, t'), AND(NOT(v), f')).
1138 // (b) v is a Tseytin auxiliary: aux vars are functionally
1139 // determined by inputs, so under WMC we want literal
1140 // weights w(v) = w(NOT v) = 1 (not (p, 1-p)). With those
1141 // weights the AND wrappers contribute 1 to either branch
1142 // and we can drop them entirely, emitting just
1143 // OR(t', f'). The OR is not determinism-preserving on
1144 // the variable v, but the input-projection of its two
1145 // arms is still disjoint by Tseytin determinism so
1146 // @c dDNNF::probabilityEvaluation() still returns the
1147 // correct weighted model count.
1148 size_t var_idx = static_cast<size_t>(var) - 1;
1149 if (var_idx < gates.size() && gates[var_idx] == BooleanGate::IN) {
1150 gate_t pos_lit = dnnf.setGate(
1151 getUUID(static_cast<gate_t>(var_idx)),
1152 BooleanGate::IN, prob[var_idx]);
1153 gate_t neg_lit = dnnf.setGate(BooleanGate::NOT);
1154 dnnf.addWire(neg_lit, pos_lit);
1155 gate_t and_t = dnnf.setGate(BooleanGate::AND);
1156 dnnf.addWire(and_t, pos_lit);
1157 dnnf.addWire(and_t, t_gate);
1158 gate_t and_f = dnnf.setGate(BooleanGate::AND);
1159 dnnf.addWire(and_f, neg_lit);
1160 dnnf.addWire(and_f, f_gate);
1161 this_gate = dnnf.setGate(BooleanGate::OR);
1162 dnnf.addWire(this_gate, and_t);
1163 dnnf.addWire(this_gate, and_f);
1164 } else {
1165 this_gate = dnnf.setGate(BooleanGate::OR);
1166 dnnf.addWire(this_gate, t_gate);
1167 dnnf.addWire(this_gate, f_gate);
1168 }
1169 }
1170 id_to_gate.push_back(this_gate);
1171 } while (std::getline(ifs, line));
1172
1173 ifs.close();
1174
1175 if (id_to_gate.empty())
1176 throw CircuitException("Panini output produced no nodes");
1177
1178 // The root of a Panini DD is the highest-id node.
1179 dnnf.setRoot(id_to_gate.back());
1180
1181 dnnf.simplify();
1182 return dnnf;
1183}
1184
1185// Preference-ranked tool selection for @p operation: honour the explicitly
1186// @p preferred tool when it is enabled, advertises the operation, and is
1187// available; otherwise return the highest-preference enabled tool advertising
1188// the operation whose binary and dependencies resolve on PATH. Returns "" if
1189// none is available, so a dispatcher can fall back or raise a clear error.
1190// This replaces the old "if d4 else c2d ..." chains: an admin reorders or
1191// disables tools (or bumps provsql.fallback_compiler, honoured via @p
1192// preferred) and selection follows.
1193static std::string selectTool(const std::string &operation,
1194 const std::string &preferred = "") {
1195 if(!preferred.empty()) {
1196 const provsql::ToolRecord *r = provsql::tool_registry().find(preferred);
1197 if(r != nullptr && r->enabled && r->hasOperation(operation)
1198 && toolAvailable(*r))
1199 return preferred;
1200 }
1201 for(const provsql::ToolRecord *r : provsql::tool_registry().byOperation(operation))
1202 if(toolAvailable(*r))
1203 return r->name;
1204 return "";
1205}
1206
1207// The compiler for makeDD's last-resort fallback route: prefer
1208// provsql.fallback_compiler (default "d4") when available, otherwise the
1209// highest-preference compiler whose binary resolves on PATH. Falls back to
1210// the configured name when nothing is available, so compilation() raises its
1211// actionable error. (The GUC governs only this fallback route; an explicit
1212// no-argument compilation() request just takes the best available compiler --
1213// symmetric with the no-tool wmc path.)
1214static std::string chooseCompiler() {
1215 const char *fb = (provsql_fallback_compiler != NULL
1216 && provsql_fallback_compiler[0] != '\0')
1218 std::string chosen = selectTool("compile", fb);
1219 return chosen.empty() ? std::string(fb) : chosen;
1220}
1221
1223 std::string *resolved) const {
1224 // No compiler named: pick the highest-preference available one (symmetric
1225 // with the no-tool wmc path). provsql.fallback_compiler is deliberately
1226 // not consulted here -- it governs only makeDD's last-resort fallback route.
1227 if(compiler.empty()) {
1228 compiler = selectTool("compile");
1229 if(compiler.empty())
1230 throw CircuitException(
1231 "no knowledge compiler is available; install one (d4, d4v2, "
1232 "c2d, minic2d, dsharp) or add its directory to "
1233 "provsql.tool_search_path");
1234 }
1235
1236 // Validate the compiler against the registry before any temp-dir or CNF
1237 // work. A name that is not a registered, enabled 'compile' tool is
1238 // rejected here. compilation() implements two output parsers: the tolerant
1239 // `nnf` reader (both the d4-family header-less NNF and the c2d-style header
1240 // form) and the `panini-dd` reader (Panini's own DD format); a compile tool
1241 // advertising any other parser is something we cannot read back, so reject
1242 // it rather than mis-parse.
1243 const provsql::ToolRecord *rec = provsql::tool_registry().find(compiler);
1244 if(rec == nullptr || !rec->hasOperation("compile"))
1245 throw CircuitException("Unknown compiler '"+compiler+"'");
1246 if(!rec->enabled)
1247 throw CircuitException(
1248 "Compiler '"+compiler+"' is disabled in the tool registry");
1249 if(rec->parser != "nnf" && rec->parser != "panini-dd")
1250 throw CircuitException(
1251 "Compiler '"+compiler+"' uses output parser '"+rec->parser
1252 +"', which compilation() does not implement");
1253 const std::string compiler_binary = rec->binary;
1254 if(resolved)
1255 *resolved = compiler; // the validated tool actually used (CLI or KCMCP)
1256
1257 // KCMCP backend: compile over a warm socket server instead of spawning a
1258 // CLI tool. The problem is sent as a native BC-S1.2 circuit when the record
1259 // advertises that input, else as a Tseytin CNF; the RESULT's d-DNNF text is
1260 // parsed by the same parseDDNNF() the CLI path uses. Any failure (connect,
1261 // protocol, server ERROR) raises, so makeDD's fallback can try another tool.
1262 if(rec->kind == "kcmcp") {
1263 std::vector<gate_t> inputOrder;
1264 std::string content;
1265 uint8_t input_format = 0; // dimacs-cnf
1266 if(rec->acceptsInput("circuit-bcs12")) {
1267 try {
1268 content = BCS12(g, inputOrder);
1269 input_format = 1; // circuit-bcs12
1270 } catch(const CircuitException &) {
1271 inputOrder.clear();
1272 content.clear();
1273 }
1274 }
1275 if(content.empty())
1276 content = TseytinCNF(g, false); // inputOrder stays empty => CNF mode
1277 // Resolve the server address: a literal 'managed' endpoint defers to the
1278 // live address the supervisor worker published in shared memory; anything
1279 // else is a fixed endpoint (unix:/path or host:port).
1280 std::string endpoint = rec->endpoint;
1281 if(endpoint == "managed")
1282 endpoint = provsql_kcmcp_managed_endpoint();
1283 if(endpoint.empty())
1284 throw CircuitException(
1285 "KCMCP tool '"+compiler+"' has no endpoint (managed server not "
1286 "running, or provsql.kcmcp_server unset)");
1287 try {
1288 std::string nnf = provsql::kcmcp_compile(endpoint, input_format, content);
1289 std::istringstream iss(nnf);
1290 return parseDDNNF(iss, inputOrder);
1291 } catch(const CircuitException &) {
1292 throw;
1293 } catch(const std::exception &e) {
1294 throw CircuitException(std::string("KCMCP compile via '")+compiler
1295 +"' failed: "+e.what());
1296 }
1297 }
1298
1299 // A compiler that advertises the BC-S1.2 circuit input (KCMCP
1300 // "circuit-bcs12") is driven with native circuit input: the Boolean circuit
1301 // is sent directly instead of a Tseytin CNF, skipping the CNF transform and
1302 // the aux-variable reconciliation in the parse-back. Requires a parser
1303 // that honours `I` declarations; on a gate shape BC-S1.2 cannot express, we
1304 // fall back to the Tseytin CNF path below. (Today only d4v2 advertises it.)
1305 bool circuit_input = rec->acceptsInput("circuit-bcs12")
1306 && !rec->argtpl_circuit.empty();
1307 if(find_external_tool(compiler_binary).empty())
1308 throw CircuitException(
1309 compiler_binary + " not found on PATH; install it or add its "
1310 "directory to provsql.tool_search_path");
1311
1312 ScopedTempDir tmp;
1313 std::string filename = tmp.file("input");
1314 std::string outfilename = tmp.file("input.nnf");
1315 // In circuit mode, inputOrder[v-1] is the IN gate for d4 variable v
1316 // (1-based); empty in CNF mode.
1317 std::vector<gate_t> inputOrder;
1318 std::string content;
1319 if(circuit_input) {
1320 try {
1321 content = BCS12(g, inputOrder);
1322 } catch(const CircuitException &) {
1323 // A gate shape BC-S1.2 cannot express: fall back to the CNF path.
1324 circuit_input = false;
1325 inputOrder.clear();
1326 }
1327 }
1328 if(!circuit_input)
1329 content = TseytinCNF(g, false);
1330 {
1331 std::ofstream ofs(filename);
1332 ofs << content;
1333 }
1334
1335 if(provsql_verbose>=20) {
1336 provsql_notice("Tseytin circuit in %s", filename.c_str());
1337 }
1338
1339 // The command line is the registry argtpl with {in}/{out} (and {binary})
1340 // substituted -- so a newly-registered compiler runs with its own
1341 // invocation, no per-name branch here. When the BC-S1.2 circuit input is
1342 // in use, the record's argtpl_circuit is used instead (it pairs with the
1343 // circuit input written above and the circuit-mode variable resolution in
1344 // the parse-back).
1345 std::string cmdline;
1346 if(circuit_input)
1348 compiler_binary, filename, outfilename);
1349 else
1350 cmdline = rec->buildCommand(filename, outfilename, compiler_binary);
1351
1352 int retvalue=run_external_tool(cmdline);
1353
1354 // run_external_tool runs the compiler in its own process group and
1355 // raises any pending cancel/terminate itself (after killing the child),
1356 // so a statement_timeout / pg_cancel_backend surfaces as 57014 here
1357 // rather than being masked by the "killed by signal" throw.
1358 //
1359 // (An older d4 CLI without `-dDNNF` is not handled as a compiled-in
1360 // special case: a deployment using it can register a tool with the
1361 // appropriate argtpl, e.g. register_tool('d4-old', argtpl => '{in} -out={out}', ...).)
1362 CHECK_FOR_INTERRUPTS();
1363
1364 if(retvalue)
1365 throw CircuitException(format_external_tool_status(retvalue, compiler));
1366
1367 // Read the result back with the parser the record advertises. Panini's DD
1368 // format has its own reader; everything else is the tolerant NNF parser.
1369 if(rec->parser == "panini-dd")
1370 return parsePaniniDD(outfilename);
1371
1372 std::ifstream ifs(outfilename.c_str());
1373 dDNNF dnnf = parseDDNNF(ifs, inputOrder);
1374 ifs.close();
1375
1376 if(provsql_verbose>=20) {
1377 tmp.keep();
1378 provsql_notice("Compiled d-DNNF in %s", outfilename.c_str());
1379 }
1380
1381 return dnnf;
1382}
1383
1384// Parse a c2d/d4 NNF stream into a dDNNF over this circuit's input gates.
1385// Shared by the CLI compilation() path and the KCMCP client; see the header.
1387 const std::vector<gate_t> &inputOrder) const {
1388 const bool circuit_input = !inputOrder.empty();
1389
1390 std::string line;
1391 getline(in,line);
1392
1393 // Tolerant NNF detection (the single `nnf` parser): the classic c2d/d4
1394 // form opens with an "nnf <nodes> <edges> <vars>" magic line and roots at
1395 // the last node; the d4-family form has no magic line and roots at gate
1396 // "1". A classic compiler emits the header iff satisfiable, so a missing
1397 // header on an empty file is an unsatisfiable formula; a missing header on
1398 // a non-empty file is the d4-family form. (The d4v2 native-circuit path
1399 // also produces the header-less d4 form.)
1400 bool new_d4;
1401 if(line.rfind("nnf", 0) != 0) {
1402 if(line.empty()) {
1403 // unsatisfiable formula (empty output)
1404 return dDNNF();
1405 }
1406 new_d4 = true;
1407 } else {
1408 new_d4 = false;
1409 std::string nnf;
1410 unsigned nb_nodes, nb_edges, nb_variables;
1411
1412 std::stringstream ss(line);
1413 ss >> nnf >> nb_nodes >> nb_edges >> nb_variables;
1414
1415 if(nb_variables!=gates.size())
1416 throw CircuitException("Unreadable d-DNNF (wrong number of variables: " + std::to_string(nb_variables) +" vs " + std::to_string(gates.size()) + ")");
1417
1418 getline(in,line);
1419 }
1420
1421 dDNNF dnnf;
1422
1423 // Map a d-DNNF literal's variable to the IN gate it stands for, if any.
1424 // CNF mode: variable = gate id + 1, real only for IN gates (every other
1425 // variable is a Tseytin auxiliary to skip). Circuit mode: variables 1..k
1426 // are the inputs in BCS12's declaration order, everything above k is an
1427 // internal-gate variable to skip.
1428 size_t k = inputOrder.size();
1429 auto resolveVar = [&](int v) -> std::pair<bool, gate_t> {
1430 unsigned idx = static_cast<unsigned>(abs(v));
1431 if(circuit_input) {
1432 if(idx>=1 && idx<=k)
1433 return {true, inputOrder[idx-1]};
1434 return {false, gate_t{}};
1435 }
1436 if(idx>=1 && (idx-1) < gates.size() && gates[idx-1]==BooleanGate::IN)
1437 return {true, static_cast<gate_t>(idx-1)};
1438 return {false, gate_t{}};
1439 };
1440
1441 unsigned i=0;
1442 do {
1443 std::stringstream ss(line);
1444
1445 std::string c;
1446 ss >> c;
1447
1448 if(c=="O") {
1449 int var, args;
1450 ss >> var >> args;
1451 auto id=dnnf.getGate(std::to_string(i));
1452 dnnf.setGate(std::to_string(i), BooleanGate::OR);
1453 int g;
1454 while(ss >> g) {
1455 auto id2=dnnf.getGate(std::to_string(g));
1456 dnnf.addWire(id,id2);
1457 }
1458 } else if(c=="A") {
1459 int args;
1460 ss >> args;
1461 auto id=dnnf.getGate(std::to_string(i));
1462 dnnf.setGate(std::to_string(i), BooleanGate::AND);
1463 int g;
1464 while(ss >> g) {
1465 auto id2=dnnf.getGate(std::to_string(g));
1466 dnnf.addWire(id,id2);
1467 }
1468 } else if(c=="L") {
1469 int leaf;
1470 ss >> leaf;
1471 auto and_gate=dnnf.setGate(std::to_string(i), BooleanGate::AND);
1472 auto [is_in, in_gate] = resolveVar(leaf);
1473 if(is_in) {
1474 auto pid = static_cast<std::underlying_type<gate_t>::type>(in_gate);
1475 auto leaf_gate = dnnf.setGate(getUUID(in_gate), BooleanGate::IN, prob[pid]);
1476 if(leaf<0) {
1477 auto not_gate = dnnf.setGate(BooleanGate::NOT);
1478 dnnf.addWire(not_gate, leaf_gate);
1479 dnnf.addWire(and_gate, not_gate);
1480 } else {
1481 dnnf.addWire(and_gate, leaf_gate);
1482 }
1483 } else {
1484 ; // Do nothing, TRUE gate
1485 }
1486 } else if(c=="f" || c=="o") {
1487 // d4 extended format
1488 // A FALSE gate is an OR gate without wires
1489 int var;
1490 ss >> var;
1491 dnnf.setGate(std::to_string(var), BooleanGate::OR);
1492 } else if(c=="t" || c=="a") {
1493 // d4 extended format
1494 // A TRUE gate is an AND gate without wires
1495 int var;
1496 ss >> var;
1497 dnnf.setGate(std::to_string(var), BooleanGate::AND);
1498 } else if(dnnf.hasGate(c)) {
1499 // d4 extended format
1500 int var;
1501 ss >> var;
1502 auto id2=dnnf.getGate(std::to_string(var));
1503
1504 std::vector<int> decisions;
1505 int decision;
1506 while(ss >> decision) {
1507 if(decision==0)
1508 break;
1509 // Edges carry decision literals over both real inputs and internal
1510 // variables (Tseytin auxiliaries in CNF mode, gate variables in
1511 // circuit mode). Keep only the input literals; the rest are
1512 // functionally determined and projected out (sound for probability).
1513 if(resolveVar(decision).first)
1514 decisions.push_back(decision);
1515 }
1516
1517 if(decisions.empty()) {
1518 dnnf.addWire(dnnf.getGate(c), id2);
1519 } else {
1520 auto and_gate = dnnf.setGate(BooleanGate::AND);
1521 dnnf.addWire(dnnf.getGate(c), and_gate);
1522 dnnf.addWire(and_gate, id2);
1523 for(auto leaf : decisions) {
1524 auto in_gate = resolveVar(leaf).second;
1525 auto pid = static_cast<std::underlying_type<gate_t>::type>(in_gate);
1526 auto leaf_gate = dnnf.setGate(getUUID(in_gate), BooleanGate::IN, prob[pid]);
1527 if(leaf<0) {
1528 auto not_gate = dnnf.setGate(BooleanGate::NOT);
1529 dnnf.addWire(not_gate, leaf_gate);
1530 dnnf.addWire(and_gate, not_gate);
1531 } else {
1532 dnnf.addWire(and_gate, leaf_gate);
1533 }
1534 }
1535 }
1536 } else
1537 throw CircuitException(std::string("Unreadable d-DNNF (unknown node type: ")+c+")");
1538
1539 ++i;
1540 } while(getline(in, line));
1541
1542 dnnf.setRoot(dnnf.getGate(new_d4?"1":std::to_string(i-1)));
1543
1544 // External NNF writers (c2d, minic2d, dsharp) leave TRUE constants
1545 // (empty AND gates) and FALSE constants (empty OR gates) embedded in
1546 // the structure, because their target formats (Decision-DNNF, SDD)
1547 // require every variable to be "covered" even when its value is
1548 // forced by the CNF. Run the standard peephole so the d-DNNF returned
1549 // to callers is in canonical form, matching the tree-decomposition
1550 // builder which already simplifies.
1551 dnnf.simplify();
1552
1553 return dnnf;
1554}
1555
1556// Generic weighted-model-counting runner. Selects the counter from the
1557// registry by logical name, checks its binary and dependencies resolve,
1558// writes the weighted CNF in the convention its `parser` implies, runs the
1559// record's argtpl and reads the count back the same way -- so the four
1560// per-counter methods (ganak, sharpsat-td, dpmc, weightmc) share one
1561// runner, and a wmc tool speaking a known convention is registrable without
1562// code. The two conventions, keyed by `parser`:
1563// wmc-line MCC-2024 weighted DIMACS in ("c t wmc" + "c p weight" lines);
1564// the count on a "c s exact" / "s wmc" line out.
1565// weightmc weightmc's own weighted DIMACS in; a "mantissa x 2^exp" out.
1566double BooleanCircuit::wmcCount(gate_t g, const std::string &requested,
1567 const std::string &opt) const {
1568 // An empty tool name means "pick the best available counter" (highest
1569 // preference whose binary + dependencies resolve on PATH).
1570 std::string tool = requested;
1571 if(tool.empty()) {
1572 tool = selectTool("wmc");
1573 if(tool.empty())
1574 throw CircuitException(
1575 "no weighted model counter is available; install one (ganak, "
1576 "sharpsat-td, dpmc, weightmc) or add its directory to "
1577 "provsql.tool_search_path");
1578 }
1579
1580 const provsql::ToolRecord *rec = provsql::tool_registry().find(tool);
1581 if(rec == nullptr || !rec->hasOperation("wmc"))
1582 throw CircuitException("Unknown wmc tool '" + tool + "'");
1583 if(!rec->enabled)
1584 throw CircuitException("Tool '" + tool + "' is disabled in the tool registry");
1585
1586 // The binary (when the tool has one of its own) and every dependency must
1587 // resolve on PATH. dpmc has no binary of its own -- it is the htb | dmc
1588 // pipeline named entirely in its template -- so its components are its
1589 // dependencies.
1590 if(!rec->binary.empty() && find_external_tool(rec->binary).empty())
1591 throw CircuitException(
1592 rec->binary + " not found on PATH; install it or add its "
1593 "directory to provsql.tool_search_path");
1594 for(const std::string &dep : rec->dependencies)
1595 if(find_external_tool(dep).empty())
1596 throw CircuitException(
1597 tool + " needs '" + dep + "' on PATH; install it or add its "
1598 "directory to provsql.tool_search_path");
1599
1600 const bool weightmc_io = (rec->parser == "weightmc");
1601
1602 ScopedTempDir tmp;
1603 const std::string &dirname = tmp.path();
1604 std::string filename = tmp.file("input");
1605 std::string outfilename = tmp.file("input.out");
1606 {
1607 std::ofstream ofs(filename);
1608 if(weightmc_io) {
1609 // weightmc reads weights inline, in its own weighted-DIMACS dialect.
1610 ofs << TseytinCNF(g, true);
1611 } else {
1612 // MCC 2024 weighted DIMACS: a plain CNF plus per-input weight lines.
1613 ofs << "c t wmc\n";
1614 ofs << TseytinCNF(g, false);
1615 for(gate_t in : inputs) {
1616 int id = static_cast<int>(in) + 1;
1617 ofs << "c p weight " << id << ' ' << getProb(in) << " 0\n";
1618 ofs << "c p weight -" << id << ' ' << (1.0 - getProb(in)) << " 0\n";
1619 }
1620 }
1621 }
1622
1623 // {tmpdir} (sharpsat-td's flowcutter scratch) and {pivotAC} (weightmc's
1624 // approximation tolerance, from opt='delta;epsilon') are offered as
1625 // template placeholders; a tool that does not reference one ignores it.
1626 double epsilon = 0.8;
1627 {
1628 std::stringstream ssopt(opt);
1629 std::string delta_s, epsilon_s;
1630 getline(ssopt, delta_s, ';');
1631 getline(ssopt, epsilon_s, ';');
1632 try { double e = stod(epsilon_s); if(e != 0) epsilon = e; }
1633 catch(const std::exception &) {}
1634 }
1635 const double pivotAC = 2*ceil(exp(3./2)*(1+1/epsilon)*(1+1/epsilon));
1636
1637 std::string cmdline = rec->buildCommand(
1638 filename, outfilename, rec->binary,
1639 {{"tmpdir", dirname}, {"pivotAC", std::to_string(pivotAC)}});
1640
1641 int retvalue = run_external_tool(cmdline);
1642 CHECK_FOR_INTERRUPTS();
1643 if(retvalue)
1644 throw CircuitException(format_external_tool_status(retvalue, tool));
1645
1646 std::ifstream ifs(outfilename.c_str());
1647 double ret;
1648 if(weightmc_io) {
1649 // weightmc prints the count as "<mantissa> x 2^<exp>" on its last line.
1650 std::string line, prev_line;
1651 while(getline(ifs, line)) prev_line = line;
1652 std::stringstream ss(prev_line);
1653 std::string result;
1654 ss >> result >> result >> result >> result >> result;
1655 std::istringstream iss(result);
1656 std::string val, exp;
1657 getline(iss, val, 'x');
1658 getline(iss, exp);
1659 if(exp.size() < 2)
1660 throw CircuitException("weightmc: could not parse '" + prev_line + "'");
1661 double value = stod(val);
1662 double exponent = stod(exp.substr(2));
1663 ret = value * pow(2.0, exponent);
1664 } else {
1665 // The count is on the last "c s exact ..." (or "s wmc ...") line;
1666 // parse_wmc_value tolerates the per-tool token layout on that line.
1667 std::string line, matched;
1668 while(getline(ifs, line))
1669 if(line.rfind("c s exact", 0) == 0 || line.rfind("s wmc", 0) == 0)
1670 matched = line;
1671 if(matched.empty())
1672 throw CircuitException(tool + ": could not find a count line in output");
1673 ret = parse_wmc_value(matched, tool.c_str());
1674 }
1675
1676 if(provsql_verbose >= 20)
1677 tmp.keep();
1678 return ret;
1679}
1680
1681#endif // external-tool compilation / counting (excluded from tdkc)
1682
1684 gate_t g, std::set<gate_t> &seen,
1685 std::unordered_map<gate_t, double> &memo) const
1686{
1687 check_stack_depth(); // recurses on wires; guard deep circuits (see GenericCircuit::evaluate)
1688 // Memoised gates are variable-free (constant-only) -- returning the cached
1689 // value is sound (it touched nothing in `seen`) and avoids re-traversing a
1690 // shared constant subgraph. A variable-bearing gate is never cached, so a
1691 // second visit re-enters its subtree and throws on the repeated variable.
1692 {
1693 auto it = memo.find(g);
1694 if(it != memo.end())
1695 return it->second;
1696 }
1697
1698 // A certified gate (see DNNF_CERT_INFO) opens a maximal island, walked
1699 // iteratively (certified circuits can be as deep as the data). A
1700 // certified gate reached a second time -- from another island or from
1701 // the uncertified region -- is re-walked: its variables then hit `seen`
1702 // and the evaluation throws, so entanglement across islands is
1703 // conservatively rejected, exactly like a read-once violation.
1704 if(isDNNFCertified(g))
1705 return evaluateCertifiedIsland(g, seen, memo);
1706
1707 const std::size_t seen_before = seen.size();
1708
1709 double result=1.;
1710
1711 switch(getGateType(g)) {
1712 case BooleanGate::AND:
1713 for(const auto &c: getWires(g)) {
1714 result*=independentEvaluationInternal(c, seen, memo);
1715 }
1716 break;
1717
1718 case BooleanGate::OR:
1719 {
1720 // We collect probability among each group of children, where we
1721 // group MULIN gates with the same key var together
1722 std::map<gate_t, double> groups;
1723 std::set<gate_t> local_mulins;
1724 std::set<std::pair<gate_t, unsigned> > mulin_seen;
1725
1726 for(const auto &c: getWires(g)) {
1727 auto group = c;
1729 group = *getWires(c).begin();
1730 if(local_mulins.find(group)==local_mulins.end()) {
1731 if(seen.find(group)!=seen.end())
1732 throw CircuitException("Not an independent circuit");
1733 else
1734 seen.insert(group);
1735 local_mulins.insert(group);
1736 }
1737 auto p = std::make_pair(group, getInfo(c));
1738 if(mulin_seen.find(p)==mulin_seen.end()) {
1739 groups[group] += getProb(c);
1740 mulin_seen.insert(p);
1741 }
1742 } else
1743 groups[group] = independentEvaluationInternal(c, seen, memo);
1744 }
1745
1746 for(const auto [k, v]: groups)
1747 result *= 1-v;
1748 result = 1-result;
1749 }
1750 break;
1751
1752 case BooleanGate::NOT:
1753 result=1-independentEvaluationInternal(*getWires(g).begin(), seen, memo);
1754 break;
1755
1756 case BooleanGate::IN:
1757 {
1758 /* A leaf with probability 0 or 1 is a constant : it carries no
1759 * Boolean variable that can collide with another occurrence of
1760 * itself. Skip the seen-set bookkeeping so circuits where the
1761 * shared subgraphs are all constants (e.g. RangeCheck-resolved
1762 * comparators flowing through a non-tree structure, or
1763 * user-flipped Bernoullis pinned to 0 / 1) stay evaluable under
1764 * the read-once `independent` method. Anything strictly between
1765 * 0 and 1 is a real Bernoulli variable and must remain
1766 * read-once. */
1767 const double p = getProb(g);
1768 if (p == 0.0 || p == 1.0) {
1769 result = p;
1770 break;
1771 }
1772 if(seen.find(g)!=seen.end())
1773 throw CircuitException("Not an independent circuit");
1774 seen.insert(g);
1775 result=p;
1776 }
1777 break;
1778
1779 case BooleanGate::MULIN:
1780 {
1781 auto child = *getWires(g).begin();
1782 if(seen.find(child)!=seen.end())
1783 throw CircuitException("Not an independent circuit");
1784 seen.insert(child);
1785 result=getProb(g);
1786 }
1787 break;
1788
1791 throw CircuitException("Bad gate");
1792 }
1793
1794 // Cache only if this gate consumed no variable (constant-only subgraph).
1795 if(seen.size() == seen_before)
1796 memo[g] = result;
1797 return result;
1798}
1799
1801 gate_t root, std::set<gate_t> &seen,
1802 std::unordered_map<gate_t, double> &memo) const
1803{
1804 const std::size_t seen_before = seen.size();
1805 // Island-local values: within the island every gate is computed once
1806 // (sharing is licensed by the certificate), and the explicit post-order
1807 // stack keeps the walk safe on circuits as deep as the data.
1808 std::unordered_map<gate_t, double> val;
1809 // Key variables of MULIN gates already registered within this island: a
1810 // BID block's alternatives share their key variable by design (they are
1811 // mutually exclusive outcomes appearing under deterministic ORs), so
1812 // only the first registers globally.
1813 std::unordered_set<gate_t> island_mulvars;
1814 std::vector<gate_t> stack{root};
1815
1816 while(!stack.empty()) {
1817 const gate_t g = stack.back();
1818 if(val.find(g) != val.end()) {
1819 stack.pop_back();
1820 continue;
1821 }
1822
1823 const auto t = getGateType(g);
1824
1825 if(t == BooleanGate::IN) {
1826 // Same constant-leaf exemption as the read-once walk: a 0/1 leaf
1827 // carries no Boolean variable.
1828 const double p = getProb(g);
1829 if(p != 0.0 && p != 1.0) {
1830 if(seen.find(g) != seen.end())
1831 throw CircuitException("Not an independent circuit");
1832 seen.insert(g);
1833 }
1834 val[g] = p;
1835 stack.pop_back();
1836 continue;
1837 }
1838 if(t == BooleanGate::MULIN) {
1839 auto child = *getWires(g).begin();
1840 if(island_mulvars.insert(child).second) {
1841 if(seen.find(child) != seen.end())
1842 throw CircuitException("Not an independent circuit");
1843 seen.insert(child);
1844 }
1845 val[g] = getProb(g);
1846 stack.pop_back();
1847 continue;
1848 }
1849 if(t != BooleanGate::NOT && !isDNNFCertified(g)) {
1850 // Uncertified gate inside the island: standard read-once rules (its
1851 // own certified descendants open fresh sub-islands).
1852 val[g] = independentEvaluationInternal(g, seen, memo);
1853 stack.pop_back();
1854 continue;
1855 }
1856
1857 bool ready = true;
1858 for(const auto &c: getWires(g))
1859 if(val.find(c) == val.end()) {
1860 stack.push_back(c);
1861 ready = false;
1862 }
1863 if(!ready)
1864 continue;
1865
1866 double result;
1867 if(t == BooleanGate::NOT)
1868 result = 1 - val[getWires(g)[0]];
1869 else if(t == BooleanGate::AND) {
1870 // Certified decomposable AND: product.
1871 result = 1.;
1872 for(const auto &c: getWires(g))
1873 result *= val[c];
1874 } else {
1875 // Certified deterministic OR: plain sum (mutual exclusivity).
1876 result = 0.;
1877 for(const auto &c: getWires(g))
1878 result += val[c];
1879 }
1880 val[g] = result;
1881 stack.pop_back();
1882 }
1883
1884 // Same global-memo rule as the recursive walk: cache only when the
1885 // island consumed no variable.
1886 if(seen.size() == seen_before)
1887 memo[root] = val[root];
1888 return val[root];
1889}
1890
1892{
1893 std::set<gate_t> seen;
1894 std::unordered_map<gate_t, double> memo;
1895 return independentEvaluationInternal(g, seen, memo);
1896}
1897
1898void BooleanCircuit::setInfo(gate_t g, unsigned int i)
1899{
1900 info[g] = i;
1901}
1902
1904{
1905 auto it = info.find(g);
1906
1907 if(it==info.end())
1908 return 0;
1909 else
1910 return it->second;
1911}
1912
1914 const std::vector<gate_t> &muls,
1915 const std::vector<double> &cumulated_probs,
1916 unsigned start,
1917 unsigned end,
1918 std::vector<gate_t> &prefix)
1919{
1920 if(start==end) {
1921 getWires(muls[start]) = prefix;
1922 return;
1923 }
1924
1925 unsigned mid = (start+end)/2;
1926 // cumulated_probs is an *inclusive* prefix sum (cumulated_probs[i] =
1927 // p[0]+...+p[i]). The conditional probability of being in the left
1928 // half [start..mid] given the range [start..end] is therefore
1929 // (cum[mid] - cum[start-1]) / (cum[end] - cum[start-1])
1930 // with cum[-1] treated as 0 when start==0.
1931 double prev_start = (start == 0) ? 0. : cumulated_probs[start - 1];
1932 auto g = setGate(
1934 (cumulated_probs[mid] - prev_start) /
1935 (cumulated_probs[end] - prev_start));
1936 auto not_g = setGate(BooleanGate::NOT);
1937 getWires(not_g).push_back(g);
1938
1939 prefix.push_back(g);
1940 rewriteMultivaluedGatesRec(muls, cumulated_probs, start, mid, prefix);
1941 prefix.pop_back();
1942 prefix.push_back(not_g);
1943 rewriteMultivaluedGatesRec(muls, cumulated_probs, mid+1, end, prefix);
1944 prefix.pop_back();
1945}
1946
1947/**
1948 * @brief Check whether two double values are approximately equal.
1949 * @param a First value.
1950 * @param b Second value.
1951 * @return @c true if @p a and @p b differ by less than 10× machine epsilon.
1952 */
1953static constexpr bool almost_equals(double a, double b)
1954{
1955 double diff = a - b;
1956 constexpr double epsilon = std::numeric_limits<double>::epsilon() * 10;
1957
1958 return (diff < epsilon && diff > -epsilon);
1959}
1960
1962{
1963 std::map<gate_t,std::vector<gate_t> > var2mulinput;
1964 for(auto mul: mulinputs) {
1965 var2mulinput[*getWires(mul).begin()].push_back(mul);
1966 }
1967 mulinputs.clear();
1968
1969 for(const auto &[var, muls]: var2mulinput)
1970 {
1971 const unsigned n = muls.size();
1972 std::vector<double> cumulated_probs(n);
1973 double cumulated_prob=0.;
1974
1975 for(unsigned i=0; i<n; ++i) {
1976 cumulated_prob += getProb(muls[i]);
1977 cumulated_probs[i] = cumulated_prob;
1978 gates[static_cast<std::underlying_type<gate_t>::type>(muls[i])] = BooleanGate::AND;
1979 getWires(muls[i]).clear();
1980 }
1981
1982 std::vector<gate_t> prefix;
1983 prefix.reserve(static_cast<unsigned>(log(n)/log(2)+2));
1984 if(!almost_equals(cumulated_probs[n-1],1.)) {
1985 prefix.push_back(setGate(BooleanGate::IN, cumulated_probs[n-1]));
1986 }
1987 rewriteMultivaluedGatesRec(muls, cumulated_probs, 0, n-1, prefix);
1988 }
1989}
1990
1991gate_t BooleanCircuit::interpretAsDDInternal(gate_t g, std::set<gate_t> &seen, dDNNF &dd) const {
1992 check_stack_depth(); // recurses on wires; guard deep circuits (see GenericCircuit::evaluate)
1993
1994 // A certified gate (see DNNF_CERT_INFO) opens a maximal island, copied
1995 // iteratively with native deterministic ORs; the recursion only walks
1996 // the uncertified region.
1997 if(isDNNFCertified(g))
1998 return interpretCertifiedIsland(g, seen, dd);
1999
2000 gate_t dg{0};
2001
2002 switch(getGateType(g)) {
2003 case BooleanGate::AND:
2004 {
2005 dg = dd.setGate(BooleanGate::AND);
2006 for(const auto &c: getWires(g)) {
2007 auto dc = interpretAsDDInternal(c, seen, dd);
2008 dd.addWire(dg, dc);
2009 }
2010 }
2011 break;
2012
2013 case BooleanGate::OR:
2014 {
2015 dg = dd.setGate(BooleanGate::NOT);
2016 auto dng = dd.setGate(BooleanGate::AND);
2017 dd.addWire(dg, dng);
2018 for(const auto &c: getWires(g)) {
2019 auto dc = interpretAsDDInternal(c, seen, dd);
2020 auto dnc = dd.setGate(BooleanGate::NOT);
2021 dd.addWire(dnc, dc);
2022 dd.addWire(dng, dnc);
2023 }
2024 }
2025 break;
2026
2027 case BooleanGate::NOT:
2028 {
2029 dg = dd.setGate(BooleanGate::NOT);
2030 auto dc = interpretAsDDInternal(getWires(g)[0], seen, dd);
2031 dd.addWire(dg, dc);
2032 }
2033 break;
2034
2035 case BooleanGate::IN:
2036 if(seen.find(g)!=seen.end())
2037 throw CircuitException("Not an independent circuit");
2038 seen.insert(g);
2039 if(getUUID(g).empty())
2040 dg = dd.setGate(BooleanGate::IN, getProb(g));
2041 else
2042 dg = dd.setGate(getUUID(g), BooleanGate::IN, getProb(g));
2043 break;
2044
2045 case BooleanGate::MULIN:
2048 throw CircuitException("Unsupported gate in interpretAsDD");
2049 }
2050
2051 return dg;
2052}
2053
2055 std::set<gate_t> &seen,
2056 dDNNF &dd) const
2057{
2058 // Same iterative island walk as evaluateCertifiedIsland, building dd
2059 // gates instead of probabilities; shared sub-circuits map to shared dd
2060 // gates, and the copied gates keep their certificate so the produced
2061 // artefact remains self-describing.
2062 std::unordered_map<gate_t, gate_t> val;
2063 std::vector<gate_t> stack{root};
2064
2065 while(!stack.empty()) {
2066 const gate_t g = stack.back();
2067 if(val.find(g) != val.end()) {
2068 stack.pop_back();
2069 continue;
2070 }
2071
2072 const auto t = getGateType(g);
2073
2074 if(t == BooleanGate::IN) {
2075 if(seen.find(g) != seen.end())
2076 throw CircuitException("Not an independent circuit");
2077 seen.insert(g);
2078 val[g] = getUUID(g).empty()
2080 : dd.setGate(getUUID(g), BooleanGate::IN, getProb(g));
2081 stack.pop_back();
2082 continue;
2083 }
2084 if(t != BooleanGate::NOT && !isDNNFCertified(g)) {
2085 // Uncertified gate inside the island: standard rules (its own
2086 // certified descendants open fresh sub-islands).
2087 val[g] = interpretAsDDInternal(g, seen, dd);
2088 stack.pop_back();
2089 continue;
2090 }
2091
2092 bool ready = true;
2093 for(const auto &c: getWires(g))
2094 if(val.find(c) == val.end()) {
2095 stack.push_back(c);
2096 ready = false;
2097 }
2098 if(!ready)
2099 continue;
2100
2101 gate_t dg;
2102 if(t == BooleanGate::NOT) {
2103 dg = dd.setGate(BooleanGate::NOT);
2104 dd.addWire(dg, val[getWires(g)[0]]);
2105 } else {
2106 dg = dd.setGate(t);
2107 dd.setInfo(dg, DNNF_CERT_INFO);
2108 for(const auto &c: getWires(g))
2109 dd.addWire(dg, val[c]);
2110 }
2111 val[g] = dg;
2112 stack.pop_back();
2113 }
2114
2115 return val[root];
2116}
2117
2119{
2120 dDNNF dd;
2121 std::set<gate_t> seen;
2122
2123 dd.setRoot(interpretAsDDInternal(g, seen, dd));
2124
2125 // The OR-as-NOT(AND(NOT, ...)) De Morgan rewriting above introduces
2126 // many redundant NOT-NOT pairs and single-child AND/OR gates that the
2127 // canonical simplify pass folds away; matches what the external-KC
2128 // and tree-decomposition paths already do.
2129 dd.simplify();
2130
2131 return dd;
2132}
2133
2134// makeDD / makeDDByName fall back to the external compilers, so they too are
2135// excluded from the external-tool-free tdkc build.
2136#ifndef TDKC
2137dDNNF BooleanCircuit::makeDD(gate_t g, const std::string &method, const std::string &args) const
2138{
2139 if(method=="compilation") {
2140 return compilation(g, args);
2141 } else if(method=="tree-decomposition") {
2142 try {
2143 TreeDecomposition td(*this);
2145 *this, g, td}.build();
2146 } catch(TreeDecompositionException &) {
2147 provsql_error("Treewidth greater than %u", TreeDecomposition::MAX_TREEWIDTH);
2148 }
2149 } else if(method=="interpret-as-dd") {
2150 return interpretAsDD(g);
2151 } else {
2152 dDNNF dd;
2153 try {
2154 dd = interpretAsDD(g);
2155 if(provsql_verbose>=20)
2156 provsql_notice("Circuit interpreted as dD, %ld gates", dd.getNbGates());
2157 } catch(CircuitException &) {
2158 try {
2159 TreeDecomposition td(*this);
2161 *this, g, td}.build();
2162 if(provsql_verbose>=25)
2163 provsql_notice("dD obtained by tree decomposition, %ld gates", dd.getNbGates());
2164 } catch(TreeDecompositionException &) {
2165 // Last-resort fallback: chooseCompiler() prefers
2166 // provsql.fallback_compiler when available, else the highest-
2167 // preference compiler on PATH.
2168 std::string chosen = chooseCompiler();
2169 dd = compilation(g, chosen);
2170 if(provsql_verbose>=20)
2171 provsql_notice("dD obtained by compilation using %s, %ld gates",
2172 chosen.c_str(), dd.getNbGates());
2173 }
2174 }
2175
2176 return dd;
2177 }
2178}
2179
2180dDNNF BooleanCircuit::makeDDByName(gate_t g, const std::string &name) const
2181{
2182 // In-process meta-routes are dispatched through makeDD: "default" runs the
2183 // whole fallback chain, "tree-decomposition" / "interpret-as-dd" the single
2184 // route. Everything else goes to compilation(); the empty string there
2185 // means "pick the highest-preference available compiler" (so a no-compiler
2186 // request uses the registry instead of a hardcoded d4).
2187 if(name=="default" || name=="tree-decomposition" || name=="interpret-as-dd")
2188 return makeDD(g, name=="default" ? std::string() : name, "");
2189 return compilation(g, name);
2190}
2191#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:89
int provsql_monte_carlo_seed
Seed for the Monte Carlo sampler; -1 means non-deterministic (std::random_device); controlled by the ...
Definition provsql.c:95
bool provsql_interrupted
Global variable that becomes true if this particular backend received an interrupt signal.
Definition provsql.c:85
char * provsql_fallback_compiler
Compiler used by BooleanCircuit::makeDD as the final fallback after interpretAsDD and tree-decomposit...
Definition provsql.c:93
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.