ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
GenericCircuit.cpp
Go to the documentation of this file.
1/**
2 * @file GenericCircuit.cpp
3 * @brief GenericCircuit method implementations.
4 *
5 * Implements the virtual methods of @c GenericCircuit that override the
6 * @c Circuit<gate_type> base class:
7 * - @c addGate(): allocates a new gate and extends the @c prob vector.
8 * - @c setGate(gate_type): creates a new gate, registering it as an
9 * input gate when the type is @c gate_input or @c gate_update.
10 * - @c setGate(const uuid&, gate_type): same with UUID binding.
11 *
12 * The template method @c evaluate() is defined in @c GenericCircuit.hpp.
13 */
14#include "GenericCircuit.h"
15
16#include <unordered_set>
17
19{
20 auto id = Circuit::setGate(type);
21 if(type == gate_input || type==gate_update) {
22 inputs.insert(id);
23 }
24 return id;
25}
26
28{
29 auto id = Circuit::setGate(u, type);
30 if(type == gate_input || type==gate_update) {
31 inputs.insert(id);
32 }
33 return id;
34}
35
37{
38 auto id=Circuit::addGate();
39 prob.push_back(1);
40 return id;
41}
42
43#include <unordered_map>
44
46{
47 GenericCircuit &gc = *this;
48 const std::size_t n = gc.getNbGates();
49 bool ever = false; ///< Whether any phase mutated the circuit.
50
51 /* Wrap the three phases in an outer fixpoint loop. Phase 3's
52 * substitutions can re-expose identity wires to a parent that
53 * referenced the collapsed gate (a single-wire @c gate_times / plus
54 * whose sole wire was itself collapsed, a @c gate_zero absorber…),
55 * which is a fresh Phase-1 opportunity. @c createGenericCircuit
56 * loads gates in BFS-from-root order (parents before children), so a
57 * single pass through Phase 1 would propagate identity-collapse in
58 * the wrong direction anyway -- the outer loop handles both effects
59 * with the same machinery. Terminates after at most one iteration
60 * per DAG level. */
61
62 /* Cumulative @c gate -> final-target redirection accumulated across
63 * iterations. We do NOT mutate a collapsed gate to carry its
64 * target's content (the naive content-copy approach): for a target
65 * that is a shared @c gate_input that would duplicate the Bernoulli variable
66 * into an independent copy under the wrong UUID, over-counting the
67 * probability of every non-read-once circuit (a single shared edge
68 * in a recursive reachability lineage, say). Instead we rewire each
69 * parent's wire straight to the target, leaving the collapsed gate
70 * orphaned, and -- after the loop -- patch the UUID map so a gate
71 * resolved by UUID (notably the caller-supplied root) follows the
72 * collapse rather than landing on the orphan. */
73 std::unordered_map<gate_t, gate_t> redirect;
74
75 bool changed = true;
76 while (changed) {
77 changed = false;
78
79 /* Phase 1: drop identity wires in place. Multiplicative identity
80 * is @c gate_one (drop from @c gate_times); additive identity is
81 * @c gate_zero (drop from @c gate_plus). If every wire of a
82 * @c gate_plus / @c gate_times was the identity, the gate is left
83 * with an empty wire list: collapse it to the identity (empty sum
84 * is @c gate_zero, empty product is @c gate_one) by mutating the
85 * gate type in place. */
86 for (std::size_t i = 0; i < n; ++i) {
87 auto g = static_cast<gate_t>(i);
88 auto t = gc.getGateType(g);
89 if (t != gate_times && t != gate_plus) continue;
90 gate_type identity = (t == gate_times) ? gate_one : gate_zero;
91 auto &wires = gc.getWires(g);
92 std::vector<gate_t> kept;
93 kept.reserve(wires.size());
94 for (gate_t c : wires) {
95 if (gc.getGateType(c) != identity) kept.push_back(c);
96 }
97 if (kept.size() != wires.size()) {
98 wires = std::move(kept);
99 changed = true;
100 }
101 if (wires.empty()) {
102 gc.setGateType(g, identity);
103 changed = true;
104 }
105 }
106
107 /* Phase 2: build a substitution map for gates that should be
108 * collapsed to a single descendant after phase 1:
109 * - @c gate_times that still has a @c gate_zero wire ->
110 * substitute to that absorber (multiplicative zero is the
111 * universal absorber across semirings);
112 * - any @c gate_times / @c gate_plus left with exactly one wire
113 * -> substitute to that wire (identity collapse).
114 * The plus-with-one absorber rewrite is intentionally omitted:
115 * @c gate_one is the additive absorber only in idempotent
116 * semirings (Boolean, MinMax) and would silently change the
117 * semantics for @c Counting / @c Formula / etc. Already-redirected
118 * (orphaned) gates are skipped so the fixpoint terminates. */
119 std::unordered_map<gate_t, gate_t> subst;
120 for (std::size_t i = 0; i < n; ++i) {
121 auto g = static_cast<gate_t>(i);
122 if (redirect.count(g)) continue;
123 auto t = gc.getGateType(g);
124 if (t != gate_times && t != gate_plus) continue;
125 const auto &wires = gc.getWires(g);
126 if (t == gate_times) {
127 bool absorbed = false;
128 for (gate_t c : wires) {
129 if (gc.getGateType(c) == gate_zero) {
130 subst[g] = c;
131 absorbed = true;
132 break;
133 }
134 }
135 if (absorbed) continue;
136 }
137 if (wires.size() == 1) subst[g] = wires[0];
138 }
139 /* Resolve transitively so a chain of singleton wrappers maps to
140 * its bottom endpoint in one lookup, chasing both this iteration's
141 * substitutions and earlier ones recorded in @c redirect. */
142 for (auto &kv : subst) {
143 gate_t cur = kv.second;
144 while (subst.count(cur) || redirect.count(cur))
145 cur = subst.count(cur) ? subst[cur] : redirect[cur];
146 kv.second = cur;
147 }
148
149 /* Phase 3: rewire every gate's wires through @c subst so parents
150 * point straight at the collapse target. Shared leaves stay a
151 * single gate (no content-copy, no UUID-aliased duplicate in the
152 * @c inputs set), so probability evaluators see the correct number
153 * of independent variables. Collapsed gates are left orphaned. */
154 if (!subst.empty()) {
155 for (std::size_t i = 0; i < n; ++i) {
156 auto &wires = gc.getWires(static_cast<gate_t>(i));
157 for (gate_t &w : wires) {
158 auto it = subst.find(w);
159 if (it != subst.end()) {
160 w = it->second;
161 changed = true;
162 }
163 }
164 }
165 for (const auto &kv : subst) redirect[kv.first] = kv.second;
166 }
167 ever |= changed;
168 }
169
170 /* Patch the UUID map and the Boolean-assumption set so a gate
171 * resolved by UUID (Circuit::getGate, used for the root and the
172 * joint-circuit roots) follows the collapse instead of landing on an
173 * orphaned wrapper, and so a non-Boolean semiring still refuses on a
174 * circuit whose marked gate was collapsed away. */
175 if (!redirect.empty()) {
176 for (auto &kv : redirect) {
177 gate_t cur = kv.second;
178 while (redirect.count(cur)) cur = redirect[cur];
179 kv.second = cur;
180 }
181 for (const auto &kv : redirect) {
182 gate_t g = kv.first, target = kv.second;
183 auto uit = id2uuid.find(g);
184 if (uit != id2uuid.end()) uuid2id[uit->second] = target;
185 if (boolean_assumed_gates.count(g)) boolean_assumed_gates.insert(target);
186 if (absorptive_assumed_gates.count(g))
187 absorptive_assumed_gates.insert(target);
188 }
189 }
190
191 return ever;
192}
193
195{
196 GenericCircuit &gc = *this;
197 bool fired = false;
198 const std::size_t n = gc.getNbGates();
199 for (std::size_t i = 0; i < n; ++i) {
200 auto g = static_cast<gate_t>(i);
201 auto t = gc.getGateType(g);
202 if (t != gate_plus && t != gate_times) continue;
203
204 /* Which class of rule fired on this gate decides the side-band
205 * marker: the absorptive rules are sound in every absorptive
206 * semiring (1 + a = 1, hence a + a = a and a + ab = a), while
207 * times-idempotence and times-absorbs-plus genuinely need the
208 * Boolean interpretation (both fail in tropical). */
209 bool absorptive_rule_fired = false;
210 bool boolean_rule_fired = false;
211
212 /* Rule 1 : idempotence. a + a = a is absorptive
213 * (a + a*1 = a); a * a = a is Boolean-only (tropical: a + a = 2a).
214 * Drop repeated child wires preserving order of first
215 * occurrence. */
216 if (t == gate_plus || boolean_level) {
217 auto &wires = gc.getWires(g);
218 std::unordered_set<gate_t> seen;
219 std::vector<gate_t> deduped;
220 deduped.reserve(wires.size());
221 for (gate_t c : wires) {
222 if (seen.insert(c).second) deduped.push_back(c);
223 }
224 if (deduped.size() != wires.size()) {
225 wires = std::move(deduped);
226 if (t == gate_plus)
227 absorptive_rule_fired = true;
228 else
229 boolean_rule_fired = true;
230 }
231 }
232
233 /* Rule 2 : plus-with-one absorber (the defining absorptive
234 * identity 1 + a = 1). Collapse
235 * @c gate_plus(..., @c gate_one, ...) directly to @c gate_one,
236 * preserving @p g's UUID. */
237 if (t == gate_plus) {
238 bool has_one = false;
239 for (gate_t c : gc.getWires(g)) {
240 if (gc.getGateType(c) == gate_one) { has_one = true; break; }
241 }
242 if (has_one) {
243 gc.setGateType(g, gate_one);
244 gc.setWires(g, std::vector<gate_t>{});
245 infos.erase(g);
246 extra.erase(g);
247 absorptive_rule_fired = true;
248 }
249 }
250
251 /* Rule 3 : absorption.
252 * gate_plus (x, gate_times(x, y, ...), ...) -> gate_plus (x, ...)
253 * -- the defining absorptive identity a + ab = a;
254 * gate_times(x, gate_plus (x, y, ...), ...) -> gate_times(x, ...)
255 * -- the lattice dual, Boolean-only (tropical:
256 * x + min(x, y) != x in general).
257 * The absorbed times / plus child is dominated by its sibling
258 * @c x present in the parent's children set. Implemented as a
259 * single pass : build a set view of the parent's children, then
260 * drop every opposite-type child whose wires intersect that set.
261 * The set view captures the wires snapshot ; in case B2 already
262 * mutated @p g into @c gate_one above, @c t still holds the
263 * pre-mutation type so absorption skips correctly via the
264 * type-mismatch check at the top. The dominating literal @c x is
265 * often only exposed as a direct sibling after a single-wire
266 * collapse (a diagonal @c gate_times(x, x) deduped by B1 then
267 * rewritten to @c x by @c foldSemiringIdentities) : the joint
268 * fixpoint in @c foldBooleanIdentities re-runs this sweep after
269 * each collapse so the exposed literal is seen on the next pass. */
270 if (t == gate_plus || boolean_level) {
271 const gate_type opposite = (t == gate_plus) ? gate_times : gate_plus;
272 const auto &wires_now = gc.getWires(g);
273 if (wires_now.size() >= 2) {
274 std::unordered_set<gate_t> sibling_set(wires_now.begin(),
275 wires_now.end());
276 std::vector<gate_t> kept;
277 kept.reserve(wires_now.size());
278 bool dropped = false;
279 for (gate_t c : wires_now) {
280 if (gc.getGateType(c) == opposite) {
281 bool absorb = false;
282 for (gate_t w : gc.getWires(c)) {
283 if (w != c && sibling_set.count(w)) {
284 absorb = true;
285 break;
286 }
287 }
288 if (absorb) { dropped = true; continue; }
289 }
290 kept.push_back(c);
291 }
292 if (dropped) {
293 gc.setWires(g, std::move(kept));
294 if (t == gate_plus)
295 absorptive_rule_fired = true;
296 else
297 boolean_rule_fired = true;
298 }
299 }
300 }
301
302 if (absorptive_rule_fired)
304 if (boolean_rule_fired)
306 if (absorptive_rule_fired || boolean_rule_fired)
307 fired = true;
308 }
309 return fired;
310}
311
313{
314 /* Interleave the Boolean rule sweep with the semiring-safe collapse
315 * to a JOINT fixpoint. Running the Boolean rules to their own
316 * fixpoint and only THEN collapsing once would miss absorptions
317 * whose dominating literal is exposed by a collapse: a diagonal
318 * @c gate_times(x, x) is deduped to a single-wire @c gate_times by
319 * B1, but only @c foldSemiringIdentities rewrites that wrapper to
320 * @c x, so a later B3 pass would never see @c x as a sibling. Alternating until neither pass changes anything closes
321 * that gap and leaves a circuit on which no rule applies.
322 *
323 * Termination: every rule (idempotence, plus-with-one, absorption,
324 * identity-drop, single-wire / empty collapse, zero-absorber) strictly
325 * removes at least one wire or gate and none ever adds one, so the
326 * well-founded (gates, wires) measure decreases; the loop exits only
327 * when both passes report no change, i.e. no rule applies to any gate.
328 * Collapses propagate one DAG level per iteration, so the sweep count
329 * is O(circuit depth) and each sweep is linear in the circuit size. */
330 bool changed = true;
331 while (changed) {
332 changed = applyFoldRuleSweep(true);
333 changed |= foldSemiringIdentities();
334 }
335}
336
338{
339 /* Same joint fixpoint as foldBooleanIdentities, restricted to the
340 * rules sound in every absorptive semiring; see the header note. */
341 bool changed = true;
342 while (changed) {
343 changed = applyFoldRuleSweep(false);
344 changed |= foldSemiringIdentities();
345 }
346}
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Semiring-agnostic in-memory provenance circuit.
std::string uuid
Definition Circuit.h:65
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:140
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
std::unordered_map< gate_t, uuid > id2uuid
Definition Circuit.h:69
virtual gate_t setGate(const uuid &u, gateType type)
Create or update the gate associated with UUID u.
Definition Circuit.hpp:73
std::unordered_map< uuid, gate_t > uuid2id
Definition Circuit.h:68
void setGateType(gate_t g, gateType t)
Update the type of an existing gate.
Definition Circuit.h:79
std::vector< std::vector< gate_t > > wires
Definition Circuit.h:72
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
virtual gate_t addGate()
Allocate a new gate with a default-initialised type.
Definition Circuit.hpp:56
In-memory provenance circuit with semiring-generic evaluation.
void setWires(gate_t g, std::vector< gate_t > w)
Replace the wires of g with w.
std::map< gate_t, std::pair< unsigned, unsigned > > infos
Per-gate (info1, info2) annotations.
void markAbsorptiveAssumed(gate_t g)
Mark gate g as absorptive-assumed (in-memory side band).
bool foldSemiringIdentities()
Drop semiring identity wires and collapse single-wire gate_times / gate_plus to their lone non-identi...
void foldAbsorptiveIdentities()
Absorptive-rules-only variant of foldBooleanIdentities(): the joint fixpoint of the absorptive fold r...
std::map< gate_t, std::string > extra
Per-gate string extras.
gate_t addGate() override
Allocate a new gate with a default-initialised type.
std::set< gate_t > inputs
Set of input (leaf) gate IDs.
std::set< gate_t > absorptive_assumed_gates
Side-band absorptive-assumption marker set by the absorptive fold rules (plus-idempotence,...
std::vector< double > prob
Per-gate probability values.
gate_t setGate(gate_type type) override
Allocate a new gate with type type and no UUID.
void markBooleanAssumed(gate_t g)
Mark gate g as Boolean-assumed (in-memory side band).
std::set< gate_t > boolean_assumed_gates
Side-band Boolean-assumption marker set by the Boolean-only fold rules ; an evaluator visiting a gate...
bool applyFoldRuleSweep(bool boolean_level)
One pass of the fold rules over every gate_plus / gate_times.
void foldBooleanIdentities()
Apply the Boolean-only AND the absorptive simplification rules to gate_plus and gate_times,...