1use anyhow::Result;
7use chrono::{DateTime, Utc};
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
11use statrs::distribution::{ContinuousCDF, StudentsT};
12use statrs::statistics::Statistics;
13use std::collections::HashMap;
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DifferentialDebuggingConfig {
19 pub enable_model_comparison: bool,
21 pub enable_ab_analysis: bool,
23 pub enable_version_diff: bool,
25 pub enable_regression_detection: bool,
27 pub enable_performance_delta: bool,
29 pub significance_threshold: f64,
31 pub max_comparison_models: usize,
33 pub regression_sensitivity: f64,
35 pub performance_delta_threshold: f64,
37}
38
39impl Default for DifferentialDebuggingConfig {
40 fn default() -> Self {
41 Self {
42 enable_model_comparison: true,
43 enable_ab_analysis: true,
44 enable_version_diff: true,
45 enable_regression_detection: true,
46 enable_performance_delta: true,
47 significance_threshold: 0.05,
48 max_comparison_models: 10,
49 regression_sensitivity: 0.8,
50 performance_delta_threshold: 5.0,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ModelSnapshot {
58 pub id: Uuid,
60 pub name: String,
62 pub timestamp: DateTime<Utc>,
64 pub version: String,
66 pub commit_hash: Option<String>,
68 pub metrics: ModelMetrics,
70 pub architecture: ArchitectureInfo,
72 pub training_config: TrainingConfig,
74 pub weights_summary: WeightsSummary,
76 pub metadata: HashMap<String, String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ModelMetrics {
83 pub train_accuracy: f64,
85 pub val_accuracy: f64,
87 pub test_accuracy: Option<f64>,
89 pub train_loss: f64,
91 pub val_loss: f64,
93 pub test_loss: Option<f64>,
95 pub inference_latency_ms: f64,
97 pub memory_usage_mb: f64,
99 pub model_size_mb: f64,
101 pub flops: u64,
103 pub training_time_s: f64,
105 pub custom_metrics: HashMap<String, f64>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ArchitectureInfo {
112 pub parameter_count: u64,
114 pub layer_count: u32,
116 pub depth: u32,
118 pub hidden_size: u32,
120 pub num_heads: Option<u32>,
122 pub ff_dim: Option<u32>,
124 pub vocab_size: Option<u32>,
126 pub max_seq_length: Option<u32>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TrainingConfig {
133 pub learning_rate: f64,
135 pub batch_size: u32,
137 pub epochs: u32,
139 pub optimizer: String,
141 pub lr_schedule: Option<String>,
143 pub regularization: HashMap<String, f64>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct WeightsSummary {
150 pub mean: f64,
152 pub std_dev: f64,
154 pub min: f64,
156 pub max: f64,
158 pub percentiles: HashMap<String, f64>,
160 pub zero_count: u64,
162 pub sparsity: f64,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ModelComparisonResult {
169 pub models: Vec<String>,
171 pub timestamp: DateTime<Utc>,
173 pub performance_comparison: PerformanceComparison,
175 pub architecture_diff: ArchitectureDiff,
177 pub statistical_analysis: StatisticalAnalysis,
179 pub summary: ComparisonSummary,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct PerformanceComparison {
186 pub accuracy_comparison: MetricComparison,
188 pub loss_comparison: MetricComparison,
190 pub latency_comparison: MetricComparison,
192 pub memory_comparison: MetricComparison,
194 pub size_comparison: MetricComparison,
196 pub custom_comparisons: HashMap<String, MetricComparison>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct MetricComparison {
203 pub values: HashMap<String, f64>,
205 pub best_model: String,
207 pub worst_model: String,
209 pub differences: HashMap<String, f64>,
211 pub significant_differences: HashMap<String, bool>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ArchitectureDiff {
218 pub parameter_diff: HashMap<String, i64>,
220 pub layer_diff: HashMap<String, i32>,
222 pub similarity_score: f64,
224 pub notable_differences: Vec<String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct StatisticalAnalysis {
231 pub p_values: HashMap<String, f64>,
233 pub effect_sizes: HashMap<String, f64>,
235 pub confidence_intervals: HashMap<String, (f64, f64)>,
237 pub significance_summary: HashMap<String, bool>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ComparisonSummary {
244 pub best_model: String,
246 pub rankings: HashMap<String, Vec<String>>,
248 pub key_findings: Vec<String>,
250 pub recommendations: Vec<String>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ABTestConfig {
257 pub name: String,
259 pub model_a: String,
261 pub model_b: String,
263 pub duration_hours: Option<u32>,
265 pub sample_size: u32,
267 pub tracked_metrics: Vec<String>,
269 pub min_effect_size: f64,
271 pub power: f64,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct ABTestResult {
278 pub config: ABTestConfig,
280 pub start_time: DateTime<Utc>,
282 pub end_time: Option<DateTime<Utc>>,
284 pub model_a_results: ABTestMetrics,
286 pub model_b_results: ABTestMetrics,
288 pub statistical_tests: HashMap<String, StatisticalTestResult>,
290 pub conclusion: ABTestConclusion,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct ABTestMetrics {
297 pub sample_size: u32,
299 pub metrics: HashMap<String, Vec<f64>>,
301 pub summary_stats: HashMap<String, SummaryStats>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct SummaryStats {
308 pub mean: f64,
309 pub std_dev: f64,
310 pub min: f64,
311 pub max: f64,
312 pub median: f64,
313 pub q25: f64,
314 pub q75: f64,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct StatisticalTestResult {
320 pub test_type: String,
322 pub statistic: f64,
324 pub p_value: f64,
326 pub effect_size: f64,
328 pub confidence_interval: (f64, f64),
330 pub is_significant: bool,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct ABTestConclusion {
337 pub winner: Option<String>,
339 pub confidence: f64,
341 pub practical_significance: bool,
343 pub recommendation: String,
345 pub summary: String,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct VersionDiff {
352 pub from_version: String,
354 pub to_version: String,
356 pub timestamp: DateTime<Utc>,
358 pub performance_delta: PerformanceDelta,
360 pub architecture_changes: Vec<ArchitectureChange>,
362 pub config_changes: Vec<ConfigChange>,
364 pub weight_changes: WeightChangesSummary,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct PerformanceDelta {
371 pub accuracy_delta: f64,
373 pub loss_delta: f64,
375 pub latency_delta: f64,
377 pub memory_delta: f64,
379 pub size_delta: f64,
381 pub training_time_delta: f64,
383 pub custom_deltas: HashMap<String, f64>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct ArchitectureChange {
390 pub change_type: String,
392 pub description: String,
394 pub impact: String,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct ConfigChange {
401 pub parameter: String,
403 pub old_value: String,
405 pub new_value: String,
407 pub impact: String,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct WeightChangesSummary {
414 pub avg_magnitude: f64,
416 pub max_change: f64,
418 pub significant_change_ratio: f64,
420 pub layer_changes: HashMap<String, f64>,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct RegressionDetectionResult {
427 pub timestamp: DateTime<Utc>,
429 pub regressions: Vec<Regression>,
431 pub improvements: Vec<Improvement>,
433 pub overall_assessment: RegressionAssessment,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct Regression {
440 pub metric: String,
442 pub current_value: f64,
444 pub previous_value: f64,
446 pub magnitude: f64,
448 pub severity: RegressionSeverity,
450 pub possible_causes: Vec<String>,
452 pub suggested_fixes: Vec<String>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct Improvement {
459 pub metric: String,
461 pub current_value: f64,
463 pub previous_value: f64,
465 pub magnitude: f64,
467 pub likely_causes: Vec<String>,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
473pub enum RegressionSeverity {
474 Critical,
475 Major,
476 Minor,
477 Negligible,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct RegressionAssessment {
483 pub health_score: f64,
485 pub critical_regressions: usize,
487 pub improvements: usize,
489 pub recommendation: String,
491}
492
493pub(crate) fn welch_t_test(
507 a: &[f64],
508 b: &[f64],
509 significance_threshold: f64,
510) -> Option<StatisticalTestResult> {
511 if a.len() < 2 || b.len() < 2 {
512 return None;
513 }
514
515 let a_mean = a.mean();
516 let b_mean = b.mean();
517 let a_var = a.variance();
518 let b_var = b.variance();
519 let n_a = a.len() as f64;
520 let n_b = b.len() as f64;
521
522 if (a_var <= 0.0 || !a_var.is_finite()) && (b_var <= 0.0 || !b_var.is_finite()) {
523 return None;
524 }
525
526 let se_a_sq = a_var / n_a;
527 let se_b_sq = b_var / n_b;
528 let standard_error = (se_a_sq + se_b_sq).sqrt();
529 if standard_error <= 0.0 || !standard_error.is_finite() {
530 return None;
531 }
532
533 let t_statistic = (a_mean - b_mean) / standard_error;
534
535 let degrees_of_freedom = (se_a_sq + se_b_sq).powi(2)
537 / (se_a_sq.powi(2) / (n_a - 1.0) + se_b_sq.powi(2) / (n_b - 1.0));
538 if !degrees_of_freedom.is_finite() || degrees_of_freedom <= 0.0 {
539 return None;
540 }
541
542 let t_dist = StudentsT::new(0.0, 1.0, degrees_of_freedom).ok()?;
543 let p_value = trustformers_core::statistics::student_t_two_sided_p_value(
549 t_statistic,
550 degrees_of_freedom,
551 )?;
552
553 let pooled_std = ((a_var + b_var) / 2.0).sqrt();
556 let effect_size = if pooled_std > 0.0 { (a_mean - b_mean) / pooled_std } else { 0.0 };
557
558 let critical_value = t_dist.inverse_cdf(0.975);
562 let margin_of_error = critical_value * standard_error;
563
564 Some(StatisticalTestResult {
565 test_type: "Welch's t-test".to_string(),
566 statistic: t_statistic,
567 p_value,
568 effect_size,
569 confidence_interval: (
570 (a_mean - b_mean) - margin_of_error,
571 (a_mean - b_mean) + margin_of_error,
572 ),
573 is_significant: p_value < significance_threshold,
574 })
575}
576
577#[derive(Debug)]
578pub struct DifferentialDebugger {
579 config: DifferentialDebuggingConfig,
580 model_snapshots: IndexMap<String, ModelSnapshot>,
581 comparison_history: Vec<ModelComparisonResult>,
582 ab_tests: Vec<ABTestResult>,
583 version_diffs: Vec<VersionDiff>,
584 regression_history: Vec<RegressionDetectionResult>,
585}
586
587impl DifferentialDebugger {
588 pub fn new(config: DifferentialDebuggingConfig) -> Self {
590 Self {
591 config,
592 model_snapshots: IndexMap::new(),
593 comparison_history: Vec::new(),
594 ab_tests: Vec::new(),
595 version_diffs: Vec::new(),
596 regression_history: Vec::new(),
597 }
598 }
599
600 pub fn add_model_snapshot(&mut self, snapshot: ModelSnapshot) -> Result<()> {
602 if self.model_snapshots.len() >= self.config.max_comparison_models {
603 self.model_snapshots.shift_remove_index(0);
605 }
606
607 self.model_snapshots.insert(snapshot.name.clone(), snapshot);
608 Ok(())
609 }
610
611 pub async fn compare_models(
613 &mut self,
614 model_names: Vec<String>,
615 ) -> Result<ModelComparisonResult> {
616 if !self.config.enable_model_comparison {
617 return Err(anyhow::anyhow!("Model comparison is disabled"));
618 }
619
620 if model_names.len() < 2 {
621 return Err(anyhow::anyhow!(
622 "At least two models are required for comparison"
623 ));
624 }
625
626 let models: Vec<&ModelSnapshot> = model_names
628 .iter()
629 .map(|name| {
630 self.model_snapshots
631 .get(name)
632 .ok_or_else(|| anyhow::anyhow!("Model '{}' not found", name))
633 })
634 .collect::<Result<Vec<_>>>()?;
635
636 let performance_comparison = self.compare_performance(&models)?;
638 let architecture_diff = self.analyze_architecture_differences(&models)?;
639 let statistical_analysis = self.perform_statistical_analysis(&models)?;
640 let summary = self.generate_comparison_summary(
641 &models,
642 &performance_comparison,
643 &statistical_analysis,
644 )?;
645
646 let result = ModelComparisonResult {
647 models: model_names,
648 timestamp: Utc::now(),
649 performance_comparison,
650 architecture_diff,
651 statistical_analysis,
652 summary,
653 };
654
655 self.comparison_history.push(result.clone());
656 Ok(result)
657 }
658
659 pub async fn run_ab_test(
661 &mut self,
662 config: ABTestConfig,
663 model_a_data: Vec<f64>,
664 model_b_data: Vec<f64>,
665 ) -> Result<ABTestResult> {
666 if !self.config.enable_ab_analysis {
667 return Err(anyhow::anyhow!("A/B analysis is disabled"));
668 }
669
670 let start_time = Utc::now();
671
672 let model_a_stats = self.calculate_summary_stats(&model_a_data);
674 let model_b_stats = self.calculate_summary_stats(&model_b_data);
675
676 let model_a_results = ABTestMetrics {
677 sample_size: model_a_data.len() as u32,
678 metrics: {
679 let mut metrics = HashMap::new();
680 metrics.insert("primary_metric".to_string(), model_a_data);
681 metrics
682 },
683 summary_stats: {
684 let mut stats = HashMap::new();
685 stats.insert("primary_metric".to_string(), model_a_stats);
686 stats
687 },
688 };
689
690 let model_b_results = ABTestMetrics {
691 sample_size: model_b_data.len() as u32,
692 metrics: {
693 let mut metrics = HashMap::new();
694 metrics.insert("primary_metric".to_string(), model_b_data);
695 metrics
696 },
697 summary_stats: {
698 let mut stats = HashMap::new();
699 stats.insert("primary_metric".to_string(), model_b_stats);
700 stats
701 },
702 };
703
704 let statistical_tests =
706 self.perform_ab_statistical_tests(&model_a_results, &model_b_results)?;
707
708 let conclusion = self.generate_ab_conclusion(
710 &config,
711 &model_a_results,
712 &model_b_results,
713 &statistical_tests,
714 )?;
715
716 let result = ABTestResult {
717 config,
718 start_time,
719 end_time: Some(Utc::now()),
720 model_a_results,
721 model_b_results,
722 statistical_tests,
723 conclusion,
724 };
725
726 self.ab_tests.push(result.clone());
727 Ok(result)
728 }
729
730 pub async fn track_version_diff(
732 &mut self,
733 from_model: &str,
734 to_model: &str,
735 ) -> Result<VersionDiff> {
736 if !self.config.enable_version_diff {
737 return Err(anyhow::anyhow!("Version diff tracking is disabled"));
738 }
739
740 let from_snapshot = self
741 .model_snapshots
742 .get(from_model)
743 .ok_or_else(|| anyhow::anyhow!("Model '{}' not found", from_model))?;
744 let to_snapshot = self
745 .model_snapshots
746 .get(to_model)
747 .ok_or_else(|| anyhow::anyhow!("Model '{}' not found", to_model))?;
748
749 let performance_delta = self.calculate_performance_delta(from_snapshot, to_snapshot)?;
750 let architecture_changes = self.detect_architecture_changes(from_snapshot, to_snapshot)?;
751 let config_changes = self.detect_config_changes(from_snapshot, to_snapshot)?;
752 let weight_changes = self.analyze_weight_changes(from_snapshot, to_snapshot)?;
753
754 let diff = VersionDiff {
755 from_version: from_snapshot.version.clone(),
756 to_version: to_snapshot.version.clone(),
757 timestamp: Utc::now(),
758 performance_delta,
759 architecture_changes,
760 config_changes,
761 weight_changes,
762 };
763
764 self.version_diffs.push(diff.clone());
765 Ok(diff)
766 }
767
768 pub async fn detect_regressions(
770 &mut self,
771 current_model: &str,
772 baseline_model: &str,
773 ) -> Result<RegressionDetectionResult> {
774 if !self.config.enable_regression_detection {
775 return Err(anyhow::anyhow!("Regression detection is disabled"));
776 }
777
778 let current = self
779 .model_snapshots
780 .get(current_model)
781 .ok_or_else(|| anyhow::anyhow!("Model '{}' not found", current_model))?;
782 let baseline = self
783 .model_snapshots
784 .get(baseline_model)
785 .ok_or_else(|| anyhow::anyhow!("Model '{}' not found", baseline_model))?;
786
787 let mut regressions = Vec::new();
788 let mut improvements = Vec::new();
789
790 if current.metrics.val_accuracy < baseline.metrics.val_accuracy {
792 let magnitude = baseline.metrics.val_accuracy - current.metrics.val_accuracy;
793 if magnitude > self.config.regression_sensitivity * 0.01 {
794 regressions.push(Regression {
795 metric: "validation_accuracy".to_string(),
796 current_value: current.metrics.val_accuracy,
797 previous_value: baseline.metrics.val_accuracy,
798 magnitude,
799 severity: self.classify_regression_severity(magnitude, "accuracy"),
800 possible_causes: vec![
801 "Learning rate too high".to_string(),
802 "Insufficient training".to_string(),
803 "Data distribution shift".to_string(),
804 ],
805 suggested_fixes: vec![
806 "Reduce learning rate".to_string(),
807 "Increase training epochs".to_string(),
808 "Check data quality".to_string(),
809 ],
810 });
811 }
812 } else if current.metrics.val_accuracy > baseline.metrics.val_accuracy {
813 let magnitude = current.metrics.val_accuracy - baseline.metrics.val_accuracy;
814 improvements.push(Improvement {
815 metric: "validation_accuracy".to_string(),
816 current_value: current.metrics.val_accuracy,
817 previous_value: baseline.metrics.val_accuracy,
818 magnitude,
819 likely_causes: vec![
820 "Better optimization".to_string(),
821 "Improved architecture".to_string(),
822 "Better hyperparameters".to_string(),
823 ],
824 });
825 }
826
827 if current.metrics.inference_latency_ms > baseline.metrics.inference_latency_ms {
829 let magnitude =
830 current.metrics.inference_latency_ms - baseline.metrics.inference_latency_ms;
831 let relative_change = magnitude / baseline.metrics.inference_latency_ms * 100.0;
832 if relative_change > self.config.performance_delta_threshold {
833 regressions.push(Regression {
834 metric: "inference_latency".to_string(),
835 current_value: current.metrics.inference_latency_ms,
836 previous_value: baseline.metrics.inference_latency_ms,
837 magnitude,
838 severity: self.classify_regression_severity(relative_change, "latency"),
839 possible_causes: vec![
840 "Model complexity increased".to_string(),
841 "Inefficient implementation".to_string(),
842 "Hardware degradation".to_string(),
843 ],
844 suggested_fixes: vec![
845 "Profile and optimize bottlenecks".to_string(),
846 "Consider model compression".to_string(),
847 "Check hardware configuration".to_string(),
848 ],
849 });
850 }
851 }
852
853 let critical_regressions = regressions
854 .iter()
855 .filter(|r| matches!(r.severity, RegressionSeverity::Critical))
856 .count();
857
858 let health_score = if critical_regressions > 0 {
859 0.0
860 } else {
861 1.0 - (regressions.len() as f64 * 0.1).min(1.0)
862 };
863
864 let recommendation = if critical_regressions > 0 {
865 "Critical regressions detected. Immediate action required.".to_string()
866 } else if !regressions.is_empty() {
867 "Some regressions detected. Review and address if necessary.".to_string()
868 } else {
869 "No significant regressions detected.".to_string()
870 };
871
872 let overall_assessment = RegressionAssessment {
873 health_score,
874 critical_regressions,
875 improvements: improvements.len(),
876 recommendation,
877 };
878
879 let result = RegressionDetectionResult {
880 timestamp: Utc::now(),
881 regressions,
882 improvements,
883 overall_assessment,
884 };
885
886 self.regression_history.push(result.clone());
887 Ok(result)
888 }
889
890 pub async fn generate_report(&self) -> Result<DifferentialDebuggingReport> {
892 Ok(DifferentialDebuggingReport {
893 timestamp: Utc::now(),
894 config: self.config.clone(),
895 total_models: self.model_snapshots.len(),
896 comparison_count: self.comparison_history.len(),
897 ab_test_count: self.ab_tests.len(),
898 version_diff_count: self.version_diffs.len(),
899 regression_detection_count: self.regression_history.len(),
900 recent_comparisons: self.comparison_history.iter().rev().take(5).cloned().collect(),
901 recent_regressions: self.regression_history.iter().rev().take(3).cloned().collect(),
902 model_summary: self.generate_model_summary(),
903 })
904 }
905
906 fn compare_performance(&self, models: &[&ModelSnapshot]) -> Result<PerformanceComparison> {
909 let mut accuracy_values = HashMap::new();
910 let mut loss_values = HashMap::new();
911 let mut latency_values = HashMap::new();
912 let mut memory_values = HashMap::new();
913 let mut size_values = HashMap::new();
914
915 for model in models {
916 accuracy_values.insert(model.name.clone(), model.metrics.val_accuracy);
917 loss_values.insert(model.name.clone(), model.metrics.val_loss);
918 latency_values.insert(model.name.clone(), model.metrics.inference_latency_ms);
919 memory_values.insert(model.name.clone(), model.metrics.memory_usage_mb);
920 size_values.insert(model.name.clone(), model.metrics.model_size_mb);
921 }
922
923 Ok(PerformanceComparison {
924 accuracy_comparison: self.create_metric_comparison(accuracy_values, true)?,
925 loss_comparison: self.create_metric_comparison(loss_values, false)?,
926 latency_comparison: self.create_metric_comparison(latency_values, false)?,
927 memory_comparison: self.create_metric_comparison(memory_values, false)?,
928 size_comparison: self.create_metric_comparison(size_values, false)?,
929 custom_comparisons: HashMap::new(),
930 })
931 }
932
933 fn create_metric_comparison(
934 &self,
935 values: HashMap<String, f64>,
936 higher_is_better: bool,
937 ) -> Result<MetricComparison> {
938 let best_model = if higher_is_better {
939 values
940 .iter()
941 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
942 .ok_or_else(|| anyhow::anyhow!("No values to compare"))?
943 .0
944 .clone()
945 } else {
946 values
947 .iter()
948 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
949 .ok_or_else(|| anyhow::anyhow!("No values to compare"))?
950 .0
951 .clone()
952 };
953
954 let worst_model = if higher_is_better {
955 values
956 .iter()
957 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
958 .ok_or_else(|| anyhow::anyhow!("No values to compare"))?
959 .0
960 .clone()
961 } else {
962 values
963 .iter()
964 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
965 .ok_or_else(|| anyhow::anyhow!("No values to compare"))?
966 .0
967 .clone()
968 };
969
970 let best_value = values[&best_model];
971 let mut differences = HashMap::new();
972 let mut significant_differences = HashMap::new();
973
974 for (model, value) in &values {
975 let diff = if higher_is_better {
976 (value - best_value) / best_value * 100.0
977 } else {
978 (best_value - value) / best_value * 100.0
979 };
980 differences.insert(model.clone(), diff);
981 significant_differences.insert(model.clone(), diff.abs() > 1.0); }
983
984 Ok(MetricComparison {
985 values,
986 best_model,
987 worst_model,
988 differences,
989 significant_differences,
990 })
991 }
992
993 fn analyze_architecture_differences(
994 &self,
995 models: &[&ModelSnapshot],
996 ) -> Result<ArchitectureDiff> {
997 if models.len() < 2 {
998 return Err(anyhow::anyhow!(
999 "Need at least 2 models for architecture diff"
1000 ));
1001 }
1002
1003 let base_model = models[0];
1004 let mut parameter_diff = HashMap::new();
1005 let mut layer_diff = HashMap::new();
1006 let mut notable_differences = Vec::new();
1007
1008 for model in models.iter().skip(1) {
1009 let param_diff = model.architecture.parameter_count as i64
1010 - base_model.architecture.parameter_count as i64;
1011 let layer_diff_val =
1012 model.architecture.layer_count as i32 - base_model.architecture.layer_count as i32;
1013
1014 parameter_diff.insert(model.name.clone(), param_diff);
1015 layer_diff.insert(model.name.clone(), layer_diff_val);
1016
1017 if param_diff.abs() > 1_000_000 {
1018 notable_differences.push(format!(
1019 "Model '{}' has {} parameter difference",
1020 model.name, param_diff
1021 ));
1022 }
1023
1024 if layer_diff_val != 0 {
1025 notable_differences.push(format!(
1026 "Model '{}' has {} layer difference",
1027 model.name, layer_diff_val
1028 ));
1029 }
1030 }
1031
1032 let mut similarity_scores = Vec::new();
1034 for model in models.iter().skip(1) {
1035 let score = self
1036 .calculate_architecture_similarity(&base_model.architecture, &model.architecture);
1037 similarity_scores.push(score);
1038 }
1039 let similarity_score =
1040 similarity_scores.iter().sum::<f64>() / similarity_scores.len() as f64;
1041
1042 Ok(ArchitectureDiff {
1043 parameter_diff,
1044 layer_diff,
1045 similarity_score,
1046 notable_differences,
1047 })
1048 }
1049
1050 fn calculate_architecture_similarity(
1051 &self,
1052 arch1: &ArchitectureInfo,
1053 arch2: &ArchitectureInfo,
1054 ) -> f64 {
1055 let mut similarity = 0.0;
1056 let mut features = 0;
1057
1058 let param_ratio = (arch1.parameter_count.min(arch2.parameter_count) as f64)
1060 / (arch1.parameter_count.max(arch2.parameter_count) as f64);
1061 similarity += param_ratio;
1062 features += 1;
1063
1064 let layer_ratio = (arch1.layer_count.min(arch2.layer_count) as f64)
1066 / (arch1.layer_count.max(arch2.layer_count) as f64);
1067 similarity += layer_ratio;
1068 features += 1;
1069
1070 let hidden_ratio = (arch1.hidden_size.min(arch2.hidden_size) as f64)
1072 / (arch1.hidden_size.max(arch2.hidden_size) as f64);
1073 similarity += hidden_ratio;
1074 features += 1;
1075
1076 similarity / features as f64
1077 }
1078
1079 fn perform_statistical_analysis(
1085 &self,
1086 models: &[&ModelSnapshot],
1087 ) -> Result<StatisticalAnalysis> {
1088 let mut p_values = HashMap::new();
1089 let mut effect_sizes = HashMap::new();
1090 let mut confidence_intervals = HashMap::new();
1091 let mut significance_summary = HashMap::new();
1092
1093 let metrics: [(&str, fn(&ModelMetrics) -> f64); 5] = [
1094 ("val_accuracy", |m| m.val_accuracy),
1095 ("val_loss", |m| m.val_loss),
1096 ("inference_latency_ms", |m| m.inference_latency_ms),
1097 ("memory_usage_mb", |m| m.memory_usage_mb),
1098 ("training_time_s", |m| m.training_time_s),
1099 ];
1100
1101 for (metric_name, extract) in metrics {
1102 let values: HashMap<String, f64> =
1103 models.iter().map(|m| (m.name.clone(), extract(&m.metrics))).collect();
1104
1105 if let Some((
1106 metric_p_values,
1107 metric_effect_sizes,
1108 metric_significant,
1109 (mean, std_dev),
1110 )) = self.cross_model_metric_significance(&values)
1111 {
1112 for (model, p) in metric_p_values {
1113 p_values.insert(format!("{metric_name}::{model}"), p);
1114 }
1115 for (model, z) in metric_effect_sizes {
1116 effect_sizes.insert(format!("{metric_name}::{model}"), z);
1117 }
1118 for (model, sig) in metric_significant {
1119 significance_summary.insert(format!("{metric_name}::{model}"), sig);
1120 }
1121
1122 let standard_error = std_dev / (values.len() as f64).sqrt();
1126 let margin = 1.96 * standard_error;
1127 confidence_intervals
1128 .insert(metric_name.to_string(), (mean - margin, mean + margin));
1129 }
1130 }
1131
1132 Ok(StatisticalAnalysis {
1133 p_values,
1134 effect_sizes,
1135 confidence_intervals,
1136 significance_summary,
1137 })
1138 }
1139
1140 fn generate_comparison_summary(
1141 &self,
1142 _models: &[&ModelSnapshot],
1143 performance: &PerformanceComparison,
1144 statistical: &StatisticalAnalysis,
1145 ) -> Result<ComparisonSummary> {
1146 let best_model = performance.accuracy_comparison.best_model.clone();
1147
1148 let mut rankings = HashMap::new();
1149 rankings.insert(
1150 "accuracy".to_string(),
1151 vec![performance.accuracy_comparison.best_model.clone()],
1152 );
1153 rankings.insert(
1154 "latency".to_string(),
1155 vec![performance.latency_comparison.best_model.clone()],
1156 );
1157
1158 let accuracy_significance_note = statistical
1167 .significance_summary
1168 .get(&format!(
1169 "val_accuracy::{}",
1170 performance.accuracy_comparison.best_model
1171 ))
1172 .map(|&is_significant| {
1173 if is_significant {
1174 " (statistically significant vs. the compared models)".to_string()
1175 } else {
1176 " (not statistically distinguishable from the compared models)".to_string()
1177 }
1178 })
1179 .unwrap_or_default();
1180 let latency_significance_note = statistical
1181 .significance_summary
1182 .get(&format!(
1183 "inference_latency_ms::{}",
1184 performance.latency_comparison.best_model
1185 ))
1186 .map(|&is_significant| {
1187 if is_significant {
1188 " (statistically significant vs. the compared models)".to_string()
1189 } else {
1190 " (not statistically distinguishable from the compared models)".to_string()
1191 }
1192 })
1193 .unwrap_or_default();
1194
1195 let key_findings = vec![
1196 format!(
1197 "Best accuracy: {} ({:.2}%){}",
1198 performance.accuracy_comparison.best_model,
1199 performance.accuracy_comparison.values[&performance.accuracy_comparison.best_model]
1200 * 100.0,
1201 accuracy_significance_note
1202 ),
1203 format!(
1204 "Fastest inference: {} ({:.2}ms){}",
1205 performance.latency_comparison.best_model,
1206 performance.latency_comparison.values[&performance.latency_comparison.best_model],
1207 latency_significance_note
1208 ),
1209 ];
1210
1211 let recommendations = vec![
1212 "Consider the trade-offs between accuracy and latency".to_string(),
1213 "Monitor memory usage for production deployment".to_string(),
1214 ];
1215
1216 Ok(ComparisonSummary {
1217 best_model,
1218 rankings,
1219 key_findings,
1220 recommendations,
1221 })
1222 }
1223
1224 fn calculate_summary_stats(&self, data: &[f64]) -> SummaryStats {
1225 let mean = data.iter().sum::<f64>() / data.len() as f64;
1226 let variance = data.variance();
1227 let std_dev = variance.sqrt();
1228
1229 let mut sorted_data = data.to_vec();
1230 sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1231
1232 let min = sorted_data[0];
1233 let max = sorted_data[sorted_data.len() - 1];
1234 let median = sorted_data[sorted_data.len() / 2];
1235 let q25 = sorted_data[sorted_data.len() / 4];
1236 let q75 = sorted_data[3 * sorted_data.len() / 4];
1237
1238 SummaryStats {
1239 mean,
1240 std_dev,
1241 min,
1242 max,
1243 median,
1244 q25,
1245 q75,
1246 }
1247 }
1248
1249 fn cross_model_metric_significance(
1275 &self,
1276 values: &HashMap<String, f64>,
1277 ) -> Option<(
1278 HashMap<String, f64>,
1279 HashMap<String, f64>,
1280 HashMap<String, bool>,
1281 (f64, f64),
1282 )> {
1283 if values.len() < 3 {
1284 return None;
1285 }
1286 let samples: Vec<f64> = values.values().copied().collect();
1287 let mean = samples.as_slice().mean();
1288 let std_dev = samples.as_slice().variance().sqrt();
1289 if !std_dev.is_finite() || std_dev <= 0.0 {
1290 return None;
1291 }
1292
1293 let normal = statrs::distribution::Normal::new(0.0, 1.0).ok()?;
1294 let mut p_values = HashMap::new();
1295 let mut effect_sizes = HashMap::new();
1296 let mut significant = HashMap::new();
1297 for (model, &value) in values {
1298 let z = (value - mean) / std_dev;
1299 let p_value = 2.0 * normal.sf(z.abs());
1308 significant.insert(model.clone(), p_value < self.config.significance_threshold);
1309 p_values.insert(model.clone(), p_value);
1310 effect_sizes.insert(model.clone(), z);
1311 }
1312 Some((p_values, effect_sizes, significant, (mean, std_dev)))
1313 }
1314
1315 fn perform_ab_statistical_tests(
1316 &self,
1317 model_a: &ABTestMetrics,
1318 model_b: &ABTestMetrics,
1319 ) -> Result<HashMap<String, StatisticalTestResult>> {
1320 let mut results = HashMap::new();
1321
1322 if let (Some(a_data), Some(b_data)) = (
1324 model_a.metrics.get("primary_metric"),
1325 model_b.metrics.get("primary_metric"),
1326 ) {
1327 if let Some(test) = welch_t_test(a_data, b_data, self.config.significance_threshold) {
1328 results.insert("primary_metric".to_string(), test);
1329 }
1330 }
1331
1332 Ok(results)
1333 }
1334
1335 fn generate_ab_conclusion(
1336 &self,
1337 config: &ABTestConfig,
1338 _model_a: &ABTestMetrics,
1339 _model_b: &ABTestMetrics,
1340 tests: &HashMap<String, StatisticalTestResult>,
1341 ) -> Result<ABTestConclusion> {
1342 let primary_test = tests.get("primary_metric");
1343
1344 let (winner, confidence, practical_significance) = if let Some(test) = primary_test {
1345 let winner = if test.effect_size > 0.0 {
1346 Some(config.model_a.clone())
1347 } else {
1348 Some(config.model_b.clone())
1349 };
1350
1351 let confidence = if test.is_significant { 0.95 } else { 0.5 };
1352 let practical_significance = test.effect_size.abs() > config.min_effect_size;
1353
1354 (winner, confidence, practical_significance)
1355 } else {
1356 (None, 0.5, false)
1357 };
1358
1359 let recommendation = match winner.as_ref() {
1360 Some(w) if practical_significance && confidence > 0.9 => {
1361 format!("Recommend deploying {}", w)
1362 },
1363 _ => "Insufficient evidence for a clear recommendation".to_string(),
1364 };
1365
1366 let summary = format!(
1367 "A/B test completed with {} confidence",
1368 if confidence > 0.9 { "high" } else { "low" }
1369 );
1370
1371 Ok(ABTestConclusion {
1372 winner,
1373 confidence,
1374 practical_significance,
1375 recommendation,
1376 summary,
1377 })
1378 }
1379
1380 fn calculate_performance_delta(
1381 &self,
1382 from: &ModelSnapshot,
1383 to: &ModelSnapshot,
1384 ) -> Result<PerformanceDelta> {
1385 Ok(PerformanceDelta {
1386 accuracy_delta: to.metrics.val_accuracy - from.metrics.val_accuracy,
1387 loss_delta: to.metrics.val_loss - from.metrics.val_loss,
1388 latency_delta: to.metrics.inference_latency_ms - from.metrics.inference_latency_ms,
1389 memory_delta: to.metrics.memory_usage_mb - from.metrics.memory_usage_mb,
1390 size_delta: to.metrics.model_size_mb - from.metrics.model_size_mb,
1391 training_time_delta: to.metrics.training_time_s - from.metrics.training_time_s,
1392 custom_deltas: HashMap::new(),
1393 })
1394 }
1395
1396 fn detect_architecture_changes(
1397 &self,
1398 from: &ModelSnapshot,
1399 to: &ModelSnapshot,
1400 ) -> Result<Vec<ArchitectureChange>> {
1401 let mut changes = Vec::new();
1402
1403 if from.architecture.parameter_count != to.architecture.parameter_count {
1404 changes.push(ArchitectureChange {
1405 change_type: "Parameter Count".to_string(),
1406 description: format!(
1407 "Changed from {} to {} parameters",
1408 from.architecture.parameter_count, to.architecture.parameter_count
1409 ),
1410 impact: "Affects model capacity and memory usage".to_string(),
1411 });
1412 }
1413
1414 if from.architecture.layer_count != to.architecture.layer_count {
1415 changes.push(ArchitectureChange {
1416 change_type: "Layer Count".to_string(),
1417 description: format!(
1418 "Changed from {} to {} layers",
1419 from.architecture.layer_count, to.architecture.layer_count
1420 ),
1421 impact: "Affects model depth and training dynamics".to_string(),
1422 });
1423 }
1424
1425 Ok(changes)
1426 }
1427
1428 fn detect_config_changes(
1429 &self,
1430 from: &ModelSnapshot,
1431 to: &ModelSnapshot,
1432 ) -> Result<Vec<ConfigChange>> {
1433 let mut changes = Vec::new();
1434
1435 if from.training_config.learning_rate != to.training_config.learning_rate {
1436 changes.push(ConfigChange {
1437 parameter: "learning_rate".to_string(),
1438 old_value: from.training_config.learning_rate.to_string(),
1439 new_value: to.training_config.learning_rate.to_string(),
1440 impact: "Affects training speed and convergence".to_string(),
1441 });
1442 }
1443
1444 if from.training_config.batch_size != to.training_config.batch_size {
1445 changes.push(ConfigChange {
1446 parameter: "batch_size".to_string(),
1447 old_value: from.training_config.batch_size.to_string(),
1448 new_value: to.training_config.batch_size.to_string(),
1449 impact: "Affects gradient noise and memory usage".to_string(),
1450 });
1451 }
1452
1453 Ok(changes)
1454 }
1455
1456 fn analyze_weight_changes(
1457 &self,
1458 from: &ModelSnapshot,
1459 to: &ModelSnapshot,
1460 ) -> Result<WeightChangesSummary> {
1461 let avg_magnitude = (to.weights_summary.mean - from.weights_summary.mean).abs();
1463 let max_change = (to.weights_summary.max - from.weights_summary.max).abs();
1464 let significant_change_ratio = if avg_magnitude > 0.01 { 0.8 } else { 0.2 };
1465
1466 Ok(WeightChangesSummary {
1467 avg_magnitude,
1468 max_change,
1469 significant_change_ratio,
1470 layer_changes: HashMap::new(),
1471 })
1472 }
1473
1474 fn classify_regression_severity(
1475 &self,
1476 magnitude: f64,
1477 metric_type: &str,
1478 ) -> RegressionSeverity {
1479 match metric_type {
1480 "accuracy" => {
1481 if magnitude > 0.1 {
1482 RegressionSeverity::Critical
1483 } else if magnitude > 0.05 {
1484 RegressionSeverity::Major
1485 } else if magnitude > 0.02 {
1486 RegressionSeverity::Minor
1487 } else {
1488 RegressionSeverity::Negligible
1489 }
1490 },
1491 "latency" => {
1492 if magnitude > 50.0 {
1493 RegressionSeverity::Critical
1494 } else if magnitude > 20.0 {
1495 RegressionSeverity::Major
1496 } else if magnitude > 10.0 {
1497 RegressionSeverity::Minor
1498 } else {
1499 RegressionSeverity::Negligible
1500 }
1501 },
1502 _ => RegressionSeverity::Minor,
1503 }
1504 }
1505
1506 fn generate_model_summary(&self) -> HashMap<String, String> {
1507 let mut summary = HashMap::new();
1508
1509 if let Some((best_name, best_model)) = self.model_snapshots.iter().max_by(|a, b| {
1510 a.1.metrics
1511 .val_accuracy
1512 .partial_cmp(&b.1.metrics.val_accuracy)
1513 .unwrap_or(std::cmp::Ordering::Equal)
1514 }) {
1515 summary.insert("best_accuracy_model".to_string(), best_name.clone());
1516 summary.insert(
1517 "best_accuracy_value".to_string(),
1518 format!("{:.4}", best_model.metrics.val_accuracy),
1519 );
1520 }
1521
1522 if let Some((fastest_name, fastest_model)) = self.model_snapshots.iter().min_by(|a, b| {
1523 a.1.metrics
1524 .inference_latency_ms
1525 .partial_cmp(&b.1.metrics.inference_latency_ms)
1526 .unwrap_or(std::cmp::Ordering::Equal)
1527 }) {
1528 summary.insert("fastest_model".to_string(), fastest_name.clone());
1529 summary.insert(
1530 "fastest_latency".to_string(),
1531 format!("{:.2}ms", fastest_model.metrics.inference_latency_ms),
1532 );
1533 }
1534
1535 summary.insert(
1536 "total_models".to_string(),
1537 self.model_snapshots.len().to_string(),
1538 );
1539 summary
1540 }
1541}
1542
1543#[derive(Debug, Clone, Serialize, Deserialize)]
1545pub struct DifferentialDebuggingReport {
1546 pub timestamp: DateTime<Utc>,
1547 pub config: DifferentialDebuggingConfig,
1548 pub total_models: usize,
1549 pub comparison_count: usize,
1550 pub ab_test_count: usize,
1551 pub version_diff_count: usize,
1552 pub regression_detection_count: usize,
1553 pub recent_comparisons: Vec<ModelComparisonResult>,
1554 pub recent_regressions: Vec<RegressionDetectionResult>,
1555 pub model_summary: HashMap<String, String>,
1556}
1557
1558#[cfg(test)]
1559#[path = "differential_debugging_tests.rs"]
1560mod differential_debugging_tests;
1561
1562#[cfg(test)]
1563mod tests {
1564 use super::*;
1565
1566 #[tokio::test]
1567 async fn test_differential_debugger_creation() {
1568 let config = DifferentialDebuggingConfig::default();
1569 let debugger = DifferentialDebugger::new(config);
1570 assert_eq!(debugger.model_snapshots.len(), 0);
1571 }
1572
1573 #[tokio::test]
1574 async fn test_model_snapshot_addition() {
1575 let config = DifferentialDebuggingConfig::default();
1576 let mut debugger = DifferentialDebugger::new(config);
1577
1578 let snapshot = create_test_snapshot("test_model");
1579 debugger.add_model_snapshot(snapshot).expect("add operation failed");
1580 assert_eq!(debugger.model_snapshots.len(), 1);
1581 }
1582
1583 #[tokio::test]
1584 async fn test_model_comparison() {
1585 let config = DifferentialDebuggingConfig::default();
1586 let mut debugger = DifferentialDebugger::new(config);
1587
1588 let snapshot1 = create_test_snapshot("model_a");
1590 let snapshot2 = create_test_snapshot("model_b");
1591
1592 debugger.add_model_snapshot(snapshot1).expect("add operation failed");
1593 debugger.add_model_snapshot(snapshot2).expect("add operation failed");
1594
1595 let result = debugger
1596 .compare_models(vec!["model_a".to_string(), "model_b".to_string()])
1597 .await;
1598 assert!(result.is_ok());
1599 }
1600
1601 #[test]
1602 fn test_config_default() {
1603 let config = DifferentialDebuggingConfig::default();
1604 assert!(config.enable_model_comparison);
1605 assert!(config.enable_ab_analysis);
1606 assert!(config.enable_version_diff);
1607 assert!(config.enable_regression_detection);
1608 assert!(config.enable_performance_delta);
1609 assert!((config.significance_threshold - 0.05).abs() < f64::EPSILON);
1610 assert_eq!(config.max_comparison_models, 10);
1611 }
1612
1613 #[tokio::test]
1614 async fn test_max_comparison_models_limit() {
1615 let mut config = DifferentialDebuggingConfig::default();
1616 config.max_comparison_models = 2;
1617 let mut debugger = DifferentialDebugger::new(config);
1618
1619 debugger
1620 .add_model_snapshot(create_test_snapshot("model_1"))
1621 .expect("add should succeed");
1622 debugger
1623 .add_model_snapshot(create_test_snapshot("model_2"))
1624 .expect("add should succeed");
1625 debugger
1626 .add_model_snapshot(create_test_snapshot("model_3"))
1627 .expect("add should succeed");
1628 assert_eq!(debugger.model_snapshots.len(), 2);
1629 }
1630
1631 #[tokio::test]
1632 async fn test_compare_models_disabled() {
1633 let mut config = DifferentialDebuggingConfig::default();
1634 config.enable_model_comparison = false;
1635 let mut debugger = DifferentialDebugger::new(config);
1636
1637 let snapshot1 = create_test_snapshot("a");
1638 let snapshot2 = create_test_snapshot("b");
1639 debugger.add_model_snapshot(snapshot1).expect("add should succeed");
1640 debugger.add_model_snapshot(snapshot2).expect("add should succeed");
1641
1642 let result = debugger.compare_models(vec!["a".to_string(), "b".to_string()]).await;
1643 assert!(result.is_err());
1644 }
1645
1646 #[tokio::test]
1647 async fn test_compare_models_too_few() {
1648 let config = DifferentialDebuggingConfig::default();
1649 let mut debugger = DifferentialDebugger::new(config);
1650 let snapshot1 = create_test_snapshot("only_one");
1651 debugger.add_model_snapshot(snapshot1).expect("add should succeed");
1652 let result = debugger.compare_models(vec!["only_one".to_string()]).await;
1653 assert!(result.is_err());
1654 }
1655
1656 #[tokio::test]
1657 async fn test_compare_models_missing_model() {
1658 let config = DifferentialDebuggingConfig::default();
1659 let mut debugger = DifferentialDebugger::new(config);
1660 let snapshot1 = create_test_snapshot("existing");
1661 debugger.add_model_snapshot(snapshot1).expect("add should succeed");
1662 let result = debugger
1663 .compare_models(vec!["existing".to_string(), "missing".to_string()])
1664 .await;
1665 assert!(result.is_err());
1666 }
1667
1668 #[tokio::test]
1669 async fn test_ab_test_analysis() {
1670 let config = DifferentialDebuggingConfig::default();
1671 let mut debugger = DifferentialDebugger::new(config);
1672
1673 let ab_config = ABTestConfig {
1674 name: "test_ab".to_string(),
1675 model_a: "model_a".to_string(),
1676 model_b: "model_b".to_string(),
1677 duration_hours: None,
1678 sample_size: 100,
1679 tracked_metrics: vec!["accuracy".to_string()],
1680 min_effect_size: 0.05,
1681 power: 0.8,
1682 };
1683
1684 let mut seed: u64 = 42;
1686 let model_a_data: Vec<f64> = (0..100)
1687 .map(|_| {
1688 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1689 0.8 + (seed as f64 / u64::MAX as f64) * 0.1
1690 })
1691 .collect();
1692 let model_b_data: Vec<f64> = (0..100)
1693 .map(|_| {
1694 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1695 0.82 + (seed as f64 / u64::MAX as f64) * 0.1
1696 })
1697 .collect();
1698
1699 let result = debugger.run_ab_test(ab_config, model_a_data, model_b_data).await;
1700 assert!(result.is_ok());
1701 let ab_result = result.expect("ab test should succeed");
1702 assert!(ab_result.conclusion.confidence >= 0.0);
1703 }
1704
1705 #[tokio::test]
1706 async fn test_ab_test_disabled() {
1707 let mut config = DifferentialDebuggingConfig::default();
1708 config.enable_ab_analysis = false;
1709 let mut debugger = DifferentialDebugger::new(config);
1710
1711 let ab_config = ABTestConfig {
1712 name: "test".to_string(),
1713 model_a: "a".to_string(),
1714 model_b: "b".to_string(),
1715 duration_hours: None,
1716 sample_size: 10,
1717 tracked_metrics: vec![],
1718 min_effect_size: 0.05,
1719 power: 0.8,
1720 };
1721
1722 let result = debugger.run_ab_test(ab_config, vec![1.0], vec![2.0]).await;
1723 assert!(result.is_err());
1724 }
1725
1726 #[test]
1727 fn test_model_metrics_creation() {
1728 let metrics = ModelMetrics {
1729 train_accuracy: 0.95,
1730 val_accuracy: 0.90,
1731 test_accuracy: None,
1732 train_loss: 0.05,
1733 val_loss: 0.10,
1734 test_loss: None,
1735 inference_latency_ms: 50.0,
1736 memory_usage_mb: 2048.0,
1737 model_size_mb: 500.0,
1738 flops: 1_000_000_000,
1739 training_time_s: 3600.0,
1740 custom_metrics: HashMap::new(),
1741 };
1742 assert!(metrics.train_accuracy > metrics.val_accuracy);
1743 assert!(metrics.test_accuracy.is_none());
1744 }
1745
1746 #[test]
1747 fn test_architecture_info() {
1748 let info = ArchitectureInfo {
1749 parameter_count: 175_000_000,
1750 layer_count: 24,
1751 depth: 24,
1752 hidden_size: 1024,
1753 num_heads: Some(16),
1754 ff_dim: Some(4096),
1755 vocab_size: Some(50257),
1756 max_seq_length: Some(2048),
1757 };
1758 assert_eq!(info.parameter_count, 175_000_000);
1759 assert_eq!(info.layer_count, 24);
1760 }
1761
1762 #[test]
1763 fn test_summary_stats() {
1764 let stats = SummaryStats {
1765 mean: 0.85,
1766 std_dev: 0.05,
1767 min: 0.70,
1768 max: 0.95,
1769 median: 0.86,
1770 q25: 0.82,
1771 q75: 0.89,
1772 };
1773 assert!(stats.min < stats.q25);
1774 assert!(stats.q25 < stats.median);
1775 assert!(stats.median < stats.q75);
1776 assert!(stats.q75 < stats.max);
1777 }
1778
1779 #[test]
1780 fn test_performance_delta() {
1781 let delta = PerformanceDelta {
1782 accuracy_delta: 0.02,
1783 loss_delta: -0.01,
1784 latency_delta: -5.0,
1785 memory_delta: 100.0,
1786 size_delta: 50.0,
1787 training_time_delta: -300.0,
1788 custom_deltas: HashMap::new(),
1789 };
1790 assert!(delta.accuracy_delta > 0.0);
1791 assert!(delta.loss_delta < 0.0);
1792 }
1793
1794 #[test]
1795 fn test_regression_severity_variants() {
1796 let severities = [
1797 RegressionSeverity::Critical,
1798 RegressionSeverity::Major,
1799 RegressionSeverity::Minor,
1800 RegressionSeverity::Negligible,
1801 ];
1802 assert_eq!(severities.len(), 4);
1803 }
1804
1805 #[test]
1806 fn test_version_diff_creation() {
1807 let diff = VersionDiff {
1808 from_version: "1.0.0".to_string(),
1809 to_version: "1.1.0".to_string(),
1810 timestamp: Utc::now(),
1811 performance_delta: PerformanceDelta {
1812 accuracy_delta: 0.01,
1813 loss_delta: -0.005,
1814 latency_delta: 0.0,
1815 memory_delta: 0.0,
1816 size_delta: 10.0,
1817 training_time_delta: 0.0,
1818 custom_deltas: HashMap::new(),
1819 },
1820 architecture_changes: vec![ArchitectureChange {
1821 change_type: "layer_added".to_string(),
1822 description: "Added dropout layer".to_string(),
1823 impact: "minor".to_string(),
1824 }],
1825 config_changes: vec![],
1826 weight_changes: WeightChangesSummary {
1827 avg_magnitude: 0.001,
1828 max_change: 0.05,
1829 significant_change_ratio: 0.1,
1830 layer_changes: HashMap::new(),
1831 },
1832 };
1833 assert_eq!(diff.from_version, "1.0.0");
1834 assert_eq!(diff.architecture_changes.len(), 1);
1835 }
1836
1837 #[test]
1838 fn test_statistical_test_result() {
1839 let result = StatisticalTestResult {
1840 test_type: "t-test".to_string(),
1841 statistic: 2.5,
1842 p_value: 0.01,
1843 effect_size: 0.4,
1844 confidence_interval: (0.01, 0.05),
1845 is_significant: true,
1846 };
1847 assert!(result.is_significant);
1848 assert!(result.p_value < 0.05);
1849 }
1850
1851 #[test]
1852 fn test_ab_test_conclusion() {
1853 let conclusion = ABTestConclusion {
1854 winner: Some("model_b".to_string()),
1855 confidence: 0.95,
1856 practical_significance: true,
1857 recommendation: "Deploy model_b".to_string(),
1858 summary: "Model B outperforms Model A significantly".to_string(),
1859 };
1860 assert!(conclusion.winner.is_some());
1861 assert!(conclusion.practical_significance);
1862 }
1863
1864 #[tokio::test]
1865 async fn test_compare_two_different_models() {
1866 let config = DifferentialDebuggingConfig::default();
1867 let mut debugger = DifferentialDebugger::new(config);
1868
1869 let mut snap_a = create_test_snapshot("model_a");
1870 snap_a.metrics.train_accuracy = 0.90;
1871 snap_a.metrics.val_accuracy = 0.85;
1872
1873 let mut snap_b = create_test_snapshot("model_b");
1874 snap_b.metrics.train_accuracy = 0.95;
1875 snap_b.metrics.val_accuracy = 0.92;
1876
1877 debugger.add_model_snapshot(snap_a).expect("add should succeed");
1878 debugger.add_model_snapshot(snap_b).expect("add should succeed");
1879
1880 let result = debugger
1881 .compare_models(vec!["model_a".to_string(), "model_b".to_string()])
1882 .await;
1883 assert!(result.is_ok());
1884 let comparison = result.expect("comparison should succeed");
1885 assert_eq!(comparison.models.len(), 2);
1886 }
1887
1888 fn create_test_snapshot(name: &str) -> ModelSnapshot {
1889 ModelSnapshot {
1890 id: Uuid::new_v4(),
1891 name: name.to_string(),
1892 timestamp: Utc::now(),
1893 version: "1.0.0".to_string(),
1894 commit_hash: Some("abc123".to_string()),
1895 metrics: ModelMetrics {
1896 train_accuracy: 0.95,
1897 val_accuracy: 0.90,
1898 test_accuracy: Some(0.88),
1899 train_loss: 0.05,
1900 val_loss: 0.10,
1901 test_loss: Some(0.12),
1902 inference_latency_ms: 50.0,
1903 memory_usage_mb: 2048.0,
1904 model_size_mb: 500.0,
1905 flops: 1_000_000_000,
1906 training_time_s: 3600.0,
1907 custom_metrics: HashMap::new(),
1908 },
1909 architecture: ArchitectureInfo {
1910 parameter_count: 175_000_000,
1911 layer_count: 24,
1912 depth: 24,
1913 hidden_size: 1024,
1914 num_heads: Some(16),
1915 ff_dim: Some(4096),
1916 vocab_size: Some(50257),
1917 max_seq_length: Some(2048),
1918 },
1919 training_config: TrainingConfig {
1920 learning_rate: 1e-4,
1921 batch_size: 32,
1922 epochs: 10,
1923 optimizer: "AdamW".to_string(),
1924 lr_schedule: Some("cosine".to_string()),
1925 regularization: HashMap::new(),
1926 },
1927 weights_summary: WeightsSummary {
1928 mean: 0.0,
1929 std_dev: 0.1,
1930 min: -0.5,
1931 max: 0.5,
1932 percentiles: HashMap::new(),
1933 zero_count: 1000,
1934 sparsity: 0.01,
1935 },
1936 metadata: HashMap::new(),
1937 }
1938 }
1939
1940 fn hundred_models_with_extreme_outlier() -> HashMap<String, f64> {
1943 let mut values: HashMap<String, f64> = (0..99).map(|i| (format!("m{i}"), 0.0)).collect();
1944 values.insert("outlier".to_string(), 1000.0);
1945 values
1946 }
1947
1948 #[test]
1952 fn naive_statrs_cdf_subtraction_underflows_where_survival_function_does_not() {
1953 use statrs::distribution::Normal;
1954 use statrs::statistics::Statistics;
1955
1956 let values = hundred_models_with_extreme_outlier();
1957 let samples: Vec<f64> = values.values().copied().collect();
1958 let mean = samples.as_slice().mean();
1959 let std_dev = samples.as_slice().variance().sqrt();
1960 let z = (1000.0 - mean) / std_dev;
1961 assert!((z - 9.9).abs() < 1e-6, "expected z ~= 9.9, got {z}");
1962
1963 let normal = Normal::new(0.0, 1.0).expect("standard normal is always valid");
1964 let naive_p_value = 2.0 * (1.0 - normal.cdf(z.abs()));
1965 assert_eq!(
1966 naive_p_value, 0.0,
1967 "expected the naive form to underflow to 0.0 at z = {z}"
1968 );
1969
1970 let fixed_p_value = 2.0 * normal.sf(z.abs());
1971 assert!(
1972 fixed_p_value > 0.0 && fixed_p_value < 1e-20,
1973 "expected a tiny nonzero p-value, got {fixed_p_value}"
1974 );
1975 }
1976
1977 #[test]
1979 fn cross_model_metric_significance_reports_tiny_nonzero_p_for_extreme_outlier() {
1980 let debugger = DifferentialDebugger::new(DifferentialDebuggingConfig::default());
1981 let values = hundred_models_with_extreme_outlier();
1982
1983 let (p_values, _effect_sizes, significant, _mean_std) = debugger
1984 .cross_model_metric_significance(&values)
1985 .expect(">= 3 models with nonzero variance must produce a result");
1986
1987 let outlier_p = p_values["outlier"];
1988 assert!(
1989 outlier_p > 0.0 && outlier_p < 1e-20,
1990 "expected a tiny nonzero p, got {outlier_p}"
1991 );
1992 assert!(
1993 significant["outlier"],
1994 "p-value {outlier_p} must be flagged significant"
1995 );
1996 }
1997}