oxiz-solver 0.2.0

Main CDCL(T) Solver API for OxiZ
Documentation
//! Conflict Clause Minimization.
//!
//! This module implements algorithms for minimizing conflict clauses learned
//! during CDCL solving. Smaller conflict clauses lead to:
//! - More effective backtracking
//! - Better propagation
//! - Reduced memory usage
//!
//! ## Techniques
//!
//! 1. **Self-Subsuming Resolution**: Remove literals that can be derived via resolution
//! 2. **Recursive Minimization**: Recursively check if literals are implied
//! 3. **Binary Resolution**: Use binary clauses for efficient minimization
//! 4. **Stamping**: Cache visited literals to avoid redundant work
//!
//! ## References
//!
//! - Sörensson & Biere: "Minimizing Learned Clauses" (SAT 2009)
//! - Beame et al.: "Understanding and Using Short Implicants" (SAT 2017)
//! - Z3's conflict minimization in `smt/smt_conflict_resolution.cpp`

#[allow(unused_imports)]
use crate::prelude::*;
use oxiz_sat::Lit;

/// Configuration for conflict minimization.
#[derive(Debug, Clone)]
pub struct MinimizerConfig {
    /// Enable recursive minimization.
    pub recursive: bool,
    /// Enable binary resolution minimization.
    pub binary_resolution: bool,
    /// Maximum recursion depth.
    pub max_depth: u32,
    /// Use stamping to cache visited literals.
    pub use_stamping: bool,
}

impl Default for MinimizerConfig {
    fn default() -> Self {
        Self {
            recursive: true,
            binary_resolution: true,
            max_depth: 100,
            use_stamping: true,
        }
    }
}

/// Statistics for conflict minimization.
#[derive(Debug, Clone, Default)]
pub struct MinimizerStats {
    /// Number of conflicts minimized.
    pub conflicts_minimized: u64,
    /// Total literals removed.
    pub literals_removed: u64,
    /// Average reduction ratio.
    pub avg_reduction: f64,
    /// Time spent minimizing (microseconds).
    pub time_us: u64,
}

/// Conflict clause minimizer.
pub struct ConflictMinimizer {
    config: MinimizerConfig,
    stats: MinimizerStats,
    /// Stamp for visited literals (for caching).
    stamp: Vec<u32>,
    /// Current stamp value.
    current_stamp: u32,
}

impl ConflictMinimizer {
    /// Create a new conflict minimizer.
    pub fn new() -> Self {
        Self::with_config(MinimizerConfig::default())
    }

    /// Create with configuration.
    pub fn with_config(config: MinimizerConfig) -> Self {
        Self {
            config,
            stats: MinimizerStats::default(),
            stamp: Vec::new(),
            current_stamp: 0,
        }
    }

    /// Get statistics.
    pub fn stats(&self) -> &MinimizerStats {
        &self.stats
    }

    /// Reset statistics.
    pub fn reset_stats(&mut self) {
        self.stats = MinimizerStats::default();
    }

    /// Minimize a conflict clause.
    ///
    /// Returns the minimized clause (subset of input).
    pub fn minimize(&mut self, conflict: &[Lit]) -> Vec<Lit> {
        #[cfg(feature = "std")]
        let start = std::time::Instant::now();
        let original_size = conflict.len();

        // Ensure stamp vector is large enough
        let max_var = conflict
            .iter()
            .map(|lit| lit.var().index())
            .max()
            .unwrap_or(0);

        if max_var >= self.stamp.len() {
            self.stamp.resize(max_var + 1, 0);
        }

        // Increment stamp for this minimization session
        self.current_stamp += 1;

        let mut minimized = Vec::with_capacity(conflict.len());
        let conflict_set: FxHashSet<Lit> = conflict.iter().copied().collect();

        for &lit in conflict {
            if self.can_be_removed(lit, &conflict_set) {
                // Literal can be removed
                continue;
            }
            minimized.push(lit);
        }

        // Update statistics
        let removed = original_size - minimized.len();
        self.stats.conflicts_minimized += 1;
        self.stats.literals_removed += removed as u64;

        if self.stats.conflicts_minimized > 0 {
            let total_removed = self.stats.literals_removed as f64;
            let total_processed = self.stats.conflicts_minimized as f64 * original_size as f64;
            self.stats.avg_reduction = total_removed / total_processed;
        }

        #[cfg(feature = "std")]
        {
            self.stats.time_us += start.elapsed().as_micros() as u64;
        }

        minimized
    }

    /// Check if a literal can be removed from the conflict.
    fn can_be_removed(&mut self, lit: Lit, _conflict: &FxHashSet<Lit>) -> bool {
        if !self.config.recursive {
            return false;
        }

        // Use stamping to avoid redundant checks
        if self.config.use_stamping {
            let var_idx = lit.var().index();
            if var_idx < self.stamp.len() && self.stamp[var_idx] == self.current_stamp {
                return false; // Already checked
            }
            if var_idx < self.stamp.len() {
                self.stamp[var_idx] = self.current_stamp;
            }
        }

        // Check if literal is implied by other literals in conflict
        // This is a simplified check - full implementation would use
        // implication graph and reason clauses

        // For now, just check if there's a unit clause implying ~lit
        // In a full implementation, this would traverse the implication graph

        false // Placeholder - real implementation would check implications
    }

    /// Minimize using binary resolution.
    ///
    /// If conflict contains literals l1, l2 where there exists a binary clause (~l1 ∨ l2),
    /// then l1 can be removed (it's subsumed by l2).
    pub fn minimize_binary(&mut self, conflict: &[Lit]) -> Vec<Lit> {
        if !self.config.binary_resolution {
            return conflict.to_vec();
        }

        // Simplified implementation - would need access to clause database
        // to find binary clauses for actual resolution
        conflict.to_vec()
    }

    /// Perform self-subsuming resolution on conflict clause.
    ///
    /// Remove literals that make the clause self-subsuming.
    pub fn self_subsume(&mut self, conflict: &[Lit]) -> Vec<Lit> {
        // Self-subsuming resolution: if conflict has (~p ∨ C) and there's a
        // clause (p ∨ D) where D ⊆ C, then p can be removed from conflict

        // Simplified placeholder - full implementation needs clause database access
        conflict.to_vec()
    }
}

impl Default for ConflictMinimizer {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use oxiz_sat::Var;

    fn lit(var: u32, positive: bool) -> Lit {
        let v = Var::new(var);
        if positive { Lit::pos(v) } else { Lit::neg(v) }
    }

    #[test]
    fn test_minimizer_creation() {
        let minimizer = ConflictMinimizer::new();
        assert_eq!(minimizer.stats().conflicts_minimized, 0);
    }

    #[test]
    fn test_minimizer_config() {
        let config = MinimizerConfig {
            recursive: false,
            binary_resolution: false,
            max_depth: 50,
            use_stamping: false,
        };
        let minimizer = ConflictMinimizer::with_config(config);
        assert!(!minimizer.config.recursive);
    }

    #[test]
    fn test_minimize_trivial() {
        let mut minimizer = ConflictMinimizer::new();
        let conflict = vec![lit(0, true), lit(1, false), lit(2, true)];

        let minimized = minimizer.minimize(&conflict);

        // Without implication graph, no literals should be removed
        assert_eq!(minimized.len(), conflict.len());
        assert_eq!(minimizer.stats().conflicts_minimized, 1);
    }

    #[test]
    fn test_stats_update() {
        let mut minimizer = ConflictMinimizer::new();

        minimizer.minimize(&[lit(0, true), lit(1, false)]);
        minimizer.minimize(&[lit(2, true), lit(3, false), lit(4, true)]);

        assert_eq!(minimizer.stats().conflicts_minimized, 2);
    }
}