Skip to main content

oxiz_sat/
assumptions.rs

1//! Enhanced assumption-based solving interface
2//!
3//! This module provides advanced assumption management for incremental solving,
4//! including assumption levels, tracking, and efficient core extraction.
5
6use crate::literal::Lit;
7#[allow(unused_imports)]
8use crate::prelude::*;
9
10#[cfg(test)]
11use crate::literal::Var;
12
13/// Assumption level for hierarchical assumption management
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct AssumptionLevel(pub u32);
16
17impl AssumptionLevel {
18    /// Create a new assumption level
19    pub const fn new(level: u32) -> Self {
20        Self(level)
21    }
22
23    /// Get the root level (0)
24    pub const fn root() -> Self {
25        Self(0)
26    }
27}
28
29/// Assumption with metadata
30#[derive(Debug, Clone)]
31pub struct Assumption {
32    /// The assumed literal
33    pub lit: Lit,
34    /// Level at which this assumption was made
35    pub level: AssumptionLevel,
36    /// Optional user data
37    pub user_data: Option<u64>,
38}
39
40impl Assumption {
41    /// Create a new assumption
42    pub fn new(lit: Lit, level: AssumptionLevel) -> Self {
43        Self {
44            lit,
45            level,
46            user_data: None,
47        }
48    }
49
50    /// Create an assumption with user data
51    pub fn with_data(lit: Lit, level: AssumptionLevel, data: u64) -> Self {
52        Self {
53            lit,
54            level,
55            user_data: Some(data),
56        }
57    }
58}
59
60/// Assumption stack with level management
61pub struct AssumptionStack {
62    /// Stack of assumptions
63    assumptions: Vec<Assumption>,
64    /// Current assumption level
65    current_level: AssumptionLevel,
66    /// Map from literal to assumption index
67    lit_to_index: HashMap<Lit, usize>,
68    /// Conflicting assumptions in the last UNSAT result
69    conflict_set: Vec<Lit>,
70}
71
72impl AssumptionStack {
73    /// Create a new assumption stack
74    pub fn new() -> Self {
75        Self {
76            assumptions: Vec::new(),
77            current_level: AssumptionLevel::root(),
78            lit_to_index: HashMap::new(),
79            conflict_set: Vec::new(),
80        }
81    }
82
83    /// Push an assumption at the current level
84    pub fn push(&mut self, lit: Lit) -> Result<(), String> {
85        // Check for conflicting assumptions
86        if self.lit_to_index.contains_key(&lit.negate()) {
87            return Err(format!(
88                "Conflicting assumption: {:?} conflicts with existing assumption",
89                lit
90            ));
91        }
92
93        let assumption = Assumption::new(lit, self.current_level);
94        self.lit_to_index.insert(lit, self.assumptions.len());
95        self.assumptions.push(assumption);
96        Ok(())
97    }
98
99    /// Push an assumption with user data
100    pub fn push_with_data(&mut self, lit: Lit, data: u64) -> Result<(), String> {
101        if self.lit_to_index.contains_key(&lit.negate()) {
102            return Err(format!(
103                "Conflicting assumption: {:?} conflicts with existing assumption",
104                lit
105            ));
106        }
107
108        let assumption = Assumption::with_data(lit, self.current_level, data);
109        self.lit_to_index.insert(lit, self.assumptions.len());
110        self.assumptions.push(assumption);
111        Ok(())
112    }
113
114    /// Create a new assumption level
115    pub fn new_level(&mut self) {
116        self.current_level = AssumptionLevel::new(self.current_level.0 + 1);
117    }
118
119    /// Pop all assumptions at the current level and above
120    pub fn pop_level(&mut self) {
121        if self.current_level.0 == 0 {
122            return;
123        }
124
125        // Remove assumptions from current level
126        self.assumptions.retain(|a| {
127            let keep = a.level < self.current_level;
128            if !keep {
129                self.lit_to_index.remove(&a.lit);
130            }
131            keep
132        });
133
134        // Decrement level
135        self.current_level = AssumptionLevel::new(self.current_level.0 - 1);
136    }
137
138    /// Clear all assumptions
139    pub fn clear(&mut self) {
140        self.assumptions.clear();
141        self.lit_to_index.clear();
142        self.current_level = AssumptionLevel::root();
143        self.conflict_set.clear();
144    }
145
146    /// Get all assumptions as a slice
147    pub fn get_all(&self) -> &[Assumption] {
148        &self.assumptions
149    }
150
151    /// Get assumptions as literals
152    pub fn get_literals(&self) -> Vec<Lit> {
153        self.assumptions.iter().map(|a| a.lit).collect()
154    }
155
156    /// Get assumptions at a specific level
157    pub fn get_at_level(&self, level: AssumptionLevel) -> Vec<&Assumption> {
158        self.assumptions
159            .iter()
160            .filter(|a| a.level == level)
161            .collect()
162    }
163
164    /// Check if a literal is assumed
165    pub fn is_assumed(&self, lit: Lit) -> bool {
166        self.lit_to_index.contains_key(&lit)
167    }
168
169    /// Get the level of an assumption
170    pub fn get_level(&self, lit: Lit) -> Option<AssumptionLevel> {
171        self.lit_to_index
172            .get(&lit)
173            .and_then(|&idx| self.assumptions.get(idx))
174            .map(|a| a.level)
175    }
176
177    /// Set the conflict set from UNSAT core
178    pub fn set_conflict(&mut self, core: Vec<Lit>) {
179        self.conflict_set = core;
180    }
181
182    /// Get the conflict set
183    pub fn get_conflict(&self) -> &[Lit] {
184        &self.conflict_set
185    }
186
187    /// Get the current level
188    pub fn current_level(&self) -> AssumptionLevel {
189        self.current_level
190    }
191
192    /// Get number of assumptions
193    pub fn len(&self) -> usize {
194        self.assumptions.len()
195    }
196
197    /// Check if empty
198    pub fn is_empty(&self) -> bool {
199        self.assumptions.is_empty()
200    }
201}
202
203impl Default for AssumptionStack {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// Assumption core minimizer
210pub struct AssumptionCoreMinimizer {
211    /// Assumptions that must be in the core
212    fixed_assumptions: HashSet<Lit>,
213}
214
215impl AssumptionCoreMinimizer {
216    /// Create a new core minimizer
217    pub fn new() -> Self {
218        Self {
219            fixed_assumptions: HashSet::new(),
220        }
221    }
222
223    /// Add a fixed assumption that must be in the core
224    pub fn add_fixed(&mut self, lit: Lit) {
225        self.fixed_assumptions.insert(lit);
226    }
227
228    /// Minimize a core using deletion-based minimization
229    pub fn minimize_deletion(&self, core: &[Lit]) -> Vec<Lit> {
230        // Simple deletion: try removing each assumption and see if still UNSAT
231        // This is a placeholder - actual implementation would need solver access
232        let mut minimal = core.to_vec();
233        minimal.retain(|&lit| self.fixed_assumptions.contains(&lit));
234        minimal
235    }
236
237    /// Minimize a core using QuickXplain algorithm
238    pub fn minimize_quickxplain(&self, core: &[Lit]) -> Vec<Lit> {
239        // QuickXplain is a divide-and-conquer algorithm for core minimization
240        // This is a placeholder implementation
241        core.to_vec()
242    }
243
244    /// Check if a literal is fixed in the core
245    pub fn is_fixed(&self, lit: Lit) -> bool {
246        self.fixed_assumptions.contains(&lit)
247    }
248}
249
250impl Default for AssumptionCoreMinimizer {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256/// Assumption-based solving context
257pub struct AssumptionContext {
258    /// Assumption stack
259    stack: AssumptionStack,
260    /// Core minimizer
261    minimizer: AssumptionCoreMinimizer,
262    /// Track assumption usage statistics
263    stats: AssumptionStats,
264}
265
266impl AssumptionContext {
267    /// Create a new assumption context
268    pub fn new() -> Self {
269        Self {
270            stack: AssumptionStack::new(),
271            minimizer: AssumptionCoreMinimizer::new(),
272            stats: AssumptionStats::default(),
273        }
274    }
275
276    /// Get the assumption stack
277    pub fn stack(&self) -> &AssumptionStack {
278        &self.stack
279    }
280
281    /// Get mutable assumption stack
282    pub fn stack_mut(&mut self) -> &mut AssumptionStack {
283        &mut self.stack
284    }
285
286    /// Get the core minimizer
287    pub fn minimizer(&self) -> &AssumptionCoreMinimizer {
288        &self.minimizer
289    }
290
291    /// Get mutable core minimizer
292    pub fn minimizer_mut(&mut self) -> &mut AssumptionCoreMinimizer {
293        &mut self.minimizer
294    }
295
296    /// Get statistics
297    pub fn stats(&self) -> &AssumptionStats {
298        &self.stats
299    }
300
301    /// Record a solve with assumptions
302    pub fn record_solve(&mut self, num_assumptions: usize, is_sat: bool) {
303        self.stats.total_calls += 1;
304        self.stats.total_assumptions += num_assumptions;
305        if !is_sat {
306            self.stats.unsat_calls += 1;
307        }
308    }
309
310    /// Record a core extraction
311    pub fn record_core(&mut self, core_size: usize, original_size: usize) {
312        self.stats.core_extractions += 1;
313        self.stats.total_core_size += core_size;
314        self.stats.total_original_size += original_size;
315    }
316}
317
318impl Default for AssumptionContext {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324/// Statistics for assumption-based solving
325#[derive(Debug, Clone, Default)]
326pub struct AssumptionStats {
327    /// Total number of solve calls with assumptions
328    pub total_calls: u64,
329    /// Number of UNSAT results
330    pub unsat_calls: u64,
331    /// Total assumptions across all calls
332    pub total_assumptions: usize,
333    /// Number of core extractions
334    pub core_extractions: u64,
335    /// Total core sizes
336    pub total_core_size: usize,
337    /// Total original assumption set sizes
338    pub total_original_size: usize,
339}
340
341impl AssumptionStats {
342    /// Get average number of assumptions per call
343    pub fn avg_assumptions(&self) -> f64 {
344        if self.total_calls == 0 {
345            return 0.0;
346        }
347        self.total_assumptions as f64 / self.total_calls as f64
348    }
349
350    /// Get average core size
351    pub fn avg_core_size(&self) -> f64 {
352        if self.core_extractions == 0 {
353            return 0.0;
354        }
355        self.total_core_size as f64 / self.core_extractions as f64
356    }
357
358    /// Get average minimization ratio
359    pub fn avg_minimization_ratio(&self) -> f64 {
360        if self.total_original_size == 0 {
361            return 1.0;
362        }
363        self.total_core_size as f64 / self.total_original_size as f64
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_assumption_stack_basic() {
373        let mut stack = AssumptionStack::new();
374
375        let lit = Lit::pos(Var(0));
376        assert!(stack.push(lit).is_ok());
377        assert_eq!(stack.len(), 1);
378        assert!(stack.is_assumed(lit));
379    }
380
381    #[test]
382    fn test_assumption_stack_conflict() {
383        let mut stack = AssumptionStack::new();
384
385        let lit = Lit::pos(Var(0));
386        stack.push(lit).expect("First assumption push must succeed");
387
388        let neg_lit = Lit::neg(Var(0));
389        assert!(stack.push(neg_lit).is_err());
390    }
391
392    #[test]
393    fn test_assumption_levels() {
394        let mut stack = AssumptionStack::new();
395
396        let lit1 = Lit::pos(Var(0));
397        stack
398            .push(lit1)
399            .expect("First assumption push must succeed");
400
401        stack.new_level();
402        let lit2 = Lit::pos(Var(1));
403        stack
404            .push(lit2)
405            .expect("Second assumption push must succeed");
406
407        assert_eq!(stack.len(), 2);
408
409        stack.pop_level();
410        assert_eq!(stack.len(), 1);
411        assert!(stack.is_assumed(lit1));
412        assert!(!stack.is_assumed(lit2));
413    }
414
415    #[test]
416    fn test_assumption_with_data() {
417        let mut stack = AssumptionStack::new();
418
419        let lit = Lit::pos(Var(0));
420        stack
421            .push_with_data(lit, 42)
422            .expect("Assumption push with data must succeed");
423
424        assert_eq!(stack.assumptions[0].user_data, Some(42));
425    }
426
427    #[test]
428    fn test_assumption_context() {
429        let mut ctx = AssumptionContext::new();
430
431        ctx.record_solve(5, false);
432        ctx.record_core(3, 5);
433
434        assert_eq!(ctx.stats().total_calls, 1);
435        assert_eq!(ctx.stats().unsat_calls, 1);
436        assert_eq!(ctx.stats().avg_core_size(), 3.0);
437    }
438}