Skip to main content

oxiz_sat/
cube.rs

1use crate::literal::Lit;
2/// Cube-and-Conquer: Advanced parallel SAT solving via search space partitioning.
3///
4/// This module implements the Cube-and-Conquer technique, which partitions the search
5/// space into "cubes" (partial assignments) that can be solved independently in parallel.
6///
7/// The algorithm works in two phases:
8/// 1. **Cube Phase**: Use lookahead to generate a set of cubes that partition the space
9/// 2. **Conquer Phase**: Solve each cube in parallel using CDCL
10///
11/// This approach is particularly effective for hard combinatorial problems.
12#[allow(unused_imports)]
13use crate::prelude::*;
14
15/// A cube represents a partial assignment (conjunction of literals).
16///
17/// Cubes partition the search space - if all cubes are UNSAT, the formula is UNSAT.
18/// If any cube is SAT, the formula is SAT.
19#[derive(Debug, Clone)]
20pub struct Cube {
21    /// The literals in this cube (partial assignment)
22    pub literals: Vec<Lit>,
23    /// Estimated difficulty score (higher = harder)
24    pub difficulty: f64,
25    /// Depth at which this cube was created
26    pub depth: usize,
27}
28
29impl Cube {
30    /// Creates a new cube from literals.
31    pub fn new(literals: Vec<Lit>) -> Self {
32        Self {
33            depth: literals.len(),
34            literals,
35            difficulty: 0.0,
36        }
37    }
38
39    /// Creates a cube with estimated difficulty.
40    pub fn with_difficulty(literals: Vec<Lit>, difficulty: f64) -> Self {
41        Self {
42            depth: literals.len(),
43            literals,
44            difficulty,
45        }
46    }
47
48    /// Returns the number of literals in the cube.
49    pub fn len(&self) -> usize {
50        self.literals.len()
51    }
52
53    /// Checks if the cube is empty.
54    pub fn is_empty(&self) -> bool {
55        self.literals.is_empty()
56    }
57
58    /// Extends the cube with an additional literal.
59    pub fn extend(&self, lit: Lit) -> Self {
60        let mut new_lits = self.literals.clone();
61        new_lits.push(lit);
62        Self {
63            literals: new_lits,
64            difficulty: 0.0,
65            depth: self.depth + 1,
66        }
67    }
68
69    /// Checks if the cube contains conflicting literals.
70    pub fn is_consistent(&self) -> bool {
71        let mut seen = HashSet::new();
72        for &lit in &self.literals {
73            if seen.contains(&lit.negate()) {
74                return false;
75            }
76            seen.insert(lit);
77        }
78        true
79    }
80}
81
82/// Strategy for cube generation.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum CubeSplittingStrategy {
85    /// Split based on variable activity (VSIDS-style)
86    Activity,
87    /// Split based on lookahead scores
88    Lookahead,
89    /// Split using the most constrained variable (smallest domain)
90    MostConstrained,
91    /// Balanced splitting to create similar-sized cubes
92    Balanced,
93}
94
95/// Configuration for cube generation.
96#[derive(Debug, Clone)]
97pub struct CubeConfig {
98    /// Maximum depth for cube splitting
99    pub max_depth: usize,
100    /// Target number of cubes to generate
101    pub target_cubes: usize,
102    /// Minimum literals per cube
103    pub min_cube_size: usize,
104    /// Maximum literals per cube
105    pub max_cube_size: usize,
106    /// Splitting strategy
107    pub strategy: CubeSplittingStrategy,
108    /// Enable adaptive depth adjustment
109    pub adaptive_depth: bool,
110}
111
112impl Default for CubeConfig {
113    fn default() -> Self {
114        Self {
115            max_depth: 10,
116            target_cubes: 100,
117            min_cube_size: 3,
118            max_cube_size: 15,
119            strategy: CubeSplittingStrategy::Lookahead,
120            adaptive_depth: true,
121        }
122    }
123}
124
125/// Cube generator using recursive splitting.
126pub struct CubeGenerator {
127    /// Configuration
128    config: CubeConfig,
129    /// Generated cubes
130    cubes: Vec<Cube>,
131    /// Number of variables
132    num_vars: usize,
133}
134
135impl CubeGenerator {
136    /// Creates a new cube generator.
137    pub fn new(num_vars: usize, config: CubeConfig) -> Self {
138        Self {
139            config,
140            cubes: Vec::new(),
141            num_vars,
142        }
143    }
144
145    /// Generates cubes by recursive splitting.
146    ///
147    /// Starting from an empty cube, recursively splits by choosing a variable
148    /// and creating two cubes: one with the positive literal, one with negative.
149    pub fn generate(&mut self, variable_scores: &[f64]) -> Vec<Cube> {
150        self.cubes.clear();
151
152        // Start with empty cube
153        let initial = Cube::new(Vec::new());
154        self.split_recursive(initial, variable_scores);
155
156        // If adaptive depth is enabled and we have too few cubes, try again with more depth
157        if self.config.adaptive_depth && self.cubes.len() < self.config.target_cubes / 2 {
158            self.config.max_depth = (self.config.max_depth * 3) / 2;
159            self.cubes.clear();
160            let initial = Cube::new(Vec::new());
161            self.split_recursive(initial, variable_scores);
162        }
163
164        core::mem::take(&mut self.cubes)
165    }
166
167    /// Recursively splits a cube into smaller cubes.
168    fn split_recursive(&mut self, cube: Cube, variable_scores: &[f64]) {
169        // Stop if we've reached max depth or target number of cubes
170        if cube.depth >= self.config.max_depth || self.cubes.len() >= self.config.target_cubes {
171            if cube.len() >= self.config.min_cube_size && cube.is_consistent() {
172                self.cubes.push(cube);
173            }
174            return;
175        }
176
177        // Select next variable to split on
178        if let Some(var) = self.select_splitting_variable(&cube, variable_scores) {
179            use crate::literal::Var;
180            let v = Var::new(var as u32);
181
182            // Create two cubes: one with positive literal, one with negative
183            let pos_cube = cube.extend(Lit::pos(v));
184            let neg_cube = cube.extend(Lit::neg(v));
185
186            // Recursively split both branches
187            self.split_recursive(pos_cube, variable_scores);
188            self.split_recursive(neg_cube, variable_scores);
189        } else {
190            // No more variables to split on, save this cube
191            if cube.len() >= self.config.min_cube_size && cube.is_consistent() {
192                self.cubes.push(cube);
193            }
194        }
195    }
196
197    /// Selects the best variable to split on based on the strategy.
198    fn select_splitting_variable(&self, cube: &Cube, variable_scores: &[f64]) -> Option<usize> {
199        // Get variables already assigned in cube
200        let mut assigned = HashSet::new();
201        for lit in &cube.literals {
202            assigned.insert(lit.var().index());
203        }
204
205        // Find best unassigned variable
206        let mut best_var = None;
207        let mut best_score = f64::NEG_INFINITY;
208
209        for var in 0..self.num_vars {
210            if assigned.contains(&var) {
211                continue;
212            }
213
214            let score = if var < variable_scores.len() {
215                variable_scores[var]
216            } else {
217                0.0
218            };
219
220            if score > best_score {
221                best_score = score;
222                best_var = Some(var);
223            }
224        }
225
226        best_var
227    }
228
229    /// Estimates the difficulty of a cube based on its size and depth.
230    #[allow(dead_code)]
231    fn estimate_difficulty(&self, cube: &Cube) -> f64 {
232        // Smaller cubes are generally harder (less constrained)
233        let size_factor = 1.0 / (cube.len() as f64 + 1.0);
234        // Deeper cubes represent more specific search spaces
235        let depth_factor = cube.depth as f64;
236
237        size_factor * depth_factor
238    }
239
240    /// Returns the current configuration.
241    pub fn config(&self) -> &CubeConfig {
242        &self.config
243    }
244
245    /// Returns the number of cubes generated.
246    pub fn num_cubes(&self) -> usize {
247        self.cubes.len()
248    }
249}
250
251/// Statistics for cube generation.
252#[derive(Debug, Clone, Default)]
253pub struct CubeStats {
254    /// Total cubes generated
255    pub total_cubes: usize,
256    /// Average cube size
257    pub avg_cube_size: f64,
258    /// Minimum cube size
259    pub min_cube_size: usize,
260    /// Maximum cube size
261    pub max_cube_size: usize,
262    /// Maximum depth reached
263    pub max_depth: usize,
264    /// Average difficulty
265    pub avg_difficulty: f64,
266}
267
268impl CubeStats {
269    /// Creates statistics from a set of cubes.
270    pub fn from_cubes(cubes: &[Cube]) -> Self {
271        if cubes.is_empty() {
272            return Self::default();
273        }
274
275        let total = cubes.len();
276        let sizes: Vec<usize> = cubes.iter().map(|c| c.len()).collect();
277        let avg_size = sizes.iter().sum::<usize>() as f64 / total as f64;
278        let min_size = sizes.iter().copied().min().unwrap_or(0);
279        let max_size = sizes.iter().copied().max().unwrap_or(0);
280        let max_depth = cubes.iter().map(|c| c.depth).max().unwrap_or(0);
281        let avg_diff = cubes.iter().map(|c| c.difficulty).sum::<f64>() / total as f64;
282
283        Self {
284            total_cubes: total,
285            avg_cube_size: avg_size,
286            min_cube_size: min_size,
287            max_cube_size: max_size,
288            max_depth,
289            avg_difficulty: avg_diff,
290        }
291    }
292
293    /// Displays the statistics.
294    pub fn display(&self) -> String {
295        format!(
296            "Cube Generation Statistics:\n\
297             - Total Cubes: {}\n\
298             - Avg Size: {:.2}\n\
299             - Size Range: [{}, {}]\n\
300             - Max Depth: {}\n\
301             - Avg Difficulty: {:.4}",
302            self.total_cubes,
303            self.avg_cube_size,
304            self.min_cube_size,
305            self.max_cube_size,
306            self.max_depth,
307            self.avg_difficulty
308        )
309    }
310}
311
312/// Result of solving cubes.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum CubeResult {
315    /// All cubes are UNSAT - formula is UNSAT
316    Unsat,
317    /// At least one cube is SAT - formula is SAT
318    Sat,
319    /// Unknown (timeout or resource limit)
320    Unknown,
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::literal::Var;
327
328    #[test]
329    fn test_cube_creation() {
330        let lit1 = Lit::pos(Var::new(0));
331        let lit2 = Lit::neg(Var::new(1));
332        let cube = Cube::new(vec![lit1, lit2]);
333
334        assert_eq!(cube.len(), 2);
335        assert!(!cube.is_empty());
336        assert_eq!(cube.depth, 2);
337    }
338
339    #[test]
340    fn test_cube_extend() {
341        let lit1 = Lit::pos(Var::new(0));
342        let cube1 = Cube::new(vec![lit1]);
343
344        let lit2 = Lit::neg(Var::new(1));
345        let cube2 = cube1.extend(lit2);
346
347        assert_eq!(cube1.len(), 1);
348        assert_eq!(cube2.len(), 2);
349        assert_eq!(cube2.depth, 2);
350    }
351
352    #[test]
353    fn test_cube_consistency() {
354        let lit1 = Lit::pos(Var::new(0));
355        let lit2 = Lit::neg(Var::new(1));
356        let cube = Cube::new(vec![lit1, lit2]);
357
358        assert!(cube.is_consistent());
359    }
360
361    #[test]
362    fn test_cube_inconsistency() {
363        let lit1 = Lit::pos(Var::new(0));
364        let lit2 = Lit::neg(Var::new(0));
365        let cube = Cube::new(vec![lit1, lit2]);
366
367        assert!(!cube.is_consistent());
368    }
369
370    #[test]
371    fn test_empty_cube() {
372        let cube = Cube::new(Vec::new());
373
374        assert_eq!(cube.len(), 0);
375        assert!(cube.is_empty());
376        assert!(cube.is_consistent());
377    }
378
379    #[test]
380    fn test_cube_config_default() {
381        let config = CubeConfig::default();
382
383        assert_eq!(config.max_depth, 10);
384        assert_eq!(config.target_cubes, 100);
385        assert!(config.adaptive_depth);
386    }
387
388    #[test]
389    fn test_cube_generator_creation() {
390        let config = CubeConfig::default();
391        let generator = CubeGenerator::new(10, config);
392
393        assert_eq!(generator.num_vars, 10);
394        assert_eq!(generator.num_cubes(), 0);
395    }
396
397    #[test]
398    fn test_cube_generation() {
399        let config = CubeConfig {
400            max_depth: 3,
401            target_cubes: 10,
402            min_cube_size: 1,
403            max_cube_size: 5,
404            strategy: CubeSplittingStrategy::Activity,
405            adaptive_depth: false,
406        };
407        let mut generator = CubeGenerator::new(5, config);
408        let scores = vec![1.0, 0.9, 0.8, 0.7, 0.6];
409
410        let cubes = generator.generate(&scores);
411
412        assert!(!cubes.is_empty());
413        assert!(cubes.len() <= 10);
414
415        // All cubes should be consistent
416        for cube in &cubes {
417            assert!(cube.is_consistent());
418        }
419    }
420
421    #[test]
422    fn test_cube_stats() {
423        let lit1 = Lit::pos(Var::new(0));
424        let lit2 = Lit::neg(Var::new(1));
425        let lit3 = Lit::pos(Var::new(2));
426
427        let cubes = vec![
428            Cube::new(vec![lit1]),
429            Cube::new(vec![lit1, lit2]),
430            Cube::new(vec![lit1, lit2, lit3]),
431        ];
432
433        let stats = CubeStats::from_cubes(&cubes);
434
435        assert_eq!(stats.total_cubes, 3);
436        assert_eq!(stats.min_cube_size, 1);
437        assert_eq!(stats.max_cube_size, 3);
438        assert_eq!(stats.avg_cube_size, 2.0);
439    }
440
441    #[test]
442    fn test_empty_cube_stats() {
443        let stats = CubeStats::from_cubes(&[]);
444
445        assert_eq!(stats.total_cubes, 0);
446        assert_eq!(stats.avg_cube_size, 0.0);
447    }
448
449    #[test]
450    fn test_cube_splitting_strategies() {
451        let strategies = vec![
452            CubeSplittingStrategy::Activity,
453            CubeSplittingStrategy::Lookahead,
454            CubeSplittingStrategy::MostConstrained,
455            CubeSplittingStrategy::Balanced,
456        ];
457
458        for strategy in strategies {
459            let config = CubeConfig {
460                strategy,
461                max_depth: 2,
462                target_cubes: 4,
463                min_cube_size: 1,
464                max_cube_size: 5,
465                adaptive_depth: false,
466            };
467
468            let mut generator = CubeGenerator::new(3, config);
469            let scores = vec![1.0, 0.5, 0.2];
470            let cubes = generator.generate(&scores);
471
472            assert!(!cubes.is_empty());
473        }
474    }
475
476    #[test]
477    fn test_adaptive_depth() {
478        let config = CubeConfig {
479            max_depth: 2,
480            target_cubes: 100,
481            min_cube_size: 1,
482            max_cube_size: 10,
483            strategy: CubeSplittingStrategy::Activity,
484            adaptive_depth: true,
485        };
486
487        let mut generator = CubeGenerator::new(3, config);
488        let scores = vec![1.0, 0.5, 0.2];
489        let _cubes = generator.generate(&scores);
490
491        // Adaptive depth should increase max_depth if needed
492        assert!(generator.config().max_depth >= 2);
493    }
494}