Skip to main content

oxiz_sat/
preprocessing_core.rs

1//! Preprocessing techniques for SAT solving
2//!
3//! This module implements various preprocessing and simplification techniques:
4//! - Blocked Clause Elimination (BCE)
5//! - Variable Elimination (VE)
6//! - Subsumption Elimination
7//! - Pure Literal Elimination
8//! - Self-Subsuming Resolution
9
10use crate::clause::{ClauseDatabase, ClauseId};
11use crate::literal::{Lit, Var};
12#[allow(unused_imports)]
13use crate::prelude::*;
14
15/// Occurrence list: maps literals to clauses containing them
16#[derive(Debug, Clone)]
17struct OccurrenceList {
18    /// occurrences[lit] = list of clause IDs containing lit
19    occurrences: Vec<Vec<ClauseId>>,
20}
21
22impl OccurrenceList {
23    fn new(num_vars: usize) -> Self {
24        Self {
25            occurrences: vec![Vec::new(); num_vars * 2],
26        }
27    }
28
29    fn add(&mut self, lit: Lit, clause_id: ClauseId) {
30        self.occurrences[lit.code() as usize].push(clause_id);
31    }
32
33    fn get(&self, lit: Lit) -> &[ClauseId] {
34        &self.occurrences[lit.code() as usize]
35    }
36
37    fn remove(&mut self, lit: Lit, clause_id: ClauseId) {
38        let list = &mut self.occurrences[lit.code() as usize];
39        if let Some(pos) = list.iter().position(|&id| id == clause_id) {
40            list.swap_remove(pos);
41        }
42    }
43
44    fn clear(&mut self) {
45        for list in &mut self.occurrences {
46            list.clear();
47        }
48    }
49}
50
51/// Preprocessing context
52#[derive(Debug)]
53pub struct Preprocessor {
54    /// Number of variables
55    num_vars: usize,
56    /// Occurrence lists
57    occurrences: OccurrenceList,
58    /// Eliminated variables
59    eliminated: HashSet<Var>,
60    /// Clauses to remove
61    removed_clauses: HashSet<ClauseId>,
62}
63
64impl Preprocessor {
65    /// Create a new preprocessor
66    pub fn new(num_vars: usize) -> Self {
67        Self {
68            num_vars,
69            occurrences: OccurrenceList::new(num_vars),
70            eliminated: HashSet::new(),
71            removed_clauses: HashSet::new(),
72        }
73    }
74
75    /// Build occurrence lists from clause database
76    pub fn build_occurrences(&mut self, clauses: &ClauseDatabase) {
77        self.occurrences.clear();
78        for i in 0..clauses.len() {
79            let clause_id = ClauseId::new(i as u32);
80            if let Some(clause) = clauses.get(clause_id)
81                && !clause.deleted
82            {
83                for &lit in &clause.lits {
84                    self.occurrences.add(lit, clause_id);
85                }
86            }
87        }
88    }
89
90    /// Check if a clause is a tautology (contains both l and ~l)
91    fn is_tautology(lits: &[Lit]) -> bool {
92        let mut seen = HashSet::new();
93        for &lit in lits {
94            if seen.contains(&lit.negate()) {
95                return true;
96            }
97            seen.insert(lit);
98        }
99        false
100    }
101
102    /// Check if clause C is blocked on literal l
103    ///
104    /// A clause C is blocked on literal l ∈ C if for every clause D with ~l ∈ D,
105    /// the resolvent of C and D on l is a tautology.
106    fn is_blocked(&self, clause_lits: &[Lit], blocking_lit: Lit, clauses: &ClauseDatabase) -> bool {
107        // Get all clauses containing ~blocking_lit
108        let neg_lit = blocking_lit.negate();
109
110        for &other_clause_id in self.occurrences.get(neg_lit) {
111            if let Some(other_clause) = clauses.get(other_clause_id) {
112                if other_clause.deleted {
113                    continue;
114                }
115
116                // Compute resolvent: (C \ {l}) ∪ (D \ {~l})
117                let mut resolvent = Vec::new();
118
119                // Add literals from C except blocking_lit
120                for &lit in clause_lits {
121                    if lit != blocking_lit {
122                        resolvent.push(lit);
123                    }
124                }
125
126                // Add literals from D except ~blocking_lit
127                for &lit in &other_clause.lits {
128                    if lit != neg_lit {
129                        resolvent.push(lit);
130                    }
131                }
132
133                // Check if resolvent is a tautology
134                if !Self::is_tautology(&resolvent) {
135                    return false;
136                }
137            }
138        }
139
140        true
141    }
142
143    /// Blocked Clause Elimination
144    ///
145    /// Remove clauses that are blocked on some literal.
146    /// Returns the number of clauses eliminated.
147    pub fn blocked_clause_elimination(&mut self, clauses: &mut ClauseDatabase) -> usize {
148        let mut eliminated = 0;
149        self.build_occurrences(clauses);
150
151        // Try to eliminate each clause
152        let clause_ids: Vec<_> = (0..clauses.len())
153            .map(|i| ClauseId::new(i as u32))
154            .collect();
155
156        for clause_id in clause_ids {
157            if self.removed_clauses.contains(&clause_id) {
158                continue;
159            }
160
161            if let Some(clause) = clauses.get(clause_id) {
162                if clause.deleted || clause.learned {
163                    continue;
164                }
165
166                let lits = clause.lits.clone();
167
168                // Try each literal as blocking literal
169                for &lit in &lits {
170                    if self.is_blocked(&lits, lit, clauses) {
171                        // Mark clause for removal
172                        if let Some(clause) = clauses.get_mut(clause_id) {
173                            clause.deleted = true;
174                        }
175                        self.removed_clauses.insert(clause_id);
176                        eliminated += 1;
177
178                        // Update occurrence lists
179                        for &l in &lits {
180                            self.occurrences.remove(l, clause_id);
181                        }
182                        break;
183                    }
184                }
185            }
186        }
187
188        eliminated
189    }
190
191    /// Pure Literal Elimination
192    ///
193    /// A pure literal is one that appears only in positive or only in negative form.
194    /// All clauses containing a pure literal can be satisfied and removed.
195    /// Returns the number of clauses eliminated.
196    pub fn pure_literal_elimination(&mut self, clauses: &mut ClauseDatabase) -> usize {
197        let mut eliminated = 0;
198        self.build_occurrences(clauses);
199
200        // Find pure literals
201        let mut pure_literals = Vec::new();
202
203        for v in 0..self.num_vars {
204            let var = Var(v as u32);
205            let pos_lit = Lit::pos(var);
206            let neg_lit = Lit::neg(var);
207
208            let pos_occurs = !self.occurrences.get(pos_lit).is_empty();
209            let neg_occurs = !self.occurrences.get(neg_lit).is_empty();
210
211            if pos_occurs && !neg_occurs {
212                pure_literals.push(pos_lit);
213            } else if neg_occurs && !pos_occurs {
214                pure_literals.push(neg_lit);
215            }
216        }
217
218        // Remove clauses containing pure literals
219        for lit in pure_literals {
220            for &clause_id in self.occurrences.get(lit).iter() {
221                if !self.removed_clauses.contains(&clause_id)
222                    && let Some(clause) = clauses.get_mut(clause_id)
223                    && !clause.deleted
224                    && !clause.learned
225                {
226                    clause.deleted = true;
227                    self.removed_clauses.insert(clause_id);
228                    eliminated += 1;
229                }
230            }
231        }
232
233        eliminated
234    }
235
236    /// Subsumption Elimination
237    ///
238    /// A clause C subsumes clause D if C ⊆ D (every literal in C is in D).
239    /// If C subsumes D, then D can be removed.
240    /// Returns the number of clauses eliminated.
241    pub fn subsumption_elimination(&mut self, clauses: &mut ClauseDatabase) -> usize {
242        let mut eliminated = 0;
243        self.build_occurrences(clauses);
244
245        let clause_ids: Vec<_> = (0..clauses.len())
246            .map(|i| ClauseId::new(i as u32))
247            .collect();
248
249        for i in 0..clause_ids.len() {
250            let clause_id = clause_ids[i];
251
252            if self.removed_clauses.contains(&clause_id) {
253                continue;
254            }
255
256            let clause_lits = if let Some(clause) = clauses.get(clause_id) {
257                if clause.deleted || clause.learned {
258                    continue;
259                }
260                clause.lits.clone()
261            } else {
262                continue;
263            };
264
265            // Check if this clause subsumes any other clause
266            for &other_id in &clause_ids[(i + 1)..] {
267                if self.removed_clauses.contains(&other_id) {
268                    continue;
269                }
270
271                let other_lits = if let Some(other_clause) = clauses.get(other_id) {
272                    if other_clause.deleted || other_clause.learned {
273                        continue;
274                    }
275                    &other_clause.lits
276                } else {
277                    continue;
278                };
279
280                // Check if clause_lits ⊆ other_lits
281                if clause_lits.iter().all(|lit| other_lits.contains(lit)) {
282                    // clause subsumes other - remove other
283                    if let Some(other_clause) = clauses.get_mut(other_id) {
284                        other_clause.deleted = true;
285                    }
286                    self.removed_clauses.insert(other_id);
287                    eliminated += 1;
288                }
289            }
290        }
291
292        eliminated
293    }
294
295    /// Variable Elimination (Bounded Variable Elimination)
296    ///
297    /// Eliminate a variable by resolving all pairs of clauses containing v and ~v,
298    /// but only if the number of resulting clauses is not too large.
299    /// Returns the number of variables eliminated.
300    #[allow(dead_code)]
301    pub fn variable_elimination(&mut self, clauses: &mut ClauseDatabase, limit: usize) -> usize {
302        let mut eliminated = 0;
303        self.build_occurrences(clauses);
304
305        for v in 0..self.num_vars {
306            let var = Var(v as u32);
307            if self.eliminated.contains(&var) {
308                continue;
309            }
310
311            let pos_lit = Lit::pos(var);
312            let neg_lit = Lit::neg(var);
313
314            let pos_clauses: Vec<_> = self.occurrences.get(pos_lit).to_vec();
315            let neg_clauses: Vec<_> = self.occurrences.get(neg_lit).to_vec();
316
317            // Bound check: only eliminate if cost is reasonable
318            let resolvents = pos_clauses.len() * neg_clauses.len();
319            let current = pos_clauses.len() + neg_clauses.len();
320
321            if resolvents > limit || resolvents > current {
322                continue;
323            }
324
325            // Generate all resolvents
326            let mut new_clauses = Vec::new();
327
328            for &pos_clause_id in &pos_clauses {
329                for &neg_clause_id in &neg_clauses {
330                    let pos_lits = if let Some(c) = clauses.get(pos_clause_id) {
331                        &c.lits
332                    } else {
333                        continue;
334                    };
335
336                    let neg_lits = if let Some(c) = clauses.get(neg_clause_id) {
337                        &c.lits
338                    } else {
339                        continue;
340                    };
341
342                    // Compute resolvent
343                    let mut resolvent = Vec::new();
344
345                    for &lit in pos_lits {
346                        if lit != pos_lit {
347                            resolvent.push(lit);
348                        }
349                    }
350
351                    for &lit in neg_lits {
352                        if lit != neg_lit && !resolvent.contains(&lit) {
353                            resolvent.push(lit);
354                        }
355                    }
356
357                    // Check if resolvent is tautology
358                    if Self::is_tautology(&resolvent) {
359                        continue;
360                    }
361
362                    new_clauses.push(resolvent);
363                }
364            }
365
366            // Eliminate the variable
367            self.eliminated.insert(var);
368            eliminated += 1;
369
370            // Remove old clauses
371            for &clause_id in &pos_clauses {
372                if let Some(clause) = clauses.get_mut(clause_id) {
373                    clause.deleted = true;
374                }
375                self.removed_clauses.insert(clause_id);
376            }
377
378            for &clause_id in &neg_clauses {
379                if let Some(clause) = clauses.get_mut(clause_id) {
380                    clause.deleted = true;
381                }
382                self.removed_clauses.insert(clause_id);
383            }
384
385            // Add new clauses
386            for resolvent in new_clauses {
387                if !resolvent.is_empty() {
388                    clauses.add_original(resolvent);
389                }
390            }
391        }
392
393        eliminated
394    }
395
396    /// Failed Literal Probing
397    ///
398    /// Try to assign each literal and propagate. If a literal leads to a conflict,
399    /// we can infer its negation must be true (failed literal).
400    /// Returns the number of failed literals found.
401    pub fn failed_literal_probing(&mut self, clauses: &mut ClauseDatabase) -> usize {
402        use crate::trail::Trail;
403        use crate::watched::{WatchLists, Watcher};
404
405        let mut found = 0;
406        self.build_occurrences(clauses);
407
408        // Create temporary trail and watch lists for probing
409        let mut trail = Trail::new(self.num_vars);
410        let mut watches = WatchLists::new(self.num_vars);
411
412        // Build watch lists from current clauses
413        for i in 0..clauses.len() {
414            let clause_id = ClauseId::new(i as u32);
415            if let Some(clause) = clauses.get(clause_id) {
416                if clause.deleted || clause.lits.len() < 2 {
417                    continue;
418                }
419
420                let lit0 = clause.lits[0];
421                let lit1 = clause.lits[1];
422                watches.add(lit0.negate(), Watcher::new(clause_id, lit1));
423                watches.add(lit1.negate(), Watcher::new(clause_id, lit0));
424            }
425        }
426
427        // Try to probe each literal
428        let mut failed_literals = Vec::new();
429
430        for v in 0..self.num_vars {
431            let var = Var(v as u32);
432            if trail.is_assigned(var) {
433                continue;
434            }
435
436            for &polarity in &[false, true] {
437                let probe_lit = if polarity {
438                    Lit::pos(var)
439                } else {
440                    Lit::neg(var)
441                };
442
443                // Save trail state
444                let saved_level = trail.decision_level();
445
446                // Try to assign the literal
447                trail.new_decision_level();
448                trail.assign_decision(probe_lit);
449
450                // Propagate and check for conflict
451                let conflict = self.propagate_probe(&mut trail, &watches, clauses);
452
453                // Backtrack
454                trail.backtrack_to(saved_level);
455
456                if conflict {
457                    // Found a failed literal! Add its negation as a unit clause
458                    failed_literals.push(probe_lit.negate());
459                    found += 1;
460                    break;
461                }
462            }
463        }
464
465        // Add all failed literals as unit clauses
466        for lit in failed_literals {
467            clauses.add_original([lit]);
468        }
469
470        found
471    }
472
473    /// Helper for propagating during probing
474    fn propagate_probe(
475        &self,
476        trail: &mut crate::trail::Trail,
477        watches: &crate::watched::WatchLists,
478        clauses: &ClauseDatabase,
479    ) -> bool {
480        use crate::literal::LBool;
481
482        while let Some(lit) = trail.next_to_propagate() {
483            let watch_list = watches.get(lit);
484
485            for &watcher in watch_list {
486                let clause_id = watcher.clause;
487                let blocker = watcher.blocker;
488
489                // Check blocker literal
490                if trail.lit_value(blocker) == LBool::True {
491                    continue;
492                }
493
494                let clause = match clauses.get(clause_id) {
495                    Some(c) if !c.deleted => c,
496                    _ => continue,
497                };
498
499                // Find the two watched literals
500                let mut first = clause.lits[0];
501                let mut second = clause.lits[1];
502
503                if first == lit.negate() {
504                    core::mem::swap(&mut first, &mut second);
505                }
506
507                // Try to find a new watch
508                let mut found_new_watch = false;
509                for &other_lit in &clause.lits[2..] {
510                    if trail.lit_value(other_lit) != LBool::False {
511                        // Found a new watch - would need to update watches but we're read-only
512                        found_new_watch = true;
513                        break;
514                    }
515                }
516
517                if !found_new_watch {
518                    // Check if other watch is false (conflict)
519                    if trail.lit_value(first) == LBool::False {
520                        return true; // Conflict found
521                    }
522
523                    // Unit propagation
524                    if !trail.is_assigned(first.var()) {
525                        trail.assign_propagation(first, clause_id);
526                    }
527                }
528            }
529        }
530
531        false // No conflict
532    }
533
534    /// Bounded Variable Addition (BVA)
535    ///
536    /// Introduce new variables to simplify formulas by factoring out common literals.
537    /// For example, if we have clauses (a ∨ b ∨ c) and (a ∨ b ∨ d), we can replace them with:
538    /// (a ∨ b ∨ x), (~x ∨ c), (~x ∨ d)
539    /// where x is a fresh variable. This can reduce total clause size and improve solving.
540    ///
541    /// Returns the number of variables added.
542    #[allow(dead_code)]
543    pub fn bounded_variable_addition(
544        &mut self,
545        clauses: &mut ClauseDatabase,
546        max_vars_to_add: usize,
547    ) -> usize {
548        let mut vars_added = 0;
549        self.build_occurrences(clauses);
550
551        // Collect all clause pairs with sufficient overlap
552        let clause_ids: Vec<_> = (0..clauses.len())
553            .map(|i| ClauseId::new(i as u32))
554            .filter(|&id| {
555                clauses
556                    .get(id)
557                    .is_some_and(|c| !c.deleted && !c.learned && c.lits.len() >= 3)
558            })
559            .collect();
560
561        for i in 0..clause_ids.len() {
562            if vars_added >= max_vars_to_add {
563                break;
564            }
565
566            let clause1_id = clause_ids[i];
567            let clause1_lits = match clauses.get(clause1_id) {
568                Some(c) if !c.deleted => c.lits.clone(),
569                _ => continue,
570            };
571
572            for &clause2_id in &clause_ids[(i + 1)..] {
573                if vars_added >= max_vars_to_add {
574                    break;
575                }
576
577                let clause2_lits = match clauses.get(clause2_id) {
578                    Some(c) if !c.deleted => c.lits.clone(),
579                    _ => continue,
580                };
581
582                // Find common literals
583                let common: Vec<Lit> = clause1_lits
584                    .iter()
585                    .filter(|&lit| clause2_lits.contains(lit))
586                    .copied()
587                    .collect();
588
589                // Only apply BVA if we have at least 2 common literals
590                if common.len() < 2 {
591                    continue;
592                }
593
594                // Check if it's beneficial (reduces total clause size)
595                let unique1: Vec<Lit> = clause1_lits
596                    .iter()
597                    .filter(|lit| !common.contains(lit))
598                    .copied()
599                    .collect();
600
601                let unique2: Vec<Lit> = clause2_lits
602                    .iter()
603                    .filter(|lit| !common.contains(lit))
604                    .copied()
605                    .collect();
606
607                // Original size: |clause1| + |clause2|
608                let original_size = clause1_lits.len() + clause2_lits.len();
609
610                // New size: |common| + 1 + |unique1| + 1 + |unique2| + 1
611                // = |common| + |unique1| + |unique2| + 3
612                let new_size = common.len() + unique1.len() + unique2.len() + 3;
613
614                // Only add variable if it reduces total size
615                if new_size >= original_size {
616                    continue;
617                }
618
619                // Create a new variable
620                let new_var = Var::new((self.num_vars + vars_added) as u32);
621                let new_lit = Lit::pos(new_var);
622
623                // Create new clauses:
624                // 1. (common literals) ∨ new_var
625                // 2. ~new_var ∨ (unique literals from clause1)
626                // 3. ~new_var ∨ (unique literals from clause2)
627
628                let mut new_clause1 = common.clone();
629                new_clause1.push(new_lit);
630
631                let mut new_clause2 = vec![new_lit.negate()];
632                new_clause2.extend(&unique1);
633
634                let mut new_clause3 = vec![new_lit.negate()];
635                new_clause3.extend(&unique2);
636
637                // Remove old clauses
638                if let Some(c) = clauses.get_mut(clause1_id) {
639                    c.deleted = true;
640                }
641                if let Some(c) = clauses.get_mut(clause2_id) {
642                    c.deleted = true;
643                }
644                self.removed_clauses.insert(clause1_id);
645                self.removed_clauses.insert(clause2_id);
646
647                // Add new clauses
648                if !new_clause1.is_empty() {
649                    clauses.add_original(new_clause1);
650                }
651                if !new_clause2.is_empty() {
652                    clauses.add_original(new_clause2);
653                }
654                if !new_clause3.is_empty() {
655                    clauses.add_original(new_clause3);
656                }
657
658                vars_added += 1;
659                break; // Process next clause pair
660            }
661        }
662
663        // Update num_vars to reflect new variables
664        self.num_vars += vars_added;
665
666        vars_added
667    }
668
669    /// Apply all preprocessing techniques
670    ///
671    /// Returns (clauses_eliminated, vars_eliminated)
672    pub fn preprocess(&mut self, clauses: &mut ClauseDatabase) -> (usize, usize) {
673        let mut total_clauses = 0;
674        let total_vars = 0;
675
676        // Iteratively apply preprocessing until fixpoint
677        loop {
678            let mut changed = false;
679
680            // Pure literal elimination
681            let pure_elim = self.pure_literal_elimination(clauses);
682            if pure_elim > 0 {
683                total_clauses += pure_elim;
684                changed = true;
685            }
686
687            // Subsumption elimination
688            let subsumption = self.subsumption_elimination(clauses);
689            if subsumption > 0 {
690                total_clauses += subsumption;
691                changed = true;
692            }
693
694            // Blocked clause elimination
695            let bce = self.blocked_clause_elimination(clauses);
696            if bce > 0 {
697                total_clauses += bce;
698                changed = true;
699            }
700
701            if !changed {
702                break;
703            }
704        }
705
706        (total_clauses, total_vars)
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713
714    #[test]
715    fn test_tautology_detection() {
716        let v0 = Var(0);
717        let l0 = Lit::pos(v0);
718        let l0_neg = Lit::neg(v0);
719
720        // Tautology: x0 ∨ ~x0
721        assert!(Preprocessor::is_tautology(&[l0, l0_neg]));
722
723        // Not a tautology: x0 ∨ x0
724        assert!(!Preprocessor::is_tautology(&[l0, l0]));
725    }
726
727    #[test]
728    fn test_pure_literal() {
729        let mut clauses = ClauseDatabase::new();
730        let v0 = Var(0);
731        let v1 = Var(1);
732
733        // Add clauses: (x0 ∨ x1), (x0)
734        // x0 and x1 are pure (only positive)
735        clauses.add_original([Lit::pos(v0), Lit::pos(v1)]);
736        clauses.add_original([Lit::pos(v0)]);
737
738        let mut prep = Preprocessor::new(2);
739        let eliminated = prep.pure_literal_elimination(&mut clauses);
740
741        assert_eq!(eliminated, 2);
742    }
743
744    #[test]
745    fn test_subsumption() {
746        let mut clauses = ClauseDatabase::new();
747        let v0 = Var(0);
748        let v1 = Var(1);
749
750        // Add clauses: (x0), (x0 ∨ x1)
751        // First clause subsumes second
752        clauses.add_original([Lit::pos(v0)]);
753        clauses.add_original([Lit::pos(v0), Lit::pos(v1)]);
754
755        let mut prep = Preprocessor::new(2);
756        let eliminated = prep.subsumption_elimination(&mut clauses);
757
758        assert_eq!(eliminated, 1);
759    }
760}