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 <string>
27#include <type_traits>
28
29#include "TreeDecomposition.h"
30#include "BooleanCircuit.h"
31#include "PermutationStrategy.h"
33
34/* The min-fill elimination loop below can grind for tens of seconds
35 * on circuits whose treewidth approaches MAX_TREEWIDTH ; without
36 * periodic CHECK_FOR_INTERRUPTS the backend ignores both
37 * statement_timeout and pg_cancel_backend. Guard pattern mirrors
38 * BooleanCircuit.cpp's : in the standalone tdkc binary the macro
39 * resolves to a no-op. */
40#ifdef TDKC
41#define CHECK_FOR_INTERRUPTS() ((void)0)
42#else
43extern "C" {
44#include "postgres.h"
45#include "miscadmin.h"
46}
47#endif
48
50{
51 in >> *this;
52}
53
54// This utility function looks for an existing bag to attach a new bag
55// that contains a single gate v
57{
58 for(bag_t i{0}; i<bags.size(); ++i)
59 for(auto g: getBag(i)) {
60 if(g == v)
61 return i;
62 }
63
64 return root;
65}
66
67// Transform a tree decomposition into one that is root-friendly for a
68// given node root, as per the definition page 6 of
69// https://doi.org/10.1007/s00224-019-09930-2 ; the transformation implemented is
70// described in Lemma 2.2 of that paper. The only difference is that we
71// do not enforce the tree to be full, as this is not required for
72// correctness; and we do not make it binary but n-ary for a small n, as
73// it is more efficient.
75 // Look for a bag root_connection to attach to the new root
76 auto root_connection = findGateConnection(v);
77
78 // Create the new root and attach it to this root_connection
79 auto new_root = addEmptyBag(root_connection);
80 addGateToBag(v, new_root);
81 reroot(new_root);
82
83 // Make the tree n-ary for a small n
84 auto nb_bags = bags.size();
85 for(bag_t i{0}; i<nb_bags; ++i) {
86 if(getChildren(i).size()<=OPTIMAL_ARITY)
87 continue;
88
89 auto current = i;
90 auto copy_children=getChildren(i);
91 for(int j=copy_children.size()-OPTIMAL_ARITY-1; j>=0; j-=OPTIMAL_ARITY-1) {
92 decltype(copy_children) new_children;
93 new_children.push_back(current);
94 for(auto k{j}; k>=0 && k>j-OPTIMAL_ARITY; --k)
95 new_children.push_back(copy_children[k]);
96 current = addEmptyBag(getParent(current), new_children);
97 for(auto g: getBag(i))
98 addGateToBag(g, current);
99 }
100 }
101
102 // Transform leaves into paths that introduce gates one at a time
103 nb_bags = bags.size();
104 for(bag_t i{0}; i<nb_bags; ++i) {
105 if(getChildren(i).empty()) {
106 auto p = i;
107 for(size_t j = 1; j < getBag(i).size(); ++j) {
108 p = addEmptyBag(p);
110 for(size_t k = 0; k < getBag(i).size() - j; ++k, ++it)
111 addGateToBag(*it, p);
112 }
113 }
114 }
115
116 // Construct for each bag the union of gates in its children
117 std::vector<std::set<gate_t>> gates_in_children(bags.size());
118 auto getChildrenGates = [&gates_in_children](bag_t i) -> auto& {
119 return gates_in_children[static_cast<std::underlying_type<bag_t>::type>(i)];
120 };
121
122 for(bag_t i{0}; i<bags.size(); ++i) {
123 if(i!=root) {
124 for(auto g: getBag(i))
125 getChildrenGates(getParent(i)).insert(g);
126 }
127 }
128
129 // For every gate that is in an internal bag but not in the union of
130 // its children, construct a subtree introducing these gates one at a
131 // time
132 nb_bags = bags.size();
133 for(bag_t i{0}; i<nb_bags; ++i) {
134 if(!getChildren(i).empty()) {
135 Bag intersection;
136 std::vector<gate_t> extra_gates;
137 for(auto g: getBag(i)) {
138 if(getChildrenGates(i).find(g) == getChildrenGates(i).end())
139 extra_gates.push_back(g);
140 else
141 intersection.insert(g);
142 }
143
144 if(!extra_gates.empty()) {
145 getBag(i) = intersection;
146
147 if(getChildren(i).size()==1 && intersection.size()==getChildrenGates(i).size()) {
148 // We can skip one level, to avoid creating a node identical to
149 // the single child
150
151 auto new_bag = addEmptyBag(i);
152 auto gate = extra_gates.back();
153 addGateToBag(gate, new_bag);
154 addGateToBag(gate, i);
155 extra_gates.pop_back();
156 }
157
158 auto b = i;
159 for(auto g: extra_gates) {
160 auto id = addEmptyBag(getParent(b), {b});
161 getBag(id) = getBag(b);
162 addGateToBag(g, id);
163
164 auto single_gate_bag = addEmptyBag(id);
165 addGateToBag(g, single_gate_bag);
166
167 b = id;
168 }
169 }
170 }
171 }
172}
173
175 const std::vector<bag_t> &ch)
176{
177 bag_t id {bags.size()};
178 bags.push_back(Bag());
179 parent.push_back(p);
180 getChildren(p).push_back(id);
181 children.push_back(ch);
182
183 for(auto c: ch) {
184 if(c!=root)
185 getChildren(getParent(c)).erase(std::find(getChildren(getParent(c)).begin(),
186 getChildren(getParent(c)).end(),
187 c));
188 setParent(c,id);
189 }
190
191 return id;
192}
193
198
200{
201 if(bag == root)
202 return;
203
204 for(bag_t b = bag, p = getParent(b), gp; b != root; b = p, p = gp) {
205 gp = getParent(p);
206 setParent(p, b);
207 if(p!=root)
208 getChildren(gp).erase(std::find(getChildren(gp).begin(),
209 getChildren(gp).end(),
210 p));
211 getChildren(b).push_back(p);
212 }
213
214 getChildren(getParent(bag)).erase(std::find(getChildren(getParent(bag)).begin(),
215 getChildren(getParent(bag)).end(),
216 bag));
217 setParent(bag, bag);
218 root = bag;
219}
220
221std::string TreeDecomposition::toDot() const
222{
223 std::string result="digraph circuit{\n graph [rankdir=UD] ;\n";
224
225 for(bag_t i{0}; i < bags.size(); ++i)
226 {
227 result += " " + to_string(i) + " [label=\"{";
228 bool first=true;
229 for(auto gate: getBag(i)) {
230 if(!first)
231 result+=",";
232 else
233 first=false;
234 result += to_string(gate);
235 }
236 result += "}\"];\n";
237
238 if(i!=root)
239 result+=" " + to_string(getParent(i)) + " -> " + to_string(i) + ";\n";
240 }
241
242 result += "}\n";
243
244 return result;
245}
246
247std::istream& operator>>(std::istream& in, TreeDecomposition &td)
248{
249 in >> td.treewidth;
251
252 unsigned long nb_bags;
253 in >> nb_bags;
254
255 td.bags.resize(nb_bags);
256 td.parent.resize(nb_bags);
257 td.children.resize(nb_bags);
258
259 for(bag_t i{0}; i<nb_bags; ++i) {
260 bag_t id_bag;
261 in >> id_bag;
262
263 assert(i==id_bag);
264
265 unsigned nb_gates;
266 in >> nb_gates;
267
268 assert(nb_gates <= td.treewidth+1);
269
270 for(unsigned long j=0; j<nb_gates; ++j) {
271 gate_t g;
272 in >> g;
273
274 td.addGateToBag(g, i);
275 }
276
278 in >> parent;
279 td.setParent(i, parent);
280 if(parent == i)
281 td.root = i;
282 else
283 td.getChildren(parent).push_back(i);
284
285 unsigned long nb_children;
286 in >> nb_children;
287
288 for(unsigned long j=0; j<nb_children; ++j) {
289 unsigned long child;
290 in >> child;
291
292 // Ignored, we used the parent link instead
293 }
294 }
295
296 return in;
297}
298
299// Taken and adapted from https://github.com/smaniu/treewidth
301{
302 Graph graph(bc);
303
304 PermutationStrategy strategy;
305
306 strategy.init_permutation(graph);
307
308 // Upper bound on size of the bags vector to avoid redimensioning
309 bags.reserve(graph.number_nodes());
310
311 unsigned max_width{0};
312 std::unordered_map<gate_t, bag_t> bag_ids;
313 bag_t bag_id{0};
314
315 //looping greedily through the permutation
316 //we stop when the maximum bag has the same width as the remaining graph
317 //or when we achive the partial decomposition condition
318 while(max_width<graph.number_nodes() && !strategy.empty()){
319 /* Yield to the postgres backend ; the min-fill elimination loop
320 * dominates the runtime of the tree-decomposition construction
321 * on circuits near MAX_TREEWIDTH, so per-iteration cancellation
322 * is the right granularity. */
323 CHECK_FOR_INTERRUPTS();
324 //getting the next node
325 unsigned long node = strategy.get_next();
326 //removing the node from the graph and getting its neighbours
327 std::unordered_set<unsigned long> neigh = graph.remove_node(node);
328 max_width = std::max<unsigned>(neigh.size(), max_width);
329 //we stop as soon as we find bag that is
330 if(max_width>MAX_TREEWIDTH)
332
333 //filling missing edges between the neighbours and recomputing statistics
334 // for relevant nodes in the graph (the neighbours, most of the time)
335 graph.fill(neigh);
336 strategy.recompute(neigh, graph);
337
338 Bag bag;
339 for(auto n: neigh) {
340 bag.insert(gate_t{n});
341 }
342 bag.insert(gate_t{node});
343
344 bag_ids[gate_t{node}] = bag_id++;
345
346 bags.push_back(bag);
347 }
348
349 if(graph.get_nodes().size()>MAX_TREEWIDTH)
351
352 if(graph.number_nodes()>0) {
353 Bag remaining_bag;
354 for(auto n: graph.get_nodes()) {
355 remaining_bag.insert(gate_t{n});
356 }
357 bags.push_back(remaining_bag);
358 }
359
360 parent.resize(bags.size());
361 children.resize(bags.size());
362
363 if(graph.number_nodes()==0)
364 treewidth=max_width;
365 else
366 treewidth = std::max<unsigned>(max_width,graph.number_nodes()-1);
367
368 for(bag_t i{0};i<bags.size()-1;++i){
369 bag_t min_bag{bags.size()-1};
370 for(auto n: getBag(i)) {
371 auto it = bag_ids.find(n);
372 if(it!=bag_ids.end() && it->second!=i)
373 min_bag = std::min(it->second, min_bag);
374 }
375 setParent(i, min_bag);
376 getChildren(min_bag).push_back(i);
377 }
378
379 // Special semantics: a node is its own parent if it is the root
380 root = bag_t{bags.size()-1};
382}
Boolean provenance circuit with support for knowledge compilation.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
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:152
const std::unordered_set< unsigned long > & get_nodes() const
Return the set of all node IDs in the graph.
Definition Graph.h:232
std::unordered_set< unsigned long > remove_node(unsigned long node)
Remove node and all its incident edges.
Definition Graph.h:96
unsigned long number_nodes() const
Return the number of nodes in the graph.
Definition Graph.h:240
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.
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.
iterator begin()
Return iterator to the first element.
Definition flat_set.hpp:51
decltype(std::begin(std::declval< const storage_t & >())) const_iterator
Definition flat_set.hpp:47
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