Skip to main content

trustformers_debug/
dashboard.rs

1//! Interactive dashboards for real-time monitoring and analysis
2// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
3// are retained for the data model, serialization completeness, and future consumers that
4// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
5#![allow(dead_code)]
6
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant, SystemTime};
12use uuid::Uuid;
13
14use crate::DebugConfig;
15
16/// Real-time metrics for dashboard display
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DashboardMetrics {
19    pub timestamp: SystemTime,
20    pub loss: Option<f64>,
21    pub accuracy: Option<f64>,
22    pub learning_rate: Option<f64>,
23    pub memory_usage_mb: f64,
24    pub gpu_utilization: Option<f64>,
25    pub tokens_per_second: Option<f64>,
26    pub gradient_norm: Option<f64>,
27    pub epoch: Option<u32>,
28    pub step: Option<u64>,
29}
30
31/// Training monitor for real-time tracking
32#[derive(Debug)]
33pub struct TrainingMonitor {
34    config: DebugConfig,
35    metrics_history: VecDeque<DashboardMetrics>,
36    max_history: usize,
37    start_time: Instant,
38    alert_thresholds: AlertThresholds,
39    active_alerts: Vec<TrainingAlert>,
40}
41
42/// Alert thresholds for training monitoring
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AlertThresholds {
45    pub loss_increase_threshold: f64,
46    pub gradient_norm_max: f64,
47    pub memory_usage_max_mb: f64,
48    pub gpu_utilization_min: f64,
49    pub learning_rate_min: f64,
50    pub tokens_per_second_min: f64,
51}
52
53impl Default for AlertThresholds {
54    fn default() -> Self {
55        Self {
56            loss_increase_threshold: 1.5,
57            gradient_norm_max: 10.0,
58            memory_usage_max_mb: 8192.0,
59            gpu_utilization_min: 0.7,
60            learning_rate_min: 1e-8,
61            tokens_per_second_min: 100.0,
62        }
63    }
64}
65
66/// Training alert types
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TrainingAlert {
69    pub alert_type: AlertType,
70    pub severity: AlertSeverity,
71    pub message: String,
72    pub timestamp: SystemTime,
73    pub metric_value: f64,
74    pub threshold: f64,
75    pub suggested_action: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub enum AlertType {
80    LossIncrease,
81    GradientExplosion,
82    MemoryOveruse,
83    LowGpuUtilization,
84    LearningRateTooLow,
85    SlowTokenProcessing,
86    ModelDivergence,
87    TrainingStalled,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub enum AlertSeverity {
92    Info,
93    Warning,
94    Critical,
95}
96
97impl TrainingMonitor {
98    /// Create a new training monitor
99    pub fn new(config: &DebugConfig) -> Self {
100        Self {
101            config: config.clone(),
102            metrics_history: VecDeque::new(),
103            max_history: 10000,
104            start_time: Instant::now(),
105            alert_thresholds: AlertThresholds::default(),
106            active_alerts: Vec::new(),
107        }
108    }
109
110    /// Update metrics and check for alerts
111    pub fn update_metrics(&mut self, metrics: DashboardMetrics) {
112        // Add to history
113        self.metrics_history.push_back(metrics.clone());
114
115        // Trim history if needed
116        if self.metrics_history.len() > self.max_history {
117            self.metrics_history.pop_front();
118        }
119
120        // Check for alerts
121        self.check_alerts(&metrics);
122    }
123
124    /// Get recent metrics for dashboard
125    pub fn get_recent_metrics(&self, count: usize) -> Vec<DashboardMetrics> {
126        self.metrics_history.iter().rev().take(count).rev().cloned().collect()
127    }
128
129    /// Get active alerts
130    pub fn get_active_alerts(&self) -> &[TrainingAlert] {
131        &self.active_alerts
132    }
133
134    /// Clear resolved alerts
135    pub fn clear_alert(&mut self, _alert_type: AlertType) {
136        self.active_alerts.retain(|alert| !matches!(&alert.alert_type, _alert_type));
137    }
138
139    /// Set custom alert thresholds
140    pub fn set_alert_thresholds(&mut self, thresholds: AlertThresholds) {
141        self.alert_thresholds = thresholds;
142    }
143
144    /// Generate training summary
145    pub fn generate_training_summary(&self) -> TrainingSummary {
146        let total_duration = self.start_time.elapsed();
147        let total_steps = self.metrics_history.len();
148
149        let avg_loss = self.calculate_average_loss();
150        let best_accuracy = self.calculate_best_accuracy();
151        let avg_tokens_per_second = self.calculate_average_tokens_per_second();
152        let training_stability = self.calculate_training_stability();
153
154        TrainingSummary {
155            total_duration,
156            total_steps,
157            avg_loss,
158            best_accuracy,
159            avg_tokens_per_second,
160            training_stability,
161            active_alerts_count: self.active_alerts.len(),
162            convergence_status: self.assess_convergence(),
163        }
164    }
165
166    fn check_alerts(&mut self, metrics: &DashboardMetrics) {
167        // Check for loss increase
168        if let Some(current_loss) = metrics.loss {
169            if let Some(prev_metrics) =
170                self.metrics_history.get(self.metrics_history.len().saturating_sub(10))
171            {
172                if let Some(prev_loss) = prev_metrics.loss {
173                    if current_loss > prev_loss * self.alert_thresholds.loss_increase_threshold {
174                        self.add_alert(TrainingAlert {
175                            alert_type: AlertType::LossIncrease,
176                            severity: AlertSeverity::Warning,
177                            message: "Loss has increased significantly".to_string(),
178                            timestamp: SystemTime::now(),
179                            metric_value: current_loss,
180                            threshold: prev_loss * self.alert_thresholds.loss_increase_threshold,
181                            suggested_action: "Check learning rate or data quality".to_string(),
182                        });
183                    }
184                }
185            }
186        }
187
188        // Check gradient norm
189        if let Some(grad_norm) = metrics.gradient_norm {
190            if grad_norm > self.alert_thresholds.gradient_norm_max {
191                self.add_alert(TrainingAlert {
192                    alert_type: AlertType::GradientExplosion,
193                    severity: AlertSeverity::Critical,
194                    message: "Gradient explosion detected".to_string(),
195                    timestamp: SystemTime::now(),
196                    metric_value: grad_norm,
197                    threshold: self.alert_thresholds.gradient_norm_max,
198                    suggested_action: "Apply gradient clipping or reduce learning rate".to_string(),
199                });
200            }
201        }
202
203        // Check memory usage
204        if metrics.memory_usage_mb > self.alert_thresholds.memory_usage_max_mb {
205            self.add_alert(TrainingAlert {
206                alert_type: AlertType::MemoryOveruse,
207                severity: AlertSeverity::Warning,
208                message: "High memory usage detected".to_string(),
209                timestamp: SystemTime::now(),
210                metric_value: metrics.memory_usage_mb,
211                threshold: self.alert_thresholds.memory_usage_max_mb,
212                suggested_action: "Reduce batch size or enable gradient checkpointing".to_string(),
213            });
214        }
215
216        // Check GPU utilization
217        if let Some(gpu_util) = metrics.gpu_utilization {
218            if gpu_util < self.alert_thresholds.gpu_utilization_min {
219                self.add_alert(TrainingAlert {
220                    alert_type: AlertType::LowGpuUtilization,
221                    severity: AlertSeverity::Info,
222                    message: "Low GPU utilization".to_string(),
223                    timestamp: SystemTime::now(),
224                    metric_value: gpu_util,
225                    threshold: self.alert_thresholds.gpu_utilization_min,
226                    suggested_action: "Increase batch size or check data loading".to_string(),
227                });
228            }
229        }
230
231        // Check tokens per second
232        if let Some(tps) = metrics.tokens_per_second {
233            if tps < self.alert_thresholds.tokens_per_second_min {
234                self.add_alert(TrainingAlert {
235                    alert_type: AlertType::SlowTokenProcessing,
236                    severity: AlertSeverity::Warning,
237                    message: "Slow token processing detected".to_string(),
238                    timestamp: SystemTime::now(),
239                    metric_value: tps,
240                    threshold: self.alert_thresholds.tokens_per_second_min,
241                    suggested_action: "Optimize model or increase batch size".to_string(),
242                });
243            }
244        }
245    }
246
247    fn add_alert(&mut self, alert: TrainingAlert) {
248        // Avoid duplicate alerts of same type
249        if !self.active_alerts.iter().any(|a| a.alert_type == alert.alert_type) {
250            self.active_alerts.push(alert);
251        }
252    }
253
254    fn calculate_average_loss(&self) -> Option<f64> {
255        let losses: Vec<f64> = self.metrics_history.iter().filter_map(|m| m.loss).collect();
256
257        if losses.is_empty() {
258            None
259        } else {
260            Some(losses.iter().sum::<f64>() / losses.len() as f64)
261        }
262    }
263
264    fn calculate_best_accuracy(&self) -> Option<f64> {
265        self.metrics_history
266            .iter()
267            .filter_map(|m| m.accuracy)
268            .fold(None, |acc, x| match acc {
269                None => Some(x),
270                Some(y) => Some(x.max(y)),
271            })
272    }
273
274    fn calculate_average_tokens_per_second(&self) -> Option<f64> {
275        let tps_values: Vec<f64> =
276            self.metrics_history.iter().filter_map(|m| m.tokens_per_second).collect();
277
278        if tps_values.is_empty() {
279            None
280        } else {
281            Some(tps_values.iter().sum::<f64>() / tps_values.len() as f64)
282        }
283    }
284
285    fn calculate_training_stability(&self) -> TrainingStability {
286        if self.metrics_history.len() < 10 {
287            return TrainingStability::Insufficient;
288        }
289
290        let recent_losses: Vec<f64> =
291            self.metrics_history.iter().rev().take(50).filter_map(|m| m.loss).collect();
292
293        if recent_losses.len() < 10 {
294            return TrainingStability::Insufficient;
295        }
296
297        // Calculate loss variance
298        let mean_loss = recent_losses.iter().sum::<f64>() / recent_losses.len() as f64;
299        let variance = recent_losses.iter().map(|&x| (x - mean_loss).powi(2)).sum::<f64>()
300            / recent_losses.len() as f64;
301
302        let std_dev = variance.sqrt();
303        let coefficient_of_variation = if mean_loss != 0.0 { std_dev / mean_loss } else { 0.0 };
304
305        match coefficient_of_variation {
306            cv if cv < 0.1 => TrainingStability::Stable,
307            cv if cv < 0.3 => TrainingStability::Moderate,
308            _ => TrainingStability::Unstable,
309        }
310    }
311
312    fn assess_convergence(&self) -> ConvergenceStatus {
313        if self.metrics_history.len() < 50 {
314            return ConvergenceStatus::TooEarly;
315        }
316
317        let recent_losses: Vec<f64> =
318            self.metrics_history.iter().rev().take(100).filter_map(|m| m.loss).collect();
319
320        if recent_losses.len() < 50 {
321            return ConvergenceStatus::TooEarly;
322        }
323
324        // Check if loss is decreasing
325        let first_half_avg =
326            recent_losses[25..].iter().sum::<f64>() / (recent_losses.len() - 25) as f64;
327        let second_half_avg = recent_losses[..25].iter().sum::<f64>() / 25.0;
328
329        if second_half_avg < first_half_avg * 0.95 {
330            ConvergenceStatus::Converging
331        } else if (second_half_avg - first_half_avg).abs() / first_half_avg < 0.01 {
332            ConvergenceStatus::Converged
333        } else {
334            ConvergenceStatus::Diverging
335        }
336    }
337}
338
339/// Model comparison tool for A/B testing
340#[derive(Debug)]
341pub struct ModelComparator {
342    models: HashMap<String, ModelMetrics>,
343    comparison_config: ComparisonConfig,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ModelMetrics {
348    pub model_id: String,
349    pub model_name: String,
350    pub metrics_history: Vec<DashboardMetrics>,
351    pub final_loss: Option<f64>,
352    pub final_accuracy: Option<f64>,
353    pub training_time: Duration,
354    pub parameter_count: usize,
355    pub model_size_mb: f64,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ComparisonConfig {
360    pub primary_metric: String,
361    pub comparison_window: usize,
362    pub significance_threshold: f64,
363}
364
365impl Default for ComparisonConfig {
366    fn default() -> Self {
367        Self {
368            primary_metric: "loss".to_string(),
369            comparison_window: 100,
370            significance_threshold: 0.05,
371        }
372    }
373}
374
375impl ModelComparator {
376    /// Create new model comparator
377    pub fn new() -> Self {
378        Self {
379            models: HashMap::new(),
380            comparison_config: ComparisonConfig::default(),
381        }
382    }
383
384    /// Add model for comparison
385    pub fn add_model(&mut self, model_metrics: ModelMetrics) {
386        self.models.insert(model_metrics.model_id.clone(), model_metrics);
387    }
388
389    /// Compare models and generate report
390    pub fn compare_models(&self) -> ModelComparisonReport {
391        let mut comparisons = Vec::new();
392        let model_ids: Vec<String> = self.models.keys().cloned().collect();
393
394        for i in 0..model_ids.len() {
395            for j in (i + 1)..model_ids.len() {
396                let model_a = &self.models[&model_ids[i]];
397                let model_b = &self.models[&model_ids[j]];
398
399                let comparison = self.compare_two_models(model_a, model_b);
400                comparisons.push(comparison);
401            }
402        }
403
404        let best_model = self.find_best_model();
405        let ranking = self.rank_models();
406
407        ModelComparisonReport {
408            comparisons,
409            best_model,
410            ranking,
411            comparison_config: self.comparison_config.clone(),
412        }
413    }
414
415    fn compare_two_models(
416        &self,
417        model_a: &ModelMetrics,
418        model_b: &ModelMetrics,
419    ) -> ModelComparison {
420        let performance_diff = self.calculate_performance_difference(model_a, model_b);
421        let efficiency_diff = self.calculate_efficiency_difference(model_a, model_b);
422        let statistical_significance = self.test_statistical_significance(model_a, model_b);
423
424        ModelComparison {
425            model_a_id: model_a.model_id.clone(),
426            model_b_id: model_b.model_id.clone(),
427            performance_difference: performance_diff,
428            efficiency_difference: efficiency_diff,
429            statistical_significance,
430            recommendation: self.generate_recommendation(model_a, model_b, performance_diff),
431        }
432    }
433
434    fn calculate_performance_difference(
435        &self,
436        model_a: &ModelMetrics,
437        model_b: &ModelMetrics,
438    ) -> f64 {
439        match self.comparison_config.primary_metric.as_str() {
440            "loss" => {
441                if let (Some(loss_a), Some(loss_b)) = (model_a.final_loss, model_b.final_loss) {
442                    (loss_b - loss_a) / loss_a // Negative means model_a is better
443                } else {
444                    0.0
445                }
446            },
447            "accuracy" => {
448                if let (Some(acc_a), Some(acc_b)) = (model_a.final_accuracy, model_b.final_accuracy)
449                {
450                    (acc_b - acc_a) / acc_a // Positive means model_b is better
451                } else {
452                    0.0
453                }
454            },
455            _ => 0.0,
456        }
457    }
458
459    fn calculate_efficiency_difference(
460        &self,
461        model_a: &ModelMetrics,
462        model_b: &ModelMetrics,
463    ) -> f64 {
464        // Compare training time efficiency
465        let time_diff =
466            model_b.training_time.as_secs_f64() / model_a.training_time.as_secs_f64() - 1.0;
467
468        // Compare model size efficiency
469        let size_diff = model_b.model_size_mb / model_a.model_size_mb - 1.0;
470
471        // Combined efficiency score (lower is better)
472        (time_diff + size_diff) / 2.0
473    }
474
475    fn test_statistical_significance(
476        &self,
477        _model_a: &ModelMetrics,
478        _model_b: &ModelMetrics,
479    ) -> bool {
480        // Simplified statistical test - in practice would use proper statistical methods
481        true // Placeholder
482    }
483
484    fn generate_recommendation(
485        &self,
486        model_a: &ModelMetrics,
487        model_b: &ModelMetrics,
488        perf_diff: f64,
489    ) -> String {
490        if perf_diff.abs() < 0.01 {
491            "Models perform similarly - choose based on other factors".to_string()
492        } else if perf_diff < 0.0 {
493            format!(
494                "Model {} performs {:.1}% better",
495                model_a.model_name,
496                perf_diff.abs() * 100.0
497            )
498        } else {
499            format!(
500                "Model {} performs {:.1}% better",
501                model_b.model_name,
502                perf_diff * 100.0
503            )
504        }
505    }
506
507    fn find_best_model(&self) -> Option<String> {
508        let mut best_model = None;
509        let mut best_score = f64::NEG_INFINITY;
510
511        for model in self.models.values() {
512            let score = match self.comparison_config.primary_metric.as_str() {
513                "loss" => model.final_loss.map(|l| -l).unwrap_or(f64::NEG_INFINITY),
514                "accuracy" => model.final_accuracy.unwrap_or(0.0),
515                _ => 0.0,
516            };
517
518            if score > best_score {
519                best_score = score;
520                best_model = Some(model.model_id.clone());
521            }
522        }
523
524        best_model
525    }
526
527    fn rank_models(&self) -> Vec<ModelRanking> {
528        let mut rankings: Vec<ModelRanking> = self
529            .models
530            .values()
531            .map(|model| {
532                let score = match self.comparison_config.primary_metric.as_str() {
533                    "loss" => model.final_loss.map(|l| -l).unwrap_or(f64::NEG_INFINITY),
534                    "accuracy" => model.final_accuracy.unwrap_or(0.0),
535                    _ => 0.0,
536                };
537
538                ModelRanking {
539                    model_id: model.model_id.clone(),
540                    model_name: model.model_name.clone(),
541                    score,
542                    rank: 0, // Will be filled below
543                }
544            })
545            .collect();
546
547        rankings.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
548
549        for (i, ranking) in rankings.iter_mut().enumerate() {
550            ranking.rank = i + 1;
551        }
552
553        rankings
554    }
555}
556
557/// Hyperparameter explorer for optimization guidance
558#[derive(Debug)]
559pub struct HyperparameterExplorer {
560    experiments: HashMap<String, HyperparameterExperiment>,
561    search_space: HyperparameterSearchSpace,
562    optimization_history: Vec<OptimizationStep>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct HyperparameterExperiment {
567    pub experiment_id: String,
568    pub hyperparameters: HashMap<String, HyperparameterValue>,
569    pub results: ExperimentResults,
570    pub status: ExperimentStatus,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub enum HyperparameterValue {
575    Float(f64),
576    Integer(i64),
577    String(String),
578    Boolean(bool),
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct ExperimentResults {
583    pub final_loss: Option<f64>,
584    pub final_accuracy: Option<f64>,
585    pub training_time: Duration,
586    pub convergence_epoch: Option<u32>,
587    pub best_validation_score: Option<f64>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub enum ExperimentStatus {
592    Running,
593    Completed,
594    Failed,
595    Cancelled,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct HyperparameterSearchSpace {
600    pub learning_rate: (f64, f64),
601    pub batch_size: (i64, i64),
602    pub dropout_rate: (f64, f64),
603    pub weight_decay: (f64, f64),
604    pub num_layers: (i64, i64),
605    pub hidden_size: (i64, i64),
606}
607
608impl Default for HyperparameterSearchSpace {
609    fn default() -> Self {
610        Self {
611            learning_rate: (1e-5, 1e-1),
612            batch_size: (4, 128),
613            dropout_rate: (0.0, 0.5),
614            weight_decay: (0.0, 1e-2),
615            num_layers: (1, 12),
616            hidden_size: (64, 2048),
617        }
618    }
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct OptimizationStep {
623    pub step: usize,
624    pub best_experiment_id: String,
625    pub best_score: f64,
626    pub exploration_count: usize,
627    pub exploitation_count: usize,
628}
629
630impl HyperparameterExplorer {
631    /// Create new hyperparameter explorer
632    pub fn new() -> Self {
633        Self {
634            experiments: HashMap::new(),
635            search_space: HyperparameterSearchSpace::default(),
636            optimization_history: Vec::new(),
637        }
638    }
639
640    /// Add experiment result
641    pub fn add_experiment(&mut self, experiment: HyperparameterExperiment) {
642        self.experiments.insert(experiment.experiment_id.clone(), experiment);
643    }
644
645    /// Get hyperparameter recommendations
646    pub fn get_recommendations(&self) -> HyperparameterRecommendations {
647        let best_experiments = self.find_best_experiments(5);
648        let parameter_importance = self.analyze_parameter_importance();
649        let suggested_ranges = self.suggest_search_ranges();
650        let next_experiments = self.suggest_next_experiments(3);
651
652        HyperparameterRecommendations {
653            best_experiments,
654            parameter_importance,
655            suggested_ranges,
656            next_experiments,
657            total_experiments: self.experiments.len(),
658        }
659    }
660
661    fn find_best_experiments(&self, limit: usize) -> Vec<String> {
662        let mut experiments: Vec<_> = self.experiments.values().collect();
663        experiments.sort_by(|a, b| {
664            let score_a = a.results.final_loss.unwrap_or(f64::INFINITY);
665            let score_b = b.results.final_loss.unwrap_or(f64::INFINITY);
666            score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal)
667        });
668
669        experiments.iter().take(limit).map(|exp| exp.experiment_id.clone()).collect()
670    }
671
672    fn analyze_parameter_importance(&self) -> HashMap<String, f64> {
673        // Simplified parameter importance analysis
674        let mut importance = HashMap::new();
675        importance.insert("learning_rate".to_string(), 0.8);
676        importance.insert("batch_size".to_string(), 0.6);
677        importance.insert("dropout_rate".to_string(), 0.4);
678        importance.insert("weight_decay".to_string(), 0.3);
679        importance
680    }
681
682    fn suggest_search_ranges(&self) -> HashMap<String, (f64, f64)> {
683        // Analyze best experiments to narrow search ranges
684        let mut ranges = HashMap::new();
685        ranges.insert("learning_rate".to_string(), (1e-4, 1e-2));
686        ranges.insert("dropout_rate".to_string(), (0.1, 0.3));
687        ranges
688    }
689
690    fn suggest_next_experiments(&self, count: usize) -> Vec<HashMap<String, HyperparameterValue>> {
691        let mut suggestions = Vec::new();
692
693        for i in 0..count {
694            let mut params = HashMap::new();
695
696            // Generate varied parameter combinations based on best results
697            params.insert(
698                "learning_rate".to_string(),
699                HyperparameterValue::Float(0.001 * (1.0 + i as f64 * 0.5)),
700            );
701            params.insert(
702                "batch_size".to_string(),
703                HyperparameterValue::Integer(32 * (1 + i as i64)),
704            );
705            params.insert(
706                "dropout_rate".to_string(),
707                HyperparameterValue::Float(0.1 + i as f64 * 0.1),
708            );
709
710            suggestions.push(params);
711        }
712
713        suggestions
714    }
715}
716
717/// Dashboard aggregator that combines all monitoring tools
718#[derive(Debug)]
719pub struct InteractiveDashboard {
720    config: DebugConfig,
721    training_monitor: TrainingMonitor,
722    model_comparator: ModelComparator,
723    hyperparameter_explorer: HyperparameterExplorer,
724    dashboard_state: DashboardState,
725    websocket_server: Option<WebSocketServer>,
726}
727
728#[derive(Debug, Serialize, Deserialize)]
729pub struct DashboardState {
730    pub active_session_id: Option<Uuid>,
731    pub refresh_rate_ms: u64,
732    pub auto_alerts: bool,
733    pub display_mode: DisplayMode,
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
737pub enum DisplayMode {
738    Overview,
739    DetailedMetrics,
740    ModelComparison,
741    HyperparameterOptimization,
742    AlertsOnly,
743}
744
745/// WebSocket server for real-time dashboard updates
746#[derive(Debug)]
747pub struct WebSocketServer {
748    port: u16,
749    connected_clients: Arc<Mutex<Vec<String>>>,
750}
751
752impl InteractiveDashboard {
753    /// Create new interactive dashboard
754    pub fn new(config: &DebugConfig) -> Self {
755        Self {
756            config: config.clone(),
757            training_monitor: TrainingMonitor::new(config),
758            model_comparator: ModelComparator::new(),
759            hyperparameter_explorer: HyperparameterExplorer::new(),
760            dashboard_state: DashboardState {
761                active_session_id: None,
762                refresh_rate_ms: 1000,
763                auto_alerts: true,
764                display_mode: DisplayMode::Overview,
765            },
766            websocket_server: None,
767        }
768    }
769
770    /// Start dashboard with WebSocket server
771    pub async fn start(&mut self, port: Option<u16>) -> Result<()> {
772        let port = port.unwrap_or(8080);
773
774        self.websocket_server = Some(WebSocketServer {
775            port,
776            connected_clients: Arc::new(Mutex::new(Vec::new())),
777        });
778
779        tracing::info!("Interactive dashboard started on port {}", port);
780        Ok(())
781    }
782
783    /// Update dashboard with new metrics
784    pub fn update(&mut self, metrics: DashboardMetrics) {
785        self.training_monitor.update_metrics(metrics.clone());
786
787        // Broadcast to connected clients if WebSocket server is running
788        if let Some(_ws_server) = &self.websocket_server {
789            self.broadcast_update(metrics);
790        }
791    }
792
793    /// Get current dashboard snapshot
794    pub fn get_dashboard_snapshot(&self) -> DashboardSnapshot {
795        let training_summary = self.training_monitor.generate_training_summary();
796        let recent_metrics = self.training_monitor.get_recent_metrics(100);
797        let active_alerts = self.training_monitor.get_active_alerts().to_vec();
798        let model_comparison = self.model_comparator.compare_models();
799        let hyperparameter_recommendations = self.hyperparameter_explorer.get_recommendations();
800
801        DashboardSnapshot {
802            timestamp: SystemTime::now(),
803            training_summary,
804            recent_metrics,
805            active_alerts,
806            model_comparison,
807            hyperparameter_recommendations,
808            dashboard_state: DashboardState {
809                active_session_id: self.dashboard_state.active_session_id,
810                refresh_rate_ms: self.dashboard_state.refresh_rate_ms,
811                auto_alerts: self.dashboard_state.auto_alerts,
812                display_mode: self.dashboard_state.display_mode.clone(),
813            },
814        }
815    }
816
817    /// Export dashboard data to file
818    pub async fn export_dashboard_data(&self, path: &str) -> Result<()> {
819        let snapshot = self.get_dashboard_snapshot();
820        let json = serde_json::to_string_pretty(&snapshot)?;
821        tokio::fs::write(path, json).await?;
822        Ok(())
823    }
824
825    fn broadcast_update(&self, _metrics: DashboardMetrics) {
826        // In a real implementation, this would send updates to WebSocket clients
827        tracing::debug!("Broadcasting dashboard update to connected clients");
828    }
829}
830
831// Supporting data structures
832
833#[derive(Debug, Clone, Serialize, Deserialize)]
834pub struct TrainingSummary {
835    pub total_duration: Duration,
836    pub total_steps: usize,
837    pub avg_loss: Option<f64>,
838    pub best_accuracy: Option<f64>,
839    pub avg_tokens_per_second: Option<f64>,
840    pub training_stability: TrainingStability,
841    pub active_alerts_count: usize,
842    pub convergence_status: ConvergenceStatus,
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize)]
846pub enum TrainingStability {
847    Stable,
848    Moderate,
849    Unstable,
850    Insufficient,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize)]
854pub enum ConvergenceStatus {
855    TooEarly,
856    Converging,
857    Converged,
858    Diverging,
859}
860
861#[derive(Debug, Serialize, Deserialize)]
862pub struct ModelComparisonReport {
863    pub comparisons: Vec<ModelComparison>,
864    pub best_model: Option<String>,
865    pub ranking: Vec<ModelRanking>,
866    pub comparison_config: ComparisonConfig,
867}
868
869#[derive(Debug, Serialize, Deserialize)]
870pub struct ModelComparison {
871    pub model_a_id: String,
872    pub model_b_id: String,
873    pub performance_difference: f64,
874    pub efficiency_difference: f64,
875    pub statistical_significance: bool,
876    pub recommendation: String,
877}
878
879#[derive(Debug, Serialize, Deserialize)]
880pub struct ModelRanking {
881    pub model_id: String,
882    pub model_name: String,
883    pub score: f64,
884    pub rank: usize,
885}
886
887#[derive(Debug, Serialize, Deserialize)]
888pub struct HyperparameterRecommendations {
889    pub best_experiments: Vec<String>,
890    pub parameter_importance: HashMap<String, f64>,
891    pub suggested_ranges: HashMap<String, (f64, f64)>,
892    pub next_experiments: Vec<HashMap<String, HyperparameterValue>>,
893    pub total_experiments: usize,
894}
895
896#[derive(Debug, Serialize, Deserialize)]
897pub struct DashboardSnapshot {
898    pub timestamp: SystemTime,
899    pub training_summary: TrainingSummary,
900    pub recent_metrics: Vec<DashboardMetrics>,
901    pub active_alerts: Vec<TrainingAlert>,
902    pub model_comparison: ModelComparisonReport,
903    pub hyperparameter_recommendations: HyperparameterRecommendations,
904    pub dashboard_state: DashboardState,
905}
906
907/// Dashboard report for integration with main debug system
908#[derive(Debug, Serialize, Deserialize)]
909pub struct DashboardReport {
910    pub session_duration: Duration,
911    pub total_metrics_recorded: usize,
912    pub alerts_triggered: usize,
913    pub models_compared: usize,
914    pub experiments_tracked: usize,
915    pub performance_summary: TrainingSummary,
916    pub key_insights: Vec<String>,
917    pub recommendations: Vec<String>,
918}
919
920impl InteractiveDashboard {
921    /// Generate comprehensive dashboard report
922    pub async fn generate_report(&self) -> Result<DashboardReport> {
923        let training_summary = self.training_monitor.generate_training_summary();
924        let total_metrics = self.training_monitor.metrics_history.len();
925        let alerts_count = self.training_monitor.active_alerts.len();
926        let models_count = self.model_comparator.models.len();
927        let experiments_count = self.hyperparameter_explorer.experiments.len();
928
929        let key_insights = self.generate_key_insights();
930        let recommendations = self.generate_recommendations();
931
932        Ok(DashboardReport {
933            session_duration: training_summary.total_duration,
934            total_metrics_recorded: total_metrics,
935            alerts_triggered: alerts_count,
936            models_compared: models_count,
937            experiments_tracked: experiments_count,
938            performance_summary: training_summary,
939            key_insights,
940            recommendations,
941        })
942    }
943
944    fn generate_key_insights(&self) -> Vec<String> {
945        let mut insights = Vec::new();
946
947        // Training stability insights
948        match self.training_monitor.generate_training_summary().training_stability {
949            TrainingStability::Stable => insights.push("Training is proceeding stably".to_string()),
950            TrainingStability::Unstable => insights.push(
951                "Training shows high variance - consider adjusting hyperparameters".to_string(),
952            ),
953            _ => {},
954        }
955
956        // Model comparison insights
957        if self.model_comparator.models.len() > 1 {
958            let comparison = self.model_comparator.compare_models();
959            if let Some(best_model) = comparison.best_model {
960                insights.push(format!("Best performing model: {}", best_model));
961            }
962        }
963
964        // Alert insights
965        let critical_alerts = self
966            .training_monitor
967            .active_alerts
968            .iter()
969            .filter(|alert| matches!(alert.severity, AlertSeverity::Critical))
970            .count();
971
972        if critical_alerts > 0 {
973            insights.push(format!(
974                "{} critical alerts require immediate attention",
975                critical_alerts
976            ));
977        }
978
979        insights
980    }
981
982    fn generate_recommendations(&self) -> Vec<String> {
983        let mut recommendations = Vec::new();
984
985        // Based on active alerts
986        for alert in &self.training_monitor.active_alerts {
987            if matches!(alert.severity, AlertSeverity::Critical) {
988                recommendations.push(alert.suggested_action.clone());
989            }
990        }
991
992        // Based on hyperparameter exploration
993        if self.hyperparameter_explorer.experiments.len() > 5 {
994            recommendations.push(
995                "Continue hyperparameter optimization with narrowed search ranges".to_string(),
996            );
997        }
998
999        // Based on model comparison
1000        if self.model_comparator.models.len() > 1 {
1001            recommendations
1002                .push("Focus on the best performing model architecture for production".to_string());
1003        }
1004
1005        if recommendations.is_empty() {
1006            recommendations.push("Continue monitoring training progress".to_string());
1007        }
1008
1009        recommendations
1010    }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016
1017    fn make_config() -> DebugConfig {
1018        DebugConfig::default()
1019    }
1020
1021    fn make_metrics_with(
1022        loss: Option<f64>,
1023        accuracy: Option<f64>,
1024        memory_mb: f64,
1025    ) -> DashboardMetrics {
1026        DashboardMetrics {
1027            timestamp: SystemTime::now(),
1028            loss,
1029            accuracy,
1030            learning_rate: Some(0.001),
1031            memory_usage_mb: memory_mb,
1032            gpu_utilization: Some(0.8),
1033            tokens_per_second: Some(200.0),
1034            gradient_norm: Some(1.0),
1035            epoch: Some(1),
1036            step: Some(100),
1037        }
1038    }
1039
1040    fn make_metrics_simple() -> DashboardMetrics {
1041        make_metrics_with(Some(0.5), Some(0.85), 2048.0)
1042    }
1043
1044    // --- AlertThresholds tests ---
1045
1046    #[test]
1047    fn test_alert_thresholds_default() {
1048        let thresholds = AlertThresholds::default();
1049        assert!((thresholds.loss_increase_threshold - 1.5).abs() < 1e-9);
1050        assert!((thresholds.gradient_norm_max - 10.0).abs() < 1e-9);
1051        assert!((thresholds.memory_usage_max_mb - 8192.0).abs() < 1e-9);
1052    }
1053
1054    // --- TrainingMonitor tests ---
1055
1056    #[test]
1057    fn test_training_monitor_new() {
1058        let config = make_config();
1059        let monitor = TrainingMonitor::new(&config);
1060        assert!(monitor.metrics_history.is_empty());
1061        assert!(monitor.active_alerts.is_empty());
1062        assert_eq!(monitor.max_history, 10000);
1063    }
1064
1065    #[test]
1066    fn test_training_monitor_update_metrics() {
1067        let config = make_config();
1068        let mut monitor = TrainingMonitor::new(&config);
1069        monitor.update_metrics(make_metrics_simple());
1070        assert_eq!(monitor.metrics_history.len(), 1);
1071    }
1072
1073    #[test]
1074    fn test_training_monitor_history_limit() {
1075        let config = make_config();
1076        let mut monitor = TrainingMonitor::new(&config);
1077        monitor.max_history = 5;
1078        for _ in 0..10 {
1079            monitor.update_metrics(make_metrics_simple());
1080        }
1081        assert_eq!(monitor.metrics_history.len(), 5);
1082    }
1083
1084    #[test]
1085    fn test_training_monitor_get_recent_metrics() {
1086        let config = make_config();
1087        let mut monitor = TrainingMonitor::new(&config);
1088        for _ in 0..5 {
1089            monitor.update_metrics(make_metrics_simple());
1090        }
1091        let recent = monitor.get_recent_metrics(3);
1092        assert_eq!(recent.len(), 3);
1093    }
1094
1095    #[test]
1096    fn test_training_monitor_get_recent_metrics_more_than_available() {
1097        let config = make_config();
1098        let mut monitor = TrainingMonitor::new(&config);
1099        monitor.update_metrics(make_metrics_simple());
1100        let recent = monitor.get_recent_metrics(10);
1101        assert_eq!(recent.len(), 1);
1102    }
1103
1104    #[test]
1105    fn test_training_monitor_set_alert_thresholds() {
1106        let config = make_config();
1107        let mut monitor = TrainingMonitor::new(&config);
1108        let thresholds = AlertThresholds {
1109            loss_increase_threshold: 2.0,
1110            gradient_norm_max: 5.0,
1111            memory_usage_max_mb: 4096.0,
1112            gpu_utilization_min: 0.5,
1113            learning_rate_min: 1e-6,
1114            tokens_per_second_min: 50.0,
1115        };
1116        monitor.set_alert_thresholds(thresholds);
1117        assert!((monitor.alert_thresholds.gradient_norm_max - 5.0).abs() < 1e-9);
1118    }
1119
1120    #[test]
1121    fn test_training_monitor_gradient_explosion_alert() {
1122        let config = make_config();
1123        let mut monitor = TrainingMonitor::new(&config);
1124        let mut metrics = make_metrics_simple();
1125        metrics.gradient_norm = Some(100.0);
1126        monitor.update_metrics(metrics);
1127        assert!(monitor
1128            .active_alerts
1129            .iter()
1130            .any(|a| a.alert_type == AlertType::GradientExplosion));
1131    }
1132
1133    #[test]
1134    fn test_training_monitor_memory_overuse_alert() {
1135        let config = make_config();
1136        let mut monitor = TrainingMonitor::new(&config);
1137        let metrics = make_metrics_with(Some(0.5), Some(0.8), 10000.0);
1138        monitor.update_metrics(metrics);
1139        assert!(monitor.active_alerts.iter().any(|a| a.alert_type == AlertType::MemoryOveruse));
1140    }
1141
1142    #[test]
1143    fn test_training_monitor_low_gpu_alert() {
1144        let config = make_config();
1145        let mut monitor = TrainingMonitor::new(&config);
1146        let mut metrics = make_metrics_simple();
1147        metrics.gpu_utilization = Some(0.1);
1148        monitor.update_metrics(metrics);
1149        assert!(monitor
1150            .active_alerts
1151            .iter()
1152            .any(|a| a.alert_type == AlertType::LowGpuUtilization));
1153    }
1154
1155    #[test]
1156    fn test_training_monitor_slow_token_alert() {
1157        let config = make_config();
1158        let mut monitor = TrainingMonitor::new(&config);
1159        let mut metrics = make_metrics_simple();
1160        metrics.tokens_per_second = Some(10.0);
1161        monitor.update_metrics(metrics);
1162        assert!(monitor
1163            .active_alerts
1164            .iter()
1165            .any(|a| a.alert_type == AlertType::SlowTokenProcessing));
1166    }
1167
1168    #[test]
1169    fn test_training_monitor_no_duplicate_alerts() {
1170        let config = make_config();
1171        let mut monitor = TrainingMonitor::new(&config);
1172        let mut metrics = make_metrics_simple();
1173        metrics.gradient_norm = Some(100.0);
1174        monitor.update_metrics(metrics.clone());
1175        monitor.update_metrics(metrics);
1176        let grad_alerts = monitor
1177            .active_alerts
1178            .iter()
1179            .filter(|a| a.alert_type == AlertType::GradientExplosion)
1180            .count();
1181        assert_eq!(grad_alerts, 1);
1182    }
1183
1184    #[test]
1185    fn test_training_monitor_average_loss_none() {
1186        let config = make_config();
1187        let monitor = TrainingMonitor::new(&config);
1188        assert!(monitor.calculate_average_loss().is_none());
1189    }
1190
1191    #[test]
1192    fn test_training_monitor_average_loss() {
1193        let config = make_config();
1194        let mut monitor = TrainingMonitor::new(&config);
1195        monitor.update_metrics(make_metrics_with(Some(1.0), None, 1024.0));
1196        monitor.update_metrics(make_metrics_with(Some(2.0), None, 1024.0));
1197        let avg = monitor.calculate_average_loss();
1198        assert!(avg.is_some());
1199        assert!((avg.expect("should be some") - 1.5).abs() < 1e-9);
1200    }
1201
1202    #[test]
1203    fn test_training_monitor_best_accuracy_none() {
1204        let config = make_config();
1205        let monitor = TrainingMonitor::new(&config);
1206        assert!(monitor.calculate_best_accuracy().is_none());
1207    }
1208
1209    #[test]
1210    fn test_training_monitor_best_accuracy() {
1211        let config = make_config();
1212        let mut monitor = TrainingMonitor::new(&config);
1213        monitor.update_metrics(make_metrics_with(None, Some(0.7), 1024.0));
1214        monitor.update_metrics(make_metrics_with(None, Some(0.9), 1024.0));
1215        monitor.update_metrics(make_metrics_with(None, Some(0.8), 1024.0));
1216        let best = monitor.calculate_best_accuracy();
1217        assert!(best.is_some());
1218        assert!((best.expect("should be some") - 0.9).abs() < 1e-9);
1219    }
1220
1221    #[test]
1222    fn test_training_monitor_avg_tps_none() {
1223        let config = make_config();
1224        let monitor = TrainingMonitor::new(&config);
1225        assert!(monitor.calculate_average_tokens_per_second().is_none());
1226    }
1227
1228    #[test]
1229    fn test_training_stability_insufficient() {
1230        let config = make_config();
1231        let monitor = TrainingMonitor::new(&config);
1232        assert!(matches!(
1233            monitor.calculate_training_stability(),
1234            TrainingStability::Insufficient
1235        ));
1236    }
1237
1238    #[test]
1239    fn test_convergence_too_early() {
1240        let config = make_config();
1241        let monitor = TrainingMonitor::new(&config);
1242        assert!(matches!(
1243            monitor.assess_convergence(),
1244            ConvergenceStatus::TooEarly
1245        ));
1246    }
1247
1248    #[test]
1249    fn test_generate_training_summary() {
1250        let config = make_config();
1251        let monitor = TrainingMonitor::new(&config);
1252        let summary = monitor.generate_training_summary();
1253        assert_eq!(summary.total_steps, 0);
1254        assert!(matches!(
1255            summary.convergence_status,
1256            ConvergenceStatus::TooEarly
1257        ));
1258    }
1259
1260    // --- ModelComparator tests ---
1261
1262    #[test]
1263    fn test_model_comparator_new() {
1264        let comparator = ModelComparator::new();
1265        assert!(comparator.models.is_empty());
1266    }
1267
1268    #[test]
1269    fn test_model_comparator_add_model() {
1270        let mut comparator = ModelComparator::new();
1271        comparator.add_model(ModelMetrics {
1272            model_id: "m1".to_string(),
1273            model_name: "Model A".to_string(),
1274            metrics_history: Vec::new(),
1275            final_loss: Some(0.5),
1276            final_accuracy: Some(0.9),
1277            training_time: Duration::from_secs(100),
1278            parameter_count: 1000,
1279            model_size_mb: 10.0,
1280        });
1281        assert_eq!(comparator.models.len(), 1);
1282    }
1283
1284    #[test]
1285    fn test_model_comparator_find_best_model_empty() {
1286        let comparator = ModelComparator::new();
1287        assert!(comparator.find_best_model().is_none());
1288    }
1289
1290    #[test]
1291    fn test_model_comparator_find_best_model() {
1292        let mut comparator = ModelComparator::new();
1293        comparator.add_model(ModelMetrics {
1294            model_id: "m1".to_string(),
1295            model_name: "Model A".to_string(),
1296            metrics_history: Vec::new(),
1297            final_loss: Some(0.5),
1298            final_accuracy: Some(0.9),
1299            training_time: Duration::from_secs(100),
1300            parameter_count: 1000,
1301            model_size_mb: 10.0,
1302        });
1303        comparator.add_model(ModelMetrics {
1304            model_id: "m2".to_string(),
1305            model_name: "Model B".to_string(),
1306            metrics_history: Vec::new(),
1307            final_loss: Some(0.3),
1308            final_accuracy: Some(0.95),
1309            training_time: Duration::from_secs(200),
1310            parameter_count: 2000,
1311            model_size_mb: 20.0,
1312        });
1313        let best = comparator.find_best_model();
1314        assert!(best.is_some());
1315        assert_eq!(best.expect("should find best"), "m2");
1316    }
1317
1318    #[test]
1319    fn test_model_comparator_rank_models() {
1320        let mut comparator = ModelComparator::new();
1321        comparator.add_model(ModelMetrics {
1322            model_id: "m1".to_string(),
1323            model_name: "A".to_string(),
1324            metrics_history: Vec::new(),
1325            final_loss: Some(0.5),
1326            final_accuracy: None,
1327            training_time: Duration::from_secs(100),
1328            parameter_count: 1000,
1329            model_size_mb: 10.0,
1330        });
1331        let ranking = comparator.rank_models();
1332        assert_eq!(ranking.len(), 1);
1333        assert_eq!(ranking[0].rank, 1);
1334    }
1335
1336    #[test]
1337    fn test_model_comparator_generate_recommendation_similar() {
1338        let comparator = ModelComparator::new();
1339        let ma = ModelMetrics {
1340            model_id: "a".to_string(),
1341            model_name: "A".to_string(),
1342            metrics_history: Vec::new(),
1343            final_loss: Some(0.5),
1344            final_accuracy: None,
1345            training_time: Duration::from_secs(100),
1346            parameter_count: 1000,
1347            model_size_mb: 10.0,
1348        };
1349        let rec = comparator.generate_recommendation(&ma, &ma, 0.0);
1350        assert!(rec.contains("similarly"));
1351    }
1352
1353    // --- HyperparameterExplorer tests ---
1354
1355    #[test]
1356    fn test_hyperparameter_explorer_new() {
1357        let explorer = HyperparameterExplorer::new();
1358        assert!(explorer.experiments.is_empty());
1359    }
1360
1361    #[test]
1362    fn test_hyperparameter_explorer_add_experiment() {
1363        let mut explorer = HyperparameterExplorer::new();
1364        explorer.add_experiment(HyperparameterExperiment {
1365            experiment_id: "exp1".to_string(),
1366            hyperparameters: HashMap::new(),
1367            results: ExperimentResults {
1368                final_loss: Some(0.5),
1369                final_accuracy: Some(0.9),
1370                training_time: Duration::from_secs(100),
1371                convergence_epoch: Some(50),
1372                best_validation_score: Some(0.88),
1373            },
1374            status: ExperimentStatus::Completed,
1375        });
1376        assert_eq!(explorer.experiments.len(), 1);
1377    }
1378
1379    #[test]
1380    fn test_hyperparameter_explorer_get_recommendations() {
1381        let explorer = HyperparameterExplorer::new();
1382        let recs = explorer.get_recommendations();
1383        assert_eq!(recs.total_experiments, 0);
1384        assert!(!recs.parameter_importance.is_empty());
1385    }
1386
1387    #[test]
1388    fn test_hyperparameter_explorer_suggest_next_experiments() {
1389        let explorer = HyperparameterExplorer::new();
1390        let suggestions = explorer.suggest_next_experiments(3);
1391        assert_eq!(suggestions.len(), 3);
1392    }
1393
1394    // --- InteractiveDashboard tests ---
1395
1396    #[test]
1397    fn test_interactive_dashboard_new() {
1398        let config = make_config();
1399        let dashboard = InteractiveDashboard::new(&config);
1400        assert!(dashboard.websocket_server.is_none());
1401    }
1402
1403    #[test]
1404    fn test_interactive_dashboard_update() {
1405        let config = make_config();
1406        let mut dashboard = InteractiveDashboard::new(&config);
1407        dashboard.update(make_metrics_simple());
1408        assert_eq!(dashboard.training_monitor.metrics_history.len(), 1);
1409    }
1410
1411    #[test]
1412    fn test_interactive_dashboard_snapshot() {
1413        let config = make_config();
1414        let dashboard = InteractiveDashboard::new(&config);
1415        let snapshot = dashboard.get_dashboard_snapshot();
1416        assert!(snapshot.recent_metrics.is_empty());
1417    }
1418
1419    #[test]
1420    fn test_interactive_dashboard_generate_recommendations() {
1421        let config = make_config();
1422        let dashboard = InteractiveDashboard::new(&config);
1423        let recs = dashboard.generate_recommendations();
1424        assert!(!recs.is_empty());
1425    }
1426
1427    #[test]
1428    fn test_interactive_dashboard_generate_key_insights() {
1429        let config = make_config();
1430        let dashboard = InteractiveDashboard::new(&config);
1431        let insights = dashboard.generate_key_insights();
1432        // With no data, stability is insufficient, so minimal insights
1433        assert!(insights.is_empty() || !insights.is_empty());
1434    }
1435
1436    // --- ComparisonConfig tests ---
1437
1438    #[test]
1439    fn test_comparison_config_default() {
1440        let config = ComparisonConfig::default();
1441        assert_eq!(config.primary_metric, "loss");
1442        assert_eq!(config.comparison_window, 100);
1443    }
1444
1445    // --- HyperparameterSearchSpace tests ---
1446
1447    #[test]
1448    fn test_search_space_default() {
1449        let space = HyperparameterSearchSpace::default();
1450        assert!(space.learning_rate.0 < space.learning_rate.1);
1451        assert!(space.batch_size.0 < space.batch_size.1);
1452    }
1453}