Skip to main content

oxiz_sat/
clause_size_manager.rs

1//! Learned clause size management
2//!
3//! This module implements adaptive management of learned clause sizes.
4//! Modern SAT solvers limit the size of learned clauses to prevent learning
5//! very long clauses that provide little benefit while consuming memory.
6//!
7//! References:
8//! - "Clause Size Management in CDCL SAT Solvers"
9//! - "Adaptive Learned Clause Minimization in SAT Solvers"
10
11#[allow(unused_imports)]
12use crate::prelude::*;
13
14/// Statistics for clause size management
15#[derive(Debug, Clone, Default)]
16pub struct SizeManagerStats {
17    /// Number of clauses rejected due to size
18    pub size_rejected: usize,
19    /// Total clauses considered
20    pub total_considered: usize,
21    /// Average size of accepted clauses
22    pub avg_accepted_size: f64,
23    /// Average size of rejected clauses
24    pub avg_rejected_size: f64,
25    /// Current size limit
26    pub current_limit: usize,
27}
28
29impl SizeManagerStats {
30    /// Display statistics
31    pub fn display(&self) {
32        println!("Clause Size Manager Statistics:");
33        println!("  Total considered: {}", self.total_considered);
34        println!("  Size rejected: {}", self.size_rejected);
35        if self.total_considered > 0 {
36            let accept_rate = 100.0 * (self.total_considered - self.size_rejected) as f64
37                / self.total_considered as f64;
38            println!("  Acceptance rate: {:.1}%", accept_rate);
39        }
40        println!("  Avg accepted size: {:.1}", self.avg_accepted_size);
41        println!("  Avg rejected size: {:.1}", self.avg_rejected_size);
42        println!("  Current limit: {}", self.current_limit);
43    }
44}
45
46/// Strategy for adjusting clause size limit
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SizeAdjustmentStrategy {
49    /// Fixed size limit
50    Fixed,
51    /// Geometric increase (start small, increase over time)
52    Geometric,
53    /// Adaptive based on solver performance
54    Adaptive,
55    /// Luby-like sequence
56    Luby,
57}
58
59/// Clause size manager
60///
61/// Manages the maximum size of learned clauses to balance between
62/// clause quality and memory usage.
63#[derive(Debug)]
64pub struct ClauseSizeManager {
65    /// Current maximum size for learned clauses
66    current_limit: usize,
67    /// Minimum size limit
68    min_limit: usize,
69    /// Maximum size limit
70    max_limit: usize,
71    /// Adjustment strategy
72    strategy: SizeAdjustmentStrategy,
73    /// Geometric growth factor (for Geometric strategy)
74    growth_factor: f64,
75    /// Statistics
76    stats: SizeManagerStats,
77    /// Number of conflicts since last adjustment
78    conflicts_since_adjustment: u64,
79    /// Adjustment interval (in conflicts)
80    adjustment_interval: u64,
81    /// Running sum for average calculations
82    accepted_size_sum: u64,
83    /// Count of accepted clauses
84    accepted_count: usize,
85    /// Running sum for rejected clauses
86    rejected_size_sum: u64,
87}
88
89impl Default for ClauseSizeManager {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl ClauseSizeManager {
96    /// Create a new clause size manager with default settings
97    ///
98    /// Defaults:
99    /// - Initial limit: 30 literals
100    /// - Min limit: 10 literals
101    /// - Max limit: 100 literals
102    /// - Strategy: Geometric
103    #[must_use]
104    pub fn new() -> Self {
105        Self {
106            current_limit: 30,
107            min_limit: 10,
108            max_limit: 100,
109            strategy: SizeAdjustmentStrategy::Geometric,
110            growth_factor: 1.1,
111            stats: SizeManagerStats::default(),
112            conflicts_since_adjustment: 0,
113            adjustment_interval: 10000,
114            accepted_size_sum: 0,
115            accepted_count: 0,
116            rejected_size_sum: 0,
117        }
118    }
119
120    /// Create with custom limits
121    #[must_use]
122    pub fn with_limits(min: usize, initial: usize, max: usize) -> Self {
123        Self {
124            current_limit: initial,
125            min_limit: min,
126            max_limit: max,
127            strategy: SizeAdjustmentStrategy::Geometric,
128            growth_factor: 1.1,
129            stats: SizeManagerStats::default(),
130            conflicts_since_adjustment: 0,
131            adjustment_interval: 10000,
132            accepted_size_sum: 0,
133            accepted_count: 0,
134            rejected_size_sum: 0,
135        }
136    }
137
138    /// Set the adjustment strategy
139    pub fn set_strategy(&mut self, strategy: SizeAdjustmentStrategy) {
140        self.strategy = strategy;
141    }
142
143    /// Check if a clause of given size should be learned
144    ///
145    /// Returns true if the clause size is within the current limit
146    pub fn should_learn(&mut self, size: usize, lbd: u32) -> bool {
147        self.stats.total_considered += 1;
148
149        // Always learn very high quality clauses (LBD <= 2) regardless of size
150        if lbd <= 2 {
151            self.accepted_size_sum += size as u64;
152            self.accepted_count += 1;
153            return true;
154        }
155
156        // Check size limit
157        if size <= self.current_limit {
158            self.accepted_size_sum += size as u64;
159            self.accepted_count += 1;
160            true
161        } else {
162            self.stats.size_rejected += 1;
163            self.rejected_size_sum += size as u64;
164            false
165        }
166    }
167
168    /// Adjust size limit based on conflicts and strategy
169    pub fn adjust_limit(&mut self, conflicts: u64) {
170        self.conflicts_since_adjustment += 1;
171
172        if self.conflicts_since_adjustment < self.adjustment_interval {
173            return;
174        }
175
176        self.conflicts_since_adjustment = 0;
177
178        // Update stats
179        if self.accepted_count > 0 {
180            self.stats.avg_accepted_size =
181                self.accepted_size_sum as f64 / self.accepted_count as f64;
182        }
183        if self.stats.size_rejected > 0 {
184            self.stats.avg_rejected_size =
185                self.rejected_size_sum as f64 / self.stats.size_rejected as f64;
186        }
187        self.stats.current_limit = self.current_limit;
188
189        match self.strategy {
190            SizeAdjustmentStrategy::Fixed => {
191                // No adjustment
192            }
193            SizeAdjustmentStrategy::Geometric => {
194                // Geometric increase
195                let new_limit = (self.current_limit as f64 * self.growth_factor) as usize;
196                self.current_limit = new_limit.min(self.max_limit);
197            }
198            SizeAdjustmentStrategy::Adaptive => {
199                // Adjust based on rejection rate
200                let rejection_rate = if self.stats.total_considered > 0 {
201                    self.stats.size_rejected as f64 / self.stats.total_considered as f64
202                } else {
203                    0.0
204                };
205
206                if rejection_rate > 0.3 {
207                    // Too many rejections, increase limit
208                    self.current_limit = (self.current_limit + 5).min(self.max_limit);
209                } else if rejection_rate < 0.1 {
210                    // Few rejections, can decrease limit
211                    self.current_limit = (self.current_limit.saturating_sub(2)).max(self.min_limit);
212                }
213            }
214            SizeAdjustmentStrategy::Luby => {
215                // Use Luby sequence for limit
216                let luby_value = self.luby((conflicts / self.adjustment_interval) as u32);
217                self.current_limit = (self.min_limit + luby_value as usize).min(self.max_limit);
218            }
219        }
220    }
221
222    /// Compute Luby sequence value
223    /// Luby sequence: 1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, ...
224    #[allow(clippy::only_used_in_recursion)]
225    fn luby(&self, i: u32) -> u32 {
226        let mut power = 1u32;
227
228        // Find the largest power of 2 <= i+1
229        while power * 2 <= i + 1 {
230            power *= 2;
231        }
232
233        if power == i + 1 {
234            // i+1 is a power of 2, return previous power (or 1 if power=1)
235            if power == 1 { 1 } else { power / 2 }
236        } else {
237            // Recursively compute
238            self.luby(i + 1 - power)
239        }
240    }
241
242    /// Get current size limit
243    #[must_use]
244    pub fn current_limit(&self) -> usize {
245        self.current_limit
246    }
247
248    /// Get statistics
249    #[must_use]
250    pub fn stats(&self) -> &SizeManagerStats {
251        &self.stats
252    }
253
254    /// Reset statistics
255    pub fn reset_stats(&mut self) {
256        self.stats = SizeManagerStats::default();
257        self.stats.current_limit = self.current_limit;
258        self.accepted_size_sum = 0;
259        self.accepted_count = 0;
260        self.rejected_size_sum = 0;
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_size_manager_creation() {
270        let manager = ClauseSizeManager::new();
271        assert_eq!(manager.current_limit(), 30);
272    }
273
274    #[test]
275    fn test_should_learn_within_limit() {
276        let mut manager = ClauseSizeManager::new();
277        assert!(manager.should_learn(20, 5)); // Within limit
278        assert!(manager.should_learn(30, 5)); // At limit
279    }
280
281    #[test]
282    fn test_should_learn_exceeds_limit() {
283        let mut manager = ClauseSizeManager::new();
284        assert!(!manager.should_learn(50, 5)); // Exceeds limit
285        assert_eq!(manager.stats().size_rejected, 1);
286    }
287
288    #[test]
289    fn test_should_learn_high_quality() {
290        let mut manager = ClauseSizeManager::new();
291        // High quality clauses (LBD <= 2) always learned
292        assert!(manager.should_learn(100, 2));
293        assert_eq!(manager.stats().size_rejected, 0);
294    }
295
296    #[test]
297    fn test_geometric_growth() {
298        let mut manager = ClauseSizeManager::new();
299        manager.set_strategy(SizeAdjustmentStrategy::Geometric);
300
301        let initial = manager.current_limit();
302        // Manually trigger adjustment by setting conflicts
303        manager.conflicts_since_adjustment = manager.adjustment_interval;
304        manager.adjust_limit(10000);
305
306        assert!(manager.current_limit() > initial);
307    }
308
309    #[test]
310    fn test_fixed_strategy() {
311        let mut manager = ClauseSizeManager::new();
312        manager.set_strategy(SizeAdjustmentStrategy::Fixed);
313
314        let initial = manager.current_limit();
315        manager.conflicts_since_adjustment = manager.adjustment_interval;
316        manager.adjust_limit(10000);
317
318        assert_eq!(manager.current_limit(), initial);
319    }
320
321    #[test]
322    fn test_adaptive_increase() {
323        let mut manager = ClauseSizeManager::new();
324        manager.set_strategy(SizeAdjustmentStrategy::Adaptive);
325
326        // Simulate high rejection rate
327        for _ in 0..100 {
328            manager.should_learn(50, 5); // Will be rejected
329        }
330
331        let initial = manager.current_limit();
332        manager.conflicts_since_adjustment = manager.adjustment_interval;
333        manager.adjust_limit(10000);
334
335        assert!(manager.current_limit() > initial);
336    }
337
338    #[test]
339    fn test_adaptive_decrease() {
340        let mut manager = ClauseSizeManager::new();
341        manager.set_strategy(SizeAdjustmentStrategy::Adaptive);
342
343        // Simulate low rejection rate
344        for _ in 0..100 {
345            manager.should_learn(10, 5); // Will be accepted
346        }
347
348        let initial = manager.current_limit();
349        manager.conflicts_since_adjustment = manager.adjustment_interval;
350        manager.adjust_limit(10000);
351
352        assert!(manager.current_limit() < initial);
353    }
354
355    #[test]
356    fn test_luby_strategy_available() {
357        let mut manager = ClauseSizeManager::new();
358        manager.set_strategy(SizeAdjustmentStrategy::Luby);
359        manager.conflicts_since_adjustment = manager.adjustment_interval;
360        manager.adjust_limit(10000);
361        // Just verify it doesn't crash
362        assert!(manager.current_limit() > 0);
363    }
364
365    #[test]
366    fn test_stats_tracking() {
367        let mut manager = ClauseSizeManager::new();
368
369        manager.should_learn(20, 5);
370        manager.should_learn(50, 5);
371        manager.should_learn(15, 5);
372
373        let stats = manager.stats();
374        assert_eq!(stats.total_considered, 3);
375        assert_eq!(stats.size_rejected, 1); // The 50-literal clause
376    }
377}