Skip to main content

oxiz_sat/
big.rs

1//! Binary Implication Graph (BIG) optimization
2//!
3//! This module implements optimizations for the binary implication graph,
4//! including transitive reduction to remove redundant binary clauses and
5//! strongly connected component detection for equivalence finding.
6//!
7//! The binary implication graph represents binary clauses as implications:
8//! - Binary clause (a v b) represents two implications: ~a => b and ~b => a
9//!
10//! References:
11//! - "Binary Clause Reasoning in Conflict-Driven Clause Learning" (Bacchus)
12//! - "Effective Preprocessing in SAT" (Eén & Biere)
13//! - "Bounded Variable Elimination" (Subbarayan & Pradhan)
14
15use crate::clause::ClauseDatabase;
16use crate::literal::Lit;
17#[allow(unused_imports)]
18use crate::prelude::*;
19
20/// Statistics for BIG optimization
21#[derive(Debug, Clone, Default)]
22pub struct BigStats {
23    /// Number of binary clauses analyzed
24    pub binary_clauses_analyzed: usize,
25    /// Number of redundant binary clauses removed
26    pub redundant_removed: usize,
27    /// Number of transitive implications found
28    pub transitive_found: usize,
29    /// Number of SCCs detected
30    pub sccs_found: usize,
31    /// Number of equivalent literal pairs from SCCs
32    pub equivalences_from_sccs: usize,
33}
34
35impl BigStats {
36    /// Display statistics
37    pub fn display(&self) {
38        println!("Binary Implication Graph Statistics:");
39        println!(
40            "  Binary clauses analyzed: {}",
41            self.binary_clauses_analyzed
42        );
43        println!("  Redundant clauses removed: {}", self.redundant_removed);
44        println!("  Transitive implications: {}", self.transitive_found);
45        println!("  SCCs found: {}", self.sccs_found);
46        println!("  Equivalences from SCCs: {}", self.equivalences_from_sccs);
47    }
48}
49
50/// Binary Implication Graph optimizer
51#[derive(Debug)]
52pub struct BinaryImplicationGraph {
53    /// Adjacency list: implications[lit] = literals implied by lit
54    implications: Vec<HashSet<Lit>>,
55    /// Reverse adjacency list for SCC detection
56    reverse_implications: Vec<HashSet<Lit>>,
57    /// Statistics
58    stats: BigStats,
59}
60
61impl BinaryImplicationGraph {
62    /// Create a new BIG optimizer
63    #[must_use]
64    pub fn new(num_vars: usize) -> Self {
65        let size = num_vars * 2;
66        Self {
67            implications: vec![HashSet::new(); size],
68            reverse_implications: vec![HashSet::new(); size],
69            stats: BigStats::default(),
70        }
71    }
72
73    /// Build the implication graph from clause database
74    pub fn build(&mut self, clauses: &ClauseDatabase) {
75        // Clear existing data
76        for imp in &mut self.implications {
77            imp.clear();
78        }
79        for imp in &mut self.reverse_implications {
80            imp.clear();
81        }
82        self.stats.binary_clauses_analyzed = 0;
83
84        // Extract binary clauses
85        for cid in clauses.iter_ids() {
86            if let Some(clause) = clauses.get(cid)
87                && clause.len() == 2
88            {
89                self.stats.binary_clauses_analyzed += 1;
90
91                let a = clause.lits[0];
92                let b = clause.lits[1];
93
94                // Binary clause (a v b) means: ~a => b and ~b => a
95                self.add_implication(!a, b);
96                self.add_implication(!b, a);
97            }
98        }
99    }
100
101    /// Add a binary implication to the graph
102    fn add_implication(&mut self, from: Lit, to: Lit) {
103        let from_idx = from.code() as usize;
104        let to_idx = to.code() as usize;
105
106        // Ensure capacity
107        while from_idx >= self.implications.len() {
108            self.implications.push(HashSet::new());
109            self.reverse_implications.push(HashSet::new());
110        }
111        while to_idx >= self.implications.len() {
112            self.implications.push(HashSet::new());
113            self.reverse_implications.push(HashSet::new());
114        }
115
116        self.implications[from_idx].insert(to);
117        self.reverse_implications[to_idx].insert(from);
118    }
119
120    /// Perform transitive reduction to remove redundant implications
121    ///
122    /// An edge a => c is redundant if there exists a path a => b => c
123    pub fn transitive_reduction(&mut self) -> Vec<(Lit, Lit)> {
124        let mut redundant = Vec::new();
125        let num_lits = self.implications.len();
126
127        for lit_idx in 0..num_lits {
128            let lit = Lit::from_code(lit_idx as u32);
129            let direct_implications: Vec<_> = self.implications[lit_idx].iter().copied().collect();
130
131            for &implied in &direct_implications {
132                // Check if there's an alternative path from lit to implied
133                if self.has_alternative_path(lit, implied) {
134                    redundant.push((lit, implied));
135                    self.stats.redundant_removed += 1;
136                }
137            }
138        }
139
140        // Remove redundant edges
141        for (from, to) in &redundant {
142            let from_idx = from.code() as usize;
143            let to_idx = to.code() as usize;
144            self.implications[from_idx].remove(to);
145            self.reverse_implications[to_idx].remove(from);
146        }
147
148        redundant
149    }
150
151    /// Check if there's a path from 'from' to 'to' without using the direct edge
152    fn has_alternative_path(&self, from: Lit, to: Lit) -> bool {
153        let from_idx = from.code() as usize;
154        if from_idx >= self.implications.len() {
155            return false;
156        }
157
158        let mut visited = HashSet::new();
159        let mut queue = Vec::new();
160
161        // Start BFS from literals implied by 'from', excluding direct edge to 'to'
162        for &implied in &self.implications[from_idx] {
163            if implied != to {
164                queue.push(implied);
165            }
166        }
167
168        while let Some(lit) = queue.pop() {
169            if lit == to {
170                return true; // Found alternative path
171            }
172
173            let lit_idx = lit.code() as usize;
174            if visited.contains(&lit_idx) || lit_idx >= self.implications.len() {
175                continue;
176            }
177
178            visited.insert(lit_idx);
179
180            for &next in &self.implications[lit_idx] {
181                if !visited.contains(&(next.code() as usize)) {
182                    queue.push(next);
183                }
184            }
185        }
186
187        false
188    }
189
190    /// Detect strongly connected components using Tarjan's algorithm
191    ///
192    /// Literals in the same SCC are equivalent (mutual implication)
193    #[allow(dead_code)]
194    pub fn find_sccs(&mut self) -> Vec<Vec<Lit>> {
195        let num_lits = self.implications.len();
196        let mut index = vec![None; num_lits];
197        let mut lowlink = vec![0; num_lits];
198        let mut on_stack = vec![false; num_lits];
199        let mut stack = Vec::new();
200        let mut sccs = Vec::new();
201        let mut current_index = 0;
202
203        for lit_idx in 0..num_lits {
204            if index[lit_idx].is_none() {
205                self.tarjan_scc(
206                    lit_idx,
207                    &mut index,
208                    &mut lowlink,
209                    &mut on_stack,
210                    &mut stack,
211                    &mut sccs,
212                    &mut current_index,
213                );
214            }
215        }
216
217        // Filter out trivial SCCs (single nodes)
218        self.stats.sccs_found = sccs.iter().filter(|scc| scc.len() > 1).count();
219        sccs.into_iter().filter(|scc| scc.len() > 1).collect()
220    }
221
222    /// Tarjan's SCC algorithm helper
223    #[allow(clippy::too_many_arguments)]
224    fn tarjan_scc(
225        &self,
226        lit_idx: usize,
227        index: &mut Vec<Option<usize>>,
228        lowlink: &mut Vec<usize>,
229        on_stack: &mut Vec<bool>,
230        stack: &mut Vec<usize>,
231        sccs: &mut Vec<Vec<Lit>>,
232        current_index: &mut usize,
233    ) {
234        index[lit_idx] = Some(*current_index);
235        lowlink[lit_idx] = *current_index;
236        *current_index += 1;
237        stack.push(lit_idx);
238        on_stack[lit_idx] = true;
239
240        // Consider successors
241        if lit_idx < self.implications.len() {
242            for &implied in &self.implications[lit_idx] {
243                let impl_idx = implied.code() as usize;
244
245                if index[impl_idx].is_none() {
246                    self.tarjan_scc(
247                        impl_idx,
248                        index,
249                        lowlink,
250                        on_stack,
251                        stack,
252                        sccs,
253                        current_index,
254                    );
255                    lowlink[lit_idx] = lowlink[lit_idx].min(lowlink[impl_idx]);
256                } else if on_stack[impl_idx] {
257                    lowlink[lit_idx] = lowlink[lit_idx]
258                        .min(index[impl_idx].expect("index set when on_stack is true"));
259                }
260            }
261        }
262
263        // If lit_idx is a root node, pop the stack to form an SCC
264        if lowlink[lit_idx] == index[lit_idx].expect("index set for lit_idx in SCC") {
265            let mut scc = Vec::new();
266            loop {
267                let node = stack.pop().expect("stack non-empty in SCC formation");
268                on_stack[node] = false;
269                scc.push(Lit::from_code(node as u32));
270                if node == lit_idx {
271                    break;
272                }
273            }
274            sccs.push(scc);
275        }
276    }
277
278    /// Apply BIG optimizations to clause database
279    ///
280    /// Removes redundant binary clauses found through transitive reduction
281    pub fn optimize(&mut self, clauses: &mut ClauseDatabase) {
282        // Build the graph
283        self.build(clauses);
284
285        // Perform transitive reduction
286        let redundant = self.transitive_reduction();
287
288        // Remove redundant binary clauses from database
289        let clause_ids: Vec<_> = clauses.iter_ids().collect();
290        for cid in clause_ids {
291            if let Some(clause) = clauses.get(cid)
292                && clause.len() == 2
293            {
294                let a = clause.lits[0];
295                let b = clause.lits[1];
296
297                // Check if this binary clause is redundant
298                if redundant.contains(&(!a, b)) || redundant.contains(&(!b, a)) {
299                    clauses.remove(cid);
300                }
301            }
302        }
303    }
304
305    /// Get all implications for a literal
306    #[must_use]
307    pub fn get_implications(&self, lit: Lit) -> Vec<Lit> {
308        let idx = lit.code() as usize;
309        if idx < self.implications.len() {
310            self.implications[idx].iter().copied().collect()
311        } else {
312            Vec::new()
313        }
314    }
315
316    /// Check if literal a implies literal b
317    #[must_use]
318    pub fn implies(&self, a: Lit, b: Lit) -> bool {
319        let idx = a.code() as usize;
320        if idx < self.implications.len() {
321            self.implications[idx].contains(&b)
322        } else {
323            false
324        }
325    }
326
327    /// Get statistics
328    #[must_use]
329    pub fn stats(&self) -> &BigStats {
330        &self.stats
331    }
332
333    /// Reset statistics
334    pub fn reset_stats(&mut self) {
335        self.stats = BigStats::default();
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::literal::Var;
343
344    #[test]
345    fn test_big_creation() {
346        let big = BinaryImplicationGraph::new(10);
347        assert_eq!(big.stats().binary_clauses_analyzed, 0);
348    }
349
350    #[test]
351    fn test_build_from_clauses() {
352        let mut big = BinaryImplicationGraph::new(10);
353        let mut db = ClauseDatabase::new();
354
355        let a = Lit::pos(Var::new(0));
356        let b = Lit::pos(Var::new(1));
357
358        db.add_original(vec![a, b]);
359        big.build(&db);
360
361        assert_eq!(big.stats().binary_clauses_analyzed, 1);
362        // Should have implications: ~a => b and ~b => a
363        assert!(big.implies(!a, b));
364        assert!(big.implies(!b, a));
365    }
366
367    #[test]
368    fn test_transitive_reduction() {
369        let mut big = BinaryImplicationGraph::new(10);
370
371        let a = Lit::pos(Var::new(0));
372        let b = Lit::pos(Var::new(1));
373        let c = Lit::pos(Var::new(2));
374
375        // Add: a => b, b => c, a => c (last one is redundant)
376        big.add_implication(a, b);
377        big.add_implication(b, c);
378        big.add_implication(a, c);
379
380        let redundant = big.transitive_reduction();
381
382        // Should find a => c as redundant
383        assert!(!redundant.is_empty());
384        assert!(redundant.contains(&(a, c)));
385    }
386
387    #[test]
388    fn test_find_sccs() {
389        let mut big = BinaryImplicationGraph::new(10);
390
391        let a = Lit::pos(Var::new(0));
392        let b = Lit::pos(Var::new(1));
393
394        // Create a cycle: a => b, b => a (they're equivalent)
395        big.add_implication(a, b);
396        big.add_implication(b, a);
397
398        let sccs = big.find_sccs();
399
400        // Should find one SCC containing a and b
401        assert!(!sccs.is_empty());
402        let scc = &sccs[0];
403        assert!(scc.contains(&a));
404        assert!(scc.contains(&b));
405    }
406
407    #[test]
408    fn test_get_implications() {
409        let mut big = BinaryImplicationGraph::new(10);
410
411        let a = Lit::pos(Var::new(0));
412        let b = Lit::pos(Var::new(1));
413
414        big.add_implication(a, b);
415
416        let implications = big.get_implications(a);
417        assert!(implications.contains(&b));
418    }
419
420    #[test]
421    fn test_optimize() {
422        let mut big = BinaryImplicationGraph::new(10);
423        let mut db = ClauseDatabase::new();
424
425        let a = Lit::pos(Var::new(0));
426        let b = Lit::pos(Var::new(1));
427        let c = Lit::pos(Var::new(2));
428
429        // Add three binary clauses creating redundancy
430        db.add_original(vec![a, b]); // ~a => b
431        db.add_original(vec![b, c]); // ~b => c
432        db.add_original(vec![a, c]); // ~a => c (redundant)
433
434        let before = db.len();
435        big.optimize(&mut db);
436        let after = db.len();
437
438        // Should have removed at least one redundant clause
439        assert!(after <= before);
440    }
441}