ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
dDNNFTreeDecompositionBuilder.cpp
Go to the documentation of this file.
1/**
2 * @file dDNNFTreeDecompositionBuilder.cpp
3 * @brief d-DNNF construction from a Boolean circuit and its tree decomposition.
4 *
5 * Implements the @c dDNNFTreeDecompositionBuilder::build() algorithm, which
6 * converts a bounded-treewidth Boolean circuit into a d-DNNF by following
7 * the construction in Section 5.1 of:
8 *
9 * > A. Amarilli, F. Capelli, M. Monet, P. Senellart,
10 * > "Connecting Knowledge Compilation Classes and Width Parameters".
11 * > Theory of Computing Systems 64(5):861–914, 2020.
12 * > https://doi.org/10.1007/s00224-019-09930-2
13 *
14 * The algorithm traverses the tree decomposition bottom-up. For each bag
15 * it maintains a set of *dDNNFGate* partial results, each carrying a
16 * valuation (truth-value assignment for the gates in the bag) and a
17 * suspicious set (gates not yet confirmed by their responsible bag).
18 *
19 * Private helpers:
20 * - @c builddDNNFLeaf(): generate partial results for a leaf bag.
21 * - @c collectGatesToOr(): group partial results by (valuation, suspicious).
22 * - @c builddDNNF(): main bottom-up recursion.
23 * - @c isAlmostValuation(), @c getInnocent(): utilities for the DP.
24 * - @c circuitHasWire(): O(log n) wire lookup using @c wiresSet.
25 */
26#include <algorithm>
27#include <stack>
28#include <variant>
29
31#include "Circuit.hpp"
32
33/* The bottom-up tree-decomposition DP can spend seconds-to-minutes on
34 * non-trivial circuits ; without periodic CHECK_FOR_INTERRUPTS the
35 * backend ignores both statement_timeout and pg_cancel_backend. In
36 * the standalone tdkc binary CHECK_FOR_INTERRUPTS resolves to a
37 * no-op, so we mirror the guard pattern from BooleanCircuit.cpp. */
38#ifdef TDKC
39// In tdkc the hot-loop interrupt check services any active KCMCP session and
40// aborts the build on CANCEL / timeout (a no-op in command-line mode).
41#include "tdkc_interrupt.h"
42#define CHECK_FOR_INTERRUPTS() provsql_tdkc_poll()
43#else
44extern "C" {
45#include "postgres.h"
46#include "miscadmin.h"
47}
48#endif
49
50/* Turn a bounded-treewidth circuit c for which a tree decomposition td
51 * is provided into a dNNF rooted at root, following the construction in
52 * Section 5.1 of https://doi.org/10.1007/s00224-019-09930-2 */
54 // We make the tree decomposition friendly
55 td.makeFriendly(root_id);
56
57 // We look for bags responsible for each variable
58 for(bag_t i{0}; i<td.bags.size(); ++i) {
59 const auto &b = td.getBag(i);
60 if(td.getChildren(i).empty() && b.size()==1 && c.getGateType(*b.begin()) == BooleanGate::IN)
61 responsible_bag[*b.begin()] = i;
62 }
63
64 // A friendly tree decomposition has leaf bags for every variable
65 // nodes. Let's just check that to be safe.
66 assert(responsible_bag.size()==c.inputs.size());
67
68 // Create the input and negated input gates. Inputs synthesised by
69 // rewriteMultivaluedGates() carry no UUID; using the empty UUID with
70 // setGate(uuid,...) would dedup them all into a single dDNNF gate
71 // (whose probability would then be overwritten on each call), which
72 // is wrong: each one is a distinct independent variable.
73 for(auto g: c.inputs) {
74 gate_t gate;
75 if(c.getUUID(g).empty())
76 gate = d.setGate(BooleanGate::IN, c.getProb(g));
77 else
78 gate = d.setGate(c.getUUID(g), BooleanGate::IN, c.getProb(g));
79 auto not_gate = d.setGate(BooleanGate::NOT);
80 d.addWire(not_gate, gate);
81 input_gate[g]=gate;
82 negated_input_gate[g]=not_gate;
83 }
84
85 gate_vector_t<dDNNFGate> result_gates = builddDNNF();
86
87 d.root = d.setGate(BooleanGate::OR);
88
89 for(const auto &p: result_gates) {
90 if(p.suspicious.empty() && p.valuation.find(root_id)->second) {
91 d.addWire(d.root, p.id);
92 break;
93 }
94 }
95
96 d.simplify();
97
98 return std::move(d);
99}
100
101/**
102 * @brief Return @c true if assigning @p value to a gate of type @p type is a "strong" assignment.
103 *
104 * A strong assignment is one that forces the gate's output to be determined
105 * by a single input (e.g., @c true for OR, @c false for AND).
106 * @param type Gate type (AND, OR, IN, or other).
107 * @param value Truth value assigned to the gate.
108 * @return @c true if the assignment is strong for this gate type.
109 */
110constexpr bool isStrong(BooleanGate type, bool value)
111{
112 switch(type) {
113 case BooleanGate::OR:
114 return value;
115 case BooleanGate::AND:
116 return !value;
117 case BooleanGate::IN:
118 return false;
119 default:
120 return true;
121 }
122}
123
124/**
125 * @brief Check whether all suspicious gates in @p suspicious appear in bag @p b.
126 * @param suspicious Set of gates that must be confirmed in the parent bag.
127 * @param b Bag to check against.
128 * @return @c true if every gate in @p suspicious is in @p b.
129 */
131 const TreeDecomposition::Bag &b)
132{
133 for(const auto &g: suspicious) {
134 if(b.find(g)==b.end())
135 return false;
136 }
137
138 return true;
139}
140
142 bag_t bag)
143{
144 // If the bag is empty, it behaves as if it was not there
145 if(td.getBag(bag).size()==0)
146 return {};
147
148 // Otherwise, since we have a friendly decomposition, we have a
149 // single gate
150 auto single_gate = *td.getBag(bag).begin();
151
152 // We check if this bag is responsible for an input variable
153 if(c.getGateType(single_gate)==BooleanGate::IN &&
154 responsible_bag.find(single_gate)->second==bag)
155 {
156 // No need to create an extra gate, just point to the variable and
157 // negated variable gate; no suspicious gate.
158 dDNNFGate pos = { input_gate.find(single_gate)->second,
159 {std::make_pair(single_gate,true)},
161 };
162 dDNNFGate neg = { negated_input_gate.find(single_gate)->second,
163 {std::make_pair(single_gate,false)},
165 };
166 return { std::move(pos), std::move(neg) };
167 } else {
168 gate_vector_t<dDNNFGate> result_gates;
169
170 // We create two TRUE gates (AND gates with no inputs)
171 for(auto v: {true, false}) {
172 // Optimization: we know the root is set to True, so no need to
173 // construct valuations incompatible with this
174 if(single_gate==root_id && !v)
175 continue;
176
177 suspicious_t suspicious;
178 if(isStrong(c.getGateType(single_gate), v))
179 suspicious.insert(single_gate);
180
181 result_gates.emplace_back(
182 d.setGate(BooleanGate::AND),
183 valuation_t{std::make_pair(single_gate, v)},
184 std::move(suspicious)
185 );
186 }
187
188 return result_gates;
189 }
190}
191
193 const valuation_t &valuation) const
194{
195 for(const auto &p1: valuation) {
196 for(const auto &p2: valuation) {
197 if(p1.first==p2.first)
198 continue;
199 if(!isStrong(c.getGateType(p1.first),p2.second))
200 continue;
201
202 if(circuitHasWire(p1.first,p2.first)) {
203 switch(c.getGateType(p1.first)) {
204 case BooleanGate::AND:
205 case BooleanGate::OR:
206 if(p1.second!=p2.second)
207 return false;
208 break;
210 if(p1.second==p2.second)
211 return false;
212 default:
213 ;
214 }
215 }
216 }
217 }
218
219 return true;
220}
221
224 const valuation_t &valuation,
225 const suspicious_t &innocent) const
226{
227 suspicious_t result = innocent;
228
229 for(const auto &[g1,val]: valuation) {
230 if(innocent.find(g1)!=innocent.end())
231 continue;
232
233 // We check if it is strong, if not it is innocent
234 if(!isStrong(c.getGateType(g1), valuation.find(g1)->second)) {
235 result.insert(g1);
236 continue;
237 }
238
239 // We have a strong gate not innocented by the children bags,
240 // it is only innocent if we also have in the bag an input to
241 // that gate which is strong for that gate
242 for(const auto &[g2, value]: valuation) {
243 if(g2==g1)
244 continue;
245
246 if(circuitHasWire(g1,g2)) {
247 if(isStrong(c.getGateType(g1), value)) {
248 result.insert(g1);
249 break;
250 }
251 }
252 }
253 }
254
255 return result;
256}
257
258/**
259 * @brief Write a @c gates_to_or_t DP table to an output stream for debugging.
260 * @param o Output stream.
261 * @param gates_to_or The DP table to display.
262 * @return Reference to @p o.
263 */
264std::ostream &operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::gates_to_or_t &gates_to_or)
265{
266 for(auto &[valuation, m]: gates_to_or) {
267 o << "{";
268 bool first=true;
269 for(auto &[var, val]: valuation) {
270 if(!first)
271 o << ",";
272 o << "(" << var << "," << val << ")";
273 first=false;
274 }
275 o << "}: ";
276
277 for(auto &[innocent, gates]: m) {
278 o << "{";
279 first=true;
280 for(auto &x: innocent) {
281 if(!first)
282 o << ",";
283 o << x;
284 first=false;
285 }
286 o << "} ";
287 o << "[";
288 first=true;
289 for(auto &x: gates) {
290 if(!first)
291 o << ",";
292 o << x;
293 first=false;
294 }
295 o << "] ";
296 }
297
298 o << "\n";
299 }
300
301 return o;
302}
303
305 bag_t bag,
306 const gate_vector_t<dDNNFGate> &children_gates,
307 const gates_to_or_t &partial)
308{
309 gates_to_or_t gates_to_or;
310
311 for(auto g: children_gates) {
312 /* Per-bag work can iterate over the cartesian product of
313 * children_gates and partial entries, blowing up well past the
314 * per-bag granularity of the outer builddDNNF loop ; check
315 * cancellation per child-gate so interruption is still
316 * responsive on heavy bags. */
317 CHECK_FOR_INTERRUPTS();
318 // We check all suspicious gates are in the bag of the parent
319 if(!isConnectible(g.suspicious,td.getBag(bag)))
320 continue;
321
322 // Find all valuations in partial that are compatible with this partial
323 // valuation, if it exists
324 auto compatibleValuation = [&g](const auto &p) {
325 for(const auto &[var, val]: p.first) {
326 auto it = g.valuation.find(var);
327 if(it != g.valuation.end() && it->second != val)
328 return false;
329 }
330 return true;
331 };
332
333 for (auto it = std::find_if(partial.begin(), partial.end(), compatibleValuation);
334 it != partial.end();
335 it = std::find_if(std::next(it), partial.end(), compatibleValuation)) {
336 auto &[matching_valuation, m] = *it;
337
338 valuation_t valuation = matching_valuation;
339 suspicious_t extra_innocent{};
340 for(auto &[var, val]: g.valuation) {
341 if(td.getBag(bag).find(var)!=td.getBag(bag).end()) {
342 if(matching_valuation.find(var)==matching_valuation.end())
343 valuation[var]=val;
344
345 if(g.suspicious.find(var)==g.suspicious.end()) {
346 extra_innocent.insert(var);
347 }
348 }
349 }
350
351 // We check valuation is still an almost-valuation
352 if(!isAlmostValuation(valuation))
353 continue;
354
355 for(auto &[innocent, gates]: m) {
356 suspicious_t new_innocent = extra_innocent;
357
358 for(auto s: innocent)
359 new_innocent.insert(s);
360
361 new_innocent = getInnocent(valuation, new_innocent);
362
363 if(gates.empty())
364 gates_to_or[valuation][new_innocent].push_back(g.id);
365 else {
366 for(auto g2: gates) {
367 gate_t and_gate;
368
369 // We optimize a bit by avoiding creating an AND gate if there
370 // is only one child, or if a second child is a TRUE gate
371 gate_t gates_children[2];
372 unsigned nb = 0;
373
374 if(!(d.getGateType(g.id)==BooleanGate::AND &&
375 d.getWires(g.id).empty()))
376 gates_children[nb++]=g.id;
377
378 if(!(d.getGateType(g2)==BooleanGate::AND &&
379 d.getWires(g2).empty()))
380 gates_children[nb++]=g2;
381
382 if(nb==0) {
383 // We have one (or two) TRUE gates; we just reuse it -- even
384 // though we reuse a child gate, connections will still make
385 // sense as the valuation and suspicious set been correctly
386 // computed
387 and_gate = g.id;
388 } else if(nb==1) {
389 // Only one non-TRUE gate; we reuse it. Similarly as in the
390 // previous case, even though we reuse this gate, the connections
391 // made from it will still take into account the valuation and
392 // suspicious set
393 and_gate = gates_children[0];
394 } else {
395 and_gate = d.setGate(BooleanGate::AND);
396 for(auto x: gates_children) {
397 d.addWire(and_gate, x);
398 }
399 }
400
401 gates_to_or[valuation][new_innocent].push_back(and_gate);
402 }
403 }
404 }
405 }
406 }
407
408 return gates_to_or;
409}
410
412{
413 // Unfortunately, tree decompositions can be quite deep so we need to
414 // simulate recursion with a heap-based stack, to avoid exhausting the
415 // actual memory stack
416 struct RecursionParams
417 {
418 bag_t bag;
419 size_t children_processed;
420 gates_to_or_t gates_to_or;
421
422 RecursionParams(bag_t b, size_t c, gates_to_or_t g) :
423 bag(b), children_processed(c), gates_to_or(std::move(g)) {
424 }
425
426 RecursionParams(bag_t b) :
427 bag(b), children_processed(0) {
428 gates_to_or_t::mapped_type m;
429 m[suspicious_t{}] = {};
430 gates_to_or[valuation_t{}] = std::move(m);
431 }
432 };
433
434 using RecursionResult = gate_vector_t<dDNNFGate>;
435
436 std::stack<std::variant<RecursionParams,RecursionResult> > stack;
437 stack.emplace(RecursionParams{td.root});
438
439 while(!stack.empty()) {
440 /* Yield to the postgres backend so statement_timeout /
441 * pg_cancel_backend can fire on long-running tree-decomp DPs.
442 * The bottom-up recursion processes one bag per outer-loop
443 * iteration ; per-bag work can itself be heavy but per-bag
444 * granularity is sufficient for cancellation latency in the
445 * sub-second range that matters interactively. */
446 CHECK_FOR_INTERRUPTS();
447 RecursionResult result;
448
449 if(stack.top().index()==1) { // RecursionResult
450 result = std::move(std::get<1>(stack.top()));
451 stack.pop();
452 if(stack.empty())
453 return result;
454 }
455
456 auto [bag, children_processed, gates_to_or] = std::move(std::get<0>(stack.top()));
457 stack.pop();
458
459 if(td.getChildren(bag).empty()) {
460 auto x = builddDNNFLeaf(bag);
461 stack.emplace(x);
462 } else {
463 if(children_processed>0) {
464 gates_to_or = collectGatesToOr(bag, result, gates_to_or);
465 }
466
467 if(children_processed==td.getChildren(bag).size()) {
468 gate_vector_t<dDNNFGate> result_gates;
469
470 for(auto &[valuation, m]: gates_to_or) {
471 for(auto &[innocent, gates]: m) {
472 gate_t result_gate;
473
474 assert(gates.size()!=0);
475
476 suspicious_t suspicious;
477 for(auto &[var, val]: valuation)
478 if(innocent.find(var)==innocent.end())
479 suspicious.insert(var);
480
481 // The reuse optimization in collectGatesToOr can push the same
482 // gate ID twice when two partial entries share a TRUE gate and
483 // collapse to the same (valuation, innocent) key. Duplicates
484 // in an OR's wire list cause probabilityEvaluation() to
485 // double-count via the cache, so remove them here.
486 std::sort(gates.begin(), gates.end());
487 gates.erase(std::unique(gates.begin(), gates.end()), gates.end());
488
489 if(gates.size()==1)
490 result_gate = *gates.begin();
491 else {
492 result_gate = d.setGate(BooleanGate::OR);
493 for(auto &g: gates) {
494 d.addWire(result_gate, g);
495 }
496 }
497
498 result_gates.emplace_back(result_gate, std::move(valuation), std::move(suspicious));
499 }
500 }
501
502 stack.emplace(std::move(result_gates));
503 } else {
504 stack.emplace(RecursionParams{bag, children_processed+1, std::move(gates_to_or)});
505 stack.emplace(RecursionParams{td.getChildren(bag)[children_processed]});
506 }
507 }
508 }
509
510 // We return from within the while loop, when we hit the last return
511 // value
512 assert(false);
513}
514
515std::ostream &operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::dDNNFGate &g)
516{
517 o << g.id << "; {";
518 bool first=true;
519 for(const auto &p: g.valuation) {
520 if(!first)
521 o << ",";
522 first=false;
523 o << "(" << p.first << "," << p.second << ")";
524 }
525 o << "}; {";
526 first=true;
527 for(auto x: g.suspicious) {
528 if(!first)
529 o << ",";
530 first=false;
531 o << x;
532 }
533 o << "}";
534
535 return o;
536}
537
539{
540 return wiresSet.find(std::make_pair(f,t))!=wiresSet.end();
541}
BooleanGate
Gate types for a Boolean provenance circuit.
@ 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.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Out-of-line template method implementations for Circuit<gateType>.
bag_t
Strongly-typed bag identifier for a tree decomposition.
flat_set< gate_t, small_vector > Bag
The type of a bag: a small flat set of gate IDs.
std::unordered_map< gate_t, bag_t > responsible_bag
Maps each gate to its "responsible" bag.
bool circuitHasWire(gate_t u, gate_t v) const
Return true if there is a wire from gate u to gate v.
std::vector< T > gate_vector_t
Generic bounded-capacity vector for intermediate d-DNNF gates.
gate_vector_t< dDNNFGate > builddDNNF()
Main recursive procedure: build the d-DNNF bottom-up.
std::set< std::pair< gate_t, gate_t > > wiresSet
Set of all wires in the source circuit.
flat_map< gate_t, bool, small_vector > valuation_t
Partial assignment of truth values to the gates of a bag.
std::unordered_map< gate_t, gate_t > negated_input_gate
Maps original IN gates to their negations.
dDNNF && build() &&
Execute the compilation and return the resulting d-DNNF.
flat_set< gate_t, small_vector > suspicious_t
Set of gates whose truth-value assignments are not yet confirmed.
std::unordered_map< gate_t, gate_t > input_gate
Maps original IN gates to d-DNNF IN gates.
gates_to_or_t collectGatesToOr(bag_t bag, const gate_vector_t< dDNNFGate > &gates, const gates_to_or_t &partial)
Group a list of dDNNFGate entries by (valuation, suspicious) pairs.
gate_t root_id
Root gate of the source circuit.
dDNNF d
The d-DNNF being constructed.
const BooleanCircuit & c
Source circuit.
std::unordered_map< valuation_t, std::map< suspicious_t, gate_vector_t< gate_t > > > gates_to_or_t
Dynamic-programming table: (valuation, suspicious) → list of children.
TreeDecomposition & td
Tree decomposition of the circuit's primal graph.
bool isAlmostValuation(const valuation_t &valuation) const
Return true if valuation is a "almost valuation".
suspicious_t getInnocent(const valuation_t &valuation, const suspicious_t &innocent) const
Compute the subset of innocent that remains innocent.
gate_vector_t< dDNNFGate > builddDNNFLeaf(bag_t bag)
Build the d-DNNF contributions for a leaf bag.
A d-DNNF circuit supporting exact probabilistic and game-theoretic evaluation.
Definition dDNNF.h:71
static bool isConnectible(const dDNNFTreeDecompositionBuilder::suspicious_t &suspicious, const TreeDecomposition::Bag &b)
Check whether all suspicious gates in suspicious appear in bag b.
constexpr bool isStrong(BooleanGate type, bool value)
Return true if assigning value to a gate of type type is a "strong" assignment.
std::ostream & operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::gates_to_or_t &gates_to_or)
Write a gates_to_or_t DP table to an output stream for debugging.
Constructs a d-DNNF from a Boolean circuit and its tree decomposition.
Intermediate representation of a partially built d-DNNF gate.
suspicious_t suspicious
Gates whose assignments are unconfirmed.
valuation_t valuation
Current bag's truth-value assignment.
iterator find(K &&k)
Find the element with key k.
Definition flat_map.hpp:137
iterator end()
Return iterator past the last element.
Definition flat_set.hpp:66
void insert(const K &value)
Insert value if not already present (const-ref overload).
Definition flat_set.hpp:129
iterator find(K &&k)
Find an element equal to k.
Definition flat_set.hpp:104
Build-loop interrupt hook for the standalone tdkc binary.