Skip to main content

trustformers_debug/
differential_debugging.rs

1//! # Differential Debugging System
2//!
3//! Advanced model comparison, A/B analysis, version diff tracking, regression identification,
4//! and performance delta analysis for TrustformeRS models.
5
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use indexmap::IndexMap;
9// use scirs2_core::ndarray::*; // SciRS2 Integration Policy - was: use ndarray::{Array1, Array2};
10use serde::{Deserialize, Serialize};
11use statrs::distribution::{ContinuousCDF, StudentsT};
12use statrs::statistics::Statistics;
13use std::collections::HashMap;
14use uuid::Uuid;
15
16/// Configuration for differential debugging
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DifferentialDebuggingConfig {
19    /// Enable model comparison analysis
20    pub enable_model_comparison: bool,
21    /// Enable A/B testing analysis
22    pub enable_ab_analysis: bool,
23    /// Enable version diff tracking
24    pub enable_version_diff: bool,
25    /// Enable regression identification
26    pub enable_regression_detection: bool,
27    /// Enable performance delta analysis
28    pub enable_performance_delta: bool,
29    /// Statistical significance threshold for comparisons
30    pub significance_threshold: f64,
31    /// Maximum number of models to compare simultaneously
32    pub max_comparison_models: usize,
33    /// Regression detection sensitivity (0.0 to 1.0)
34    pub regression_sensitivity: f64,
35    /// Performance delta threshold (percentage)
36    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/// Model snapshot for comparison
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ModelSnapshot {
58    /// Unique identifier for the model snapshot
59    pub id: Uuid,
60    /// Model name or version identifier
61    pub name: String,
62    /// Timestamp when snapshot was created
63    pub timestamp: DateTime<Utc>,
64    /// Model version information
65    pub version: String,
66    /// Git commit hash (if available)
67    pub commit_hash: Option<String>,
68    /// Model performance metrics
69    pub metrics: ModelMetrics,
70    /// Model architecture information
71    pub architecture: ArchitectureInfo,
72    /// Training configuration
73    pub training_config: TrainingConfig,
74    /// Model weights summary statistics
75    pub weights_summary: WeightsSummary,
76    /// Additional metadata
77    pub metadata: HashMap<String, String>,
78}
79
80/// Performance metrics for a model
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ModelMetrics {
83    /// Training accuracy
84    pub train_accuracy: f64,
85    /// Validation accuracy
86    pub val_accuracy: f64,
87    /// Test accuracy (if available)
88    pub test_accuracy: Option<f64>,
89    /// Training loss
90    pub train_loss: f64,
91    /// Validation loss
92    pub val_loss: f64,
93    /// Test loss (if available)
94    pub test_loss: Option<f64>,
95    /// Inference latency (ms)
96    pub inference_latency_ms: f64,
97    /// Memory usage (MB)
98    pub memory_usage_mb: f64,
99    /// Model size (MB)
100    pub model_size_mb: f64,
101    /// FLOPS count
102    pub flops: u64,
103    /// Training time (seconds)
104    pub training_time_s: f64,
105    /// Custom metrics
106    pub custom_metrics: HashMap<String, f64>,
107}
108
109/// Architecture information
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ArchitectureInfo {
112    /// Number of parameters
113    pub parameter_count: u64,
114    /// Number of layers
115    pub layer_count: u32,
116    /// Model depth
117    pub depth: u32,
118    /// Hidden dimension size
119    pub hidden_size: u32,
120    /// Number of attention heads
121    pub num_heads: Option<u32>,
122    /// Feed-forward dimension
123    pub ff_dim: Option<u32>,
124    /// Vocabulary size
125    pub vocab_size: Option<u32>,
126    /// Sequence length
127    pub max_seq_length: Option<u32>,
128}
129
130/// Training configuration
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TrainingConfig {
133    /// Learning rate
134    pub learning_rate: f64,
135    /// Batch size
136    pub batch_size: u32,
137    /// Number of epochs
138    pub epochs: u32,
139    /// Optimizer type
140    pub optimizer: String,
141    /// Learning rate schedule
142    pub lr_schedule: Option<String>,
143    /// Regularization parameters
144    pub regularization: HashMap<String, f64>,
145}
146
147/// Summary statistics for model weights
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct WeightsSummary {
150    /// Mean weight value
151    pub mean: f64,
152    /// Standard deviation of weights
153    pub std_dev: f64,
154    /// Minimum weight value
155    pub min: f64,
156    /// Maximum weight value
157    pub max: f64,
158    /// Weight distribution percentiles
159    pub percentiles: HashMap<String, f64>,
160    /// Number of zero weights
161    pub zero_count: u64,
162    /// Sparsity ratio
163    pub sparsity: f64,
164}
165
166/// Result of model comparison analysis
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ModelComparisonResult {
169    /// Models being compared
170    pub models: Vec<String>,
171    /// Comparison timestamp
172    pub timestamp: DateTime<Utc>,
173    /// Performance comparison
174    pub performance_comparison: PerformanceComparison,
175    /// Architecture differences
176    pub architecture_diff: ArchitectureDiff,
177    /// Statistical significance results
178    pub statistical_analysis: StatisticalAnalysis,
179    /// Overall comparison summary
180    pub summary: ComparisonSummary,
181}
182
183/// Performance comparison between models
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct PerformanceComparison {
186    /// Accuracy comparison
187    pub accuracy_comparison: MetricComparison,
188    /// Loss comparison
189    pub loss_comparison: MetricComparison,
190    /// Latency comparison
191    pub latency_comparison: MetricComparison,
192    /// Memory usage comparison
193    pub memory_comparison: MetricComparison,
194    /// Model size comparison
195    pub size_comparison: MetricComparison,
196    /// Custom metric comparisons
197    pub custom_comparisons: HashMap<String, MetricComparison>,
198}
199
200/// Comparison result for a specific metric
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct MetricComparison {
203    /// Values for each model
204    pub values: HashMap<String, f64>,
205    /// Best performing model for this metric
206    pub best_model: String,
207    /// Worst performing model for this metric
208    pub worst_model: String,
209    /// Performance differences (relative to best)
210    pub differences: HashMap<String, f64>,
211    /// Statistical significance of differences
212    pub significant_differences: HashMap<String, bool>,
213}
214
215/// Architecture differences between models
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ArchitectureDiff {
218    /// Parameter count differences
219    pub parameter_diff: HashMap<String, i64>,
220    /// Layer count differences
221    pub layer_diff: HashMap<String, i32>,
222    /// Architecture similarity score (0.0 to 1.0)
223    pub similarity_score: f64,
224    /// Notable differences
225    pub notable_differences: Vec<String>,
226}
227
228/// Statistical analysis of comparisons
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct StatisticalAnalysis {
231    /// P-values for metric comparisons
232    pub p_values: HashMap<String, f64>,
233    /// Effect sizes (Cohen's d)
234    pub effect_sizes: HashMap<String, f64>,
235    /// Confidence intervals
236    pub confidence_intervals: HashMap<String, (f64, f64)>,
237    /// Statistical significance summary
238    pub significance_summary: HashMap<String, bool>,
239}
240
241/// Overall comparison summary
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ComparisonSummary {
244    /// Overall best model
245    pub best_model: String,
246    /// Model rankings by different criteria
247    pub rankings: HashMap<String, Vec<String>>,
248    /// Key findings
249    pub key_findings: Vec<String>,
250    /// Recommendations
251    pub recommendations: Vec<String>,
252}
253
254/// A/B test configuration
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ABTestConfig {
257    /// Test name
258    pub name: String,
259    /// Model A identifier
260    pub model_a: String,
261    /// Model B identifier
262    pub model_b: String,
263    /// Test duration (if applicable)
264    pub duration_hours: Option<u32>,
265    /// Sample size for each group
266    pub sample_size: u32,
267    /// Metrics to track
268    pub tracked_metrics: Vec<String>,
269    /// Minimum detectable effect size
270    pub min_effect_size: f64,
271    /// Statistical power
272    pub power: f64,
273}
274
275/// A/B test result
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct ABTestResult {
278    /// Test configuration
279    pub config: ABTestConfig,
280    /// Test start time
281    pub start_time: DateTime<Utc>,
282    /// Test end time
283    pub end_time: Option<DateTime<Utc>>,
284    /// Model A results
285    pub model_a_results: ABTestMetrics,
286    /// Model B results
287    pub model_b_results: ABTestMetrics,
288    /// Statistical test results
289    pub statistical_tests: HashMap<String, StatisticalTestResult>,
290    /// Test conclusion
291    pub conclusion: ABTestConclusion,
292}
293
294/// Metrics for A/B test
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct ABTestMetrics {
297    /// Sample size
298    pub sample_size: u32,
299    /// Metric values
300    pub metrics: HashMap<String, Vec<f64>>,
301    /// Summary statistics
302    pub summary_stats: HashMap<String, SummaryStats>,
303}
304
305/// Summary statistics
306#[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/// Statistical test result
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct StatisticalTestResult {
320    /// Test type (t-test, Mann-Whitney U, etc.)
321    pub test_type: String,
322    /// Test statistic
323    pub statistic: f64,
324    /// P-value
325    pub p_value: f64,
326    /// Effect size
327    pub effect_size: f64,
328    /// Confidence interval for difference
329    pub confidence_interval: (f64, f64),
330    /// Is result statistically significant?
331    pub is_significant: bool,
332}
333
334/// A/B test conclusion
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct ABTestConclusion {
337    /// Winner (if any)
338    pub winner: Option<String>,
339    /// Confidence level
340    pub confidence: f64,
341    /// Practical significance
342    pub practical_significance: bool,
343    /// Recommendation
344    pub recommendation: String,
345    /// Summary
346    pub summary: String,
347}
348
349/// Version diff tracking information
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct VersionDiff {
352    /// Previous version
353    pub from_version: String,
354    /// Current version
355    pub to_version: String,
356    /// Diff timestamp
357    pub timestamp: DateTime<Utc>,
358    /// Performance changes
359    pub performance_delta: PerformanceDelta,
360    /// Architecture changes
361    pub architecture_changes: Vec<ArchitectureChange>,
362    /// Configuration changes
363    pub config_changes: Vec<ConfigChange>,
364    /// Weight changes summary
365    pub weight_changes: WeightChangesSummary,
366}
367
368/// Performance delta between versions
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct PerformanceDelta {
371    /// Accuracy change
372    pub accuracy_delta: f64,
373    /// Loss change
374    pub loss_delta: f64,
375    /// Latency change
376    pub latency_delta: f64,
377    /// Memory usage change
378    pub memory_delta: f64,
379    /// Model size change
380    pub size_delta: f64,
381    /// Training time change
382    pub training_time_delta: f64,
383    /// Custom metric changes
384    pub custom_deltas: HashMap<String, f64>,
385}
386
387/// Architecture change description
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct ArchitectureChange {
390    /// Type of change
391    pub change_type: String,
392    /// Description
393    pub description: String,
394    /// Impact assessment
395    pub impact: String,
396}
397
398/// Configuration change description
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct ConfigChange {
401    /// Parameter name
402    pub parameter: String,
403    /// Old value
404    pub old_value: String,
405    /// New value
406    pub new_value: String,
407    /// Change impact
408    pub impact: String,
409}
410
411/// Summary of weight changes
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct WeightChangesSummary {
414    /// Average magnitude of weight changes
415    pub avg_magnitude: f64,
416    /// Maximum weight change
417    pub max_change: f64,
418    /// Percentage of weights that changed significantly
419    pub significant_change_ratio: f64,
420    /// Layer-wise change summary
421    pub layer_changes: HashMap<String, f64>,
422}
423
424/// Regression detection result
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct RegressionDetectionResult {
427    /// Analysis timestamp
428    pub timestamp: DateTime<Utc>,
429    /// Detected regressions
430    pub regressions: Vec<Regression>,
431    /// Performance improvements
432    pub improvements: Vec<Improvement>,
433    /// Overall assessment
434    pub overall_assessment: RegressionAssessment,
435}
436
437/// Detected regression
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct Regression {
440    /// Metric that regressed
441    pub metric: String,
442    /// Current value
443    pub current_value: f64,
444    /// Previous value
445    pub previous_value: f64,
446    /// Regression magnitude
447    pub magnitude: f64,
448    /// Severity level
449    pub severity: RegressionSeverity,
450    /// Possible causes
451    pub possible_causes: Vec<String>,
452    /// Suggested fixes
453    pub suggested_fixes: Vec<String>,
454}
455
456/// Performance improvement
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct Improvement {
459    /// Metric that improved
460    pub metric: String,
461    /// Current value
462    pub current_value: f64,
463    /// Previous value
464    pub previous_value: f64,
465    /// Improvement magnitude
466    pub magnitude: f64,
467    /// Likely causes
468    pub likely_causes: Vec<String>,
469}
470
471/// Regression severity levels
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub enum RegressionSeverity {
474    Critical,
475    Major,
476    Minor,
477    Negligible,
478}
479
480/// Overall regression assessment
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct RegressionAssessment {
483    /// Overall health score (0.0 to 1.0)
484    pub health_score: f64,
485    /// Number of critical regressions
486    pub critical_regressions: usize,
487    /// Number of improvements
488    pub improvements: usize,
489    /// Recommendation
490    pub recommendation: String,
491}
492
493/// Main differential debugging analyzer
494/// Real Welch's t-test (unequal variances, unequal sample sizes) between two
495/// independent samples, with a real two-tailed p-value computed from the
496/// Student's t-distribution CDF via the Welch-Satterthwaite degrees of
497/// freedom -- not the old `if t.abs() > 2.0 { 0.01 } else { 0.1 }`
498/// two-bucket fabrication.
499///
500/// Returns `None` when either sample has fewer than 2 points, when both
501/// variances are zero (samples are degenerate constants, so no meaningful
502/// test exists), or when the resulting Welch-Satterthwaite degrees of
503/// freedom or `StudentsT` distribution parameters are not finite/positive
504/// (all statrs invariants) -- callers get an honest absence rather than a
505/// nonsensical or NaN test result.
506pub(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    // Welch-Satterthwaite equation for the effective degrees of freedom.
536    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    // Two-tailed p-value: P(|T| >= |t_statistic|), from the workspace's own
544    // regularized-incomplete-beta implementation. `2 * (1 - statrs_cdf(|t|))`
545    // cancels catastrophically in the upper tail -- it reaches 3.7e-5 relative
546    // error by t=12, df=30 and underflows to exactly 0.0 at t=12, df=120
547    // (true p 2.78e-22).
548    let p_value = trustformers_core::statistics::student_t_two_sided_p_value(
549        t_statistic,
550        degrees_of_freedom,
551    )?;
552
553    // Pooled std for Cohen's d (uses the simple average of the two
554    // variances, the standard convention for an unequal-n effect size).
555    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    // 95% CI on the mean difference, using the same Welch standard error
559    // and the t-distribution's critical value (not a fixed z=1.96, which
560    // is only exact for infinite degrees of freedom).
561    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    /// Create a new differential debugger
589    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    /// Add a model snapshot for comparison
601    pub fn add_model_snapshot(&mut self, snapshot: ModelSnapshot) -> Result<()> {
602        if self.model_snapshots.len() >= self.config.max_comparison_models {
603            // Remove oldest snapshot
604            self.model_snapshots.shift_remove_index(0);
605        }
606
607        self.model_snapshots.insert(snapshot.name.clone(), snapshot);
608        Ok(())
609    }
610
611    /// Compare two or more models
612    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        // Get model snapshots
627        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        // Perform comparison analysis
637        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    /// Run A/B test analysis
660    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        // Calculate summary statistics for both models
673        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        // Perform statistical tests
705        let statistical_tests =
706            self.perform_ab_statistical_tests(&model_a_results, &model_b_results)?;
707
708        // Generate conclusion
709        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    /// Track version differences
731    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    /// Detect performance regressions
769    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        // Check accuracy regression
791        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        // Check latency regression
828        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    /// Generate comprehensive differential debugging report
891    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    // Helper methods
907
908    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); // 1% threshold
982        }
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        // Calculate similarity score based on architecture features
1033        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        // Parameter count similarity
1059        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        // Layer count similarity
1065        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        // Hidden size similarity (if available)
1071        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    /// Real statistical analysis over the models' point-estimate metrics.
1080    /// See [`Self::cross_model_metric_significance`] for why this is a
1081    /// cross-model z-score/p-value rather than a pairwise t-test, and why
1082    /// fewer than 3 models yields an (honestly) empty
1083    /// [`StatisticalAnalysis`] rather than a fabricated one.
1084    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                // Real 95% confidence interval on the cross-model mean,
1123                // computed from the actual sample (standard error of the
1124                // mean, not a fabricated width).
1125                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        // Whether the "best" model's advantage clears the real
1159        // significance test in `statistical` (see
1160        // `Self::cross_model_metric_significance`) -- `None` when there
1161        // were too few models (<3) or too little metric spread for the
1162        // test to have run at all, in which case the finding is stated as
1163        // an unqualified point-estimate, exactly like the old
1164        // implementation always was, rather than implying a false
1165        // certainty either way.
1166        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    /// Real per-metric cross-model significance test used by
1250    /// [`Self::perform_statistical_analysis`], derived from
1251    /// [`ModelSnapshot::metrics`]'s point-estimate fields.
1252    ///
1253    /// `ModelSnapshot` carries a single point estimate per metric (not a
1254    /// distribution of repeated runs), so a true Welch's t-test between two
1255    /// models is not available here -- that requires `welch_t_test`'s two
1256    /// `&[f64]` samples, which [`Self::perform_ab_statistical_tests`] does
1257    /// have (real per-batch measurements from `run_ab_test`). What *is*
1258    /// real and honestly computable from point estimates across N>=3
1259    /// models is each model's z-score relative to the cross-model
1260    /// distribution of that metric: how many standard deviations a model's
1261    /// value falls from the mean of all compared models (used directly as
1262    /// the effect size), converted to a real two-tailed p-value via the
1263    /// standard normal CDF. This flags models whose metric is a genuine
1264    /// outlier relative to its peers (unlike the old stub, which returned
1265    /// empty maps and let `generate_comparison_summary` declare "winners"
1266    /// backed by nothing). With fewer than 3 models, or a metric that is
1267    /// identical across every model (zero variance), there's no meaningful
1268    /// distribution to compare against, so the metric is omitted entirely
1269    /// -- never fabricated.
1270    ///
1271    /// Returns `(per-model p-values, per-model effect sizes/z-scores,
1272    /// per-model significance flags, (cross-model mean, std_dev))`, all
1273    /// keyed by model name.
1274    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            // Two-tailed p-value P(|Z| >= |z|): naive `2 * (1 - cdf)`
1300            // cancels catastrophically for |z| ~ 9+, as the Student-t
1301            // p-value above (:544-549) once did. No t-distribution DOF
1302            // exists for a z-score (see this fn's doc comment), and
1303            // `trustformers_core::statistics` has no normal-tail primitive
1304            // to delegate to instead (adding one is outside this package's
1305            // ownership this pass -- tracked as a follow-up). `sf` computes
1306            // the tail directly via statrs' own `erfc`, unlike `cdf`.
1307            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        // Real Welch's t-test for primary metric (see `welch_t_test`).
1323        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        // Simplified weight change analysis
1462        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/// Comprehensive differential debugging report
1544#[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        // Add two test models
1589        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        // Simple LCG for deterministic test data
1685        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    /// 99 models tied at 0.0 plus one outlier: gives an exact z = 9.9,
1941    /// value-independent (mean/std_dev both scale with the outlier).
1942    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    /// Regression guard: the naive `2 * (1 - cdf)` form underflows to 0.0
1949    /// at z = 9.9, though the true p is a tiny but real ~4.16e-23; `sf`
1950    /// does not go through that subtraction.
1951    #[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    /// Same scenario through the real code path, not the primitive.
1978    #[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}