problemreductions/rules/
circuit_ilp.rs

1//! Reduction from CircuitSAT to ILP via gate constraint encoding.
2//!
3//! Each boolean gate is encoded as linear constraints over binary variables.
4//! The expression tree is flattened by introducing an auxiliary variable per
5//! internal node (Tseitin-style).
6//!
7//! ## Gate Encodings (all variables binary)
8//! - NOT(a) = c:           c + a = 1
9//! - AND(a₁,...,aₖ) = c:  c ≤ aᵢ (∀i), c ≥ Σaᵢ - (k-1)
10//! - OR(a₁,...,aₖ) = c:   c ≥ aᵢ (∀i), c ≤ Σaᵢ
11//! - XOR(a, b) = c:        c ≤ a+b, c ≥ a-b, c ≥ b-a, c ≤ 2-a-b
12//! - Const(v) = c:          c = v
13//!
14//! ## Objective
15//! Trivial (minimize 0): any feasible ILP solution is a satisfying assignment.
16
17use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
18use crate::models::formula::{BooleanExpr, BooleanOp, CircuitSAT};
19use crate::reduction;
20use crate::rules::traits::{ReduceTo, ReductionResult};
21use std::collections::HashMap;
22
23/// Result of reducing CircuitSAT to ILP.
24#[derive(Debug, Clone)]
25pub struct ReductionCircuitToILP {
26    target: ILP<bool>,
27    source_variables: Vec<String>,
28    variable_map: HashMap<String, usize>,
29}
30
31impl ReductionResult for ReductionCircuitToILP {
32    type Source = CircuitSAT;
33    type Target = ILP<bool>;
34
35    fn target_problem(&self) -> &ILP<bool> {
36        &self.target
37    }
38
39    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
40        self.source_variables
41            .iter()
42            .map(|name| target_solution[self.variable_map[name]])
43            .collect()
44    }
45}
46
47/// Builder that accumulates ILP variables and constraints.
48struct ILPBuilder {
49    num_vars: usize,
50    constraints: Vec<LinearConstraint>,
51    variable_map: HashMap<String, usize>,
52}
53
54impl ILPBuilder {
55    fn new() -> Self {
56        Self {
57            num_vars: 0,
58            constraints: Vec::new(),
59            variable_map: HashMap::new(),
60        }
61    }
62
63    /// Get or create a variable index for a named circuit variable.
64    fn get_or_create_var(&mut self, name: &str) -> usize {
65        if let Some(&idx) = self.variable_map.get(name) {
66            idx
67        } else {
68            let idx = self.num_vars;
69            self.variable_map.insert(name.to_string(), idx);
70            self.num_vars += 1;
71            idx
72        }
73    }
74
75    /// Allocate an anonymous auxiliary variable.
76    fn alloc_aux(&mut self) -> usize {
77        let idx = self.num_vars;
78        self.num_vars += 1;
79        idx
80    }
81
82    /// Recursively process a BooleanExpr, returning the ILP variable index
83    /// that holds the expression's value.
84    fn process_expr(&mut self, expr: &BooleanExpr) -> usize {
85        match &expr.op {
86            BooleanOp::Var(name) => self.get_or_create_var(name),
87            BooleanOp::Const(value) => {
88                let c = self.alloc_aux();
89                let v = if *value { 1.0 } else { 0.0 };
90                self.constraints
91                    .push(LinearConstraint::eq(vec![(c, 1.0)], v));
92                c
93            }
94            BooleanOp::Not(inner) => {
95                let a = self.process_expr(inner);
96                let c = self.alloc_aux();
97                // c + a = 1
98                self.constraints
99                    .push(LinearConstraint::eq(vec![(c, 1.0), (a, 1.0)], 1.0));
100                c
101            }
102            BooleanOp::And(args) => {
103                let inputs: Vec<usize> = args.iter().map(|a| self.process_expr(a)).collect();
104                let c = self.alloc_aux();
105                let k = inputs.len() as f64;
106                // c ≤ a_i for all i
107                for &a_i in &inputs {
108                    self.constraints
109                        .push(LinearConstraint::le(vec![(c, 1.0), (a_i, -1.0)], 0.0));
110                }
111                // c ≥ Σa_i - (k - 1)
112                let mut terms: Vec<(usize, f64)> = vec![(c, 1.0)];
113                for &a_i in &inputs {
114                    terms.push((a_i, -1.0));
115                }
116                self.constraints
117                    .push(LinearConstraint::ge(terms, -(k - 1.0)));
118                c
119            }
120            BooleanOp::Or(args) => {
121                let inputs: Vec<usize> = args.iter().map(|a| self.process_expr(a)).collect();
122                let c = self.alloc_aux();
123                // c ≥ a_i for all i
124                for &a_i in &inputs {
125                    self.constraints
126                        .push(LinearConstraint::ge(vec![(c, 1.0), (a_i, -1.0)], 0.0));
127                }
128                // c ≤ Σa_i
129                let mut terms: Vec<(usize, f64)> = vec![(c, 1.0)];
130                for &a_i in &inputs {
131                    terms.push((a_i, -1.0));
132                }
133                self.constraints.push(LinearConstraint::le(terms, 0.0));
134                c
135            }
136            BooleanOp::Xor(args) => {
137                // Chain pairwise: XOR(a1, a2, a3) = XOR(XOR(a1, a2), a3)
138                let inputs: Vec<usize> = args.iter().map(|a| self.process_expr(a)).collect();
139                assert!(!inputs.is_empty());
140                let mut result = inputs[0];
141                for &next in &inputs[1..] {
142                    let c = self.alloc_aux();
143                    let a = result;
144                    let b = next;
145                    // c ≤ a + b
146                    self.constraints.push(LinearConstraint::le(
147                        vec![(c, 1.0), (a, -1.0), (b, -1.0)],
148                        0.0,
149                    ));
150                    // c ≥ a - b
151                    self.constraints.push(LinearConstraint::ge(
152                        vec![(c, 1.0), (a, -1.0), (b, 1.0)],
153                        0.0,
154                    ));
155                    // c ≥ b - a
156                    self.constraints.push(LinearConstraint::ge(
157                        vec![(c, 1.0), (a, 1.0), (b, -1.0)],
158                        0.0,
159                    ));
160                    // c ≤ 2 - a - b
161                    self.constraints.push(LinearConstraint::le(
162                        vec![(c, 1.0), (a, 1.0), (b, 1.0)],
163                        2.0,
164                    ));
165                    result = c;
166                }
167                result
168            }
169        }
170    }
171}
172
173#[reduction(
174    overhead = {
175        num_vars = "num_variables + num_assignments",
176        num_constraints = "num_variables + num_assignments",
177    }
178)]
179impl ReduceTo<ILP<bool>> for CircuitSAT {
180    type Result = ReductionCircuitToILP;
181
182    fn reduce_to(&self) -> Self::Result {
183        let mut builder = ILPBuilder::new();
184
185        // Pre-register all circuit variables to preserve ordering
186        for name in self.variable_names() {
187            builder.get_or_create_var(name);
188        }
189
190        // Process each assignment
191        for assignment in &self.circuit().assignments {
192            let expr_var = builder.process_expr(&assignment.expr);
193            // Constrain each output to equal the expression result
194            for output_name in &assignment.outputs {
195                let out_var = builder.get_or_create_var(output_name);
196                if out_var != expr_var {
197                    // out = expr_var
198                    builder.constraints.push(LinearConstraint::eq(
199                        vec![(out_var, 1.0), (expr_var, -1.0)],
200                        0.0,
201                    ));
202                }
203            }
204        }
205
206        // Trivial objective: minimize 0 (satisfaction problem)
207        let objective = vec![];
208        let target = ILP::new(
209            builder.num_vars,
210            builder.constraints,
211            objective,
212            ObjectiveSense::Minimize,
213        );
214
215        ReductionCircuitToILP {
216            target,
217            source_variables: self.variable_names().to_vec(),
218            variable_map: builder.variable_map,
219        }
220    }
221}
222
223#[cfg(test)]
224#[path = "../unit_tests/rules/circuit_ilp.rs"]
225mod tests;