Skip to main content

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(
40        &self,
41        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
42    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
43        crate::rules::traits::validate_target_witness(
44            self.target_problem(),
45            target_solution,
46            |value| value.value.is_some(),
47            "target ILP assignment is infeasible",
48        )?;
49
50        Ok({
51            self.source_variables
52                .iter()
53                .map(|name| target_solution[self.variable_map[name]] == 1)
54                .collect()
55        })
56    }
57}
58
59/// Builder that accumulates ILP variables and constraints.
60struct ILPBuilder {
61    num_vars: usize,
62    constraints: Vec<LinearConstraint>,
63    variable_map: HashMap<String, usize>,
64}
65
66impl ILPBuilder {
67    fn new() -> Self {
68        Self {
69            num_vars: 0,
70            constraints: Vec::new(),
71            variable_map: HashMap::new(),
72        }
73    }
74
75    /// Get or create a variable index for a named circuit variable.
76    fn get_or_create_var(&mut self, name: &str) -> Result<usize, crate::rules::ReductionError> {
77        if let Some(&idx) = self.variable_map.get(name) {
78            Ok(idx)
79        } else {
80            let idx = self.alloc_aux()?;
81            self.variable_map.insert(name.to_string(), idx);
82            Ok(idx)
83        }
84    }
85
86    /// Allocate an anonymous auxiliary variable.
87    fn alloc_aux(&mut self) -> Result<usize, crate::rules::ReductionError> {
88        let idx = self.num_vars;
89        self.num_vars = self.num_vars.checked_add(1).ok_or_else(|| {
90            crate::rules::ReductionError::integer_overflow::<CircuitSAT, ILP<bool>>(
91                "allocating a circuit ILP variable",
92            )
93        })?;
94        Ok(idx)
95    }
96
97    /// Recursively process a BooleanExpr, returning the ILP variable index
98    /// that holds the expression's value.
99    fn process_expr(&mut self, expr: &BooleanExpr) -> Result<usize, crate::rules::ReductionError> {
100        Ok(match &expr.op {
101            BooleanOp::Var(name) => self.get_or_create_var(name)?,
102            BooleanOp::Const(value) => {
103                let c = self.alloc_aux()?;
104                let v = if *value { 1 } else { 0 };
105                self.constraints.push(LinearConstraint::eq(vec![(c, 1)], v));
106                c
107            }
108            BooleanOp::Not(inner) => {
109                let a = self.process_expr(inner)?;
110                let c = self.alloc_aux()?;
111                // c + a = 1
112                self.constraints
113                    .push(LinearConstraint::eq(vec![(c, 1), (a, 1)], 1));
114                c
115            }
116            BooleanOp::And(args) => {
117                let inputs: Vec<usize> = args
118                    .iter()
119                    .map(|arg| self.process_expr(arg))
120                    .collect::<Result<_, _>>()?;
121                let c = self.alloc_aux()?;
122                let k = <CircuitSAT as ReduceTo<ILP<bool>>>::exact_i64(
123                    inputs.len(),
124                    "encoding a circuit gate arity",
125                )?;
126                // c ≤ a_i for all i
127                for &a_i in &inputs {
128                    self.constraints
129                        .push(LinearConstraint::le(vec![(c, 1), (a_i, -1)], 0));
130                }
131                // c ≥ Σa_i - (k - 1)
132                let mut terms: Vec<(usize, i64)> = vec![(c, 1)];
133                for &a_i in &inputs {
134                    terms.push((a_i, -1));
135                }
136                self.constraints.push(LinearConstraint::ge(terms, 1 - k));
137                c
138            }
139            BooleanOp::Or(args) => {
140                let inputs: Vec<usize> = args
141                    .iter()
142                    .map(|arg| self.process_expr(arg))
143                    .collect::<Result<_, _>>()?;
144                let c = self.alloc_aux()?;
145                // c ≥ a_i for all i
146                for &a_i in &inputs {
147                    self.constraints
148                        .push(LinearConstraint::ge(vec![(c, 1), (a_i, -1)], 0));
149                }
150                // c ≤ Σa_i
151                let mut terms: Vec<(usize, i64)> = vec![(c, 1)];
152                for &a_i in &inputs {
153                    terms.push((a_i, -1));
154                }
155                self.constraints.push(LinearConstraint::le(terms, 0));
156                c
157            }
158            BooleanOp::Xor(args) => {
159                // Chain pairwise: XOR(a1, a2, a3) = XOR(XOR(a1, a2), a3)
160                let inputs: Vec<usize> = args
161                    .iter()
162                    .map(|arg| self.process_expr(arg))
163                    .collect::<Result<_, _>>()?;
164                let mut inputs = inputs.into_iter();
165                let mut result = match inputs.next() {
166                    Some(first) => first,
167                    // False is the identity of the Boolean XOR fold.
168                    None => self.process_expr(&BooleanExpr::constant(false))?,
169                };
170                for next in inputs {
171                    let c = self.alloc_aux()?;
172                    let a = result;
173                    let b = next;
174                    // c ≤ a + b
175                    self.constraints
176                        .push(LinearConstraint::le(vec![(c, 1), (a, -1), (b, -1)], 0));
177                    // c ≥ a - b
178                    self.constraints
179                        .push(LinearConstraint::ge(vec![(c, 1), (a, -1), (b, 1)], 0));
180                    // c ≥ b - a
181                    self.constraints
182                        .push(LinearConstraint::ge(vec![(c, 1), (a, 1), (b, -1)], 0));
183                    // c ≤ 2 - a - b
184                    self.constraints
185                        .push(LinearConstraint::le(vec![(c, 1), (a, 1), (b, 1)], 2));
186                    result = c;
187                }
188                result
189            }
190        })
191    }
192}
193
194#[crate::aggregate_reduction(ilp_feasibility)]
195impl crate::rules::AggregateReductionResult for ReductionCircuitToILP {}
196
197// With N expression nodes and A roots there are N-A child edges. Each
198// gate contributes at most 12 terms per child plus one per node (including
199// constants and empty gates); each output link contributes at most two.
200// Repeated operands merge/cancel. All normalized magnitudes are at most N+2.
201#[reduction(transform = upper_bound {
202    max_constraint_magnitude_bits = "num_expression_nodes / 2 + 2",
203    num_vars = "num_variables + 2 * num_expression_nodes",
204    num_constraints = "5 * num_expression_nodes + num_assignment_outputs",
205    num_nonzeros = "13 * num_expression_nodes - 12 * num_assignments + 2 * num_assignment_outputs",
206})]
207impl ReduceTo<ILP<bool>> for CircuitSAT {
208    type Result = ReductionCircuitToILP;
209
210    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
211        let mut builder = ILPBuilder::new();
212
213        // Pre-register all circuit variables to preserve ordering
214        for name in self.variable_names() {
215            builder.get_or_create_var(name)?;
216        }
217
218        // Process each assignment
219        for assignment in &self.circuit().assignments {
220            let expr_var = builder.process_expr(&assignment.expr)?;
221            // Constrain each output to equal the expression result
222            for output_name in &assignment.outputs {
223                let out_var = builder.get_or_create_var(output_name)?;
224                if out_var != expr_var {
225                    // out = expr_var
226                    builder
227                        .constraints
228                        .push(LinearConstraint::eq(vec![(out_var, 1), (expr_var, -1)], 0));
229                }
230            }
231        }
232
233        // Trivial objective: minimize 0 (satisfaction problem)
234        let objective = vec![];
235        let target = ILP::new(
236            builder.num_vars,
237            builder.constraints,
238            objective,
239            ObjectiveSense::Minimize,
240        )
241        .map_err(<Self as ReduceTo<ILP<bool>>>::target_construction)?;
242
243        Ok(ReductionCircuitToILP {
244            target,
245            source_variables: self.variable_names().to_vec(),
246            variable_map: builder.variable_map,
247        })
248    }
249}
250
251#[cfg(feature = "example-db")]
252pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
253    use crate::export::SolutionPair;
254    use crate::models::formula::{Assignment, BooleanExpr, Circuit};
255
256    fn full_adder_circuit_sat() -> CircuitSAT {
257        let circuit = Circuit::new(vec![
258            Assignment::new(
259                vec!["t".to_string()],
260                BooleanExpr::xor(vec![BooleanExpr::var("a"), BooleanExpr::var("b")]),
261            ),
262            Assignment::new(
263                vec!["sum".to_string()],
264                BooleanExpr::xor(vec![BooleanExpr::var("t"), BooleanExpr::var("cin")]),
265            ),
266            Assignment::new(
267                vec!["ab".to_string()],
268                BooleanExpr::and(vec![BooleanExpr::var("a"), BooleanExpr::var("b")]),
269            ),
270            Assignment::new(
271                vec!["cin_t".to_string()],
272                BooleanExpr::and(vec![BooleanExpr::var("cin"), BooleanExpr::var("t")]),
273            ),
274            Assignment::new(
275                vec!["cout".to_string()],
276                BooleanExpr::or(vec![BooleanExpr::var("ab"), BooleanExpr::var("cin_t")]),
277            ),
278        ]);
279        CircuitSAT::new(circuit)
280    }
281
282    vec![crate::example_db::specs::RuleExampleSpec {
283        id: "circuitsat_to_ilp",
284        build: || {
285            crate::example_db::specs::rule_example_with_witness::<
286                _,
287                crate::models::algebraic::ILP<bool>,
288            >(
289                full_adder_circuit_sat(),
290                SolutionPair {
291                    source_config: serde_json::json!(vec![
292                        false, false, false, false, false, false, false, false
293                    ]),
294                    target_config: serde_json::json!(vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
295                },
296            )
297        },
298    }]
299}
300
301#[cfg(test)]
302#[path = "../unit_tests/rules/circuit_ilp.rs"]
303mod tests;