ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
TreeDecomposition.cpp
Go to the documentation of this file.
1/**
2 * @file TreeDecomposition.cpp
3 * @brief Tree decomposition construction, manipulation, and I/O.
4 *
5 * Implements the @c TreeDecomposition class declared in
6 * @c TreeDecomposition.h:
7 *
8 * - @c TreeDecomposition(const BooleanCircuit&): constructs the tree
9 * decomposition of the circuit's primal graph using the min-fill
10 * elimination-ordering heuristic (@c PermutationStrategy). Throws
11 * @c TreeDecompositionException if the treewidth exceeds @c MAX_TREEWIDTH.
12 * - @c TreeDecomposition(std::istream&): parses a PACE-format
13 * decomposition from a stream.
14 * - Copy constructor and copy-assignment operator.
15 * - @c makeFriendly(): restructures the tree into the friendly normal
16 * form expected by @c dDNNFTreeDecompositionBuilder.
17 * - @c toDot(): GraphViz DOT string for visualising the tree.
18 * - @c operator>>(): stream extraction in PACE format.
19 *
20 * The @c reroot() helper and @c addEmptyBag(), @c addGateToBag(),
21 * @c findGateConnection() are private utilities for tree restructuring.
22 */
23#include <cassert>
24#include <set>
25#include <algorithm>
26#include <set>
27#include <string>
28#include <type_traits>
29#include <unordered_map>
30#include <unordered_set>
31#include <vector>
32
33#include "TreeDecomposition.h"
34#include "BooleanCircuit.h"
35#include "Graph.h"
36#include "PermutationStrategy.h"
38
39/* The min-fill elimination loop below can grind for tens of seconds
40 * on circuits whose treewidth approaches MAX_TREEWIDTH ; without
41 * periodic CHECK_FOR_INTERRUPTS the backend ignores both
42 * statement_timeout and pg_cancel_backend. Guard pattern mirrors
43 * BooleanCircuit.cpp's : in the standalone tdkc binary the macro
44 * resolves to a no-op. */
45#ifdef TDKC
46// In tdkc the hot-loop interrupt check services any active KCMCP session
47// (PONG / PROGRESS) and aborts the build on CANCEL or a timeout; a no-op in
48// the plain command-line mode.
49#include "tdkc_interrupt.h"
50#define CHECK_FOR_INTERRUPTS() provsql_tdkc_poll()
51#else
52extern "C" {
53#include "postgres.h"
54#include "miscadmin.h"
55}
56#endif
57
59 unsigned &max_degree)
60{
61 Graph graph(bc);
62 return degeneracyLowerBound(graph, max_degree);
63}
64
66 unsigned &max_degree)
67{
68 max_degree = 0;
69 const auto &nodes = graph.get_nodes();
70 if(nodes.empty())
71 return 0;
72
73 // Initial degrees and degree buckets. An isolated node (a disconnected
74 // constant/gate) has no adjacency entry -- get_neighbours would dereference a
75 // missing key -- so guard with has_neighbours and treat it as degree 0.
76 std::unordered_map<unsigned long, unsigned> deg;
77 deg.reserve(nodes.size());
78 unsigned max_deg = 0;
79 for(unsigned long n : nodes) {
80 unsigned d = graph.has_neighbours(n)
81 ? static_cast<unsigned>(graph.get_neighbours(n).size()) : 0;
82 deg[n] = d;
83 max_deg = std::max(max_deg, d);
84 }
85 max_degree = max_deg;
86 // Ordered buckets so the min-degree peel below picks the smallest-id node
87 // deterministically. The k-core degeneracy is a graph invariant (independent
88 // of the tie-break), but an unordered_set's begin() is an arbitrary element,
89 // which is a non-determinism smell -- a std::set makes the whole pass provably
90 // deterministic for the price of log-time bucket ops (degeneracy is only a
91 // cheap proxy, so this is negligible).
92 std::vector<std::set<unsigned long> > bucket(max_deg + 1);
93 for(const auto &kv : deg)
94 bucket[kv.second].insert(kv.first);
95
96 // Matula-Beck peel: repeatedly remove a minimum-degree node; the degeneracy
97 // is the largest degree a node has at its own removal. The graph itself is
98 // not mutated -- removed nodes are tracked in a set and skipped -- so
99 // get_neighbours stays the original adjacency. O(V+E).
100 unsigned degeneracy = 0;
101 std::unordered_set<unsigned long> removed;
102 removed.reserve(nodes.size());
103 unsigned scan = 0;
104 const size_t n_nodes = nodes.size();
105 for(size_t processed = 0; processed < n_nodes; ++processed) {
106 while(scan <= max_deg && bucket[scan].empty())
107 ++scan;
108 if(scan > max_deg)
109 break;
110 unsigned long v = *bucket[scan].begin();
111 bucket[scan].erase(bucket[scan].begin());
112 degeneracy = std::max(degeneracy, scan);
113 removed.insert(v);
114 if(graph.has_neighbours(v)) {
115 for(unsigned long u : graph.get_neighbours(v)) {
116 if(removed.find(u) != removed.end())
117 continue;
118 unsigned d = deg[u];
119 bucket[d].erase(u);
120 deg[u] = d - 1;
121 bucket[d - 1].insert(u);
122 if(d - 1 < scan)
123 scan = d - 1;
124 }
125 }
126 CHECK_FOR_INTERRUPTS();
127 }
128 return degeneracy;
129}
130
132{
133 in >> *this;
134}
135
136// This utility function looks for an existing bag to attach a new bag
137// that contains a single gate v
139{
140 for(bag_t i{0}; i<bags.size(); ++i)
141 for(auto g: getBag(i)) {
142 if(g == v)
143 return i;
144 }
145
146 return root;
147}
148
149// Transform a tree decomposition into one that is root-friendly for a
150// given node root, as per the definition page 6 of
151// https://doi.org/10.1007/s00224-019-09930-2 ; the transformation implemented is
152// described in Lemma 2.2 of that paper. The only difference is that we
153// do not enforce the tree to be full, as this is not required for
154// correctness; and we do not make it binary but n-ary for a small n, as
155// it is more efficient.
157 // Look for a bag root_connection to attach to the new root
158 auto root_connection = findGateConnection(v);
159
160 // Create the new root and attach it to this root_connection
161 auto new_root = addEmptyBag(root_connection);
162 addGateToBag(v, new_root);
163 reroot(new_root);
164
165 // Make the tree n-ary for a small n
166 auto nb_bags = bags.size();
167 for(bag_t i{0}; i<nb_bags; ++i) {
168 if(getChildren(i).size()<=OPTIMAL_ARITY)
169 continue;
170
171 auto current = i;
172 auto copy_children=getChildren(i);
173 for(int j=copy_children.size()-OPTIMAL_ARITY-1; j>=0; j-=OPTIMAL_ARITY-1) {
174 decltype(copy_children) new_children;
175 new_children.push_back(current);
176 for(auto k{j}; k>=0 && k>j-OPTIMAL_ARITY; --k)
177 new_children.push_back(copy_children[k]);
178 current = addEmptyBag(getParent(current), new_children);
179 for(auto g: getBag(i))
180 addGateToBag(g, current);
181 }
182 }
183
184 // Transform leaves into paths that introduce gates one at a time
185 nb_bags = bags.size();
186 for(bag_t i{0}; i<nb_bags; ++i) {
187 if(getChildren(i).empty()) {
188 auto p = i;
189 for(size_t j = 1; j < getBag(i).size(); ++j) {
190 p = addEmptyBag(p);
192 for(size_t k = 0; k < getBag(i).size() - j; ++k, ++it)
193 addGateToBag(*it, p);
194 }
195 }
196 }
197
198 // Construct for each bag the union of gates in its children
199 std::vector<std::set<gate_t>> gates_in_children(bags.size());
200 auto getChildrenGates = [&gates_in_children](bag_t i) -> auto& {
201 return gates_in_children[static_cast<std::underlying_type<bag_t>::type>(i)];
202 };
203
204 for(bag_t i{0}; i<bags.size(); ++i) {
205 if(i!=root) {
206 for(auto g: getBag(i))
207 getChildrenGates(getParent(i)).insert(g);
208 }
209 }
210
211 // For every gate that is in an internal bag but not in the union of
212 // its children, construct a subtree introducing these gates one at a
213 // time
214 nb_bags = bags.size();
215 for(bag_t i{0}; i<nb_bags; ++i) {
216 if(!getChildren(i).empty()) {
217 Bag intersection;
218 std::vector<gate_t> extra_gates;
219 for(auto g: getBag(i)) {
220 if(getChildrenGates(i).find(g) == getChildrenGates(i).end())
221 extra_gates.push_back(g);
222 else
223 intersection.insert(g);
224 }
225
226 if(!extra_gates.empty()) {
227 getBag(i) = intersection;
228
229 if(getChildren(i).size()==1 && intersection.size()==getChildrenGates(i).size()) {
230 // We can skip one level, to avoid creating a node identical to
231 // the single child
232
233 auto new_bag = addEmptyBag(i);
234 auto gate = extra_gates.back();
235 addGateToBag(gate, new_bag);
236 addGateToBag(gate, i);
237 extra_gates.pop_back();
238 }
239
240 auto b = i;
241 for(auto g: extra_gates) {
242 auto id = addEmptyBag(getParent(b), {b});
243 getBag(id) = getBag(b);
244 addGateToBag(g, id);
245
246 auto single_gate_bag = addEmptyBag(id);
247 addGateToBag(g, single_gate_bag);
248
249 b = id;
250 }
251 }
252 }
253 }
254}
255
257 const std::vector<bag_t> &ch)
258{
259 bag_t id {bags.size()};
260 bags.push_back(Bag());
261 parent.push_back(p);
262 getChildren(p).push_back(id);
263 children.push_back(ch);
264
265 for(auto c: ch) {
266 if(c!=root)
267 getChildren(getParent(c)).erase(std::find(getChildren(getParent(c)).begin(),
268 getChildren(getParent(c)).end(),
269 c));
270 setParent(c,id);
271 }
272
273 return id;
274}
275
280
282{
283 if(bag == root)
284 return;
285
286 for(bag_t b = bag, p = getParent(b), gp; b != root; b = p, p = gp) {
287 gp = getParent(p);
288 setParent(p, b);
289 if(p!=root)
290 getChildren(gp).erase(std::find(getChildren(gp).begin(),
291 getChildren(gp).end(),
292 p));
293 getChildren(b).push_back(p);
294 }
295
296 getChildren(getParent(bag)).erase(std::find(getChildren(getParent(bag)).begin(),
297 getChildren(getParent(bag)).end(),
298 bag));
299 setParent(bag, bag);
300 root = bag;
301}
302
303std::string TreeDecomposition::toDot() const
304{
305 std::string result="digraph circuit{\n graph [rankdir=UD] ;\n";
306
307 for(bag_t i{0}; i < bags.size(); ++i)
308 {
309 result += " " + to_string(i) + " [label=\"{";
310 bool first=true;
311 for(auto gate: getBag(i)) {
312 if(!first)
313 result+=",";
314 else
315 first=false;
316 result += to_string(gate);
317 }
318 result += "}\"];\n";
319
320 if(i!=root)
321 result+=" " + to_string(getParent(i)) + " -> " + to_string(i) + ";\n";
322 }
323
324 result += "}\n";
325
326 return result;
327}
328
329std::istream& operator>>(std::istream& in, TreeDecomposition &td)
330{
331 in >> td.treewidth;
333
334 unsigned long nb_bags;
335 in >> nb_bags;
336
337 td.bags.resize(nb_bags);
338 td.parent.resize(nb_bags);
339 td.children.resize(nb_bags);
340
341 for(bag_t i{0}; i<nb_bags; ++i) {
342 bag_t id_bag;
343 in >> id_bag;
344
345 assert(i==id_bag);
346
347 unsigned nb_gates;
348 in >> nb_gates;
349
350 assert(nb_gates <= td.treewidth+1);
351
352 for(unsigned long j=0; j<nb_gates; ++j) {
353 gate_t g;
354 in >> g;
355
356 td.addGateToBag(g, i);
357 }
358
360 in >> parent;
361 td.setParent(i, parent);
362 if(parent == i)
363 td.root = i;
364 else
365 td.getChildren(parent).push_back(i);
366
367 unsigned long nb_children;
368 in >> nb_children;
369
370 for(unsigned long j=0; j<nb_children; ++j) {
371 unsigned long child;
372 in >> child;
373
374 // Ignored, we used the parent link instead
375 }
376 }
377
378 return in;
379}
380
385
386// Taken and adapted from https://github.com/smaniu/treewidth
388 Graph graph, std::unordered_map<unsigned long, bag_t> *elimination_bag)
389{
390 PermutationStrategy strategy;
391
392 strategy.init_permutation(graph);
393
394 // Upper bound on size of the bags vector to avoid redimensioning
395 bags.reserve(graph.number_nodes());
396
397 unsigned max_width{0};
398 std::unordered_map<gate_t, bag_t> bag_ids;
399 bag_t bag_id{0};
400
401 //looping greedily through the permutation
402 //we stop when the maximum bag has the same width as the remaining graph
403 //or when we achive the partial decomposition condition
404 while(max_width<graph.number_nodes() && !strategy.empty()){
405 /* Yield to the postgres backend ; the min-fill elimination loop
406 * dominates the runtime of the tree-decomposition construction
407 * on circuits near MAX_TREEWIDTH, so per-iteration cancellation
408 * is the right granularity. */
409 CHECK_FOR_INTERRUPTS();
410 //getting the next node
411 unsigned long node = strategy.get_next();
412 //removing the node from the graph and getting its neighbours
413 std::unordered_set<unsigned long> neigh = graph.remove_node(node);
414 max_width = std::max<unsigned>(neigh.size(), max_width);
415 //we stop as soon as we find bag that is
416 if(max_width>MAX_TREEWIDTH)
418
419 //filling missing edges between the neighbours and recomputing statistics
420 // for relevant nodes in the graph (the neighbours, most of the time)
421 graph.fill(neigh);
422 strategy.recompute(neigh, graph);
423
424 Bag bag;
425 for(auto n: neigh) {
426 bag.insert(gate_t{n});
427 }
428 bag.insert(gate_t{node});
429
430 if(elimination_bag)
431 (*elimination_bag)[node] = bag_id;
432 bag_ids[gate_t{node}] = bag_id++;
433
434 bags.push_back(bag);
435 }
436
437 if(graph.get_nodes().size()>MAX_TREEWIDTH)
439
440 if(graph.number_nodes()>0) {
441 Bag remaining_bag;
442 for(auto n: graph.get_nodes()) {
443 remaining_bag.insert(gate_t{n});
444 if(elimination_bag)
445 (*elimination_bag)[n] = bag_t{bags.size()};
446 }
447 bags.push_back(remaining_bag);
448 }
449
450 parent.resize(bags.size());
451 children.resize(bags.size());
452
453 if(graph.number_nodes()==0)
454 treewidth=max_width;
455 else
456 treewidth = std::max<unsigned>(max_width,graph.number_nodes()-1);
457
458 for(bag_t i{0};i<bags.size()-1;++i){
459 bag_t min_bag{bags.size()-1};
460 for(auto n: getBag(i)) {
461 auto it = bag_ids.find(n);
462 if(it!=bag_ids.end() && it->second!=i)
463 min_bag = std::min(it->second, min_bag);
464 }
465 setParent(i, min_bag);
466 getChildren(min_bag).push_back(i);
467 }
468
469 // Special semantics: a node is its own parent if it is the root
470 root = bag_t{bags.size()-1};
472}
Boolean provenance circuit with support for knowledge compilation.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Undirected graph used in tree-decomposition computations.
Priority-queue-based node-elimination ordering for tree decomposition.
std::istream & operator>>(std::istream &in, TreeDecomposition &td)
Read a tree decomposition in PACE challenge format.
Tree decomposition of a Boolean circuit for knowledge compilation.
std::string to_string(bag_t b)
Convert a bag_t to its decimal string representation.
bag_t
Strongly-typed bag identifier for a tree decomposition.
Boolean circuit for provenance formula evaluation.
Mutable adjacency-list graph over unsigned-long node IDs.
Definition Graph.h:33
void fill(const std::unordered_set< unsigned long > &nodes, bool undirected=true)
Add all missing edges within nodes (clique fill).
Definition Graph.h:155
const std::unordered_set< unsigned long > & get_nodes() const
Return the set of all node IDs in the graph.
Definition Graph.h:235
bool has_neighbours(unsigned long node) const
Return true if node has any adjacent edges.
Definition Graph.h:189
std::unordered_set< unsigned long > remove_node(unsigned long node)
Remove node and all its incident edges.
Definition Graph.h:99
const std::unordered_set< unsigned long > & get_neighbours(unsigned long node) const
Return the neighbour set of node.
Definition Graph.h:225
unsigned long number_nodes() const
Return the number of nodes in the graph.
Definition Graph.h:243
Node-elimination ordering strategy using a priority queue.
unsigned long get_next()
Pop and return the node with the smallest statistic.
virtual void recompute(const std::unordered_set< unsigned long > &nodes, Graph &graph)
Recompute statistics for a subset of nodes and update the queue.
bool empty()
Return true if the queue is empty.
void init_permutation(Graph &graph)
Populate the priority queue with all nodes in graph.
Exception thrown when a tree decomposition cannot be constructed.
bag_t findGateConnection(gate_t v) const
Find the bag whose gate set is closest to gate v (for rooting).
bag_t addEmptyBag(bag_t parent, const std::vector< bag_t > &children=std::vector< bag_t >())
Insert a new empty bag as a child of parent.
std::vector< Bag > bags
Bag contents, indexed by bag_t.
void addGateToBag(gate_t g, bag_t b)
Add gate g to the contents of bag b.
static unsigned degeneracyLowerBound(const BooleanCircuit &bc, unsigned &max_degree)
Cheap degeneracy lower bound on the treewidth of bc's primal graph.
flat_set< gate_t, small_vector > Bag
The type of a bag: a small flat set of gate IDs.
void setParent(bag_t b, bag_t p)
Set the parent of bag b to p.
TreeDecomposition()=default
std::string toDot() const
Render the tree decomposition as a GraphViz DOT string.
void reroot(bag_t bag)
Re-root the tree so that bag becomes the root.
static constexpr int OPTIMAL_ARITY
Preferred maximum arity of bags in the friendly form.
std::vector< bag_t > parent
Parent of each bag (root points to itself).
unsigned treewidth
Treewidth of the decomposition.
bag_t getParent(bag_t b) const
Return the parent of bag b.
static constexpr int MAX_TREEWIDTH
Maximum supported treewidth.
std::vector< std::vector< bag_t > > children
Children of each bag.
bag_t root
Identifier of the root bag.
Bag & getBag(bag_t b)
Mutable access to bag b.
void makeFriendly(gate_t root)
Restructure the tree into the friendly normal form.
std::vector< bag_t > & getChildren(bag_t b)
Mutable access to the children of bag b.
Constructs a d-DNNF from a Boolean circuit and its tree decomposition.
decltype(std::begin(std::declval< const storage_t & >())) const_iterator
Definition flat_set.hpp:47
iterator begin()
Return iterator to the first element.
Definition flat_set.hpp:51
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
Build-loop interrupt hook for the standalone tdkc binary.