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#define CHECK_FOR_INTERRUPTS() ((void)0)
40#else
41extern "C" {
42#include "postgres.h"
43#include "miscadmin.h"
44}
45#endif
46
47/* Turn a bounded-treewidth circuit c for which a tree decomposition td
48 * is provided into a dNNF rooted at root, following the construction in
49 * Section 5.1 of https://doi.org/10.1007/s00224-019-09930-2 */
51 // We make the tree decomposition friendly
52 td.makeFriendly(root_id);
53
54 // We look for bags responsible for each variable
55 for(bag_t i{0}; i<td.bags.size(); ++i) {
56 const auto &b = td.getBag(i);
57 if(td.getChildren(i).empty() && b.size()==1 && c.getGateType(*b.begin()) == BooleanGate::IN)
58 responsible_bag[*b.begin()] = i;
59 }
60
61 // A friendly tree decomposition has leaf bags for every variable
62 // nodes. Let's just check that to be safe.
63 assert(responsible_bag.size()==c.inputs.size());
64
65 // Create the input and negated input gates. Inputs synthesised by
66 // rewriteMultivaluedGates() carry no UUID; using the empty UUID with
67 // setGate(uuid,...) would dedup them all into a single dDNNF gate
68 // (whose probability would then be overwritten on each call), which
69 // is wrong: each one is a distinct independent variable.
70 for(auto g: c.inputs) {
71 gate_t gate;
72 if(c.getUUID(g).empty())
73 gate = d.setGate(BooleanGate::IN, c.getProb(g));
74 else
75 gate = d.setGate(c.getUUID(g), BooleanGate::IN, c.getProb(g));
76 auto not_gate = d.setGate(BooleanGate::NOT);
77 d.addWire(not_gate, gate);
78 input_gate[g]=gate;
79 negated_input_gate[g]=not_gate;
80 }
81
82 gate_vector_t<dDNNFGate> result_gates = builddDNNF();
83
84 d.root = d.setGate(BooleanGate::OR);
85
86 for(const auto &p: result_gates) {
87 if(p.suspicious.empty() && p.valuation.find(root_id)->second) {
88 d.addWire(d.root, p.id);
89 break;
90 }
91 }
92
93 d.simplify();
94
95 return std::move(d);
96}
97
98/**
99 * @brief Return @c true if assigning @p value to a gate of type @p type is a "strong" assignment.
100 *
101 * A strong assignment is one that forces the gate's output to be determined
102 * by a single input (e.g., @c true for OR, @c false for AND).
103 * @param type Gate type (AND, OR, IN, or other).
104 * @param value Truth value assigned to the gate.
105 * @return @c true if the assignment is strong for this gate type.
106 */
107constexpr bool isStrong(BooleanGate type, bool value)
108{
109 switch(type) {
110 case BooleanGate::OR:
111 return value;
112 case BooleanGate::AND:
113 return !value;
114 case BooleanGate::IN:
115 return false;
116 default:
117 return true;
118 }
119}
120
121/**
122 * @brief Check whether all suspicious gates in @p suspicious appear in bag @p b.
123 * @param suspicious Set of gates that must be confirmed in the parent bag.
124 * @param b Bag to check against.
125 * @return @c true if every gate in @p suspicious is in @p b.
126 */
128 const TreeDecomposition::Bag &b)
129{
130 for(const auto &g: suspicious) {
131 if(b.find(g)==b.end())
132 return false;
133 }
134
135 return true;
136}
137
139 bag_t bag)
140{
141 // If the bag is empty, it behaves as if it was not there
142 if(td.getBag(bag).size()==0)
143 return {};
144
145 // Otherwise, since we have a friendly decomposition, we have a
146 // single gate
147 auto single_gate = *td.getBag(bag).begin();
148
149 // We check if this bag is responsible for an input variable
150 if(c.getGateType(single_gate)==BooleanGate::IN &&
151 responsible_bag.find(single_gate)->second==bag)
152 {
153 // No need to create an extra gate, just point to the variable and
154 // negated variable gate; no suspicious gate.
155 dDNNFGate pos = { input_gate.find(single_gate)->second,
156 {std::make_pair(single_gate,true)},
158 };
159 dDNNFGate neg = { negated_input_gate.find(single_gate)->second,
160 {std::make_pair(single_gate,false)},
162 };
163 return { std::move(pos), std::move(neg) };
164 } else {
165 gate_vector_t<dDNNFGate> result_gates;
166
167 // We create two TRUE gates (AND gates with no inputs)
168 for(auto v: {true, false}) {
169 // Optimization: we know the root is set to True, so no need to
170 // construct valuations incompatible with this
171 if(single_gate==root_id && !v)
172 continue;
173
174 suspicious_t suspicious;
175 if(isStrong(c.getGateType(single_gate), v))
176 suspicious.insert(single_gate);
177
178 result_gates.emplace_back(
179 d.setGate(BooleanGate::AND),
180 valuation_t{std::make_pair(single_gate, v)},
181 std::move(suspicious)
182 );
183 }
184
185 return result_gates;
186 }
187}
188
190 const valuation_t &valuation) const
191{
192 for(const auto &p1: valuation) {
193 for(const auto &p2: valuation) {
194 if(p1.first==p2.first)
195 continue;
196 if(!isStrong(c.getGateType(p1.first),p2.second))
197 continue;
198
199 if(circuitHasWire(p1.first,p2.first)) {
200 switch(c.getGateType(p1.first)) {
201 case BooleanGate::AND:
202 case BooleanGate::OR:
203 if(p1.second!=p2.second)
204 return false;
205 break;
206 case BooleanGate::NOT:
207 if(p1.second==p2.second)
208 return false;
209 default:
210 ;
211 }
212 }
213 }
214 }
215
216 return true;
217}
218
221 const valuation_t &valuation,
222 const suspicious_t &innocent) const
223{
224 suspicious_t result = innocent;
225
226 for(const auto &[g1,val]: valuation) {
227 if(innocent.find(g1)!=innocent.end())
228 continue;
229
230 // We check if it is strong, if not it is innocent
231 if(!isStrong(c.getGateType(g1), valuation.find(g1)->second)) {
232 result.insert(g1);
233 continue;
234 }
235
236 // We have a strong gate not innocented by the children bags,
237 // it is only innocent if we also have in the bag an input to
238 // that gate which is strong for that gate
239 for(const auto &[g2, value]: valuation) {
240 if(g2==g1)
241 continue;
242
243 if(circuitHasWire(g1,g2)) {
244 if(isStrong(c.getGateType(g1), value)) {
245 result.insert(g1);
246 break;
247 }
248 }
249 }
250 }
251
252 return result;
253}
254
255/**
256 * @brief Write a @c gates_to_or_t DP table to an output stream for debugging.
257 * @param o Output stream.
258 * @param gates_to_or The DP table to display.
259 * @return Reference to @p o.
260 */
261std::ostream &operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::gates_to_or_t &gates_to_or)
262{
263 for(auto &[valuation, m]: gates_to_or) {
264 o << "{";
265 bool first=true;
266 for(auto &[var, val]: valuation) {
267 if(!first)
268 o << ",";
269 o << "(" << var << "," << val << ")";
270 first=false;
271 }
272 o << "}: ";
273
274 for(auto &[innocent, gates]: m) {
275 o << "{";
276 first=true;
277 for(auto &x: innocent) {
278 if(!first)
279 o << ",";
280 o << x;
281 first=false;
282 }
283 o << "} ";
284 o << "[";
285 first=true;
286 for(auto &x: gates) {
287 if(!first)
288 o << ",";
289 o << x;
290 first=false;
291 }
292 o << "] ";
293 }
294
295 o << "\n";
296 }
297
298 return o;
299}
300
302 bag_t bag,
303 const gate_vector_t<dDNNFGate> &children_gates,
304 const gates_to_or_t &partial)
305{
306 gates_to_or_t gates_to_or;
307
308 for(auto g: children_gates) {
309 /* Per-bag work can iterate over the cartesian product of
310 * children_gates and partial entries, blowing up well past the
311 * per-bag granularity of the outer builddDNNF loop ; check
312 * cancellation per child-gate so interruption is still
313 * responsive on heavy bags. */
314 CHECK_FOR_INTERRUPTS();
315 // We check all suspicious gates are in the bag of the parent
316 if(!isConnectible(g.suspicious,td.getBag(bag)))
317 continue;
318
319 // Find all valuations in partial that are compatible with this partial
320 // valuation, if it exists
321 auto compatibleValuation = [&g](const auto &p) {
322 for(const auto &[var, val]: p.first) {
323 auto it = g.valuation.find(var);
324 if(it != g.valuation.end() && it->second != val)
325 return false;
326 }
327 return true;
328 };
329
330 for (auto it = std::find_if(partial.begin(), partial.end(), compatibleValuation);
331 it != partial.end();
332 it = std::find_if(std::next(it), partial.end(), compatibleValuation)) {
333 auto &[matching_valuation, m] = *it;
334
335 valuation_t valuation = matching_valuation;
336 suspicious_t extra_innocent{};
337 for(auto &[var, val]: g.valuation) {
338 if(td.getBag(bag).find(var)!=td.getBag(bag).end()) {
339 if(matching_valuation.find(var)==matching_valuation.end())
340 valuation[var]=val;
341
342 if(g.suspicious.find(var)==g.suspicious.end()) {
343 extra_innocent.insert(var);
344 }
345 }
346 }
347
348 // We check valuation is still an almost-valuation
349 if(!isAlmostValuation(valuation))
350 continue;
351
352 for(auto &[innocent, gates]: m) {
353 suspicious_t new_innocent = extra_innocent;
354
355 for(auto s: innocent)
356 new_innocent.insert(s);
357
358 new_innocent = getInnocent(valuation, new_innocent);
359
360 if(gates.empty())
361 gates_to_or[valuation][new_innocent].push_back(g.id);
362 else {
363 for(auto g2: gates) {
364 gate_t and_gate;
365
366 // We optimize a bit by avoiding creating an AND gate if there
367 // is only one child, or if a second child is a TRUE gate
368 gate_t gates_children[2];
369 unsigned nb = 0;
370
371 if(!(d.getGateType(g.id)==BooleanGate::AND &&
372 d.getWires(g.id).empty()))
373 gates_children[nb++]=g.id;
374
375 if(!(d.getGateType(g2)==BooleanGate::AND &&
376 d.getWires(g2).empty()))
377 gates_children[nb++]=g2;
378
379 if(nb==0) {
380 // We have one (or two) TRUE gates; we just reuse it -- even
381 // though we reuse a child gate, connections will still make
382 // sense as the valuation and suspicious set been correctly
383 // computed
384 and_gate = g.id;
385 } else if(nb==1) {
386 // Only one non-TRUE gate; we reuse it. Similarly as in the
387 // previous case, even though we reuse this gate, the connections
388 // made from it will still take into account the valuation and
389 // suspicious set
390 and_gate = gates_children[0];
391 } else {
392 and_gate = d.setGate(BooleanGate::AND);
393 for(auto x: gates_children) {
394 d.addWire(and_gate, x);
395 }
396 }
397
398 gates_to_or[valuation][new_innocent].push_back(and_gate);
399 }
400 }
401 }
402 }
403 }
404
405 return gates_to_or;
406}
407
409{
410 // Unfortunately, tree decompositions can be quite deep so we need to
411 // simulate recursion with a heap-based stack, to avoid exhausting the
412 // actual memory stack
413 struct RecursionParams
414 {
415 bag_t bag;
416 size_t children_processed;
417 gates_to_or_t gates_to_or;
418
419 RecursionParams(bag_t b, size_t c, gates_to_or_t g) :
420 bag(b), children_processed(c), gates_to_or(std::move(g)) {
421 }
422
423 RecursionParams(bag_t b) :
424 bag(b), children_processed(0) {
425 gates_to_or_t::mapped_type m;
426 m[suspicious_t{}] = {};
427 gates_to_or[valuation_t{}] = std::move(m);
428 }
429 };
430
431 using RecursionResult = gate_vector_t<dDNNFGate>;
432
433 std::stack<std::variant<RecursionParams,RecursionResult> > stack;
434 stack.emplace(RecursionParams{td.root});
435
436 while(!stack.empty()) {
437 /* Yield to the postgres backend so statement_timeout /
438 * pg_cancel_backend can fire on long-running tree-decomp DPs.
439 * The bottom-up recursion processes one bag per outer-loop
440 * iteration ; per-bag work can itself be heavy but per-bag
441 * granularity is sufficient for cancellation latency in the
442 * sub-second range that matters interactively. */
443 CHECK_FOR_INTERRUPTS();
444 RecursionResult result;
445
446 if(stack.top().index()==1) { // RecursionResult
447 result = std::move(std::get<1>(stack.top()));
448 stack.pop();
449 if(stack.empty())
450 return result;
451 }
452
453 auto [bag, children_processed, gates_to_or] = std::move(std::get<0>(stack.top()));
454 stack.pop();
455
456 if(td.getChildren(bag).empty()) {
457 auto x = builddDNNFLeaf(bag);
458 stack.emplace(x);
459 } else {
460 if(children_processed>0) {
461 gates_to_or = collectGatesToOr(bag, result, gates_to_or);
462 }
463
464 if(children_processed==td.getChildren(bag).size()) {
465 gate_vector_t<dDNNFGate> result_gates;
466
467 for(auto &[valuation, m]: gates_to_or) {
468 for(auto &[innocent, gates]: m) {
469 gate_t result_gate;
470
471 assert(gates.size()!=0);
472
473 suspicious_t suspicious;
474 for(auto &[var, val]: valuation)
475 if(innocent.find(var)==innocent.end())
476 suspicious.insert(var);
477
478 // The reuse optimization in collectGatesToOr can push the same
479 // gate ID twice when two partial entries share a TRUE gate and
480 // collapse to the same (valuation, innocent) key. Duplicates
481 // in an OR's wire list cause probabilityEvaluation() to
482 // double-count via the cache, so remove them here.
483 std::sort(gates.begin(), gates.end());
484 gates.erase(std::unique(gates.begin(), gates.end()), gates.end());
485
486 if(gates.size()==1)
487 result_gate = *gates.begin();
488 else {
489 result_gate = d.setGate(BooleanGate::OR);
490 for(auto &g: gates) {
491 d.addWire(result_gate, g);
492 }
493 }
494
495 result_gates.emplace_back(result_gate, std::move(valuation), std::move(suspicious));
496 }
497 }
498
499 stack.emplace(std::move(result_gates));
500 } else {
501 stack.emplace(RecursionParams{bag, children_processed+1, std::move(gates_to_or)});
502 stack.emplace(RecursionParams{td.getChildren(bag)[children_processed]});
503 }
504 }
505 }
506
507 // We return from within the while loop, when we hit the last return
508 // value
509 assert(false);
510}
511
512std::ostream &operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::dDNNFGate &g)
513{
514 o << g.id << "; {";
515 bool first=true;
516 for(const auto &p: g.valuation) {
517 if(!first)
518 o << ",";
519 first=false;
520 o << "(" << p.first << "," << p.second << ")";
521 }
522 o << "}; {";
523 first=true;
524 for(auto x: g.suspicious) {
525 if(!first)
526 o << ",";
527 first=false;
528 o << x;
529 }
530 o << "}";
531
532 return o;
533}
534
536{
537 return wiresSet.find(std::make_pair(f,t))!=wiresSet.end();
538}
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:69
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