1use 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#[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;