ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
ReachabilityCompiler.cpp
Go to the documentation of this file.
1/**
2 * @file ReachabilityCompiler.cpp
3 * @brief Implementation of the decomposition-aligned reachability compiler.
4 *
5 * See @c ReachabilityCompiler.h for the construction's design and the
6 * structural argument (determinism and decomposability by construction).
7 *
8 * The shared implementation makes two sweeps over a min-fill tree
9 * decomposition of the data graph, both with explicit stacks
10 * (decompositions of path-like data are themselves path-like, so
11 * recursion depth would be linear in the data):
12 *
13 * - **bottom-up**: for every node, the table mapping each reachable
14 * *below state* -- the state over the node's domain induced by the
15 * edges introduced in its subtree -- to the gate computing "the
16 * subtree edges induce exactly this state";
17 * - **top-down**: symmetrically, the *above state* tables over the edges
18 * introduced outside the subtree, derived from the parent's above
19 * table joined with the sibling subtrees' below tables (prefix/suffix
20 * joins keep this linear in the node arity) and the parent's local
21 * edges.
22 *
23 * The domain of every node is its bag plus the source vertex
24 * (equivalently, the DP runs on the decomposition with the source added
25 * to every bag, still a valid decomposition of width at most tw+1).
26 * Each vertex is then read at its elimination bag: below and above
27 * states partition the worlds by their disjoint edge sets, so the
28 * acceptance test over the combination of the two states is a
29 * deterministic OR over decomposable AND pairs -- one linear-size
30 * certified d-D whose gates are shared across all the per-vertex
31 * roots.
32 *
33 * The DP scaffold is generic over the *state algebra* (the @c Ops
34 * template parameter below). Four instantiations:
35 *
36 * - @c BoolOps -- plain reachability: the state is the transitively
37 * closed reachability relation over the domain (a bitset), composed
38 * by Warshall closure. This is the default behaviour.
39 * - @c HopOps -- bounded-hop reachability: each relation entry is the
40 * *set of achievable walk lengths* up to the hop bound (a bitmask),
41 * composed in the capped min-plus-set semiring by the algebraic-path
42 * (Floyd-Warshall-Kleene) algorithm with diagonal star.
43 * - @c SetReachOps -- any-of-S reachability: the relation plus one bit
44 * per position, "reaches an S-vertex within the processed part".
45 * - @c CoverOps -- all-of-S (k-terminal) reachability: the relation
46 * plus the pending rescuer-set antichain (see its doc comment).
47 *
48 * In every case worlds map to exactly one state, so states partition
49 * worlds and every emitted OR remains deterministic; each edge
50 * variable is still introduced at one node, so ANDs remain
51 * decomposable.
52 */
54
55#include <algorithm>
56#include <array>
57#include <bitset>
58#include <cstdint>
59#include <functional>
60#include <map>
61#include <unordered_map>
62#include <unordered_set>
63#include <vector>
64
65/* The DP loops are the runtime hot spots on large instances; keep the
66 * backend cancellable, mirroring TreeDecomposition.cpp's guard pattern
67 * (no-op outside the PostgreSQL extension build). */
68#ifdef TDKC
69#include "tdkc_interrupt.h"
70#define CHECK_FOR_INTERRUPTS() provsql_tdkc_poll()
71#else
72extern "C" {
73#include "postgres.h"
74#include "miscadmin.h"
75}
76#endif
77
78namespace {
79
80/** @brief Underlying integer of a @c bag_t. */
81inline std::size_t bag_index(bag_t b)
82{
83 return static_cast<std::underlying_type<bag_t>::type>(b);
84}
85
86/** @brief Maximum size of a DP domain: a bag (@c MAX_TREEWIDTH+1) plus the two terminals. */
87constexpr int MAXD = TreeDecomposition::MAX_TREEWIDTH+3;
88
89/** @brief One edge variable: a provenance token gating one or two arcs between two vertices. */
90struct EdgeVariable {
91 unsigned long u; ///< First endpoint.
92 unsigned long v; ///< Second endpoint.
93 bool arc_uv; ///< Arc u -> v present when the variable is true.
94 bool arc_vu; ///< Arc v -> u present when the variable is true.
95 bool certain = false; ///< Always-present arc(s): no gating variable (super-source arcs of untracked / constant sources).
96 unsigned weight = 1; ///< Walk-length contribution (0 for super-source arcs; only the hop mode reads it).
97 std::string token; ///< Provenance token (UUID; empty when certain).
98 double prob; ///< Tuple probability (unused when certain).
99};
100
101/** @brief One alternative of a BID block: an arc present iff its @c mulinput outcome is drawn. */
102struct BlockAlternative {
103 unsigned long u; ///< First endpoint.
104 unsigned long v; ///< Second endpoint.
105 bool arc_uv; ///< Arc u -> v in this outcome.
106 bool arc_vu; ///< Arc v -> u in this outcome.
107 std::string token; ///< The @c mulinput token (the outcome's literal).
108 double prob; ///< Outcome probability.
109 unsigned index; ///< Outcome index within the block.
110};
111
112/** @brief A block of mutually exclusive arc alternatives (one @c repair_key block). */
113struct EdgeBlock {
114 std::string key; ///< The block's key variable (MULVAR UUID).
115 std::vector<BlockAlternative> alts; ///< The alternatives.
116 std::vector<unsigned long> endpoints; ///< Distinct non-self-loop endpoints (must share a bag).
117};
118
119// ---------------------------------------------------------------------
120// State algebras.
121// ---------------------------------------------------------------------
122
123/**
124 * @brief Context handed to @c Ops::liftExtra() by the domain
125 * re-expression: the outgoing domain's vertices and the source's
126 * position in it, for algebras whose extra state depends on
127 * *which* vertices are forgotten (the coverage algebra records a
128 * rescuer set when a forgotten vertex is a target-set member).
129 */
130struct LiftContext {
131 const std::vector<unsigned long> &from_domain; ///< Vertices of the outgoing domain, position-indexed.
132 int from_ps; ///< Source position in the outgoing domain.
133};
134
135/**
136 * @brief Plain-reachability state algebra.
137 *
138 * The state is a reachability relation over the first @c d domain
139 * positions, as a bitset: bit @c i*MAXD+j set iff position @c j is
140 * reachable from position @c i within the processed part of the graph.
141 * Always reflexive and transitively closed.
142 */
143struct BoolOps {
144 static constexpr bool tracks_lengths = false;
145 static constexpr bool has_target_set = false;
146 static constexpr bool final_collapse = false;
147 using State = std::bitset<MAXD*MAXD>;
148 using Entry = bool;
149 /** @brief Hash functor (delegates to @c std::hash of the bitset). */
150 struct Hash {
151 std::size_t operator()(const State &s) const noexcept {
152 return std::hash<State>()(s);
153 }
154 };
155
156 /** @brief The identity (reflexive, empty) relation over @p d positions. */
157 State identity(int d) const {
158 State r;
159 for (int i = 0; i < d; ++i)
160 r.set(i*MAXD + i);
161 return r;
162 }
163 /** @brief Warshall transitive closure over the first @p d positions, in place. */
164 void close(State &r, int d) const {
165 for (int k = 0; k < d; ++k)
166 for (int i = 0; i < d; ++i)
167 if (r[i*MAXD + k])
168 for (int j = 0; j < d; ++j)
169 if (r[k*MAXD + j])
170 r.set(i*MAXD + j);
171 }
172 /** @brief Entrywise union of @p b into @p a (caller closes afterwards). */
173 void unite(State &a, const State &b) const {
174 a |= b;
175 }
176 /** @brief Record the arc @p pu -> @p pv (weight is a hop-mode concept). */
177 void addArc(State &s, int pu, int pv, unsigned /*weight*/) const {
178 s.set(pu*MAXD + pv);
179 }
180 /** @brief Read entry (@p i, @p j). */
181 Entry get(const State &s, int i, int j) const {
182 return s[i*MAXD + j];
183 }
184 /** @brief Whether an entry carries no information. */
185 static bool emptyEntry(Entry e) {
186 return !e;
187 }
188 /** @brief Merge an entry into (@p i, @p j) (used by domain re-expression). */
189 void merge(State &s, int i, int j, Entry /*e*/) const {
190 s.set(i*MAXD + j);
191 }
192 /** @brief Domain-dependent state seeding (a set-reachability concept). */
193 void seed(State &, const std::vector<unsigned long> &) const {
194 }
195 /** @brief Per-position extra state transfer under re-expression (no-op). */
196 void liftExtra(const State &, State &, const std::vector<int> &,
197 const LiftContext &) const {
198 }
199 /** @brief Post-closure canonicalisation hook (a coverage concept). */
200 void normalize(State &, int, int) const {
201 }
202};
203
204/**
205 * @brief Bounded-hop state algebra.
206 *
207 * Each relation entry is the set of achievable walk lengths between two
208 * domain positions within the processed part of the graph, as a bitmask
209 * over lengths @c 0..bound (so the diagonal carries bit 0). Entries
210 * compose by capped Minkowski sum -- @c mink(a,b) is the set of sums of
211 * a length in @p a and one in @p b, lengths above the bound dropped --
212 * and the closure is the algebraic-path (Kleene) algorithm with the
213 * diagonal star, exact in this finite idempotent semiring.
214 */
215struct HopOps {
216 static constexpr bool tracks_lengths = true;
217 static constexpr bool has_target_set = false;
218 static constexpr bool final_collapse = false;
219 using Entry = std::uint64_t;
220 /** @brief A matrix of length-set bitmasks (unused cells stay zero). */
221 struct State {
222 std::array<Entry, MAXD*MAXD> m;
223 State() : m{} {
224 }
225 bool operator==(const State &o) const {
226 return m == o.m;
227 }
228 };
229 /** @brief FNV-1a over the matrix words. */
230 struct Hash {
231 std::size_t operator()(const State &s) const noexcept {
232 std::uint64_t h = 1469598103934665603ull;
233 for (Entry e : s.m) {
234 h ^= e;
235 h *= 1099511628211ull;
236 }
237 return static_cast<std::size_t>(h);
238 }
239 };
240
241 Entry full; ///< Mask of representable lengths: bits 0..bound.
242
243 /** @brief Construct for walk lengths up to @p bound (at most 62). */
244 explicit HopOps(unsigned bound)
245 : full(bound >= 63 ? ~Entry(0) : ((Entry(1) << (bound+1)) - 1)) {
246 }
247
248 /** @brief Capped Minkowski sum of two length sets. */
249 Entry mink(Entry a, Entry b) const {
250 Entry r = 0;
251 while (b) {
252 const int s = __builtin_ctzll(b);
253 b &= b - 1;
254 r |= (a << s);
255 }
256 return r & full;
257 }
258 /** @brief Kleene star of a diagonal length set (always contains 0). */
259 Entry star(Entry diag) const {
260 Entry s = 1;
261 for (;;) {
262 const Entry ns = s | mink(s, diag);
263 if (ns == s)
264 return s;
265 s = ns;
266 }
267 }
268
269 /** @brief Identity: length 0 on the diagonal, nothing elsewhere. */
270 State identity(int d) const {
271 State r;
272 for (int i = 0; i < d; ++i)
273 r.m[i*MAXD + i] = 1;
274 return r;
275 }
276 /**
277 * @brief Algebraic-path closure over the first @p d positions, in place.
278 *
279 * Standard Floyd-Warshall-Kleene: for each pivot @c k,
280 * @c r[i][j] |= r[i][k] * star(r[k][k]) * r[k][j], reading the pivot
281 * row and column as snapshots from before the pass.
282 */
283 void close(State &r, int d) const {
284 Entry col[MAXD], row[MAXD];
285 for (int k = 0; k < d; ++k) {
286 const Entry sk = star(r.m[k*MAXD + k]);
287 for (int i = 0; i < d; ++i) {
288 col[i] = r.m[i*MAXD + k];
289 row[i] = mink(sk, r.m[k*MAXD + i]);
290 }
291 for (int i = 0; i < d; ++i) {
292 if (!col[i])
293 continue;
294 for (int j = 0; j < d; ++j)
295 if (row[j])
296 r.m[i*MAXD + j] |= mink(col[i], row[j]);
297 }
298 }
299 }
300 /** @brief Entrywise union of @p b into @p a (caller closes afterwards). */
301 void unite(State &a, const State &b) const {
302 for (std::size_t i = 0; i < a.m.size(); ++i)
303 a.m[i] |= b.m[i];
304 }
305 /** @brief Record an arc of walk length @p weight (a no-op above the bound). */
306 void addArc(State &s, int pu, int pv, unsigned weight) const {
307 const Entry b = (weight < 64) ? (Entry(1) << weight) & full : 0;
308 s.m[pu*MAXD + pv] |= b;
309 }
310 /** @brief Read entry (@p i, @p j). */
311 Entry get(const State &s, int i, int j) const {
312 return s.m[i*MAXD + j];
313 }
314 /** @brief Whether an entry carries no information. */
315 static bool emptyEntry(Entry e) {
316 return e == 0;
317 }
318 /** @brief Merge an entry into (@p i, @p j) (used by domain re-expression). */
319 void merge(State &s, int i, int j, Entry e) const {
320 s.m[i*MAXD + j] |= e;
321 }
322 /** @brief Domain-dependent state seeding (a set-reachability concept). */
323 void seed(State &, const std::vector<unsigned long> &) const {
324 }
325 /** @brief Per-position extra state transfer under re-expression (no-op). */
326 void liftExtra(const State &, State &, const std::vector<int> &,
327 const LiftContext &) const {
328 }
329 /** @brief Post-closure canonicalisation hook (a coverage concept). */
330 void normalize(State &, int, int) const {
331 }
332};
333
334/**
335 * @brief Set-reachability state algebra: plain reachability plus, per
336 * domain position, one bit "this position reaches some vertex
337 * of the target set within the processed part".
338 *
339 * The bit is the Courcelle congruence for @f$\exists y\, S(y) \wedge
340 * \mathrm{reach}(x, y)@f$: set membership of a domain vertex seeds its
341 * own bit; closure propagates bits backwards along the (transitively
342 * closed) relation in one pass; forgetting a vertex needs no special
343 * handling, since closure already folded its bit into every position
344 * that reaches it. Worlds still map to exactly one state, so the
345 * emitted ORs stay deterministic.
346 */
347struct SetReachOps {
348 static constexpr bool tracks_lengths = false;
349 static constexpr bool has_target_set = true;
350 static constexpr bool final_collapse = false;
351 using Entry = bool;
352 /** @brief Closed relation plus the per-position set-reachability bits. */
353 struct State {
354 std::bitset<MAXD*MAXD> rel; ///< Reachability relation (as @c BoolOps).
355 std::bitset<MAXD> dvec; ///< Position reaches a set vertex within the part.
356 bool operator==(const State &o) const {
357 return rel == o.rel && dvec == o.dvec;
358 }
359 };
360 /** @brief Hash over both components. */
361 struct Hash {
362 std::size_t operator()(const State &s) const noexcept {
363 return std::hash<std::bitset<MAXD*MAXD> >()(s.rel) * 1099511628211ull
364 ^ std::hash<std::bitset<MAXD> >()(s.dvec);
365 }
366 };
367
368 const std::unordered_set<unsigned long> *target_set; ///< The vertex set S.
369
370 /** @brief Identity relation, no bits (seeding is domain-dependent). */
371 State identity(int d) const {
372 State s;
373 for (int i = 0; i < d; ++i)
374 s.rel.set(i*MAXD + i);
375 return s;
376 }
377 /** @brief Warshall closure, then one backward propagation of the bits. */
378 void close(State &s, int d) const {
379 for (int k = 0; k < d; ++k)
380 for (int i = 0; i < d; ++i)
381 if (s.rel[i*MAXD + k])
382 for (int j = 0; j < d; ++j)
383 if (s.rel[k*MAXD + j])
384 s.rel.set(i*MAXD + j);
385 // One pass suffices: rel is transitively closed, and dvec(y)
386 // already accounts for everything y reaches within the part.
387 for (int i = 0; i < d; ++i) {
388 if (s.dvec[i])
389 continue;
390 for (int j = 0; j < d; ++j)
391 if (s.rel[i*MAXD + j] && s.dvec[j]) {
392 s.dvec.set(i);
393 break;
394 }
395 }
396 }
397 /** @brief Entrywise union (caller closes afterwards). */
398 void unite(State &a, const State &b) const {
399 a.rel |= b.rel;
400 a.dvec |= b.dvec;
401 }
402 /** @brief Record the arc @p pu -> @p pv. */
403 void addArc(State &s, int pu, int pv, unsigned /*weight*/) const {
404 s.rel.set(pu*MAXD + pv);
405 }
406 /** @brief Read relation entry (@p i, @p j). */
407 Entry get(const State &s, int i, int j) const {
408 return s.rel[i*MAXD + j];
409 }
410 /** @brief Whether an entry carries no information. */
411 static bool emptyEntry(Entry e) {
412 return !e;
413 }
414 /** @brief Merge a relation entry (domain re-expression). */
415 void merge(State &s, int i, int j, Entry /*e*/) const {
416 s.rel.set(i*MAXD + j);
417 }
418 /** @brief Seed the bits of the domain's set vertices. */
419 void seed(State &s, const std::vector<unsigned long> &domain) const {
420 for (std::size_t i = 0; i < domain.size(); ++i)
421 if (target_set->count(domain[i]))
422 s.dvec.set(static_cast<int>(i));
423 }
424 /** @brief Carry surviving positions' bits through a re-expression. */
425 void liftExtra(const State &from, State &to,
426 const std::vector<int> &map, const LiftContext &) const {
427 for (std::size_t i = 0; i < map.size(); ++i)
428 if (map[i] >= 0 && from.dvec[static_cast<int>(i)])
429 to.dvec.set(map[i]);
430 }
431 /** @brief Post-closure canonicalisation hook (a coverage concept). */
432 void normalize(State &, int, int) const {
433 }
434};
435
436/**
437 * @brief Coverage (k-terminal) state algebra: plain reachability plus
438 * the *pending rescuer-set antichain* -- the Courcelle
439 * congruence for @f$\forall y\, S(y) \rightarrow
440 * \mathrm{reach}(x, y)@f$.
441 *
442 * When a target-set vertex @c v is forgotten, either the source
443 * already reaches it (nothing recorded) or its fate now depends only
444 * on the boundary: @c v ends up reachable iff the source eventually
445 * reaches one of its *rescuers*, the domain positions that reach
446 * @c v within the processed part. The state therefore carries, next
447 * to the closed relation, the family of pending rescuer sets:
448 *
449 * - kept *closed* under the relation (anything reaching a rescuer is
450 * one; the relation being transitively closed, one backward pass
451 * per closure suffices), which makes the forget step a plain
452 * intersection with the surviving positions -- lossless by
453 * transitivity;
454 * - *discharged* (dropped) as soon as a set acquires the source's
455 * position: the vertex is reached, in every world of this state;
456 * - the *empty* set is the absorbing reject -- a vertex none of whose
457 * rescuers survived can never be reached -- and absorbs the whole
458 * antichain (it is a subset of every set);
459 * - reduced to the *antichain of minimal sets*: hitting a subset
460 * implies hitting its supersets, so only minimal sets constrain.
461 *
462 * Acceptance, after a final re-expression onto the singleton
463 * @c {source} domain (which forgets -- and thereby resolves -- every
464 * remaining target vertex): no pending set. Worlds still map to
465 * exactly one state, so the emitted ORs stay deterministic and the
466 * d-D certificate carries over; the antichain is bounded by the
467 * domain size alone (at most @f$\binom{d}{\lfloor d/2\rfloor}@f$
468 * sets), so the state space stays a function of the treewidth, with
469 * the usual @c max_states guard.
470 */
471struct CoverOps {
472 static constexpr bool tracks_lengths = false;
473 static constexpr bool has_target_set = true;
474 static constexpr bool final_collapse = true;
475 using Entry = bool;
476 using Mask = std::uint32_t;
477 static_assert(MAXD <= 32, "rescuer sets are 32-bit masks");
478 /** @brief Closed relation plus the sorted antichain of pending rescuer sets. */
479 struct State {
480 std::bitset<MAXD*MAXD> rel; ///< Reachability relation (as @c BoolOps).
481 std::vector<Mask> pending; ///< Minimal rescuer sets, sorted (canonical).
482 bool operator==(const State &o) const {
483 return rel == o.rel && pending == o.pending;
484 }
485 };
486 /** @brief Hash over both components. */
487 struct Hash {
488 std::size_t operator()(const State &s) const noexcept {
489 std::size_t h = std::hash<std::bitset<MAXD*MAXD> >()(s.rel);
490 for (Mask m : s.pending)
491 h = h * 1099511628211ull ^ m;
492 return h;
493 }
494 };
495
496 const std::unordered_set<unsigned long> *target_set; ///< The vertex set S.
497
498 /** @brief Identity relation, nothing pending. */
499 State identity(int d) const {
500 State s;
501 for (int i = 0; i < d; ++i)
502 s.rel.set(i*MAXD + i);
503 return s;
504 }
505 /** @brief Warshall closure (the pending sets are updated by @c normalize()). */
506 void close(State &s, int d) const {
507 for (int k = 0; k < d; ++k)
508 for (int i = 0; i < d; ++i)
509 if (s.rel[i*MAXD + k])
510 for (int j = 0; j < d; ++j)
511 if (s.rel[k*MAXD + j])
512 s.rel.set(i*MAXD + j);
513 }
514 /** @brief Entrywise union; pending sets concatenate (normalised next). */
515 void unite(State &a, const State &b) const {
516 a.rel |= b.rel;
517 a.pending.insert(a.pending.end(), b.pending.begin(), b.pending.end());
518 }
519 /** @brief Record the arc @p pu -> @p pv. */
520 void addArc(State &s, int pu, int pv, unsigned /*weight*/) const {
521 s.rel.set(pu*MAXD + pv);
522 }
523 /** @brief Read relation entry (@p i, @p j). */
524 Entry get(const State &s, int i, int j) const {
525 return s.rel[i*MAXD + j];
526 }
527 /** @brief Whether an entry carries no information. */
528 static bool emptyEntry(Entry e) {
529 return !e;
530 }
531 /** @brief Merge a relation entry (domain re-expression). */
532 void merge(State &s, int i, int j, Entry /*e*/) const {
533 s.rel.set(i*MAXD + j);
534 }
535 /** @brief No seeding: target vertices are resolved when forgotten. */
536 void seed(State &, const std::vector<unsigned long> &) const {
537 }
538 /** @brief Re-express the pending sets; resolve forgotten target vertices. */
539 void liftExtra(const State &from, State &to,
540 const std::vector<int> &map, const LiftContext &ctx) const {
541 // Surviving rescuers keep their sets alive (an emptied set is the
542 // absorbing reject; normalize() collapses around it).
543 for (Mask m : from.pending) {
544 Mask r = 0;
545 for (std::size_t i = 0; i < map.size(); ++i)
546 if (map[i] >= 0 && (m & (Mask{1} << i)))
547 r |= Mask{1} << map[i];
548 to.pending.push_back(r);
549 }
550 // A forgotten target vertex is either already reached by the
551 // source, or pends on the surviving positions that reach it.
552 for (std::size_t i = 0; i < map.size(); ++i) {
553 if (map[i] >= 0 || !target_set->count(ctx.from_domain[i]))
554 continue;
555 if (from.rel[static_cast<std::size_t>(ctx.from_ps)*MAXD + i])
556 continue; // discharged
557 Mask r = 0;
558 for (std::size_t u = 0; u < map.size(); ++u)
559 if (map[u] >= 0 && from.rel[u*MAXD + i])
560 r |= Mask{1} << map[u];
561 to.pending.push_back(r);
562 }
563 }
564 /**
565 * @brief Canonicalise after a closure: re-close the pending sets
566 * under the relation, discharge the source-reached ones, and
567 * reduce to the sorted minimal antichain.
568 */
569 void normalize(State &s, int d, int ps) const {
570 if (s.pending.empty())
571 return;
572 for (Mask &m : s.pending) {
573 // One backward pass (rel is transitively closed).
574 Mask grown = m;
575 for (int u = 0; u < d; ++u) {
576 if (grown & (Mask{1} << u))
577 continue;
578 for (int w = 0; w < d; ++w)
579 if ((m & (Mask{1} << w)) && s.rel[static_cast<std::size_t>(u)*MAXD + w]) {
580 grown |= Mask{1} << u;
581 break;
582 }
583 }
584 m = grown;
585 }
586 // Discharge, then keep the sorted minimal antichain (the empty set,
587 // the absorbing reject, is minimal and absorbs everything).
588 const Mask ps_bit = Mask{1} << ps;
589 std::vector<Mask> keep;
590 keep.reserve(s.pending.size());
591 for (Mask m : s.pending)
592 if (!(m & ps_bit))
593 keep.push_back(m);
594 std::sort(keep.begin(), keep.end());
595 keep.erase(std::unique(keep.begin(), keep.end()), keep.end());
596 s.pending.clear();
597 for (Mask m : keep) {
598 bool dominated = false;
599 for (Mask k : s.pending)
600 if ((k & m) == k) { // an already-kept subset constrains more
601 dominated = true;
602 break;
603 }
604 if (!dominated)
605 s.pending.push_back(m);
606 }
607 }
608};
609
610// ---------------------------------------------------------------------
611// The generic DP.
612// ---------------------------------------------------------------------
613
614/**
615 * @brief Run the decomposition-aligned DP and report the per-vertex reads.
616 *
617 * Shared scaffold of @c compileAll() / @c compileAllHops(): groups the
618 * rows into edge variables and BID blocks, decomposes the data graph,
619 * makes the bottom-up and top-down sweeps, and at every vertex's
620 * elimination bag calls @p onRead once per accepting (below, above)
621 * state pair with the pair's combined relation entry (source row,
622 * vertex column) and the pair's AND gate. The pairs partition the
623 * worlds, so any OR the caller builds over a fixed predicate of the
624 * reported entries is deterministic.
625 *
626 * @param rows Edge tuples.
627 * @param source Source vertex (ignored in multi-source mode).
628 * @param directed If @c false, every edge contributes both arcs.
629 * @param max_states Bound on the DP state count per node.
630 * @param only_target When set: ensure the vertex exists in the graph
631 * (an isolated target is legal) and read it alone.
632 * @param multi_sources When set: virtual super-source mode.
633 * @param ops The state algebra.
634 * @param dd Output circuit (gates are emitted into it).
635 * @param stats Output statistics.
636 * @param onRead Read callback: @c (vertex, entry, pair_gate).
637 * @return The constant-true gate (the empty AND).
638 */
639template<class Ops, class ReadSink>
640gate_t runReachabilityDP(const std::vector<ReachabilityCompiler::EdgeRow> &rows,
641 unsigned long source,
642 bool directed,
643 std::size_t max_states,
644 const unsigned long *only_target,
645 const std::vector<ReachabilityCompiler::SourceArc> *multi_sources,
646 const Ops &ops,
647 dDNNF &dd,
649 ReadSink onRead,
650 /* When set, acceptance is a predicate of the root's
651 * below-table alone: the sink is called once per
652 * root state with (state, gate, source position) and
653 * the whole top-down sweep is skipped. */
654 const std::function<void(const typename Ops::State &,
655 gate_t, int)> *root_sink
656 = nullptr,
657 /* Multi-set mode (Ops with a target_set member,
658 * i.e. SetReachOps): the prelude -- variable
659 * grouping, tree decomposition, bag assignments,
660 * literal gates -- is built once, then one
661 * bottom-up sweep runs per target set, with
662 * content-deduplicated (hash-consed) gate
663 * emission so the parts of the circuit a set's
664 * seeds do not touch come out as the *same*
665 * gates across sets. The sink receives
666 * (set index, root state, gate, source position);
667 * the top-down sweep is skipped. */
668 const std::vector<std::unordered_set<unsigned long> >
669 *multi_sets = nullptr,
670 const std::function<void(std::size_t,
671 const typename Ops::State &,
672 gate_t, int)> *multi_sink
673 = nullptr)
674{
675 using State = typename Ops::State;
676 using Table = std::unordered_map<State, gate_t, typename Ops::Hash>;
677 using Accumulator = std::unordered_map<State, std::vector<gate_t>,
678 typename Ops::Hash>;
679
680 /* The state algebra the sweeps read through: re-pointed per set in
681 * multi-set mode (each sweep seeds a different target set), constant
682 * otherwise. */
683 const Ops *opsp = &ops;
684
685 // Multi-source mode: reachability is from a virtual super-source whose
686 // arcs to the given sources are ordinary (or certain) directed edge
687 // variables; everything downstream is the single-source DP. The
688 // super-source gets an ID above every real vertex.
689 unsigned long super_source = 0;
690 if (multi_sources) {
691 if (multi_sources->empty())
692 throw ReachabilityCompilerException("no sources given");
693 for (const auto &row : rows)
694 super_source = std::max({super_source, row.src, row.dst});
695 for (const auto &sa : *multi_sources)
696 super_source = std::max(super_source, sa.vertex);
697 ++super_source;
698 source = super_source;
699 }
700
701 // ------------------------------------------------------------------
702 // 1. Group rows into edge variables (one per provenance token).
703 //
704 // A token shared by two mutual-reverse rows is the natural encoding
705 // of an undirected edge in a directed edge relation and becomes one
706 // bidirectional variable; any other sharing would break the
707 // independence (and decomposability) assumptions, so it is rejected.
708 // Self-loops never affect plain reachability and are dropped there,
709 // but they *do* pump walk lengths -- the recursive fixpoint derives
710 // (v, h+1) from a self-loop at v -- so the hop mode keeps them as
711 // ordinary weight-1 arcs.
712 // ------------------------------------------------------------------
713 std::vector<EdgeVariable> variables;
714 std::vector<EdgeBlock> blocks;
715 {
716 // BID blocks first: rows carrying a block key are mutually exclusive
717 // alternatives of one (k+1)-way variable; their tokens must not be
718 // shared with anything else, and a pure self-loop alternative keeps
719 // its probability mass but contributes no arc.
720 std::unordered_map<std::string, std::size_t> key_to_block;
721 std::unordered_set<std::string> block_tokens;
722 for (const auto &row : rows) {
723 if (row.block_key.empty())
724 continue;
725 auto [it, fresh] = key_to_block.try_emplace(row.block_key, blocks.size());
726 if (fresh) {
727 EdgeBlock blk;
728 blk.key = row.block_key;
729 blocks.push_back(std::move(blk));
730 }
731 EdgeBlock &blk = blocks[it->second];
732 if (!block_tokens.insert(row.token).second)
734 "provenance token " + row.token +
735 " appears on several block alternatives");
736 BlockAlternative alt;
737 alt.u = row.src;
738 alt.v = row.dst;
739 if (row.src != row.dst) {
740 alt.arc_uv = true;
741 alt.arc_vu = !directed;
742 } else {
743 // A self-loop alternative is a pure probability-mass outcome for
744 // plain reachability, but a length-pumping arc in hop mode.
745 alt.arc_uv = Ops::tracks_lengths;
746 alt.arc_vu = false;
747 }
748 alt.token = row.token;
749 alt.prob = row.prob;
750 alt.index = row.block_index;
751 if (alt.arc_uv || alt.arc_vu) {
752 for (unsigned long e : {row.src, row.dst})
753 if (std::find(blk.endpoints.begin(), blk.endpoints.end(), e) ==
754 blk.endpoints.end())
755 blk.endpoints.push_back(e);
756 }
757 blk.alts.push_back(std::move(alt));
758 }
759
760 std::unordered_map<std::string, std::size_t> token_to_var;
761 for (const auto &row : rows) {
762 if (!row.block_key.empty())
763 continue; // handled above
764 if (row.src == row.dst && !Ops::tracks_lengths)
765 continue; // self-loop, irrelevant to plain reachability
766 if (block_tokens.find(row.token) != block_tokens.end())
768 "provenance token " + row.token +
769 " is shared between a block alternative and an edge");
770
771 auto it = token_to_var.find(row.token);
772 if (it == token_to_var.end()) {
773 EdgeVariable var;
774 var.u = row.src;
775 var.v = row.dst;
776 var.arc_uv = true;
777 var.arc_vu = !directed && row.src != row.dst;
778 var.token = row.token;
779 var.prob = row.prob;
780 token_to_var[row.token] = variables.size();
781 variables.push_back(var);
782 } else {
783 EdgeVariable &var = variables[it->second];
784 if (row.src == var.u && row.dst == var.v) {
785 // duplicate arc, idempotent
786 } else if (row.src == var.v && row.dst == var.u) {
787 var.arc_uv = var.arc_vu = true; // mutual-reverse pair
788 } else {
790 "provenance token " + row.token +
791 " is shared by edges with different endpoints");
792 }
793 }
794 }
795 }
796 if (multi_sources) {
797 // Source arcs: super-source -> vertex, gated by the source tuple's
798 // token (one variable per token; a duplicate token must target the
799 // same vertex) or always present for certain sources (dedup'd). A
800 // token sharing with an *edge* variable would couple the source to
801 // an edge and break decomposability: rejected. Source arcs carry
802 // walk length zero: the reported lengths count graph edges only.
803 std::unordered_set<std::string> edge_tokens;
804 for (const auto &var : variables)
805 if (!var.certain)
806 edge_tokens.insert(var.token);
807 std::unordered_map<std::string, std::size_t> token_to_var;
808 std::unordered_set<unsigned long> certain_done;
809 for (const auto &sa : *multi_sources) {
810 if (sa.certain) {
811 if (!certain_done.insert(sa.vertex).second)
812 continue;
813 EdgeVariable var;
814 var.u = super_source;
815 var.v = sa.vertex;
816 var.arc_uv = true;
817 var.arc_vu = false;
818 var.certain = true;
819 var.weight = 0;
820 variables.push_back(var);
821 } else {
822 if (edge_tokens.find(sa.token) != edge_tokens.end())
824 "provenance token " + sa.token +
825 " is shared between a source and an edge");
826 auto it = token_to_var.find(sa.token);
827 if (it != token_to_var.end()) {
828 if (variables[it->second].v != sa.vertex)
830 "provenance token " + sa.token +
831 " is shared by sources with different vertices");
832 continue;
833 }
834 EdgeVariable var;
835 var.u = super_source;
836 var.v = sa.vertex;
837 var.arc_uv = true;
838 var.arc_vu = false;
839 var.weight = 0;
840 var.token = sa.token;
841 var.prob = sa.prob;
842 token_to_var[sa.token] = variables.size();
843 variables.push_back(var);
844 }
845 }
846 }
847 for (const auto &var : variables)
848 if (!var.certain)
849 ++stats.nb_variables;
850 stats.nb_variables += blocks.size();
851
852 // ------------------------------------------------------------------
853 // 2. Tree decomposition of the data graph (vertices: all endpoints
854 // plus the source, plus an explicitly requested target so an
855 // isolated target is legal), by min-fill elimination.
856 // ------------------------------------------------------------------
857 Graph graph;
858 graph.add_node(source);
859 if (only_target)
860 graph.add_node(*only_target);
861 for (const auto &var : variables) {
862 if (var.u != var.v)
863 graph.add_edge(var.u, var.v);
864 else
865 graph.add_node(var.u); // hop-mode self-loop: no Gaifman edge
866 }
867 for (const auto &blk : blocks) {
868 // All endpoints of a block must share a bag (the whole block is
869 // introduced at once): force it with a clique. This is the honest
870 // treewidth condition for BID data -- a block spanning many distant
871 // vertices genuinely raises the width.
872 for (std::size_t i = 0; i < blk.endpoints.size(); ++i)
873 for (std::size_t j = i+1; j < blk.endpoints.size(); ++j)
874 graph.add_edge(blk.endpoints[i], blk.endpoints[j]);
875 if (blk.endpoints.size() == 1)
876 graph.add_node(blk.endpoints[0]);
877 }
878
879 /* No degeneracy pre-probe here, deliberately: it was implemented and
880 * measured (TreeDecomposition::degeneracyLowerBound now accepts a
881 * Graph for that purpose), but min-fill's own abort -- the first
882 * elimination whose neighbourhood exceeds the cap -- rejects every
883 * adversarial family tried (cliques, supercritical random graphs) at
884 * least as fast as the O(V+E) peel, while an always-on probe would tax
885 * every *accepted* compilation by a linear pass. */
886 std::unordered_map<unsigned long, bag_t> elimination_bag;
887 const TreeDecomposition td(std::move(graph), &elimination_bag);
888 stats.data_treewidth = td.getTreewidth();
889 stats.nb_bags = td.getNbBags();
890 const std::size_t nb_bags = td.getNbBags();
891
892 // Each variable is introduced at exactly one node: the bag created
893 // when the earlier-eliminated endpoint was eliminated contains both
894 // endpoints (elimination invariant), and a unique introduction point
895 // is what makes the emitted AND gates decomposable.
896 std::vector<std::vector<std::size_t> > variables_at_bag(nb_bags);
897 for (std::size_t i = 0; i < variables.size(); ++i) {
898 bag_t bu = elimination_bag.at(variables[i].u);
899 bag_t bv = elimination_bag.at(variables[i].v);
900 bag_t b = bag_index(bu) < bag_index(bv) ? bu : bv;
901 variables_at_bag[bag_index(b)].push_back(i);
902 }
903 // A block is introduced at the bag of its earliest-eliminated endpoint,
904 // which (by the clique above and the elimination invariant) contains
905 // every endpoint of the block. A block with no real arc is irrelevant
906 // to reachability and is skipped entirely.
907 std::vector<std::vector<std::size_t> > blocks_at_bag(nb_bags);
908 for (std::size_t i = 0; i < blocks.size(); ++i) {
909 if (blocks[i].endpoints.empty())
910 continue;
911 bag_t best = elimination_bag.at(blocks[i].endpoints[0]);
912 for (unsigned long e : blocks[i].endpoints) {
913 bag_t be = elimination_bag.at(e);
914 if (bag_index(be) < bag_index(best))
915 best = be;
916 }
917 blocks_at_bag[bag_index(best)].push_back(i);
918 }
919
920 // Read points: every vertex is read at its elimination bag (which
921 // contains it); a single-target compilation reads only that vertex.
922 std::vector<std::vector<unsigned long> > reads_at_bag(nb_bags);
923 for (const auto &[v, b] : elimination_bag) {
924 if (multi_sources && v == super_source)
925 continue; // the virtual super-source is not a user vertex
926 if (!only_target || v == *only_target)
927 reads_at_bag[bag_index(b)].push_back(v);
928 }
929
930 // ------------------------------------------------------------------
931 // 3. Gate-emission helpers.
932 //
933 // Every emitted OR is deterministic and every emitted AND decomposable
934 // *by construction*; mark them with the d-D certificate so the
935 // certificate-aware consumers (independentEvaluation, interpretAsDD)
936 // can evaluate the circuit linearly.
937 // ------------------------------------------------------------------
938 const gate_t invalid_gate{static_cast<std::underlying_type<gate_t>::type>(-1)};
939 const gate_t true_gate = dd.setGate(BooleanGate::AND); // empty AND = true
940 dd.setInfo(true_gate, DNNF_CERT_INFO);
941
942 std::vector<gate_t> var_in(variables.size(), invalid_gate);
943 std::vector<gate_t> var_not(variables.size(), invalid_gate);
944 auto inGate = [&](std::size_t i) {
945 if (var_in[i] == invalid_gate)
946 var_in[i] = dd.setGate(variables[i].token, BooleanGate::IN,
947 variables[i].prob);
948 return var_in[i];
949 };
950 auto notGate = [&](std::size_t i) {
951 if (var_not[i] == invalid_gate) {
952 var_not[i] = dd.setGate(BooleanGate::NOT);
953 dd.addWire(var_not[i], inGate(i));
954 }
955 return var_not[i];
956 };
957 std::vector<gate_t> block_mulvar(blocks.size(), invalid_gate);
958 std::vector<std::vector<gate_t> > block_mulin(blocks.size());
959 std::vector<gate_t> block_none(blocks.size(), invalid_gate);
960 auto mulinGate = [&](std::size_t bi, std::size_t ai) {
961 if (block_mulin[bi].empty())
962 block_mulin[bi].assign(blocks[bi].alts.size(),
963 invalid_gate);
964 if (block_mulin[bi][ai] == invalid_gate) {
965 if (block_mulvar[bi] == invalid_gate)
966 block_mulvar[bi] =
967 dd.setGate(blocks[bi].key, BooleanGate::MULVAR);
968 const auto &alt = blocks[bi].alts[ai];
969 gate_t m = dd.setGate(alt.token, BooleanGate::MULIN,
970 alt.prob);
971 dd.setInfo(m, alt.index);
972 dd.addWire(m, block_mulvar[bi]);
973 block_mulin[bi][ai] = m;
974 }
975 return block_mulin[bi][ai];
976 };
977 auto noneGate = [&](std::size_t bi) {
978 if (block_none[bi] == invalid_gate) {
979 // "No alternative drawn": NOT over the (deterministic:
980 // the alternatives are mutually exclusive) OR of the
981 // block's mulinput literals; probability 1 - sum p_i.
983 dd.setInfo(o, DNNF_CERT_INFO);
984 for (std::size_t ai = 0; ai < blocks[bi].alts.size(); ++ai)
985 dd.addWire(o, mulinGate(bi, ai));
987 dd.addWire(n, o);
988 block_none[bi] = n;
989 }
990 return block_none[bi];
991 };
992 /* Content dedup (hash-consing) of the emitted AND / OR gates,
993 * enabled when several target sets share the circuit: per-set sweeps
994 * re-derive identical subcircuits everywhere their seeds make no
995 * difference, and consing makes those the *same* gate, so downstream
996 * consumers (notably the store materialisation, which walks every
997 * gate instance) pay for the shared structure once. Sound
998 * unconditionally: structural identity implies functional identity,
999 * and determinism / decomposability are properties of a gate's
1000 * children. The (dominant) binary ANDs are keyed on the packed
1001 * child-id pair; the ORs on the gate type plus the sorted child ids
1002 * as raw bytes. */
1003 const bool consing = multi_sets != nullptr && multi_sets->size() > 1;
1004 struct PairHash {
1005 std::size_t operator()(const std::pair<std::uint64_t,
1006 std::uint64_t> &p) const noexcept {
1007 // Mix the halves (splitmix-style) so consecutive ids spread.
1008 std::uint64_t x = p.first * 0x9e3779b97f4a7c15ull ^ p.second;
1009 x ^= x >> 30; x *= 0xbf58476d1ce4e5b9ull;
1010 x ^= x >> 27; x *= 0x94d049bb133111ebull;
1011 return static_cast<std::size_t>(x ^ (x >> 31));
1012 }
1013 };
1014 std::unordered_map<std::pair<std::uint64_t, std::uint64_t>, gate_t,
1015 PairHash> cons_and;
1016 std::unordered_map<std::string, gate_t> cons_or;
1017 auto consKey = [](std::vector<gate_t> children) {
1018 std::sort(children.begin(), children.end());
1019 std::string key;
1020 key.reserve(children.size() * sizeof(gate_t));
1021 for (gate_t c : children)
1022 key.append(reinterpret_cast<const char *>(&c),
1023 sizeof(gate_t));
1024 return key;
1025 };
1026
1027 auto andGate = [&](gate_t a, gate_t b) {
1028 if (a == true_gate)
1029 return b;
1030 if (b == true_gate)
1031 return a;
1032 std::pair<std::uint64_t, std::uint64_t> key;
1033 if (consing) {
1034 const auto ai =
1035 static_cast<std::uint64_t>(
1036 static_cast<std::underlying_type<gate_t>::type>(a));
1037 const auto bi =
1038 static_cast<std::uint64_t>(
1039 static_cast<std::underlying_type<gate_t>::type>(b));
1040 key = std::minmax(ai, bi);
1041 auto it = cons_and.find(key);
1042 if (it != cons_and.end())
1043 return it->second;
1044 }
1046 dd.setInfo(g, DNNF_CERT_INFO);
1047 dd.addWire(g, a);
1048 dd.addWire(g, b);
1049 if (consing)
1050 cons_and.emplace(key, g);
1051 return g;
1052 };
1053
1054 // A DP table maps each reachable state over the node's domain to the
1055 // gate computing "this part's valuation induces exactly this state";
1056 // an accumulator collects the (mutually exclusive) contributions to
1057 // each state before they are OR-ed.
1058 auto finalize = [&](Accumulator &acc) {
1059 Table t;
1060 t.reserve(acc.size());
1061 for (auto &entry : acc) {
1062 if (entry.second.size() == 1)
1063 t.emplace(entry.first, entry.second[0]);
1064 else {
1065 // Deterministic OR: the contributions partition the
1066 // worlds inducing this state.
1067 std::string key;
1068 if (consing) {
1069 key = consKey(entry.second);
1070 auto it = cons_or.find(key);
1071 if (it != cons_or.end()) {
1072 t.emplace(entry.first, it->second);
1073 continue;
1074 }
1075 }
1077 dd.setInfo(g, DNNF_CERT_INFO);
1078 for (gate_t c : entry.second)
1079 dd.addWire(g, c);
1080 if (consing)
1081 cons_or.emplace(std::move(key), g);
1082 t.emplace(entry.first, g);
1083 }
1084 }
1085 acc.clear();
1086 stats.max_states = std::max(stats.max_states, t.size());
1087 if (t.size() > max_states)
1089 "state space exceeds the per-node bound (" +
1090 std::to_string(max_states) +
1091 "); the data treewidth is too large for "
1092 "reachability compilation");
1093 return t;
1094 };
1095
1096 // ------------------------------------------------------------------
1097 // 4. Domains. Every node's domain is its bag plus the source (the DP
1098 // runs on the decomposition with the source added to every bag,
1099 // still a valid tree decomposition).
1100 // ------------------------------------------------------------------
1101 std::vector<std::vector<unsigned long> > domains(nb_bags);
1102 for (std::size_t b = 0; b < nb_bags; ++b) {
1103 auto &d = domains[b];
1104 d.reserve(td.getBag(bag_t{b}).size()+1);
1105 for (gate_t g : td.getBag(bag_t{b}))
1106 d.push_back(static_cast<std::underlying_type<gate_t>::type>(g));
1107 d.push_back(source);
1108 std::sort(d.begin(), d.end());
1109 d.erase(std::unique(d.begin(), d.end()), d.end());
1110 }
1111
1112 auto positionIn = [](const std::vector<unsigned long> &domain, unsigned long v) {
1113 return static_cast<int>(
1114 std::lower_bound(domain.begin(), domain.end(), v) -
1115 domain.begin());
1116 };
1117 auto trivialState = [&](const std::vector<unsigned long> &domain) {
1118 auto s = opsp->identity(static_cast<int>(domain.size()));
1119 opsp->seed(s, domain);
1120 return s;
1121 };
1122 auto trivialTable = [&](const std::vector<unsigned long> &domain) {
1123 return Table{{trivialState(domain), true_gate}};
1124 };
1125 auto isTrivial = [&](const Table &t,
1126 const std::vector<unsigned long> &domain) {
1127 // A table is a join identity only if it is the single
1128 // always-true *trivial-state* entry (the identity
1129 // relation plus the domain's seeds): a certain arc
1130 // produces single-state TRUE tables whose state is
1131 // not trivial, and dropping those in join() would
1132 // lose the arc.
1133 return t.size() == 1 && t.begin()->second == true_gate &&
1134 t.begin()->first == trivialState(domain);
1135 };
1136
1137 // Re-express a table over another domain: forget the vertices that
1138 // leave (restriction of a closed state stays closed; any walk
1139 // through a forgotten vertex between surviving vertices was already
1140 // recorded by closure) and introduce the fresh ones with identity
1141 // only. States may collapse, hence the accumulator.
1142 auto lift = [&](const Table &t, const std::vector<unsigned long> &from,
1143 const std::vector<unsigned long> &to) {
1144 if (from == to)
1145 return t;
1146 const int df = static_cast<int>(from.size());
1147 const int dt = static_cast<int>(to.size());
1148 std::vector<int> map(from.size());
1149 for (int i = 0; i < df; ++i) {
1150 auto it = std::lower_bound(to.begin(), to.end(), from[i]);
1151 map[i] = (it != to.end() && *it == from[i])
1152 ? static_cast<int>(it - to.begin()) : -1;
1153 }
1154 State id = opsp->identity(dt);
1155 opsp->seed(id, to);
1156 const LiftContext ctx{from, positionIn(from, source)};
1157 const int to_ps = positionIn(to, source);
1158 Accumulator acc;
1159 for (const auto &entry : t) {
1160 State r = id;
1161 for (int i = 0; i < df; ++i) {
1162 if (map[i] < 0)
1163 continue;
1164 for (int j = 0; j < df; ++j) {
1165 if (map[j] < 0)
1166 continue;
1167 const auto e = opsp->get(entry.first, i, j);
1168 if (!Ops::emptyEntry(e))
1169 opsp->merge(r, map[i], map[j], e);
1170 }
1171 }
1172 opsp->liftExtra(entry.first, r, map, ctx);
1173 opsp->normalize(r, dt, to_ps);
1174 acc[r].push_back(entry.second);
1175 }
1176 return finalize(acc);
1177 };
1178
1179 // Join two tables over the same domain, covering disjoint edge sets:
1180 // pairs of states are mutually exclusive (deterministic ORs) and the
1181 // gates variable-disjoint (decomposable ANDs); walks across the two
1182 // parts only alternate through domain vertices (the bag separates
1183 // them), hence the closure of the union.
1184 auto join = [&](const Table &t1, const Table &t2,
1185 const std::vector<unsigned long> &domain) {
1186 const int d = static_cast<int>(domain.size());
1187 if (isTrivial(t1, domain))
1188 return t2;
1189 if (isTrivial(t2, domain))
1190 return t1;
1191 const int ps = positionIn(domain, source);
1192 Accumulator acc;
1193 for (const auto &left : t1)
1194 for (const auto &right : t2) {
1195 CHECK_FOR_INTERRUPTS();
1196 State r = left.first;
1197 opsp->unite(r, right.first);
1198 opsp->close(r, d);
1199 opsp->normalize(r, d, ps);
1200 acc[std::move(r)].push_back(andGate(left.second,
1201 right.second));
1202 }
1203 return finalize(acc);
1204 };
1205
1206 // Introduce the edge variables assigned to bag b into a table over
1207 // that bag's domain.
1208 auto applyEdges = [&](Table table, std::size_t b) {
1209 const auto &domain = domains[b];
1210 const int d = static_cast<int>(domain.size());
1211 const int ps = positionIn(domain, source);
1212 for (std::size_t vi : variables_at_bag[b]) {
1213 const EdgeVariable &var = variables[vi];
1214 const int pu = positionIn(domain, var.u);
1215 const int pv = positionIn(domain, var.v);
1216
1217 Accumulator acc;
1218 for (const auto &entry : table) {
1219 State present = entry.first;
1220 if (var.arc_uv)
1221 opsp->addArc(present, pu, pv, var.weight);
1222 if (var.arc_vu)
1223 opsp->addArc(present, pv, pu, var.weight);
1224 opsp->close(present, d);
1225 opsp->normalize(present, d, ps);
1226
1227 if (var.certain) {
1228 // Always-present arc: every world of this state
1229 // moves to the augmented state, no branching
1230 // (states may merge; their gates stay mutually
1231 // exclusive).
1232 acc[present].push_back(entry.second);
1233 continue;
1234 }
1235 if (present == entry.first) {
1236 // The edge cannot change the state in these
1237 // worlds: its value is irrelevant, keep the gate
1238 // as is (the OR of the two cofactors would
1239 // simplify to it anyway).
1240 acc[entry.first].push_back(entry.second);
1241 } else {
1242 acc[present].push_back(
1243 andGate(entry.second, inGate(vi)));
1244 acc[entry.first].push_back(
1245 andGate(entry.second, notGate(vi)));
1246 }
1247 }
1248 table = finalize(acc);
1249 }
1250 for (std::size_t bi : blocks_at_bag[b]) {
1251 const EdgeBlock &blk = blocks[bi];
1252 Accumulator acc;
1253 for (const auto &entry : table) {
1254 // (k+1)-way deterministic branching: one outcome
1255 // per alternative (its arcs applied, gated by its
1256 // mulinput literal) plus the none outcome. If no
1257 // outcome can change the state, the block is
1258 // irrelevant for these worlds.
1259 std::vector<State> outs(blk.alts.size());
1260 bool all_same = true;
1261 for (std::size_t ai = 0; ai < blk.alts.size(); ++ai) {
1262 State present = entry.first;
1263 const auto &alt = blk.alts[ai];
1264 if (alt.arc_uv || alt.arc_vu) {
1265 const int pu = positionIn(domain, alt.u);
1266 const int pv = positionIn(domain, alt.v);
1267 if (alt.arc_uv)
1268 opsp->addArc(present, pu, pv, 1);
1269 if (alt.arc_vu)
1270 opsp->addArc(present, pv, pu, 1);
1271 opsp->close(present, d);
1272 opsp->normalize(present, d, ps);
1273 }
1274 outs[ai] = present;
1275 if (!(present == entry.first))
1276 all_same = false;
1277 }
1278 if (all_same) {
1279 acc[entry.first].push_back(entry.second);
1280 continue;
1281 }
1282 acc[entry.first].push_back(
1283 andGate(entry.second, noneGate(bi)));
1284 for (std::size_t ai = 0; ai < blk.alts.size(); ++ai)
1285 acc[outs[ai]].push_back(
1286 andGate(entry.second, mulinGate(bi, ai)));
1287 }
1288 table = finalize(acc);
1289 }
1290 return table;
1291 };
1292
1293 // ------------------------------------------------------------------
1294 // 5. Bottom-up sweep: below[b] = state table of bag b's subtree
1295 // (children joined, local edges applied), retained for the
1296 // top-down sweep and the reads.
1297 // ------------------------------------------------------------------
1298 auto runBottomUp = [&]() {
1299 std::vector<Table> below(nb_bags);
1300 struct Frame {
1301 bag_t bag;
1302 std::size_t next_child = 0;
1303 Table table;
1304 bool has_table = false;
1305 explicit Frame(bag_t b) : bag(b) {
1306 }
1307 };
1308
1309 std::vector<Frame> stack;
1310 stack.push_back(Frame(td.getRoot()));
1311
1312 while (!stack.empty()) {
1313 Frame &frame = stack.back();
1314 const auto &children = td.getChildren(frame.bag);
1315
1316 if (frame.next_child < children.size()) {
1317 bag_t c = children[frame.next_child++];
1318 stack.push_back(Frame(c));
1319 continue;
1320 }
1321
1322 CHECK_FOR_INTERRUPTS();
1323
1324 const std::size_t b = bag_index(frame.bag);
1325 Table table = frame.has_table
1326 ? std::move(frame.table)
1327 : trivialTable(domains[b]);
1328 below[b] = applyEdges(std::move(table), b);
1329
1330 if (stack.size() == 1) {
1331 stack.pop_back();
1332 break;
1333 }
1334
1335 // Merge into the parent's partial join.
1336 Frame &parent = stack[stack.size()-2];
1337 const std::size_t pb = bag_index(parent.bag);
1338 Table lifted = lift(below[b], domains[b],
1339 domains[pb]);
1340 if (!parent.has_table) {
1341 parent.table = std::move(lifted);
1342 parent.has_table = true;
1343 } else {
1344 parent.table = join(parent.table, lifted,
1345 domains[pb]);
1346 }
1347 stack.pop_back();
1348 }
1349 return below;
1350 };
1351
1352 // ------------------------------------------------------------------
1353 // 5m. Multi-set mode: one bottom-up sweep per target set over the
1354 // shared prelude, the per-set acceptance read off the root table
1355 // (as in 5b); the consed emission makes the seed-independent
1356 // parts of the per-set circuits literally shared.
1357 // ------------------------------------------------------------------
1358 if (multi_sets) {
1359 if constexpr (Ops::has_target_set) {
1360 const std::size_t rb = bag_index(td.getRoot());
1361 const int ps = positionIn(domains[rb], source);
1362 const std::vector<unsigned long> source_domain{source};
1363 Ops per_set_ops = ops;
1364 opsp = &per_set_ops;
1365 for (std::size_t si = 0; si < multi_sets->size(); ++si) {
1366 CHECK_FOR_INTERRUPTS();
1367 per_set_ops.target_set = &(*multi_sets)[si];
1368 const std::vector<Table> below = runBottomUp();
1369 if constexpr (Ops::final_collapse) {
1370 // Re-express the root table onto the singleton source domain:
1371 // forgetting every remaining vertex resolves the in-domain
1372 // target vertices, so acceptance is a predicate of the
1373 // collapsed state alone.
1374 const Table collapsed =
1375 lift(below[rb], domains[rb], source_domain);
1376 for (const auto &[R, g] : collapsed)
1377 (*multi_sink)(si, R, g, 0);
1378 } else {
1379 for (const auto &[R, g] : below[rb])
1380 (*multi_sink)(si, R, g, ps);
1381 }
1382 }
1383 stats.nb_gates = dd.getNbGates();
1384 return true_gate;
1385 } else {
1387 "multi-set mode requires a set-seeded state algebra");
1388 }
1389 }
1390
1391 const std::vector<Table> below = runBottomUp();
1392
1393 // ------------------------------------------------------------------
1394 // 5b. Root-only acceptance: when the caller's predicate is a function
1395 // of the root's below-table alone (set reachability reads one bit
1396 // at the source's position), report each root state and skip the
1397 // whole top-down sweep.
1398 // ------------------------------------------------------------------
1399 if (root_sink) {
1400 const std::size_t rb = bag_index(td.getRoot());
1401 const int ps = positionIn(domains[rb], source);
1402 for (const auto &[R, g] : below[rb])
1403 (*root_sink)(R, g, ps);
1404 stats.nb_gates = dd.getNbGates();
1405 return true_gate;
1406 }
1407
1408 // ------------------------------------------------------------------
1409 // 6. Top-down sweep and reads. above[b] covers exactly the edges
1410 // introduced outside bag b's subtree; for a child c of b,
1411 // above(c) = lift( applyEdges_b( above(b) ⊗ siblings' below ) ).
1412 // Reads at b pair below[b] with above[b]: the closure of the union
1413 // is the full-graph state over b's domain.
1414 // ------------------------------------------------------------------
1415 {
1416 std::vector<Table> above(nb_bags);
1417 const std::size_t rb = bag_index(td.getRoot());
1418 above[rb] = trivialTable(domains[rb]);
1419
1420 std::vector<bag_t> stack{td.getRoot()};
1421 while (!stack.empty()) {
1422 const bag_t nu = stack.back();
1423 stack.pop_back();
1424 CHECK_FOR_INTERRUPTS();
1425 const std::size_t b = bag_index(nu);
1426 const int d = static_cast<int>(domains[b].size());
1427
1428 // Reads: for every (below, above) state pair, the closure of the
1429 // union; the pairs partition the worlds, so any OR the sink
1430 // builds over a fixed predicate of the entries is deterministic.
1431 if (!reads_at_bag[b].empty()) {
1432 const int ps = positionIn(domains[b], source);
1433 for (const auto &[R, g] : below[b])
1434 for (const auto &[A, h] : above[b]) {
1435 CHECK_FOR_INTERRUPTS();
1436 State closed = R;
1437 opsp->unite(closed, A);
1438 opsp->close(closed, d);
1439 gate_t pair_gate = invalid_gate; // lazily created, shared
1440 for (unsigned long v : reads_at_bag[b]) {
1441 const auto e = opsp->get(closed, ps, positionIn(domains[b], v));
1442 if (Ops::emptyEntry(e))
1443 continue;
1444 if (pair_gate == invalid_gate)
1445 pair_gate = andGate(g, h);
1446 onRead(v, e, pair_gate);
1447 }
1448 }
1449 }
1450
1451 // Children: prefix/suffix joins of the lifted sibling tables keep
1452 // this linear in the arity.
1453 const auto &children = td.getChildren(nu);
1454 const std::size_t m = children.size();
1455 if (m > 0) {
1456 std::vector<Table> lifted(m);
1457 for (std::size_t i = 0; i < m; ++i)
1458 lifted[i] = lift(below[bag_index(children[i])],
1459 domains[bag_index(children[i])], domains[b]);
1460
1461 std::vector<Table> prefix(m+1), suffix(m+1);
1462 prefix[0] = trivialTable(domains[b]);
1463 for (std::size_t i = 0; i < m; ++i)
1464 prefix[i+1] = join(prefix[i], lifted[i], domains[b]);
1465 suffix[m] = trivialTable(domains[b]);
1466 for (std::size_t i = m; i-- > 0; )
1467 suffix[i] = join(lifted[i], suffix[i+1], domains[b]);
1468
1469 for (std::size_t i = 0; i < m; ++i) {
1470 Table siblings = join(prefix[i], suffix[i+1], domains[b]);
1471 Table a = applyEdges(join(above[b], siblings, domains[b]), b);
1472 const std::size_t cb = bag_index(children[i]);
1473 above[cb] = lift(a, domains[b], domains[cb]);
1474 stack.push_back(children[i]);
1475 }
1476 }
1477
1478 // above[b] is no longer needed (children got theirs, reads done).
1479 above[b] = Table();
1480 }
1481 }
1482
1483 stats.nb_gates = dd.getNbGates();
1484 return true_gate;
1485}
1486
1487/**
1488 * @brief Build a deterministic OR over @p gates (or pass a single gate
1489 * through), with the d-D certificate.
1490 */
1491gate_t finalizeRoot(dDNNF &dd, const std::vector<gate_t> &gates)
1492{
1493 if (gates.size() == 1)
1494 return gates[0];
1496 dd.setInfo(o, DNNF_CERT_INFO);
1497 for (gate_t g : gates)
1498 dd.addWire(o, g);
1499 return o;
1500}
1501
1502/**
1503 * @brief Shared implementation of the two @c compileAllHops() overloads.
1504 */
1505ReachabilityCompiler::AllHopsResult compileAllHopsInternal(
1506 const std::vector<ReachabilityCompiler::EdgeRow> &rows,
1507 unsigned long source,
1508 bool directed,
1509 unsigned hop_bound,
1510 std::size_t max_states,
1511 const std::vector<ReachabilityCompiler::SourceArc> *multi_sources)
1512{
1515 "hop bound exceeds the supported maximum (" +
1516 std::to_string(ReachabilityCompiler::MAX_HOP_BOUND) + ")");
1517
1519 dDNNF &dd = result.dd;
1520 const HopOps ops(hop_bound);
1521
1522 // Per-(vertex, length) and per-vertex accumulators; the pairs reported
1523 // by the DP partition the worlds, so each OR built below is
1524 // deterministic. Ordered maps make the output order deterministic.
1525 std::map<std::pair<unsigned long, unsigned>, std::vector<gate_t> > hop_acc;
1526 std::map<unsigned long, std::vector<gate_t> > within_acc;
1527
1528 const gate_t true_gate = runReachabilityDP(
1529 rows, source, directed, max_states, nullptr, multi_sources, ops, dd,
1530 result.stats,
1531 [&](unsigned long v, HopOps::Entry mask, gate_t pair_gate) {
1532 within_acc[v].push_back(pair_gate);
1533 while (mask) {
1534 const unsigned h = static_cast<unsigned>(__builtin_ctzll(mask));
1535 mask &= mask - 1;
1536 hop_acc[{v, h}].push_back(pair_gate);
1537 }
1538 });
1539
1540 result.roots.reserve(hop_acc.size());
1541 for (const auto &[key, gates] : hop_acc)
1542 result.roots.push_back(
1543 ReachabilityCompiler::VertexHopRoot{key.first, key.second,
1544 finalizeRoot(dd, gates)});
1545 result.within_roots.reserve(within_acc.size());
1546 for (const auto &[v, gates] : within_acc)
1547 result.within_roots.push_back(
1548 ReachabilityCompiler::VertexRoot{v, finalizeRoot(dd, gates)});
1549
1550 dd.setRoot(true_gate);
1551 result.stats.nb_gates = dd.getNbGates();
1552 return result;
1553}
1554
1555/**
1556 * @brief Shared implementation of @c compile() / @c compileAll().
1557 *
1558 * @param rows Edge tuples.
1559 * @param source Source vertex.
1560 * @param directed If @c false, every edge contributes both arcs.
1561 * @param max_states Bound on the DP state count per node.
1562 * @param only_target When set: ensure the vertex exists in the graph
1563 * (an isolated target is legal) and emit a root for
1564 * it alone, skipping the other vertices' reads.
1565 * @param multi_sources When set: virtual super-source mode.
1566 * @return The shared d-D, per-vertex roots, statistics.
1567 */
1568ReachabilityCompiler::AllResult compileAllBoolInternal(
1569 const std::vector<ReachabilityCompiler::EdgeRow> &rows,
1570 unsigned long source,
1571 bool directed,
1572 std::size_t max_states,
1573 const unsigned long *only_target,
1574 const std::vector<ReachabilityCompiler::SourceArc> *multi_sources)
1575{
1577 dDNNF &dd = result.dd;
1578 const BoolOps ops;
1579
1580 std::map<unsigned long, std::vector<gate_t> > accepting;
1581 const gate_t true_gate = runReachabilityDP(
1582 rows, source, directed, max_states, only_target, multi_sources, ops, dd,
1583 result.stats,
1584 [&](unsigned long v, BoolOps::Entry, gate_t pair_gate) {
1585 accepting[v].push_back(pair_gate);
1586 });
1587
1588 result.roots.reserve(accepting.size());
1589 for (const auto &[v, gates] : accepting)
1590 result.roots.push_back(
1591 ReachabilityCompiler::VertexRoot{v, finalizeRoot(dd, gates)});
1592
1593 dd.setRoot(true_gate); // single-target callers re-point this
1594 result.stats.nb_gates = dd.getNbGates();
1595 return result;
1596}
1597
1598/**
1599 * @brief Implementation of @c compileAnyReachAll() (and, through a
1600 * one-set wrapper, @c compileAnyReach()): the shared prelude --
1601 * variable grouping, tree decomposition, bag assignments,
1602 * literal gates -- built once, then one bottom-up sweep per
1603 * target set with the set-reachability algebra and
1604 * content-deduplicated gate emission, each set's acceptance
1605 * read off the root table.
1606 */
1607ReachabilityCompiler::AnyReachAllResult compileAnyReachAllInternal(
1608 const std::vector<ReachabilityCompiler::EdgeRow> &rows,
1609 const std::vector<ReachabilityCompiler::SourceArc> &sources,
1610 const std::vector<std::vector<unsigned long> > &sets,
1611 bool directed,
1612 std::size_t max_states)
1613{
1615 dDNNF &dd = result.dd;
1616
1617 if (sets.empty())
1618 throw ReachabilityCompilerException("no target sets given");
1619
1620 /* Restrict each target set to vertices that actually exist (edge
1621 * endpoints and source vertices): an absent vertex is unreachable
1622 * and contributes nothing -- and, crucially, the virtual
1623 * super-source is allocated the first id *above* this universe, so
1624 * filtering also prevents a caller-supplied id from accidentally
1625 * colliding with it and seeding the acceptance bit everywhere. */
1626 std::unordered_set<unsigned long> universe;
1627 for (const auto &row : rows) {
1628 universe.insert(row.src);
1629 universe.insert(row.dst);
1630 }
1631 for (const auto &sa : sources)
1632 universe.insert(sa.vertex);
1633 std::vector<std::unordered_set<unsigned long> > target_sets(sets.size());
1634 for (std::size_t si = 0; si < sets.size(); ++si)
1635 for (unsigned long v : sets[si])
1636 if (universe.count(v))
1637 target_sets[si].insert(v);
1638 SetReachOps ops;
1639 ops.target_set = &target_sets[0];
1640
1641 // Per set, the accepting root states are those whose source position
1642 // carries the set-reachability bit; they partition the worlds, so
1643 // their OR is deterministic.
1644 std::vector<std::vector<gate_t> > accepting(sets.size());
1645 const std::function<void(std::size_t, const SetReachOps::State &,
1646 gate_t, int)> sink =
1647 [&](std::size_t si, const SetReachOps::State &state, gate_t g, int ps) {
1648 if (state.dvec[ps])
1649 accepting[si].push_back(g);
1650 };
1651
1652 runReachabilityDP(
1653 rows, 0, directed, max_states, nullptr, &sources, ops, dd,
1654 result.stats,
1655 [](unsigned long, SetReachOps::Entry, gate_t) {
1656 },
1657 nullptr, &target_sets, &sink);
1658
1659 result.roots.reserve(sets.size());
1660 for (std::size_t si = 0; si < sets.size(); ++si) {
1661 gate_t root;
1662 if (accepting[si].empty()) {
1663 // No world reaches the set: constant false.
1664 root = dd.setGate(BooleanGate::OR);
1665 dd.setInfo(root, DNNF_CERT_INFO);
1666 } else
1667 root = finalizeRoot(dd, accepting[si]);
1668 result.roots.push_back(root);
1669 }
1670 dd.setRoot(result.roots[0]); // single-set callers read this
1671 result.stats.nb_gates = dd.getNbGates();
1672 return result;
1673}
1674
1675/**
1676 * @brief Implementation of @c compileCoverReachAll() (and, through a
1677 * one-set wrapper, @c compileCoverReach()): the coverage
1678 * algebra over the shared prelude, one bottom-up sweep per
1679 * target set, acceptance -- no pending rescuer set after the
1680 * final collapse onto the source domain -- read per set.
1681 */
1682ReachabilityCompiler::AnyReachAllResult compileCoverReachAllInternal(
1683 const std::vector<ReachabilityCompiler::EdgeRow> &rows,
1684 const std::vector<ReachabilityCompiler::SourceArc> &sources,
1685 const std::vector<std::vector<unsigned long> > &sets,
1686 bool directed,
1687 std::size_t max_states)
1688{
1690 dDNNF &dd = result.dd;
1691
1692 if (sets.empty())
1693 throw ReachabilityCompilerException("no target sets given");
1694
1695 /* The universe filter serves the super-source collision as in the
1696 * any-reach case -- but here an absent vertex is *unreachable*, so a
1697 * set containing one compiles to constant false rather than being
1698 * silently shrunk. The universe is the *decomposed graph's* vertex
1699 * set: a vertex appearing only in self-loops never enters the DP
1700 * (self-loops are irrelevant to plain reachability), is unreachable
1701 * from anywhere else, and must count as absent. */
1702 std::unordered_set<unsigned long> universe;
1703 for (const auto &row : rows)
1704 if (row.src != row.dst) {
1705 universe.insert(row.src);
1706 universe.insert(row.dst);
1707 }
1708 for (const auto &sa : sources)
1709 universe.insert(sa.vertex);
1710 std::vector<std::unordered_set<unsigned long> > target_sets(sets.size());
1711 std::vector<bool> absent(sets.size(), false);
1712 for (std::size_t si = 0; si < sets.size(); ++si)
1713 for (unsigned long v : sets[si]) {
1714 if (universe.count(v))
1715 target_sets[si].insert(v);
1716 else
1717 absent[si] = true;
1718 }
1719 CoverOps ops;
1720 ops.target_set = &target_sets[0];
1721
1722 // Per set, the accepting collapsed states are those with no pending
1723 // rescuer set; they partition the worlds, so their OR is
1724 // deterministic.
1725 std::vector<std::vector<gate_t> > accepting(sets.size());
1726 const std::function<void(std::size_t, const CoverOps::State &,
1727 gate_t, int)> sink =
1728 [&](std::size_t si, const CoverOps::State &state, gate_t g, int) {
1729 if (state.pending.empty())
1730 accepting[si].push_back(g);
1731 };
1732
1733 runReachabilityDP(
1734 rows, 0, directed, max_states, nullptr, &sources, ops, dd,
1735 result.stats,
1736 [](unsigned long, CoverOps::Entry, gate_t) {
1737 },
1738 nullptr, &target_sets, &sink);
1739
1740 result.roots.reserve(sets.size());
1741 for (std::size_t si = 0; si < sets.size(); ++si) {
1742 gate_t root;
1743 if (absent[si] || accepting[si].empty()) {
1744 // An absent target vertex, or no covering world: constant false.
1745 root = dd.setGate(BooleanGate::OR);
1746 dd.setInfo(root, DNNF_CERT_INFO);
1747 } else
1748 root = finalizeRoot(dd, accepting[si]);
1749 result.roots.push_back(root);
1750 }
1751 dd.setRoot(result.roots[0]); // single-set callers read this
1752 result.stats.nb_gates = dd.getNbGates();
1753 return result;
1754}
1755
1756} // namespace
1757
1759 const std::vector<EdgeRow> &rows,
1760 unsigned long source,
1761 bool directed,
1762 std::size_t max_states)
1763{
1764 return compileAllBoolInternal(rows, source, directed, max_states, nullptr,
1765 nullptr);
1766}
1767
1769 const std::vector<EdgeRow> &rows,
1770 const std::vector<SourceArc> &sources,
1771 bool directed,
1772 std::size_t max_states)
1773{
1774 return compileAllBoolInternal(rows, 0, directed, max_states, nullptr,
1775 &sources);
1776}
1777
1779 const std::vector<EdgeRow> &rows,
1780 unsigned long source,
1781 bool directed,
1782 unsigned hop_bound,
1783 std::size_t max_states)
1784{
1785 return compileAllHopsInternal(rows, source, directed, hop_bound, max_states,
1786 nullptr);
1787}
1788
1790 const std::vector<EdgeRow> &rows,
1791 const std::vector<SourceArc> &sources,
1792 bool directed,
1793 unsigned hop_bound,
1794 std::size_t max_states)
1795{
1796 return compileAllHopsInternal(rows, 0, directed, hop_bound, max_states,
1797 &sources);
1798}
1799
1801 const std::vector<EdgeRow> &rows,
1802 const std::vector<SourceArc> &sources,
1803 const std::vector<unsigned long> &set,
1804 bool directed,
1805 std::size_t max_states)
1806{
1807 AnyReachAllResult all =
1808 compileAnyReachAllInternal(rows, sources, {set}, directed, max_states);
1809 Result result;
1810 result.dd = std::move(all.dd);
1811 result.stats = all.stats;
1812 return result;
1813}
1814
1816 const std::vector<EdgeRow> &rows,
1817 const std::vector<SourceArc> &sources,
1818 const std::vector<std::vector<unsigned long> > &sets,
1819 bool directed,
1820 std::size_t max_states)
1821{
1822 return compileAnyReachAllInternal(rows, sources, sets, directed, max_states);
1823}
1824
1826 const std::vector<EdgeRow> &rows,
1827 const std::vector<SourceArc> &sources,
1828 const std::vector<unsigned long> &set,
1829 bool directed,
1830 std::size_t max_states)
1831{
1832 AnyReachAllResult all =
1833 compileCoverReachAllInternal(rows, sources, {set}, directed, max_states);
1834 Result result;
1835 result.dd = std::move(all.dd);
1836 result.stats = all.stats;
1837 return result;
1838}
1839
1841 const std::vector<EdgeRow> &rows,
1842 const std::vector<SourceArc> &sources,
1843 const std::vector<std::vector<unsigned long> > &sets,
1844 bool directed,
1845 std::size_t max_states)
1846{
1847 return compileCoverReachAllInternal(rows, sources, sets, directed,
1848 max_states);
1849}
1850
1852 const std::vector<EdgeRow> &rows,
1853 unsigned long source,
1854 unsigned long target,
1855 bool directed,
1856 std::size_t max_states)
1857{
1858 AllResult all = compileAllBoolInternal(rows, source, directed, max_states,
1859 &target, nullptr);
1860 Result result;
1861 result.stats = all.stats;
1862
1863 gate_t root{0};
1864 bool found = false;
1865 for (const auto &vr : all.roots)
1866 if (vr.vertex == target) {
1867 root = vr.root;
1868 found = true;
1869 break;
1870 }
1871 if (!found) {
1872 // The target is certainly unreachable: constant false.
1873 root = all.dd.setGate(BooleanGate::OR);
1874 all.dd.setInfo(root, DNNF_CERT_INFO);
1875 result.stats.nb_gates = all.dd.getNbGates();
1876 }
1877 all.dd.setRoot(root);
1878 result.dd = std::move(all.dd);
1879 return result;
1880}
constexpr unsigned DNNF_CERT_INFO
d-DNNF certificate value for the (gate-type-specific) per-gate info field.
@ MULVAR
Auxiliary gate grouping all MULIN siblings.
@ 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.
@ MULIN
Multivalued-input gate (one of several options).
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
static std::string join(Range const &elements, const char *const delimiter)
Concatenate elements of a range with a delimiter.
Definition Formula.h:46
Decomposition-aligned compilation of two-terminal reachability over bounded-treewidth data into a d-D...
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.
void setInfo(gate_t g, unsigned info)
Store an integer annotation on gate g.
void addWire(gate_t f, gate_t t)
Add a directed wire from gate f (parent) to gate t (child).
Definition Circuit.hpp:81
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
Mutable adjacency-list graph over unsigned-long node IDs.
Definition Graph.h:33
void add_edge(unsigned long src, unsigned long tgt, bool undirected=true)
Add an edge between src and tgt.
Definition Graph.h:75
void add_node(unsigned long node)
Add node to the graph (no edges).
Definition Graph.h:89
Exception thrown when reachability compilation fails.
static AllHopsResult compileAllHops(const std::vector< EdgeRow > &rows, unsigned long source, bool directed, unsigned hop_bound, std::size_t max_states=DEFAULT_MAX_STATES)
Bounded-hop variant of compileAll(): per-(vertex, exact walk length) circuits for every length up to ...
static Result compile(const std::vector< EdgeRow > &rows, unsigned long source, unsigned long target, bool directed, std::size_t max_states=DEFAULT_MAX_STATES)
Compile s-t reachability over rows into a d-D.
static constexpr unsigned MAX_HOP_BOUND
Maximum supported hop bound for compileAllHops().
static Result compileCoverReach(const std::vector< EdgeRow > &rows, const std::vector< SourceArc > &sources, const std::vector< unsigned long > &set, bool directed, std::size_t max_states=DEFAULT_MAX_STATES)
Compile "every vertex of @p set is reachable" (k-terminal / coverage reachability) into one certified...
static Result compileAnyReach(const std::vector< EdgeRow > &rows, const std::vector< SourceArc > &sources, const std::vector< unsigned long > &set, bool directed, std::size_t max_states=DEFAULT_MAX_STATES)
Compile "some vertex of @p set is reachable" into one certified circuit.
static AnyReachAllResult compileAnyReachAll(const std::vector< EdgeRow > &rows, const std::vector< SourceArc > &sources, const std::vector< std::vector< unsigned long > > &sets, bool directed, std::size_t max_states=DEFAULT_MAX_STATES)
Multi-set variant of compileAnyReach(): one shared circuit, one root per target set.
static AnyReachAllResult compileCoverReachAll(const std::vector< EdgeRow > &rows, const std::vector< SourceArc > &sources, const std::vector< std::vector< unsigned long > > &sets, bool directed, std::size_t max_states=DEFAULT_MAX_STATES)
Multi-set variant of compileCoverReach(): one shared (content-deduplicated) circuit,...
static AllResult compileAll(const std::vector< EdgeRow > &rows, unsigned long source, bool directed, std::size_t max_states=DEFAULT_MAX_STATES)
Compile the reachability circuits of every vertex in one pass.
Tree decomposition of a Boolean circuit's primal graph.
static constexpr int MAX_TREEWIDTH
Maximum supported treewidth.
A d-DNNF circuit supporting exact probabilistic and game-theoretic evaluation.
Definition dDNNF.h:71
void setRoot(gate_t g)
Set the root gate.
Definition dDNNF.h:127
bool operator==(const pg_uuid_t &u, const pg_uuid_t &v)
Test two pg_uuid_t values for equality.
A bounded-hop all-targets compilation.
std::vector< VertexHopRoot > roots
Per (vertex, exact length) roots.
std::vector< VertexRoot > within_roots
Per-vertex "within the bound" roots.
An all-targets compilation: one shared d-D, one root per reachable vertex.
std::vector< VertexRoot > roots
One entry per vertex reachable in the all-edges-present world (including the source itself,...
Stats stats
Compilation statistics.
dDNNF dd
Shared circuit (gates are reused across vertices).
A multi-set any-reach compilation: one shared circuit, one root per target set.
dDNNF dd
Shared circuit (consed: identical subcircuits are the same gate).
Stats stats
Compilation statistics (max_states maxed over the sweeps).
std::vector< gate_t > roots
One root per input set, in input order.
A compiled reachability query: the d-D and its statistics.
Stats stats
Compilation statistics.
dDNNF dd
d-D whose root computes "t is reachable from s"; input gates carry the edge tokens (UUID) and probabi...
Structural statistics of a compilation, for diagnostics and tests.
std::size_t nb_gates
Number of gates of the emitted d-D.
std::size_t nb_bags
Number of bags of the decomposition.
std::size_t max_states
Maximum number of DP states at any node.
std::size_t nb_variables
Number of edge variables (provenance tokens).
unsigned data_treewidth
Treewidth of the min-fill decomposition of the data graph.
One (vertex, walk length) circuit of a bounded-hop compilation.
One vertex's reachability circuit in an all-targets compilation.
Build-loop interrupt hook for the standalone tdkc binary.