Skip to main content

oxiz_sat/
autotuning.rs

1//! Auto-tuning Framework
2//!
3//! This module provides automatic parameter optimization for the SAT solver.
4//! It can tune various solver parameters based on performance feedback.
5//!
6//! Features:
7//! - Parameter space exploration
8//! - Performance-based parameter adjustment
9//! - Adaptive parameter tuning during solving
10//! - Configuration scoring and comparison
11
12#[allow(unused_imports)]
13use crate::prelude::*;
14
15/// A tunable parameter
16#[derive(Debug, Clone)]
17pub struct Parameter {
18    /// Parameter name
19    pub name: String,
20    /// Current value
21    pub value: f64,
22    /// Minimum value
23    pub min: f64,
24    /// Maximum value
25    pub max: f64,
26    /// Step size for adjustments
27    pub step: f64,
28}
29
30impl Parameter {
31    /// Create a new parameter
32    #[must_use]
33    pub fn new(name: String, value: f64, min: f64, max: f64, step: f64) -> Self {
34        Self {
35            name,
36            value: value.clamp(min, max),
37            min,
38            max,
39            step,
40        }
41    }
42
43    /// Increase the parameter value
44    pub fn increase(&mut self) {
45        self.value = (self.value + self.step).min(self.max);
46    }
47
48    /// Decrease the parameter value
49    pub fn decrease(&mut self) {
50        self.value = (self.value - self.step).max(self.min);
51    }
52
53    /// Set to a specific value
54    pub fn set(&mut self, value: f64) {
55        self.value = value.clamp(self.min, self.max);
56    }
57
58    /// Get the current value
59    #[must_use]
60    pub fn get(&self) -> f64 {
61        self.value
62    }
63}
64
65/// Performance metrics for tuning evaluation
66#[derive(Debug, Clone, Default)]
67pub struct PerformanceMetrics {
68    /// Number of conflicts
69    pub conflicts: u64,
70    /// Number of decisions
71    pub decisions: u64,
72    /// Number of propagations
73    pub propagations: u64,
74    /// Solving time in milliseconds
75    pub time_ms: u64,
76    /// Whether the instance was solved
77    pub solved: bool,
78    /// Result (SAT/UNSAT)
79    pub satisfiable: Option<bool>,
80}
81
82impl PerformanceMetrics {
83    /// Compute a score for this configuration (lower is better)
84    ///
85    /// The score is primarily based on solving time, with bonuses for efficiency
86    #[must_use]
87    pub fn score(&self) -> f64 {
88        if !self.solved {
89            return f64::MAX;
90        }
91
92        // Base score is time
93        let mut score = self.time_ms as f64;
94
95        // Penalty for excessive conflicts
96        let conflict_rate = self.conflicts as f64 / self.time_ms.max(1) as f64;
97        if conflict_rate > 100.0 {
98            score *= 1.0 + (conflict_rate - 100.0) / 100.0;
99        }
100
101        // Bonus for high propagation rate
102        let prop_rate = self.propagations as f64 / self.time_ms.max(1) as f64;
103        if prop_rate > 1000.0 {
104            score *= 0.95;
105        }
106
107        score
108    }
109}
110
111/// Configuration with parameters and performance metrics
112#[derive(Debug, Clone)]
113pub struct Configuration {
114    /// Parameter values
115    pub parameters: HashMap<String, f64>,
116    /// Performance metrics
117    pub metrics: Option<PerformanceMetrics>,
118}
119
120impl Configuration {
121    /// Create a new configuration
122    #[must_use]
123    pub fn new(parameters: HashMap<String, f64>) -> Self {
124        Self {
125            parameters,
126            metrics: None,
127        }
128    }
129
130    /// Get the score for this configuration
131    #[must_use]
132    pub fn score(&self) -> f64 {
133        self.metrics.as_ref().map_or(f64::MAX, |m| m.score())
134    }
135}
136
137/// Tuning strategy
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum TuningStrategy {
140    /// Grid search - exhaustive search over parameter space
141    GridSearch,
142    /// Random search - random sampling of parameter space
143    RandomSearch,
144    /// Hill climbing - local search starting from current parameters
145    HillClimbing,
146    /// Adaptive tuning - adjust parameters based on runtime feedback
147    Adaptive,
148}
149
150/// Statistics for auto-tuning
151#[derive(Debug, Default, Clone)]
152pub struct AutotuningStats {
153    /// Number of configurations tried
154    pub configurations_tried: u64,
155    /// Best score found
156    pub best_score: f64,
157    /// Number of improvements
158    pub improvements: u64,
159}
160
161/// Auto-tuning manager
162pub struct Autotuner {
163    /// Tunable parameters
164    parameters: HashMap<String, Parameter>,
165    /// Tuning strategy
166    strategy: TuningStrategy,
167    /// Configuration history
168    history: Vec<Configuration>,
169    /// Best configuration found
170    best_config: Option<Configuration>,
171    /// Statistics
172    stats: AutotuningStats,
173}
174
175impl Autotuner {
176    /// Create a new auto-tuner
177    #[must_use]
178    pub fn new(strategy: TuningStrategy) -> Self {
179        Self {
180            parameters: HashMap::new(),
181            strategy,
182            history: Vec::new(),
183            best_config: None,
184            stats: AutotuningStats::default(),
185        }
186    }
187
188    /// Add a tunable parameter
189    pub fn add_parameter(&mut self, param: Parameter) {
190        self.parameters.insert(param.name.clone(), param);
191    }
192
193    /// Get current parameter values
194    #[must_use]
195    pub fn get_parameters(&self) -> HashMap<String, f64> {
196        self.parameters
197            .iter()
198            .map(|(name, param)| (name.clone(), param.value))
199            .collect()
200    }
201
202    /// Get statistics
203    #[must_use]
204    pub fn stats(&self) -> &AutotuningStats {
205        &self.stats
206    }
207
208    /// Reset statistics
209    pub fn reset_stats(&mut self) {
210        self.stats = AutotuningStats::default();
211        self.stats.best_score = f64::MAX;
212    }
213
214    /// Record performance for current configuration
215    pub fn record_performance(&mut self, metrics: PerformanceMetrics) {
216        let config = Configuration {
217            parameters: self.get_parameters(),
218            metrics: Some(metrics),
219        };
220
221        let score = config.score();
222        self.stats.configurations_tried += 1;
223
224        // Update best configuration
225        if score < self.stats.best_score || self.best_config.is_none() {
226            self.stats.best_score = score;
227            self.stats.improvements += 1;
228            self.best_config = Some(config.clone());
229        }
230
231        self.history.push(config);
232
233        // Apply strategy-specific parameter adjustments
234        self.adjust_parameters();
235    }
236
237    /// Get the best configuration found
238    #[must_use]
239    pub fn best_configuration(&self) -> Option<&Configuration> {
240        self.best_config.as_ref()
241    }
242
243    /// Apply the best configuration
244    pub fn apply_best(&mut self) {
245        if let Some(best) = &self.best_config {
246            for (name, &value) in &best.parameters {
247                if let Some(param) = self.parameters.get_mut(name) {
248                    param.set(value);
249                }
250            }
251        }
252    }
253
254    /// Generate next configuration to try (for grid/random search)
255    #[must_use]
256    pub fn next_configuration(&mut self) -> HashMap<String, f64> {
257        match self.strategy {
258            TuningStrategy::GridSearch => self.grid_search_next(),
259            TuningStrategy::RandomSearch => self.random_search_next(),
260            _ => self.get_parameters(),
261        }
262    }
263
264    /// Adjust parameters based on strategy
265    fn adjust_parameters(&mut self) {
266        match self.strategy {
267            TuningStrategy::HillClimbing => self.hill_climbing_adjust(),
268            TuningStrategy::Adaptive => self.adaptive_adjust(),
269            _ => {}
270        }
271    }
272
273    /// Hill climbing parameter adjustment
274    fn hill_climbing_adjust(&mut self) {
275        if self.history.len() < 2 {
276            return;
277        }
278
279        let current = &self.history[self.history.len() - 1];
280        let previous = &self.history[self.history.len() - 2];
281
282        let current_score = current.score();
283        let previous_score = previous.score();
284
285        // If we improved, continue in the same direction
286        // If we got worse, try the opposite direction
287        for (name, param) in &mut self.parameters {
288            let current_val = current.parameters.get(name).copied().unwrap_or(param.value);
289            let previous_val = previous
290                .parameters
291                .get(name)
292                .copied()
293                .unwrap_or(param.value);
294
295            if current_score < previous_score {
296                // Improved - continue in same direction
297                if current_val > previous_val {
298                    param.increase();
299                } else if current_val < previous_val {
300                    param.decrease();
301                }
302            } else {
303                // Got worse - try opposite direction
304                if current_val > previous_val {
305                    param.decrease();
306                } else if current_val < previous_val {
307                    param.increase();
308                }
309            }
310        }
311    }
312
313    /// Adaptive parameter adjustment based on runtime feedback
314    fn adaptive_adjust(&mut self) {
315        if self.history.is_empty() {
316            return;
317        }
318
319        let recent_window = 5.min(self.history.len());
320        let recent_configs: Vec<_> = self.history.iter().rev().take(recent_window).collect();
321
322        // Calculate average metrics
323        let mut total_conflicts = 0;
324        let mut total_decisions = 0;
325        let mut count = 0;
326
327        for config in &recent_configs {
328            if let Some(ref metrics) = config.metrics {
329                total_conflicts += metrics.conflicts;
330                total_decisions += metrics.decisions;
331                count += 1;
332            }
333        }
334
335        if count == 0 {
336            return;
337        }
338
339        let avg_conflicts = total_conflicts / count;
340        let avg_decisions = total_decisions / count;
341
342        // Adaptive adjustments based on heuristics
343        // High conflict rate -> increase restart aggressiveness
344        // High decision rate -> adjust branching heuristic parameters
345
346        // Example: adjust restart_factor based on conflict rate
347        if let Some(restart_param) = self.parameters.get_mut("restart_factor") {
348            if avg_conflicts > 10000 {
349                restart_param.increase();
350            } else if avg_conflicts < 1000 {
351                restart_param.decrease();
352            }
353        }
354
355        // Example: adjust variable_decay based on decision rate
356        if let Some(decay_param) = self.parameters.get_mut("variable_decay") {
357            if avg_decisions > 5000 {
358                decay_param.increase();
359            } else if avg_decisions < 500 {
360                decay_param.decrease();
361            }
362        }
363    }
364
365    /// Grid search - systematically try all combinations
366    fn grid_search_next(&self) -> HashMap<String, f64> {
367        // Simple implementation: just return current parameters
368        // A full grid search would enumerate all combinations
369        self.get_parameters()
370    }
371
372    /// Random search - randomly sample parameter space
373    fn random_search_next(&mut self) -> HashMap<String, f64> {
374        use core::hash::{BuildHasher, Hash, Hasher};
375
376        let mut result = HashMap::new();
377        let state = core::hash::BuildHasherDefault::<rustc_hash::FxHasher>::default();
378
379        for (name, param) in &self.parameters {
380            // Simple pseudo-random value generation using hash
381            let mut hasher = state.build_hasher();
382            name.hash(&mut hasher);
383            self.history.len().hash(&mut hasher);
384            let hash = hasher.finish();
385
386            let range = param.max - param.min;
387            let random_factor = (hash % 1000) as f64 / 1000.0;
388            let value = param.min + range * random_factor;
389
390            result.insert(name.clone(), value);
391        }
392
393        result
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_parameter_creation() {
403        let param = Parameter::new("test".to_string(), 0.5, 0.0, 1.0, 0.1);
404        assert_eq!(param.name, "test");
405        assert_eq!(param.value, 0.5);
406    }
407
408    #[test]
409    fn test_parameter_increase_decrease() {
410        let mut param = Parameter::new("test".to_string(), 0.5, 0.0, 1.0, 0.1);
411
412        param.increase();
413        assert_eq!(param.value, 0.6);
414
415        param.decrease();
416        assert_eq!(param.value, 0.5);
417    }
418
419    #[test]
420    fn test_parameter_bounds() {
421        let mut param = Parameter::new("test".to_string(), 0.9, 0.0, 1.0, 0.2);
422
423        param.increase();
424        assert_eq!(param.value, 1.0); // Clamped to max
425
426        param.decrease();
427        param.decrease();
428        param.decrease();
429        param.decrease();
430        param.decrease();
431        param.decrease();
432        assert_eq!(param.value, 0.0); // Clamped to min
433    }
434
435    #[test]
436    fn test_autotuner_creation() {
437        let tuner = Autotuner::new(TuningStrategy::HillClimbing);
438        assert_eq!(tuner.stats().configurations_tried, 0);
439    }
440
441    #[test]
442    fn test_add_parameter() {
443        let mut tuner = Autotuner::new(TuningStrategy::HillClimbing);
444        let param = Parameter::new("test".to_string(), 0.5, 0.0, 1.0, 0.1);
445
446        tuner.add_parameter(param);
447        let params = tuner.get_parameters();
448
449        assert_eq!(params.get("test"), Some(&0.5));
450    }
451
452    #[test]
453    fn test_record_performance() {
454        let mut tuner = Autotuner::new(TuningStrategy::HillClimbing);
455        let param = Parameter::new("test".to_string(), 0.5, 0.0, 1.0, 0.1);
456        tuner.add_parameter(param);
457
458        let metrics = PerformanceMetrics {
459            conflicts: 100,
460            decisions: 200,
461            propagations: 1000,
462            time_ms: 50,
463            solved: true,
464            satisfiable: Some(true),
465        };
466
467        tuner.record_performance(metrics);
468        assert_eq!(tuner.stats().configurations_tried, 1);
469    }
470
471    #[test]
472    fn test_best_configuration() {
473        let mut tuner = Autotuner::new(TuningStrategy::HillClimbing);
474        let param = Parameter::new("test".to_string(), 0.5, 0.0, 1.0, 0.1);
475        tuner.add_parameter(param);
476
477        let metrics1 = PerformanceMetrics {
478            conflicts: 100,
479            decisions: 200,
480            propagations: 1000,
481            time_ms: 100,
482            solved: true,
483            satisfiable: Some(true),
484        };
485
486        tuner.record_performance(metrics1);
487
488        let metrics2 = PerformanceMetrics {
489            conflicts: 50,
490            decisions: 100,
491            propagations: 500,
492            time_ms: 50,
493            solved: true,
494            satisfiable: Some(true),
495        };
496
497        tuner.record_performance(metrics2);
498
499        assert_eq!(tuner.stats().improvements, 2);
500        assert!(tuner.best_configuration().is_some());
501    }
502
503    #[test]
504    fn test_performance_score() {
505        let metrics = PerformanceMetrics {
506            conflicts: 100,
507            decisions: 200,
508            propagations: 1000,
509            time_ms: 50,
510            solved: true,
511            satisfiable: Some(true),
512        };
513
514        let score = metrics.score();
515        assert!(score > 0.0);
516        assert!(score < f64::MAX);
517    }
518
519    #[test]
520    fn test_unsolved_score() {
521        let metrics = PerformanceMetrics {
522            conflicts: 100,
523            decisions: 200,
524            propagations: 1000,
525            time_ms: 50,
526            solved: false,
527            satisfiable: None,
528        };
529
530        let score = metrics.score();
531        assert_eq!(score, f64::MAX);
532    }
533
534    #[test]
535    fn test_apply_best() {
536        let mut tuner = Autotuner::new(TuningStrategy::HillClimbing);
537        let param = Parameter::new("test".to_string(), 0.5, 0.0, 1.0, 0.1);
538        tuner.add_parameter(param);
539
540        let metrics = PerformanceMetrics {
541            conflicts: 100,
542            decisions: 200,
543            propagations: 1000,
544            time_ms: 50,
545            solved: true,
546            satisfiable: Some(true),
547        };
548
549        tuner.record_performance(metrics);
550
551        // Manually change parameter
552        if let Some(param) = tuner.parameters.get_mut("test") {
553            param.set(0.8);
554        }
555
556        // Apply best should revert to best configuration
557        tuner.apply_best();
558        let params = tuner.get_parameters();
559        assert_eq!(params.get("test"), Some(&0.5));
560    }
561
562    #[test]
563    fn test_different_strategies() {
564        let strategies = vec![
565            TuningStrategy::GridSearch,
566            TuningStrategy::RandomSearch,
567            TuningStrategy::HillClimbing,
568            TuningStrategy::Adaptive,
569        ];
570
571        for strategy in strategies {
572            let tuner = Autotuner::new(strategy);
573            assert_eq!(tuner.strategy, strategy);
574        }
575    }
576}