Skip to main content

oxiz_sat/
drat_inprocessing.rs

1//! DRAT-based Inprocessing
2//!
3//! Provides inprocessing (simplification during search) while maintaining
4//! DRAT proof correctness. All simplification steps are logged to ensure
5//! the final proof remains valid.
6//!
7//! Key features:
8//! - Proof-producing clause elimination
9//! - Proof-producing subsumption
10//! - Proof-producing variable elimination
11//! - Proof-producing blocked clause elimination
12//! - Ensures all simplifications are verifiable via DRAT
13
14use crate::clause::{ClauseDatabase, ClauseId};
15use crate::literal::{Lit, Var};
16#[allow(unused_imports)]
17use crate::prelude::*;
18use crate::proof::DratProof;
19
20/// Statistics for DRAT-based inprocessing
21#[derive(Debug, Default, Clone)]
22pub struct DratInprocessingStats {
23    /// Number of clauses eliminated
24    pub clauses_eliminated: usize,
25    /// Number of variables eliminated
26    pub variables_eliminated: usize,
27    /// Number of subsumptions performed
28    pub subsumptions: usize,
29    /// Number of blocked clauses eliminated
30    pub blocked_clauses_eliminated: usize,
31    /// Number of proof steps logged
32    pub proof_steps: usize,
33    /// Number of inprocessing rounds
34    pub rounds: usize,
35}
36
37impl DratInprocessingStats {
38    /// Display statistics in a human-readable format
39    pub fn display(&self) -> String {
40        format!(
41            "DRAT Inprocessing Stats:\n\
42             - Rounds: {}\n\
43             - Clauses eliminated: {}\n\
44             - Variables eliminated: {}\n\
45             - Subsumptions: {}\n\
46             - Blocked clauses: {}\n\
47             - Proof steps: {}",
48            self.rounds,
49            self.clauses_eliminated,
50            self.variables_eliminated,
51            self.subsumptions,
52            self.blocked_clauses_eliminated,
53            self.proof_steps,
54        )
55    }
56}
57
58/// Configuration for DRAT-based inprocessing
59#[derive(Debug, Clone)]
60pub struct DratInprocessingConfig {
61    /// Enable subsumption elimination
62    pub enable_subsumption: bool,
63    /// Enable variable elimination
64    pub enable_variable_elimination: bool,
65    /// Enable blocked clause elimination
66    pub enable_blocked_clause_elimination: bool,
67    /// Maximum clause size for variable elimination
68    pub max_clause_size_for_elimination: usize,
69    /// Maximum resolution size (product of clause sizes)
70    pub max_resolution_size: usize,
71}
72
73impl Default for DratInprocessingConfig {
74    fn default() -> Self {
75        Self {
76            enable_subsumption: true,
77            enable_variable_elimination: true,
78            enable_blocked_clause_elimination: true,
79            max_clause_size_for_elimination: 10,
80            max_resolution_size: 100,
81        }
82    }
83}
84
85/// DRAT-based Inprocessor
86///
87/// Performs simplifications during search while maintaining proof correctness
88pub struct DratInprocessor {
89    /// Configuration
90    config: DratInprocessingConfig,
91    /// Statistics
92    stats: DratInprocessingStats,
93    /// Clauses that have been eliminated
94    eliminated_clauses: HashSet<ClauseId>,
95    /// Variables that have been eliminated
96    eliminated_vars: HashSet<Var>,
97}
98
99impl DratInprocessor {
100    /// Create a new DRAT inprocessor
101    pub fn new(config: DratInprocessingConfig) -> Self {
102        Self {
103            config,
104            stats: DratInprocessingStats::default(),
105            eliminated_clauses: HashSet::new(),
106            eliminated_vars: HashSet::new(),
107        }
108    }
109
110    /// Create with default configuration
111    pub fn default_config() -> Self {
112        Self::new(DratInprocessingConfig::default())
113    }
114
115    /// Perform inprocessing on the clause database
116    ///
117    /// Returns the number of simplifications made
118    pub fn inprocess(
119        &mut self,
120        db: &mut ClauseDatabase,
121        proof: &mut DratProof,
122    ) -> std::io::Result<usize> {
123        self.stats.rounds += 1;
124        let mut simplifications = 0;
125
126        // Subsumption elimination
127        if self.config.enable_subsumption {
128            simplifications += self.eliminate_subsumed(db, proof)?;
129        }
130
131        // Blocked clause elimination
132        if self.config.enable_blocked_clause_elimination {
133            simplifications += self.eliminate_blocked_clauses(db, proof)?;
134        }
135
136        // Variable elimination
137        if self.config.enable_variable_elimination {
138            simplifications += self.eliminate_variables(db, proof)?;
139        }
140
141        Ok(simplifications)
142    }
143
144    /// Eliminate subsumed clauses with DRAT logging
145    fn eliminate_subsumed(
146        &mut self,
147        db: &mut ClauseDatabase,
148        proof: &mut DratProof,
149    ) -> std::io::Result<usize> {
150        let mut eliminated = 0;
151
152        // Collect all clause IDs to check
153        let clause_ids: Vec<ClauseId> = db.iter_ids().collect();
154
155        for i in 0..clause_ids.len() {
156            let cid1 = clause_ids[i];
157            if self.eliminated_clauses.contains(&cid1) {
158                continue;
159            }
160
161            let clause1 = match db.get(cid1) {
162                Some(c) => c.lits.to_vec(),
163                None => continue,
164            };
165
166            for &cid2 in clause_ids.iter().skip(i + 1) {
167                if self.eliminated_clauses.contains(&cid2) {
168                    continue;
169                }
170
171                let clause2 = match db.get(cid2) {
172                    Some(c) => c.lits.to_vec(),
173                    None => continue,
174                };
175
176                // Check if clause1 subsumes clause2
177                if Self::subsumes(&clause1, &clause2) {
178                    // Log deletion to proof
179                    proof.delete_clause(&clause2)?;
180                    self.stats.proof_steps += 1;
181
182                    // Mark clause2 as eliminated
183                    self.eliminated_clauses.insert(cid2);
184                    self.stats.subsumptions += 1;
185                    eliminated += 1;
186                } else if Self::subsumes(&clause2, &clause1) {
187                    // Log deletion to proof
188                    proof.delete_clause(&clause1)?;
189                    self.stats.proof_steps += 1;
190
191                    // Mark clause1 as eliminated
192                    self.eliminated_clauses.insert(cid1);
193                    self.stats.subsumptions += 1;
194                    eliminated += 1;
195                    break; // clause1 is eliminated, move to next i
196                }
197            }
198        }
199
200        self.stats.clauses_eliminated += eliminated;
201        Ok(eliminated)
202    }
203
204    /// Check if clause1 subsumes clause2
205    fn subsumes(clause1: &[Lit], clause2: &[Lit]) -> bool {
206        if clause1.len() > clause2.len() {
207            return false;
208        }
209
210        // Every literal in clause1 must be in clause2
211        clause1.iter().all(|lit1| clause2.contains(lit1))
212    }
213
214    /// Eliminate blocked clauses with DRAT logging
215    fn eliminate_blocked_clauses(
216        &mut self,
217        db: &mut ClauseDatabase,
218        proof: &mut DratProof,
219    ) -> std::io::Result<usize> {
220        let mut eliminated = 0;
221
222        let clause_ids: Vec<ClauseId> = db.iter_ids().collect();
223
224        for cid in clause_ids {
225            if self.eliminated_clauses.contains(&cid) {
226                continue;
227            }
228
229            let clause = match db.get(cid) {
230                Some(c) => c.lits.to_vec(),
231                None => continue,
232            };
233
234            // Check if this clause is blocked on any of its literals
235            for &lit in &clause {
236                if self.is_blocked(db, &clause, lit) {
237                    // Log deletion to proof
238                    proof.delete_clause(&clause)?;
239                    self.stats.proof_steps += 1;
240
241                    // Mark as eliminated
242                    self.eliminated_clauses.insert(cid);
243                    self.stats.blocked_clauses_eliminated += 1;
244                    eliminated += 1;
245                    break;
246                }
247            }
248        }
249
250        self.stats.clauses_eliminated += eliminated;
251        Ok(eliminated)
252    }
253
254    /// Check if a clause is blocked on a literal
255    ///
256    /// A clause C is blocked on literal l if for every clause D containing ~l,
257    /// the resolvent of C and D is a tautology
258    fn is_blocked(&self, db: &ClauseDatabase, clause: &[Lit], lit: Lit) -> bool {
259        let neg_lit = lit.negate();
260
261        // Find all clauses containing ~lit
262        for other_cid in db.iter_ids() {
263            if self.eliminated_clauses.contains(&other_cid) {
264                continue;
265            }
266
267            let other_clause = match db.get(other_cid) {
268                Some(c) => &c.lits,
269                None => continue,
270            };
271
272            // Check if other_clause contains ~lit
273            if !other_clause.contains(&neg_lit) {
274                continue;
275            }
276
277            // Compute resolvent and check if it's a tautology
278            let resolvent = Self::resolve(clause, other_clause, lit.var());
279            if !Self::is_tautology(&resolvent) {
280                return false; // Found a non-tautological resolvent
281            }
282        }
283
284        true // All resolvents are tautologies
285    }
286
287    /// Resolve two clauses on a variable
288    fn resolve(clause1: &[Lit], clause2: &[Lit], var: Var) -> Vec<Lit> {
289        let mut resolvent = Vec::new();
290
291        // Add all literals from clause1 except those involving var
292        for &lit in clause1 {
293            if lit.var() != var {
294                resolvent.push(lit);
295            }
296        }
297
298        // Add all literals from clause2 except those involving var
299        for &lit in clause2 {
300            if lit.var() != var && !resolvent.contains(&lit) {
301                resolvent.push(lit);
302            }
303        }
304
305        resolvent
306    }
307
308    /// Check if a clause is a tautology (contains both l and ~l)
309    fn is_tautology(clause: &[Lit]) -> bool {
310        for &lit in clause {
311            if clause.contains(&lit.negate()) {
312                return true;
313            }
314        }
315        false
316    }
317
318    /// Eliminate variables with DRAT logging
319    fn eliminate_variables(
320        &mut self,
321        db: &mut ClauseDatabase,
322        proof: &mut DratProof,
323    ) -> std::io::Result<usize> {
324        let mut eliminated = 0;
325
326        // Find candidate variables for elimination
327        let vars = self.find_elimination_candidates(db);
328
329        for var in vars {
330            if self.can_eliminate_variable(db, var) {
331                eliminated += self.eliminate_variable(db, proof, var)?;
332                self.eliminated_vars.insert(var);
333                self.stats.variables_eliminated += 1;
334            }
335        }
336
337        Ok(eliminated)
338    }
339
340    /// Find candidate variables for elimination
341    fn find_elimination_candidates(&self, db: &ClauseDatabase) -> Vec<Var> {
342        let mut var_counts: HashMap<Var, usize> = HashMap::new();
343
344        // Count occurrences of each variable
345        for cid in db.iter_ids() {
346            if self.eliminated_clauses.contains(&cid) {
347                continue;
348            }
349
350            let clause = match db.get(cid) {
351                Some(c) => &c.lits,
352                None => continue,
353            };
354
355            for &lit in clause {
356                *var_counts.entry(lit.var()).or_insert(0) += 1;
357            }
358        }
359
360        // Select variables that appear infrequently
361        let mut candidates: Vec<_> = var_counts
362            .into_iter()
363            .filter(|(_, count)| *count <= 5) // Heuristic: low occurrence count
364            .map(|(var, _)| var)
365            .collect();
366
367        candidates.sort_unstable_by_key(|v| v.0);
368        candidates
369    }
370
371    /// Check if a variable can be eliminated
372    fn can_eliminate_variable(&self, db: &ClauseDatabase, var: Var) -> bool {
373        if self.eliminated_vars.contains(&var) {
374            return false;
375        }
376
377        // Collect clauses containing var positively and negatively
378        let (pos_clauses, neg_clauses) = self.collect_clauses_with_var(db, var);
379
380        // Check if elimination would create too many or too large clauses
381        let num_resolvents = pos_clauses.len() * neg_clauses.len();
382        if num_resolvents > self.config.max_resolution_size {
383            return false;
384        }
385
386        // Check clause sizes
387        for pos_clause in &pos_clauses {
388            if pos_clause.len() > self.config.max_clause_size_for_elimination {
389                return false;
390            }
391        }
392
393        for neg_clause in &neg_clauses {
394            if neg_clause.len() > self.config.max_clause_size_for_elimination {
395                return false;
396            }
397        }
398
399        true
400    }
401
402    /// Collect clauses containing a variable
403    fn collect_clauses_with_var(
404        &self,
405        db: &ClauseDatabase,
406        var: Var,
407    ) -> (Vec<Vec<Lit>>, Vec<Vec<Lit>>) {
408        let mut pos_clauses = Vec::new();
409        let mut neg_clauses = Vec::new();
410
411        for cid in db.iter_ids() {
412            if self.eliminated_clauses.contains(&cid) {
413                continue;
414            }
415
416            let clause = match db.get(cid) {
417                Some(c) => c.lits.to_vec(),
418                None => continue,
419            };
420
421            let pos_lit = Lit::pos(var);
422            let neg_lit = Lit::neg(var);
423
424            if clause.contains(&pos_lit) {
425                pos_clauses.push(clause);
426            } else if clause.contains(&neg_lit) {
427                neg_clauses.push(clause);
428            }
429        }
430
431        (pos_clauses, neg_clauses)
432    }
433
434    /// Eliminate a variable by resolution
435    fn eliminate_variable(
436        &mut self,
437        db: &mut ClauseDatabase,
438        proof: &mut DratProof,
439        var: Var,
440    ) -> std::io::Result<usize> {
441        let (pos_clauses, neg_clauses) = self.collect_clauses_with_var(db, var);
442
443        // Generate all resolvents
444        for pos_clause in &pos_clauses {
445            for neg_clause in &neg_clauses {
446                let resolvent = Self::resolve(pos_clause, neg_clause, var);
447
448                // Skip tautologies
449                if Self::is_tautology(&resolvent) {
450                    continue;
451                }
452
453                // Add resolvent to proof
454                proof.add_clause(&resolvent)?;
455                self.stats.proof_steps += 1;
456            }
457        }
458
459        // Delete original clauses from proof
460        for clause in pos_clauses.iter().chain(neg_clauses.iter()) {
461            proof.delete_clause(clause)?;
462            self.stats.proof_steps += 1;
463        }
464
465        let eliminated = pos_clauses.len() + neg_clauses.len();
466        self.stats.clauses_eliminated += eliminated;
467
468        Ok(eliminated)
469    }
470
471    /// Get statistics
472    pub fn stats(&self) -> &DratInprocessingStats {
473        &self.stats
474    }
475
476    /// Reset statistics
477    pub fn reset_stats(&mut self) {
478        self.stats = DratInprocessingStats::default();
479    }
480
481    /// Clear eliminated clauses and variables
482    pub fn clear(&mut self) {
483        self.eliminated_clauses.clear();
484        self.eliminated_vars.clear();
485        self.stats = DratInprocessingStats::default();
486    }
487}
488
489impl Default for DratInprocessor {
490    fn default() -> Self {
491        Self::default_config()
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_drat_inprocessor_creation() {
501        let inprocessor = DratInprocessor::default();
502        assert_eq!(inprocessor.stats().rounds, 0);
503    }
504
505    #[test]
506    fn test_subsumes() {
507        let v0 = Var(0);
508        let v1 = Var(1);
509
510        let clause1 = vec![Lit::pos(v0)];
511        let clause2 = vec![Lit::pos(v0), Lit::pos(v1)];
512
513        assert!(DratInprocessor::subsumes(&clause1, &clause2));
514        assert!(!DratInprocessor::subsumes(&clause2, &clause1));
515    }
516
517    #[test]
518    fn test_subsumes_equal() {
519        let v0 = Var(0);
520        let v1 = Var(1);
521
522        let clause1 = vec![Lit::pos(v0), Lit::pos(v1)];
523        let clause2 = vec![Lit::pos(v0), Lit::pos(v1)];
524
525        assert!(DratInprocessor::subsumes(&clause1, &clause2));
526        assert!(DratInprocessor::subsumes(&clause2, &clause1));
527    }
528
529    #[test]
530    fn test_resolve() {
531        let v0 = Var(0);
532        let v1 = Var(1);
533
534        let clause1 = vec![Lit::pos(v0), Lit::pos(v1)];
535        let clause2 = vec![Lit::neg(v0)];
536
537        let resolvent = DratInprocessor::resolve(&clause1, &clause2, v0);
538        assert_eq!(resolvent.len(), 1);
539        assert!(resolvent.contains(&Lit::pos(v1)));
540    }
541
542    #[test]
543    fn test_is_tautology() {
544        let v0 = Var(0);
545        let v1 = Var(1);
546
547        let tautology = vec![Lit::pos(v0), Lit::neg(v0), Lit::pos(v1)];
548        let non_tautology = vec![Lit::pos(v0), Lit::pos(v1)];
549
550        assert!(DratInprocessor::is_tautology(&tautology));
551        assert!(!DratInprocessor::is_tautology(&non_tautology));
552    }
553
554    #[test]
555    fn test_config_default() {
556        let config = DratInprocessingConfig::default();
557        assert!(config.enable_subsumption);
558        assert!(config.enable_variable_elimination);
559        assert!(config.enable_blocked_clause_elimination);
560    }
561
562    #[test]
563    fn test_stats_display() {
564        let stats = DratInprocessingStats {
565            clauses_eliminated: 10,
566            variables_eliminated: 2,
567            subsumptions: 5,
568            blocked_clauses_eliminated: 3,
569            proof_steps: 20,
570            rounds: 1,
571        };
572
573        let display = stats.display();
574        assert!(display.contains("10"));
575        assert!(display.contains("2"));
576        assert!(display.contains("5"));
577    }
578
579    #[test]
580    fn test_clear() {
581        let mut inprocessor = DratInprocessor::default();
582        inprocessor.stats.rounds = 5;
583        inprocessor.eliminated_vars.insert(Var(0));
584
585        inprocessor.clear();
586
587        assert_eq!(inprocessor.stats.rounds, 0);
588        assert!(inprocessor.eliminated_vars.is_empty());
589    }
590}