Skip to main content

oxiz_sat/
backbone.rs

1//! Backbone detection and computation
2//!
3//! The backbone of a SAT formula consists of all literals that have the same
4//! truth value in every satisfying assignment. Backbone detection can help
5//! simplify formulas and guide search strategies.
6//!
7//! References:
8//! - "Computing Backbones of Propositional Formulas" (Janota et al.)
9//! - "Backbone Computation for Propositional Formulas" (Marques-Silva)
10//! - "Efficient Backbone Computation" (Kullmann)
11
12use crate::literal::Lit;
13#[allow(unused_imports)]
14use crate::prelude::*;
15
16/// Statistics for backbone computation
17#[derive(Debug, Clone, Default)]
18pub struct BackboneStats {
19    /// Number of backbone literals found
20    pub backbone_size: usize,
21    /// Number of positive backbone literals
22    pub positive_lits: usize,
23    /// Number of negative backbone literals
24    pub negative_lits: usize,
25    /// Number of SAT solver calls made
26    pub solver_calls: usize,
27    /// Number of iterations performed
28    pub iterations: usize,
29}
30
31impl BackboneStats {
32    /// Display statistics
33    pub fn display(&self) {
34        println!("Backbone Computation Statistics:");
35        println!("  Backbone size: {}", self.backbone_size);
36        println!("  Positive literals: {}", self.positive_lits);
37        println!("  Negative literals: {}", self.negative_lits);
38        println!("  Solver calls: {}", self.solver_calls);
39        println!("  Iterations: {}", self.iterations);
40        if self.solver_calls > 0 {
41            println!(
42                "  Avg backbone per call: {:.2}",
43                self.backbone_size as f64 / self.solver_calls as f64
44            );
45        }
46    }
47}
48
49/// Backbone computation algorithm
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum BackboneAlgorithm {
52    /// Iterative algorithm - test each literal individually
53    Iterative,
54    /// Binary search - partition literals and test groups
55    BinarySearch,
56    /// Assumption-based - use assumptions to test multiple literals
57    AssumptionBased,
58}
59
60/// Backbone detector
61#[derive(Debug)]
62pub struct BackboneDetector {
63    /// Detected backbone literals
64    backbone: HashSet<Lit>,
65    /// Algorithm to use
66    algorithm: BackboneAlgorithm,
67    /// Statistics
68    stats: BackboneStats,
69}
70
71impl Default for BackboneDetector {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl BackboneDetector {
78    /// Create a new backbone detector with default algorithm
79    #[must_use]
80    pub fn new() -> Self {
81        Self {
82            backbone: HashSet::new(),
83            algorithm: BackboneAlgorithm::Iterative,
84            stats: BackboneStats::default(),
85        }
86    }
87
88    /// Create with specific algorithm
89    #[must_use]
90    pub fn with_algorithm(algorithm: BackboneAlgorithm) -> Self {
91        Self {
92            backbone: HashSet::new(),
93            algorithm,
94            stats: BackboneStats::default(),
95        }
96    }
97
98    /// Check if a literal is in the backbone
99    #[must_use]
100    pub fn is_backbone(&self, lit: Lit) -> bool {
101        self.backbone.contains(&lit)
102    }
103
104    /// Add a literal to the backbone
105    pub fn add_backbone(&mut self, lit: Lit) {
106        if self.backbone.insert(lit) {
107            self.stats.backbone_size += 1;
108            if lit.is_pos() {
109                self.stats.positive_lits += 1;
110            } else {
111                self.stats.negative_lits += 1;
112            }
113        }
114    }
115
116    /// Remove a literal from the backbone
117    pub fn remove_backbone(&mut self, lit: Lit) {
118        if self.backbone.remove(&lit) {
119            self.stats.backbone_size = self.stats.backbone_size.saturating_sub(1);
120            if lit.is_pos() {
121                self.stats.positive_lits = self.stats.positive_lits.saturating_sub(1);
122            } else {
123                self.stats.negative_lits = self.stats.negative_lits.saturating_sub(1);
124            }
125        }
126    }
127
128    /// Detect backbone using iterative algorithm
129    ///
130    /// For each literal l, check if the formula is UNSAT under assumption ~l.
131    /// If UNSAT, then l must be in the backbone.
132    pub fn detect_iterative<F>(&mut self, candidates: &[Lit], mut is_sat_with_assumption: F)
133    where
134        F: FnMut(Lit) -> bool,
135    {
136        self.backbone.clear();
137        self.stats = BackboneStats::default();
138
139        for &lit in candidates {
140            self.stats.iterations += 1;
141            self.stats.solver_calls += 1;
142
143            // Test if formula is SAT with ~lit
144            if !is_sat_with_assumption(!lit) {
145                // UNSAT with ~lit means lit must be true
146                self.add_backbone(lit);
147            }
148        }
149    }
150
151    /// Detect backbone using binary search
152    ///
153    /// Recursively partition the candidate set and test groups.
154    /// More efficient when the backbone is large.
155    pub fn detect_binary_search<F>(&mut self, candidates: &[Lit], is_sat_with_assumptions: F)
156    where
157        F: Fn(&[Lit]) -> bool + Clone,
158    {
159        self.backbone.clear();
160        self.stats = BackboneStats::default();
161
162        let mut to_process = vec![candidates.to_vec()];
163
164        while let Some(group) = to_process.pop() {
165            if group.is_empty() {
166                continue;
167            }
168
169            self.stats.iterations += 1;
170            self.stats.solver_calls += 1;
171
172            // Test if formula is SAT with all negations of this group
173            let negated: Vec<_> = group.iter().map(|&lit| !lit).collect();
174
175            if !is_sat_with_assumptions(&negated) {
176                // UNSAT - all literals in group are backbone
177                for &lit in &group {
178                    self.add_backbone(lit);
179                }
180            } else if group.len() > 1 {
181                // SAT - split group and continue
182                let mid = group.len() / 2;
183                to_process.push(group[..mid].to_vec());
184                to_process.push(group[mid..].to_vec());
185            }
186            // If group has size 1 and is SAT, it's not backbone
187        }
188    }
189
190    /// Compute rotatable literals (non-backbone)
191    ///
192    /// A literal is rotatable if it can be both true and false in different
193    /// satisfying assignments.
194    #[must_use]
195    pub fn compute_rotatable(&self, all_lits: &[Lit]) -> Vec<Lit> {
196        all_lits
197            .iter()
198            .filter(|&&lit| !self.is_backbone(lit) && !self.is_backbone(!lit))
199            .copied()
200            .collect()
201    }
202
203    /// Get all backbone literals
204    #[must_use]
205    pub fn backbone(&self) -> Vec<Lit> {
206        self.backbone.iter().copied().collect()
207    }
208
209    /// Get backbone size
210    #[must_use]
211    pub fn size(&self) -> usize {
212        self.backbone.len()
213    }
214
215    /// Check if backbone is empty
216    #[must_use]
217    pub fn is_empty(&self) -> bool {
218        self.backbone.is_empty()
219    }
220
221    /// Get statistics
222    #[must_use]
223    pub fn stats(&self) -> &BackboneStats {
224        &self.stats
225    }
226
227    /// Reset the detector
228    pub fn clear(&mut self) {
229        self.backbone.clear();
230        self.stats = BackboneStats::default();
231    }
232
233    /// Get the algorithm being used
234    #[must_use]
235    pub fn algorithm(&self) -> BackboneAlgorithm {
236        self.algorithm
237    }
238
239    /// Set the algorithm to use
240    pub fn set_algorithm(&mut self, algorithm: BackboneAlgorithm) {
241        self.algorithm = algorithm;
242    }
243}
244
245/// Backbone filter for literal selection
246///
247/// Use backbone information to improve branching decisions
248#[derive(Debug)]
249pub struct BackboneFilter {
250    /// Backbone detector
251    detector: BackboneDetector,
252    /// Whether to prefer backbone literals
253    prefer_backbone: bool,
254}
255
256impl Default for BackboneFilter {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262impl BackboneFilter {
263    /// Create a new backbone filter
264    #[must_use]
265    pub fn new() -> Self {
266        Self {
267            detector: BackboneDetector::new(),
268            prefer_backbone: true,
269        }
270    }
271
272    /// Filter literals based on backbone
273    ///
274    /// Prioritize or deprioritize backbone literals based on configuration
275    #[must_use]
276    pub fn filter(&self, candidates: &[Lit]) -> Vec<Lit> {
277        let mut backbone = Vec::new();
278        let mut non_backbone = Vec::new();
279
280        for &lit in candidates {
281            if self.detector.is_backbone(lit) {
282                backbone.push(lit);
283            } else {
284                non_backbone.push(lit);
285            }
286        }
287
288        if self.prefer_backbone {
289            backbone.extend(non_backbone);
290            backbone
291        } else {
292            non_backbone.extend(backbone);
293            non_backbone
294        }
295    }
296
297    /// Get the backbone detector
298    #[must_use]
299    pub fn detector(&self) -> &BackboneDetector {
300        &self.detector
301    }
302
303    /// Get mutable backbone detector
304    pub fn detector_mut(&mut self) -> &mut BackboneDetector {
305        &mut self.detector
306    }
307
308    /// Set whether to prefer backbone literals
309    pub fn set_prefer_backbone(&mut self, prefer: bool) {
310        self.prefer_backbone = prefer;
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::literal::Var;
318
319    #[test]
320    fn test_backbone_detector_creation() {
321        let detector = BackboneDetector::new();
322        assert_eq!(detector.size(), 0);
323        assert!(detector.is_empty());
324    }
325
326    #[test]
327    fn test_add_backbone() {
328        let mut detector = BackboneDetector::new();
329        let lit = Lit::pos(Var::new(0));
330
331        assert!(!detector.is_backbone(lit));
332        detector.add_backbone(lit);
333        assert!(detector.is_backbone(lit));
334        assert_eq!(detector.size(), 1);
335    }
336
337    #[test]
338    fn test_remove_backbone() {
339        let mut detector = BackboneDetector::new();
340        let lit = Lit::pos(Var::new(0));
341
342        detector.add_backbone(lit);
343        assert_eq!(detector.size(), 1);
344
345        detector.remove_backbone(lit);
346        assert_eq!(detector.size(), 0);
347        assert!(!detector.is_backbone(lit));
348    }
349
350    #[test]
351    fn test_detect_iterative() {
352        let mut detector = BackboneDetector::new();
353        let a = Lit::pos(Var::new(0));
354        let b = Lit::pos(Var::new(1));
355
356        // Simulate: a is backbone (formula UNSAT with ~a), b is not
357        let is_sat = |lit: Lit| -> bool {
358            lit != !a // UNSAT only when testing ~a
359        };
360
361        detector.detect_iterative(&[a, b], is_sat);
362
363        assert!(detector.is_backbone(a));
364        assert!(!detector.is_backbone(b));
365        assert_eq!(detector.size(), 1);
366    }
367
368    #[test]
369    fn test_compute_rotatable() {
370        let mut detector = BackboneDetector::new();
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        detector.add_backbone(a);
376
377        let rotatable = detector.compute_rotatable(&[a, b, c]);
378
379        assert!(!rotatable.contains(&a));
380        assert!(rotatable.contains(&b));
381        assert!(rotatable.contains(&c));
382    }
383
384    #[test]
385    fn test_backbone_filter() {
386        let mut filter = BackboneFilter::new();
387        let a = Lit::pos(Var::new(0));
388        let b = Lit::pos(Var::new(1));
389        let c = Lit::pos(Var::new(2));
390
391        filter.detector_mut().add_backbone(a);
392
393        let filtered = filter.filter(&[c, b, a]);
394
395        // Should prioritize backbone (a) first
396        assert_eq!(filtered[0], a);
397    }
398
399    #[test]
400    fn test_statistics() {
401        let mut detector = BackboneDetector::new();
402        detector.add_backbone(Lit::pos(Var::new(0)));
403        detector.add_backbone(Lit::neg(Var::new(1)));
404
405        let stats = detector.stats();
406        assert_eq!(stats.backbone_size, 2);
407        assert_eq!(stats.positive_lits, 1);
408        assert_eq!(stats.negative_lits, 1);
409    }
410
411    #[test]
412    fn test_algorithm_selection() {
413        let mut detector = BackboneDetector::with_algorithm(BackboneAlgorithm::BinarySearch);
414        assert_eq!(detector.algorithm(), BackboneAlgorithm::BinarySearch);
415
416        detector.set_algorithm(BackboneAlgorithm::Iterative);
417        assert_eq!(detector.algorithm(), BackboneAlgorithm::Iterative);
418    }
419
420    #[test]
421    fn test_clear() {
422        let mut detector = BackboneDetector::new();
423        detector.add_backbone(Lit::pos(Var::new(0)));
424        assert!(!detector.is_empty());
425
426        detector.clear();
427        assert!(detector.is_empty());
428        assert_eq!(detector.stats().backbone_size, 0);
429    }
430}