Skip to main content

oxiz_sat/
clause.rs

1//! Clause representation and database
2
3use crate::literal::Lit;
4#[allow(unused_imports)]
5use crate::prelude::*;
6use smallvec::SmallVec;
7
8/// Unique identifier for a clause
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct ClauseId(pub u32);
11
12impl ClauseId {
13    /// The null clause ID (indicates no clause)
14    pub const NULL: Self = Self(u32::MAX);
15
16    /// Create a new clause ID
17    #[must_use]
18    pub const fn new(id: u32) -> Self {
19        Self(id)
20    }
21
22    /// Check if this is a null ID
23    #[must_use]
24    pub const fn is_null(self) -> bool {
25        self.0 == u32::MAX
26    }
27
28    /// Get the raw index
29    #[must_use]
30    pub const fn index(self) -> usize {
31        self.0 as usize
32    }
33}
34
35/// Clause tier for tiered database management
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
37pub enum ClauseTier {
38    /// Tier 3: Local clauses (recently learned, deleted aggressively)
39    Local = 3,
40    /// Tier 2: Mid-tier clauses (useful but not essential, deleted conservatively)
41    Mid = 2,
42    /// Tier 1: Core/GLUE clauses (very high quality, rarely deleted)
43    Core = 1,
44}
45
46/// A clause is a disjunction of literals
47///
48/// Cache-line aligned for better memory performance
49#[derive(Debug, Clone)]
50#[repr(align(64))]
51pub struct Clause {
52    /// The literals in this clause
53    pub lits: SmallVec<[Lit; 4]>,
54    /// Whether this is a learned clause
55    pub learned: bool,
56    /// Activity for clause deletion heuristic
57    pub activity: f64,
58    /// LBD (Literal Block Distance) for quality metric
59    pub lbd: u32,
60    /// Whether this clause is marked for deletion
61    pub deleted: bool,
62    /// Tier for tiered database management (only used for learned clauses)
63    pub tier: ClauseTier,
64    /// Number of times this clause was used in conflict analysis (for tier promotion)
65    pub usage_count: u32,
66}
67
68impl Clause {
69    /// Create a new clause
70    #[must_use]
71    pub fn new(lits: impl IntoIterator<Item = Lit>, learned: bool) -> Self {
72        Self {
73            lits: lits.into_iter().collect(),
74            learned,
75            activity: 0.0,
76            lbd: 0,
77            deleted: false,
78            tier: ClauseTier::Local, // All learned clauses start in Local tier
79            usage_count: 0,
80        }
81    }
82
83    /// Create an original (non-learned) clause
84    #[must_use]
85    pub fn original(lits: impl IntoIterator<Item = Lit>) -> Self {
86        Self::new(lits, false)
87    }
88
89    /// Create a learned clause
90    #[must_use]
91    pub fn learned(lits: impl IntoIterator<Item = Lit>) -> Self {
92        Self::new(lits, true)
93    }
94
95    /// Get the number of literals
96    #[must_use]
97    pub fn len(&self) -> usize {
98        self.lits.len()
99    }
100
101    /// Check if empty
102    #[must_use]
103    pub fn is_empty(&self) -> bool {
104        self.lits.is_empty()
105    }
106
107    /// Check if this is a unit clause
108    #[must_use]
109    pub fn is_unit(&self) -> bool {
110        self.lits.len() == 1
111    }
112
113    /// Check if this is a binary clause
114    #[must_use]
115    pub fn is_binary(&self) -> bool {
116        self.lits.len() == 2
117    }
118
119    /// Get the first literal (for unit clauses)
120    #[must_use]
121    pub fn unit_lit(&self) -> Option<Lit> {
122        if self.is_unit() {
123            Some(self.lits[0])
124        } else {
125            None
126        }
127    }
128
129    /// Swap literals at indices i and j
130    pub fn swap(&mut self, i: usize, j: usize) {
131        self.lits.swap(i, j);
132    }
133
134    /// Increment usage count and potentially promote tier
135    pub fn record_usage(&mut self) {
136        self.usage_count += 1;
137
138        // Promote to Mid tier after 3 uses
139        if self.usage_count >= 3 && self.tier == ClauseTier::Local {
140            self.tier = ClauseTier::Mid;
141        }
142        // Promote to Core tier after 10 uses or if LBD ≤ 2
143        else if (self.usage_count >= 10 || self.lbd <= 2) && self.tier == ClauseTier::Mid {
144            self.tier = ClauseTier::Core;
145        }
146    }
147
148    /// Promote clause to Core tier (for GLUE clauses)
149    pub fn promote_to_core(&mut self) {
150        self.tier = ClauseTier::Core;
151    }
152
153    /// Normalize clause: remove duplicates, sort literals, check for tautology
154    /// Returns true if clause is a tautology (contains both l and ~l)
155    pub fn normalize(&mut self) -> bool {
156        if self.lits.is_empty() {
157            return false;
158        }
159
160        // Sort literals for better cache locality and faster operations
161        self.lits.sort_unstable_by_key(|lit| lit.code());
162
163        // Remove duplicates and check for tautology in a single pass
164        let mut write_idx = 0;
165        let mut prev_lit = self.lits[0];
166
167        for read_idx in 1..self.lits.len() {
168            let curr_lit = self.lits[read_idx];
169
170            // Check for tautology (complementary literals)
171            if curr_lit == prev_lit.negate() {
172                return true;
173            }
174
175            // Skip duplicates
176            if curr_lit != prev_lit {
177                write_idx += 1;
178                self.lits[write_idx] = curr_lit;
179                prev_lit = curr_lit;
180            }
181        }
182
183        // Truncate to remove duplicates
184        self.lits.truncate(write_idx + 1);
185        false
186    }
187
188    /// Check if this clause subsumes another clause
189    /// A clause C subsumes D if C ⊆ D (all literals of C are in D)
190    #[must_use]
191    pub fn subsumes(&self, other: &Clause) -> bool {
192        if self.lits.len() > other.lits.len() {
193            return false;
194        }
195
196        // Both clauses should be sorted for efficient checking
197        let mut i = 0;
198        let mut j = 0;
199
200        while i < self.lits.len() && j < other.lits.len() {
201            if self.lits[i] == other.lits[j] {
202                i += 1;
203                j += 1;
204            } else if self.lits[i].code() < other.lits[j].code() {
205                // Literal from self not in other
206                return false;
207            } else {
208                j += 1;
209            }
210        }
211
212        i == self.lits.len()
213    }
214
215    /// Check if this clause is a self-subsuming resolvent of another clause
216    /// Returns the literal to remove from other if self-subsumption is possible
217    #[must_use]
218    pub fn self_subsuming_resolvent(&self, other: &Clause) -> Option<Lit> {
219        if self.lits.len() >= other.lits.len() {
220            return None;
221        }
222
223        let mut diff_lit = None;
224        let mut matches = 0;
225
226        for &other_lit in &other.lits {
227            if self.lits.contains(&other_lit) {
228                matches += 1;
229            } else if self.lits.contains(&other_lit.negate()) {
230                if diff_lit.is_some() {
231                    return None; // More than one difference
232                }
233                diff_lit = Some(other_lit);
234            }
235        }
236
237        // Self-subsuming resolution requires exactly one complementary literal
238        // and all other literals of self must be in other
239        if matches == self.lits.len() - 1 && diff_lit.is_some() {
240            diff_lit
241        } else {
242            None
243        }
244    }
245}
246
247/// Statistics for clause database
248#[derive(Debug, Clone, Default)]
249pub struct ClauseDatabaseStats {
250    /// Number of clauses in each tier
251    pub tier_counts: [usize; 3], // [Core, Mid, Local]
252    /// Total LBD sum for computing average
253    pub total_lbd: u64,
254    /// Number of clauses with LBD counted
255    pub lbd_count: usize,
256    /// Distribution of clause sizes
257    pub size_distribution: [usize; 10], // [binary, ternary, 4-lit, ..., 10+]
258    /// Number of clause promotions
259    pub promotions: usize,
260    /// Number of clause demotions
261    pub demotions: usize,
262}
263
264impl ClauseDatabaseStats {
265    /// Get average LBD across all learned clauses
266    #[must_use]
267    pub fn avg_lbd(&self) -> f64 {
268        if self.lbd_count == 0 {
269            0.0
270        } else {
271            self.total_lbd as f64 / self.lbd_count as f64
272        }
273    }
274
275    /// Display statistics
276    pub fn display(&self) {
277        println!("Clause Database Statistics:");
278        println!("  Tier distribution:");
279        println!("    Core:  {}", self.tier_counts[0]);
280        println!("    Mid:   {}", self.tier_counts[1]);
281        println!("    Local: {}", self.tier_counts[2]);
282        println!("  Average LBD: {:.2}", self.avg_lbd());
283        println!("  Size distribution:");
284        for (i, &count) in self.size_distribution.iter().enumerate() {
285            if count > 0 {
286                let size = if i < 9 {
287                    format!("{}", i + 2)
288                } else {
289                    "10+".to_string()
290                };
291                println!("    {} literals: {}", size, count);
292            }
293        }
294        println!(
295            "  Promotions: {}, Demotions: {}",
296            self.promotions, self.demotions
297        );
298    }
299}
300
301/// Database of clauses with memory pool
302#[derive(Debug)]
303pub struct ClauseDatabase {
304    /// All clauses
305    clauses: Vec<Clause>,
306    /// Number of original clauses
307    num_original: usize,
308    /// Number of learned clauses
309    num_learned: usize,
310    /// Free list for reusing deleted clause slots (memory pool)
311    free_list: Vec<ClauseId>,
312    /// Statistics
313    stats: ClauseDatabaseStats,
314}
315
316impl Default for ClauseDatabase {
317    fn default() -> Self {
318        Self::new()
319    }
320}
321
322impl ClauseDatabase {
323    /// Create a new clause database
324    #[must_use]
325    pub fn new() -> Self {
326        Self {
327            clauses: Vec::new(),
328            num_original: 0,
329            num_learned: 0,
330            free_list: Vec::new(),
331            stats: ClauseDatabaseStats::default(),
332        }
333    }
334
335    /// Get statistics about the clause database
336    #[must_use]
337    pub fn stats(&self) -> &ClauseDatabaseStats {
338        &self.stats
339    }
340
341    /// Update statistics for a clause
342    fn update_stats_add(&mut self, clause: &Clause) {
343        if clause.learned {
344            // Update tier count
345            let tier_idx = match clause.tier {
346                ClauseTier::Core => 0,
347                ClauseTier::Mid => 1,
348                ClauseTier::Local => 2,
349            };
350            self.stats.tier_counts[tier_idx] += 1;
351
352            // Update LBD stats
353            if clause.lbd > 0 {
354                self.stats.total_lbd += clause.lbd as u64;
355                self.stats.lbd_count += 1;
356            }
357        }
358
359        // Update size distribution (only for clauses with 2+ literals)
360        if clause.len() >= 2 {
361            let size_idx = if clause.len() >= 12 {
362                9 // 10+ bucket
363            } else {
364                clause.len() - 2
365            };
366            self.stats.size_distribution[size_idx] += 1;
367        }
368    }
369
370    /// Update statistics when removing a clause
371    fn update_stats_remove(&mut self, clause: &Clause) {
372        if clause.learned {
373            // Update tier count
374            let tier_idx = match clause.tier {
375                ClauseTier::Core => 0,
376                ClauseTier::Mid => 1,
377                ClauseTier::Local => 2,
378            };
379            if self.stats.tier_counts[tier_idx] > 0 {
380                self.stats.tier_counts[tier_idx] -= 1;
381            }
382
383            // Update LBD stats
384            if clause.lbd > 0 && self.stats.lbd_count > 0 {
385                self.stats.total_lbd = self.stats.total_lbd.saturating_sub(clause.lbd as u64);
386                self.stats.lbd_count -= 1;
387            }
388        }
389
390        // Update size distribution (only for clauses with 2+ literals)
391        if clause.len() >= 2 {
392            let size_idx = if clause.len() >= 12 {
393                9 // 10+ bucket
394            } else {
395                clause.len() - 2
396            };
397            if self.stats.size_distribution[size_idx] > 0 {
398                self.stats.size_distribution[size_idx] -= 1;
399            }
400        }
401    }
402
403    /// Add a clause to the database
404    ///
405    /// Uses the memory pool (free list) to reuse deleted clause slots when available
406    pub fn add(&mut self, clause: Clause) -> ClauseId {
407        // Update statistics
408        self.update_stats_add(&clause);
409
410        // Try to reuse a slot from the free list
411        if let Some(id) = self.free_list.pop() {
412            // Reuse this slot
413            if let Some(slot) = self.clauses.get_mut(id.index()) {
414                *slot = clause.clone();
415                if clause.learned {
416                    self.num_learned += 1;
417                } else {
418                    self.num_original += 1;
419                }
420                return id;
421            }
422        }
423
424        // No free slot available, allocate new
425        let id = ClauseId::new(self.clauses.len() as u32);
426        if clause.learned {
427            self.num_learned += 1;
428        } else {
429            self.num_original += 1;
430        }
431        self.clauses.push(clause);
432        id
433    }
434
435    /// Add an original clause
436    pub fn add_original(&mut self, lits: impl IntoIterator<Item = Lit>) -> ClauseId {
437        self.add(Clause::original(lits))
438    }
439
440    /// Add a learned clause
441    pub fn add_learned(&mut self, lits: impl IntoIterator<Item = Lit>) -> ClauseId {
442        self.add(Clause::learned(lits))
443    }
444
445    /// Get a clause by ID
446    #[must_use]
447    pub fn get(&self, id: ClauseId) -> Option<&Clause> {
448        self.clauses.get(id.index())
449    }
450
451    /// Get a mutable reference to a clause
452    pub fn get_mut(&mut self, id: ClauseId) -> Option<&mut Clause> {
453        self.clauses.get_mut(id.index())
454    }
455
456    /// Mark a clause as deleted
457    ///
458    /// The deleted clause slot is added to the free list for reuse (memory pool)
459    pub fn remove(&mut self, id: ClauseId) {
460        if let Some(clause) = self.clauses.get_mut(id.index())
461            && !clause.deleted
462        {
463            // Clone necessary info for stats update
464            let clause_copy = clause.clone();
465
466            clause.deleted = true;
467            if clause.learned {
468                self.num_learned -= 1;
469            } else {
470                self.num_original -= 1;
471            }
472            // Add to free list for reuse
473            self.free_list.push(id);
474
475            // Update statistics after marking as deleted
476            self.update_stats_remove(&clause_copy);
477        }
478    }
479
480    /// Compact the database by removing deleted clauses from the free list
481    ///
482    /// This should be called periodically to prevent the free list from growing too large
483    pub fn compact(&mut self) {
484        // Limit free list size to avoid memory bloat
485        const MAX_FREE_LIST_SIZE: usize = 1000;
486
487        if self.free_list.len() > MAX_FREE_LIST_SIZE {
488            // Keep only the most recent freed slots
489            self.free_list
490                .drain(0..self.free_list.len() - MAX_FREE_LIST_SIZE);
491        }
492    }
493
494    /// Get the number of active clauses
495    #[must_use]
496    pub fn len(&self) -> usize {
497        self.num_original + self.num_learned
498    }
499
500    /// Check if empty
501    #[must_use]
502    pub fn is_empty(&self) -> bool {
503        self.len() == 0
504    }
505
506    /// Get the number of original clauses
507    #[must_use]
508    pub fn num_original(&self) -> usize {
509        self.num_original
510    }
511
512    /// Get the number of learned clauses
513    #[must_use]
514    pub fn num_learned(&self) -> usize {
515        self.num_learned
516    }
517
518    /// Iterate over all non-deleted clause IDs
519    pub fn iter_ids(&self) -> impl Iterator<Item = ClauseId> + '_ {
520        self.clauses
521            .iter()
522            .enumerate()
523            .filter(|(_, c)| !c.deleted)
524            .map(|(i, _)| ClauseId::new(i as u32))
525    }
526
527    /// Bump activity of a clause
528    pub fn bump_activity(&mut self, id: ClauseId, increment: f64) {
529        if let Some(clause) = self.get_mut(id) {
530            clause.activity += increment;
531        }
532    }
533
534    /// Decay all clause activities
535    pub fn decay_activity(&mut self, factor: f64) {
536        for clause in &mut self.clauses {
537            if !clause.deleted {
538                clause.activity *= factor;
539            }
540        }
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::literal::Var;
548
549    #[test]
550    fn test_clause_creation() {
551        let lits = vec![
552            Lit::pos(Var::new(0)),
553            Lit::neg(Var::new(1)),
554            Lit::pos(Var::new(2)),
555        ];
556        let clause = Clause::original(lits.clone());
557
558        assert_eq!(clause.len(), 3);
559        assert!(!clause.is_unit());
560        assert!(!clause.is_binary());
561        assert!(!clause.learned);
562    }
563
564    #[test]
565    fn test_clause_database() {
566        let mut db = ClauseDatabase::new();
567
568        let c1 = db.add_original([Lit::pos(Var::new(0)), Lit::neg(Var::new(1))]);
569        let _c2 = db.add_learned([Lit::pos(Var::new(2))]);
570
571        assert_eq!(db.len(), 2);
572        assert_eq!(db.num_original(), 1);
573        assert_eq!(db.num_learned(), 1);
574
575        db.remove(c1);
576        assert_eq!(db.len(), 1);
577        assert_eq!(db.num_original(), 0);
578    }
579
580    #[test]
581    fn test_clause_normalize() {
582        let mut clause = Clause::original([
583            Lit::pos(Var::new(2)),
584            Lit::pos(Var::new(0)),
585            Lit::pos(Var::new(2)), // duplicate
586            Lit::pos(Var::new(1)),
587        ]);
588
589        let is_tautology = clause.normalize();
590        assert!(!is_tautology);
591        assert_eq!(clause.len(), 3); // duplicate removed
592        // Check sorted order
593        assert_eq!(clause.lits[0], Lit::pos(Var::new(0)));
594        assert_eq!(clause.lits[1], Lit::pos(Var::new(1)));
595        assert_eq!(clause.lits[2], Lit::pos(Var::new(2)));
596    }
597
598    #[test]
599    fn test_clause_normalize_tautology() {
600        let mut clause = Clause::original([
601            Lit::pos(Var::new(0)),
602            Lit::neg(Var::new(0)), // tautology
603            Lit::pos(Var::new(1)),
604        ]);
605
606        let is_tautology = clause.normalize();
607        assert!(is_tautology);
608    }
609
610    #[test]
611    fn test_clause_subsumes() {
612        let mut c1 = Clause::original([Lit::pos(Var::new(0)), Lit::pos(Var::new(1))]);
613        let mut c2 = Clause::original([
614            Lit::pos(Var::new(0)),
615            Lit::pos(Var::new(1)),
616            Lit::pos(Var::new(2)),
617        ]);
618
619        c1.normalize();
620        c2.normalize();
621
622        assert!(c1.subsumes(&c2)); // c1 ⊆ c2
623        assert!(!c2.subsumes(&c1)); // c2 ⊈ c1
624    }
625
626    #[test]
627    fn test_clause_self_subsuming_resolvent() {
628        // C1: (a v b), C2: (~a v b v c)
629        // C1 can strengthen C2 to (b v c) by removing ~a
630        let mut c1 = Clause::original([Lit::pos(Var::new(0)), Lit::pos(Var::new(1))]);
631        let mut c2 = Clause::original([
632            Lit::neg(Var::new(0)),
633            Lit::pos(Var::new(1)),
634            Lit::pos(Var::new(2)),
635        ]);
636
637        c1.normalize();
638        c2.normalize();
639
640        if let Some(lit_to_remove) = c1.self_subsuming_resolvent(&c2) {
641            assert_eq!(lit_to_remove, Lit::neg(Var::new(0)));
642        } else {
643            panic!("Expected self-subsuming resolvent");
644        }
645    }
646}