oxirs_arq/advanced_optimizer/
model_manager.rs1use std::collections::VecDeque;
7use std::path::Path;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{Arc, RwLock};
10use std::time::SystemTime;
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14
15use crate::advanced_optimizer::ml_predictor::MLPredictor;
16use crate::advanced_optimizer::training_collector::TrainingCollector;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ManagerConfig {
21 pub retraining_interval_hours: u64,
23 pub min_examples_for_training: usize,
25 pub quality_threshold_r2: f64,
27 pub quality_threshold_mae: f64,
29 pub enable_auto_retraining: bool,
31 pub enable_rollback: bool,
33 pub max_prediction_buffer: usize,
35}
36
37impl Default for ManagerConfig {
38 fn default() -> Self {
39 Self {
40 retraining_interval_hours: 24,
41 min_examples_for_training: 100,
42 quality_threshold_r2: 0.8,
43 quality_threshold_mae: 0.2, enable_auto_retraining: true,
45 enable_rollback: true,
46 max_prediction_buffer: 1000,
47 }
48 }
49}
50
51pub struct ModelManager {
53 active_model: Arc<RwLock<MLPredictor>>,
54 previous_model: Option<Arc<RwLock<MLPredictor>>>,
55 training_collector: Option<Arc<RwLock<TrainingCollector>>>,
56 config: ManagerConfig,
57 performance_tracker: Arc<RwLock<PerformanceTracker>>,
58 retraining_in_progress: Arc<AtomicBool>,
59 last_retraining: Option<SystemTime>,
60}
61
62impl ModelManager {
63 pub fn new(predictor: MLPredictor, config: ManagerConfig) -> Self {
65 let performance_tracker = Arc::new(RwLock::new(PerformanceTracker::new(
66 config.max_prediction_buffer,
67 )));
68
69 Self {
70 active_model: Arc::new(RwLock::new(predictor)),
71 previous_model: None,
72 training_collector: None,
73 config,
74 performance_tracker,
75 retraining_in_progress: Arc::new(AtomicBool::new(false)),
76 last_retraining: None,
77 }
78 }
79
80 pub fn with_training_collector(mut self, collector: Arc<RwLock<TrainingCollector>>) -> Self {
82 self.training_collector = Some(collector);
83 self
84 }
85
86 pub fn get_predictor(&self) -> Arc<RwLock<MLPredictor>> {
88 Arc::clone(&self.active_model)
89 }
90
91 pub fn record_prediction(&self, predicted: f64, actual: f64) -> Result<()> {
93 let mut tracker = self
94 .performance_tracker
95 .write()
96 .map_err(|e| anyhow::anyhow!("Failed to acquire write lock: {}", e))?;
97
98 tracker.record(PredictionResult {
99 predicted_cost: predicted,
100 actual_cost: Some(actual),
101 timestamp: SystemTime::now(),
102 error: Some((predicted - actual).abs()),
103 });
104
105 Ok(())
106 }
107
108 pub fn evaluate_model_quality(&self) -> Result<ModelQuality> {
110 let tracker = self
111 .performance_tracker
112 .read()
113 .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
114
115 let quality = tracker.calculate_quality();
116
117 Ok(quality)
118 }
119
120 pub fn should_use_ml(&self) -> bool {
122 if let Ok(quality) = self.evaluate_model_quality() {
123 quality.is_acceptable
124 } else {
125 false
126 }
127 }
128
129 pub fn should_retrain(&self) -> bool {
131 if !self.config.enable_auto_retraining {
132 return false;
133 }
134
135 if self.retraining_in_progress.load(Ordering::Relaxed) {
137 return false;
138 }
139
140 if let Some(last_training) = self.last_retraining {
142 if let Ok(elapsed) = SystemTime::now().duration_since(last_training) {
143 let hours_elapsed = elapsed.as_secs() / 3600;
144 if hours_elapsed < self.config.retraining_interval_hours {
145 return false;
146 }
147 }
148 }
149
150 if let Some(ref collector) = self.training_collector {
152 if let Ok(collector_guard) = collector.read() {
153 if collector_guard.len() < self.config.min_examples_for_training {
154 return false;
155 }
156 } else {
157 return false;
158 }
159 } else {
160 return false;
162 }
163
164 true
165 }
166
167 pub fn trigger_retraining(&mut self) -> Result<()> {
169 if self
171 .retraining_in_progress
172 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
173 .is_err()
174 {
175 return Err(anyhow::anyhow!("Retraining already in progress"));
176 }
177
178 let result = self.retrain_internal();
180
181 self.retraining_in_progress.store(false, Ordering::SeqCst);
183
184 result
185 }
186
187 fn retrain_internal(&mut self) -> Result<()> {
189 let training_collector = self
191 .training_collector
192 .as_ref()
193 .ok_or_else(|| anyhow::anyhow!("No training collector available"))?;
194
195 let examples = {
196 let collector = training_collector
197 .read()
198 .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
199 collector.get_all_examples()?
200 };
201
202 if examples.len() < self.config.min_examples_for_training {
203 return Err(anyhow::anyhow!(
204 "Insufficient training examples: {} < {}",
205 examples.len(),
206 self.config.min_examples_for_training
207 ));
208 }
209
210 if self.config.enable_rollback {
212 let current = self
213 .active_model
214 .read()
215 .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
216
217 self.previous_model = Some(Arc::new(RwLock::new(current.clone())));
218 }
219
220 let old_quality = self.evaluate_model_quality()?;
222
223 {
225 let mut model = self
226 .active_model
227 .write()
228 .map_err(|e| anyhow::anyhow!("Failed to acquire write lock: {}", e))?;
229
230 for example in examples {
232 model.add_training_example(example);
233 }
234
235 model.train_model().context("Failed to train model")?;
237 }
238
239 let new_quality = self.evaluate_model_quality()?;
241
242 if self.config.enable_rollback && new_quality.r_squared < old_quality.r_squared {
244 tracing::warn!(
245 "New model quality degraded (R²: {} → {}). Rolling back.",
246 old_quality.r_squared,
247 new_quality.r_squared
248 );
249 self.rollback_to_previous()?;
250 } else {
251 tracing::info!(
252 "Model retrained successfully. R²: {} → {}, MAE: {} → {}",
253 old_quality.r_squared,
254 new_quality.r_squared,
255 old_quality.mae,
256 new_quality.mae
257 );
258 }
259
260 self.last_retraining = Some(SystemTime::now());
262
263 Ok(())
264 }
265
266 pub fn rollback_to_previous(&mut self) -> Result<()> {
268 let previous = self
269 .previous_model
270 .take()
271 .ok_or_else(|| anyhow::anyhow!("No previous model available for rollback"))?;
272
273 self.active_model = previous;
274
275 tracing::info!("Rolled back to previous model");
276
277 Ok(())
278 }
279
280 pub fn save_checkpoint(&self, path: &Path) -> Result<()> {
282 let model = self
283 .active_model
284 .read()
285 .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
286
287 model
288 .save_model(path)
289 .context("Failed to save model checkpoint")?;
290
291 Ok(())
292 }
293
294 pub fn load_checkpoint(path: &Path, config: ManagerConfig) -> Result<Self> {
296 let predictor =
297 MLPredictor::load_model(path).context("Failed to load model from checkpoint")?;
298
299 Ok(Self::new(predictor, config))
300 }
301
302 pub fn get_performance_metrics(&self) -> Result<PerformanceMetrics> {
304 let tracker = self
305 .performance_tracker
306 .read()
307 .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
308
309 let model = self
310 .active_model
311 .read()
312 .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
313
314 Ok(PerformanceMetrics {
315 predictions_made: model.predictions_count(),
316 training_examples: model.training_data_count(),
317 mae: tracker.mae,
318 rmse: tracker.rmse,
319 r_squared: tracker.r_squared,
320 is_using_ml: self.should_use_ml(),
321 })
322 }
323}
324
325pub struct PerformanceTracker {
327 predictions: VecDeque<PredictionResult>,
328 max_buffer: usize,
329 pub mae: f64,
330 pub rmse: f64,
331 pub r_squared: f64,
332 last_update: SystemTime,
333}
334
335impl PerformanceTracker {
336 pub fn new(max_buffer: usize) -> Self {
338 Self {
339 predictions: VecDeque::with_capacity(max_buffer.min(1000)),
340 max_buffer,
341 mae: 0.0,
342 rmse: 0.0,
343 r_squared: 0.0,
344 last_update: SystemTime::now(),
345 }
346 }
347
348 pub fn record(&mut self, result: PredictionResult) {
350 self.predictions.push_back(result);
351
352 if self.predictions.len() > self.max_buffer {
354 self.predictions.pop_front();
355 }
356
357 self.update_metrics();
359 }
360
361 pub fn update_metrics(&mut self) {
363 let valid_predictions: Vec<&PredictionResult> = self
364 .predictions
365 .iter()
366 .filter(|p| p.actual_cost.is_some())
367 .collect();
368
369 if valid_predictions.is_empty() {
370 return;
371 }
372
373 let n = valid_predictions.len() as f64;
374
375 let total_error: f64 = valid_predictions.iter().filter_map(|p| p.error).sum();
377 self.mae = total_error / n;
378
379 let squared_errors: f64 = valid_predictions
381 .iter()
382 .filter_map(|p| p.error.map(|e| e * e))
383 .sum();
384 self.rmse = (squared_errors / n).sqrt();
385
386 let mean_actual: f64 = valid_predictions
388 .iter()
389 .filter_map(|p| p.actual_cost)
390 .sum::<f64>()
391 / n;
392
393 let ss_tot: f64 = valid_predictions
394 .iter()
395 .filter_map(|p| p.actual_cost.map(|a| (a - mean_actual).powi(2)))
396 .sum();
397
398 let ss_res: f64 = valid_predictions
399 .iter()
400 .filter_map(|p| {
401 if let (Some(_actual), Some(error)) = (p.actual_cost, p.error) {
402 Some(error.powi(2))
403 } else {
404 None
405 }
406 })
407 .sum();
408
409 self.r_squared = if ss_tot > 1e-10 {
410 1.0 - (ss_res / ss_tot)
411 } else {
412 0.0
413 };
414
415 self.last_update = SystemTime::now();
416 }
417
418 pub fn calculate_quality(&self) -> ModelQuality {
420 let is_acceptable = self.r_squared >= 0.8 && self.mae <= 0.2;
421
422 let recommendation = if self.r_squared < 0.5 {
423 QualityRecommendation::UseFallback
424 } else if self.r_squared < 0.8 {
425 QualityRecommendation::NeedsRetraining
426 } else {
427 QualityRecommendation::UseMl
428 };
429
430 ModelQuality {
431 r_squared: self.r_squared,
432 mae: self.mae,
433 rmse: self.rmse,
434 is_acceptable,
435 recommendation,
436 }
437 }
438
439 pub fn prediction_count(&self) -> usize {
441 self.predictions.len()
442 }
443}
444
445#[derive(Debug, Clone)]
447pub struct PredictionResult {
448 pub predicted_cost: f64,
449 pub actual_cost: Option<f64>,
450 pub timestamp: SystemTime,
451 pub error: Option<f64>,
452}
453
454#[derive(Debug, Clone)]
456pub struct ModelQuality {
457 pub r_squared: f64,
458 pub mae: f64,
459 pub rmse: f64,
460 pub is_acceptable: bool,
461 pub recommendation: QualityRecommendation,
462}
463
464#[derive(Debug, Clone, PartialEq)]
466pub enum QualityRecommendation {
467 UseMl,
469 UseFallback,
471 NeedsRetraining,
473}
474
475#[derive(Debug, Clone)]
477pub struct PerformanceMetrics {
478 pub predictions_made: usize,
479 pub training_examples: usize,
480 pub mae: f64,
481 pub rmse: f64,
482 pub r_squared: f64,
483 pub is_using_ml: bool,
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::advanced_optimizer::ml_predictor::MLModelType;
490
491 #[test]
492 fn test_model_manager_creation() -> Result<()> {
493 let predictor = MLPredictor::from_model_type(MLModelType::LinearRegression)?;
494 let config = ManagerConfig::default();
495 let manager = ModelManager::new(predictor, config);
496
497 assert!(!manager.should_retrain()); Ok(())
500 }
501
502 #[test]
503 fn test_performance_tracker() {
504 let mut tracker = PerformanceTracker::new(10);
505
506 for i in 1..=5 {
508 let predicted = i as f64 * 10.0;
509 let actual = i as f64 * 10.0 + 5.0; tracker.record(PredictionResult {
512 predicted_cost: predicted,
513 actual_cost: Some(actual),
514 timestamp: SystemTime::now(),
515 error: Some(5.0),
516 });
517 }
518
519 assert_eq!(tracker.prediction_count(), 5);
520 assert!((tracker.mae - 5.0).abs() < 1e-6); }
522
523 #[test]
524 fn test_model_quality_assessment() {
525 let mut tracker = PerformanceTracker::new(10);
526
527 for i in 1..=10 {
529 let cost = i as f64 * 10.0;
530 tracker.record(PredictionResult {
531 predicted_cost: cost,
532 actual_cost: Some(cost),
533 timestamp: SystemTime::now(),
534 error: Some(0.0),
535 });
536 }
537
538 let quality = tracker.calculate_quality();
539 assert_eq!(quality.mae, 0.0);
540 assert_eq!(quality.rmse, 0.0);
541 assert!(quality.is_acceptable);
542 assert_eq!(quality.recommendation, QualityRecommendation::UseMl);
543 }
544
545 #[test]
546 fn test_quality_recommendation_poor() {
547 let mut tracker = PerformanceTracker::new(10);
548
549 for i in 1..=5 {
551 let predicted = i as f64 * 10.0;
552 let actual = i as f64 * 50.0; tracker.record(PredictionResult {
555 predicted_cost: predicted,
556 actual_cost: Some(actual),
557 timestamp: SystemTime::now(),
558 error: Some((predicted - actual).abs()),
559 });
560 }
561
562 let quality = tracker.calculate_quality();
563 assert!(!quality.is_acceptable);
564 assert!(quality.r_squared < 0.8);
566 }
567
568 #[test]
569 fn test_buffer_limit() {
570 let max_buffer = 5;
571 let mut tracker = PerformanceTracker::new(max_buffer);
572
573 for i in 1..=10 {
575 tracker.record(PredictionResult {
576 predicted_cost: i as f64,
577 actual_cost: Some(i as f64),
578 timestamp: SystemTime::now(),
579 error: Some(0.0),
580 });
581 }
582
583 assert_eq!(tracker.prediction_count(), max_buffer);
585 }
586
587 #[test]
588 fn test_record_prediction() -> Result<()> {
589 let predictor = MLPredictor::from_model_type(MLModelType::LinearRegression)?;
590 let config = ManagerConfig::default();
591 let manager = ModelManager::new(predictor, config);
592
593 manager.record_prediction(100.0, 105.0)?;
594 manager.record_prediction(200.0, 195.0)?;
595
596 let metrics = manager.get_performance_metrics()?;
597 assert_eq!(metrics.predictions_made, 0); Ok(())
600 }
601}