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