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. Jha and D. Suciu, "Knowledge Compilation Meets Database Theory:
10 * > Compiling Queries to d-DNNFs". ICDT 2013. https://arxiv.org/pdf/1811.02944
11 *
12 * The algorithm traverses the tree decomposition bottom-up. For each bag
13 * it maintains a set of *dDNNFGate* partial results, each carrying a
14 * valuation (truth-value assignment for the gates in the bag) and a
15 * suspicious set (gates not yet confirmed by their responsible bag).
16 *
17 * Private helpers:
18 * - @c builddDNNFLeaf(): generate partial results for a leaf bag.
19 * - @c collectGatesToOr(): group partial results by (valuation, suspicious).
20 * - @c builddDNNF(): main bottom-up recursion.
21 * - @c isAlmostValuation(), @c getInnocent(): utilities for the DP.
22 * - @c circuitHasWire(): O(log n) wire lookup using @c wiresSet.
23 */
24#include <algorithm>
25#include <stack>
26#include <variant>
27
29#include "Circuit.hpp"
30
31/* Turn a bounded-treewidth circuit c for which a tree decomposition td
32 * is provided into a dNNF rooted at root, following the construction in
33 * Section 5.1 of https://arxiv.org/pdf/1811.02944 */
35 // We make the tree decomposition friendly
36 td.makeFriendly(root_id);
37
38 // We look for bags responsible for each variable
39 for(bag_t i{0}; i<td.bags.size(); ++i) {
40 const auto &b = td.getBag(i);
41 if(td.getChildren(i).empty() && b.size()==1 && c.getGateType(*b.begin()) == BooleanGate::IN)
42 responsible_bag[*b.begin()] = i;
43 }
44
45 // A friendly tree decomposition has leaf bags for every variable
46 // nodes. Let's just check that to be safe.
47 assert(responsible_bag.size()==c.inputs.size());
48
49 // Create the input and negated input gates
50 for(auto g: c.inputs) {
51 auto gate = d.setGate(c.getUUID(g), BooleanGate::IN, c.getProb(g));
52 auto not_gate = d.setGate(BooleanGate::NOT);
53 d.addWire(not_gate, gate);
54 input_gate[g]=gate;
55 negated_input_gate[g]=not_gate;
56 }
57
58 gate_vector_t<dDNNFGate> result_gates = builddDNNF();
59
60 d.root = d.setGate(BooleanGate::OR);
61
62 for(const auto &p: result_gates) {
63 if(p.suspicious.empty() && p.valuation.find(root_id)->second) {
64 d.addWire(d.root, p.id);
65 break;
66 }
67 }
68
69 d.simplify();
70
71 return std::move(d);
72}
73
74/**
75 * @brief Return @c true if assigning @p value to a gate of type @p type is a "strong" assignment.
76 *
77 * A strong assignment is one that forces the gate's output to be determined
78 * by a single input (e.g., @c true for OR, @c false for AND).
79 * @param type Gate type (AND, OR, IN, or other).
80 * @param value Truth value assigned to the gate.
81 * @return @c true if the assignment is strong for this gate type.
82 */
83constexpr bool isStrong(BooleanGate type, bool value)
84{
85 switch(type) {
86 case BooleanGate::OR:
87 return value;
89 return !value;
90 case BooleanGate::IN:
91 return false;
92 default:
93 return true;
94 }
95}
96
97/**
98 * @brief Check whether all suspicious gates in @p suspicious appear in bag @p b.
99 * @param suspicious Set of gates that must be confirmed in the parent bag.
100 * @param b Bag to check against.
101 * @return @c true if every gate in @p suspicious is in @p b.
102 */
104 const TreeDecomposition::Bag &b)
105{
106 for(const auto &g: suspicious) {
107 if(b.find(g)==b.end())
108 return false;
109 }
110
111 return true;
112}
113
115 bag_t bag)
116{
117 // If the bag is empty, it behaves as if it was not there
118 if(td.getBag(bag).size()==0)
119 return {};
120
121 // Otherwise, since we have a friendly decomposition, we have a
122 // single gate
123 auto single_gate = *td.getBag(bag).begin();
124
125 // We check if this bag is responsible for an input variable
126 if(c.getGateType(single_gate)==BooleanGate::IN &&
127 responsible_bag.find(single_gate)->second==bag)
128 {
129 // No need to create an extra gate, just point to the variable and
130 // negated variable gate; no suspicious gate.
131 dDNNFGate pos = { input_gate.find(single_gate)->second,
132 {std::make_pair(single_gate,true)},
134 };
135 dDNNFGate neg = { negated_input_gate.find(single_gate)->second,
136 {std::make_pair(single_gate,false)},
138 };
139 return { std::move(pos), std::move(neg) };
140 } else {
141 gate_vector_t<dDNNFGate> result_gates;
142
143 // We create two TRUE gates (AND gates with no inputs)
144 for(auto v: {true, false}) {
145 // Optimization: we know the root is set to True, so no need to
146 // construct valuations incompatible with this
147 if(single_gate==root_id && !v)
148 continue;
149
150 suspicious_t suspicious;
151 if(isStrong(c.getGateType(single_gate), v))
152 suspicious.insert(single_gate);
153
154 result_gates.emplace_back(
156 valuation_t{std::make_pair(single_gate, v)},
157 std::move(suspicious)
158 );
159 }
160
161 return result_gates;
162 }
163}
164
166 const valuation_t &valuation) const
167{
168 for(const auto &p1: valuation) {
169 for(const auto &p2: valuation) {
170 if(p1.first==p2.first)
171 continue;
172 if(!isStrong(c.getGateType(p1.first),p2.second))
173 continue;
174
175 if(circuitHasWire(p1.first,p2.first)) {
176 switch(c.getGateType(p1.first)) {
177 case BooleanGate::AND:
178 case BooleanGate::OR:
179 if(p1.second!=p2.second)
180 return false;
181 break;
182 case BooleanGate::NOT:
183 if(p1.second==p2.second)
184 return false;
185 default:
186 ;
187 }
188 }
189 }
190 }
191
192 return true;
193}
194
197 const valuation_t &valuation,
198 const suspicious_t &innocent) const
199{
200 suspicious_t result = innocent;
201
202 for(const auto &[g1,val]: valuation) {
203 if(innocent.find(g1)!=innocent.end())
204 continue;
205
206 // We check if it is strong, if not it is innocent
207 if(!isStrong(c.getGateType(g1), valuation.find(g1)->second)) {
208 result.insert(g1);
209 continue;
210 }
211
212 // We have a strong gate not innocented by the children bags,
213 // it is only innocent if we also have in the bag an input to
214 // that gate which is strong for that gate
215 for(const auto &[g2, value]: valuation) {
216 if(g2==g1)
217 continue;
218
219 if(circuitHasWire(g1,g2)) {
220 if(isStrong(c.getGateType(g1), value)) {
221 result.insert(g1);
222 break;
223 }
224 }
225 }
226 }
227
228 return result;
229}
230
231/**
232 * @brief Write a @c gates_to_or_t DP table to an output stream for debugging.
233 * @param o Output stream.
234 * @param gates_to_or The DP table to display.
235 * @return Reference to @p o.
236 */
237std::ostream &operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::gates_to_or_t &gates_to_or)
238{
239 for(auto &[valuation, m]: gates_to_or) {
240 o << "{";
241 bool first=true;
242 for(auto &[var, val]: valuation) {
243 if(!first)
244 o << ",";
245 o << "(" << var << "," << val << ")";
246 first=false;
247 }
248 o << "}: ";
249
250 for(auto &[innocent, gates]: m) {
251 o << "{";
252 first=true;
253 for(auto &x: innocent) {
254 if(!first)
255 o << ",";
256 o << x;
257 first=false;
258 }
259 o << "} ";
260 o << "[";
261 first=true;
262 for(auto &x: gates) {
263 if(!first)
264 o << ",";
265 o << x;
266 first=false;
267 }
268 o << "] ";
269 }
270
271 o << "\n";
272 }
273
274 return o;
275}
276
278 bag_t bag,
279 const gate_vector_t<dDNNFGate> &children_gates,
280 const gates_to_or_t &partial)
281{
282 gates_to_or_t gates_to_or;
283
284 for(auto g: children_gates) {
285 // We check all suspicious gates are in the bag of the parent
286 if(!isConnectible(g.suspicious,td.getBag(bag)))
287 continue;
288
289 // Find all valuations in partial that are compatible with this partial
290 // valuation, if it exists
291 auto compatibleValuation = [&g](const auto &p) {
292 for(const auto &[var, val]: p.first) {
293 auto it = g.valuation.find(var);
294 if(it != g.valuation.end() && it->second != val)
295 return false;
296 }
297 return true;
298 };
299
300 for (auto it = std::find_if(partial.begin(), partial.end(), compatibleValuation);
301 it != partial.end();
302 it = std::find_if(std::next(it), partial.end(), compatibleValuation)) {
303 auto &[matching_valuation, m] = *it;
304
305 valuation_t valuation = matching_valuation;
306 suspicious_t extra_innocent{};
307 for(auto &[var, val]: g.valuation) {
308 if(td.getBag(bag).find(var)!=td.getBag(bag).end()) {
309 if(matching_valuation.find(var)==matching_valuation.end())
310 valuation[var]=val;
311
312 if(g.suspicious.find(var)==g.suspicious.end()) {
313 extra_innocent.insert(var);
314 }
315 }
316 }
317
318 // We check valuation is still an almost-valuation
319 if(!isAlmostValuation(valuation))
320 continue;
321
322 for(auto &[innocent, gates]: m) {
323 suspicious_t new_innocent = extra_innocent;
324
325 for(auto s: innocent)
326 new_innocent.insert(s);
327
328 new_innocent = getInnocent(valuation, new_innocent);
329
330 if(gates.empty())
331 gates_to_or[valuation][new_innocent].push_back(g.id);
332 else {
333 for(auto g2: gates) {
334 gate_t and_gate;
335
336 // We optimize a bit by avoiding creating an AND gate if there
337 // is only one child, or if a second child is a TRUE gate
338 gate_t gates_children[2];
339 unsigned nb = 0;
340
341 if(!(d.getGateType(g.id)==BooleanGate::AND &&
342 d.getWires(g.id).empty()))
343 gates_children[nb++]=g.id;
344
345 if(!(d.getGateType(g2)==BooleanGate::AND &&
346 d.getWires(g2).empty()))
347 gates_children[nb++]=g2;
348
349 if(nb==0) {
350 // We have one (or two) TRUE gates; we just reuse it -- even
351 // though we reuse a child gate, connections will still make
352 // sense as the valuation and suspicious set been correctly
353 // computed
354 and_gate = g.id;
355 } else if(nb==1) {
356 // Only one non-TRUE gate; we reuse it. Similarly as in the
357 // previous case, even though we reuse this gate, the connections
358 // made from it will still take into account the valuation and
359 // suspicious set
360 and_gate = gates_children[0];
361 } else {
362 and_gate = d.setGate(BooleanGate::AND);
363 for(auto x: gates_children) {
364 d.addWire(and_gate, x);
365 }
366 }
367
368 gates_to_or[valuation][new_innocent].push_back(and_gate);
369 }
370 }
371 }
372 }
373 }
374
375 return gates_to_or;
376}
377
379{
380 // Unfortunately, tree decompositions can be quite deep so we need to
381 // simulate recursion with a heap-based stack, to avoid exhausting the
382 // actual memory stack
383 struct RecursionParams
384 {
385 bag_t bag;
386 size_t children_processed;
387 gates_to_or_t gates_to_or;
388
389 RecursionParams(bag_t b, size_t c, gates_to_or_t g) :
390 bag(b), children_processed(c), gates_to_or(std::move(g)) {
391 }
392
393 RecursionParams(bag_t b) :
394 bag(b), children_processed(0) {
395 gates_to_or_t::mapped_type m;
396 m[suspicious_t{}] = {};
397 gates_to_or[valuation_t{}] = std::move(m);
398 }
399 };
400
401 using RecursionResult = gate_vector_t<dDNNFGate>;
402
403 std::stack<std::variant<RecursionParams,RecursionResult> > stack;
404 stack.emplace(RecursionParams{td.root});
405
406 while(!stack.empty()) {
407 RecursionResult result;
408
409 if(stack.top().index()==1) { // RecursionResult
410 result = std::move(std::get<1>(stack.top()));
411 stack.pop();
412 if(stack.empty())
413 return result;
414 }
415
416 auto [bag, children_processed, gates_to_or] = std::move(std::get<0>(stack.top()));
417 stack.pop();
418
419 if(td.getChildren(bag).empty()) {
420 auto x = builddDNNFLeaf(bag);
421 stack.emplace(x);
422 } else {
423 if(children_processed>0) {
424 gates_to_or = collectGatesToOr(bag, result, gates_to_or);
425 }
426
427 if(children_processed==td.getChildren(bag).size()) {
428 gate_vector_t<dDNNFGate> result_gates;
429
430 for(auto &[valuation, m]: gates_to_or) {
431 for(auto &[innocent, gates]: m) {
432 gate_t result_gate;
433
434 assert(gates.size()!=0);
435
436 suspicious_t suspicious;
437 for(auto &[var, val]: valuation)
438 if(innocent.find(var)==innocent.end())
439 suspicious.insert(var);
440
441 if(gates.size()==1)
442 result_gate = *gates.begin();
443 else {
444 result_gate = d.setGate(BooleanGate::OR);
445 for(auto &g: gates) {
446 d.addWire(result_gate, g);
447 }
448 }
449
450 result_gates.emplace_back(result_gate, std::move(valuation), std::move(suspicious));
451 }
452 }
453
454 stack.emplace(std::move(result_gates));
455 } else {
456 stack.emplace(RecursionParams{bag, children_processed+1, std::move(gates_to_or)});
457 stack.emplace(RecursionParams{td.getChildren(bag)[children_processed]});
458 }
459 }
460 }
461
462 // We return from within the while loop, when we hit the last return
463 // value
464 assert(false);
465}
466
467std::ostream &operator<<(std::ostream &o, const dDNNFTreeDecompositionBuilder::dDNNFGate &g)
468{
469 o << g.id << "; {";
470 bool first=true;
471 for(const auto &p: g.valuation) {
472 if(!first)
473 o << ",";
474 first=false;
475 o << "(" << p.first << "," << p.second << ")";
476 }
477 o << "}; {";
478 first=true;
479 for(auto x: g.suspicious) {
480 if(!first)
481 o << ",";
482 first=false;
483 o << x;
484 }
485 o << "}";
486
487 return o;
488}
489
491{
492 return wiresSet.find(std::make_pair(f,t))!=wiresSet.end();
493}
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:48
Out-of-line template method implementations for Circuit<gateType>.
bag_t
Strongly-typed bag identifier for a tree decomposition.
gate_t setGate(BooleanGate type) override
Allocate a new gate with type type and no UUID.
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:139
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:129
void addWire(gate_t f, gate_t t)
Add a directed wire from gate f (parent) to gate t (child).
Definition Circuit.hpp:81
bag_t root
Identifier of the root bag.
Bag & getBag(bag_t b)
Mutable access to bag b.
std::vector< bag_t > & getChildren(bag_t b)
Mutable access to the children of bag b.
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.
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.
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.
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.
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
void insert(P &&value)
Insert or update a key-value pair.
Definition flat_map.hpp:162
iterator begin()
Return iterator to the first element.
Definition flat_set.hpp:51
iterator end()
Return iterator past the last element.
Definition flat_set.hpp:66
size_t size() const
Return the number of elements in the set.
Definition flat_set.hpp:81
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