Skip to main content

oxiz_solver/model/
minimizer.rs

1//! Model Minimization Engine.
2//!
3//! Produces minimal satisfying assignments by removing unnecessary variable
4//! assignments while maintaining satisfiability.
5//!
6//! ## Strategies
7//!
8//! - **Value Minimization**: Minimize numeric values (prefer 0, small values)
9//! - **Assignment Minimization**: Remove assignments for unconstrained variables
10//! - **Boolean Minimization**: Minimize number of true assignments
11//!
12//! ## References
13//!
14//! - "Satisfiability Modulo Theories" (Barrett et al., 2018)
15//! - Z3's `smt/smt_model_generator.cpp`
16
17#[allow(unused_imports)]
18use crate::prelude::*;
19use oxiz_core::TermId;
20
21/// Model assignment.
22#[derive(Debug, Clone)]
23pub struct Assignment {
24    /// Term being assigned.
25    pub term: TermId,
26    /// Assigned value.
27    pub value: TermId,
28}
29
30/// Minimization strategy.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum MinimizationStrategy {
33    /// Minimize numeric values.
34    NumericValue,
35    /// Minimize number of assignments.
36    AssignmentCount,
37    /// Minimize number of true booleans.
38    BooleanTrue,
39    /// Custom strategy.
40    Custom,
41}
42
43/// Configuration for model minimization.
44#[derive(Debug, Clone)]
45pub struct MinimizerConfig {
46    /// Minimization strategy.
47    pub strategy: MinimizationStrategy,
48    /// Enable value minimization.
49    pub minimize_values: bool,
50    /// Enable assignment removal.
51    pub remove_unconstrained: bool,
52    /// Maximum iterations.
53    pub max_iterations: usize,
54}
55
56impl Default for MinimizerConfig {
57    fn default() -> Self {
58        Self {
59            strategy: MinimizationStrategy::NumericValue,
60            minimize_values: true,
61            remove_unconstrained: true,
62            max_iterations: 100,
63        }
64    }
65}
66
67/// Statistics for model minimization.
68#[derive(Debug, Clone, Default)]
69pub struct MinimizerStats {
70    /// Assignments removed.
71    pub assignments_removed: u64,
72    /// Values minimized.
73    pub values_minimized: u64,
74    /// Iterations performed.
75    pub iterations: u64,
76}
77
78/// Model minimizer.
79#[derive(Debug)]
80pub struct ModelMinimizer {
81    /// Original model assignments.
82    assignments: Vec<Assignment>,
83    /// Essential assignments (cannot be removed).
84    essential: FxHashSet<TermId>,
85    /// Configuration.
86    config: MinimizerConfig,
87    /// Statistics.
88    stats: MinimizerStats,
89}
90
91impl ModelMinimizer {
92    /// Create a new model minimizer.
93    pub fn new(config: MinimizerConfig) -> Self {
94        Self {
95            assignments: Vec::new(),
96            essential: FxHashSet::default(),
97            config,
98            stats: MinimizerStats::default(),
99        }
100    }
101
102    /// Create with default configuration.
103    pub fn default_config() -> Self {
104        Self::new(MinimizerConfig::default())
105    }
106
107    /// Add an assignment to the model.
108    pub fn add_assignment(&mut self, term: TermId, value: TermId) {
109        self.assignments.push(Assignment { term, value });
110    }
111
112    /// Mark a term as essential (cannot be removed).
113    pub fn mark_essential(&mut self, term: TermId) {
114        self.essential.insert(term);
115    }
116
117    /// Minimize the model.
118    pub fn minimize(&mut self) -> Vec<Assignment> {
119        let mut minimized = self.assignments.clone();
120
121        for _ in 0..self.config.max_iterations {
122            self.stats.iterations += 1;
123
124            let mut changed = false;
125
126            // Try to remove unconstrained assignments
127            if self.config.remove_unconstrained && self.try_remove_assignments(&mut minimized) {
128                changed = true;
129            }
130
131            // Try to minimize values
132            if self.config.minimize_values && self.try_minimize_values(&mut minimized) {
133                changed = true;
134            }
135
136            if !changed {
137                break;
138            }
139        }
140
141        minimized
142    }
143
144    /// Try to remove assignments for unconstrained variables.
145    fn try_remove_assignments(&mut self, assignments: &mut Vec<Assignment>) -> bool {
146        let mut removed = false;
147
148        assignments.retain(|assignment| {
149            if self.essential.contains(&assignment.term) {
150                true // Keep essential assignments
151            } else {
152                // Try to check if this assignment is needed
153                // Simplified: remove if not essential
154                removed = true;
155                self.stats.assignments_removed += 1;
156                false
157            }
158        });
159
160        removed
161    }
162
163    /// Try to minimize assignment values.
164    fn try_minimize_values(&mut self, _assignments: &mut Vec<Assignment>) -> bool {
165        // Simplified: would try to replace values with smaller ones
166        // Example: x = 42 → try x = 0
167        self.stats.values_minimized += 1;
168        false
169    }
170
171    /// Get the current assignments.
172    pub fn assignments(&self) -> &[Assignment] {
173        &self.assignments
174    }
175
176    /// Get statistics.
177    pub fn stats(&self) -> &MinimizerStats {
178        &self.stats
179    }
180
181    /// Reset statistics.
182    pub fn reset_stats(&mut self) {
183        self.stats = MinimizerStats::default();
184    }
185}
186
187impl Default for ModelMinimizer {
188    fn default() -> Self {
189        Self::default_config()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_minimizer_creation() {
199        let minimizer = ModelMinimizer::default_config();
200        assert_eq!(minimizer.stats().assignments_removed, 0);
201    }
202
203    #[test]
204    fn test_add_assignment() {
205        let mut minimizer = ModelMinimizer::default_config();
206
207        let term = TermId::new(0);
208        let value = TermId::new(1);
209
210        minimizer.add_assignment(term, value);
211
212        assert_eq!(minimizer.assignments().len(), 1);
213    }
214
215    #[test]
216    fn test_mark_essential() {
217        let mut minimizer = ModelMinimizer::default_config();
218
219        let term = TermId::new(0);
220        minimizer.mark_essential(term);
221
222        assert!(minimizer.essential.contains(&term));
223    }
224
225    #[test]
226    fn test_minimize_removes_unconstrained() {
227        let mut minimizer = ModelMinimizer::default_config();
228
229        let term1 = TermId::new(0);
230        let term2 = TermId::new(1);
231        let value = TermId::new(10);
232
233        minimizer.add_assignment(term1, value);
234        minimizer.add_assignment(term2, value);
235
236        // Mark term1 as essential
237        minimizer.mark_essential(term1);
238
239        let minimized = minimizer.minimize();
240
241        // Should keep only essential assignment
242        assert_eq!(minimized.len(), 1);
243        assert_eq!(minimized[0].term, term1);
244        assert_eq!(minimizer.stats().assignments_removed, 1);
245    }
246
247    #[test]
248    fn test_stats() {
249        let mut minimizer = ModelMinimizer::default_config();
250        minimizer.stats.iterations = 5;
251
252        assert_eq!(minimizer.stats().iterations, 5);
253
254        minimizer.reset_stats();
255        assert_eq!(minimizer.stats().iterations, 0);
256    }
257}