Skip to main content

oxiz_math/
bdd.rs

1//! Binary Decision Diagrams (BDDs) for efficient boolean function representation.
2//!
3//! This module implements Reduced Ordered Binary Decision Diagrams (ROBDDs),
4//! which provide a canonical representation for boolean functions.
5//!
6//! Reference: Bryant, "Graph-Based Algorithms for Boolean Function Manipulation" (1986)
7
8#[allow(unused_imports)]
9use crate::prelude::*;
10use core::fmt;
11
12/// Variable identifier in a BDD.
13pub type VarId = u32;
14
15/// Node identifier in the BDD.
16/// Special values: 0 = FALSE, 1 = TRUE
17pub type NodeId = usize;
18
19/// The constant FALSE BDD node (node id 0).
20pub const BDD_FALSE: NodeId = 0;
21/// The constant TRUE BDD node (node id 1).
22pub const BDD_TRUE: NodeId = 1;
23
24/// A BDD node representing: if var then high else low
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26struct BddNode {
27    var: VarId,
28    low: NodeId,
29    high: NodeId,
30}
31
32/// A Binary Decision Diagram manager.
33/// Maintains a unique table to ensure node sharing and canonical representation.
34#[derive(Debug, Clone)]
35pub struct BddManager {
36    /// All nodes in the BDD. Index 0 is FALSE, index 1 is TRUE.
37    nodes: Vec<BddNode>,
38    /// Unique table mapping (var, low, high) to node ID.
39    unique_table: FxHashMap<(VarId, NodeId, NodeId), NodeId>,
40    /// Cache for binary operations: (op, left, right) -> result
41    op_cache: FxHashMap<(BddOp, NodeId, NodeId), NodeId>,
42    /// Computed table for negation: node -> !node.
43    ///
44    /// Without it, `not` re-expands every shared node: an n-variable BDD
45    /// has O(n) nodes but exponentially many tree unfoldings, which is the
46    /// entire point of a BDD's sharing.
47    not_cache: FxHashMap<NodeId, NodeId>,
48    /// Computed table for if-then-else: (cond, then, else) -> result.
49    ite_cache: FxHashMap<(NodeId, NodeId, NodeId), NodeId>,
50}
51
52/// One entry on the explicit work-stack used by [`BddManager::not`].
53enum NotStep {
54    /// Resolve `!node`, scheduling its cofactors if needed.
55    Enter(NodeId),
56    /// Both cofactors of `node` are memoized; build the result node.
57    Combine(NodeId),
58}
59
60/// One entry on the explicit work-stack used by [`BddManager::ite`].
61enum IteStep {
62    /// Resolve `ite(cond, then, else)` for this triple.
63    Enter(IteKey),
64    /// Both cofactor triples are memoized; build the result node.
65    Combine {
66        key: IteKey,
67        top_var: VarId,
68        low: IteKey,
69        high: IteKey,
70    },
71}
72
73/// An if-then-else triple, the key of the ITE computed table.
74type IteKey = (NodeId, NodeId, NodeId);
75
76/// An `(op, left, right)` triple, the key of the binary-operation
77/// computed table.
78type ApplyKey = (BddOp, NodeId, NodeId);
79
80/// One entry on the explicit work-stack used by [`BddManager::apply`].
81enum ApplyStep {
82    /// Resolve `op(left, right)` for this triple.
83    Enter(ApplyKey),
84    /// Both operand pairs are memoized; build the result node.
85    Combine {
86        key: ApplyKey,
87        var: VarId,
88        low: ApplyKey,
89        high: ApplyKey,
90    },
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94enum BddOp {
95    And,
96    Or,
97    Xor,
98}
99
100impl BddManager {
101    /// Creates a new BDD manager.
102    pub fn new() -> Self {
103        let mut manager = Self {
104            nodes: Vec::new(),
105            unique_table: FxHashMap::default(),
106            op_cache: FxHashMap::default(),
107            not_cache: FxHashMap::default(),
108            ite_cache: FxHashMap::default(),
109        };
110
111        // Initialize constant nodes
112        // FALSE node (id = 0): dummy node, never actually used for var/low/high
113        manager.nodes.push(BddNode {
114            var: u32::MAX,
115            low: 0,
116            high: 0,
117        });
118
119        // TRUE node (id = 1): dummy node, never actually used for var/low/high
120        manager.nodes.push(BddNode {
121            var: u32::MAX,
122            low: 1,
123            high: 1,
124        });
125
126        manager
127    }
128
129    /// Returns the FALSE constant.
130    pub fn constant_false(&self) -> NodeId {
131        BDD_FALSE
132    }
133
134    /// Returns the TRUE constant.
135    pub fn constant_true(&self) -> NodeId {
136        BDD_TRUE
137    }
138
139    /// Creates a BDD for a single variable.
140    pub fn variable(&mut self, var: VarId) -> NodeId {
141        self.make_node(var, BDD_FALSE, BDD_TRUE)
142    }
143
144    /// Creates or retrieves a BDD node.
145    /// Implements the reduction rules:
146    /// - If low == high, return low (redundancy elimination)
147    /// - Otherwise, check unique table for existing node
148    fn make_node(&mut self, var: VarId, low: NodeId, high: NodeId) -> NodeId {
149        // Reduction rule: if both branches are the same, return that branch
150        if low == high {
151            return low;
152        }
153
154        // Check if this node already exists
155        let key = (var, low, high);
156        if let Some(&node_id) = self.unique_table.get(&key) {
157            return node_id;
158        }
159
160        // Create new node
161        let node_id = self.nodes.len();
162        self.nodes.push(BddNode { var, low, high });
163        self.unique_table.insert(key, node_id);
164        node_id
165    }
166
167    /// Negates a BDD node.
168    ///
169    /// Runs on an explicit work-stack with a computed table (`not_cache`),
170    /// rather than the tree recursion this replaces. Two defects are fixed:
171    ///
172    /// * The recursion had no computed table, so every shared node was
173    ///   re-expanded. `not` on a 40-variable parity BDD — O(n) nodes, 2ⁿ
174    ///   tree unfoldings — never returned.
175    /// * Recursion depth was the BDD height, i.e. the variable count, with
176    ///   no guard. The return type is `NodeId`, so a depth cap could only
177    ///   have produced a silently wrong diagram.
178    pub fn not(&mut self, node: NodeId) -> NodeId {
179        if node == BDD_FALSE {
180            return BDD_TRUE;
181        }
182        if node == BDD_TRUE {
183            return BDD_FALSE;
184        }
185        if let Some(&result) = self.not_cache.get(&node) {
186            return result;
187        }
188
189        let mut stack = vec![NotStep::Enter(node)];
190        while let Some(step) = stack.pop() {
191            match step {
192                NotStep::Enter(id) => {
193                    if id == BDD_FALSE || id == BDD_TRUE || self.not_cache.contains_key(&id) {
194                        continue;
195                    }
196                    let n = self.nodes[id];
197                    stack.push(NotStep::Combine(id));
198                    stack.push(NotStep::Enter(n.low));
199                    stack.push(NotStep::Enter(n.high));
200                }
201                NotStep::Combine(id) => {
202                    if self.not_cache.contains_key(&id) {
203                        continue;
204                    }
205                    let n = self.nodes[id];
206                    let low = self.negated(n.low);
207                    let high = self.negated(n.high);
208                    let result = self.make_node(n.var, low, high);
209                    self.not_cache.insert(id, result);
210                }
211            }
212        }
213
214        self.not_cache[&node]
215    }
216
217    /// Negation of an already-resolved node: the two constants plus a
218    /// computed-table hit. Panics (loudly, via the map index) rather than
219    /// returning a wrong diagram if called on a node the walk has not yet
220    /// resolved — the driver above always schedules cofactors first.
221    fn negated(&self, node: NodeId) -> NodeId {
222        if node == BDD_FALSE {
223            return BDD_TRUE;
224        }
225        if node == BDD_TRUE {
226            return BDD_FALSE;
227        }
228        self.not_cache[&node]
229    }
230
231    /// Computes the AND of two BDD nodes.
232    pub fn and(&mut self, left: NodeId, right: NodeId) -> NodeId {
233        self.apply(BddOp::And, left, right)
234    }
235
236    /// Computes the OR of two BDD nodes.
237    pub fn or(&mut self, left: NodeId, right: NodeId) -> NodeId {
238        self.apply(BddOp::Or, left, right)
239    }
240
241    /// Computes the XOR of two BDD nodes.
242    pub fn xor(&mut self, left: NodeId, right: NodeId) -> NodeId {
243        self.apply(BddOp::Xor, left, right)
244    }
245
246    /// Computes the implication: left => right (equivalent to !left | right)
247    pub fn implies(&mut self, left: NodeId, right: NodeId) -> NodeId {
248        let not_left = self.not(left);
249        self.or(not_left, right)
250    }
251
252    /// Computes the equivalence: left <=> right
253    pub fn iff(&mut self, left: NodeId, right: NodeId) -> NodeId {
254        let not_xor = self.xor(left, right);
255        self.not(not_xor)
256    }
257
258    /// Apply a binary operation with memoization.
259    ///
260    /// Like [`Self::not`] and [`Self::ite`], this runs on an explicit
261    /// work-stack. The computed table (`op_cache`) already kept the *work*
262    /// linear in diagram size, but the recursion depth was still the BDD
263    /// height — the variable count — with no guard and a `NodeId` return
264    /// that leaves nowhere to report a cap.
265    fn apply(&mut self, op: BddOp, left: NodeId, right: NodeId) -> NodeId {
266        let root = (op, left, right);
267        if let Some(result) = self.apply_resolved(root) {
268            return result;
269        }
270
271        let mut stack = vec![ApplyStep::Enter(root)];
272        while let Some(step) = stack.pop() {
273            match step {
274                ApplyStep::Enter(key) => {
275                    if self.apply_resolved(key).is_some() {
276                        continue;
277                    }
278                    let (var, low, high) = self.apply_cofactors(key);
279                    stack.push(ApplyStep::Combine {
280                        key,
281                        var,
282                        low,
283                        high,
284                    });
285                    stack.push(ApplyStep::Enter(low));
286                    stack.push(ApplyStep::Enter(high));
287                }
288                ApplyStep::Combine {
289                    key,
290                    var,
291                    low,
292                    high,
293                } => {
294                    if self.op_cache.contains_key(&key) {
295                        continue;
296                    }
297                    // Both operand pairs were scheduled above this frame.
298                    let (Some(low_id), Some(high_id)) =
299                        (self.apply_resolved(low), self.apply_resolved(high))
300                    else {
301                        continue;
302                    };
303                    let result = self.make_node(var, low_id, high_id);
304                    self.op_cache.insert(key, result);
305                }
306            }
307        }
308
309        self.op_cache[&root]
310    }
311
312    /// The value of an `(op, left, right)` triple if it is already known:
313    /// an operator terminal case, or a computed-table hit.
314    fn apply_resolved(&mut self, (op, left, right): ApplyKey) -> Option<NodeId> {
315        // Terminal cases
316        match op {
317            BddOp::And => {
318                if left == BDD_FALSE || right == BDD_FALSE {
319                    return Some(BDD_FALSE);
320                }
321                if left == BDD_TRUE {
322                    return Some(right);
323                }
324                if right == BDD_TRUE {
325                    return Some(left);
326                }
327                if left == right {
328                    return Some(left);
329                }
330            }
331            BddOp::Or => {
332                if left == BDD_TRUE || right == BDD_TRUE {
333                    return Some(BDD_TRUE);
334                }
335                if left == BDD_FALSE {
336                    return Some(right);
337                }
338                if right == BDD_FALSE {
339                    return Some(left);
340                }
341                if left == right {
342                    return Some(left);
343                }
344            }
345            BddOp::Xor => {
346                if left == BDD_FALSE {
347                    return Some(right);
348                }
349                if right == BDD_FALSE {
350                    return Some(left);
351                }
352                if left == BDD_TRUE {
353                    return Some(self.not(right));
354                }
355                if right == BDD_TRUE {
356                    return Some(self.not(left));
357                }
358                if left == right {
359                    return Some(BDD_FALSE);
360                }
361            }
362        }
363
364        self.op_cache.get(&(op, left, right)).copied()
365    }
366
367    /// Shannon expansion of a non-terminal `apply` triple: the branching
368    /// variable and the two operand pairs. Pure code motion out of the
369    /// former recursive `apply` body.
370    fn apply_cofactors(&self, (op, left, right): ApplyKey) -> (VarId, ApplyKey, ApplyKey) {
371        let left_node = self.nodes[left];
372        let right_node = self.nodes[right];
373
374        if left_node.var == right_node.var {
375            // Same variable
376            (
377                left_node.var,
378                (op, left_node.low, right_node.low),
379                (op, left_node.high, right_node.high),
380            )
381        } else if left_node.var < right_node.var {
382            // Left has smaller variable (higher in ordering)
383            (
384                left_node.var,
385                (op, left_node.low, right),
386                (op, left_node.high, right),
387            )
388        } else {
389            // Right has smaller variable
390            (
391                right_node.var,
392                (op, left, right_node.low),
393                (op, left, right_node.high),
394            )
395        }
396    }
397
398    /// If-then-else operation: if cond then then_node else else_node.
399    ///
400    /// Runs on an explicit work-stack with the standard ITE computed table
401    /// keyed on the `(cond, then, else)` triple. The recursion this replaces
402    /// had neither: with triple fan-out and no memo, shared cofactors of the
403    /// hash-consed diagram were re-expanded exponentially, and the depth was
404    /// the (unguarded) variable count. `NodeId` has no error channel, so a
405    /// depth cap could only have returned a silently wrong diagram.
406    pub fn ite(&mut self, cond: NodeId, then_node: NodeId, else_node: NodeId) -> NodeId {
407        let root: IteKey = (cond, then_node, else_node);
408        if let Some(result) = self.ite_resolved(root) {
409            return result;
410        }
411
412        let mut stack = vec![IteStep::Enter(root)];
413        while let Some(step) = stack.pop() {
414            match step {
415                IteStep::Enter(key) => {
416                    if self.ite_resolved(key).is_some() {
417                        continue;
418                    }
419                    let (top_var, low, high) = self.ite_cofactors(key);
420                    stack.push(IteStep::Combine {
421                        key,
422                        top_var,
423                        low,
424                        high,
425                    });
426                    stack.push(IteStep::Enter(low));
427                    stack.push(IteStep::Enter(high));
428                }
429                IteStep::Combine {
430                    key,
431                    top_var,
432                    low,
433                    high,
434                } => {
435                    if self.ite_cache.contains_key(&key) {
436                        continue;
437                    }
438                    // Both cofactor triples were scheduled above this frame,
439                    // so both are resolved by now.
440                    let (Some(low_id), Some(high_id)) =
441                        (self.ite_resolved(low), self.ite_resolved(high))
442                    else {
443                        continue;
444                    };
445                    let result = self.make_node(top_var, low_id, high_id);
446                    self.ite_cache.insert(key, result);
447                }
448            }
449        }
450
451        self.ite_cache[&root]
452    }
453
454    /// The value of an ITE triple if it is already known: a terminal case,
455    /// or a computed-table hit.
456    fn ite_resolved(&self, (cond, then_node, else_node): IteKey) -> Option<NodeId> {
457        if cond == BDD_TRUE {
458            return Some(then_node);
459        }
460        if cond == BDD_FALSE {
461            return Some(else_node);
462        }
463        if then_node == else_node {
464            return Some(then_node);
465        }
466        if then_node == BDD_TRUE && else_node == BDD_FALSE {
467            return Some(cond);
468        }
469        self.ite_cache.get(&(cond, then_node, else_node)).copied()
470    }
471
472    /// Shannon expansion of a non-terminal ITE triple: the top variable and
473    /// the two cofactor triples. Pure code motion out of the former
474    /// recursive `ite` body.
475    fn ite_cofactors(&self, (cond, then_node, else_node): IteKey) -> (VarId, IteKey, IteKey) {
476        let cond_node = self.nodes[cond];
477        let then_n = if then_node <= BDD_TRUE {
478            None
479        } else {
480            Some(self.nodes[then_node])
481        };
482        let else_n = if else_node <= BDD_TRUE {
483            None
484        } else {
485            Some(self.nodes[else_node])
486        };
487
488        // Find the top variable
489        let mut top_var = cond_node.var;
490        if let Some(n) = then_n
491            && n.var < top_var
492        {
493            top_var = n.var;
494        }
495        if let Some(n) = else_n
496            && n.var < top_var
497        {
498            top_var = n.var;
499        }
500
501        // Cofactors
502        let cond_low = if cond_node.var == top_var {
503            cond_node.low
504        } else {
505            cond
506        };
507        let cond_high = if cond_node.var == top_var {
508            cond_node.high
509        } else {
510            cond
511        };
512
513        let then_low = if let Some(n) = then_n {
514            if n.var == top_var { n.low } else { then_node }
515        } else {
516            then_node
517        };
518        let then_high = if let Some(n) = then_n {
519            if n.var == top_var { n.high } else { then_node }
520        } else {
521            then_node
522        };
523
524        let else_low = if let Some(n) = else_n {
525            if n.var == top_var { n.low } else { else_node }
526        } else {
527            else_node
528        };
529        let else_high = if let Some(n) = else_n {
530            if n.var == top_var { n.high } else { else_node }
531        } else {
532            else_node
533        };
534
535        (
536            top_var,
537            (cond_low, then_low, else_low),
538            (cond_high, then_high, else_high),
539        )
540    }
541
542    /// Evaluates a BDD with a given variable assignment.
543    pub fn eval(&self, node: NodeId, assignment: &FxHashMap<VarId, bool>) -> bool {
544        if node == BDD_FALSE {
545            return false;
546        }
547        if node == BDD_TRUE {
548            return true;
549        }
550
551        let n = self.nodes[node];
552        let var_value = assignment.get(&n.var).copied().unwrap_or(false);
553
554        if var_value {
555            self.eval(n.high, assignment)
556        } else {
557            self.eval(n.low, assignment)
558        }
559    }
560
561    /// Counts the number of satisfying assignments for a BDD.
562    pub fn sat_count(&self, node: NodeId, num_vars: u32) -> u64 {
563        let mut cache = FxHashMap::default();
564        self.sat_count_rec(node, &mut cache)
565            * (1u64 << num_vars.saturating_sub(self.max_var(node).map_or(0, |v| v + 1)))
566    }
567
568    fn sat_count_rec(&self, node: NodeId, cache: &mut FxHashMap<NodeId, u64>) -> u64 {
569        if node == BDD_FALSE {
570            return 0;
571        }
572        if node == BDD_TRUE {
573            return 1;
574        }
575
576        if let Some(&count) = cache.get(&node) {
577            return count;
578        }
579
580        let n = self.nodes[node];
581        let low_count = self.sat_count_rec(n.low, cache);
582        let high_count = self.sat_count_rec(n.high, cache);
583
584        // Account for the difference in variable levels
585        let low_mult = if n.low <= BDD_TRUE {
586            1
587        } else {
588            let low_var = self.nodes[n.low].var;
589            1u64 << (low_var - n.var - 1)
590        };
591        let high_mult = if n.high <= BDD_TRUE {
592            1
593        } else {
594            let high_var = self.nodes[n.high].var;
595            1u64 << (high_var - n.var - 1)
596        };
597
598        let count = low_count * low_mult + high_count * high_mult;
599        cache.insert(node, count);
600        count
601    }
602
603    /// Returns the maximum variable ID in a BDD.
604    fn max_var(&self, node: NodeId) -> Option<VarId> {
605        if node <= BDD_TRUE {
606            return None;
607        }
608        Some(self.nodes[node].var)
609    }
610
611    /// Returns the number of nodes in the manager.
612    pub fn node_count(&self) -> usize {
613        self.nodes.len()
614    }
615
616    /// Clears the operation caches (useful for memory management).
617    pub fn clear_cache(&mut self) {
618        self.op_cache.clear();
619        self.not_cache.clear();
620        self.ite_cache.clear();
621    }
622}
623
624impl Default for BddManager {
625    fn default() -> Self {
626        Self::new()
627    }
628}
629
630impl fmt::Display for BddManager {
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        writeln!(f, "BDD Manager: {} nodes", self.nodes.len())?;
633        writeln!(f, "  Cache size: {}", self.op_cache.len())?;
634        Ok(())
635    }
636}
637
638/// Zero-suppressed Binary Decision Diagram (ZDD) manager.
639/// ZDDs use a different reduction rule optimized for sparse sets:
640/// - Eliminate node if high == 0 (instead of low == high for BDDs)
641///
642/// ZDDs are particularly efficient for representing:
643/// - Sparse sets and families of sets
644/// - Combinatorial structures
645/// - Paths in graphs
646#[derive(Debug, Clone)]
647pub struct ZddManager {
648    nodes: Vec<BddNode>,
649    unique_table: FxHashMap<(VarId, NodeId, NodeId), NodeId>,
650    op_cache: FxHashMap<(ZddOp, NodeId, NodeId), NodeId>,
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
654enum ZddOp {
655    Union,
656    Intersection,
657    Difference,
658}
659
660/// The empty set in ZDD.
661pub const ZDD_EMPTY: NodeId = 0;
662/// The set containing only the empty set in ZDD.
663pub const ZDD_BASE: NodeId = 1;
664
665impl ZddManager {
666    /// Creates a new ZDD manager.
667    pub fn new() -> Self {
668        let mut manager = Self {
669            nodes: Vec::new(),
670            unique_table: FxHashMap::default(),
671            op_cache: FxHashMap::default(),
672        };
673
674        // EMPTY set (0)
675        manager.nodes.push(BddNode {
676            var: u32::MAX,
677            low: 0,
678            high: 0,
679        });
680
681        // BASE set containing empty set (1)
682        manager.nodes.push(BddNode {
683            var: u32::MAX,
684            low: 1,
685            high: 1,
686        });
687
688        manager
689    }
690
691    /// Returns the empty set.
692    pub fn empty(&self) -> NodeId {
693        ZDD_EMPTY
694    }
695
696    /// Returns the base set (set containing only the empty set).
697    pub fn base(&self) -> NodeId {
698        ZDD_BASE
699    }
700
701    /// Creates a ZDD for a singleton set containing just one variable.
702    pub fn singleton(&mut self, var: VarId) -> NodeId {
703        self.make_node(var, ZDD_EMPTY, ZDD_BASE)
704    }
705
706    /// Creates or retrieves a ZDD node with ZDD reduction rule.
707    /// ZDD reduction: If high == 0 (empty), return low
708    fn make_node(&mut self, var: VarId, low: NodeId, high: NodeId) -> NodeId {
709        // ZDD reduction rule: if high branch is empty, eliminate node
710        if high == ZDD_EMPTY {
711            return low;
712        }
713
714        // Check if node exists
715        let key = (var, low, high);
716        if let Some(&node_id) = self.unique_table.get(&key) {
717            return node_id;
718        }
719
720        // Create new node
721        let node_id = self.nodes.len();
722        self.nodes.push(BddNode { var, low, high });
723        self.unique_table.insert(key, node_id);
724        node_id
725    }
726
727    /// Computes the union of two ZDD sets.
728    pub fn union(&mut self, left: NodeId, right: NodeId) -> NodeId {
729        self.apply(ZddOp::Union, left, right)
730    }
731
732    /// Computes the intersection of two ZDD sets.
733    pub fn intersection(&mut self, left: NodeId, right: NodeId) -> NodeId {
734        self.apply(ZddOp::Intersection, left, right)
735    }
736
737    /// Computes the set difference: left \ right
738    pub fn difference(&mut self, left: NodeId, right: NodeId) -> NodeId {
739        self.apply(ZddOp::Difference, left, right)
740    }
741
742    fn apply(&mut self, op: ZddOp, left: NodeId, right: NodeId) -> NodeId {
743        // Terminal cases
744        match op {
745            ZddOp::Union => {
746                if left == ZDD_EMPTY {
747                    return right;
748                }
749                if right == ZDD_EMPTY {
750                    return left;
751                }
752                if left == right {
753                    return left;
754                }
755            }
756            ZddOp::Intersection => {
757                if left == ZDD_EMPTY || right == ZDD_EMPTY {
758                    return ZDD_EMPTY;
759                }
760                if left == right {
761                    return left;
762                }
763                // BASE only intersects with itself
764                if left == ZDD_BASE || right == ZDD_BASE {
765                    return ZDD_EMPTY;
766                }
767            }
768            ZddOp::Difference => {
769                if left == ZDD_EMPTY {
770                    return ZDD_EMPTY;
771                }
772                if right == ZDD_EMPTY {
773                    return left;
774                }
775                if left == right {
776                    return ZDD_EMPTY;
777                }
778            }
779        }
780
781        // Check cache
782        let cache_key = (op, left, right);
783        if let Some(&result) = self.op_cache.get(&cache_key) {
784            return result;
785        }
786
787        // Get top variable
788        let left_node = self.nodes[left];
789        let right_node = self.nodes[right];
790
791        let result = if left_node.var == right_node.var {
792            let low = self.apply(op, left_node.low, right_node.low);
793            let high = self.apply(op, left_node.high, right_node.high);
794            self.make_node(left_node.var, low, high)
795        } else if left_node.var < right_node.var {
796            let low = self.apply(op, left_node.low, right);
797            let high = match op {
798                ZddOp::Union => left_node.high,
799                ZddOp::Intersection => self.apply(op, left_node.high, right),
800                ZddOp::Difference => left_node.high,
801            };
802            self.make_node(left_node.var, low, high)
803        } else {
804            let low = self.apply(op, left, right_node.low);
805            let high = match op {
806                ZddOp::Union => right_node.high,
807                ZddOp::Intersection => self.apply(op, left, right_node.high),
808                ZddOp::Difference => ZDD_EMPTY,
809            };
810            self.make_node(right_node.var, low, high)
811        };
812
813        self.op_cache.insert(cache_key, result);
814        result
815    }
816
817    /// Returns the number of sets in the family represented by this ZDD.
818    pub fn count(&self, node: NodeId) -> u64 {
819        let mut cache = FxHashMap::default();
820        self.count_rec(node, &mut cache)
821    }
822
823    fn count_rec(&self, node: NodeId, cache: &mut FxHashMap<NodeId, u64>) -> u64 {
824        if node == ZDD_EMPTY {
825            return 0;
826        }
827        if node == ZDD_BASE {
828            return 1;
829        }
830
831        if let Some(&count) = cache.get(&node) {
832            return count;
833        }
834
835        let n = self.nodes[node];
836        let count = self.count_rec(n.low, cache) + self.count_rec(n.high, cache);
837        cache.insert(node, count);
838        count
839    }
840
841    /// Returns the number of nodes in the manager.
842    pub fn node_count(&self) -> usize {
843        self.nodes.len()
844    }
845
846    /// Clears the operation cache.
847    pub fn clear_cache(&mut self) {
848        self.op_cache.clear();
849    }
850}
851
852impl Default for ZddManager {
853    fn default() -> Self {
854        Self::new()
855    }
856}
857
858impl fmt::Display for ZddManager {
859    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
860        writeln!(f, "ZDD Manager: {} nodes", self.nodes.len())?;
861        writeln!(f, "  Cache size: {}", self.op_cache.len())?;
862        Ok(())
863    }
864}
865
866/// Algebraic Decision Diagram (ADD) for arithmetic functions.
867/// ADDs generalize BDDs by allowing arbitrary terminal values (not just 0/1).
868/// They can represent functions from boolean vectors to any algebraic domain.
869///
870/// Reference: Bahar et al., "Algebraic Decision Diagrams and Their Applications" (1993)
871use num_rational::BigRational;
872use num_traits::Zero;
873
874/// An ADD node with rational number terminals.
875#[derive(Debug, Clone, PartialEq, Eq, Hash)]
876enum AddNode {
877    /// Terminal node with a rational value.
878    Terminal(BigRational),
879    /// Internal node: if var then high else low
880    Internal {
881        var: VarId,
882        low: NodeId,
883        high: NodeId,
884    },
885}
886
887/// ADD manager for rational-valued functions.
888#[derive(Debug, Clone)]
889pub struct AddManager {
890    nodes: Vec<AddNode>,
891    unique_table: FxHashMap<AddNode, NodeId>,
892    op_cache: FxHashMap<(AddOp, NodeId, NodeId), NodeId>,
893}
894
895#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
896enum AddOp {
897    Add,
898    Mul,
899    Max,
900    Min,
901}
902
903impl AddManager {
904    /// Creates a new ADD manager.
905    pub fn new() -> Self {
906        let mut manager = Self {
907            nodes: Vec::new(),
908            unique_table: FxHashMap::default(),
909            op_cache: FxHashMap::default(),
910        };
911
912        // Create zero terminal
913        let zero = AddNode::Terminal(BigRational::zero());
914        manager.nodes.push(zero.clone());
915        manager.unique_table.insert(zero, 0);
916
917        manager
918    }
919
920    /// Creates a constant ADD.
921    pub fn constant(&mut self, value: BigRational) -> NodeId {
922        let node = AddNode::Terminal(value);
923
924        // Check if this terminal already exists
925        if let Some(&id) = self.unique_table.get(&node) {
926            return id;
927        }
928
929        let id = self.nodes.len();
930        self.nodes.push(node.clone());
931        self.unique_table.insert(node, id);
932        id
933    }
934
935    /// Creates an ADD for a single variable (returns 1 if true, 0 if false).
936    pub fn variable(&mut self, var: VarId) -> NodeId {
937        let zero = self.constant(BigRational::zero());
938        let one = self.constant(BigRational::from_integer(1.into()));
939        self.make_node(var, zero, one)
940    }
941
942    /// Creates or retrieves an ADD node.
943    fn make_node(&mut self, var: VarId, low: NodeId, high: NodeId) -> NodeId {
944        // Reduction rule: if both branches are the same, return that branch
945        if low == high {
946            return low;
947        }
948
949        let node = AddNode::Internal { var, low, high };
950
951        // Check if this node already exists
952        if let Some(&id) = self.unique_table.get(&node) {
953            return id;
954        }
955
956        // Create new node
957        let id = self.nodes.len();
958        self.nodes.push(node.clone());
959        self.unique_table.insert(node, id);
960        id
961    }
962
963    /// Evaluates the ADD given a variable assignment.
964    pub fn eval(&self, node: NodeId, assignment: &FxHashMap<VarId, bool>) -> BigRational {
965        match &self.nodes[node] {
966            AddNode::Terminal(value) => value.clone(),
967            AddNode::Internal { var, low, high } => {
968                if *assignment.get(var).unwrap_or(&false) {
969                    self.eval(*high, assignment)
970                } else {
971                    self.eval(*low, assignment)
972                }
973            }
974        }
975    }
976
977    /// Computes the sum of two ADDs.
978    pub fn add(&mut self, left: NodeId, right: NodeId) -> NodeId {
979        self.apply(AddOp::Add, left, right)
980    }
981
982    /// Computes the product of two ADDs.
983    pub fn mul(&mut self, left: NodeId, right: NodeId) -> NodeId {
984        self.apply(AddOp::Mul, left, right)
985    }
986
987    /// Computes the maximum of two ADDs.
988    pub fn max(&mut self, left: NodeId, right: NodeId) -> NodeId {
989        self.apply(AddOp::Max, left, right)
990    }
991
992    /// Computes the minimum of two ADDs.
993    pub fn min(&mut self, left: NodeId, right: NodeId) -> NodeId {
994        self.apply(AddOp::Min, left, right)
995    }
996
997    /// Multiplies an ADD by a scalar.
998    pub fn scale(&mut self, node: NodeId, scalar: &BigRational) -> NodeId {
999        let scalar_node = self.constant(scalar.clone());
1000        self.mul(node, scalar_node)
1001    }
1002
1003    /// Apply a binary operation with memoization.
1004    fn apply(&mut self, op: AddOp, left: NodeId, right: NodeId) -> NodeId {
1005        // Terminal cases
1006        if let (AddNode::Terminal(l), AddNode::Terminal(r)) =
1007            (&self.nodes[left], &self.nodes[right])
1008        {
1009            let result_value = match op {
1010                AddOp::Add => l + r,
1011                AddOp::Mul => l * r,
1012                AddOp::Max => {
1013                    if l > r {
1014                        l.clone()
1015                    } else {
1016                        r.clone()
1017                    }
1018                }
1019                AddOp::Min => {
1020                    if l < r {
1021                        l.clone()
1022                    } else {
1023                        r.clone()
1024                    }
1025                }
1026            };
1027            return self.constant(result_value);
1028        }
1029
1030        // Check cache
1031        let cache_key = (op, left, right);
1032        if let Some(&result) = self.op_cache.get(&cache_key) {
1033            return result;
1034        }
1035
1036        // Recursive case
1037        let (var, left_low, left_high, right_low, right_high) = self.split_nodes(left, right);
1038
1039        let low = self.apply(op, left_low, right_low);
1040        let high = self.apply(op, left_high, right_high);
1041
1042        let result = self.make_node(var, low, high);
1043        self.op_cache.insert(cache_key, result);
1044        result
1045    }
1046
1047    /// Helper function to split nodes for apply operation.
1048    fn split_nodes(&self, left: NodeId, right: NodeId) -> (VarId, NodeId, NodeId, NodeId, NodeId) {
1049        let (left_var, left_low, left_high) = match &self.nodes[left] {
1050            AddNode::Terminal(_) => (u32::MAX, left, left),
1051            AddNode::Internal { var, low, high } => (*var, *low, *high),
1052        };
1053
1054        let (right_var, right_low, right_high) = match &self.nodes[right] {
1055            AddNode::Terminal(_) => (u32::MAX, right, right),
1056            AddNode::Internal { var, low, high } => (*var, *low, *high),
1057        };
1058
1059        if left_var == u32::MAX && right_var == u32::MAX {
1060            panic!("Both nodes are terminals - should have been handled earlier");
1061        }
1062
1063        if left_var == right_var {
1064            (left_var, left_low, left_high, right_low, right_high)
1065        } else if left_var < right_var {
1066            (left_var, left_low, left_high, right, right)
1067        } else {
1068            (right_var, left, left, right_low, right_high)
1069        }
1070    }
1071
1072    /// If-then-else operation for ADDs.
1073    pub fn ite(&mut self, cond: NodeId, then_add: NodeId, else_add: NodeId) -> NodeId {
1074        // Convert boolean condition to ADD if needed
1075        // ite(c, t, e) = c * t + (1 - c) * e
1076        let one = self.constant(BigRational::from_integer(1.into()));
1077        let neg_one = BigRational::from_integer((-1).into());
1078        let scaled_cond = self.scale(cond, &neg_one);
1079        let not_cond = self.apply(AddOp::Add, one, scaled_cond);
1080
1081        let then_part = self.mul(cond, then_add);
1082        let else_part = self.mul(not_cond, else_add);
1083        self.add(then_part, else_part)
1084    }
1085
1086    /// Returns the number of nodes in the manager.
1087    pub fn node_count(&self) -> usize {
1088        self.nodes.len()
1089    }
1090
1091    /// Clears the operation cache.
1092    pub fn clear_cache(&mut self) {
1093        self.op_cache.clear();
1094    }
1095
1096    /// Gets the variable of a node, if it's internal.
1097    pub fn get_var(&self, node: NodeId) -> Option<VarId> {
1098        match &self.nodes[node] {
1099            AddNode::Terminal(_) => None,
1100            AddNode::Internal { var, .. } => Some(*var),
1101        }
1102    }
1103
1104    /// Gets the terminal value of a node, if it's a terminal.
1105    pub fn get_value(&self, node: NodeId) -> Option<&BigRational> {
1106        match &self.nodes[node] {
1107            AddNode::Terminal(value) => Some(value),
1108            AddNode::Internal { .. } => None,
1109        }
1110    }
1111}
1112
1113impl Default for AddManager {
1114    fn default() -> Self {
1115        Self::new()
1116    }
1117}
1118
1119impl fmt::Display for AddManager {
1120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1121        writeln!(f, "ADD Manager: {} nodes", self.nodes.len())?;
1122        writeln!(f, "  Cache size: {}", self.op_cache.len())?;
1123        Ok(())
1124    }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129    use super::*;
1130
1131    #[test]
1132    fn test_bdd_constants() {
1133        let manager = BddManager::new();
1134        assert_eq!(manager.constant_false(), BDD_FALSE);
1135        assert_eq!(manager.constant_true(), BDD_TRUE);
1136    }
1137
1138    #[test]
1139    fn test_bdd_variable() {
1140        let mut manager = BddManager::new();
1141        let x = manager.variable(0);
1142
1143        let mut assignment = FxHashMap::default();
1144        assignment.insert(0, true);
1145        assert!(manager.eval(x, &assignment));
1146
1147        assignment.insert(0, false);
1148        assert!(!manager.eval(x, &assignment));
1149    }
1150
1151    /// Build the conjunction `x0 ∧ x1 ∧ … ∧ x_{n-1}` directly, bottom-up.
1152    /// Going through `and` would be O(n²); `make_node` gives an n-node,
1153    /// n-high diagram in O(n).
1154    fn conjunction_chain(manager: &mut BddManager, vars: u32) -> NodeId {
1155        let mut node = BDD_TRUE;
1156        for var in (0..vars).rev() {
1157            node = manager.make_node(var, BDD_FALSE, node);
1158        }
1159        node
1160    }
1161
1162    /// A 100_000-variable-high BDD negated on a 1 MiB stack. The recursive
1163    /// form descended once per level and aborted the process; returning at
1164    /// all is the assertion. Double negation pins the result.
1165    #[test]
1166    fn test_not_deep_diagram_does_not_overflow() {
1167        let worker = std::thread::Builder::new().stack_size(1 << 20).spawn(|| {
1168            let mut manager = BddManager::new();
1169            let chain = conjunction_chain(&mut manager, 100_000);
1170            let negated = manager.not(chain);
1171            (negated != chain, manager.not(negated) == chain)
1172        });
1173        let (differs, involutive) = match worker.map(std::thread::JoinHandle::join) {
1174            Ok(Ok(result)) => result,
1175            _ => panic!("deep-BDD negation worker thread did not complete"),
1176        };
1177        assert!(differs);
1178        assert!(involutive);
1179    }
1180
1181    /// Same depth, through `apply` (`and`) and `ite`.
1182    #[test]
1183    fn test_apply_and_ite_deep_diagram_do_not_overflow() {
1184        let worker = std::thread::Builder::new().stack_size(1 << 20).spawn(|| {
1185            let mut manager = BddManager::new();
1186            let chain = conjunction_chain(&mut manager, 100_000);
1187            let shifted = {
1188                let mut node = BDD_TRUE;
1189                for var in (0..100_000u32).rev() {
1190                    node = manager.make_node(var, node, BDD_FALSE);
1191                }
1192                node
1193            };
1194            let conj = manager.and(chain, shifted);
1195            let selected = manager.ite(chain, shifted, BDD_FALSE);
1196            (conj, selected)
1197        });
1198        let (conj, selected) = match worker.map(std::thread::JoinHandle::join) {
1199            Ok(Ok(result)) => result,
1200            _ => panic!("deep-BDD apply/ite worker thread did not complete"),
1201        };
1202        // `chain` requires every variable true, `shifted` every variable
1203        // false, so both operations yield the empty function.
1204        assert_eq!(conj, BDD_FALSE);
1205        assert_eq!(selected, BDD_FALSE);
1206    }
1207
1208    /// A 40-variable parity (XOR) BDD has ~80 nodes but 2^40 tree
1209    /// unfoldings. Without a computed table `not` re-expands every shared
1210    /// node and never returns; this test simply has to finish.
1211    #[test]
1212    fn test_not_on_parity_bdd_uses_computed_table() {
1213        let mut manager = BddManager::new();
1214        let mut parity = BDD_FALSE;
1215        for var in 0..40 {
1216            let x = manager.variable(var);
1217            parity = manager.xor(parity, x);
1218        }
1219        let negated = manager.not(parity);
1220        assert_ne!(negated, parity);
1221        assert_eq!(manager.not(negated), parity);
1222
1223        // Semantic pin: !parity(x) == parity(x) xor 1.
1224        let complement = manager.xor(parity, BDD_TRUE);
1225        assert_eq!(negated, complement);
1226    }
1227
1228    #[test]
1229    fn test_bdd_not() {
1230        let mut manager = BddManager::new();
1231        let x = manager.variable(0);
1232        let not_x = manager.not(x);
1233
1234        let mut assignment = FxHashMap::default();
1235        assignment.insert(0, true);
1236        assert!(!manager.eval(not_x, &assignment));
1237
1238        assignment.insert(0, false);
1239        assert!(manager.eval(not_x, &assignment));
1240    }
1241
1242    #[test]
1243    fn test_bdd_and() {
1244        let mut manager = BddManager::new();
1245        let x = manager.variable(0);
1246        let y = manager.variable(1);
1247        let x_and_y = manager.and(x, y);
1248
1249        let mut assignment = FxHashMap::default();
1250        assignment.insert(0, true);
1251        assignment.insert(1, true);
1252        assert!(manager.eval(x_and_y, &assignment));
1253
1254        assignment.insert(0, false);
1255        assert!(!manager.eval(x_and_y, &assignment));
1256    }
1257
1258    #[test]
1259    fn test_bdd_or() {
1260        let mut manager = BddManager::new();
1261        let x = manager.variable(0);
1262        let y = manager.variable(1);
1263        let x_or_y = manager.or(x, y);
1264
1265        let mut assignment = FxHashMap::default();
1266        assignment.insert(0, false);
1267        assignment.insert(1, false);
1268        assert!(!manager.eval(x_or_y, &assignment));
1269
1270        assignment.insert(0, true);
1271        assert!(manager.eval(x_or_y, &assignment));
1272    }
1273
1274    #[test]
1275    fn test_bdd_xor() {
1276        let mut manager = BddManager::new();
1277        let x = manager.variable(0);
1278        let y = manager.variable(1);
1279        let x_xor_y = manager.xor(x, y);
1280
1281        let mut assignment = FxHashMap::default();
1282        assignment.insert(0, true);
1283        assignment.insert(1, true);
1284        assert!(!manager.eval(x_xor_y, &assignment));
1285
1286        assignment.insert(0, true);
1287        assignment.insert(1, false);
1288        assert!(manager.eval(x_xor_y, &assignment));
1289    }
1290
1291    #[test]
1292    fn test_bdd_ite() {
1293        let mut manager = BddManager::new();
1294        let x = manager.variable(0);
1295        let y = manager.variable(1);
1296        let z = manager.variable(2);
1297
1298        // if x then y else z
1299        let result = manager.ite(x, y, z);
1300
1301        let mut assignment = FxHashMap::default();
1302        assignment.insert(0, true);
1303        assignment.insert(1, true);
1304        assignment.insert(2, false);
1305        assert!(manager.eval(result, &assignment)); // x=T, y=T => T
1306
1307        assignment.insert(0, false);
1308        assignment.insert(1, true);
1309        assignment.insert(2, false);
1310        assert!(!manager.eval(result, &assignment)); // x=F, z=F => F
1311    }
1312
1313    #[test]
1314    fn test_bdd_implies() {
1315        let mut manager = BddManager::new();
1316        let x = manager.variable(0);
1317        let y = manager.variable(1);
1318        let x_implies_y = manager.implies(x, y);
1319
1320        let mut assignment = FxHashMap::default();
1321        assignment.insert(0, true);
1322        assignment.insert(1, false);
1323        assert!(!manager.eval(x_implies_y, &assignment));
1324
1325        assignment.insert(0, false);
1326        assignment.insert(1, false);
1327        assert!(manager.eval(x_implies_y, &assignment));
1328    }
1329
1330    #[test]
1331    fn test_bdd_sharing() {
1332        let mut manager = BddManager::new();
1333        let x = manager.variable(0);
1334        let y = manager.variable(0); // Same variable
1335        assert_eq!(x, y); // Should return the same node
1336    }
1337
1338    // ZDD tests
1339    #[test]
1340    fn test_zdd_empty_and_base() {
1341        let manager = ZddManager::new();
1342        assert_eq!(manager.empty(), ZDD_EMPTY);
1343        assert_eq!(manager.base(), ZDD_BASE);
1344        assert_eq!(manager.count(ZDD_EMPTY), 0);
1345        assert_eq!(manager.count(ZDD_BASE), 1);
1346    }
1347
1348    #[test]
1349    fn test_zdd_singleton() {
1350        let mut manager = ZddManager::new();
1351        let x = manager.singleton(0);
1352        assert_eq!(manager.count(x), 1); // One set: {0}
1353        assert_ne!(x, ZDD_EMPTY);
1354        assert_ne!(x, ZDD_BASE);
1355    }
1356
1357    #[test]
1358    fn test_zdd_union() {
1359        let mut manager = ZddManager::new();
1360        let x = manager.singleton(0);
1361        let y = manager.singleton(1);
1362        let union = manager.union(x, y);
1363
1364        // Union of {0} and {1} should give us 2 sets: {0} and {1}
1365        assert_eq!(manager.count(union), 2);
1366    }
1367
1368    #[test]
1369    fn test_zdd_intersection() {
1370        let mut manager = ZddManager::new();
1371        let x = manager.singleton(0);
1372        let y = manager.singleton(1);
1373        let intersection = manager.intersection(x, y);
1374
1375        // Intersection of {0} and {1} should be empty
1376        assert_eq!(intersection, ZDD_EMPTY);
1377        assert_eq!(manager.count(intersection), 0);
1378    }
1379
1380    #[test]
1381    fn test_zdd_difference() {
1382        let mut manager = ZddManager::new();
1383        let x = manager.singleton(0);
1384        let y = manager.singleton(1);
1385
1386        let diff1 = manager.difference(x, y);
1387        assert_eq!(manager.count(diff1), 1); // {0} - {1} = {0}
1388
1389        let diff2 = manager.difference(x, x);
1390        assert_eq!(diff2, ZDD_EMPTY); // {0} - {0} = empty
1391    }
1392
1393    #[test]
1394    fn test_zdd_union_associative() {
1395        let mut manager = ZddManager::new();
1396        let x = manager.singleton(0);
1397        let y = manager.singleton(1);
1398        let z = manager.singleton(2);
1399
1400        let xy = manager.union(x, y);
1401        let xyz1 = manager.union(xy, z);
1402
1403        let yz = manager.union(y, z);
1404        let xyz2 = manager.union(x, yz);
1405
1406        assert_eq!(manager.count(xyz1), manager.count(xyz2));
1407        assert_eq!(manager.count(xyz1), 3); // Three sets
1408    }
1409
1410    // ADD tests
1411    #[test]
1412    fn test_add_constant() {
1413        use num_bigint::BigInt;
1414        let mut manager = AddManager::new();
1415        let five = manager.constant(BigRational::from_integer(BigInt::from(5)));
1416
1417        let assignment = FxHashMap::default();
1418        let result = manager.eval(five, &assignment);
1419        assert_eq!(result, BigRational::from_integer(BigInt::from(5)));
1420    }
1421
1422    #[test]
1423    fn test_add_variable() {
1424        use num_bigint::BigInt;
1425        let mut manager = AddManager::new();
1426        let x = manager.variable(0);
1427
1428        let mut assignment = FxHashMap::default();
1429        assignment.insert(0, true);
1430        let result = manager.eval(x, &assignment);
1431        assert_eq!(result, BigRational::from_integer(BigInt::from(1)));
1432
1433        assignment.insert(0, false);
1434        let result = manager.eval(x, &assignment);
1435        assert_eq!(result, BigRational::from_integer(BigInt::from(0)));
1436    }
1437
1438    #[test]
1439    fn test_add_addition() {
1440        use num_bigint::BigInt;
1441        let mut manager = AddManager::new();
1442        let three = manager.constant(BigRational::from_integer(BigInt::from(3)));
1443        let five = manager.constant(BigRational::from_integer(BigInt::from(5)));
1444        let sum = manager.add(three, five);
1445
1446        let assignment = FxHashMap::default();
1447        let result = manager.eval(sum, &assignment);
1448        assert_eq!(result, BigRational::from_integer(BigInt::from(8)));
1449    }
1450
1451    #[test]
1452    fn test_add_multiplication() {
1453        use num_bigint::BigInt;
1454        let mut manager = AddManager::new();
1455        let three = manager.constant(BigRational::from_integer(BigInt::from(3)));
1456        let five = manager.constant(BigRational::from_integer(BigInt::from(5)));
1457        let product = manager.mul(three, five);
1458
1459        let assignment = FxHashMap::default();
1460        let result = manager.eval(product, &assignment);
1461        assert_eq!(result, BigRational::from_integer(BigInt::from(15)));
1462    }
1463
1464    #[test]
1465    fn test_add_max_min() {
1466        use num_bigint::BigInt;
1467        let mut manager = AddManager::new();
1468        let three = manager.constant(BigRational::from_integer(BigInt::from(3)));
1469        let five = manager.constant(BigRational::from_integer(BigInt::from(5)));
1470
1471        let max_val = manager.max(three, five);
1472        let min_val = manager.min(three, five);
1473
1474        let assignment = FxHashMap::default();
1475        assert_eq!(
1476            manager.eval(max_val, &assignment),
1477            BigRational::from_integer(BigInt::from(5))
1478        );
1479        assert_eq!(
1480            manager.eval(min_val, &assignment),
1481            BigRational::from_integer(BigInt::from(3))
1482        );
1483    }
1484
1485    #[test]
1486    fn test_add_scale() {
1487        use num_bigint::BigInt;
1488        let mut manager = AddManager::new();
1489        let x = manager.variable(0);
1490        let scaled = manager.scale(x, &BigRational::from_integer(BigInt::from(5)));
1491
1492        let mut assignment = FxHashMap::default();
1493        assignment.insert(0, true);
1494        let result = manager.eval(scaled, &assignment);
1495        assert_eq!(result, BigRational::from_integer(BigInt::from(5)));
1496
1497        assignment.insert(0, false);
1498        let result = manager.eval(scaled, &assignment);
1499        assert_eq!(result, BigRational::from_integer(BigInt::from(0)));
1500    }
1501
1502    #[test]
1503    fn test_add_sharing() {
1504        use num_bigint::BigInt;
1505        let mut manager = AddManager::new();
1506        let x = manager.constant(BigRational::from_integer(BigInt::from(5)));
1507        let y = manager.constant(BigRational::from_integer(BigInt::from(5)));
1508        assert_eq!(x, y); // Should return the same node
1509    }
1510}