ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
UCQJointCompiler.cpp
Go to the documentation of this file.
1/**
2 * @file UCQJointCompiler.cpp
3 * @brief Implementation of the joint-width UCQ compiler's homomorphism
4 * DP (data-graph regime).
5 *
6 * See @c UCQJointCompiler.h for the construction's design and the
7 * structural argument (determinism and decomposability by
8 * construction). This translation unit implements the §3.5 fast path:
9 * every fact gated by an independent Bernoulli event, a single
10 * bottom-up sweep over a min-fill tree decomposition of the joint
11 * (here, Gaifman) graph of the facts, the Boolean UCQ read off the root
12 * table. The correlated regime (in-bag gate valuations, MULVAR blocks)
13 * is not handled by this fast path.
14 *
15 * ### The homomorphism-type state
16 *
17 * For each disjunct, the state holds the set of partial-homomorphism
18 * *codes* realised by the present facts of the processed subtree. A
19 * code assigns each query variable one of: UNASSIGNED (its image is not
20 * yet decided -- it will be pinned higher in the tree), a position in
21 * the current bag (its image is an in-bag element), or DONE (its image
22 * is an already-forgotten element, and *every* atom incident to it has
23 * been witnessed). Alongside, a bitmask records which atoms are
24 * witnessed by a present fact under this assignment. Because witnessing
25 * under a fixed assignment is deterministic, the set of achievable codes
26 * is a function of the world; states therefore partition worlds and the
27 * emitted ORs are deterministic. When a disjunct's witnessed mask
28 * becomes full the whole state collapses to the absorbing @c sat marker
29 * -- the main state-pruning lever, the UCQ analogue of the reachability
30 * compiler's final collapse.
31 */
32#include "UCQJointCompiler.h"
33
34#include <algorithm>
35#include <cstdint>
36#include <functional>
37#include <map>
38#include <memory>
39#include <set>
40#include <tuple>
41#include <unordered_map>
42#include <vector>
43
44/* The DP loops are the runtime hot spots on large instances; keep the
45 * backend cancellable, mirroring ReachabilityCompiler.cpp's guard
46 * pattern (no-op outside the PostgreSQL extension build). */
47#ifdef TDKC
48#include "tdkc_interrupt.h"
49#define CHECK_FOR_INTERRUPTS() provsql_tdkc_poll()
50#else
51extern "C" {
52#include "postgres.h"
53#include "miscadmin.h"
54}
55#endif
56
57namespace {
58
59/** @brief Underlying integer of a @c bag_t. */
60inline std::size_t bag_index(bag_t b)
61{
62 return static_cast<std::underlying_type<bag_t>::type>(b);
63}
64
65/** @brief Variable-status sentinels for a homomorphism code. */
66constexpr std::int8_t UNASSIGNED = -2; ///< Image not yet decided.
67constexpr std::int8_t DONE = -1; ///< Image forgotten, all incident atoms witnessed.
68
69/** @brief Per-disjunct precomputed query structure. */
70struct DisjunctInfo {
71 unsigned n_vars = 0; ///< Number of query variables.
72 std::vector<Atom> atoms; ///< The conjuncts.
73 std::uint64_t full = 0; ///< Mask of all atoms (sat threshold).
74 std::vector<std::uint64_t> atoms_of_var; ///< Per variable, the mask of incident atoms.
75};
76
77/** @brief Precomputed query context shared by all DP operations. */
78struct QueryCtx {
79 std::vector<DisjunctInfo> disjuncts;
80};
81
82/**
83 * @brief One partial-homomorphism code over a disjunct's variables.
84 *
85 * @c st[v] is UNASSIGNED, DONE, or a bag position; @c w is the
86 * witnessed-atom bitmask. Ordered (for canonical sorted code sets) and
87 * hashable through @c State.
88 */
89struct DCode {
90 std::vector<std::int8_t> st; ///< Per-variable status.
91 std::uint64_t w = 0; ///< Witnessed-atom mask.
92
93 bool operator==(const DCode &o) const {
94 return w == o.w && st == o.st;
95 }
96 bool operator<(const DCode &o) const {
97 if (w != o.w)
98 return w < o.w;
99 return st < o.st;
100 }
101};
102
103/**
104 * @brief One DP state: per-disjunct hom-set, or the absorbing @c sat
105 * marker.
106 *
107 * When @c sat is set the hom-sets are dropped (canonical), so every
108 * accepting world collapses to one state whose gate is their
109 * deterministic OR.
110 */
111struct State {
112 bool sat = false;
113 std::vector<std::vector<DCode> > homs; ///< Per disjunct, sorted unique codes.
114
115 // Correlated regime only (empty in the data-graph fast path): the
116 // valuation of the in-bag slice-gate vertices, and the subset whose
117 // (strong) value is asserted but not yet justified by an in-bag child
118 // -- the same valuation/suspicious mechanism as
119 // dDNNFTreeDecompositionBuilder (Amarilli-Capelli-Monet-Senellart,
120 // ToCS 2020). The @c sat collapse drops @c homs but keeps these
121 // (events still contribute probability).
122 std::map<unsigned long, bool> gate_val; ///< In-bag gate vertex -> value.
123 std::vector<unsigned long> susp; ///< Suspicious gate vertices (sorted).
124
125 bool operator==(const State &o) const {
126 if (sat != o.sat || gate_val != o.gate_val || susp != o.susp)
127 return false;
128 if (sat)
129 return true;
130 return homs == o.homs;
131 }
132};
133
134/** @brief Hash functor over @c State. */
135struct StateHash {
136 std::size_t operator()(const State &s) const noexcept {
137 std::uint64_t h = 1469598103934665603ull;
138 auto mix = [&](std::uint64_t x) {
139 h ^= x;
140 h *= 1099511628211ull;
141 };
142 mix(s.sat ? 0x9e3779b9ull : 0x1234567ull);
143 for (const auto &[g, v] : s.gate_val)
144 mix((g << 1) | (v ? 1u : 0u));
145 for (unsigned long g : s.susp)
146 mix(g * 0x9e3779b97f4a7c15ull);
147 if (!s.sat)
148 for (const auto &codes : s.homs) {
149 mix(codes.size());
150 for (const auto &c : codes) {
151 mix(c.w);
152 for (std::int8_t v : c.st)
153 mix(static_cast<std::uint64_t>(static_cast<std::uint8_t>(v)));
154 }
155 }
156 return static_cast<std::size_t>(h);
157 }
158};
159
160/** @brief Position of @p v in the sorted @p domain. */
161inline int positionIn(const std::vector<unsigned long> &domain, unsigned long v)
162{
163 return static_cast<int>(
164 std::lower_bound(domain.begin(), domain.end(), v) - domain.begin());
165}
166
167/** @brief Sort and deduplicate a disjunct's code set in place. */
168void canonicalize(std::vector<DCode> &codes)
169{
170 std::sort(codes.begin(), codes.end());
171 codes.erase(std::unique(codes.begin(), codes.end()), codes.end());
172}
173
174/** @brief The trivial state: every variable unassigned, nothing witnessed. */
175State trivialState(const QueryCtx &q)
176{
177 State s;
178 s.sat = false;
179 s.homs.resize(q.disjuncts.size());
180 for (std::size_t d = 0; d < q.disjuncts.size(); ++d)
181 s.homs[d].push_back(
182 DCode{std::vector<std::int8_t>(q.disjuncts[d].n_vars, UNASSIGNED), 0});
183 return s;
184}
185
186/** @brief The absorbing satisfied state. */
187State satState()
188{
189 State s;
190 s.sat = true;
191 return s;
192}
193
194/**
195 * @brief Close a disjunct's code set under witnessing @p fact, in place.
196 * @return @c true if some code reached the full witnessed mask (sat).
197 */
198bool closeDisjunct(const DisjunctInfo &di,
199 std::vector<DCode> &codes,
200 const Fact &fact,
201 const std::vector<unsigned long> &domain,
202 const std::map<unsigned, unsigned long> *head_pin = nullptr)
203{
204 // Atoms of this disjunct a present @p fact could witness.
205 std::vector<std::size_t> cand;
206 for (std::size_t ai = 0; ai < di.atoms.size(); ++ai)
207 if (di.atoms[ai].relation_id == fact.relation_id &&
208 di.atoms[ai].vars.size() == fact.elements.size())
209 cand.push_back(ai);
210 if (cand.empty())
211 return false;
212
213 std::vector<int> pos(fact.elements.size());
214 for (std::size_t i = 0; i < fact.elements.size(); ++i)
215 pos[i] = positionIn(domain, fact.elements[i]);
216
217 std::set<DCode> seen(codes.begin(), codes.end());
218 std::vector<DCode> work(codes.begin(), codes.end());
219 bool sat = false;
220
221 while (!work.empty()) {
222 DCode c = std::move(work.back());
223 work.pop_back();
224 for (std::size_t ai : cand) {
225 if ((c.w >> ai) & 1u)
226 continue; // already witnessed under this assignment
227 const Atom &a = di.atoms[ai];
228 DCode c2 = c;
229 bool ok = true;
230 for (std::size_t i = 0; i < a.vars.size(); ++i) {
231 const unsigned v = a.vars[i];
232 const std::int8_t p = static_cast<std::int8_t>(pos[i]);
233 // Per-answer head pin: a head variable may bind only to its answer's
234 // value (the same restriction the Sel-pin enforces, but without
235 // touching the encoding -- so the decomposition is shared).
236 if (head_pin) {
237 auto pit = head_pin->find(v);
238 if (pit != head_pin->end() && fact.elements[i] != pit->second) {
239 ok = false;
240 break;
241 }
242 }
243 if (c2.st[v] == UNASSIGNED)
244 c2.st[v] = p;
245 else if (c2.st[v] != p) { // already pinned to a different element
246 ok = false;
247 break;
248 }
249 }
250 if (!ok)
251 continue;
252 c2.w |= (std::uint64_t{1} << ai);
253 if (seen.insert(c2).second) {
254 if (c2.w == di.full)
255 sat = true;
256 work.push_back(c2);
257 }
258 }
259 }
260
261 codes.assign(seen.begin(), seen.end());
262 canonicalize(codes);
263 return sat;
264}
265
266/** @brief Apply a present fact to a whole state (closure over all disjuncts). */
267State closeWithFact(const QueryCtx &q, const State &s, const Fact &fact,
268 const std::vector<unsigned long> &domain,
269 const std::map<unsigned, unsigned long> *head_pin = nullptr)
270{
271 if (s.sat)
272 return s;
273 State out = s;
274 for (std::size_t d = 0; d < q.disjuncts.size(); ++d)
275 if (closeDisjunct(q.disjuncts[d], out.homs[d], fact, domain, head_pin))
276 return satState();
277 return out;
278}
279
280/**
281 * @brief Re-express a state over @p to_domain: forget the leaving
282 * elements, remap the surviving ones.
283 *
284 * A variable pinned to a leaving element becomes DONE if every incident
285 * atom is witnessed, else its code is dropped (the assignment can never
286 * complete: the element will not reappear, so an unwitnessed incident
287 * atom can never be witnessed).
288 */
289State forgetLift(const QueryCtx &q, const State &s,
290 const std::vector<unsigned long> &from_domain,
291 const std::vector<unsigned long> &to_domain)
292{
293 if (s.sat || from_domain == to_domain)
294 return s;
295 std::vector<int> map(from_domain.size());
296 for (std::size_t i = 0; i < from_domain.size(); ++i) {
297 auto it = std::lower_bound(to_domain.begin(), to_domain.end(),
298 from_domain[i]);
299 map[i] = (it != to_domain.end() && *it == from_domain[i])
300 ? static_cast<int>(it - to_domain.begin()) : -1;
301 }
302
303 State out;
304 out.sat = false;
305 out.homs.resize(q.disjuncts.size());
306 for (std::size_t d = 0; d < q.disjuncts.size(); ++d) {
307 const DisjunctInfo &di = q.disjuncts[d];
308 std::vector<DCode> &dst = out.homs[d];
309 for (const DCode &c : s.homs[d]) {
310 DCode c2;
311 c2.w = c.w;
312 c2.st.resize(di.n_vars);
313 bool dead = false;
314 for (unsigned v = 0; v < di.n_vars; ++v) {
315 const std::int8_t st = c.st[v];
316 if (st == UNASSIGNED || st == DONE) {
317 c2.st[v] = st;
318 } else {
319 const int np = map[static_cast<std::size_t>(st)];
320 if (np >= 0) {
321 c2.st[v] = static_cast<std::int8_t>(np);
322 } else if ((c.w & di.atoms_of_var[v]) == di.atoms_of_var[v]) {
323 c2.st[v] = DONE; // discharged, safe to forget
324 } else {
325 dead = true; // an incident atom can never be witnessed
326 break;
327 }
328 }
329 }
330 if (!dead)
331 dst.push_back(std::move(c2));
332 }
333 canonicalize(dst);
334 }
335 return out;
336}
337
338/**
339 * @brief Join two states over the same domain, covering disjoint fact
340 * sets (so the gates are decomposable and the states partition
341 * the combined worlds).
342 */
343State join(const QueryCtx &q, const State &s1, const State &s2)
344{
345 if (s1.sat || s2.sat)
346 return satState();
347 State out;
348 out.sat = false;
349 out.homs.resize(q.disjuncts.size());
350 for (std::size_t d = 0; d < q.disjuncts.size(); ++d) {
351 const DisjunctInfo &di = q.disjuncts[d];
352 std::vector<DCode> &dst = out.homs[d];
353 for (const DCode &c1 : s1.homs[d])
354 for (const DCode &c2 : s2.homs[d]) {
355 DCode c;
356 c.w = c1.w | c2.w;
357 c.st.resize(di.n_vars);
358 bool ok = true;
359 for (unsigned v = 0; v < di.n_vars; ++v) {
360 const std::int8_t a = c1.st[v];
361 const std::int8_t b = c2.st[v];
362 std::int8_t r;
363 if (a == UNASSIGNED)
364 r = b;
365 else if (b == UNASSIGNED)
366 r = a;
367 else if (a == DONE && b == DONE)
368 r = DONE;
369 else if (a == DONE || b == DONE) { // forgotten one side, in-bag the other
370 ok = false;
371 break;
372 } else if (a == b)
373 r = a;
374 else { // pinned to different elements
375 ok = false;
376 break;
377 }
378 c.st[v] = r;
379 }
380 if (!ok)
381 continue;
382 if (c.w == di.full)
383 return satState();
384 dst.push_back(std::move(c));
385 }
386 canonicalize(dst);
387 }
388 return out;
389}
390
391// ---------------------------------------------------------------------
392// Correlated regime: the homomorphism DP merged with the
393// valuation/suspicious gate machinery (Amarilli-Capelli-Monet-Senellart,
394// ToCS 2020), run over the joint graph (element + gate vertices).
395// ---------------------------------------------------------------------
396
397/**
398 * @brief Number of *essential* (enumerating) variables of a disjunct.
399 *
400 * The exponential parameter of the DP is not every join variable but only
401 * those that must be enumerated: a variable functionally determined by others
402 * does not multiply the partial-homomorphism state. This returns the size of
403 * a minimum set @c S of join variables whose FD closure covers all of them
404 * (the @c e of the @f$2^{O(k^e)}@f$ bound).
405 *
406 * The FDs are **mined from the gathered tuples** (@p enc.facts, already
407 * post-selection): a column set determines a column when no two tuples of the
408 * relation agree on the former and differ on the latter. This captures any
409 * declared key (which necessarily holds on the data) along with FDs incidental
410 * to this instance, and needs no catalog lookup. The closure is sound for the
411 * instance being compiled: if the FD holds on these tuples the variable is
412 * genuinely fixed in every world of this computation.
413 *
414 * The minimum-cover search is @f$2^{|V|}@f$ in the join-variable count @c |V|;
415 * above @c MAX_FD_VARS it is skipped and the full count returned (a sound upper
416 * bound), the design target being a handful of enumerating variables.
417 */
418unsigned essentialVarCount(const CQ &cq, const std::vector<bool> &appears,
419 const JointEncoding &enc)
420{
421 std::vector<unsigned> V; // the join variables
422 for (unsigned v = 0; v < cq.n_vars; ++v)
423 if (appears[v])
424 V.push_back(v);
425 const unsigned nV = static_cast<unsigned>(V.size());
426 static constexpr unsigned MAX_FD_VARS = 12;
427 if (nV <= 1 || nV > MAX_FD_VARS)
428 return nV;
429
430 // Tuples per (relation, arity), as pointers into enc.facts.
431 std::map<std::pair<unsigned, std::size_t>,
432 std::vector<const std::vector<unsigned long> *> > byrel;
433 for (const Fact &f : enc.facts)
434 byrel[{f.relation_id, f.elements.size()}].push_back(&f.elements);
435
436 // Memoised data FD: do the columns in bitmask @c known determine column @c c
437 // in the tuples of (@c rel, @c arity)?
438 std::map<std::tuple<unsigned, std::size_t, std::uint64_t, unsigned>, bool> memo;
439 auto dataFD = [&](unsigned rel, std::size_t arity,
440 std::uint64_t known, unsigned c) -> bool {
441 auto key = std::make_tuple(rel, arity, known, c);
442 auto it = memo.find(key);
443 if (it != memo.end())
444 return it->second;
445 bool holds = true;
446 auto rit = byrel.find({rel, arity});
447 if (rit != byrel.end()) {
448 std::map<std::vector<unsigned long>, unsigned long> grp;
449 for (const std::vector<unsigned long> *t : rit->second) {
450 std::vector<unsigned long> kv;
451 for (std::size_t p = 0; p < arity; ++p)
452 if (known & (std::uint64_t{1} << p))
453 kv.push_back((*t)[p]);
454 auto git = grp.find(kv);
455 if (git == grp.end())
456 grp.emplace(std::move(kv), (*t)[c]);
457 else if (git->second != (*t)[c]) {
458 holds = false;
459 break;
460 }
461 }
462 }
463 memo[key] = holds;
464 return holds;
465 };
466
467 std::unordered_map<unsigned, unsigned> bitOf;
468 for (unsigned i = 0; i < nV; ++i)
469 bitOf[V[i]] = i;
470 const std::uint32_t fullMask = (std::uint32_t{1} << nV) - 1;
471
472 // FD closure of a determined set (bitmask over @c V).
473 auto closure = [&](std::uint32_t det) -> std::uint32_t {
474 bool changed = true;
475 while (changed) {
476 changed = false;
477 for (const Atom &a : cq.atoms) {
478 const std::size_t arity = a.vars.size();
479 std::uint64_t knownPos = 0;
480 for (std::size_t p = 0; p < arity; ++p)
481 if (det & (std::uint32_t{1} << bitOf[a.vars[p]]))
482 knownPos |= (std::uint64_t{1} << p);
483 for (std::size_t p = 0; p < arity; ++p) {
484 if (knownPos & (std::uint64_t{1} << p))
485 continue; // this column already determined
486 const unsigned bit = bitOf[a.vars[p]];
487 if (dataFD(a.relation_id, arity, knownPos, static_cast<unsigned>(p))) {
488 det |= (std::uint32_t{1} << bit);
489 knownPos |= (std::uint64_t{1} << p);
490 changed = true;
491 }
492 }
493 }
494 }
495 return det;
496 };
497
498 unsigned best = nV;
499 for (std::uint32_t s = 0; s <= fullMask; ++s) {
500 const unsigned pc = static_cast<unsigned>(__builtin_popcount(s));
501 if (pc >= best)
502 continue; // cannot beat the current minimum
503 if (closure(s) == fullMask)
504 best = pc;
505 }
506 return best;
507}
508
509/** @brief Build the per-disjunct query context and the query stats. */
510void buildQueryCtx(const UCQ &ucq, const JointEncoding &enc,
511 QueryCtx &q, UCQJointCompiler::Stats &stats)
512{
513 q.disjuncts.resize(ucq.disjuncts.size());
514 stats.n_enumerating.resize(ucq.disjuncts.size());
515 for (std::size_t d = 0; d < ucq.disjuncts.size(); ++d) {
516 const CQ &cq = ucq.disjuncts[d];
517 DisjunctInfo &di = q.disjuncts[d];
518 di.n_vars = cq.n_vars;
519 di.atoms = cq.atoms;
520 if (cq.atoms.empty())
521 throw JointCompilerException("a UCQ disjunct has no atoms");
522 if (cq.atoms.size() > 64)
523 throw JointCompilerException("a UCQ disjunct has more than 64 atoms");
524 di.full = (cq.atoms.size() == 64)
525 ? ~std::uint64_t{0}
526 : ((std::uint64_t{1} << cq.atoms.size()) - 1);
527 di.atoms_of_var.assign(di.n_vars, 0);
528 std::vector<bool> appears(di.n_vars, false);
529 for (std::size_t ai = 0; ai < cq.atoms.size(); ++ai)
530 for (unsigned v : cq.atoms[ai].vars) {
531 if (v >= di.n_vars)
532 throw JointCompilerException("atom variable out of range");
533 di.atoms_of_var[v] |= (std::uint64_t{1} << ai);
534 appears[v] = true;
535 }
536 // The exponential parameter is the number of *essential* join variables:
537 // those that must be enumerated once the ones functionally determined by
538 // others (via FDs mined from the gathered data) are removed.
539 stats.n_enumerating[d] = essentialVarCount(cq, appears, enc);
540 }
543 stats.nb_variables = enc.events.size();
544}
545
546/** @brief Strong assignment (forced by one input): OR=true, AND=false. */
547inline bool isStrong(SliceGateType t, bool v)
548{
549 switch (t) {
550 case SliceGateType::OR: return v;
551 case SliceGateType::AND: return !v;
552 case SliceGateType::INPUT: return false;
553 default: return true; // NOT: forced either way
554 }
555}
556
557/**
558 * @brief Compile the correlated regime: the merged valuation + hom DP.
559 *
560 * The world variables are the slice's @c INPUT leaves. Each fact's
561 * presence is the value of its slice gate; a fact is witnessed (extends
562 * the homomorphisms) exactly when its gate is true in the state. The
563 * gate valuation is carried in @c State::gate_val with the suspicious
564 * set @c State::susp; events are introduced (and their literal emitted)
565 * at the unique bag where they are forgotten, so each variable's IN gate
566 * appears under one node (decomposability), and the per-world state is a
567 * function of the introduced events (determinism).
568 */
569UCQJointCompiler::Result mergedCompile(const JointEncoding &enc,
570 const QueryCtx &q,
572 unsigned max_treewidth,
573 std::size_t max_states,
574 const TreeDecomposition *shared_td,
575 const std::unordered_map<unsigned long,
576 bag_t> *shared_elim,
577 const std::map<unsigned,
578 unsigned long> *head_pin)
579{
581 dDNNF &dd = result.dd;
582
583 const unsigned long E = enc.n_elements;
584 const std::vector<SliceGate> &slice = enc.slice;
585 const std::size_t D = q.disjuncts.size();
586 auto isElem = [&](unsigned long v) { return v < E; };
587 auto gidx = [&](unsigned long v) { return static_cast<std::size_t>(v - E); };
588
589 // 1. Width screen + decomposition of the joint graph (or reuse a shared
590 // one: the single-sweep per-answer path builds it once and pins the
591 // head in the DP, so the joint graph -- data plus circuit -- is
592 // identical across answers).
593 std::unordered_map<unsigned long, bag_t> elim_local;
594 std::unique_ptr<TreeDecomposition> built_td;
595 const TreeDecomposition *tdp = shared_td;
596 if (tdp == nullptr) {
597 Graph graph = enc.buildGraph();
598 unsigned max_degree = 0;
599 if (TreeDecomposition::degeneracyLowerBound(graph, max_degree) > max_treewidth)
601 built_td.reset(new TreeDecomposition(std::move(graph), &elim_local));
602 if (built_td->getTreewidth() > max_treewidth)
604 tdp = built_td.get();
605 }
606 const TreeDecomposition &td = *tdp;
607 const std::unordered_map<unsigned long, bag_t> &elim =
608 shared_elim ? *shared_elim : elim_local;
609 stats.joint_treewidth = td.getTreewidth();
610 stats.nb_bags = td.getNbBags();
611 const std::size_t nb_bags = td.getNbBags();
612
613 auto bagIndex = [](bag_t b) {
614 return static_cast<std::underlying_type<bag_t>::type>(b);
615 };
616
617 // Bag domains (all vertices) and element subdomains (for hom positions).
618 std::vector<std::vector<unsigned long> > dom(nb_bags), edom(nb_bags);
619 for (std::size_t b = 0; b < nb_bags; ++b) {
620 for (gate_t g : td.getBag(bag_t{b}))
621 dom[b].push_back(static_cast<std::underlying_type<gate_t>::type>(g));
622 std::sort(dom[b].begin(), dom[b].end());
623 dom[b].erase(std::unique(dom[b].begin(), dom[b].end()), dom[b].end());
624 for (unsigned long v : dom[b])
625 if (isElem(v))
626 edom[b].push_back(v);
627 }
628
629 // Each fact at its rep bag: the earliest-eliminated of its clique
630 // (elements ∪ its gate vertex).
631 std::vector<std::vector<std::size_t> > facts_at_bag(nb_bags);
632 for (std::size_t fi = 0; fi < enc.facts.size(); ++fi) {
633 const Fact &f = enc.facts[fi];
634 std::vector<unsigned long> cl = f.elements;
635 if (f.kind == FactGateKind::GATE)
636 cl.push_back(E + f.gate);
637 bag_t best = elim.at(cl[0]);
638 for (unsigned long v : cl)
639 if (bagIndex(elim.at(v)) < bagIndex(best))
640 best = elim.at(v);
641 facts_at_bag[bagIndex(best)].push_back(fi);
642 }
643
644 // 2. Gate emission (deterministic OR / decomposable AND, certified).
645 using Table = std::unordered_map<State, gate_t, StateHash>;
646 using Accumulator = std::unordered_map<State, std::vector<gate_t>, StateHash>;
647 const gate_t invalid{static_cast<std::underlying_type<gate_t>::type>(-1)};
648 const gate_t true_gate = dd.setGate(BooleanGate::AND);
649 dd.setInfo(true_gate, DNNF_CERT_INFO);
650 std::vector<gate_t> ev_in(slice.size(), invalid), ev_not(slice.size(), invalid);
651 auto inGate = [&](std::size_t i) {
652 if (ev_in[i] == invalid)
653 ev_in[i] = dd.setGate(slice[i].token, BooleanGate::IN,
654 slice[i].prob);
655 return ev_in[i];
656 };
657 auto notGate = [&](std::size_t i) {
658 if (ev_not[i] == invalid) {
659 ev_not[i] = dd.setGate(BooleanGate::NOT);
660 dd.addWire(ev_not[i], inGate(i));
661 }
662 return ev_not[i];
663 };
664 auto andGate = [&](gate_t a, gate_t b) {
665 if (a == true_gate) return b;
666 if (b == true_gate) return a;
668 dd.setInfo(g, DNNF_CERT_INFO);
669 dd.addWire(g, a);
670 dd.addWire(g, b);
671 return g;
672 };
673 auto finalize = [&](Accumulator &acc) {
674 Table t;
675 t.reserve(acc.size());
676 for (auto &e : acc) {
677 if (e.second.size() == 1)
678 t.emplace(e.first, e.second[0]);
679 else {
681 dd.setInfo(o, DNNF_CERT_INFO);
682 for (gate_t c : e.second)
683 dd.addWire(o, c);
684 t.emplace(e.first, o);
685 }
686 }
687 acc.clear();
688 stats.max_states = std::max(stats.max_states, t.size());
689 if (t.size() > max_states)
691 "joint DP state space exceeds the per-node bound (" +
692 std::to_string(max_states) + ")");
693 return t;
694 };
695
696 // Weak-constraint local consistency and strong-gate justification.
697 auto almost = [&](const State &s) {
698 for (const auto &[gv, gval] : s.gate_val) {
699 const SliceGate &G = slice[gidx(gv)];
700 for (unsigned c : G.children) {
701 auto it = s.gate_val.find(E + c);
702 if (it == s.gate_val.end())
703 continue;
704 const bool cval = it->second;
705 if (G.type == SliceGateType::AND) {
706 if (gval && !cval) return false;
707 } else if (G.type == SliceGateType::OR) {
708 if (!gval && cval) return false;
709 } else if (G.type == SliceGateType::NOT) {
710 if (gval == cval) return false;
711 }
712 }
713 }
714 return true;
715 };
716 auto justify = [&](State &s) {
717 std::vector<unsigned long> keep;
718 for (unsigned long g : s.susp) {
719 const SliceGate &G = slice[gidx(g)];
720 const bool gval = s.gate_val.at(g);
721 bool just = false;
722 for (unsigned c : G.children) {
723 auto it = s.gate_val.find(E + c);
724 if (it == s.gate_val.end())
725 continue;
726 const bool cval = it->second;
727 if (G.type == SliceGateType::OR && gval && cval) just = true;
728 if (G.type == SliceGateType::AND && !gval && !cval) just = true;
729 if (G.type == SliceGateType::NOT && cval != gval) just = true;
730 }
731 if (!just)
732 keep.push_back(g);
733 }
734 s.susp = std::move(keep); // already sorted (susp was sorted)
735 };
736 auto addSusp = [](State &s, unsigned long g) {
737 auto it = std::lower_bound(s.susp.begin(), s.susp.end(), g);
738 if (it == s.susp.end() || *it != g)
739 s.susp.insert(it, g);
740 };
741
742 // Close a state's homs under a present fact (reuse the hom machinery).
743 // The head pin (single-sweep per-answer) is threaded through here: a fact
744 // binding a head variable to an element other than the answer's value is
745 // rejected, so a pinned sweep computes P(exists witness with head = v).
746 auto closeFact = [&](State s, const Fact &f,
747 const std::vector<unsigned long> &ed) {
748 if (s.sat)
749 return s;
750 for (std::size_t d = 0; d < D; ++d)
751 if (closeDisjunct(q.disjuncts[d], s.homs[d], f, ed,
752 head_pin)) {
753 s.sat = true;
754 s.homs.clear();
755 return s;
756 }
757 return s;
758 };
759
760 // Re-express a state's homs over a new element subdomain (M1 forget).
761 auto remapHoms = [&](const State &in, State &out,
762 const std::vector<unsigned long> &ef,
763 const std::vector<unsigned long> &et) {
764 if (in.sat) {
765 out.sat = true;
766 out.homs.clear();
767 return true;
768 }
769 out.sat = false;
770 out.homs.assign(D, {});
771 std::vector<int> map(ef.size());
772 for (std::size_t i = 0; i < ef.size(); ++i) {
773 auto it = std::lower_bound(et.begin(), et.end(), ef[i]);
774 map[i] = (it != et.end() && *it == ef[i])
775 ? static_cast<int>(it - et.begin()) : -1;
776 }
777 for (std::size_t d = 0; d < D; ++d) {
778 const DisjunctInfo &di = q.disjuncts[d];
779 std::vector<DCode> &dst = out.homs[d];
780 for (const DCode &c : in.homs[d]) {
781 DCode c2;
782 c2.w = c.w;
783 c2.st.resize(di.n_vars);
784 bool dead = false;
785 for (unsigned v = 0; v < di.n_vars; ++v) {
786 const std::int8_t st = c.st[v];
787 if (st == UNASSIGNED || st == DONE)
788 c2.st[v] = st;
789 else {
790 const int np = map[static_cast<std::size_t>(st)];
791 if (np >= 0)
792 c2.st[v] = static_cast<std::int8_t>(np);
793 else if ((c.w & di.atoms_of_var[v]) == di.atoms_of_var[v])
794 c2.st[v] = DONE;
795 else { dead = true; break; }
796 }
797 }
798 if (!dead)
799 dst.push_back(std::move(c2));
800 }
801 canonicalize(dst);
802 }
803 return true;
804 };
805
806 // lift: forget (event literal / gate non-suspicious / element remap),
807 // then introduce fresh gate vertices (enumerate values).
808 std::function<Table(const Table &, const std::vector<unsigned long> &,
809 const std::vector<unsigned long> &)> lift =
810 [&](const Table &tab, const std::vector<unsigned long> &from,
811 const std::vector<unsigned long> &to) -> Table {
812 if (from == to)
813 return tab;
814 std::vector<unsigned long> ef, et;
815 for (unsigned long v : from) if (isElem(v)) ef.push_back(v);
816 for (unsigned long v : to) if (isElem(v)) et.push_back(v);
817 std::vector<unsigned long> gforget, gintro;
818 for (unsigned long v : from)
819 if (!isElem(v) && !std::binary_search(to.begin(), to.end(), v))
820 gforget.push_back(v);
821 for (unsigned long v : to)
822 if (!isElem(v) && !std::binary_search(from.begin(), from.end(), v))
823 gintro.push_back(v);
824 Accumulator acc;
825 for (const auto &[St, g] : tab) {
826 CHECK_FOR_INTERRUPTS();
827 State cur;
828 cur.gate_val = St.gate_val;
829 cur.susp = St.susp;
830 gate_t cg = g;
831 bool dead = false;
832 for (unsigned long v : gforget) {
833 if (slice[gidx(v)].type == SliceGateType::INPUT)
834 cg = andGate(cg, St.gate_val.at(v) ? inGate(gidx(v)) : notGate(gidx(v)));
835 else if (std::binary_search(St.susp.begin(), St.susp.end(), v)) {
836 dead = true; // strong gate forgotten unjustified
837 break;
838 }
839 cur.gate_val.erase(v);
840 auto it = std::lower_bound(cur.susp.begin(), cur.susp.end(), v);
841 if (it != cur.susp.end() && *it == v)
842 cur.susp.erase(it);
843 }
844 if (dead)
845 continue;
846 State base;
847 base.gate_val = cur.gate_val;
848 base.susp = cur.susp;
849 remapHoms(St, base, ef, et);
850 std::vector<State> sts = {std::move(base)};
851 std::vector<gate_t> gs = {cg};
852 for (unsigned long v : gintro) {
853 std::vector<State> ns;
854 std::vector<gate_t> n2;
855 for (std::size_t i = 0; i < sts.size(); ++i)
856 for (int b = 0; b < 2; ++b) {
857 State nv = sts[i];
858 nv.gate_val[v] = static_cast<bool>(b);
859 if (slice[gidx(v)].type != SliceGateType::INPUT &&
860 isStrong(slice[gidx(v)].type, static_cast<bool>(b)))
861 addSusp(nv, v);
862 ns.push_back(std::move(nv));
863 n2.push_back(gs[i]);
864 }
865 sts = std::move(ns);
866 gs = std::move(n2);
867 }
868 for (std::size_t i = 0; i < sts.size(); ++i) {
869 justify(sts[i]);
870 if (!almost(sts[i]))
871 continue;
872 acc[std::move(sts[i])].push_back(gs[i]);
873 }
874 }
875 return finalize(acc);
876 };
877
878 auto join = [&](const Table &t1, const Table &t2) {
879 Accumulator acc;
880 for (const auto &[A, ga] : t1)
881 for (const auto &[B, gb] : t2) {
882 CHECK_FOR_INTERRUPTS();
883 bool ok = true;
884 for (const auto &[k, v] : A.gate_val) {
885 auto it = B.gate_val.find(k);
886 if (it != B.gate_val.end() && it->second != v) {
887 ok = false;
888 break;
889 }
890 }
891 if (!ok)
892 continue;
893 State nv;
894 nv.gate_val = A.gate_val;
895 for (const auto &[k, v] : B.gate_val)
896 nv.gate_val[k] = v;
897 // Suspicious only if suspicious in both subtrees.
898 for (unsigned long x : A.susp)
899 if (std::binary_search(B.susp.begin(), B.susp.end(), x))
900 nv.susp.push_back(x);
901 justify(nv);
902 if (!almost(nv))
903 continue;
904 if (A.sat || B.sat) {
905 nv.sat = true;
906 nv.homs.clear();
907 } else {
908 nv.sat = false;
909 nv.homs.assign(D, {});
910 bool sat = false;
911 for (std::size_t d = 0; d < D; ++d) {
912 const DisjunctInfo &di = q.disjuncts[d];
913 std::vector<DCode> &dst = nv.homs[d];
914 for (const DCode &c1 : A.homs[d])
915 for (const DCode &c2 : B.homs[d]) {
916 DCode c;
917 c.w = c1.w | c2.w;
918 c.st.resize(di.n_vars);
919 bool good = true;
920 for (unsigned v = 0; v < di.n_vars; ++v) {
921 const std::int8_t a = c1.st[v], bb = c2.st[v];
922 std::int8_t r;
923 if (a == UNASSIGNED) r = bb;
924 else if (bb == UNASSIGNED) r = a;
925 else if (a == DONE && bb == DONE) r = DONE;
926 else if (a == DONE || bb == DONE) { good = false; break; }
927 else if (a == bb) r = a;
928 else { good = false; break; }
929 c.st[v] = r;
930 }
931 if (!good)
932 continue;
933 if (c.w == di.full) sat = true;
934 dst.push_back(std::move(c));
935 }
936 canonicalize(dst);
937 }
938 if (sat) {
939 nv.sat = true;
940 nv.homs.clear();
941 }
942 }
943 acc[std::move(nv)].push_back(andGate(ga, gb));
944 }
945 return finalize(acc);
946 };
947
948 auto applyFacts = [&](Table tab, std::size_t b) {
949 for (std::size_t fi : facts_at_bag[b]) {
950 const Fact &f = enc.facts[fi];
951 Accumulator acc;
952 for (const auto &[St, g] : tab) {
953 const bool present =
955 (St.gate_val.count(E + f.gate) &&
956 St.gate_val.at(E + f.gate));
957 State ns = present ? closeFact(St, f, edom[b]) : St;
958 acc[std::move(ns)].push_back(g);
959 }
960 tab = finalize(acc);
961 }
962 return tab;
963 };
964
965 // The empty/trivial state introduced into a bag's domain.
966 auto trivialTable = [&](const std::vector<unsigned long> &d) {
967 State z;
968 z.homs.assign(D, {});
969 for (std::size_t i = 0; i < D; ++i)
970 z.homs[i].push_back(
971 DCode{std::vector<std::int8_t>(q.disjuncts[i].n_vars,
972 UNASSIGNED), 0});
973 Table e;
974 e.emplace(std::move(z), true_gate);
975 return lift(e, {}, d);
976 };
977
978 // 3. Bottom-up sweep (single sweep; the query is Boolean).
979 struct Frame {
980 bag_t bag;
981 std::size_t next_child = 0;
982 Table table;
983 bool has_table = false;
984 explicit Frame(bag_t b) : bag(b) {
985 }
986 };
987 std::vector<Frame> stack;
988 stack.push_back(Frame(td.getRoot()));
989 std::size_t root_bag = bagIndex(td.getRoot());
990 Table root_table;
991 while (!stack.empty()) {
992 Frame &frame = stack.back();
993 const auto &children = td.getChildren(frame.bag);
994 if (frame.next_child < children.size()) {
995 stack.push_back(Frame(children[frame.next_child++]));
996 continue;
997 }
998 CHECK_FOR_INTERRUPTS();
999 const std::size_t b = bagIndex(frame.bag);
1000 Table table = frame.has_table ? std::move(frame.table) : trivialTable(dom[b]);
1001 table = applyFacts(std::move(table), b);
1002 if (stack.size() == 1) {
1003 root_table = std::move(table);
1004 stack.pop_back();
1005 break;
1006 }
1007 Frame &parent = stack[stack.size() - 2];
1008 const std::size_t pb = bagIndex(parent.bag);
1009 Table lifted = lift(table, dom[b], dom[pb]);
1010 if (!parent.has_table) {
1011 parent.table = std::move(lifted);
1012 parent.has_table = true;
1013 } else {
1014 parent.table = join(parent.table, lifted);
1015 }
1016 stack.pop_back();
1017 }
1018
1019 // 4. Forget everything (apply remaining event literals); accept sat.
1020 Table top = lift(root_table, dom[root_bag], {});
1021 std::vector<gate_t> accepting;
1022 for (const auto &[s, g] : top)
1023 if (s.sat)
1024 accepting.push_back(g);
1025 gate_t root;
1026 if (accepting.empty()) {
1027 root = dd.setGate(BooleanGate::OR);
1028 dd.setInfo(root, DNNF_CERT_INFO);
1029 } else if (accepting.size() == 1) {
1030 root = accepting[0];
1031 } else {
1032 root = dd.setGate(BooleanGate::OR);
1033 dd.setInfo(root, DNNF_CERT_INFO);
1034 for (gate_t g : accepting)
1035 dd.addWire(root, g);
1036 }
1037 dd.setRoot(root);
1038 stats.dd_size = dd.getNbGates();
1039 result.stats = std::move(stats);
1040 return result;
1041}
1042
1043// =====================================================================
1044// Full top-down single-DP for per-answer evaluation (data-graph regime).
1045//
1046// One bottom-up sweep emits one d-DNNF root per answer, replacing k
1047// head-pinned @c compile() sweeps. The head variables become a
1048// STATE-LEVEL key: a head variable is never existentially projected (when
1049// its element leaves the bag it is recorded as a fixed value, not collapsed
1050// to DONE-and-forgotten), so different head bindings live in different
1051// states. Completed answers are tracked per head-tuple in the state's
1052// @c done set, and an answer is EMITTED as its own circuit root at the lift
1053// where the last of its head elements leaves the decomposition (no future
1054// fact can touch it, so its provenance is final). The answer roots share
1055// one circuit; the gate cache values them all in (amortised) one pass.
1056// =====================================================================
1057
1058/** @brief Head bookkeeping: which query variables are the head, and the slot
1059 * of each. @c slot_of_var[v] is the head position of variable @p v,
1060 * or -1 if @p v is not a head variable. */
1061struct HeadInfo {
1062 std::vector<unsigned> head_vars; ///< Query-variable indices of the head.
1063 std::vector<int> slot_of_var; ///< var -> head slot, or -1.
1064 std::size_t n_head() const { return head_vars.size(); }
1065};
1066
1067/** @brief No-value sentinel for an unbound / in-bag head slot. */
1068constexpr unsigned long NO_VAL = static_cast<unsigned long>(-1);
1069
1070/** @brief Augmented hom code: head variables additionally carry the fixed
1071 * element value once forgotten (@c st[v]==DONE for a head var). */
1072struct ADCode {
1073 std::vector<std::int8_t> st; ///< Per variable: UNASSIGNED/DONE/pos.
1074 std::uint64_t w = 0; ///< Witnessed-atom mask.
1075 std::vector<unsigned long> hval; ///< Per head slot: fixed value or NO_VAL.
1076
1077 bool operator==(const ADCode &o) const {
1078 return w == o.w && st == o.st && hval == o.hval;
1079 }
1080 bool operator<(const ADCode &o) const {
1081 if (w != o.w) return w < o.w;
1082 if (st != o.st) return st < o.st;
1083 return hval < o.hval;
1084 }
1085};
1086
1087/** @brief Augmented DP state: per-disjunct hom-set plus the set of head
1088 * tuples already satisfied (sorted, unique). */
1089struct AState {
1090 std::vector<std::vector<ADCode> > homs;
1091 std::vector<std::vector<unsigned long> > done; ///< Completed head tuples.
1092
1093 bool operator==(const AState &o) const {
1094 return done == o.done && homs == o.homs;
1095 }
1096};
1097
1098struct AStateHash {
1099 std::size_t operator()(const AState &s) const noexcept {
1100 std::uint64_t h = 1469598103934665603ull;
1101 auto mix = [&](std::uint64_t x) { h ^= x; h *= 1099511628211ull; };
1102 for (const auto &t : s.done) {
1103 mix(0xD0E + t.size());
1104 for (unsigned long e : t) mix(e * 0x9e3779b97f4a7c15ull);
1105 }
1106 for (const auto &codes : s.homs) {
1107 mix(codes.size());
1108 for (const auto &c : codes) {
1109 mix(c.w);
1110 for (std::int8_t v : c.st)
1111 mix(static_cast<std::uint64_t>(static_cast<std::uint8_t>(v)));
1112 for (unsigned long e : c.hval) mix(e + 0x9e37);
1113 }
1114 }
1115 return static_cast<std::size_t>(h);
1116 }
1117};
1118
1119inline void canonicalizeA(std::vector<ADCode> &codes)
1120{
1121 std::sort(codes.begin(), codes.end());
1122 codes.erase(std::unique(codes.begin(), codes.end()), codes.end());
1123}
1124
1125/** @brief Insert a head tuple into the sorted-unique @c done set. */
1126inline void addDone(std::vector<std::vector<unsigned long> > &done,
1127 const std::vector<unsigned long> &t)
1128{
1129 auto it = std::lower_bound(done.begin(), done.end(), t);
1130 if (it == done.end() || *it != t)
1131 done.insert(it, t);
1132}
1133
1134/** @brief The head-tuple value of a full code (every head var bound). */
1135inline std::vector<unsigned long> readHeadTuple(
1136 const ADCode &c, const HeadInfo &hi,
1137 const std::vector<unsigned long> &domain)
1138{
1139 std::vector<unsigned long> t(hi.n_head());
1140 for (std::size_t i = 0; i < hi.n_head(); ++i) {
1141 const unsigned v = hi.head_vars[i];
1142 const std::int8_t s = c.st[v];
1143 if (s >= 0)
1144 t[i] = domain[static_cast<std::size_t>(s)];
1145 else if (s == DONE)
1146 t[i] = c.hval[i];
1147 else
1149 "compileAnswersOneDP: head variable unbound at completion "
1150 "(head must occur in every disjunct)");
1151 }
1152 return t;
1153}
1154
1155/**
1156 * @brief Close a disjunct's augmented code set under a present fact.
1157 *
1158 * Like @c closeDisjunct but (a) the head variables are bound like any other
1159 * (no pin), and (b) a code that reaches the full witnessed mask is a
1160 * COMPLETION: its head tuple is appended to @p completions and the code is
1161 * discharged (dropped) rather than turned into a sat collapse.
1162 */
1163void closeDisjunctA(const DisjunctInfo &di, const HeadInfo &hi,
1164 std::vector<ADCode> &codes, const Fact &fact,
1165 const std::vector<unsigned long> &domain,
1166 std::vector<std::vector<unsigned long> > &completions)
1167{
1168 std::vector<std::size_t> cand;
1169 for (std::size_t ai = 0; ai < di.atoms.size(); ++ai)
1170 if (di.atoms[ai].relation_id == fact.relation_id &&
1171 di.atoms[ai].vars.size() == fact.elements.size())
1172 cand.push_back(ai);
1173 if (cand.empty())
1174 return;
1175
1176 std::vector<int> pos(fact.elements.size());
1177 for (std::size_t i = 0; i < fact.elements.size(); ++i)
1178 pos[i] = positionIn(domain, fact.elements[i]);
1179
1180 std::set<ADCode> seen(codes.begin(), codes.end());
1181 std::vector<ADCode> work(codes.begin(), codes.end());
1182
1183 while (!work.empty()) {
1184 ADCode c = std::move(work.back());
1185 work.pop_back();
1186 for (std::size_t ai : cand) {
1187 if ((c.w >> ai) & 1u)
1188 continue;
1189 const Atom &a = di.atoms[ai];
1190 ADCode c2 = c;
1191 bool ok = true;
1192 for (std::size_t i = 0; i < a.vars.size(); ++i) {
1193 const unsigned v = a.vars[i];
1194 const std::int8_t p = static_cast<std::int8_t>(pos[i]);
1195 if (c2.st[v] == UNASSIGNED)
1196 c2.st[v] = p;
1197 else if (c2.st[v] != p) {
1198 ok = false;
1199 break;
1200 }
1201 }
1202 if (!ok)
1203 continue;
1204 c2.w |= (std::uint64_t{1} << ai);
1205 if (seen.insert(c2).second) {
1206 if (c2.w == di.full)
1207 completions.push_back(readHeadTuple(c2, hi, domain));
1208 else
1209 work.push_back(c2);
1210 }
1211 }
1212 }
1213
1214 codes.clear();
1215 for (const ADCode &c : seen)
1216 if (c.w != di.full)
1217 codes.push_back(c);
1218 canonicalizeA(codes);
1219}
1220
1221/**
1222 * @brief Re-express an augmented state over @p to_domain.
1223 *
1224 * Non-head variables forget exactly as @c forgetLift (DONE if discharged,
1225 * else the code dies). A head variable pinned to a leaving element is, if
1226 * discharged, set DONE with its element VALUE recorded in @c hval (so the
1227 * answer survives); else the code dies. The @c done set is carried verbatim
1228 * (its tuples are element values, immune to position remapping).
1229 */
1230AState forgetLiftA(const QueryCtx &q, const HeadInfo &hi, const AState &s,
1231 const std::vector<unsigned long> &from_domain,
1232 const std::vector<unsigned long> &to_domain)
1233{
1234 if (from_domain == to_domain)
1235 return s;
1236 std::vector<int> map(from_domain.size());
1237 for (std::size_t i = 0; i < from_domain.size(); ++i) {
1238 auto it = std::lower_bound(to_domain.begin(), to_domain.end(),
1239 from_domain[i]);
1240 map[i] = (it != to_domain.end() && *it == from_domain[i])
1241 ? static_cast<int>(it - to_domain.begin()) : -1;
1242 }
1243
1244 AState out;
1245 out.done = s.done;
1246 out.homs.resize(q.disjuncts.size());
1247 for (std::size_t d = 0; d < q.disjuncts.size(); ++d) {
1248 const DisjunctInfo &di = q.disjuncts[d];
1249 std::vector<ADCode> &dst = out.homs[d];
1250 for (const ADCode &c : s.homs[d]) {
1251 ADCode c2;
1252 c2.w = c.w;
1253 c2.st.resize(di.n_vars);
1254 c2.hval = c.hval;
1255 bool dead = false;
1256 for (unsigned v = 0; v < di.n_vars; ++v) {
1257 const std::int8_t st = c.st[v];
1258 if (st == UNASSIGNED || st == DONE) {
1259 c2.st[v] = st;
1260 } else {
1261 const int np = map[static_cast<std::size_t>(st)];
1262 if (np >= 0) {
1263 c2.st[v] = static_cast<std::int8_t>(np);
1264 } else if ((c.w & di.atoms_of_var[v]) == di.atoms_of_var[v]) {
1265 c2.st[v] = DONE;
1266 const int slot = hi.slot_of_var[v];
1267 if (slot >= 0)
1268 c2.hval[static_cast<std::size_t>(slot)] =
1269 from_domain[static_cast<std::size_t>(st)];
1270 } else {
1271 dead = true;
1272 break;
1273 }
1274 }
1275 }
1276 if (!dead)
1277 dst.push_back(std::move(c2));
1278 }
1279 canonicalizeA(dst);
1280 }
1281 return out;
1282}
1283
1284/** @brief Join two augmented states over the same domain (disjoint facts). */
1285AState joinA(const QueryCtx &q, const HeadInfo &hi,
1286 const std::vector<unsigned long> &domain,
1287 const AState &s1, const AState &s2)
1288{
1289 AState out;
1290 out.done = s1.done;
1291 for (const auto &t : s2.done)
1292 addDone(out.done, t);
1293 out.homs.resize(q.disjuncts.size());
1294 for (std::size_t d = 0; d < q.disjuncts.size(); ++d) {
1295 const DisjunctInfo &di = q.disjuncts[d];
1296 std::vector<ADCode> &dst = out.homs[d];
1297 for (const ADCode &c1 : s1.homs[d])
1298 for (const ADCode &c2 : s2.homs[d]) {
1299 ADCode c;
1300 c.w = c1.w | c2.w;
1301 c.st.resize(di.n_vars);
1302 c.hval.assign(hi.n_head(), NO_VAL);
1303 bool ok = true;
1304 for (unsigned v = 0; v < di.n_vars; ++v) {
1305 const std::int8_t a = c1.st[v];
1306 const std::int8_t b = c2.st[v];
1307 std::int8_t r;
1308 if (a == UNASSIGNED)
1309 r = b;
1310 else if (b == UNASSIGNED)
1311 r = a;
1312 else if (a == DONE && b == DONE)
1313 r = DONE;
1314 else if (a == DONE || b == DONE) {
1315 ok = false;
1316 break;
1317 } else if (a == b)
1318 r = a;
1319 else {
1320 ok = false;
1321 break;
1322 }
1323 c.st[v] = r;
1324 // Reconcile the head value when a head variable is (now) forgotten.
1325 const int slot = hi.slot_of_var[v];
1326 if (r == DONE && slot >= 0) {
1327 const unsigned long v1 = (a == DONE)
1328 ? c1.hval[static_cast<std::size_t>(slot)] : NO_VAL;
1329 const unsigned long v2 = (b == DONE)
1330 ? c2.hval[static_cast<std::size_t>(slot)] : NO_VAL;
1331 if (v1 != NO_VAL && v2 != NO_VAL && v1 != v2) {
1332 ok = false;
1333 break;
1334 }
1335 c.hval[static_cast<std::size_t>(slot)] =
1336 (v1 != NO_VAL) ? v1 : v2;
1337 }
1338 }
1339 if (!ok)
1340 continue;
1341 if (c.w == di.full)
1342 addDone(out.done, readHeadTuple(c, hi, domain));
1343 else
1344 dst.push_back(std::move(c));
1345 }
1346 canonicalizeA(dst);
1347 }
1348 return out;
1349}
1350
1351/** @brief The trivial augmented state (every @c hval slot unbound). */
1352AState trivialAState(const QueryCtx &q, const HeadInfo &hi)
1353{
1354 AState s;
1355 s.homs.resize(q.disjuncts.size());
1356 for (std::size_t d = 0; d < q.disjuncts.size(); ++d)
1357 s.homs[d].push_back(
1358 ADCode{std::vector<std::int8_t>(q.disjuncts[d].n_vars, UNASSIGNED), 0,
1359 std::vector<unsigned long>(hi.n_head(), NO_VAL)});
1360 return s;
1361}
1362
1363/**
1364 * @brief The data-graph single top-down DP: build one d-DNNF root per answer
1365 * and evaluate them all from the shared circuit.
1366 */
1367/** @brief Head bookkeeping built from a UCQ + head-variable list. */
1368HeadInfo buildHeadInfo(const QueryCtx &q, const std::vector<unsigned> &head_vars)
1369{
1370 HeadInfo hi;
1371 hi.head_vars = head_vars;
1372 unsigned maxv = 0;
1373 for (const auto &di : q.disjuncts)
1374 maxv = std::max(maxv, di.n_vars);
1375 hi.slot_of_var.assign(maxv, -1);
1376 for (std::size_t i = 0; i < head_vars.size(); ++i) {
1377 if (head_vars[i] >= maxv)
1378 throw JointCompilerException("compileAnswersOneDP: head var out of range");
1379 hi.slot_of_var[head_vars[i]] = static_cast<int>(i);
1380 }
1381 for (const auto &di : q.disjuncts)
1382 for (unsigned hv : head_vars)
1383 if (hv >= di.n_vars || di.atoms_of_var[hv] == 0)
1385 "compileAnswersOneDP: a head variable does not occur in a disjunct");
1386 return hi;
1387}
1388
1389/**
1390 * @brief Augmented merged state for the CORRELATED single top-down DP: the
1391 * answer core (per-disjunct hom codes + completed head tuples) carried
1392 * alongside the slice-gate valuation and suspicious set.
1393 */
1394struct CState {
1395 AState core; ///< homs (with hval) + done tuples.
1396 std::map<unsigned long, bool> gate_val; ///< In-bag gate vertex -> value.
1397 std::vector<unsigned long> susp; ///< Suspicious gate vertices (sorted).
1398
1399 bool operator==(const CState &o) const {
1400 return gate_val == o.gate_val && susp == o.susp && core == o.core;
1401 }
1402};
1403
1404struct CStateHash {
1405 std::size_t operator()(const CState &s) const noexcept {
1406 std::uint64_t h = static_cast<std::uint64_t>(AStateHash{}(s.core));
1407 auto mix = [&](std::uint64_t x) { h ^= x; h *= 1099511628211ull; };
1408 for (const auto &[g, v] : s.gate_val)
1409 mix((g << 1) | (v ? 1u : 0u));
1410 for (unsigned long g : s.susp)
1411 mix(g * 0x9e3779b97f4a7c15ull);
1412 return static_cast<std::size_t>(h);
1413 }
1414};
1415
1416/**
1417 * @brief The CORRELATED single top-down DP: one bottom-up sweep over the
1418 * joint data+circuit decomposition emits one d-DNNF root per answer.
1419 *
1420 * Combines the merged valuation/suspicious gate DP of @c mergedCompile (the
1421 * world variables are the slice INPUT leaves; a fact is present iff its slice
1422 * gate is true) with the answer machinery of @c compileAnswersOneDPImpl (the
1423 * head is a state-level key carried in @c hval, completions are per-tuple in
1424 * @c done, and an answer is emitted -- at the gate already accumulating its
1425 * subtree's input literals -- when its head elements leave and no surviving
1426 * code can still witness it).
1427 */
1428UCQJointCompiler::AnswerCircuit compileAnswersOneDPCorrelated(
1429 const JointEncoding &enc, const UCQ &ucq, const QueryCtx &q,
1430 const HeadInfo &hi, UCQJointCompiler::Stats stats,
1431 unsigned max_treewidth, std::size_t max_states)
1432{
1434 dDNNF &dd = result.dd;
1435
1436 const unsigned long E = enc.n_elements;
1437 const std::vector<SliceGate> &slice = enc.slice;
1438 const std::size_t D = q.disjuncts.size();
1439 auto isElem = [&](unsigned long v) { return v < E; };
1440 auto gidx = [&](unsigned long v) { return static_cast<std::size_t>(v - E); };
1441
1442 // Width screen + decomposition of the joint graph (data + circuit slice).
1443 Graph graph = enc.buildGraph();
1444 unsigned max_degree = 0;
1445 if (TreeDecomposition::degeneracyLowerBound(graph, max_degree) > max_treewidth)
1447 std::unordered_map<unsigned long, bag_t> elim;
1448 const TreeDecomposition td(std::move(graph), &elim);
1449 if (td.getTreewidth() > max_treewidth)
1451 const std::size_t nb_bags = td.getNbBags();
1452 stats.joint_treewidth = td.getTreewidth();
1453 stats.nb_bags = nb_bags;
1454 auto bagIdx = [](bag_t b) {
1455 return static_cast<std::underlying_type<bag_t>::type>(b);
1456 };
1457
1458 std::vector<std::vector<unsigned long> > dom(nb_bags), edom(nb_bags);
1459 for (std::size_t b = 0; b < nb_bags; ++b) {
1460 for (gate_t g : td.getBag(bag_t{b}))
1461 dom[b].push_back(static_cast<std::underlying_type<gate_t>::type>(g));
1462 std::sort(dom[b].begin(), dom[b].end());
1463 dom[b].erase(std::unique(dom[b].begin(), dom[b].end()), dom[b].end());
1464 for (unsigned long v : dom[b])
1465 if (isElem(v))
1466 edom[b].push_back(v);
1467 }
1468 std::vector<std::vector<std::size_t> > facts_at_bag(nb_bags);
1469 for (std::size_t fi = 0; fi < enc.facts.size(); ++fi) {
1470 const Fact &f = enc.facts[fi];
1471 std::vector<unsigned long> cl = f.elements;
1472 if (f.kind == FactGateKind::GATE)
1473 cl.push_back(E + f.gate);
1474 bag_t best = elim.at(cl[0]);
1475 for (unsigned long v : cl)
1476 if (bagIdx(elim.at(v)) < bagIdx(best))
1477 best = elim.at(v);
1478 facts_at_bag[bagIdx(best)].push_back(fi);
1479 }
1480
1481 // Connected components of the joint graph (elements + gate vertices, joined
1482 // by each fact's clique). An answer is settled -- safe to emit -- only when
1483 // its whole component has left the bag: a witness fact's gate vertex can
1484 // outlive the head element, and the input literal it folds in then is part
1485 // of the answer's provenance. So we keep an answer open until no vertex of
1486 // its component remains, by when every component gate has folded into the
1487 // state gate. Independent answers fall in separate components (no
1488 // co-carrying); correlated answers share one (and must be carried together).
1489 const unsigned long NV = E + slice.size();
1490 std::vector<unsigned long> uf(NV);
1491 for (unsigned long v = 0; v < NV; ++v) uf[v] = v;
1492 std::function<unsigned long(unsigned long)> ufind =
1493 [&](unsigned long x) { while (uf[x] != x) { uf[x] = uf[uf[x]]; x = uf[x]; } return x; };
1494 auto uunion = [&](unsigned long a, unsigned long b) { uf[ufind(a)] = ufind(b); };
1495 for (const Fact &f : enc.facts) {
1496 std::vector<unsigned long> cl = f.elements;
1497 if (f.kind == FactGateKind::GATE)
1498 cl.push_back(E + f.gate);
1499 for (std::size_t i = 1; i < cl.size(); ++i)
1500 uunion(cl[0], cl[i]);
1501 }
1502
1503 // Gate emission (certified deterministic OR / decomposable AND).
1504 using Table = std::unordered_map<CState, gate_t, CStateHash>;
1505 using Accumulator = std::unordered_map<CState, std::vector<gate_t>, CStateHash>;
1506 const gate_t invalid{static_cast<std::underlying_type<gate_t>::type>(-1)};
1507 const gate_t true_gate = dd.setGate(BooleanGate::AND);
1508 dd.setInfo(true_gate, DNNF_CERT_INFO);
1509 std::vector<gate_t> ev_in(slice.size(), invalid), ev_not(slice.size(), invalid);
1510 auto inGate = [&](std::size_t i) {
1511 if (ev_in[i] == invalid)
1512 ev_in[i] = dd.setGate(slice[i].token, BooleanGate::IN,
1513 slice[i].prob);
1514 return ev_in[i];
1515 };
1516 auto notGate = [&](std::size_t i) {
1517 if (ev_not[i] == invalid) {
1518 ev_not[i] = dd.setGate(BooleanGate::NOT);
1519 dd.addWire(ev_not[i], inGate(i));
1520 }
1521 return ev_not[i];
1522 };
1523 auto andGate = [&](gate_t a, gate_t b) {
1524 if (a == true_gate) return b;
1525 if (b == true_gate) return a;
1527 dd.setInfo(g, DNNF_CERT_INFO);
1528 dd.addWire(g, a);
1529 dd.addWire(g, b);
1530 return g;
1531 };
1532 auto orGates = [&](const std::vector<gate_t> &gs) {
1533 if (gs.size() == 1) return gs[0];
1535 dd.setInfo(o, DNNF_CERT_INFO);
1536 for (gate_t c : gs) dd.addWire(o, c);
1537 return o;
1538 };
1539 auto finalize = [&](Accumulator &acc) {
1540 Table t;
1541 t.reserve(acc.size());
1542 for (auto &e : acc)
1543 t.emplace(e.first, orGates(e.second));
1544 acc.clear();
1545 stats.max_states = std::max(stats.max_states, t.size());
1546 if (t.size() > max_states)
1548 "joint DP state space exceeds the per-node bound (" +
1549 std::to_string(max_states) + ")");
1550 return t;
1551 };
1552
1553 // Gate-valuation local consistency / strong-gate justification.
1554 auto almost = [&](const std::map<unsigned long, bool> &gv) {
1555 for (const auto &[g, gval] : gv) {
1556 const SliceGate &G = slice[gidx(g)];
1557 for (unsigned c : G.children) {
1558 auto it = gv.find(E + c);
1559 if (it == gv.end()) continue;
1560 const bool cval = it->second;
1561 if (G.type == SliceGateType::AND) {
1562 if (gval && !cval) return false;
1563 } else if (G.type == SliceGateType::OR) {
1564 if (!gval && cval) return false;
1565 } else if (G.type == SliceGateType::NOT) {
1566 if (gval == cval) return false;
1567 }
1568 }
1569 }
1570 return true;
1571 };
1572 auto justify = [&](std::map<unsigned long, bool> &gv,
1573 std::vector<unsigned long> &susp) {
1574 std::vector<unsigned long> keep;
1575 for (unsigned long g : susp) {
1576 const SliceGate &G = slice[gidx(g)];
1577 const bool gval = gv.at(g);
1578 bool just = false;
1579 for (unsigned c : G.children) {
1580 auto it = gv.find(E + c);
1581 if (it == gv.end()) continue;
1582 const bool cval = it->second;
1583 if (G.type == SliceGateType::OR && gval && cval) just = true;
1584 if (G.type == SliceGateType::AND && !gval && !cval) just = true;
1585 if (G.type == SliceGateType::NOT && cval != gval) just = true;
1586 }
1587 if (!just) keep.push_back(g);
1588 }
1589 susp = std::move(keep);
1590 };
1591 auto addSusp = [](std::vector<unsigned long> &susp, unsigned long g) {
1592 auto it = std::lower_bound(susp.begin(), susp.end(), g);
1593 if (it == susp.end() || *it != g) susp.insert(it, g);
1594 };
1595
1596 std::map<std::vector<unsigned long>, std::vector<gate_t> > answer_acc;
1597
1598 // Apply a bag's facts: a fact is present iff its slice gate is true in the
1599 // valuation; a present fact closes the disjuncts (completions -> done).
1600 auto applyFacts = [&](Table table, std::size_t b) {
1601 for (std::size_t fi : facts_at_bag[b]) {
1602 const Fact &f = enc.facts[fi];
1603 Accumulator acc;
1604 for (const auto &[St, g] : table) {
1605 CHECK_FOR_INTERRUPTS();
1606 const bool present =
1608 (St.gate_val.count(E + f.gate) &&
1609 St.gate_val.at(E + f.gate));
1610 CState ns = St;
1611 if (present) {
1612 std::vector<std::vector<unsigned long> > comp;
1613 for (std::size_t d = 0; d < D; ++d)
1614 closeDisjunctA(q.disjuncts[d], hi, ns.core.homs[d],
1615 f, edom[b], comp);
1616 for (const auto &t : comp)
1617 addDone(ns.core.done, t);
1618 }
1619 acc[std::move(ns)].push_back(g);
1620 }
1621 table = finalize(acc);
1622 }
1623 return table;
1624 };
1625
1626 // Lift to the parent: forget leaving gates (fold INPUT literals, kill an
1627 // unjustified strong gate), forget/remap elements on the core, EMIT settled
1628 // answers, then introduce the parent's fresh gates (enumerate values).
1629 std::function<Table(const Table &, const std::vector<unsigned long> &,
1630 const std::vector<unsigned long> &)> lift =
1631 [&](const Table &tab, const std::vector<unsigned long> &from,
1632 const std::vector<unsigned long> &to) -> Table {
1633 if (from == to)
1634 return tab;
1635 std::vector<unsigned long> ef, et;
1636 for (unsigned long v : from) if (isElem(v)) ef.push_back(v);
1637 for (unsigned long v : to) if (isElem(v)) et.push_back(v);
1638 std::vector<unsigned long> gforget, gintro;
1639 for (unsigned long v : from)
1640 if (!isElem(v) && !std::binary_search(to.begin(), to.end(), v))
1641 gforget.push_back(v);
1642 for (unsigned long v : to)
1643 if (!isElem(v) && !std::binary_search(from.begin(), from.end(), v))
1644 gintro.push_back(v);
1645 Accumulator acc;
1646 for (const auto &[St, g] : tab) {
1647 CHECK_FOR_INTERRUPTS();
1648 std::map<unsigned long, bool> cur_gv = St.gate_val;
1649 std::vector<unsigned long> cur_susp = St.susp;
1650 gate_t cg = g;
1651 bool dead = false;
1652 for (unsigned long v : gforget) {
1653 if (slice[gidx(v)].type == SliceGateType::INPUT)
1654 cg = andGate(cg, St.gate_val.at(v) ? inGate(gidx(v)) : notGate(gidx(v)));
1655 else if (std::binary_search(St.susp.begin(), St.susp.end(), v)) {
1656 dead = true;
1657 break;
1658 }
1659 cur_gv.erase(v);
1660 auto it = std::lower_bound(cur_susp.begin(), cur_susp.end(), v);
1661 if (it != cur_susp.end() && *it == v)
1662 cur_susp.erase(it);
1663 }
1664 if (dead)
1665 continue;
1666
1667 AState core = forgetLiftA(q, hi, St.core, ef, et);
1668 // Emit answers whose whole component has left @p to: then every
1669 // witness gate has folded into @c cg and no future fact can touch the
1670 // answer. (The component of the head element subsumes both the
1671 // head-element-departure and the no-pending-witness tests.)
1672 std::vector<std::vector<unsigned long> > keep;
1673 for (const auto &tup : core.done) {
1674 const unsigned long cv = ufind(tup[0]);
1675 bool present = false;
1676 for (unsigned long u : to)
1677 if (ufind(u) == cv) { present = true; break; }
1678 if (!present)
1679 answer_acc[tup].push_back(cg);
1680 else
1681 keep.push_back(tup);
1682 }
1683 core.done = std::move(keep);
1684
1685 std::vector<CState> sts;
1686 std::vector<gate_t> gs;
1687 sts.push_back(CState{std::move(core), std::move(cur_gv),
1688 std::move(cur_susp)});
1689 gs.push_back(cg);
1690 for (unsigned long v : gintro) {
1691 std::vector<CState> ns;
1692 std::vector<gate_t> n2;
1693 for (std::size_t i = 0; i < sts.size(); ++i)
1694 for (int bv = 0; bv < 2; ++bv) {
1695 CState nv = sts[i];
1696 nv.gate_val[v] = static_cast<bool>(bv);
1697 if (slice[gidx(v)].type != SliceGateType::INPUT &&
1698 isStrong(slice[gidx(v)].type, static_cast<bool>(bv)))
1699 addSusp(nv.susp, v);
1700 ns.push_back(std::move(nv));
1701 n2.push_back(gs[i]);
1702 }
1703 sts = std::move(ns);
1704 gs = std::move(n2);
1705 }
1706 for (std::size_t i = 0; i < sts.size(); ++i) {
1707 justify(sts[i].gate_val, sts[i].susp);
1708 if (!almost(sts[i].gate_val))
1709 continue;
1710 acc[std::move(sts[i])].push_back(gs[i]);
1711 }
1712 }
1713 return finalize(acc);
1714 };
1715
1716 auto join = [&](const Table &t1, const Table &t2,
1717 const std::vector<unsigned long> &edomain) {
1718 Accumulator acc;
1719 for (const auto &[A, ga] : t1)
1720 for (const auto &[B, gb] : t2) {
1721 CHECK_FOR_INTERRUPTS();
1722 bool ok = true;
1723 for (const auto &[k, v] : A.gate_val) {
1724 auto it = B.gate_val.find(k);
1725 if (it != B.gate_val.end() && it->second != v) {
1726 ok = false;
1727 break;
1728 }
1729 }
1730 if (!ok)
1731 continue;
1732 CState nv;
1733 nv.gate_val = A.gate_val;
1734 for (const auto &[k, v] : B.gate_val)
1735 nv.gate_val[k] = v;
1736 for (unsigned long x : A.susp)
1737 if (std::binary_search(B.susp.begin(), B.susp.end(), x))
1738 nv.susp.push_back(x);
1739 justify(nv.gate_val, nv.susp);
1740 if (!almost(nv.gate_val))
1741 continue;
1742 nv.core = joinA(q, hi, edomain, A.core, B.core);
1743 acc[std::move(nv)].push_back(andGate(ga, gb));
1744 }
1745 return finalize(acc);
1746 };
1747
1748 auto trivialTable = [&](const std::vector<unsigned long> &d) {
1749 CState z;
1750 z.core = trivialAState(q, hi);
1751 Table e;
1752 e.emplace(std::move(z), true_gate);
1753 return lift(e, {}, d);
1754 };
1755
1756 // Bottom-up sweep.
1757 struct Frame {
1758 bag_t bag;
1759 std::size_t next_child = 0;
1760 Table table;
1761 bool has_table = false;
1762 explicit Frame(bag_t b) : bag(b) {}
1763 };
1764 Table root_table;
1765 std::size_t root_bag = bagIdx(td.getRoot());
1766 {
1767 std::vector<Frame> stack;
1768 stack.push_back(Frame(td.getRoot()));
1769 while (!stack.empty()) {
1770 Frame &frame = stack.back();
1771 const auto &children = td.getChildren(frame.bag);
1772 if (frame.next_child < children.size()) {
1773 stack.push_back(Frame(children[frame.next_child++]));
1774 continue;
1775 }
1776 CHECK_FOR_INTERRUPTS();
1777 const std::size_t b = bagIdx(frame.bag);
1778 Table table = frame.has_table ? std::move(frame.table) : trivialTable(dom[b]);
1779 table = applyFacts(std::move(table), b);
1780 if (stack.size() == 1) {
1781 root_table = std::move(table);
1782 stack.pop_back();
1783 break;
1784 }
1785 Frame &parent = stack[stack.size() - 2];
1786 const std::size_t pb = bagIdx(parent.bag);
1787 Table lifted = lift(table, dom[b], dom[pb]);
1788 if (!parent.has_table) {
1789 parent.table = std::move(lifted);
1790 parent.has_table = true;
1791 } else {
1792 parent.table = join(parent.table, lifted, edom[pb]);
1793 }
1794 stack.pop_back();
1795 }
1796 }
1797 // Forget the root domain: folds the remaining input literals and emits
1798 // every remaining answer.
1799 lift(root_table, dom[root_bag], {});
1800
1801 // One root per discovered answer; the caller materialises / evaluates them.
1802 result.answers.reserve(answer_acc.size());
1803 for (auto &entry : answer_acc)
1804 result.answers.push_back(
1805 UCQJointCompiler::AnswerRoot{entry.first, orGates(entry.second)});
1806 result.max_states = stats.max_states;
1807 return result;
1808}
1809
1810UCQJointCompiler::AnswerCircuit compileAnswersOneDPImpl(
1811 const JointEncoding &enc, const UCQ &ucq,
1812 const std::vector<unsigned> &head_vars,
1813 unsigned max_treewidth, std::size_t max_states)
1814{
1815 using Stats = UCQJointCompiler::Stats;
1816 if (ucq.disjuncts.empty())
1817 throw JointCompilerException("empty UCQ");
1818
1819 QueryCtx q;
1820 Stats stats;
1821 buildQueryCtx(ucq, enc, q, stats);
1822 HeadInfo hi = buildHeadInfo(q, head_vars);
1823
1824 // The per-answer route always materialises over the real provenance tokens,
1825 // so the encoding is always correlated (JointEncoding::fromCorrelated): one
1826 // merged valuation + answer DP over the joint data+circuit decomposition.
1827 // The data-graph-only single-sweep answer DP was retired with its SRFs
1828 // (commit 928afb5).
1829 return compileAnswersOneDPCorrelated(enc, ucq, q, hi, std::move(stats),
1830 max_treewidth, max_states);
1831}
1832
1833} // namespace
1834
1835/**
1836 * @brief Data-graph (TID/BID) compile, with optional decomposition reuse
1837 * and an optional in-DP head pin.
1838 *
1839 * With @p shared_td == nullptr and @p head_pin == nullptr this is the
1840 * Boolean data-graph compiler verbatim (the path the test suite exercises),
1841 * which is how @c compile() always calls it. The shared-decomposition /
1842 * per-answer head-pin parameters remain for a head-pinned per-answer sweep
1843 * (no current caller; the live per-answer path is @c compileAnswersOneDP).
1844 */
1846 const JointEncoding &enc,
1847 const UCQ &ucq,
1848 unsigned max_treewidth,
1849 std::size_t max_states,
1850 const TreeDecomposition *shared_td,
1851 const std::unordered_map<unsigned long, bag_t> *shared_elim,
1852 const std::map<unsigned, unsigned long> *head_pin)
1853{
1854 using Result = UCQJointCompiler::Result;
1855 using Stats = UCQJointCompiler::Stats;
1856 Result result;
1857 dDNNF &dd = result.dd;
1858 Stats &stats = result.stats;
1859
1860 if (ucq.disjuncts.empty())
1861 throw JointCompilerException("empty UCQ");
1862
1863 // ------------------------------------------------------------------
1864 // 0. Precompute the per-disjunct query structure.
1865 // ------------------------------------------------------------------
1866 QueryCtx q;
1867 buildQueryCtx(ucq, enc, q, stats);
1868
1869 // Correlated regime (facts gated by internal circuit gates): the
1870 // merged valuation + homomorphism DP. The single-sweep per-answer path
1871 // (shared_td / shared_elim / head_pin) is supported here too: the joint
1872 // graph spans data and circuit, is built once, and the head is pinned in
1873 // the DP rather than by a Sel atom (which would change the encoding).
1874 if (enc.correlated)
1875 return mergedCompile(enc, q, stats, max_treewidth, max_states,
1876 shared_td, shared_elim, head_pin);
1877
1878 // ------------------------------------------------------------------
1879 // 1. Width screen + tree decomposition of the joint graph (or reuse a
1880 // shared one). The degeneracy lower bound proves an unconstructible
1881 // width without paying for min-fill (thesis Prop. 4.2.11: the screen
1882 // must be on the joint graph; for the data-graph regime that is the
1883 // data treewidth, the sound screen there).
1884 // ------------------------------------------------------------------
1885 std::unordered_map<unsigned long, bag_t> elim_local;
1886 std::unique_ptr<TreeDecomposition> built_td;
1887 const TreeDecomposition *tdp = shared_td;
1888 if (tdp == nullptr) {
1889 Graph graph = enc.buildGraph();
1890 unsigned max_degree = 0;
1891 if (TreeDecomposition::degeneracyLowerBound(graph, max_degree) >
1892 max_treewidth)
1894 built_td.reset(new TreeDecomposition(std::move(graph), &elim_local));
1895 if (built_td->getTreewidth() > max_treewidth)
1897 tdp = built_td.get();
1898 }
1899 const TreeDecomposition &td = *tdp;
1900 const std::unordered_map<unsigned long, bag_t> &elimination_bag =
1901 shared_elim ? *shared_elim : elim_local;
1902 stats.joint_treewidth = td.getTreewidth();
1903 stats.nb_bags = td.getNbBags();
1904 const std::size_t nb_bags = td.getNbBags();
1905
1906 // Each fact is introduced at exactly one bag: the bag created when the
1907 // earliest-eliminated of its elements was eliminated contains all the
1908 // fact's elements (elimination invariant over the fact's clique). A
1909 // unique introduction point is what makes the AND gates decomposable.
1910 std::vector<std::vector<std::size_t> > facts_at_bag(nb_bags);
1911 for (std::size_t i = 0; i < enc.facts.size(); ++i) {
1912 const Fact &f = enc.facts[i];
1913 bag_t best = elimination_bag.at(f.elements[0]);
1914 for (unsigned long e : f.elements) {
1915 bag_t be = elimination_bag.at(e);
1916 if (bag_index(be) < bag_index(best))
1917 best = be;
1918 }
1919 facts_at_bag[bag_index(best)].push_back(i);
1920 }
1921
1922 // Bag domains: the bag's element ids, sorted.
1923 std::vector<std::vector<unsigned long> > domains(nb_bags);
1924 for (std::size_t b = 0; b < nb_bags; ++b) {
1925 auto &dom = domains[b];
1926 dom.reserve(td.getBag(bag_t{b}).size());
1927 for (gate_t g : td.getBag(bag_t{b}))
1928 dom.push_back(static_cast<std::underlying_type<gate_t>::type>(g));
1929 std::sort(dom.begin(), dom.end());
1930 dom.erase(std::unique(dom.begin(), dom.end()), dom.end());
1931 }
1932
1933 // ------------------------------------------------------------------
1934 // 2. Gate-emission helpers. Every emitted OR is deterministic and
1935 // every AND decomposable by construction; mark them with the d-D
1936 // certificate so the certificate-aware consumers can evaluate the
1937 // circuit linearly.
1938 // ------------------------------------------------------------------
1939 using Table = std::unordered_map<State, gate_t, StateHash>;
1940 using Accumulator = std::unordered_map<State, std::vector<gate_t>, StateHash>;
1941
1942 const gate_t invalid_gate{static_cast<std::underlying_type<gate_t>::type>(-1)};
1943 const gate_t true_gate = dd.setGate(BooleanGate::AND); // empty AND = true
1944 dd.setInfo(true_gate, DNNF_CERT_INFO);
1945
1946 std::vector<gate_t> ev_in(enc.events.size(), invalid_gate);
1947 std::vector<gate_t> ev_not(enc.events.size(), invalid_gate);
1948 auto inGate = [&](std::size_t e) {
1949 if (ev_in[e] == invalid_gate)
1950 ev_in[e] = dd.setGate(enc.events[e].token, BooleanGate::IN,
1951 enc.events[e].prob);
1952 return ev_in[e];
1953 };
1954 auto notGate = [&](std::size_t e) {
1955 if (ev_not[e] == invalid_gate) {
1956 ev_not[e] = dd.setGate(BooleanGate::NOT);
1957 dd.addWire(ev_not[e], inGate(e));
1958 }
1959 return ev_not[e];
1960 };
1961 auto andGate = [&](gate_t a, gate_t b) {
1962 if (a == true_gate)
1963 return b;
1964 if (b == true_gate)
1965 return a;
1967 dd.setInfo(g, DNNF_CERT_INFO);
1968 dd.addWire(g, a);
1969 dd.addWire(g, b);
1970 return g;
1971 };
1972
1973 auto finalize = [&](Accumulator &acc) {
1974 Table t;
1975 t.reserve(acc.size());
1976 for (auto &entry : acc) {
1977 if (entry.second.size() == 1)
1978 t.emplace(entry.first, entry.second[0]);
1979 else {
1981 dd.setInfo(g, DNNF_CERT_INFO);
1982 for (gate_t c : entry.second)
1983 dd.addWire(g, c);
1984 t.emplace(entry.first, g);
1985 }
1986 }
1987 acc.clear();
1988 stats.max_states = std::max(stats.max_states, t.size());
1989 if (t.size() > max_states)
1991 "joint DP state space exceeds the per-node bound (" +
1992 std::to_string(max_states) + ")");
1993 return t;
1994 };
1995
1996 const State kTrivial = trivialState(q);
1997 auto trivialTable = [&]() {
1998 return Table{{kTrivial, true_gate}};
1999 };
2000 auto isTrivial = [&](const Table &t) {
2001 return t.size() == 1 && t.begin()->second == true_gate &&
2002 t.begin()->first == kTrivial;
2003 };
2004
2005 // Re-express a child's below table over the parent domain (forget the
2006 // leaving elements, remap the survivors). States may collapse, hence
2007 // the accumulator.
2008 auto lift = [&](const Table &t, const std::vector<unsigned long> &from,
2009 const std::vector<unsigned long> &to) {
2010 if (from == to)
2011 return t;
2012 Accumulator acc;
2013 for (const auto &entry : t)
2014 acc[forgetLift(q, entry.first, from, to)].push_back(
2015 entry.second);
2016 return finalize(acc);
2017 };
2018
2019 // Join two tables over the same domain (disjoint fact sets).
2020 auto joinTables = [&](const Table &t1, const Table &t2) {
2021 if (isTrivial(t1))
2022 return t2;
2023 if (isTrivial(t2))
2024 return t1;
2025 Accumulator acc;
2026 for (const auto &left : t1)
2027 for (const auto &right : t2) {
2028 CHECK_FOR_INTERRUPTS();
2029 acc[join(q, left.first, right.first)].push_back(
2030 andGate(left.second, right.second));
2031 }
2032 return finalize(acc);
2033 };
2034
2035 // Introduce bag b's facts into a table over b's domain.
2036 auto applyFacts = [&](Table table, std::size_t b) {
2037 const auto &domain = domains[b];
2038 for (std::size_t fi : facts_at_bag[b]) {
2039 const Fact &fact = enc.facts[fi];
2040 Accumulator acc;
2041 for (const auto &entry : table) {
2042 CHECK_FOR_INTERRUPTS();
2043 State present = closeWithFact(q, entry.first, fact,
2044 domain, head_pin);
2045 if (fact.kind == FactGateKind::CERTAIN) {
2046 acc[std::move(present)].push_back(entry.second);
2047 } else if (present == entry.first) {
2048 // The fact cannot change the state in these
2049 // worlds: keep the gate (its value marginalises).
2050 acc[entry.first].push_back(entry.second);
2051 } else {
2052 acc[std::move(present)].push_back(
2053 andGate(entry.second, inGate(fact.event)));
2054 acc[entry.first].push_back(
2055 andGate(entry.second, notGate(fact.event)));
2056 }
2057 }
2058 table = finalize(acc);
2059 }
2060 return table;
2061 };
2062
2063 // ------------------------------------------------------------------
2064 // 3. Bottom-up sweep (single sweep: the query is Boolean, so the root
2065 // table already determines satisfaction -- the top-down sweep is
2066 // only needed for free first-order variables).
2067 // ------------------------------------------------------------------
2068 struct Frame {
2069 bag_t bag;
2070 std::size_t next_child = 0;
2071 Table table;
2072 bool has_table = false;
2073 explicit Frame(bag_t b) : bag(b) {
2074 }
2075 };
2076
2077 Table root_table;
2078 {
2079 std::vector<Frame> stack;
2080 stack.push_back(Frame(td.getRoot()));
2081 while (!stack.empty()) {
2082 Frame &frame = stack.back();
2083 const auto &children = td.getChildren(frame.bag);
2084 if (frame.next_child < children.size()) {
2085 bag_t c = children[frame.next_child++];
2086 stack.push_back(Frame(c));
2087 continue;
2088 }
2089
2090 CHECK_FOR_INTERRUPTS();
2091 const std::size_t b = bag_index(frame.bag);
2092 Table table = frame.has_table ? std::move(frame.table) : trivialTable();
2093 table = applyFacts(std::move(table), b);
2094
2095 if (stack.size() == 1) {
2096 root_table = std::move(table);
2097 stack.pop_back();
2098 break;
2099 }
2100
2101 Frame &parent = stack[stack.size() - 2];
2102 const std::size_t pb = bag_index(parent.bag);
2103 Table lifted = lift(table, domains[b], domains[pb]);
2104 if (!parent.has_table) {
2105 parent.table = std::move(lifted);
2106 parent.has_table = true;
2107 } else {
2108 parent.table = joinTables(parent.table, lifted);
2109 }
2110 stack.pop_back();
2111 }
2112 }
2113
2114 // ------------------------------------------------------------------
2115 // 4. Root: the answer is the deterministic OR over the accepting
2116 // (sat) root states. Satisfaction is captured during the sweep
2117 // (witnessing only happens at fact introduction / join), so no
2118 // final re-expression is needed.
2119 // ------------------------------------------------------------------
2120 std::vector<gate_t> accepting;
2121 for (const auto &[s, g] : root_table)
2122 if (s.sat)
2123 accepting.push_back(g);
2124
2125 gate_t root;
2126 if (accepting.empty()) {
2127 root = dd.setGate(BooleanGate::OR); // constant false
2128 dd.setInfo(root, DNNF_CERT_INFO);
2129 } else if (accepting.size() == 1) {
2130 root = accepting[0];
2131 } else {
2132 root = dd.setGate(BooleanGate::OR);
2133 dd.setInfo(root, DNNF_CERT_INFO);
2134 for (gate_t g : accepting)
2135 dd.addWire(root, g);
2136 }
2137 dd.setRoot(root);
2138 stats.dd_size = dd.getNbGates();
2139 return result;
2140}
2141
2143 const JointEncoding &enc,
2144 const UCQ &ucq,
2145 unsigned max_treewidth,
2146 std::size_t max_states)
2147{
2148 return compileImpl(enc, ucq, max_treewidth, max_states,
2149 nullptr, nullptr, nullptr);
2150}
2151
2153 const JointEncoding &enc,
2154 const UCQ &ucq,
2155 const std::vector<unsigned> &head_vars,
2156 unsigned max_treewidth,
2157 std::size_t max_states)
2158{
2159 return compileAnswersOneDPImpl(enc, ucq, head_vars, max_treewidth,
2160 max_states);
2161}
constexpr unsigned DNNF_CERT_INFO
d-DNNF certificate value for the (gate-type-specific) per-gate info field.
@ 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.
bool operator<(gate_t t, std::vector< gate_t >::size_type u)
Compare a gate_t against a std::vector size type.
Definition Circuit.h:240
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
SliceGateType
Gate kind of a circuit-slice node (correlated regime).
@ GATE
Present iff its slice gate evaluates true (correlated regime).
@ CERTAIN
Always present: an untracked relation, constant-true token.
bag_t
Strongly-typed bag identifier for a tree decomposition.
static UCQJointCompiler::Result compileImpl(const JointEncoding &enc, const UCQ &ucq, unsigned max_treewidth, std::size_t max_states, const TreeDecomposition *shared_td, const std::unordered_map< unsigned long, bag_t > *shared_elim, const std::map< unsigned, unsigned long > *head_pin)
Data-graph (TID/BID) compile, with optional decomposition reuse and an optional in-DP head pin.
Phase C of the joint-width UCQ compiler: a UCQ-specialised homomorphism-type DP that runs directly ov...
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
Exception thrown when joint-width compilation cannot proceed.
The joint encoding of an instance: facts, world events, and the joint graph the screen and the DP run...
Graph buildGraph() const
Construct the joint graph for the screen and the DP.
std::vector< Event > events
Independent world variables (data-graph regime).
bool correlated
true when the slice is present (correlated regime).
unsigned circuit_treewidth_lb
Degeneracy lower bound of the slice-only graph (0 in the data-graph regime).
std::vector< Fact > facts
Deduplicated facts.
std::vector< SliceGate > slice
Circuit slice (correlated regime); gate vertex = n_elements + index.
unsigned data_treewidth_lb
Degeneracy lower bound of the data-only graph (diagnostics).
unsigned long n_elements
One past the largest domain element id (vertex ids [0,n_elements)).
Exception thrown when a tree decomposition cannot be constructed.
Tree decomposition of a Boolean circuit's primal graph.
std::size_t getNbBags() const
Return the number of bags in the decomposition.
static unsigned degeneracyLowerBound(const BooleanCircuit &bc, unsigned &max_degree)
Cheap degeneracy lower bound on the treewidth of bc's primal graph.
unsigned getTreewidth() const
Return the treewidth of this decomposition.
bag_t getRoot() const
Return the root bag of the decomposition.
Bag & getBag(bag_t b)
Mutable access to bag b.
std::vector< bag_t > & getChildren(bag_t b)
Mutable access to the children of bag b.
static AnswerCircuit compileAnswersOneDP(const JointEncoding &enc, const UCQ &ucq, const std::vector< unsigned > &head_vars, unsigned max_treewidth=TreeDecomposition::MAX_TREEWIDTH, std::size_t max_states=DEFAULT_MAX_STATES)
static Result compile(const JointEncoding &enc, const UCQ &ucq, unsigned max_treewidth=TreeDecomposition::MAX_TREEWIDTH, std::size_t max_states=DEFAULT_MAX_STATES)
Compile a Boolean UCQ over enc into a certified d-D.
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
constexpr bool isStrong(BooleanGate type, bool value)
Return true if assigning value to a gate of type type is a "strong" assignment.
bool operator==(const pg_uuid_t &u, const pg_uuid_t &v)
Test two pg_uuid_t values for equality.
One atom of a conjunctive query: a relation symbol applied to query variables.
std::vector< unsigned > vars
Query-variable indices, one per column.
unsigned relation_id
Dense id of the relation symbol.
One conjunctive query (a disjunct of the UCQ).
std::vector< Atom > atoms
The conjuncts.
unsigned n_vars
Number of query variables.
A deduplicated fact participating in the DP.
std::size_t event
Index into events (when INDEP).
std::size_t gate
Index into slice (when GATE).
std::vector< unsigned long > elements
Dense element ids (the fact's tuple).
FactGateKind kind
How the fact's presence is gated.
unsigned relation_id
Dense id of the relation symbol.
One node of the extracted circuit slice (correlated regime).
SliceGateType type
Node kind.
std::vector< unsigned > children
Child slice indices (≤ 2; empty for INPUT).
std::size_t max_states
Peak DP state count.
dDNNF dd
The shared certified d-D.
std::vector< AnswerRoot > answers
One root per discovered answer.
Per-answer evaluation by a single top-down DP (data-graph regime).
A compiled UCQ: the d-D and its statistics.
Stats stats
Compilation statistics.
dDNNF dd
d-D whose root computes "the UCQ holds"; input gates carry the event tokens and probabilities.
Structural statistics of a compilation, for diagnostics and tests.
std::size_t nb_variables
Number of world variables (events).
unsigned joint_treewidth
Treewidth of the min-fill decomposition of the joint graph.
std::vector< unsigned > n_enumerating
Per-disjunct static count of enumerating variables.
std::size_t dd_size
Number of gates of the emitted d-D.
std::size_t max_states
Maximum number of DP states at any node.
unsigned data_treewidth_lb
Degeneracy lower bound of the data-only graph.
unsigned circuit_treewidth_lb
Degeneracy lower bound of the slice-only graph.
std::size_t nb_bags
Number of bags of the decomposition.
A union of conjunctive queries.
std::vector< CQ > disjuncts
The disjuncts (at least one).
size_t size() const
Return the number of elements in the set.
Definition flat_set.hpp:81
Build-loop interrupt hook for the standalone tdkc binary.