/** * @file BooleanCircuit.cpp * @brief Boolean circuit implementation and evaluation algorithms. * * Implements the methods declared in @c BooleanCircuit.h, including: * - Gate management (@c addGate, @c setGate, @c setInfo, @c setProb). * - Probability evaluation algorithms: possible worlds, Monte Carlo, * WeightMC, independent evaluation. * - Knowledge compilation: @c compilation() (external tools), * @c interpretAsDD() (direct from circuit structure), * @c makeDD() (dispatcher). * - @c rewriteMultivaluedGates(): replaces MULVAR/MULIN clusters with * standard AND/OR/NOT circuits. * - @c Tseytin(): DIMACS/weighted CNF generation for model counters. * - @c exportCircuit(): serialisation in the @c tdkc text format. * - @c toString(): human-readable gate description. * * In the standalone @c tdkc build (when @c TDKC is defined) a lightweight * @c elog() stub replaces the PostgreSQL error-reporting function. */ #include "BooleanCircuit.h" #include "Circuit.hpp" #include extern "C" { #include #include } #include #include #include #include #include #include #include #include #include #include #include "dDNNFTreeDecompositionBuilder.h" // "provsql_utils.h" #ifdef TDKC constexpr bool provsql_interrupted = false; constexpr int provsql_verbose = 0; enum levels {ERROR, NOTICE}; #define elog(level, ...) {fprintf(stderr, __VA_ARGS__); if(level==ERROR) exit(EXIT_FAILURE);} #else extern "C" { #include "provsql_utils.h" #include "utils/elog.h" } #endif #include "provsql_error.h" gate_t BooleanCircuit::setGate(BooleanGate type) { auto id = Circuit::setGate(type); if(type == BooleanGate::IN) { setProb(id,1.); inputs.insert(id); } else if(type == BooleanGate::MULIN) { mulinputs.insert(id); } return id; } gate_t BooleanCircuit::setGate(const uuid &u, BooleanGate type) { auto id = Circuit::setGate(u, type); if(type == BooleanGate::IN) { setProb(id,1.); inputs.insert(id); } else if(type == BooleanGate::MULIN) { mulinputs.insert(id); } return id; } gate_t BooleanCircuit::setGate(const uuid &u, BooleanGate type, double p) { auto id = setGate(u, type); if(std::isnan(p)) p=1.; setProb(id,p); return id; } gate_t BooleanCircuit::setGate(BooleanGate type, double p) { auto id = setGate(type); if(std::isnan(p)) p=1.; setProb(id,p); return id; } gate_t BooleanCircuit::addGate() { auto id=Circuit::addGate(); prob.push_back(1); return id; } std::string BooleanCircuit::toString(gate_t g) const { std::string op; std::string result; switch(getGateType(g)) { case BooleanGate::IN: return "x"+to_string(g); case BooleanGate::MULIN: return "{" + to_string(*getWires(g).begin()) + "=" + std::to_string(getInfo(g)) + "}[" + std::to_string(getProb(g)) + "]"; case BooleanGate::NOT: op="¬"; break; case BooleanGate::UNDETERMINED: op="?"; break; case BooleanGate::AND: op="∧"; break; case BooleanGate::OR: op="∨"; break; case BooleanGate::MULVAR: ; // already dealt with in MULIN } if(getWires(g).empty()) { if(getGateType(g)==BooleanGate::AND) return "⊤"; else if(getGateType(g)==BooleanGate::OR) return "⊥"; else return op; } for(auto s: getWires(g)) { if(getGateType(g)==BooleanGate::NOT) result = op; else if(!result.empty()) result+=" "+op+" "; result+=toString(s); } return "("+result+")"; } std::string BooleanCircuit::exportCircuit(gate_t root) const { std::stringstream ss; std::unordered_set processed; std::stack to_process; to_process.push(root); while(!to_process.empty()) { auto g=to_process.top(); to_process.pop(); if(processed.find(g)!=processed.end()) continue; ss << g << " "; switch(getGateType(g)) { case BooleanGate::IN: ss << "IN " << getProb(g); break; case BooleanGate::NOT: ss << "NOT " << getWires(g)[0]; break; case BooleanGate::AND: ss << "AND"; for(auto s:getWires(g)) ss << " " << s; break; case BooleanGate::OR: ss << "OR"; for(auto s:getWires(g)) ss << " " << s; break; case BooleanGate::MULVAR: case BooleanGate::MULIN: case BooleanGate::UNDETERMINED: assert(false); // not done } ss << "\n"; for(auto s: getWires(g)) { if(processed.find(s)==processed.end()) to_process.push(s); } processed.insert(g); } return ss.str(); } bool BooleanCircuit::evaluate(gate_t g, const std::unordered_set &sampled) const { bool disjunction=false; switch(getGateType(g)) { case BooleanGate::IN: return sampled.find(g)!=sampled.end(); case BooleanGate::MULIN: case BooleanGate::MULVAR: throw CircuitException("Monte-Carlo sampling not implemented on multivalued inputs"); case BooleanGate::NOT: return !evaluate(*(getWires(g).begin()), sampled); case BooleanGate::AND: disjunction = false; break; case BooleanGate::OR: disjunction = true; break; case BooleanGate::UNDETERMINED: throw CircuitException("Incorrect gate type"); } for(auto s: getWires(g)) { bool e = evaluate(s, sampled); if(disjunction && e) return true; if(!disjunction && !e) return false; } if(disjunction) return false; else return true; } double BooleanCircuit::monteCarlo(gate_t g, unsigned samples) const { auto success{0u}; for(unsigned i=0; i sampled; for(auto in: inputs) { if(rand() *1. / RAND_MAX < getProb(in)) { sampled.insert(in); } } if(evaluate(g, sampled)) ++success; if(provsql_interrupted) throw CircuitException("Interrupted after "+std::to_string(i+1)+" samples"); } return success*1./samples; } double BooleanCircuit::possibleWorlds(gate_t g) const { if(inputs.size()>=8*sizeof(unsigned long long)) throw CircuitException("Too many possible worlds to iterate over"); unsigned long long nb=(1< s; double p = 1; unsigned j=0; for(gate_t in : inputs) { if(i & (1 << j)) { s.insert(in); p*=getProb(in); } else { p*=1-getProb(in); } ++j; } if(evaluate(g, s)) totalp+=p; if(provsql_interrupted) throw CircuitException("Interrupted"); } return totalp; } std::string BooleanCircuit::Tseytin(gate_t g, bool display_prob=false) const { std::vector > clauses; // Tseytin transformation for(gate_t i{0}; i(i)+1}; std::vector c = {id}; for(auto s: getWires(i)) { clauses.push_back({-id, static_cast(s)+1}); c.push_back(-static_cast(s)-1); } clauses.push_back(c); break; } case BooleanGate::OR: { int id{static_cast(i)+1}; std::vector c = {-id}; for(auto s: getWires(i)) { clauses.push_back({id, -static_cast(s)-1}); c.push_back(static_cast(s)+1); } clauses.push_back(c); } break; case BooleanGate::NOT: { int id=static_cast(i)+1; auto s=*getWires(i).begin(); clauses.push_back({-id,-static_cast(s)-1}); clauses.push_back({id,static_cast(s)+1}); break; } case BooleanGate::MULIN: throw CircuitException("Multivalued inputs should have been removed by then."); case BooleanGate::MULVAR: case BooleanGate::IN: case BooleanGate::UNDETERMINED: ; } } clauses.push_back({(int)g+1}); int fd; char cfilename[] = "/tmp/provsqlXXXXXX"; fd = mkstemp(cfilename); close(fd); std::string filename=cfilename; std::ofstream ofs(filename.c_str()); ofs << "p cnf " << gates.size() << " " << clauses.size() << "\n"; for(unsigned i=0; i::type>(in)+1) << " " << getProb(in) << "\n"; ofs << "w -" << (static_cast::type>(in)+1) << " " << (1. - getProb(in)) << "\n"; } } ofs.close(); return filename; } dDNNF BooleanCircuit::compilation(gate_t g, std::string compiler) const { std::string filename=BooleanCircuit::Tseytin(g); std::string outfilename=filename+".nnf"; if(provsql_verbose>=20) { provsql_notice("Tseytin circuit in %s", filename.c_str()); } bool new_d4 {false}; std::string cmdline=compiler+" "; if(compiler=="d4") { cmdline+="-dDNNF "+filename+" -out="+outfilename; new_d4 = true; } else if(compiler=="c2d") { cmdline+="-in "+filename+" -silent"; } else if(compiler=="minic2d") { cmdline+="-in "+filename; } else if(compiler=="dsharp") { cmdline+="-q -Fnnf "+outfilename+" "+filename; } else { throw CircuitException("Unknown compiler '"+compiler+"'"); } int retvalue=system(cmdline.c_str()); if(retvalue && compiler=="d4") { // Temporary support for older version of d4 new_d4 = false; cmdline = "d4 "+filename+" -out="+outfilename; retvalue=system(cmdline.c_str()); } if(retvalue) throw CircuitException("Error executing "+compiler); if(provsql_verbose<20) { if(unlink(filename.c_str())) { throw CircuitException("Error removing "+filename); } } std::ifstream ifs(outfilename.c_str()); std::string line; getline(ifs,line); if(line.rfind("nnf", 0) != 0) { // New d4 does not include this magic line if(compiler != "d4") { // unsatisfiable formula return dDNNF(); } } else { std::string nnf; unsigned nb_nodes, nb_edges, nb_variables; std::stringstream ss(line); ss >> nnf >> nb_nodes >> nb_edges >> nb_variables; if(nb_variables!=gates.size()) throw CircuitException("Unreadable d-DNNF (wrong number of variables: " + std::to_string(nb_variables) +" vs " + std::to_string(gates.size()) + ")"); getline(ifs,line); } dDNNF dnnf; unsigned i=0; do { std::stringstream ss(line); std::string c; ss >> c; if(c=="O") { int var, args; ss >> var >> args; auto id=dnnf.getGate(std::to_string(i)); dnnf.setGate(std::to_string(i), BooleanGate::OR); int g; while(ss >> g) { auto id2=dnnf.getGate(std::to_string(g)); dnnf.addWire(id,id2); } } else if(c=="A") { int args; ss >> args; auto id=dnnf.getGate(std::to_string(i)); dnnf.setGate(std::to_string(i), BooleanGate::AND); int g; while(ss >> g) { auto id2=dnnf.getGate(std::to_string(g)); dnnf.addWire(id,id2); } } else if(c=="L") { int leaf; ss >> leaf; auto and_gate=dnnf.setGate(std::to_string(i), BooleanGate::AND); if(gates[abs(leaf)-1]==BooleanGate::IN) { if(leaf<0) { auto leaf_gate = dnnf.setGate(getUUID(static_cast(-leaf-1)), BooleanGate::IN, prob[-leaf-1]); auto not_gate = dnnf.setGate(BooleanGate::NOT); dnnf.addWire(not_gate, leaf_gate); dnnf.addWire(and_gate, not_gate); } else { auto leaf_gate = dnnf.setGate(getUUID(static_cast(leaf-1)), BooleanGate::IN, prob[leaf-1]); dnnf.addWire(and_gate, leaf_gate); } } else { ; // Do nothing, TRUE gate } } else if(c=="f" || c=="o") { // d4 extended format // A FALSE gate is an OR gate without wires int var; ss >> var; dnnf.setGate(std::to_string(var), BooleanGate::OR); } else if(c=="t" || c=="a") { // d4 extended format // A TRUE gate is an AND gate without wires int var; ss >> var; dnnf.setGate(std::to_string(var), BooleanGate::AND); } else if(dnnf.hasGate(c)) { // d4 extended format int var; ss >> var; auto id2=dnnf.getGate(std::to_string(var)); std::vector decisions; int decision; while(ss >> decision) { if(decision==0) break; if(gates[abs(decision)-1]==BooleanGate::IN) decisions.push_back(decision); } if(decisions.empty()) { dnnf.addWire(dnnf.getGate(c), id2); } else { auto and_gate = dnnf.setGate(BooleanGate::AND); dnnf.addWire(dnnf.getGate(c), and_gate); dnnf.addWire(and_gate, id2); for(auto leaf : decisions) { if(leaf<0) { auto leaf_gate = dnnf.setGate(getUUID(static_cast(-leaf-1)), BooleanGate::IN, prob[-leaf-1]); auto not_gate = dnnf.setGate(BooleanGate::NOT); dnnf.addWire(not_gate, leaf_gate); dnnf.addWire(and_gate, not_gate); } else { auto leaf_gate = dnnf.setGate(getUUID(static_cast(leaf-1)), BooleanGate::IN, prob[leaf-1]); dnnf.addWire(and_gate, leaf_gate); } } } } else throw CircuitException(std::string("Unreadable d-DNNF (unknown node type: ")+c+")"); ++i; } while(getline(ifs, line)); ifs.close(); if(provsql_verbose<20) { if(unlink(outfilename.c_str())) { throw CircuitException("Error removing "+outfilename); } } else provsql_notice("Compiled d-DNNF in %s", outfilename.c_str()); dnnf.setRoot(dnnf.getGate(new_d4?"1":std::to_string(i-1))); return dnnf; } double BooleanCircuit::WeightMC(gate_t g, std::string opt) const { std::string filename=BooleanCircuit::Tseytin(g, true); //opt of the form 'delta;epsilon' std::stringstream ssopt(opt); std::string delta_s, epsilon_s; getline(ssopt, delta_s, ';'); getline(ssopt, epsilon_s, ';'); double delta = 0; try { delta=stod(delta_s); } catch (std::invalid_argument &) { delta=0; } double epsilon = 0; try { epsilon=stod(epsilon_s); } catch (std::invalid_argument &) { epsilon=0; } if(delta == 0) delta=0.2; if(epsilon == 0) epsilon=0.8; //TODO calcul numIterations //calcul pivotAC const double pivotAC=2*ceil(exp(3./2)*(1+1/epsilon)*(1+1/epsilon)); std::string cmdline="weightmc --startIteration=0 --gaussuntil=400 --verbosity=0 --pivotAC="+std::to_string(pivotAC)+ " "+filename+" > "+filename+".out"; int retvalue=system(cmdline.c_str()); if(retvalue) { throw CircuitException("Error executing weightmc"); } //parsing std::ifstream ifs((filename+".out").c_str()); std::string line, prev_line; while(getline(ifs,line)) prev_line=line; std::stringstream ss(prev_line); std::string result; ss >> result >> result >> result >> result >> result; std::istringstream iss(result); std::string val, exp; getline(iss, val, 'x'); getline(iss, exp); double value=stod(val); exp=exp.substr(2); double exponent=stod(exp); double ret=value*(pow(2.0,exponent)); if(unlink(filename.c_str())) { throw CircuitException("Error removing "+filename); } if(unlink((filename+".out").c_str())) { throw CircuitException("Error removing "+filename+".out"); } return ret; } double BooleanCircuit::independentEvaluationInternal( gate_t g, std::set &seen) const { double result=1.; switch(getGateType(g)) { case BooleanGate::AND: for(const auto &c: getWires(g)) { result*=independentEvaluationInternal(c, seen); } break; case BooleanGate::OR: { // We collect probability among each group of children, where we // group MULIN gates with the same key var together std::map groups; std::set local_mulins; std::set > mulin_seen; for(const auto &c: getWires(g)) { auto group = c; if(getGateType(c) == BooleanGate::MULIN) { group = *getWires(c).begin(); if(local_mulins.find(group)==local_mulins.end()) { if(seen.find(group)!=seen.end()) throw CircuitException("Not an independent circuit"); else seen.insert(group); local_mulins.insert(group); } auto p = std::make_pair(group, getInfo(c)); if(mulin_seen.find(p)==mulin_seen.end()) { groups[group] += getProb(c); mulin_seen.insert(p); } } else groups[group] = independentEvaluationInternal(c, seen); } for(const auto [k, v]: groups) result *= 1-v; result = 1-result; } break; case BooleanGate::NOT: result=1-independentEvaluationInternal(*getWires(g).begin(), seen); break; case BooleanGate::IN: if(seen.find(g)!=seen.end()) throw CircuitException("Not an independent circuit"); seen.insert(g); result=getProb(g); break; case BooleanGate::MULIN: { auto child = *getWires(g).begin(); if(seen.find(child)!=seen.end()) throw CircuitException("Not an independent circuit"); seen.insert(child); result=getProb(g); } break; case BooleanGate::UNDETERMINED: case BooleanGate::MULVAR: throw CircuitException("Bad gate"); } return result; } double BooleanCircuit::independentEvaluation(gate_t g) const { std::set seen; return independentEvaluationInternal(g, seen); } void BooleanCircuit::setInfo(gate_t g, unsigned int i) { info[g] = i; } unsigned BooleanCircuit::getInfo(gate_t g) const { auto it = info.find(g); if(it==info.end()) return 0; else return it->second; } void BooleanCircuit::rewriteMultivaluedGatesRec( const std::vector &muls, const std::vector &cumulated_probs, unsigned start, unsigned end, std::vector &prefix) { if(start==end) { getWires(muls[start]) = prefix; return; } unsigned mid = (start+end)/2; // cumulated_probs is an *inclusive* prefix sum (cumulated_probs[i] = // p[0]+...+p[i]). The conditional probability of being in the left // half [start..mid] given the range [start..end] is therefore // (cum[mid] - cum[start-1]) / (cum[end] - cum[start-1]) // with cum[-1] treated as 0 when start==0. double prev_start = (start == 0) ? 0. : cumulated_probs[start - 1]; auto g = setGate( BooleanGate::IN, (cumulated_probs[mid] - prev_start) / (cumulated_probs[end] - prev_start)); auto not_g = setGate(BooleanGate::NOT); getWires(not_g).push_back(g); prefix.push_back(g); rewriteMultivaluedGatesRec(muls, cumulated_probs, start, mid, prefix); prefix.pop_back(); prefix.push_back(not_g); rewriteMultivaluedGatesRec(muls, cumulated_probs, mid+1, end, prefix); prefix.pop_back(); } /** * @brief Check whether two double values are approximately equal. * @param a First value. * @param b Second value. * @return @c true if @p a and @p b differ by less than 10× machine epsilon. */ static constexpr bool almost_equals(double a, double b) { double diff = a - b; constexpr double epsilon = std::numeric_limits::epsilon() * 10; return (diff < epsilon && diff > -epsilon); } void BooleanCircuit::rewriteMultivaluedGates() { std::map > var2mulinput; for(auto mul: mulinputs) { var2mulinput[*getWires(mul).begin()].push_back(mul); } mulinputs.clear(); for(const auto &[var, muls]: var2mulinput) { const unsigned n = muls.size(); std::vector cumulated_probs(n); double cumulated_prob=0.; for(unsigned i=0; i::type>(muls[i])] = BooleanGate::AND; getWires(muls[i]).clear(); } std::vector prefix; prefix.reserve(static_cast(log(n)/log(2)+2)); if(!almost_equals(cumulated_probs[n-1],1.)) { prefix.push_back(setGate(BooleanGate::IN, cumulated_probs[n-1])); } rewriteMultivaluedGatesRec(muls, cumulated_probs, 0, n-1, prefix); } } gate_t BooleanCircuit::interpretAsDDInternal(gate_t g, std::set &seen, dDNNF &dd) const { gate_t dg{0}; switch(getGateType(g)) { case BooleanGate::AND: { dg = dd.setGate(BooleanGate::AND); for(const auto &c: getWires(g)) { auto dc = interpretAsDDInternal(c, seen, dd); dd.addWire(dg, dc); } } break; case BooleanGate::OR: { dg = dd.setGate(BooleanGate::NOT); auto dng = dd.setGate(BooleanGate::AND); dd.addWire(dg, dng); for(const auto &c: getWires(g)) { auto dc = interpretAsDDInternal(c, seen, dd); auto dnc = dd.setGate(BooleanGate::NOT); dd.addWire(dnc, dc); dd.addWire(dng, dnc); } } break; case BooleanGate::NOT: { dg = dd.setGate(BooleanGate::NOT); auto dc = interpretAsDDInternal(getWires(g)[0], seen, dd); dd.addWire(dg, dc); } break; case BooleanGate::IN: if(seen.find(g)!=seen.end()) throw CircuitException("Not an independent circuit"); seen.insert(g); if(getUUID(g).empty()) dg = dd.setGate(BooleanGate::IN, getProb(g)); else dg = dd.setGate(getUUID(g), BooleanGate::IN, getProb(g)); break; case BooleanGate::MULIN: case BooleanGate::MULVAR: case BooleanGate::UNDETERMINED: throw CircuitException("Unsupported gate in interpretAsDD"); } return dg; } dDNNF BooleanCircuit::interpretAsDD(gate_t g) const { dDNNF dd; std::set seen; dd.setRoot(interpretAsDDInternal(g, seen, dd)); return dd; } dDNNF BooleanCircuit::makeDD(gate_t g, const std::string &method, const std::string &args) const { if(method=="compilation") { return compilation(g, args); } else if(method=="tree-decomposition") { try { TreeDecomposition td(*this); return dDNNFTreeDecompositionBuilder{ *this, g, td}.build(); } catch(TreeDecompositionException &) { provsql_error("Treewidth greater than %u", TreeDecomposition::MAX_TREEWIDTH); } } else { dDNNF dd; try { dd = interpretAsDD(g); if(provsql_verbose>=20) provsql_notice("Circuit interpreted as dD, %ld gates", dd.getNbGates()); } catch(CircuitException &) { try { TreeDecomposition td(*this); dd = dDNNFTreeDecompositionBuilder{ *this, g, td}.build(); if(provsql_verbose>=20) provsql_notice("dD obtained by tree decomposition, %ld gates", dd.getNbGates()); } catch(TreeDecompositionException &) { dd = compilation(g, "d4"); if(provsql_verbose>=20) provsql_notice("dD obtained by compilation using d4, %ld gates", dd.getNbGates()); } } return dd; } }