Skip to main content

oxiz_sat/
subsumption.rs

1//! Clause Subsumption Checking
2//!
3//! This module implements efficient subsumption checking for learned clauses.
4//! A clause C subsumes clause D if every literal in C is also in D.
5//! When C subsumes D, D can be removed as it's redundant.
6//!
7//! This is particularly useful for:
8//! - Removing redundant learned clauses
9//! - Strengthening the clause database
10//! - Reducing memory usage
11//!
12//! Reference: "Efficient Clause Subsumption" and MiniSat
13
14use crate::clause::{ClauseDatabase, ClauseId};
15use crate::literal::Lit;
16#[allow(unused_imports)]
17use crate::prelude::*;
18
19/// Statistics for subsumption checking
20#[derive(Debug, Default, Clone)]
21pub struct SubsumptionStats {
22    /// Number of subsumption checks performed
23    pub checks_performed: u64,
24    /// Number of clauses subsumed and removed
25    pub clauses_subsumed: u64,
26    /// Number of forward subsumptions (new clause subsumes old)
27    pub forward_subsumptions: u64,
28    /// Number of backward subsumptions (old clause subsumes new)
29    pub backward_subsumptions: u64,
30}
31
32/// Subsumption checker for learned clauses
33pub struct SubsumptionChecker {
34    /// Temporary set for clause literals (reused across checks)
35    clause_lits: FxHashSet<Lit>,
36    /// Temporary set for candidate literals
37    candidate_lits: FxHashSet<Lit>,
38    /// Maximum clause size to check for subsumption
39    max_check_size: usize,
40    /// Statistics
41    stats: SubsumptionStats,
42}
43
44impl SubsumptionChecker {
45    /// Create a new subsumption checker
46    pub fn new(max_check_size: usize) -> Self {
47        Self {
48            clause_lits: FxHashSet::default(),
49            candidate_lits: FxHashSet::default(),
50            max_check_size,
51            stats: SubsumptionStats::default(),
52        }
53    }
54
55    /// Get statistics
56    #[must_use]
57    pub fn stats(&self) -> &SubsumptionStats {
58        &self.stats
59    }
60
61    /// Check if clause A subsumes clause B
62    ///
63    /// A subsumes B if all literals in A are present in B
64    fn subsumes(&mut self, clause_a: &[Lit], clause_b: &[Lit]) -> bool {
65        // A cannot subsume B if A is larger than B
66        if clause_a.len() > clause_b.len() {
67            return false;
68        }
69
70        // Build set of literals in B
71        self.candidate_lits.clear();
72        for &lit in clause_b {
73            self.candidate_lits.insert(lit);
74        }
75
76        // Check if all literals in A are in B
77        for &lit in clause_a {
78            if !self.candidate_lits.contains(&lit) {
79                return false;
80            }
81        }
82
83        true
84    }
85
86    /// Check if a newly learned clause subsumes any existing clauses
87    ///
88    /// Returns a list of clause IDs that are subsumed by the new clause
89    pub fn check_forward_subsumption(
90        &mut self,
91        new_clause: &[Lit],
92        clauses: &ClauseDatabase,
93    ) -> Vec<ClauseId> {
94        // Don't check if the new clause is too large
95        if new_clause.len() > self.max_check_size {
96            return Vec::new();
97        }
98
99        let mut subsumed = Vec::new();
100
101        // Check against all learned clauses
102        for clause_id in clauses.iter_ids() {
103            let Some(clause) = clauses.get(clause_id) else {
104                continue;
105            };
106
107            // Only check learned clauses
108            if !clause.learned || clause.deleted {
109                continue;
110            }
111
112            // Skip if the existing clause is too large
113            if clause.len() > self.max_check_size {
114                continue;
115            }
116
117            self.stats.checks_performed += 1;
118
119            // Check if new clause subsumes this clause
120            if self.subsumes(new_clause, &clause.lits) {
121                subsumed.push(clause_id);
122                self.stats.forward_subsumptions += 1;
123            }
124        }
125
126        self.stats.clauses_subsumed += subsumed.len() as u64;
127        subsumed
128    }
129
130    /// Check if a newly learned clause is subsumed by any existing clause
131    ///
132    /// Returns true if the new clause is subsumed (redundant)
133    pub fn check_backward_subsumption(
134        &mut self,
135        new_clause: &[Lit],
136        clauses: &ClauseDatabase,
137    ) -> bool {
138        // Don't check if the new clause is too large
139        if new_clause.len() > self.max_check_size {
140            return false;
141        }
142
143        // Build set of literals in new clause
144        self.clause_lits.clear();
145        for &lit in new_clause {
146            self.clause_lits.insert(lit);
147        }
148
149        // Check against all learned clauses
150        for clause_id in clauses.iter_ids() {
151            let Some(clause) = clauses.get(clause_id) else {
152                continue;
153            };
154
155            // Only check learned clauses
156            if !clause.learned || clause.deleted {
157                continue;
158            }
159
160            // Skip if the existing clause is larger (can't subsume)
161            if clause.len() > new_clause.len() {
162                continue;
163            }
164
165            // Skip if the existing clause is too large
166            if clause.len() > self.max_check_size {
167                continue;
168            }
169
170            self.stats.checks_performed += 1;
171
172            // Check if existing clause subsumes new clause
173            if self.subsumes(&clause.lits, new_clause) {
174                self.stats.backward_subsumptions += 1;
175                return true;
176            }
177        }
178
179        false
180    }
181
182    /// Perform full subsumption check for a new clause
183    ///
184    /// Returns (is_subsumed, subsumed_clauses)
185    /// - is_subsumed: true if the new clause is redundant
186    /// - subsumed_clauses: list of clause IDs that the new clause makes redundant
187    pub fn check_subsumption(
188        &mut self,
189        new_clause: &[Lit],
190        clauses: &ClauseDatabase,
191    ) -> (bool, Vec<ClauseId>) {
192        // First check backward subsumption (is new clause redundant?)
193        let is_subsumed = self.check_backward_subsumption(new_clause, clauses);
194
195        // If new clause is subsumed, don't check forward subsumption
196        if is_subsumed {
197            return (true, Vec::new());
198        }
199
200        // Check forward subsumption (does new clause make others redundant?)
201        let subsumed = self.check_forward_subsumption(new_clause, clauses);
202
203        (false, subsumed)
204    }
205
206    /// Reset statistics
207    pub fn reset_stats(&mut self) {
208        self.stats = SubsumptionStats::default();
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::literal::Var;
216
217    #[test]
218    fn test_subsumption_creation() {
219        let checker = SubsumptionChecker::new(10);
220        assert_eq!(checker.max_check_size, 10);
221    }
222
223    #[test]
224    fn test_subsumes() {
225        let mut checker = SubsumptionChecker::new(20);
226
227        // {x1, x2} subsumes {x1, x2, x3}
228        let clause_a = vec![Lit::pos(Var::new(0)), Lit::pos(Var::new(1))];
229        let clause_b = vec![
230            Lit::pos(Var::new(0)),
231            Lit::pos(Var::new(1)),
232            Lit::pos(Var::new(2)),
233        ];
234
235        assert!(checker.subsumes(&clause_a, &clause_b));
236        assert!(!checker.subsumes(&clause_b, &clause_a)); // Reverse should be false
237    }
238
239    #[test]
240    fn test_no_subsumption() {
241        let mut checker = SubsumptionChecker::new(20);
242
243        // {x1, x2} does not subsume {x1, x3}
244        let clause_a = vec![Lit::pos(Var::new(0)), Lit::pos(Var::new(1))];
245        let clause_b = vec![Lit::pos(Var::new(0)), Lit::pos(Var::new(2))];
246
247        assert!(!checker.subsumes(&clause_a, &clause_b));
248    }
249
250    #[test]
251    fn test_forward_subsumption() {
252        let mut checker = SubsumptionChecker::new(20);
253        let mut db = ClauseDatabase::new();
254
255        // Add a learned clause {x1, x2, x3}
256        let _old_id = db.add_learned([
257            Lit::pos(Var::new(0)),
258            Lit::pos(Var::new(1)),
259            Lit::pos(Var::new(2)),
260        ]);
261
262        // New clause {x1, x2} should subsume the old one
263        let new_clause = vec![Lit::pos(Var::new(0)), Lit::pos(Var::new(1))];
264
265        let subsumed = checker.check_forward_subsumption(&new_clause, &db);
266        assert_eq!(subsumed.len(), 1);
267    }
268
269    #[test]
270    fn test_backward_subsumption() {
271        let mut checker = SubsumptionChecker::new(20);
272        let mut db = ClauseDatabase::new();
273
274        // Add a learned clause {x1, x2}
275        let _old_id = db.add_learned([Lit::pos(Var::new(0)), Lit::pos(Var::new(1))]);
276
277        // New clause {x1, x2, x3} should be subsumed by the old one
278        let new_clause = vec![
279            Lit::pos(Var::new(0)),
280            Lit::pos(Var::new(1)),
281            Lit::pos(Var::new(2)),
282        ];
283
284        assert!(checker.check_backward_subsumption(&new_clause, &db));
285    }
286
287    #[test]
288    fn test_stats() {
289        let mut checker = SubsumptionChecker::new(20);
290        let stats = checker.stats();
291        assert_eq!(stats.checks_performed, 0);
292        assert_eq!(stats.clauses_subsumed, 0);
293
294        checker.reset_stats();
295        let stats = checker.stats();
296        assert_eq!(stats.checks_performed, 0);
297    }
298}