Skip to main content

problemreductions/rules/
circuit_sat.rs

1//! Reduction from CircuitSAT to Satisfiability via Tseitin encoding.
2
3use crate::models::formula::{
4    Assignment, BooleanExpr, BooleanOp, CNFClause, CircuitSAT, Satisfiability,
5};
6use crate::reduction;
7use crate::rules::sat_helpers::SatVariableAllocator;
8use crate::rules::traits::{ReduceTo, ReductionResult};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12enum NormalizedExpr {
13    Var(String),
14    Const(bool),
15    Not(Box<NormalizedExpr>),
16    And(Box<NormalizedExpr>, Box<NormalizedExpr>),
17    Or(Box<NormalizedExpr>, Box<NormalizedExpr>),
18    Xor(Box<NormalizedExpr>, Box<NormalizedExpr>),
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22enum EncodedTerm {
23    Const(bool),
24    Var(i64),
25}
26
27#[derive(Debug, Clone)]
28struct TseitinEncoding {
29    num_vars: usize,
30    clauses: Vec<CNFClause>,
31}
32
33#[derive(Debug)]
34struct TseitinEncoder {
35    source_var_ids: HashMap<String, i64>,
36    clauses: Vec<CNFClause>,
37    variables: SatVariableAllocator,
38}
39
40impl TseitinEncoder {
41    fn new(source: &CircuitSAT) -> Self {
42        let mut variables = SatVariableAllocator::new("CircuitSAT -> Satisfiability", 0)
43            .unwrap_or_else(|message| panic!("{message}"));
44        let source_ids = variables
45            .allocate_many(source.num_variables())
46            .unwrap_or_else(|message| panic!("{message}"));
47        let source_var_ids = source
48            .variable_names()
49            .iter()
50            .zip(source_ids)
51            .map(|(name, variable)| (name.clone(), variable))
52            .collect();
53        Self {
54            source_var_ids,
55            clauses: Vec::new(),
56            variables,
57        }
58    }
59
60    fn encode_problem(mut self, source: &CircuitSAT) -> TseitinEncoding {
61        for assignment in &source.circuit().assignments {
62            self.encode_assignment(assignment);
63        }
64
65        TseitinEncoding {
66            num_vars: self.variables.num_vars(),
67            clauses: self.clauses,
68        }
69    }
70
71    fn encode_assignment(&mut self, assignment: &Assignment) {
72        if assignment.outputs.is_empty() {
73            return;
74        }
75
76        let root = self.encode_expr(&normalize_expr(&assignment.expr));
77        match root {
78            EncodedTerm::Const(value) => {
79                let literal_sign = if value { 1 } else { -1 };
80                for output in &assignment.outputs {
81                    let output_var = self.source_var(output);
82                    self.push_clause(vec![literal_sign * output_var]);
83                }
84            }
85            EncodedTerm::Var(root_var) => {
86                for output in &assignment.outputs {
87                    let output_var = self.source_var(output);
88                    self.push_equivalence(output_var, root_var);
89                }
90            }
91        }
92    }
93
94    fn encode_expr(&mut self, expr: &NormalizedExpr) -> EncodedTerm {
95        match expr {
96            NormalizedExpr::Var(name) => EncodedTerm::Var(self.source_var(name)),
97            NormalizedExpr::Const(value) => EncodedTerm::Const(*value),
98            NormalizedExpr::Not(inner) => match self.encode_expr(inner) {
99                EncodedTerm::Const(value) => EncodedTerm::Const(!value),
100                EncodedTerm::Var(child_var) => {
101                    let gate_var = self.allocate_auxiliary_var();
102                    self.push_clause(vec![-gate_var, -child_var]);
103                    self.push_clause(vec![gate_var, child_var]);
104                    EncodedTerm::Var(gate_var)
105                }
106            },
107            NormalizedExpr::And(left, right) => {
108                let left_term = self.encode_expr(left);
109                let right_term = self.encode_expr(right);
110                let left_var = self.expect_var(left_term, "AND left input");
111                let right_var = self.expect_var(right_term, "AND right input");
112                let gate_var = self.allocate_auxiliary_var();
113                self.push_clause(vec![-gate_var, left_var]);
114                self.push_clause(vec![-gate_var, right_var]);
115                self.push_clause(vec![gate_var, -left_var, -right_var]);
116                EncodedTerm::Var(gate_var)
117            }
118            NormalizedExpr::Or(left, right) => {
119                let left_term = self.encode_expr(left);
120                let right_term = self.encode_expr(right);
121                let left_var = self.expect_var(left_term, "OR left input");
122                let right_var = self.expect_var(right_term, "OR right input");
123                let gate_var = self.allocate_auxiliary_var();
124                self.push_clause(vec![gate_var, -left_var]);
125                self.push_clause(vec![gate_var, -right_var]);
126                self.push_clause(vec![-gate_var, left_var, right_var]);
127                EncodedTerm::Var(gate_var)
128            }
129            NormalizedExpr::Xor(left, right) => {
130                let left_term = self.encode_expr(left);
131                let right_term = self.encode_expr(right);
132                let left_var = self.expect_var(left_term, "XOR left input");
133                let right_var = self.expect_var(right_term, "XOR right input");
134                let gate_var = self.allocate_auxiliary_var();
135                self.push_clause(vec![-left_var, -right_var, -gate_var]);
136                self.push_clause(vec![left_var, right_var, -gate_var]);
137                self.push_clause(vec![left_var, -right_var, gate_var]);
138                self.push_clause(vec![-left_var, right_var, gate_var]);
139                EncodedTerm::Var(gate_var)
140            }
141        }
142    }
143
144    fn expect_var(&self, term: EncodedTerm, context: &str) -> i64 {
145        match term {
146            EncodedTerm::Var(var) => var,
147            EncodedTerm::Const(_) => {
148                panic!("normalized Tseitin encoding produced a constant for {context}")
149            }
150        }
151    }
152
153    fn source_var(&self, name: &str) -> i64 {
154        *self
155            .source_var_ids
156            .get(name)
157            .unwrap_or_else(|| panic!("CircuitSAT variable {name:?} missing from source ordering"))
158    }
159
160    fn allocate_auxiliary_var(&mut self) -> i64 {
161        self.variables
162            .allocate()
163            .unwrap_or_else(|message| panic!("{message}"))
164    }
165
166    fn push_equivalence(&mut self, left: i64, right: i64) {
167        self.push_clause(vec![-left, right]);
168        self.push_clause(vec![left, -right]);
169    }
170
171    fn push_clause(&mut self, literals: Vec<i64>) {
172        self.clauses.push(CNFClause::new(literals));
173    }
174}
175
176fn make_and(left: NormalizedExpr, right: NormalizedExpr) -> NormalizedExpr {
177    NormalizedExpr::And(Box::new(left), Box::new(right))
178}
179
180fn make_or(left: NormalizedExpr, right: NormalizedExpr) -> NormalizedExpr {
181    NormalizedExpr::Or(Box::new(left), Box::new(right))
182}
183
184fn make_xor(left: NormalizedExpr, right: NormalizedExpr) -> NormalizedExpr {
185    NormalizedExpr::Xor(Box::new(left), Box::new(right))
186}
187
188fn build_balanced_expr(
189    mut items: Vec<NormalizedExpr>,
190    combine: fn(NormalizedExpr, NormalizedExpr) -> NormalizedExpr,
191) -> NormalizedExpr {
192    if items.len() == 1 {
193        return items.pop().expect("single item exists");
194    }
195
196    let right = items.split_off(items.len() / 2);
197    combine(
198        build_balanced_expr(items, combine),
199        build_balanced_expr(right, combine),
200    )
201}
202
203fn normalize_expr(expr: &BooleanExpr) -> NormalizedExpr {
204    match &expr.op {
205        BooleanOp::Var(name) => NormalizedExpr::Var(name.clone()),
206        BooleanOp::Const(value) => NormalizedExpr::Const(*value),
207        BooleanOp::Not(inner) => match normalize_expr(inner) {
208            NormalizedExpr::Const(value) => NormalizedExpr::Const(!value),
209            NormalizedExpr::Not(grandchild) => *grandchild,
210            normalized => NormalizedExpr::Not(Box::new(normalized)),
211        },
212        BooleanOp::And(args) => normalize_nary_gate(args, false, true, make_and),
213        BooleanOp::Or(args) => normalize_nary_gate(args, true, false, make_or),
214        BooleanOp::Xor(args) => {
215            let mut parity = false;
216            let mut normalized_args = Vec::new();
217
218            for arg in args {
219                match normalize_expr(arg) {
220                    NormalizedExpr::Const(value) => parity ^= value,
221                    normalized => normalized_args.push(normalized),
222                }
223            }
224
225            match normalized_args.len() {
226                0 => NormalizedExpr::Const(parity),
227                1 => {
228                    let base = normalized_args.pop().expect("single item exists");
229                    if parity {
230                        NormalizedExpr::Not(Box::new(base))
231                    } else {
232                        base
233                    }
234                }
235                _ => {
236                    let base = build_balanced_expr(normalized_args, make_xor);
237                    if parity {
238                        NormalizedExpr::Not(Box::new(base))
239                    } else {
240                        base
241                    }
242                }
243            }
244        }
245    }
246}
247
248fn normalize_nary_gate(
249    args: &[BooleanExpr],
250    absorbing_value: bool,
251    identity_value: bool,
252    combine: fn(NormalizedExpr, NormalizedExpr) -> NormalizedExpr,
253) -> NormalizedExpr {
254    let mut normalized_args = Vec::new();
255
256    for arg in args {
257        match normalize_expr(arg) {
258            NormalizedExpr::Const(value) if value == absorbing_value => {
259                return NormalizedExpr::Const(absorbing_value)
260            }
261            NormalizedExpr::Const(value) if value == identity_value => {}
262            normalized => normalized_args.push(normalized),
263        }
264    }
265
266    match normalized_args.len() {
267        0 => NormalizedExpr::Const(identity_value),
268        1 => normalized_args.pop().expect("single item exists"),
269        _ => build_balanced_expr(normalized_args, combine),
270    }
271}
272
273fn build_tseitin_encoding(source: &CircuitSAT) -> TseitinEncoding {
274    TseitinEncoder::new(source).encode_problem(source)
275}
276
277/// Result of reducing CircuitSAT to SAT.
278#[derive(Debug, Clone)]
279pub struct ReductionCircuitSATToSAT {
280    target: Satisfiability,
281    source_var_count: usize,
282}
283
284impl ReductionResult for ReductionCircuitSATToSAT {
285    type Source = CircuitSAT;
286    type Target = Satisfiability;
287
288    fn target_problem(&self) -> &Self::Target {
289        &self.target
290    }
291
292    fn extract_solution(
293        &self,
294        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
295    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
296        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
297
298        Ok(target_solution[..self.source_var_count].to_vec())
299    }
300}
301
302#[reduction(
303    transform = unavailable {
304        num_vars = "the exact Tseitin variable count is specific to this reduction and is not a CircuitSAT parameter",
305        num_clauses = "the exact Tseitin clause count is specific to this reduction and is not a CircuitSAT parameter",
306            num_literals = "the exact target parameter is not represented by this reduction's symbolic transform",
307}
308)]
309impl ReduceTo<Satisfiability> for CircuitSAT {
310    type Result = ReductionCircuitSATToSAT;
311
312    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
313        let encoding = build_tseitin_encoding(self);
314        Ok(ReductionCircuitSATToSAT {
315            target: Satisfiability::new(encoding.num_vars, encoding.clauses),
316            source_var_count: self.num_variables(),
317        })
318    }
319}
320
321#[cfg(any(test, feature = "example-db"))]
322fn issue_example_source() -> CircuitSAT {
323    use crate::models::formula::Circuit;
324
325    CircuitSAT::new(Circuit::new(vec![Assignment::new(
326        vec!["r".to_string()],
327        BooleanExpr::or(vec![
328            BooleanExpr::and(vec![BooleanExpr::var("x1"), BooleanExpr::var("x2")]),
329            BooleanExpr::and(vec![
330                BooleanExpr::not(BooleanExpr::var("x3")),
331                BooleanExpr::var("x4"),
332            ]),
333        ]),
334    )]))
335}
336
337#[cfg(feature = "example-db")]
338pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
339    use crate::export::SolutionPair;
340    use crate::solvers::BruteForce;
341
342    vec![crate::example_db::specs::RuleExampleSpec {
343        id: "circuitsat_to_satisfiability",
344        build: || {
345            let source = issue_example_source();
346            let source_config = vec![true, true, true, false, true];
347            let reduction =
348                ReduceTo::<Satisfiability>::reduce_to(&source).expect("reduction should succeed");
349            let target_config = BruteForce::new()
350                .find_all_witnesses(reduction.target_problem())
351                .expect("canonical target evaluation must succeed")
352                .into_iter()
353                .find(|candidate| reduction.extract_solution(candidate).unwrap() == source_config)
354                .expect("canonical CircuitSAT -> Satisfiability example must be satisfiable");
355
356            crate::example_db::specs::assemble_rule_example(
357                &source,
358                reduction.target_problem(),
359                vec![SolutionPair {
360                    source_config: serde_json::to_value(source_config)
361                        .expect("solution serialization must succeed"),
362                    target_config: serde_json::to_value(target_config)
363                        .expect("solution serialization must succeed"),
364                }],
365            )
366        },
367    }]
368}
369
370#[cfg(test)]
371#[path = "../unit_tests/rules/circuit_sat.rs"]
372mod tests;