Skip to main content

oxiz_sat/
hyper_binary.rs

1//! Hyper-binary resolution and advanced probing
2//!
3//! This module implements hyper-binary resolution (HBR), an advanced technique
4//! for discovering and adding binary clauses during search. HBR can significantly
5//! reduce the search space by identifying implied binary clauses.
6//!
7//! References:
8//! - "Hyper Binary Resolution: A Proof System for SAT Solver Analysis"
9//! - "Binary Clause Reasoning in Conflict-Driven Clause Learning"
10//! - "Vivification and Hyper-Binary Resolution" (Biere)
11
12use crate::literal::Lit;
13#[allow(unused_imports)]
14use crate::prelude::*;
15use smallvec::SmallVec;
16
17/// Statistics for hyper-binary resolution
18#[derive(Debug, Clone, Default)]
19pub struct HyperBinaryStats {
20    /// Number of hyper-binary resolutions performed
21    pub hbr_attempts: u64,
22    /// Number of binary clauses discovered
23    pub binary_clauses_found: usize,
24    /// Number of unit clauses discovered
25    pub unit_clauses_found: usize,
26    /// Number of conflicts discovered
27    pub conflicts_found: usize,
28    /// Total literals probed
29    pub literals_probed: u64,
30}
31
32impl HyperBinaryStats {
33    /// Display statistics
34    pub fn display(&self) {
35        println!("Hyper-Binary Resolution Statistics:");
36        println!("  HBR attempts: {}", self.hbr_attempts);
37        println!("  Binary clauses found: {}", self.binary_clauses_found);
38        println!("  Unit clauses found: {}", self.unit_clauses_found);
39        println!("  Conflicts found: {}", self.conflicts_found);
40        println!("  Literals probed: {}", self.literals_probed);
41        if self.hbr_attempts > 0 {
42            let success_rate = 100.0 * self.binary_clauses_found as f64 / self.hbr_attempts as f64;
43            println!("  Success rate: {:.1}%", success_rate);
44        }
45    }
46}
47
48/// Result of hyper-binary resolution
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum HbrResult {
51    /// No new information discovered
52    None,
53    /// Binary clause discovered: (~probe => implied)
54    BinaryClause {
55        /// The probe literal
56        probe: Lit,
57        /// The implied literal
58        implied: Lit,
59    },
60    /// Unit clause discovered (forced assignment)
61    UnitClause {
62        /// The forced literal
63        lit: Lit,
64    },
65    /// Conflict discovered (probe leads to contradiction)
66    Conflict {
67        /// The probe literal that caused conflict
68        probe: Lit,
69    },
70}
71
72/// Hyper-binary resolution manager
73///
74/// Performs probing and hyper-binary resolution to discover new binary clauses.
75/// This can strengthen the clause database and reduce search space.
76#[derive(Debug)]
77pub struct HyperBinaryResolver {
78    /// Binary implication graph: implications[lit] = list of literals implied by ~lit
79    implications: Vec<SmallVec<[Lit; 4]>>,
80    /// Mark for literals seen during propagation
81    seen: Vec<bool>,
82    /// Work queue for propagation
83    queue: Vec<Lit>,
84    /// Statistics
85    stats: HyperBinaryStats,
86}
87
88impl Default for HyperBinaryResolver {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl HyperBinaryResolver {
95    /// Create a new hyper-binary resolver
96    #[must_use]
97    pub fn new() -> Self {
98        Self {
99            implications: Vec::new(),
100            seen: Vec::new(),
101            queue: Vec::new(),
102            stats: HyperBinaryStats::default(),
103        }
104    }
105
106    /// Resize for new number of variables
107    pub fn resize(&mut self, num_vars: usize) {
108        self.implications.resize(num_vars * 2, SmallVec::new());
109        self.seen.resize(num_vars * 2, false);
110    }
111
112    /// Add a binary clause to the implication graph
113    ///
114    /// For binary clause (a v b), this means:
115    /// - ~a => b (when a is false, b must be true)
116    /// - ~b => a (when b is false, a must be true)
117    pub fn add_binary_clause(&mut self, a: Lit, b: Lit) {
118        let not_a_idx = (!a).code() as usize;
119        let not_b_idx = (!b).code() as usize;
120
121        // Ensure space
122        let max_idx = not_a_idx.max(not_b_idx);
123        if max_idx >= self.implications.len() {
124            self.implications.resize(max_idx + 1, SmallVec::new());
125        }
126
127        // Add implications if not already present
128        if !self.implications[not_a_idx].contains(&b) {
129            self.implications[not_a_idx].push(b);
130        }
131        if !self.implications[not_b_idx].contains(&a) {
132            self.implications[not_b_idx].push(a);
133        }
134    }
135
136    /// Probe a literal to discover implied literals
137    ///
138    /// Assumes the literal is true and propagates to find all implications.
139    /// Returns discovered binary clauses, units, or conflicts.
140    pub fn probe(&mut self, probe_lit: Lit) -> Vec<HbrResult> {
141        self.stats.hbr_attempts += 1;
142        self.stats.literals_probed += 1;
143
144        let mut results = Vec::new();
145
146        // Clear state
147        for seen in &mut self.seen {
148            *seen = false;
149        }
150        self.queue.clear();
151
152        // Start with probe literal
153        self.queue.push(probe_lit);
154        self.seen[probe_lit.code() as usize] = true;
155
156        // Propagate implications
157        while let Some(lit) = self.queue.pop() {
158            let lit_idx = lit.code() as usize;
159
160            if lit_idx >= self.implications.len() {
161                continue;
162            }
163
164            // Check all literals implied by lit
165            for &implied in &self.implications[lit_idx] {
166                let impl_idx = implied.code() as usize;
167
168                // Check for conflict
169                if self.seen[(!implied).code() as usize] {
170                    results.push(HbrResult::Conflict { probe: probe_lit });
171                    self.stats.conflicts_found += 1;
172                    return results;
173                }
174
175                // Add new implication
176                if !self.seen[impl_idx] {
177                    self.seen[impl_idx] = true;
178                    self.queue.push(implied);
179
180                    // Record binary clause: ~probe => implied
181                    if implied != probe_lit {
182                        results.push(HbrResult::BinaryClause {
183                            probe: probe_lit,
184                            implied,
185                        });
186                        self.stats.binary_clauses_found += 1;
187                    }
188                }
189            }
190        }
191
192        results
193    }
194
195    /// Perform double lookahead probing
196    ///
197    /// Probes both polarities of a literal to discover implications.
198    /// Can detect:
199    /// - Failed literals (both polarities lead to conflict)
200    /// - Forced assignments (one polarity always conflicts)
201    /// - Common implications (both polarities imply same literals)
202    pub fn double_probe(&mut self, lit: Lit) -> Vec<HbrResult> {
203        let mut results = Vec::new();
204
205        // Probe positive polarity
206        let pos_results = self.probe(lit);
207        let pos_conflict = pos_results
208            .iter()
209            .any(|r| matches!(r, HbrResult::Conflict { .. }));
210
211        // Probe negative polarity
212        let neg_results = self.probe(!lit);
213        let neg_conflict = neg_results
214            .iter()
215            .any(|r| matches!(r, HbrResult::Conflict { .. }));
216
217        // Check for failed literal (both polarities conflict)
218        if pos_conflict && neg_conflict {
219            // This is a contradiction - should not happen in SAT instances
220            results.push(HbrResult::Conflict { probe: lit });
221            self.stats.conflicts_found += 1;
222        } else if pos_conflict {
223            // Positive polarity conflicts, so ~lit is forced
224            results.push(HbrResult::UnitClause { lit: !lit });
225            self.stats.unit_clauses_found += 1;
226        } else if neg_conflict {
227            // Negative polarity conflicts, so lit is forced
228            results.push(HbrResult::UnitClause { lit });
229            self.stats.unit_clauses_found += 1;
230        } else {
231            // Find common implications (literals implied by both polarities)
232            let pos_implied: Vec<_> = pos_results
233                .iter()
234                .filter_map(|r| match r {
235                    HbrResult::BinaryClause { implied, .. } => Some(*implied),
236                    _ => None,
237                })
238                .collect();
239
240            let neg_implied: Vec<_> = neg_results
241                .iter()
242                .filter_map(|r| match r {
243                    HbrResult::BinaryClause { implied, .. } => Some(*implied),
244                    _ => None,
245                })
246                .collect();
247
248            // Common implications are units
249            for &implied in &pos_implied {
250                if neg_implied.contains(&implied) {
251                    results.push(HbrResult::UnitClause { lit: implied });
252                    self.stats.unit_clauses_found += 1;
253                }
254            }
255        }
256
257        results
258    }
259
260    /// Get statistics
261    #[must_use]
262    pub fn stats(&self) -> &HyperBinaryStats {
263        &self.stats
264    }
265
266    /// Clear all implications
267    pub fn clear(&mut self) {
268        for implications in &mut self.implications {
269            implications.clear();
270        }
271        self.seen.clear();
272        self.queue.clear();
273    }
274
275    /// Reset statistics
276    pub fn reset_stats(&mut self) {
277        self.stats = HyperBinaryStats::default();
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::literal::Var;
285
286    #[test]
287    fn test_hyper_binary_resolver_creation() {
288        let resolver = HyperBinaryResolver::new();
289        assert_eq!(resolver.implications.len(), 0);
290    }
291
292    #[test]
293    fn test_add_binary_clause() {
294        let mut resolver = HyperBinaryResolver::new();
295        resolver.resize(10);
296
297        let a = Lit::pos(Var::new(0));
298        let b = Lit::pos(Var::new(1));
299
300        resolver.add_binary_clause(a, b);
301
302        // Check implications: ~a => b and ~b => a
303        let not_a_idx = (!a).code() as usize;
304        let not_b_idx = (!b).code() as usize;
305
306        assert!(resolver.implications[not_a_idx].contains(&b));
307        assert!(resolver.implications[not_b_idx].contains(&a));
308    }
309
310    #[test]
311    fn test_simple_probe() {
312        let mut resolver = HyperBinaryResolver::new();
313        resolver.resize(10);
314
315        // Add binary clauses: (a v b) and (b v c)
316        let a = Lit::pos(Var::new(0));
317        let b = Lit::pos(Var::new(1));
318        let c = Lit::pos(Var::new(2));
319
320        resolver.add_binary_clause(a, b);
321        resolver.add_binary_clause(b, c);
322
323        // Probe ~a should imply b and c
324        let results = resolver.probe(!a);
325
326        assert!(!results.is_empty());
327        // Should find that ~a => b and ~a => c
328    }
329
330    #[test]
331    fn test_conflict_detection() {
332        let mut resolver = HyperBinaryResolver::new();
333        resolver.resize(10);
334
335        // Add contradictory binary clauses
336        let a = Lit::pos(Var::new(0));
337        let b = Lit::pos(Var::new(1));
338
339        // (!a v b) and (!a v !b) means: a => b and a => !b (conflict when a is true)
340        resolver.add_binary_clause(!a, b);
341        resolver.add_binary_clause(!a, !b);
342
343        let results = resolver.probe(a);
344
345        // Should detect conflict
346        assert!(
347            results
348                .iter()
349                .any(|r| matches!(r, HbrResult::Conflict { .. }))
350        );
351    }
352
353    #[test]
354    fn test_double_probe_unit() {
355        let mut resolver = HyperBinaryResolver::new();
356        resolver.resize(10);
357
358        let a = Lit::pos(Var::new(0));
359        let b = Lit::pos(Var::new(1));
360
361        // a => b (so ~a conflicts)
362        resolver.add_binary_clause(a, b);
363        resolver.add_binary_clause(a, !b); // a implies both b and ~b
364
365        let results = resolver.double_probe(a);
366
367        // Should find that a must be false (unit clause ~a)
368        assert!(
369            results
370                .iter()
371                .any(|r| matches!(r, HbrResult::UnitClause { .. }))
372        );
373    }
374
375    #[test]
376    fn test_stats_tracking() {
377        let mut resolver = HyperBinaryResolver::new();
378        resolver.resize(10);
379
380        let a = Lit::pos(Var::new(0));
381        let b = Lit::pos(Var::new(1));
382
383        resolver.add_binary_clause(a, b);
384        resolver.probe(a);
385
386        assert_eq!(resolver.stats().hbr_attempts, 1);
387        assert_eq!(resolver.stats().literals_probed, 1);
388    }
389
390    #[test]
391    fn test_clear() {
392        let mut resolver = HyperBinaryResolver::new();
393        resolver.resize(10);
394
395        let a = Lit::pos(Var::new(0));
396        let b = Lit::pos(Var::new(1));
397
398        resolver.add_binary_clause(a, b);
399        resolver.clear();
400
401        assert!(resolver.implications.iter().all(|v| v.is_empty()));
402    }
403
404    #[test]
405    fn test_reset_stats() {
406        let mut resolver = HyperBinaryResolver::new();
407        resolver.resize(10);
408
409        resolver.probe(Lit::pos(Var::new(0)));
410        resolver.reset_stats();
411
412        assert_eq!(resolver.stats().hbr_attempts, 0);
413    }
414}