Skip to main content

oxiz_nlsat/
inprocessing.rs

1//! Inprocessing techniques for clause database optimization.
2//!
3//! This module implements various inprocessing techniques that can be
4//! applied during the search to simplify the clause database and improve
5//! solver performance. These include subsumption checking, clause strengthening,
6//! and other optimizations.
7//!
8//! Reference: Modern SAT solvers like Glucose, CryptoMiniSat
9
10use crate::clause::{Clause, ClauseId};
11use crate::types::Literal;
12use rustc_hash::FxHashSet;
13use std::collections::HashMap;
14
15/// Statistics for inprocessing operations.
16#[derive(Debug, Clone, Default)]
17pub struct InprocessStats {
18    /// Number of subsumed clauses removed.
19    pub subsumed_clauses: u64,
20    /// Number of clauses strengthened.
21    pub strengthened_clauses: u64,
22    /// Number of literals removed by strengthening.
23    pub literals_removed: u64,
24    /// Number of inprocessing calls.
25    pub inprocess_calls: u64,
26}
27
28/// Configuration for inprocessing.
29#[derive(Debug, Clone)]
30pub struct InprocessConfig {
31    /// Enable subsumption checking.
32    pub enable_subsumption: bool,
33    /// Enable clause strengthening.
34    pub enable_strengthening: bool,
35    /// Maximum clause size for subsumption checks.
36    pub max_subsumption_size: usize,
37    /// Perform inprocessing every N conflicts.
38    pub inprocess_interval: u64,
39}
40
41impl Default for InprocessConfig {
42    fn default() -> Self {
43        Self {
44            enable_subsumption: true,
45            enable_strengthening: true,
46            max_subsumption_size: 30,
47            inprocess_interval: 5000,
48        }
49    }
50}
51
52/// Inprocessing engine for clause database optimization.
53pub struct Inprocessor {
54    config: InprocessConfig,
55    stats: InprocessStats,
56    /// Map from literal to clause IDs containing it.
57    literal_watch: HashMap<Literal, Vec<ClauseId>>,
58}
59
60impl Inprocessor {
61    /// Create a new inprocessor with default configuration.
62    pub fn new() -> Self {
63        Self::with_config(InprocessConfig::default())
64    }
65
66    /// Create a new inprocessor with the given configuration.
67    pub fn with_config(config: InprocessConfig) -> Self {
68        Self {
69            config,
70            stats: InprocessStats::default(),
71            literal_watch: HashMap::new(),
72        }
73    }
74
75    /// Get the statistics.
76    pub fn stats(&self) -> &InprocessStats {
77        &self.stats
78    }
79
80    /// Get the configuration.
81    pub fn config(&self) -> &InprocessConfig {
82        &self.config
83    }
84
85    /// Check if a clause subsumes another clause.
86    ///
87    /// Clause C subsumes clause D if every literal in C appears in D.
88    pub fn subsumes(clause_c: &[Literal], clause_d: &[Literal]) -> bool {
89        if clause_c.len() > clause_d.len() {
90            return false;
91        }
92
93        let d_set: FxHashSet<_> = clause_d.iter().copied().collect();
94
95        clause_c.iter().all(|&lit| d_set.contains(&lit))
96    }
97
98    /// Check if clause C self-subsumes clause D at a literal.
99    ///
100    /// Self-subsumption: C ∨ x subsumes D ∨ ¬x, allowing us to
101    /// strengthen D by removing ¬x.
102    ///
103    /// Returns Some(literal) if self-subsumption is possible, where
104    /// literal is the one that can be removed from D.
105    pub fn self_subsumes(clause_c: &[Literal], clause_d: &[Literal]) -> Option<Literal> {
106        if clause_c.len() > clause_d.len() {
107            return None;
108        }
109
110        let d_set: FxHashSet<_> = clause_d.iter().copied().collect();
111
112        // Find complementary literals between C and D
113        for &lit_c in clause_c {
114            let neg_lit_c = lit_c.negate();
115
116            if d_set.contains(&neg_lit_c) {
117                // Check if all other literals in C are in D
118                let all_match = clause_c
119                    .iter()
120                    .filter(|&&lit| lit != lit_c)
121                    .all(|&lit| d_set.contains(&lit));
122
123                if all_match {
124                    return Some(neg_lit_c); // This literal can be removed from D
125                }
126            }
127        }
128
129        None
130    }
131
132    /// Find all subsumptions between clauses.
133    ///
134    /// Returns a list of (subsumed_id, subsuming_id) pairs.
135    pub fn find_subsumptions(&mut self, clauses: &[Clause]) -> Vec<(ClauseId, ClauseId)> {
136        if !self.config.enable_subsumption {
137            return Vec::new();
138        }
139
140        let mut subsumptions = Vec::new();
141
142        // Build literal watch map for efficient lookup
143        self.literal_watch.clear();
144        for clause in clauses {
145            if clause.len() > self.config.max_subsumption_size {
146                continue;
147            }
148
149            for &lit in clause.literals() {
150                self.literal_watch.entry(lit).or_default().push(clause.id());
151            }
152        }
153
154        // Check each clause for subsumptions
155        for i in 0..clauses.len() {
156            let clause_c = &clauses[i];
157
158            if clause_c.len() > self.config.max_subsumption_size {
159                continue;
160            }
161
162            // Find candidate clauses that might be subsumed by clause_c
163            // by looking at clauses that contain the first literal of C
164            if let Some(&first_lit) = clause_c.literals().first()
165                && let Some(candidates) = self.literal_watch.get(&first_lit)
166            {
167                for &candidate_id in candidates {
168                    if candidate_id == clause_c.id() {
169                        continue; // Skip self
170                    }
171
172                    if let Some(clause_d) = clauses.iter().find(|c| c.id() == candidate_id)
173                        && Self::subsumes(clause_c.literals(), clause_d.literals())
174                    {
175                        subsumptions.push((clause_d.id(), clause_c.id()));
176                        self.stats.subsumed_clauses += 1;
177                    }
178                }
179            }
180        }
181
182        subsumptions
183    }
184
185    /// Find all possible clause strengthening opportunities.
186    ///
187    /// Returns a list of (clause_id, literal_to_remove) pairs.
188    pub fn find_strengthenings(&mut self, clauses: &[Clause]) -> Vec<(ClauseId, Literal)> {
189        if !self.config.enable_strengthening {
190            return Vec::new();
191        }
192
193        let mut strengthenings = Vec::new();
194
195        for i in 0..clauses.len() {
196            for j in 0..clauses.len() {
197                if i == j {
198                    continue;
199                }
200
201                let clause_c = &clauses[i];
202                let clause_d = &clauses[j];
203
204                if clause_c.len() >= clause_d.len() {
205                    continue;
206                }
207
208                if clause_c.len() > self.config.max_subsumption_size
209                    || clause_d.len() > self.config.max_subsumption_size
210                {
211                    continue;
212                }
213
214                if let Some(lit_to_remove) =
215                    Self::self_subsumes(clause_c.literals(), clause_d.literals())
216                {
217                    strengthenings.push((clause_d.id(), lit_to_remove));
218                    self.stats.strengthened_clauses += 1;
219                    self.stats.literals_removed += 1;
220                }
221            }
222        }
223
224        strengthenings
225    }
226
227    /// Perform inprocessing on the clause database.
228    ///
229    /// Returns the number of clauses removed.
230    pub fn inprocess(&mut self, conflicts: u64) -> bool {
231        self.stats.inprocess_calls += 1;
232
233        // Check if we should run inprocessing
234        conflicts.is_multiple_of(self.config.inprocess_interval)
235    }
236}
237
238impl Default for Inprocessor {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::types::BoolVar;
248
249    fn make_literal(var: BoolVar, sign: bool) -> Literal {
250        Literal::new(var, sign)
251    }
252
253    #[test]
254    fn test_subsumption_basic() {
255        // {x1} subsumes {x1, x2}
256        let c = vec![make_literal(0, true)];
257        let d = vec![make_literal(0, true), make_literal(1, true)];
258
259        assert!(Inprocessor::subsumes(&c, &d));
260        assert!(!Inprocessor::subsumes(&d, &c));
261    }
262
263    #[test]
264    fn test_subsumption_same_size() {
265        // {x1, x2} subsumes {x1, x2}
266        let c = vec![make_literal(0, true), make_literal(1, true)];
267        let d = vec![make_literal(0, true), make_literal(1, true)];
268
269        assert!(Inprocessor::subsumes(&c, &d));
270    }
271
272    #[test]
273    fn test_subsumption_different_literals() {
274        // {x1, x2} does not subsume {x1, x3}
275        let c = vec![make_literal(0, true), make_literal(1, true)];
276        let d = vec![make_literal(0, true), make_literal(2, true)];
277
278        assert!(!Inprocessor::subsumes(&c, &d));
279    }
280
281    #[test]
282    fn test_self_subsumption() {
283        // {x1} self-subsumes {x1, ~x2} ∪ {x2} = can remove ~x2 from {x1, ~x2}
284        // This means: {x1, x2} and {x1, ~x2} -> strengthen to {x1}
285
286        let c = vec![make_literal(0, true), make_literal(1, true)];
287        let d = vec![make_literal(0, true), make_literal(1, false)];
288
289        assert_eq!(
290            Inprocessor::self_subsumes(&c, &d),
291            Some(make_literal(1, false))
292        );
293    }
294
295    #[test]
296    fn test_self_subsumption_none() {
297        let c = vec![make_literal(0, true), make_literal(1, true)];
298        let d = vec![make_literal(0, true), make_literal(2, true)];
299
300        assert_eq!(Inprocessor::self_subsumes(&c, &d), None);
301    }
302
303    #[test]
304    fn test_inprocessor_new() {
305        let inproc = Inprocessor::new();
306        assert_eq!(inproc.stats().subsumed_clauses, 0);
307        assert_eq!(inproc.stats().strengthened_clauses, 0);
308    }
309
310    #[test]
311    fn test_inprocess_interval() {
312        let mut inproc = Inprocessor::new();
313        assert!(inproc.inprocess(5000));
314        assert!(!inproc.inprocess(5001));
315        assert!(inproc.inprocess(10000));
316    }
317}