pldag/
lib.rs

1use std::collections::HashSet;
2use std::collections::{HashMap, VecDeque, hash_map::DefaultHasher};
3use std::hash::{Hash, Hasher};
4use std::ops::{Index, RangeInclusive};
5use itertools::Itertools;
6use indexmap::{IndexMap, IndexSet};
7
8/// Represents a bound with minimum and maximum values.
9/// Used to specify the allowed range for variables and constraints.
10pub type Bound = (i64, i64);
11
12/// Type alias for node identifiers in the DAG.
13pub type ID = String;
14
15/// Creates a hash from a vector of string-integer pairs and a standalone integer.
16/// 
17/// This function is used to generate unique IDs for constraints by hashing their
18/// coefficients and bias together.
19/// 
20/// # Arguments
21/// * `data` - Vector of (variable_name, coefficient) pairs
22/// * `num` - Additional integer value to include in the hash (typically the bias)
23/// 
24/// # Returns
25/// A 64-bit hash value representing the input data
26fn create_hash(data: &Vec<(String, i64)>, num: i64) -> u64 {
27    // Create a new hasher
28    let mut hasher = DefaultHasher::new();
29    
30    // Hash the vector
31    for (s, i) in data {
32        s.hash(&mut hasher);
33        i.hash(&mut hasher);
34    }
35    
36    // Hash the standalone i64 value
37    num.hash(&mut hasher);
38    
39    // Return the final hash value
40    hasher.finish()
41}
42
43/// Checks if a bound represents a fixed value (min equals max).
44/// 
45/// # Arguments
46/// * `b` - A bound tuple (min, max)
47/// 
48/// # Returns
49/// `true` if the bound represents a single fixed value, `false` otherwise
50fn bound_fixed(b: Bound) -> bool {
51    b.0 == b.1
52}
53
54/// Checks if a bound represents a boolean range [0, 1].
55/// 
56/// # Arguments
57/// * `b` - A bound tuple (min, max)
58/// 
59/// # Returns
60/// `true` if the bound represents a boolean value (0 to 1), `false` otherwise
61fn bound_bool(b: Bound) -> bool {
62    b.0 == 0 && b.1 == 1
63}
64
65/// Adds two bounds together element-wise.
66/// 
67/// # Arguments
68/// * `b1` - First bound (min1, max1)
69/// * `b2` - Second bound (min2, max2)
70/// 
71/// # Returns
72/// A new bound (min1 + min2, max1 + max2)
73fn bound_add(b1: Bound, b2: Bound) -> Bound {
74    return (b1.0 + b2.0, b1.1 + b2.1);
75}
76
77/// Multiplies a bound by a scalar coefficient.
78/// 
79/// When the coefficient is negative, the min and max values are swapped
80/// to maintain the correct bound ordering.
81/// 
82/// # Arguments
83/// * `k` - Scalar coefficient to multiply by
84/// * `b` - Bound to multiply (min, max)
85/// 
86/// # Returns
87/// A new bound with the multiplication applied
88fn bound_multiply(k: i64, b: Bound) -> Bound {
89    if k < 0 {
90        return (k*b.1, k*b.0);
91    } else {
92        return (k*b.0, k*b.1);
93    }
94}
95
96/// Calculates the span (range) of a bound.
97/// 
98/// # Arguments
99/// * `b` - A bound tuple (min, max)
100/// 
101/// # Returns
102/// The absolute difference between max and min values
103fn bound_span(b: Bound) -> i64 {
104    return (b.1 - b.0).abs();
105}
106
107/// Sparse representation of an integer matrix.
108/// 
109/// Stores only non-zero elements using coordinate format (COO):
110/// - `rows\[i\]`, `cols\[i\]`, `vals\[i\]` represent a non-zero element at position (rows\[i\], cols\[i\]) with value vals\[i\]
111pub struct SparseIntegerMatrix {
112    /// Row indices of non-zero elements
113    pub rows: Vec<usize>,
114    /// Column indices of non-zero elements  
115    pub cols: Vec<usize>,
116    /// Values of non-zero elements
117    pub vals: Vec<i64>,
118    /// Matrix dimensions: (number_of_rows, number_of_columns)
119    pub shape: (usize, usize),
120}
121
122/// Dense representation of an integer matrix.
123/// 
124/// Stores all elements in a 2D vector structure.
125pub struct DenseIntegerMatrix {
126    /// Matrix data stored as a vector of rows
127    pub data: Vec<Vec<i64>>,
128    /// Matrix dimensions: (number_of_rows, number_of_columns)
129    pub shape: (usize, usize),
130}
131
132impl DenseIntegerMatrix {
133    /// Creates a new dense integer matrix filled with zeros.
134    /// 
135    /// # Arguments
136    /// * `rows` - Number of rows in the matrix
137    /// * `cols` - Number of columns in the matrix
138    /// 
139    /// # Returns
140    /// A new `DenseIntegerMatrix` with all elements initialized to zero
141    pub fn new(rows: usize, cols: usize) -> DenseIntegerMatrix {
142        DenseIntegerMatrix {
143            data: vec![vec![0; cols]; rows],
144            shape: (rows, cols),
145        }
146    }
147
148    /// Computes the matrix-vector dot product.
149    /// 
150    /// Multiplies this matrix by a vector and returns the resulting vector.
151    /// The input vector length must match the number of columns in the matrix.
152    /// 
153    /// # Arguments
154    /// * `vector` - Input vector to multiply with
155    /// 
156    /// # Returns
157    /// A vector representing the matrix-vector product
158    /// 
159    /// # Panics
160    /// May panic if the vector length doesn't match the matrix column count
161    pub fn dot_product(&self, vector: &Vec<i64>) -> Vec<i64> {
162        let mut result = vec![0; self.shape.0];
163        for i in 0..self.shape.0 {
164            for j in 0..self.shape.1 {
165                result[i] += self.data[i][j] * vector[j];
166            }
167        }
168        result
169    }
170}
171
172/// Dense representation of a polyhedron defined by linear constraints.
173/// 
174/// Represents the constraint system Ax >= b where:
175/// - A is the constraint matrix
176/// - b is the right-hand side vector
177/// - columns maps matrix columns to variable names
178pub struct DensePolyhedron {
179    /// Constraint matrix A
180    pub A: DenseIntegerMatrix,
181    /// Right-hand side vector b
182    pub b: Vec<i64>,
183    /// Variable names corresponding to matrix columns
184    pub columns: Vec<String>,
185    /// Subset of columns that represent integer variables
186    pub integer_columns: Vec<String>,
187}
188
189impl DensePolyhedron {
190    /// Converts a variable assignment map to a vector ordered by the polyhedron's columns.
191    /// 
192    /// # Arguments
193    /// * `from_assignments` - Map of variable names to their assigned values
194    /// 
195    /// # Returns
196    /// A vector where each position corresponds to a column in the polyhedron,
197    /// with values from the assignment map or 0 if not assigned
198    pub fn to_vector(&self, from_assignments: &HashMap<String, i64>) -> Vec<i64> {
199        let mut vector: Vec<i64> = vec![0; self.columns.len()];
200        for (index, v) in from_assignments
201            .iter()
202            .filter_map(|(k, v)| self.columns.iter().position(|col| col == k).map(|index| (index, v)))
203        {
204            vector[index] = *v;
205        }
206        vector
207    }
208
209    /// Creates a new polyhedron by fixing certain variables to specific values.
210    /// 
211    /// This operation eliminates the specified variables from the polyhedron by
212    /// substituting their fixed values into the constraints and removing their columns.
213    /// 
214    /// # Arguments
215    /// * `values` - Map of variable names to their fixed values
216    /// 
217    /// # Returns
218    /// A new `DensePolyhedron` with the specified variables eliminated
219    pub fn assume(&self, values: &HashMap<String, i64>) -> DensePolyhedron {
220        // 1) Make mutable copies of everything
221        let mut new_A_data   = self.A.data.clone();     // Vec<Vec<i64>>
222        let mut new_b        = self.b.clone();          // Vec<i64>
223        let mut new_columns  = self.columns.clone();    // Vec<String>
224        let mut new_int_cols = self.integer_columns.clone();  // Vec<String>
225
226        // 2) Find which columns we’re going to remove, along with their assigned values.
227        //    We capture (idx_in_matrix, column_name, assigned_value).
228        let mut to_remove: Vec<(usize, String, i64)> = values
229            .iter()
230            .filter_map(|(name, &val)| {
231                // look up current index of `name` in self.columns
232                self.columns
233                    .iter()
234                    .position(|col| col == name)
235                    .map(|idx| (idx, name.clone(), val))
236            })
237            .collect();
238
239        // 3) Remove from highest index to lowest so earlier removals
240        //    don’t shift the positions of later ones.
241        to_remove.sort_by(|a, b| b.0.cmp(&a.0));
242
243        // 4) For each (idx, name, val) do:
244        //      - b := b - A[:, idx] * val
245        //      - remove column idx from every row of A
246        //      - remove from columns and integer_columns
247        for (col_idx, col_name, fixed_val) in to_remove {
248            for row in 0..new_A_data.len() {
249                // subtract A[row][col_idx] * fixed_val from b[row]
250                new_b[row] -= new_A_data[row][col_idx] * fixed_val;
251                // now remove that column entry
252                new_A_data[row].remove(col_idx);
253            }
254            // drop the column name
255            new_columns.remove(col_idx);
256            // if it was an integer column, drop it too
257            new_int_cols.retain(|c| c != &col_name);
258        }
259
260        // 5) Rebuild the DenseIntegerMatrix with updated shape
261        let new_shape = (new_A_data.len(), new_columns.len());
262        let new_A = DenseIntegerMatrix {
263            data: new_A_data,
264            shape: new_shape,
265        };
266
267        // 6) Return the shrunken polyhedron
268        DensePolyhedron {
269            A: new_A,
270            b: new_b,
271            columns: new_columns,
272            integer_columns: new_int_cols,
273        }
274    }
275
276    /// Evaluates the polyhedron constraints against variable bounds.
277    /// 
278    /// Tests whether the lower and upper bounds of the given variables
279    /// satisfy all constraints in the polyhedron.
280    /// 
281    /// # Arguments
282    /// * `assignments` - Map of variable names to their bounds (min, max)
283    /// 
284    /// # Returns
285    /// A bound where:
286    /// - .0 is 1 if lower bounds satisfy all constraints, 0 otherwise
287    /// - .1 is 1 if upper bounds satisfy all constraints, 0 otherwise
288    pub fn evaluate(&self, assignments: &IndexMap<String, Bound>) -> Bound {
289        let mut lower_bounds = HashMap::new();
290        let mut upper_bounds = HashMap::new();
291        for (key, bound) in assignments {
292            lower_bounds.insert(key.clone(), bound.0);
293            upper_bounds.insert(key.clone(), bound.1);
294        }
295
296        let lower_result = self.A.dot_product(&self.to_vector(&lower_bounds))
297            .iter()
298            .zip(&self.b)
299            .all(|(a, b)| a >= b);
300
301        let upper_result = self.A.dot_product(&self.to_vector(&upper_bounds))
302            .iter()
303            .zip(&self.b)
304            .all(|(a, b)| a >= b);
305
306        (lower_result as i64, upper_result as i64)
307    }
308}
309
310impl From<SparseIntegerMatrix> for DenseIntegerMatrix {
311    fn from(sparse: SparseIntegerMatrix) -> DenseIntegerMatrix {
312        let mut dense = DenseIntegerMatrix::new(sparse.shape.0, sparse.shape.1);
313        for ((&row, &col), &val) in sparse.rows.iter().zip(&sparse.cols).zip(&sparse.vals) {
314            dense.data[row][col] = val;
315        }
316        dense
317    }
318}
319
320impl From<DenseIntegerMatrix> for SparseIntegerMatrix {
321    fn from(dense: DenseIntegerMatrix) -> SparseIntegerMatrix {
322        let mut rows = Vec::new();
323        let mut cols = Vec::new();
324        let mut vals = Vec::new();
325        for (i, row) in dense.data.iter().enumerate() {
326            for (j, &val) in row.iter().enumerate() {
327                if val != 0 {
328                    rows.push(i);
329                    cols.push(j);
330                    vals.push(val);
331                }
332            }
333        }
334        SparseIntegerMatrix {
335            rows,
336            cols,
337            vals,
338            shape: dense.shape,
339        }
340    }
341}
342
343impl SparseIntegerMatrix {
344    /// Creates a new empty sparse integer matrix.
345    /// 
346    /// # Returns
347    /// A new `SparseIntegerMatrix` with no entries and shape (0, 0)
348    pub fn new() -> SparseIntegerMatrix {
349        SparseIntegerMatrix {
350            rows: Vec::new(),
351            cols: Vec::new(),
352            vals: Vec::new(),
353            shape: (0, 0),
354        }
355    }
356}
357
358/// Sparse representation of a polyhedron defined by linear constraints.
359/// 
360/// Represents the constraint system Ax >= b in sparse format for memory efficiency.
361pub struct SparsePolyhedron {
362    /// Sparse constraint matrix A
363    pub A: SparseIntegerMatrix,
364    /// Right-hand side vector b
365    pub b: Vec<i64>,
366    /// Variable names corresponding to matrix columns
367    pub columns: Vec<String>,
368    /// Subset of columns that represent integer variables
369    pub integer_columns: Vec<String>,
370}
371
372impl From<SparsePolyhedron> for DensePolyhedron {
373    fn from(sparse: SparsePolyhedron) -> DensePolyhedron {
374        let mut dense_matrix = DenseIntegerMatrix::new(sparse.A.shape.0, sparse.A.shape.1);
375        for ((&row, &col), &val) in sparse.A.rows.iter().zip(&sparse.A.cols).zip(&sparse.A.vals) {
376            dense_matrix.data[row][col] = val;
377        }
378        DensePolyhedron {
379            A: dense_matrix,
380            b: sparse.b,
381            columns: sparse.columns,
382            integer_columns: sparse.integer_columns,
383        }
384    }
385}
386
387impl From<DensePolyhedron> for SparsePolyhedron {
388    fn from(dense: DensePolyhedron) -> SparsePolyhedron {
389        let mut rows = Vec::new();
390        let mut cols = Vec::new();
391        let mut vals = Vec::new();
392        for (i, row) in dense.A.data.iter().enumerate() {
393            for (j, &val) in row.iter().enumerate() {
394                if val != 0 {
395                    rows.push(i);
396                    cols.push(j);
397                    vals.push(val);
398                }
399            }
400        }
401        SparsePolyhedron {
402            A: SparseIntegerMatrix {
403                rows,
404                cols,
405                vals,
406                shape: dense.A.shape,
407            },
408            b: dense.b,
409            columns: dense.columns,
410            integer_columns: dense.integer_columns,
411        }
412    }
413}
414
415/// Represents a coefficient in a linear constraint: (variable_name, coefficient_value).
416pub type Coefficient = (String, i64);
417
418/// Maps node IDs to their bound values.
419pub type Assignment = IndexMap<ID, Bound>;
420
421/// Represents bounds for floating-point coefficient values.
422pub type VBound = (f64, f64);
423
424/// Combines integer bounds with floating-point coefficient bounds.
425pub type MultiBound = (Bound, VBound);
426
427/// Maps node IDs to their bounds and accumulated coefficients.
428pub type ValuedAssignment = IndexMap<ID, MultiBound>;
429
430/// Represents a linear constraint in the form: sum(coeff_i * var_i) + bias >= 0.
431pub struct Constraint {
432    /// Vector of (variable_name, coefficient) pairs
433    pub coefficients: Vec<Coefficient>,
434    /// Bias/constant term with potential bounds
435    pub bias: Bound,
436}
437
438impl Constraint {
439    /// Computes the dot product of the constraint coefficients with variable bounds.
440    /// 
441    /// This calculates the range of possible values for the linear combination
442    /// of variables in this constraint, excluding the bias term.
443    /// 
444    /// # Arguments
445    /// * `values` - Map of variable names to their bounds
446    /// 
447    /// # Returns
448    /// A bound representing the min and max possible values of the dot product
449    pub fn dot(&self, values: &IndexMap<String, Bound>) -> Bound {
450        self.coefficients.iter().fold((0, 0), |acc, (key, coeff)| {
451            let bound = values.get(key).unwrap_or(&(0, 0));
452            let (min, max) = bound_multiply(*coeff, *bound);
453            (acc.0 + min, acc.1 + max)
454        })
455    }
456
457    /// Evaluates the constraint against variable bounds.
458    /// 
459    /// Computes whether the constraint (dot product + bias >= 0) is satisfied
460    /// for the given variable bounds.
461    /// 
462    /// # Arguments
463    /// * `values` - Map of variable names to their bounds
464    /// 
465    /// # Returns
466    /// A bound where:
467    /// - .0 is 1 if the constraint is satisfied with lower bounds, 0 otherwise
468    /// - .1 is 1 if the constraint is satisfied with upper bounds, 0 otherwise
469    pub fn evaluate(&self, values: &IndexMap<String, Bound>) -> Bound {
470        let bound = self.dot(values);
471        return (
472            (bound.0 + self.bias.0 >= 0) as i64,
473            (bound.1 + self.bias.1 >= 0) as i64
474        )
475    }
476
477    /// Creates the negation of this constraint.
478    /// 
479    /// Transforms the constraint from (ax + b >= 0) to (-ax - b - 1 >= 0),
480    /// which is equivalent to (ax + b < 0).
481    /// 
482    /// # Returns
483    /// A new `Constraint` representing the negation of this constraint
484    pub fn negate(&self) -> Constraint {
485        Constraint {
486            coefficients: self.coefficients.iter().map(|(key, val)| {
487                (key.clone(), -val)
488            }).collect(),
489            bias: (
490                -self.bias.0-1,
491                 -self.bias.1-1
492            ),
493        }
494    }
495}
496/// Represents different types of boolean expressions in the DAG.
497pub enum BoolExpression {
498    /// A composite node representing a linear constraint
499    Composite(Constraint),
500    /// A primitive (leaf) node with a bound on its value
501    Primitive(Bound),
502}
503
504/// Represents a node in the PL-DAG containing both logical expression and coefficient.
505pub struct Node {
506    /// The logical expression (either primitive or composite)
507    pub expression: BoolExpression,
508    /// Coefficient associated with this node for accumulation
509    pub coefficient: f64,
510}
511
512/// A Primitive Logic Directed Acyclic Graph (PL-DAG).
513/// 
514/// The PL-DAG represents a logical system where:
515/// - Primitive nodes are leaf variables with bounds
516/// - Composite nodes represent logical constraints over other nodes
517/// - Each node has an associated coefficient for accumulation operations
518/// 
519/// The DAG structure ensures no cycles and enables efficient bottom-up propagation.
520pub struct Pldag {
521    /// Map from node IDs to their corresponding nodes
522    pub nodes: IndexMap<String, Node>,
523}
524
525impl Pldag {
526    /// Creates a new empty PL-DAG.
527    /// 
528    /// # Returns
529    /// A new `Pldag` instance with no nodes
530    pub fn new() -> Pldag {
531        Pldag {
532            nodes: IndexMap::new(),
533        }
534    }
535
536    /// Computes the transitive dependency closure for all nodes.
537    /// 
538    /// For each node in the DAG, this method calculates all nodes that can be
539    /// reached by following dependency edges (the set of all descendants).
540    /// 
541    /// # Returns
542    /// A map from each node ID to the set of all node IDs it transitively depends on
543    pub fn transitive_dependencies(&self) -> HashMap<ID, HashSet<ID>> {
544        // memo: node -> its already-computed reachables
545        let mut memo: HashMap<String, HashSet<String>> = HashMap::new();
546        let mut result: HashMap<String, HashSet<String>> = HashMap::new();
547
548        for key in self.nodes.keys() {
549            // compute (or fetch) and store
550            let deps = self._collect_deps(key, &mut memo);
551            result.insert(key.clone(), deps);
552        }
553
554        result
555    }
556
557    /// Helper function to recursively collect dependencies with memoization.
558    /// 
559    /// # Arguments
560    /// * `node` - The node to collect dependencies for
561    /// * `memo` - Memoization cache to avoid recomputing dependencies
562    /// 
563    /// # Returns
564    /// The set of all nodes that the given node transitively depends on
565    fn _collect_deps(&self, node: &ID, memo: &mut HashMap<ID, HashSet<ID>>) -> HashSet<ID> {
566        // if we’ve already done this node, just clone the result
567        if let Some(cached) = memo.get(node) {
568            return cached.clone();
569        }
570
571        let mut deps = HashSet::new();
572
573        if let Some(node_data) = self.nodes.get(node) {
574            if let BoolExpression::Composite(constraint) = &node_data.expression {
575                for (child_id, _) in &constraint.coefficients {
576                    // direct edge
577                    deps.insert(child_id.clone());
578                    // transitive edges
579                    let sub = self._collect_deps(child_id, memo);
580                    deps.extend(sub);
581                }
582            }
583            // if Primitive, we leave deps empty
584        }
585
586        // memoize before returning
587        memo.insert(node.clone(), deps.clone());
588        deps
589    }
590
591    /// Generates all possible combinations of primitive variable assignments.
592    /// 
593    /// Enumerates the Cartesian product of all primitive variable bounds,
594    /// yielding every possible complete assignment of values to primitive variables.
595    /// 
596    /// # Returns
597    /// An iterator over all possible variable assignments as HashMaps
598    pub fn primitive_combinations(&self) -> impl Iterator<Item = HashMap<ID, i64>> {
599        // 1. Pull out [(var_name, (low, high)), …]
600        let primitives: Vec<(String, (i64, i64))> = self.nodes
601            .iter()
602            .filter_map(|(key, node)| {
603                if let BoolExpression::Primitive(bound) = &node.expression {
604                    Some((key.clone(), *bound))
605                } else {
606                    None
607                }
608            })
609            .collect();
610
611        // 2. Extract names in order
612        let keys: Vec<String> = primitives.iter().map(|(k, _)| k.clone()).collect();
613
614        // 3. Turn each bound into an inclusive range  low..=high
615        let ranges: Vec<RangeInclusive<i64>> = primitives
616            .iter()
617            .map(|(_, (low, high))| *low..=*high)
618            .collect();
619
620        // 4. For each range, collect it into a Vec<i64>, then do the cartesian product
621        ranges
622            .into_iter()
623            .map(|r| r.collect::<Vec<_>>())
624            .multi_cartesian_product()
625            // 5. Zip back with keys to build each assignment map
626            .map(move |values| {
627                keys.iter()
628                    .cloned()
629                    .zip(values.into_iter())
630                    .collect::<HashMap<String, i64>>()
631            })
632    }
633
634    /// Propagates bounds through the DAG bottom-up.
635    /// 
636    /// Starting from the given variable assignments, this method computes bounds
637    /// for all composite nodes by propagating constraints upward through the DAG.
638    /// 
639    /// # Arguments
640    /// * `assignment` - Initial assignment of bounds to variables
641    /// 
642    /// # Returns
643    /// Complete assignment including bounds for all reachable nodes
644    pub fn propagate(&self, assignment: &Assignment) -> Assignment {
645
646        let mut result= assignment.clone();
647
648        // Fill result with the primitive variable bounds
649        for (key, node) in self.nodes.iter() {
650            if !result.contains_key(key) {
651                if let BoolExpression::Primitive(bound) = &node.expression {
652                    result.insert(key.clone(), *bound);
653                }
654            }
655        }
656
657        // S = All composites that 
658        // (1) have only primitive variables as input and
659        // (2) are not present in `result/assignments`
660        let mut S: VecDeque<String> = self.nodes
661            .iter()
662            .filter(|(key, node)| {
663                match &node.expression {
664                    BoolExpression::Composite(composite) => {
665                    composite.coefficients.iter().all(|x| {
666                        match self.nodes.get(&x.0) {
667                        Some(node_data) => matches!(node_data.expression, BoolExpression::Primitive(_)),
668                        _ => false
669                        }
670                    }) && !result.contains_key(&key.to_string())
671                    },
672                    BoolExpression::Primitive(_) => false,
673                }
674            })
675            .map(|(key, _)| key.clone())
676            .collect();
677
678        // Add a set to keep track of the visited nodes
679        // This is to avoid infinite loops in case of cycles
680        // in the graph
681        let mut visited = HashSet::new();
682
683        // Iterate until S is empty
684        while let Some(s) = S.pop_front() {
685
686            // If we have already visited this node, skip it
687            if visited.contains(&s) {
688                panic!("Cycle detected in the graph");
689            }
690
691            // Mark the current node as visited
692            visited.insert(s.clone());
693            
694            // Start by propagating the bounds from current_id's children
695            // to current_id
696            match self.nodes.get(&s) {
697                Some(node_data) => {
698                    if let BoolExpression::Composite(composite) = &node_data.expression {
699                        result.insert(
700                            s.clone(), 
701                            composite.evaluate(&result)
702                        );
703            
704                        // Now find all composites having `current_id` as its input.
705                        // And if that composite's children are in `result`, then add to S.
706                        let incoming = self.nodes
707                            .iter()
708                            .filter(|(key, node)| {
709                                !result.contains_key(&key.to_string()) && match &node.expression {
710                                    BoolExpression::Composite(sub_composite) => {
711                                        sub_composite.coefficients.iter().any(|x| x.0 == s)
712                                    },
713                                    _ => false
714                                }
715                            })
716                            .map(|(key, _)| key.clone())
717                            .collect::<Vec<String>>();
718            
719                        // Add the incoming composites to S without any checks
720                        for incoming_id in incoming {
721                            if !S.contains(&incoming_id) {
722                                S.push_back(incoming_id);
723                            }
724                        }
725                    }
726                },
727                _ => {}
728            }
729        }
730
731        return result;
732    }
733    
734    /// Propagates bounds using default primitive variable bounds.
735    /// 
736    /// Convenience method that calls `propagate` with the default bounds
737    /// of all primitive variables as defined in the DAG.
738    /// 
739    /// # Returns
740    /// Complete assignment with bounds for all nodes
741    pub fn propagate_default(&self) -> Assignment {
742        let assignments: IndexMap<String, Bound> = self.nodes.iter().filter_map(|(key, node)| {
743            if let BoolExpression::Primitive(bound) = &node.expression {
744                Some((key.clone(), *bound))
745            } else {
746                None
747            }
748        }).collect();
749        self.propagate(&assignments)
750    }
751
752    /// Propagates bounds and accumulates coefficients for multiple assignments.
753    /// 
754    /// For each input assignment, this method:
755    /// 1. Propagates bounds through the DAG
756    /// 2. Accumulates coefficients where each parent's coefficient is the sum
757    ///    of its children's coefficients plus its own coefficient
758    /// 
759    /// # Arguments
760    /// * `assignments` - Vector of assignments to process
761    /// 
762    /// # Returns
763    /// Vector of valued assignments, each containing bounds and accumulated coefficients
764    pub fn propagate_many_coefs(&self, assignments: Vec<&Assignment>) -> Vec<ValuedAssignment> {
765        // Calculate transitive dependencies
766        let transitive_deps = self.transitive_dependencies();
767
768        let mut assignment_results: Vec<ValuedAssignment> = Vec::new();
769        
770        for assignment in assignments {
771
772            // Start with the propagated bounds
773            let result = self.propagate(assignment);
774    
775            let mut valued_assigment: ValuedAssignment = IndexMap::new();
776            // Calculate the accumulated coefficients with the new bound result
777            for (key, deps) in transitive_deps.iter() {
778                let mut coef_sum: VBound = (0.0, 0.0);
779                if let Some(node) = self.nodes.get(key) {
780                    coef_sum = (node.coefficient, node.coefficient);
781                }
782                for dep in deps {
783                    let dep_res = result.get(dep).unwrap_or(&(0, 1));
784                    if let Some(node) = self.nodes.get(dep) {
785                        if node.coefficient < 0.0 {
786                            coef_sum.0 += node.coefficient * dep_res.1 as f64;
787                            coef_sum.1 += node.coefficient * dep_res.0 as f64;
788                        } else {
789                            coef_sum.0 += node.coefficient * dep_res.0 as f64;
790                            coef_sum.1 += node.coefficient * dep_res.1 as f64;
791                        }
792                    }
793                }
794                if let Some(bound) = result.get(key) {
795                    valued_assigment.insert(key.clone(), (*bound, coef_sum));
796                }
797            }
798    
799            // Return the accumulated coefficients
800            assignment_results.push(valued_assigment);
801        }
802
803        return assignment_results;
804
805    }
806
807    /// Propagates bounds and accumulates coefficients for a single assignment.
808    /// 
809    /// Convenience method that calls `propagate_many_coefs` with a single assignment.
810    /// 
811    /// # Arguments
812    /// * `assignment` - The assignment to propagate
813    /// 
814    /// # Returns
815    /// A valued assignment containing bounds and accumulated coefficients for all nodes
816    /// 
817    /// # Panics
818    /// Panics if no assignments are returned (should not happen under normal circumstances)
819    pub fn propagate_coefs(&self, assignment: &Assignment) -> ValuedAssignment {
820        // Propagate the assignment through the Pldag
821        return self.propagate_many_coefs(vec![assignment]).into_iter().next()
822            .unwrap_or_else(|| panic!("No assignments found after propagation with {:?}", assignment));
823    }
824
825    /// Propagates bounds and coefficients using default primitive bounds.
826    /// 
827    /// Convenience method that calls `propagate_coefs` with the default bounds
828    /// of all primitive variables.
829    /// 
830    /// # Returns
831    /// A valued assignment with bounds and coefficients for all nodes
832    pub fn propagate_coefs_default(&self) -> ValuedAssignment {
833        // Propagate the default assignment through the Pldag
834        let assignments: IndexMap<String, Bound> = self.nodes.iter().filter_map(|(key, node)| {
835            if let BoolExpression::Primitive(bound) = &node.expression {
836                Some((key.clone(), *bound))
837            } else {
838                None
839            }
840        }).collect();
841        self.propagate_coefs(&assignments)
842    }
843    
844    /// Retrieves the objective function coefficients from all primitive nodes.
845    /// 
846    /// Collects the coefficients of all primitive (leaf) nodes in the DAG,
847    /// which represent the objective function values for ILP optimization.
848    /// Composite nodes are excluded from the result.
849    /// 
850    /// # Returns
851    /// A map from primitive variable names to their objective coefficients
852    pub fn get_objective(&self) -> IndexMap<String, f64> {
853        // Collect objective coefficients from primitive nodes
854        self.nodes.iter().filter_map(|(key, node)| {
855            if let BoolExpression::Primitive(_) = &node.expression {
856                Some((key.clone(), node.coefficient))
857            } else {
858                None
859            }
860        }).collect()
861    }
862    
863    /// Converts the PL-DAG to a sparse polyhedron for ILP solving.
864    /// 
865    /// Transforms the logical constraints in the DAG into a system of linear
866    /// inequalities suitable for integer linear programming solvers.
867    /// 
868    /// # Arguments
869    /// * `double_binding` - If true, creates bidirectional implications for composite nodes
870    /// * `integer_constraints` - If true, adds bounds constraints for integer variables
871    /// * `fixed_constraints` - If true, adds equality constraints for fixed primitive variables
872    /// 
873    /// # Returns
874    /// A `SparsePolyhedron` representing the DAG constraints
875    pub fn to_sparse_polyhedron(&self, double_binding: bool, integer_constraints: bool, fixed_constraints: bool) -> SparsePolyhedron {
876
877        fn get_coef_bounds(composite: &Constraint, nodes: &IndexMap<String, Node>) -> IndexMap<String, Bound> {
878            let mut coef_bounds: IndexMap<String, Bound> = IndexMap::new();
879            for (coef_key, _) in composite.coefficients.iter() {
880                let coef_node = nodes.get(&coef_key.to_string())
881                    .unwrap_or_else(|| panic!("Coefficient key '{}' not found in nodes", coef_key));
882                match &coef_node.expression {
883                    BoolExpression::Primitive(bound) => {
884                        coef_bounds.insert(coef_key.to_string(), *bound);
885                    },
886                    _ => {coef_bounds.insert(coef_key.to_string(), (0,1));}
887                }
888            }
889            return coef_bounds;
890        }
891
892        // Create a new sparse matrix
893        let mut A_matrix = SparseIntegerMatrix::new();
894        let mut b_vector: Vec<i64> = Vec::new();
895
896        // Filter out all BoolExpressions that are primitives
897        let primitives: HashMap<&String, Bound> = self.nodes.iter()
898            .filter_map(|(key, node)| {
899                if let BoolExpression::Primitive(bound) = &node.expression {
900                    Some((key, *bound))
901                } else {
902                    None
903                }
904            })
905            .collect();
906
907        // Filter out all BoolExpressions that are composites
908        let composites: HashMap<&String, &Constraint> = self.nodes.iter()
909            .filter_map(|(key, node)| {
910                if let BoolExpression::Composite(constraint) = &node.expression {
911                    Some((key, constraint))
912                } else {
913                    None
914                }
915            })
916            .collect();
917
918        // Create a index mapping for all columns
919        let column_names_map: IndexMap<String, usize> = primitives.keys().chain(composites.keys()).enumerate().map(|(i, key)| (key.to_string(), i)).collect();
920
921        // Keep track of the current row index
922        let mut row_i: usize = 0;
923
924        for (key, composite) in composites {
925
926            // Get the index of the current key
927            let ki = *column_names_map.get(key).unwrap();
928
929            // Construct the inner bound of the composite
930            let coef_bounds = get_coef_bounds(composite, &self.nodes);
931
932            // An inner bound $\text{ib}(\phi)$ of a 
933            // linear inequality constraint $\phi$ is the sum of all variable's 
934            // step bounds, excl bias and before evaluated with the $\geq$ operator. 
935            // For instance, the inner bound of the linear inequality $-2x + y + z \geq 0$, 
936            // where $x,y,z \in \{0,1\}^3$, is $[-2, 2]$, since the lowest value the 
937            // sum can be is $-2$ (given from the combination $x=1, y=0, z=0$) and the 
938            // highest value is $2$ (from $x=0, y=1, z=1$).
939            let ib_phi = composite.dot(&coef_bounds);
940
941            // 1. Let $d = max(|ib(phi)|)$. 
942            let d_pi = std::cmp::max(ib_phi.0.abs(), ib_phi.1.abs());
943            
944            // Push values for pi variable coefficient
945            A_matrix.rows.push(row_i);
946            A_matrix.cols.push(ki);
947            A_matrix.vals.push(-d_pi);
948
949            // Push values for phi coefficients
950            for (coef_key, coef_val) in composite.coefficients.iter() {
951                let ck_index: usize = *column_names_map.get(coef_key).unwrap();
952                A_matrix.rows.push(row_i);
953                A_matrix.cols.push(ck_index);
954                A_matrix.vals.push(*coef_val);
955            }
956
957            // Push bias value
958            let b_phi = composite.bias.0 + d_pi;
959            b_vector.push(-1 * b_phi);
960
961            if double_binding {
962
963                // ## Transforming $\phi \rightarrow \pi$
964                // First note that $\phi \rightarrow \pi$ is equivilant to $\neg \phi \lor \pi$. 
965
966                // 1. Calculate $\phi' = \neg \phi$
967                // 2. Let $d = \text{max}(|\text{ib}(\phi')|)$ 
968                // 3. Append $(d - \text{bias}(\phi'))\pi$ to left side of $\phi'$. 
969                // 4. Let bias be as is
970
971                let phi_prim = composite.negate();
972                let phi_prim_ib = phi_prim.dot(&coef_bounds);
973                let d_phi_prim = std::cmp::max(phi_prim_ib.0.abs(), phi_prim_ib.1.abs());
974                let pi_coef = d_phi_prim - phi_prim.bias.0;
975
976                // Push values for pi variable coefficient
977                A_matrix.rows.push(row_i + 1);
978                A_matrix.cols.push(ki);
979                A_matrix.vals.push(pi_coef);
980
981                // Push values for phi coefficients
982                for (phi_coef_key, phi_coef_val) in phi_prim.coefficients.iter() {
983                    let ck_index: usize = *column_names_map.get(phi_coef_key).unwrap();
984                    A_matrix.rows.push(row_i + 1);
985                    A_matrix.cols.push(ck_index);
986                    A_matrix.vals.push(*phi_coef_val);
987                }
988
989                // Push bias value
990                b_vector.push(-1 * phi_prim.bias.0);
991
992                // Increment one more row when double binding
993                row_i += 1;
994            }
995
996            // Increment the row index
997            row_i += 1;
998        }
999
1000        if fixed_constraints {
1001            // Add the bounds for the primitive variables that are fixed.
1002            // We start by creating a grouping on the lower and upper bounds of the primitive variables
1003            let mut fixed_bound_map: HashMap<i64, Vec<usize>> = HashMap::new();
1004            for (key, bound) in primitives.iter().filter(|(_, bound)| bound_fixed(**bound)) {
1005                fixed_bound_map.entry(bound.0).or_insert_with(Vec::new).push(*column_names_map.get(&key.to_string()).unwrap());
1006            }
1007    
1008            for (v, primitive_ids) in fixed_bound_map.iter() {
1009                let b = *v * primitive_ids.len() as i64;
1010                for i in vec![-1, 1] {
1011                    for primitive_id in primitive_ids {
1012                        A_matrix.rows.push(row_i);
1013                        A_matrix.cols.push(*primitive_id);
1014                        A_matrix.vals.push(i);
1015                    }
1016                    b_vector.push(i * b);
1017                    row_i += 1;
1018                }
1019            }
1020        }
1021
1022        // Collect all integer variables
1023        let mut integer_variables: Vec<String> = Vec::new();
1024
1025        // Restrain integer bounds
1026        for (p_key, p_bound) in primitives.iter().filter(|(_, bound)| bound.0 < 0 || bound.1 > 1) {
1027            
1028            // Add the variable to the integer variables list
1029            integer_variables.push(p_key.to_string());
1030            
1031            if integer_constraints {
1032                // Get the index of the current key
1033                let pi = *column_names_map.get(&p_key.to_string()).unwrap();
1034                
1035                if p_bound.0 < 0 {
1036                    A_matrix.rows.push(row_i);
1037                    A_matrix.cols.push(pi);
1038                    A_matrix.vals.push(-1);
1039                    b_vector.push(-1 * p_bound.0);
1040                    row_i += 1;
1041                }
1042    
1043                if p_bound.1 > 1 {
1044                    A_matrix.rows.push(row_i);
1045                    A_matrix.cols.push(pi);
1046                    A_matrix.vals.push(1);
1047                    b_vector.push(p_bound.1);
1048                    row_i += 1;
1049                } 
1050            }
1051        }
1052
1053        // Set the shape of the A matrix
1054        A_matrix.shape = (row_i, column_names_map.len());
1055
1056        // Create the polyhedron
1057        let polyhedron = SparsePolyhedron {
1058            A: A_matrix,
1059            b: b_vector,
1060            columns: column_names_map.keys().cloned().collect(),
1061            integer_columns: integer_variables,
1062        };
1063
1064        return polyhedron;
1065    }
1066
1067    /// Converts the PL-DAG to a sparse polyhedron with default settings.
1068    /// 
1069    /// Convenience method that calls `to_sparse_polyhedron` with all options enabled:
1070    /// double_binding=true, integer_constraints=true, fixed_constraints=true.
1071    /// 
1072    /// # Returns
1073    /// A `SparsePolyhedron` with full constraint encoding
1074    pub fn to_sparse_polyhedron_default(&self) -> SparsePolyhedron {
1075        self.to_sparse_polyhedron(true, true, true)
1076    }
1077
1078    /// Converts the PL-DAG to a dense polyhedron.
1079    /// 
1080    /// # Arguments
1081    /// * `double_binding` - If true, creates bidirectional implications
1082    /// * `integer_constraints` - If true, adds integer bounds constraints
1083    /// * `fixed_constraints` - If true, adds fixed variable constraints
1084    /// 
1085    /// # Returns
1086    /// A `DensePolyhedron` representing the DAG constraints
1087    pub fn to_dense_polyhedron(&self, double_binding: bool, integer_constraints: bool, fixed_constraints: bool) -> DensePolyhedron {
1088        DensePolyhedron::from(self.to_sparse_polyhedron(double_binding, integer_constraints, fixed_constraints))
1089    }
1090
1091    /// Converts the PL-DAG to a dense polyhedron with default settings.
1092    /// 
1093    /// # Returns
1094    /// A `DensePolyhedron` with all constraint options enabled
1095    pub fn to_dense_polyhedron_default(&self) -> DensePolyhedron {
1096        self.to_dense_polyhedron(true, true, true)
1097    }
1098    
1099    /// Sets the coefficient for a specific node.
1100    /// 
1101    /// Updates the coefficient value associated with the given node ID.
1102    /// If the node doesn't exist, this method has no effect.
1103    /// 
1104    /// # Arguments
1105    /// * `id` - The node ID to update
1106    /// * `coefficient` - The new coefficient value
1107    pub fn set_coef(&mut self, id: ID, coefficient: f64) {
1108        // Update the coefficient for the given node
1109        if let Some(node) = self.nodes.get_mut(&id) {
1110            node.coefficient = coefficient;
1111        }
1112    }
1113    
1114    /// Retrieves the coefficient for a specific node.
1115    /// 
1116    /// # Arguments
1117    /// * `id` - The node ID to query
1118    /// 
1119    /// # Returns
1120    /// The coefficient value for the node, or 0.0 if the node doesn't exist
1121    pub fn get_coef(&self, id: &ID) -> f64 {
1122        // Get the coefficient for the given node
1123        self.nodes.get(id).map(|node| node.coefficient).unwrap_or(0.0)
1124    }
1125    
1126    /// Creates a primitive (leaf) variable with the specified bounds.
1127    /// 
1128    /// Primitive variables represent the base variables in the DAG and have
1129    /// no dependencies on other nodes.
1130    /// 
1131    /// # Arguments
1132    /// * `id` - Unique identifier for the variable
1133    /// * `bound` - The allowed range (min, max) for this variable
1134    pub fn set_primitive(&mut self, id: ID, bound: Bound) {
1135        // Insert the primitive variable as a node
1136        self.nodes.insert(id.clone(), Node {
1137            expression: BoolExpression::Primitive(bound),
1138            coefficient: 0.0,
1139        });
1140    }
1141
1142    /// Creates multiple primitive variables with the same bounds.
1143    /// 
1144    /// Convenience method to create several primitive variables at once.
1145    /// Duplicate IDs are automatically filtered out.
1146    /// 
1147    /// # Arguments
1148    /// * `ids` - Vector of unique identifiers for the variables
1149    /// * `bound` - The common bound to apply to all variables
1150    pub fn set_primitives(&mut self, ids: Vec<ID>, bound: Bound) {
1151        let unique_ids: IndexSet<_> = ids.into_iter().collect();
1152        for id in unique_ids {
1153            self.set_primitive(id, bound);
1154        }
1155    }
1156
1157    /// Creates a general linear inequality constraint.
1158    /// 
1159    /// Creates a constraint of the form: sum(coeff_i * var_i) + bias >= 0.
1160    /// The constraint is automatically assigned a unique ID based on its content.
1161    /// 
1162    /// # Arguments
1163    /// * `coefficient_variables` - Vector of (variable_id, coefficient) pairs
1164    /// * `bias` - Constant bias term
1165    /// 
1166    /// # Returns
1167    /// The unique ID assigned to this constraint
1168    pub fn set_gelineq(&mut self, coefficient_variables: Vec<Coefficient>, bias: i64) -> ID {
1169        // Ensure coefficients have unique keys by summing duplicate values
1170        let mut unique_coefficients: IndexMap<ID, i64> = IndexMap::new();
1171        for (key, value) in coefficient_variables {
1172            *unique_coefficients.entry(key).or_insert(0) += value;
1173        }
1174        let coefficient_variables: Vec<Coefficient> = unique_coefficients.into_iter().collect();
1175
1176        // Create a hash from the input data
1177        let hash = create_hash(&coefficient_variables, bias);
1178        
1179        // Return the hash as a string
1180        let id = hash.to_string();
1181
1182        // Insert the constraint as a node
1183        self.nodes.insert(id.clone(), Node {
1184            expression: BoolExpression::Composite(Constraint { coefficients: coefficient_variables, bias: (bias, bias) }),
1185            coefficient: 0.0,
1186        });
1187
1188        return id;
1189    }
1190
1191    /// Creates an "at least" constraint: sum(variables) >= value.
1192    /// 
1193    /// # Arguments
1194    /// * `references` - Vector of variable IDs to sum
1195    /// * `value` - Minimum required sum
1196    /// 
1197    /// # Returns
1198    /// The unique ID assigned to this constraint
1199    pub fn set_atleast(&mut self, references: Vec<ID>, value: i64) -> ID {
1200        let unique_references: IndexSet<_> = references.into_iter().collect();
1201        self.set_gelineq(unique_references.into_iter().map(|x| (x, 1)).collect(), -value)
1202    }
1203
1204    /// Creates an "at most" constraint: sum(variables) <= value.
1205    /// 
1206    /// # Arguments
1207    /// * `references` - Vector of variable IDs to sum
1208    /// * `value` - Maximum allowed sum
1209    /// 
1210    /// # Returns
1211    /// The unique ID assigned to this constraint
1212    pub fn set_atmost(&mut self, references: Vec<ID>, value: i64) -> ID {
1213        let unique_references: IndexSet<_> = references.into_iter().collect();
1214        self.set_gelineq(unique_references.into_iter().map(|x| (x, -1)).collect(), value)
1215    }
1216
1217    /// Creates an equality constraint: sum(variables) == value.
1218    /// 
1219    /// Implemented as the conjunction of "at least" and "at most" constraints.
1220    /// 
1221    /// # Arguments
1222    /// * `references` - Vector of variable IDs to sum
1223    /// * `value` - Required exact sum
1224    /// 
1225    /// # Returns
1226    /// The unique ID assigned to this constraint
1227    pub fn set_equal(&mut self, references: Vec<ID>, value: i64) -> ID {
1228        let unique_references: IndexSet<_> = references.into_iter().collect();
1229        let ub = self.set_atleast(unique_references.clone().into_iter().collect(), value);
1230        let lb = self.set_atmost(unique_references.into_iter().collect(), value);
1231        self.set_and(vec![ub, lb])
1232    }
1233
1234    /// Creates a logical AND constraint.
1235    /// 
1236    /// Returns true if and only if ALL referenced variables are true.
1237    /// Implemented as: sum(variables) >= count(variables).
1238    /// 
1239    /// # Arguments
1240    /// * `references` - Vector of variable IDs to AND together
1241    /// 
1242    /// # Returns
1243    /// The unique ID assigned to this constraint
1244    pub fn set_and(&mut self, references: Vec<ID>) -> ID {
1245        let unique_references: IndexSet<_> = references.into_iter().collect();
1246        let length = unique_references.len();
1247        self.set_atleast(unique_references.into_iter().collect(), length as i64)
1248    }
1249
1250    /// Creates a logical OR constraint.
1251    /// 
1252    /// Returns true if AT LEAST ONE of the referenced variables is true.
1253    /// Implemented as: sum(variables) >= 1.
1254    /// 
1255    /// # Arguments
1256    /// * `references` - Vector of variable IDs to OR together
1257    /// 
1258    /// # Returns
1259    /// The unique ID assigned to this constraint
1260    pub fn set_or(&mut self, references: Vec<ID>) -> ID {
1261        let unique_references: IndexSet<_> = references.into_iter().collect();
1262        self.set_atleast(unique_references.into_iter().collect(), 1)
1263    }
1264
1265    /// Creates a logical NAND constraint.
1266    /// 
1267    /// Returns true if NOT ALL of the referenced variables are true.
1268    /// Implemented as: sum(variables) <= count(variables) - 1.
1269    /// 
1270    /// # Arguments
1271    /// * `references` - Vector of variable IDs to NAND together
1272    /// 
1273    /// # Returns
1274    /// The unique ID assigned to this constraint
1275    pub fn set_nand(&mut self, references: Vec<ID>) -> ID {
1276        let unique_references: IndexSet<_> = references.into_iter().collect();
1277        let length = unique_references.len();
1278        self.set_atmost(unique_references.into_iter().collect(), length as i64 - 1)
1279    }
1280    
1281    /// Creates a logical NOR constraint.
1282    /// 
1283    /// Returns true if NONE of the referenced variables are true.
1284    /// Implemented as: sum(variables) <= 0.
1285    /// 
1286    /// # Arguments
1287    /// * `references` - Vector of variable IDs to NOR together
1288    /// 
1289    /// # Returns
1290    /// The unique ID assigned to this constraint
1291    pub fn set_nor(&mut self, references: Vec<ID>) -> ID {
1292        let unique_references: IndexSet<_> = references.into_iter().collect();
1293        self.set_atmost(unique_references.into_iter().collect(), 0)
1294    }
1295
1296    /// Creates a logical NOT constraint.
1297    /// 
1298    /// Returns true if NONE of the referenced variables are true.
1299    /// Functionally equivalent to NOR. Implemented as: sum(variables) <= 0.
1300    /// 
1301    /// # Arguments
1302    /// * `references` - Vector of variable IDs to negate
1303    /// 
1304    /// # Returns
1305    /// The unique ID assigned to this constraint
1306    pub fn set_not(&mut self, references: Vec<ID>) -> ID {
1307        let unique_references: IndexSet<_> = references.into_iter().collect();
1308        self.set_atmost(unique_references.into_iter().collect(), 0)
1309    }
1310
1311    /// Creates a logical XOR constraint.
1312    /// 
1313    /// Returns true if EXACTLY ONE of the referenced variables is true.
1314    /// Implemented as the conjunction of OR and "at most 1" constraints.
1315    /// 
1316    /// # Arguments
1317    /// * `references` - Vector of variable IDs to XOR together
1318    /// 
1319    /// # Returns
1320    /// The unique ID assigned to this constraint
1321    pub fn set_xor(&mut self, references: Vec<ID>) -> ID {
1322        let unique_references: IndexSet<_> = references.into_iter().collect();
1323        let atleast = self.set_or(unique_references.clone().into_iter().collect());
1324        let atmost = self.set_atmost(unique_references.into_iter().collect(), 1);
1325        self.set_and(vec![atleast, atmost])
1326    }
1327
1328    /// Creates a logical XNOR constraint.
1329    /// 
1330    /// Returns true if an EVEN NUMBER of the referenced variables are true
1331    /// (including zero). Implemented as: (sum >= 2) OR (sum <= 0).
1332    /// 
1333    /// # Arguments
1334    /// * `references` - Vector of variable IDs to XNOR together
1335    /// 
1336    /// # Returns
1337    /// The unique ID assigned to this constraint
1338    pub fn set_xnor(&mut self, references: Vec<ID>) -> ID {
1339        let unique_references: IndexSet<_> = references.into_iter().collect();
1340        let atleast = self.set_atleast(unique_references.clone().into_iter().collect(), 2);
1341        let atmost = self.set_atmost(unique_references.into_iter().collect(), 0);
1342        self.set_or(vec![atleast, atmost])
1343    }
1344
1345    /// Creates a logical IMPLICATION constraint: condition -> consequence.
1346    /// 
1347    /// Returns true if the condition is false OR the consequence is true.
1348    /// Implemented as: NOT(condition) OR consequence.
1349    /// 
1350    /// # Arguments
1351    /// * `condition` - The condition variable ID
1352    /// * `consequence` - The consequence variable ID
1353    /// 
1354    /// # Returns
1355    /// The unique ID assigned to this constraint
1356    pub fn set_imply(&mut self, condition: ID, consequence: ID) -> ID {
1357        let not_condition = self.set_not(vec![condition]);
1358        self.set_or(vec![not_condition, consequence])
1359    }
1360
1361    /// Creates a logical EQUIVALENCE constraint: lhs <-> rhs.
1362    /// 
1363    /// Returns true if both variables have the same truth value.
1364    /// Implemented as: (lhs -> rhs) AND (rhs -> lhs).
1365    /// 
1366    /// # Arguments
1367    /// * `lhs` - The left-hand side variable ID
1368    /// * `rhs` - The right-hand side variable ID
1369    /// 
1370    /// # Returns
1371    /// The unique ID assigned to this constraint
1372    pub fn set_equiv(&mut self, lhs: ID, rhs: ID) -> ID {
1373        let imply_lr = self.set_imply(lhs.clone(), rhs.clone());
1374        let imply_rl = self.set_imply(rhs.clone(), lhs.clone());
1375        self.set_and(vec![imply_lr, imply_rl])
1376    }
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381    use super::*;
1382
1383    /// Helper: for every primitive combination,
1384    ///   1) run `propagate` on the PLDAG model  
1385    ///   2) build the corresponding assignments  
1386    ///   3) run `assume(root=1)` on the polyhedron  
1387    ///   4) evaluate the shrunken polyhedron on the same assignments  
1388    ///   5) assert they agree at `root`.
1389    fn evaluate_model_polyhedron(
1390        model: &Pldag,
1391        poly: &DensePolyhedron,
1392        root: &String
1393    ) {
1394        for combo in model.primitive_combinations() {
1395            // build an IndexMap<String,Bound> as propagate expects
1396            let interp = combo.iter()
1397                .map(|(k,&v)| (k.clone(), (v,v)))
1398                .collect::<IndexMap<String,Bound>>();
1399
1400            // what the DAG says the root can be
1401            let prop = model.propagate(&interp);
1402            let model_root_val = *prop.get(root).unwrap();
1403
1404            // now shrink the polyhedron by assuming root=1
1405            let mut assumption = HashMap::new();
1406            assumption.insert(root.clone(), 1);
1407            let shrunk = poly.assume(&assumption);
1408
1409            // and evaluate that shrunk system on the same propagated bounds
1410            let poly_val = shrunk.evaluate(&prop);
1411            assert_eq!(
1412                poly_val,
1413                model_root_val,
1414                "Disagreement on {:?}: model={:?}, poly={:?}",
1415                combo,
1416                model_root_val,
1417                poly_val
1418            );
1419        }
1420    }
1421
1422    #[test]
1423    fn test_propagate() {
1424        let mut model = Pldag::new();
1425        model.set_primitive("x".to_string(), (0, 1));
1426        model.set_primitive("y".to_string(), (0, 1));
1427        let root = model.set_and(vec!["x".to_string(), "y".to_string()]);
1428
1429        let result = model.propagate(&IndexMap::new());
1430        assert_eq!(result.get("x").unwrap(), &(0, 1));
1431        assert_eq!(result.get("y").unwrap(), &(0, 1));
1432        assert_eq!(result.get(&root).unwrap(), &(0, 1));
1433
1434        let mut assignments = IndexMap::new();
1435        assignments.insert("x".to_string(), (1, 1));
1436        assignments.insert("y".to_string(), (1, 1));
1437        let result = model.propagate(&assignments);
1438        assert_eq!(result.get(&root).unwrap(), &(1, 1));
1439
1440        let mut model = Pldag::new();
1441        model.set_primitive("x".to_string(), (0, 1));
1442        model.set_primitive("y".to_string(), (0, 1));
1443        model.set_primitive("z".to_string(), (0, 1));
1444        let root = model.set_xor(vec!["x".to_string(), "y".to_string(), "z".to_string()]);
1445        let result = model.propagate(&IndexMap::new());
1446        assert_eq!(result.get("x").unwrap(), &(0, 1));
1447        assert_eq!(result.get("y").unwrap(), &(0, 1));
1448        assert_eq!(result.get("z").unwrap(), &(0, 1));
1449        assert_eq!(result.get(&root).unwrap(), &(0, 1));
1450
1451        let mut assignments = IndexMap::new();
1452        assignments.insert("x".to_string(), (1, 1));
1453        assignments.insert("y".to_string(), (1, 1));
1454        assignments.insert("z".to_string(), (1, 1));
1455        let result = model.propagate(&assignments);
1456        assert_eq!(result.get(&root).unwrap(), &(0, 0));
1457        
1458        let mut assignments = IndexMap::new();
1459        assignments.insert("x".to_string(), (0, 1));
1460        assignments.insert("y".to_string(), (1, 1));
1461        assignments.insert("z".to_string(), (1, 1));
1462        let result = model.propagate(&assignments);
1463        assert_eq!(result.get(&root).unwrap(), &(0, 0));
1464        
1465        let mut assignments = IndexMap::new();
1466        assignments.insert("x".to_string(), (0, 0));
1467        assignments.insert("y".to_string(), (1, 1));
1468        assignments.insert("z".to_string(), (0, 0));
1469        let result = model.propagate(&assignments);
1470        assert_eq!(result.get(&root).unwrap(), &(1, 1));
1471    }
1472
1473    /// XOR already covered; test the OR gate
1474    #[test]
1475    fn test_propagate_or_gate() {
1476        let mut model = Pldag::new();
1477        model.set_primitive("a".into(), (0, 1));
1478        model.set_primitive("b".into(), (0, 1));
1479        let or_root = model.set_or(vec!["a".into(), "b".into()]);
1480
1481        // No assignment: both inputs full [0,1], output [0,1]
1482        let res = model.propagate(&IndexMap::new());
1483        assert_eq!(res["a"], (0, 1));
1484        assert_eq!(res["b"], (0, 1));
1485        assert_eq!(res[&or_root], (0, 1));
1486
1487        // a=1 ⇒ output must be 1
1488        let mut interp = IndexMap::new();
1489        interp.insert("a".into(), (1, 1));
1490        let res = model.propagate(&interp);
1491        assert_eq!(res[&or_root], (1, 1));
1492
1493        // both zero ⇒ output zero
1494        let mut interp = IndexMap::new();
1495        interp.insert("a".into(), (0, 0));
1496        interp.insert("b".into(), (0, 0));
1497        let res = model.propagate(&interp);
1498        assert_eq!(res[&or_root], (0, 0));
1499
1500        // partial: a=[0,1], b=0 ⇒ output=[0,1]
1501        let mut interp = IndexMap::new();
1502        interp.insert("b".into(), (0, 0));
1503        let res = model.propagate(&interp);
1504        assert_eq!(res[&or_root], (0, 1));
1505    }
1506
1507    /// Test the NOT gate (negation)
1508    #[test]
1509    fn test_propagate_not_gate() {
1510        let mut model = Pldag::new();
1511        model.set_primitive("p".into(), (0, 1));
1512        let not_root = model.set_not(vec!["p".into()]);
1513
1514        // no assignment ⇒ [0,1]
1515        let res = model.propagate(&IndexMap::new());
1516        assert_eq!(res["p"], (0, 1));
1517        assert_eq!(res[&not_root], (0, 1));
1518
1519        // p = 0 ⇒ root = 1
1520        let mut interp = IndexMap::new();
1521        interp.insert("p".into(), (0, 0));
1522        let res = model.propagate(&interp);
1523        assert_eq!(res[&not_root], (1, 1));
1524
1525        // p = 1 ⇒ root = 0
1526        let mut interp = IndexMap::new();
1527        interp.insert("p".into(), (1, 1));
1528        let res = model.propagate(&interp);
1529        assert_eq!(res[&not_root], (0, 0));
1530    }
1531
1532    #[test]
1533    fn test_to_polyhedron_and() {
1534        let mut m = Pldag::new();
1535        m.set_primitive("x".into(), (0,1));
1536        m.set_primitive("y".into(), (0,1));
1537        let root = m.set_and(vec!["x".into(), "y".into()]);
1538        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1539        evaluate_model_polyhedron(&m, &poly, &root);
1540    }
1541
1542    #[test]
1543    fn test_to_polyhedron_or() {
1544        let mut m = Pldag::new();
1545        m.set_primitive("a".into(), (0,1));
1546        m.set_primitive("b".into(), (0,1));
1547        m.set_primitive("c".into(), (0,1));
1548        let root = m.set_or(vec!["a".into(), "b".into(), "c".into()]);
1549        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1550        evaluate_model_polyhedron(&m, &poly, &root);
1551    }
1552
1553    #[test]
1554    fn test_to_polyhedron_not() {
1555        let mut m = Pldag::new();
1556        m.set_primitive("p".into(), (0,1));
1557        let root = m.set_not(vec!["p".into()]);
1558        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1559        evaluate_model_polyhedron(&m, &poly, &root);
1560    }
1561
1562    #[test]
1563    fn test_to_polyhedron_xor() {
1564        let mut m = Pldag::new();
1565        m.set_primitive("x".into(), (0,1));
1566        m.set_primitive("y".into(), (0,1));
1567        m.set_primitive("z".into(), (0,1));
1568        let root = m.set_xor(vec!["x".into(), "y".into(), "z".into()]);
1569        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1570        evaluate_model_polyhedron(&m, &poly, &root);
1571    }
1572
1573    #[test]
1574    fn test_to_polyhedron_nested() {
1575        // Build a small two‐level circuit:
1576        //   w = AND(x,y),  v = OR(w, NOT(z))
1577        let mut m = Pldag::new();
1578        m.set_primitive("x".into(), (0,1));
1579        m.set_primitive("y".into(), (0,1));
1580        m.set_primitive("z".into(), (0,1));
1581
1582        let w = m.set_and(vec!["x".into(), "y".into()]);
1583        let nz = m.set_not(vec!["z".into()]);
1584        let v = m.set_or(vec![w.clone(), nz.clone()]);
1585
1586        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1587        evaluate_model_polyhedron(&m, &poly, &v);
1588    }
1589
1590    /// Nested/composed AND then XOR: 
1591    ///   w = AND(x,y);  v = XOR(w,z)
1592    #[test]
1593    fn test_propagate_nested_composite() {
1594        let mut model = Pldag::new();
1595        model.set_primitive("x".into(), (0, 1));
1596        model.set_primitive("y".into(), (0, 1));
1597        model.set_primitive("z".into(), (0, 1));
1598
1599        let w = model.set_and(vec!["x".into(), "y".into()]);
1600        let v = model.set_xor(vec![w.clone(), "z".into()]);
1601
1602        // no assignment: everything [0,1]
1603        let res = model.propagate(&IndexMap::new());
1604        for var in &["x","y","z"] {
1605            assert_eq!(res[*var], (0,1), "{}", var);
1606        }
1607        assert_eq!(res[&w], (0,1));
1608        assert_eq!(res[&v], (0,1));
1609
1610        // x=1,y=1,z=0 ⇒ w=1,v=1
1611        let mut interp = IndexMap::new();
1612        interp.insert("x".into(), (1,1));
1613        interp.insert("y".into(), (1,1));
1614        interp.insert("z".into(), (0,0));
1615        let res = model.propagate(&interp);
1616        assert_eq!(res[&w], (1,1));
1617        assert_eq!(res[&v], (1,1));
1618
1619        // x=0,y=1,z=1 ⇒ w=0,v=1
1620        let mut interp = IndexMap::new();
1621        interp.insert("x".into(), (0,0));
1622        interp.insert("y".into(), (1,1));
1623        interp.insert("z".into(), (1,1));
1624        let res = model.propagate(&interp);
1625        assert_eq!(res[&w], (0,0));
1626        assert_eq!(res[&v], (1,1));
1627
1628        // x=0,y=0,z=0 ⇒ w=0,v=0
1629        let mut interp = IndexMap::new();
1630        interp.insert("x".into(), (0,0));
1631        interp.insert("y".into(), (0,0));
1632        interp.insert("z".into(), (0,0));
1633        let res = model.propagate(&interp);
1634        assert_eq!(res[&w], (0,0));
1635        assert_eq!(res[&v], (0,0));
1636    }
1637
1638    /// If you ever get an inconsistent assignment (out‐of‐bounds for a primitive),
1639    /// propagate should leave it as given (or you could choose to clamp / panic)—here
1640    /// we simply check that nothing blows up.
1641    #[test]
1642    fn test_propagate_out_of_bounds_does_not_crash() {
1643        let mut model = Pldag::new();
1644        model.set_primitive("u".into(), (0, 1));
1645        let root = model.set_not(vec!["u".into()]);
1646
1647        let mut interp = IndexMap::new();
1648        // ← deliberately illegal: u ∈ {0,1} but we assign 5
1649        interp.insert("u".into(), (5,5));
1650        let res = model.propagate(&interp);
1651
1652        // we expect propagate to return exactly (5,5) for "u" and compute root = negate(5)
1653        assert_eq!(res["u"], (5,5));
1654        // Depending on your semantic for negate, it might be
1655        //   bound_multiply(-1,(5,5)) + bias
1656        // so just check it didn’t panic:
1657        let _ = res[&root];
1658    }
1659
1660    #[test]
1661    fn test_to_polyhedron() {
1662
1663        fn evaluate_model_polyhedron(model: &Pldag, polyhedron: &DensePolyhedron, root: &String) {
1664            for combination in model.primitive_combinations() {
1665                let assignments = combination
1666                    .iter()
1667                    .map(|(k, &v)| (k.clone(), (v, v)))
1668                    .collect::<IndexMap<String, Bound>>();
1669                let model_prop = model.propagate(&assignments);
1670                let model_eval = *model_prop.get(root).unwrap();
1671                let mut assumption = HashMap::new();
1672                assumption.insert(root.clone(), 1);
1673                let assumed_polyhedron = polyhedron.assume(&assumption);
1674                let assumed_poly_eval = assumed_polyhedron.evaluate(&model_prop);
1675                assert_eq!(assumed_poly_eval, model_eval);
1676            }
1677        }
1678
1679        let mut model: Pldag = Pldag::new();
1680        model.set_primitive("x".to_string(), (0, 1));
1681        model.set_primitive("y".to_string(), (0, 1));
1682        model.set_primitive("z".to_string(), (0, 1));
1683        let root = model.set_xor(vec!["x".to_string(), "y".to_string(), "z".to_string()]);
1684        let polyhedron: DensePolyhedron = model.to_sparse_polyhedron_default().into();
1685        evaluate_model_polyhedron(&model, &polyhedron, &root);
1686
1687        let mut model = Pldag::new();
1688        model.set_primitive("x".to_string(), (0, 1));
1689        model.set_primitive("y".to_string(), (0, 1));
1690        let root = model.set_and(vec!["x".to_string(), "y".to_string()]);
1691        let polyhedron = model.to_sparse_polyhedron_default().into();
1692        evaluate_model_polyhedron(&model, &polyhedron, &root);
1693
1694        let mut model: Pldag = Pldag::new();
1695        model.set_primitive("x".to_string(), (0, 1));
1696        model.set_primitive("y".to_string(), (0, 1));
1697        model.set_primitive("z".to_string(), (0, 1));
1698        let root = model.set_xor(vec!["x".to_string(), "y".to_string(), "z".to_string()]);
1699        let polyhedron = model.to_sparse_polyhedron_default().into();
1700        evaluate_model_polyhedron(&model, &polyhedron, &root);
1701    }
1702
1703    /// Single‐operand composites should act as identity: root == operand
1704    #[test]
1705    fn test_to_polyhedron_single_operand_identity() {
1706        // AND(x) == x
1707        {
1708            let mut m = Pldag::new();
1709            m.set_primitive("x".into(), (0,1));
1710            let root = m.set_and(vec!["x".into()]);
1711            let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1712            evaluate_model_polyhedron(&m, &poly, &root);
1713        }
1714        // OR(y) == y
1715        {
1716            let mut m = Pldag::new();
1717            m.set_primitive("y".into(), (0,1));
1718            let root = m.set_or(vec!["y".into()]);
1719            let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1720            evaluate_model_polyhedron(&m, &poly, &root);
1721        }
1722        // XOR(z) == z
1723        {
1724            let mut m = Pldag::new();
1725            m.set_primitive("z".into(), (0,1));
1726            let root = m.set_xor(vec!["z".into()]);
1727            let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1728            evaluate_model_polyhedron(&m, &poly, &root);
1729        }
1730    }
1731
1732    /// Duplicate‐operand AND(x,x) should also behave like identity(x)
1733    #[test]
1734    fn test_to_polyhedron_duplicate_operands_and() {
1735        let mut m = Pldag::new();
1736        m.set_primitive("x".into(), (0,1));
1737        let root = m.set_and(vec!["x".into(), "x".into()]);
1738        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1739        evaluate_model_polyhedron(&m, &poly, &root);
1740    }
1741
1742    /// Deeply nested 5‐level chain:
1743    ///    w1 = AND(a,b)
1744    ///    w2 = OR(w1,c)
1745    ///    w3 = XOR(w2,d)
1746    ///    root = NOT(w3)
1747    #[test]
1748    fn test_to_polyhedron_deeply_nested_chain() {
1749        let mut m = Pldag::new();
1750        // primitives a,b,c,d,e  (e unused but shows extra var)
1751        for &v in &["a","b","c","d","e"] {
1752            m.set_primitive(v.into(), (0,1));
1753        }
1754        let a = "a".to_string();
1755        let b = "b".to_string();
1756        let c = "c".to_string();
1757        let d = "d".to_string();
1758
1759        let w1 = m.set_and(vec![a.clone(), b.clone()]);
1760        let w2 = m.set_or(vec![w1.clone(), c.clone()]);
1761        let w3 = m.set_xor(vec![w2.clone(), d.clone()]);
1762        let root = m.set_not(vec![w3.clone()]);
1763
1764        let poly: DensePolyhedron = m.to_sparse_polyhedron_default().into();
1765        evaluate_model_polyhedron(&m, &poly, &root);
1766    }
1767
1768    #[test]
1769    fn make_simple_dag() {
1770        let mut pldag = Pldag::new();
1771        pldag.set_primitive("b".into(), (0, 1));
1772        pldag.set_primitive("d".into(), (0, 1));
1773        pldag.set_primitive("e".into(), (0, 1));
1774        let c = pldag.set_or(vec!["d".into(), "e".into()]);
1775        let a = pldag.set_or(vec!["b".into(), c.clone()]);
1776        let deps = pldag.transitive_dependencies();
1777        let expect = |xs: &[&str]| {
1778            xs.iter().cloned().map(String::from).collect::<HashSet<_>>()
1779        };
1780        assert_eq!(deps.get(&a), Some(&expect(&["b", &c.to_string(), "d", "e"])));
1781        assert_eq!(deps.get(&c), Some(&expect(&["d", "e"])));
1782        assert_eq!(deps.get("b"), Some(&expect(&[])));
1783        assert_eq!(deps.get("d"), Some(&expect(&[])));
1784        assert_eq!(deps.get("e"), Some(&expect(&[])));
1785    }
1786
1787    #[test]
1788    fn test_chain_dag() {
1789        // x → [y], y → [z], z → []
1790        let mut pldag = Pldag::new();
1791        pldag.set_primitive("z".into(), (0, 0));;
1792        let y = pldag.set_or(vec!["z".into()]);
1793        let x = pldag.set_or(vec![y.clone()]);
1794        let deps = pldag.transitive_dependencies();
1795
1796        let expect = |xs: &[&str]| {
1797            xs.iter().cloned().map(String::from).collect::<HashSet<_>>()
1798        };
1799
1800        assert_eq!(deps.get(&x), Some(&expect(&[&y.to_string(), "z"])));
1801        assert_eq!(deps.get(&y), Some(&expect(&["z"])));
1802        assert_eq!(deps.get("z"), Some(&expect(&[])));
1803    }
1804
1805    #[test]
1806    fn test_all_primitives() {
1807        // no Composite at all
1808        let mut pldag = Pldag::new();
1809        for &name in &["p", "q", "r"] {
1810            pldag.nodes.insert(name.into(), Node {
1811                expression: BoolExpression::Primitive((1, 5)),
1812                coefficient: 0.0,
1813            });
1814        }
1815        let deps = pldag.transitive_dependencies();
1816
1817        for &name in &["p", "q", "r"] {
1818            assert!(deps.get(name).unwrap().is_empty(), "{} should have no deps", name);
1819        }
1820    }
1821
1822    #[test]
1823    fn test_propagate_weighted() {
1824        let mut model = Pldag::new();
1825        model.set_primitive("x".to_string(), (0, 1));
1826        model.set_primitive("y".to_string(), (0, 1));
1827        let root = model.set_and(vec!["x".to_string(), "y".to_string()]);
1828        
1829        // Set coefficients for nodes
1830        model.set_coef("x".to_string(), 2.0);
1831        model.set_coef("y".to_string(), 3.0);
1832        
1833        let mut assignments = IndexMap::new();
1834        assignments.insert("x".to_string(), (1, 1));
1835        assignments.insert("y".to_string(), (1, 1));
1836        
1837        let propagated = model.propagate_coefs(&assignments);
1838        
1839        // Check the results: bounds should be (1,1) and coefficients should be accumulated
1840        assert_eq!(propagated.get("x").unwrap().0, (1, 1)); // bounds
1841        assert_eq!(propagated.get("x").unwrap().1, (2.0, 2.0)); // coefficients
1842        assert_eq!(propagated.get("y").unwrap().0, (1, 1)); // bounds
1843        assert_eq!(propagated.get("y").unwrap().1, (3.0, 3.0)); // coefficients
1844        assert_eq!(propagated.get(&root).unwrap().0, (1, 1)); // bounds
1845        assert_eq!(propagated.get(&root).unwrap().1, (5.0, 5.0)); // accumulated coefficients
1846    }
1847
1848    #[test]
1849    fn test_readme_example() {
1850        // Build your PL-DAG (omitting details)...
1851        // For example, we create a model of three boolean variables x, y and z.
1852        // We bind them to an xor constraint.
1853        let mut pldag: Pldag = Pldag::new();
1854
1855        // First setup the primitive variables
1856        pldag.set_primitive("x".to_string(), (0, 1));
1857        pldag.set_primitive("y".to_string(), (0, 1));
1858        pldag.set_primitive("z".to_string(), (0, 1));
1859
1860        // A reference ID is returned
1861        let root = pldag.set_or(vec![
1862            "x".to_string(),
1863            "y".to_string(),
1864            "z".to_string(),
1865        ]);
1866
1867        // 1. Validate a combination:
1868        let mut inputs: IndexMap<String, Bound> = IndexMap::new();
1869        let validited = pldag.propagate(&inputs);
1870        // Since nothing is given, and all other variable inplicitly is (0, 1) from the pldag model,
1871        // the root will be (0,1) since there's not enough information to evalute the root `or` node.
1872        println!("Root valid? {}", *validited.get(&root).unwrap() == (1, 1)); // This will be False
1873
1874        // If we however fix x to be zero, then the root is false
1875        inputs.insert("x".to_string(), (0,0));
1876        let revalidited = pldag.propagate(&inputs);
1877        println!("Root valid? {}", *revalidited.get(&root).unwrap() == (1, 1)); // This will be false
1878
1879        // However, fixing y and z to 1 will yield the root node to be true (since the root will be true if any of x, y or z is true).
1880        inputs.insert("y".to_string(), (1,1));
1881        inputs.insert("z".to_string(), (1,1));
1882        let revalidited = pldag.propagate(&inputs);
1883        println!("Root valid? {}", *revalidited.get(&root).unwrap() == (1, 1)); // This will be true
1884
1885        // 2. Score a configuration:
1886        // We can score a configuration by setting coefficients on nodes.
1887        pldag.set_coef("x".to_string(), 1.0);
1888        pldag.set_coef("y".to_string(), 2.0);
1889        pldag.set_coef("z".to_string(), 3.0);
1890        // Add a discount value if the root is true
1891        pldag.set_coef(root.clone(), -1.0);
1892        let scores = pldag.propagate_coefs(&inputs);
1893        println!("Total score: {:?}", scores.get(&root).unwrap().1); // .1 is the coefficient part
1894
1895        // And notice what will happen if we remove the x value (i.e. x being (0,1))
1896        inputs.insert("x".to_string(), (0,1));
1897        let scores = pldag.propagate_coefs(&inputs);
1898        // The root will return bounds with coefficient range meaning the value is between bounds with not enough information to
1899        // determine the exact value. 
1900        println!("Total score: {:?}", scores.get(&root).unwrap().1); // .1 is the coefficient part
1901
1902        // .. and if we set x to be 0, then the root will be definitely determined.
1903        inputs.insert("x".to_string(), (0,0));
1904        let scores = pldag.propagate_coefs(&inputs);
1905        println!("Total score: {:?}", scores.get(&root).unwrap().1); // .1 is the coefficient part
1906
1907        // .. and if we set y and z to be 0, then the root will be 0.
1908        inputs.insert("y".to_string(), (0,0));
1909        inputs.insert("z".to_string(), (0,0));
1910        let scores = pldag.propagate_coefs(&inputs);
1911        println!("Total score: {:?}", scores.get(&root).unwrap().1); // .1 is the coefficient part
1912    }
1913
1914    #[test]
1915    fn test_get_objective() {
1916        let mut model = Pldag::new();
1917        
1918        // Create primitive nodes with different coefficients
1919        model.set_primitive("x".to_string(), (0, 1));
1920        model.set_primitive("y".to_string(), (0, 1));
1921        model.set_primitive("z".to_string(), (0, 1));
1922        
1923        // Create a composite node (should not appear in coefficients)
1924        let root = model.set_and(vec!["x".to_string(), "y".to_string()]);
1925        
1926        // Initially, all objective coefficients should be 0.0 (default)
1927        let coeffs = model.get_objective();
1928        assert_eq!(coeffs.len(), 3); // Only primitives should be included
1929        assert_eq!(coeffs.get("x"), Some(&0.0));
1930        assert_eq!(coeffs.get("y"), Some(&0.0));
1931        assert_eq!(coeffs.get("z"), Some(&0.0));
1932        assert_eq!(coeffs.get(&root), None); // Composite nodes should not be included
1933        
1934        // Set some coefficients
1935        model.set_coef("x".to_string(), 2.5);
1936        model.set_coef("y".to_string(), -1.0);
1937        model.set_coef("z".to_string(), 3.14);
1938        model.set_coef(root.clone(), 10.0); // This should not appear in get_objective
1939        
1940        // Test that get_objective returns the updated values for primitives only
1941        let coeffs = model.get_objective();
1942        assert_eq!(coeffs.len(), 3); // Still only primitives
1943        assert_eq!(coeffs.get("x"), Some(&2.5));
1944        assert_eq!(coeffs.get("y"), Some(&-1.0));
1945        assert_eq!(coeffs.get("z"), Some(&3.14));
1946        assert_eq!(coeffs.get(&root), None); // Composite still not included
1947        
1948        // Verify the order is preserved (IndexMap should maintain insertion order)
1949        let keys: Vec<&String> = coeffs.keys().collect();
1950        assert_eq!(keys, vec!["x", "y", "z"]);
1951        
1952        // Test with a model that has only composite nodes
1953        let mut model_no_primitives = Pldag::new();
1954        model_no_primitives.set_primitive("a".to_string(), (0, 1));
1955        model_no_primitives.set_primitive("b".to_string(), (0, 1));
1956        let composite1 = model_no_primitives.set_and(vec!["a".to_string(), "b".to_string()]);
1957        let composite2 = model_no_primitives.set_or(vec![composite1.clone()]);
1958        
1959        // Remove the primitives by replacing them with composites
1960        // (This is a bit artificial, but tests the filtering logic)
1961        let coeffs_mixed = model_no_primitives.get_objective();
1962        assert_eq!(coeffs_mixed.len(), 2); // Should only include "a" and "b" (primitives)
1963        assert!(coeffs_mixed.contains_key("a"));
1964        assert!(coeffs_mixed.contains_key("b"));
1965        assert!(!coeffs_mixed.contains_key(&composite1));
1966        assert!(!coeffs_mixed.contains_key(&composite2));
1967    }
1968
1969    
1970}