Skip to main content

optirs_learned/transformer/training/
evaluation.rs

1use std::fmt::Debug;
2// Evaluation metrics and methods for transformer optimization
3//
4// This module implements comprehensive evaluation strategies for assessing
5// the performance of transformer-based learned optimizers across various metrics.
6
7use scirs2_core::numeric::Float;
8use std::collections::{HashMap, VecDeque};
9
10use crate::error::{OptimError, Result};
11
12/// Evaluation strategies for transformer optimizers
13#[derive(Debug, Clone, Copy)]
14pub enum EvaluationStrategy {
15    /// Single-task evaluation
16    SingleTask,
17    /// Multi-task evaluation
18    MultiTask,
19    /// Cross-domain evaluation
20    CrossDomain,
21    /// Few-shot evaluation
22    FewShot,
23    /// Continual learning evaluation
24    ContinualLearning,
25    /// Robustness evaluation
26    Robustness,
27    /// Efficiency evaluation
28    Efficiency,
29    /// Comprehensive evaluation
30    Comprehensive,
31}
32
33/// Performance evaluator for transformer optimizers
34#[derive(Debug, Clone)]
35pub struct TransformerEvaluator<T: Float + Debug + Send + Sync + 'static> {
36    /// Evaluation strategy
37    strategy: EvaluationStrategy,
38
39    /// Evaluation parameters
40    eval_params: EvaluationParams<T>,
41
42    /// Metric calculators
43    metric_calculators: HashMap<String, MetricCalculator<T>>,
44
45    /// Performance history
46    performance_history: VecDeque<EvaluationResult<T>>,
47
48    /// Baseline comparisons
49    baseline_comparisons: HashMap<String, BaselineComparison<T>>,
50
51    /// Robustness scores per task, keyed by task id then by score name.
52    ///
53    /// [`TransformerEvaluator::evaluate_robustness`] used to compute these and
54    /// hand them straight back to the caller, discarding the `task_id` it was
55    /// given, so the evaluator itself retained no robustness record and
56    /// [`TransformerEvaluator::robustness_results`] had nothing to report.
57    robustness_results: HashMap<String, HashMap<String, T>>,
58}
59
60/// Evaluation parameters
61#[derive(Debug, Clone)]
62pub struct EvaluationParams<T: Float + Debug + Send + Sync + 'static> {
63    /// Number of evaluation episodes
64    pub num_episodes: usize,
65
66    /// Evaluation frequency
67    pub eval_frequency: usize,
68
69    /// Convergence tolerance
70    pub convergence_tolerance: T,
71
72    /// Maximum evaluation steps
73    pub max_eval_steps: usize,
74
75    /// Confidence level for statistical tests
76    pub confidence_level: T,
77
78    /// Number of bootstrap samples
79    pub bootstrap_samples: usize,
80
81    /// Cross-validation folds
82    pub cv_folds: usize,
83
84    /// Robustness test severity (relative noise amplitude)
85    pub robustness_severity: T,
86
87    /// Width of the (disjoint) moving-average windows used for convergence detection
88    pub convergence_window: usize,
89
90    /// Number of consecutive windows that must be stable before declaring convergence
91    pub convergence_consecutive_hits: usize,
92
93    /// Number of leading samples used by the few-shot evaluation variant
94    pub few_shot_samples: usize,
95
96    /// Number of noise-perturbed replicas used by the robustness variant
97    pub robustness_replicas: usize,
98
99    /// Seed for the deterministic robustness perturbations
100    pub robustness_seed: u64,
101}
102
103/// Evaluation result
104#[derive(Debug, Clone)]
105pub struct EvaluationResult<T: Float + Debug + Send + Sync + 'static> {
106    /// Evaluation identifier
107    eval_id: String,
108
109    /// Task identifier
110    task_id: String,
111
112    /// Performance metrics
113    metrics: HashMap<String, T>,
114
115    /// Convergence information
116    convergence_info: ConvergenceInfo<T>,
117
118    /// Efficiency metrics
119    efficiency_metrics: EfficiencyMetrics<T>,
120
121    /// Statistical significance, present only when a baseline has been registered
122    statistical_significance: Option<StatisticalSignificance<T>>,
123
124    /// Evaluation timestamp
125    timestamp: usize,
126}
127
128impl<T: Float + Debug + Send + Sync + 'static> EvaluationResult<T> {
129    /// Identifier of this evaluation
130    pub fn eval_id(&self) -> &str {
131        &self.eval_id
132    }
133
134    /// Task this evaluation belongs to
135    pub fn task_id(&self) -> &str {
136        &self.task_id
137    }
138
139    /// Metrics computed for this evaluation
140    pub fn metrics(&self) -> &HashMap<String, T> {
141        &self.metrics
142    }
143
144    /// Convergence information
145    pub fn convergence_info(&self) -> &ConvergenceInfo<T> {
146        &self.convergence_info
147    }
148
149    /// Efficiency metrics
150    pub fn efficiency_metrics(&self) -> &EfficiencyMetrics<T> {
151        &self.efficiency_metrics
152    }
153
154    /// Statistical significance versus the registered baseline, if any
155    pub fn statistical_significance(&self) -> Option<&StatisticalSignificance<T>> {
156        self.statistical_significance.as_ref()
157    }
158
159    /// Evaluation timestamp
160    pub fn timestamp(&self) -> usize {
161        self.timestamp
162    }
163}
164
165impl<T: Float + Debug + Send + Sync + 'static> ConvergenceInfo<T> {
166    /// Whether convergence was detected
167    pub fn converged(&self) -> bool {
168        self.converged
169    }
170
171    /// Step index at which convergence was detected
172    pub fn steps_to_convergence(&self) -> Option<usize> {
173        self.steps_to_convergence
174    }
175
176    /// Final loss value
177    pub fn final_loss(&self) -> T {
178        self.final_loss
179    }
180
181    /// Average relative improvement per step
182    pub fn convergence_rate(&self) -> T {
183        self.convergence_rate
184    }
185
186    /// Recorded loss trajectory
187    pub fn loss_trajectory(&self) -> &[T] {
188        &self.loss_trajectory
189    }
190
191    /// Recorded gradient norms
192    pub fn gradient_norms(&self) -> &[T] {
193        &self.gradient_norms
194    }
195}
196
197impl<T: Float + Debug + Send + Sync + 'static> EfficiencyMetrics<T> {
198    /// Wall-clock time
199    pub fn wall_time(&self) -> T {
200        self.wall_time
201    }
202
203    /// Estimated floating point operations
204    pub fn flops(&self) -> u64 {
205        self.flops
206    }
207
208    /// Peak memory usage in bytes
209    pub fn peak_memory(&self) -> u64 {
210        self.peak_memory
211    }
212
213    /// Parameter efficiency
214    pub fn parameter_efficiency(&self) -> T {
215        self.parameter_efficiency
216    }
217
218    /// Sample efficiency
219    pub fn sample_efficiency(&self) -> T {
220        self.sample_efficiency
221    }
222
223    /// Energy consumption estimate
224    pub fn energy_consumption(&self) -> T {
225        self.energy_consumption
226    }
227}
228
229impl<T: Float + Debug + Send + Sync + 'static> StatisticalSignificance<T> {
230    /// Two-sided p-value
231    pub fn p_value(&self) -> T {
232        self.p_value
233    }
234
235    /// Standardized effect size (Cohen's d)
236    pub fn effect_size(&self) -> T {
237        self.effect_size
238    }
239
240    /// Confidence interval of the mean difference
241    pub fn confidence_interval(&self) -> (T, T) {
242        self.confidence_interval
243    }
244
245    /// Statistical power estimate
246    pub fn statistical_power(&self) -> T {
247        self.statistical_power
248    }
249
250    /// Test statistic
251    pub fn test_statistic(&self) -> T {
252        self.test_statistic
253    }
254}
255
256/// Convergence information
257#[derive(Debug, Clone)]
258pub struct ConvergenceInfo<T: Float + Debug + Send + Sync + 'static> {
259    /// Whether convergence was achieved
260    converged: bool,
261
262    /// Number of steps to convergence
263    steps_to_convergence: Option<usize>,
264
265    /// Final loss value
266    final_loss: T,
267
268    /// Convergence rate
269    convergence_rate: T,
270
271    /// Loss trajectory
272    loss_trajectory: Vec<T>,
273
274    /// Gradient norms
275    gradient_norms: Vec<T>,
276}
277
278/// Efficiency metrics
279#[derive(Debug, Clone)]
280pub struct EfficiencyMetrics<T: Float + Debug + Send + Sync + 'static> {
281    /// Wall-clock time
282    wall_time: T,
283
284    /// Computational FLOPs
285    flops: u64,
286
287    /// Memory usage peak
288    peak_memory: u64,
289
290    /// Parameter efficiency
291    parameter_efficiency: T,
292
293    /// Sample efficiency
294    sample_efficiency: T,
295
296    /// Energy consumption estimate
297    energy_consumption: T,
298}
299
300/// Statistical significance analysis
301#[derive(Debug, Clone)]
302pub struct StatisticalSignificance<T: Float + Debug + Send + Sync + 'static> {
303    /// P-value for performance comparison
304    p_value: T,
305
306    /// Effect size
307    effect_size: T,
308
309    /// Confidence interval
310    confidence_interval: (T, T),
311
312    /// Statistical power
313    statistical_power: T,
314
315    /// Test statistic
316    test_statistic: T,
317}
318
319/// Metric calculator for specific metrics
320#[derive(Debug, Clone)]
321pub struct MetricCalculator<T: Float + Debug + Send + Sync + 'static> {
322    /// Historical values for trend analysis
323    historical_values: VecDeque<T>,
324
325    /// Aggregation method
326    aggregation_method: AggregationMethod,
327}
328
329/// Baseline comparison data
330#[derive(Debug, Clone)]
331pub struct BaselineComparison<T: Float + Debug + Send + Sync + 'static> {
332    /// Baseline performance
333    baseline_performance: HashMap<String, T>,
334}
335
336/// Robustness test suite
337#[derive(Debug, Clone)]
338pub struct RobustnessTestSuite<T: Float + Debug + Send + Sync + 'static> {
339    /// Noise injection tests
340    pub noise_tests: Vec<NoiseTest<T>>,
341
342    /// Adversarial perturbation tests
343    pub adversarial_tests: Vec<AdversarialTest<T>>,
344
345    /// Hyperparameter sensitivity tests
346    pub sensitivity_tests: Vec<SensitivityTest<T>>,
347
348    /// Distribution shift tests
349    pub distribution_tests: Vec<DistributionTest<T>>,
350}
351
352/// Individual robustness tests
353#[derive(Debug, Clone)]
354pub struct NoiseTest<T: Float + Debug + Send + Sync + 'static> {
355    pub noise_type: NoiseType,
356    pub noise_level: T,
357    pub performance_degradation: T,
358}
359
360#[derive(Debug, Clone)]
361pub struct AdversarialTest<T: Float + Debug + Send + Sync + 'static> {
362    pub attack_type: AttackType,
363    pub attack_strength: T,
364    pub robustness_score: T,
365}
366
367#[derive(Debug, Clone)]
368pub struct SensitivityTest<T: Float + Debug + Send + Sync + 'static> {
369    pub parameter_name: String,
370    pub parameter_range: (T, T),
371    pub sensitivity_score: T,
372}
373
374#[derive(Debug, Clone)]
375pub struct DistributionTest<T: Float + Debug + Send + Sync + 'static> {
376    pub shift_type: DistributionShiftType,
377    pub shift_magnitude: T,
378    pub adaptation_score: T,
379}
380
381/// Aggregation methods for metrics
382#[derive(Debug, Clone, Copy)]
383pub enum AggregationMethod {
384    Mean,
385    Median,
386    Max,
387    Min,
388    WeightedAverage,
389    ExponentialMovingAverage,
390    Percentile(u8),
391}
392
393/// Noise types for robustness testing
394#[derive(Debug, Clone, Copy)]
395pub enum NoiseType {
396    Gaussian,
397    Uniform,
398    SaltPepper,
399    Dropout,
400}
401
402/// Attack types for adversarial testing
403#[derive(Debug, Clone, Copy)]
404pub enum AttackType {
405    FGSM,
406    PGD,
407    CarliniWagner,
408    DeepFool,
409}
410
411/// Distribution shift types
412#[derive(Debug, Clone, Copy)]
413pub enum DistributionShiftType {
414    CovariateShift,
415    ConceptDrift,
416    DatasetShift,
417    TemporalShift,
418}
419
420impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> TransformerEvaluator<T> {
421    /// Create new transformer evaluator
422    pub fn new(strategy: EvaluationStrategy) -> Result<Self> {
423        let mut metric_calculators = HashMap::new();
424
425        // Initialize standard metric calculators
426        metric_calculators.insert(
427            "convergence_speed".to_string(),
428            MetricCalculator::new(AggregationMethod::Mean)?,
429        );
430        metric_calculators.insert(
431            "final_performance".to_string(),
432            MetricCalculator::new(AggregationMethod::Mean)?,
433        );
434        metric_calculators.insert(
435            "sample_efficiency".to_string(),
436            MetricCalculator::new(AggregationMethod::Mean)?,
437        );
438
439        Ok(Self {
440            strategy,
441            eval_params: EvaluationParams::default(),
442            metric_calculators,
443            performance_history: VecDeque::new(),
444            baseline_comparisons: HashMap::new(),
445            robustness_results: HashMap::new(),
446        })
447    }
448
449    /// Evaluate transformer optimizer performance
450    pub fn evaluate(
451        &mut self,
452        task_id: &str,
453        loss_trajectory: &[T],
454        gradient_norms: &[T],
455        wall_time: T,
456        memory_usage: u64,
457    ) -> Result<EvaluationResult<T>> {
458        let eval_id = format!("eval_{}_{}", task_id, self.performance_history.len());
459
460        // Compute convergence information
461        let convergence_info = self.compute_convergence_info(loss_trajectory, gradient_norms)?;
462
463        // Compute efficiency metrics
464        let efficiency_metrics =
465            self.compute_efficiency_metrics(wall_time, memory_usage, loss_trajectory.len())?;
466
467        // Compute performance metrics
468        let mut metrics = HashMap::new();
469        metrics.insert("final_loss".to_string(), convergence_info.final_loss);
470        metrics.insert(
471            "convergence_rate".to_string(),
472            convergence_info.convergence_rate,
473        );
474        metrics.insert(
475            "sample_efficiency".to_string(),
476            efficiency_metrics.sample_efficiency,
477        );
478
479        // Strategy-specific metrics. Each variant contributes a distinct
480        // family of measurements; `Comprehensive` runs all of them.
481        let strategy = self.strategy;
482        let run_all = matches!(strategy, EvaluationStrategy::Comprehensive);
483
484        if run_all || matches!(strategy, EvaluationStrategy::Robustness) {
485            for (key, value) in self.robustness_metrics(task_id, loss_trajectory)? {
486                metrics.insert(key, value);
487            }
488        }
489        if run_all || matches!(strategy, EvaluationStrategy::FewShot) {
490            for (key, value) in self.few_shot_metrics(loss_trajectory)? {
491                metrics.insert(key, value);
492            }
493        }
494        if run_all || matches!(strategy, EvaluationStrategy::MultiTask) {
495            for (key, value) in self.multi_task_metrics() {
496                metrics.insert(key, value);
497            }
498        }
499        if run_all || matches!(strategy, EvaluationStrategy::CrossDomain) {
500            for (key, value) in self.cross_domain_metrics(task_id, convergence_info.final_loss) {
501                metrics.insert(key, value);
502            }
503        }
504        if run_all || matches!(strategy, EvaluationStrategy::ContinualLearning) {
505            for (key, value) in self.continual_learning_metrics(task_id, &convergence_info) {
506                metrics.insert(key, value);
507            }
508        }
509        if run_all || matches!(strategy, EvaluationStrategy::Efficiency) {
510            for (key, value) in Self::efficiency_extra_metrics(&efficiency_metrics) {
511                metrics.insert(key, value);
512            }
513        }
514
515        // Update metric calculators
516        for (metric_name, metric_value) in &metrics {
517            if let Some(calculator) = self.metric_calculators.get_mut(metric_name) {
518                calculator.update(*metric_value)?;
519            }
520        }
521
522        // Compute statistical significance if baseline exists
523        let statistical_significance = self.compute_statistical_significance(&metrics)?;
524
525        let result = EvaluationResult {
526            eval_id,
527            task_id: task_id.to_string(),
528            metrics,
529            convergence_info,
530            efficiency_metrics,
531            statistical_significance,
532            timestamp: self.performance_history.len(),
533        };
534
535        self.performance_history.push_back(result.clone());
536        if self.performance_history.len() > 1000 {
537            self.performance_history.pop_front();
538        }
539
540        Ok(result)
541    }
542
543    /// Compute convergence information from trajectories
544    fn compute_convergence_info(
545        &self,
546        loss_trajectory: &[T],
547        gradient_norms: &[T],
548    ) -> Result<ConvergenceInfo<T>> {
549        if loss_trajectory.is_empty() {
550            return Err(OptimError::InvalidConfig(
551                "Empty loss trajectory".to_string(),
552            ));
553        }
554
555        let Some(&final_loss) = loss_trajectory.last() else {
556            return Err(OptimError::InvalidConfig(
557                "Empty loss trajectory".to_string(),
558            ));
559        };
560        let initial_loss = loss_trajectory[0];
561
562        // Detect convergence
563        let (converged, steps_to_convergence) = self.detect_convergence(loss_trajectory)?;
564
565        // Compute convergence rate
566        let convergence_rate = if loss_trajectory.len() > 1 {
567            let improvement = (initial_loss - final_loss)
568                / initial_loss
569                    .max(scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero()));
570            improvement
571                / scirs2_core::numeric::NumCast::from(loss_trajectory.len() as f64)
572                    .unwrap_or_else(|| T::one())
573        } else {
574            T::zero()
575        };
576
577        Ok(ConvergenceInfo {
578            converged,
579            steps_to_convergence,
580            final_loss,
581            convergence_rate,
582            loss_trajectory: loss_trajectory.to_vec(),
583            gradient_norms: gradient_norms.to_vec(),
584        })
585    }
586
587    /// Detect convergence from a loss trajectory.
588    ///
589    /// Two *disjoint* windows of `convergence_window` samples are compared; the
590    /// trajectory is declared converged once their relative difference stays
591    /// below the tolerance for `convergence_consecutive_hits` consecutive
592    /// positions. The scan runs to the end of the trajectory (inclusive), so
593    /// the final sample is not ignored, and all index arithmetic is bounded
594    /// below by the loop start, so no `usize` underflow is possible.
595    fn detect_convergence(&self, loss_trajectory: &[T]) -> Result<(bool, Option<usize>)> {
596        let window_size = self.eval_params.convergence_window.max(1);
597        let required_hits = self.eval_params.convergence_consecutive_hits.max(1);
598        let tolerance = self.eval_params.convergence_tolerance;
599
600        // Two disjoint windows are needed before any comparison is possible.
601        if loss_trajectory.len() < 2 * window_size {
602            return Ok((false, None));
603        }
604
605        let window_len: T =
606            scirs2_core::numeric::NumCast::from(window_size as f64).unwrap_or_else(|| T::one());
607        let floor: T = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero());
608        let mut consecutive_hits = 0usize;
609
610        for end in (2 * window_size)..=loss_trajectory.len() {
611            let current_window = &loss_trajectory[end - window_size..end];
612            let prev_window = &loss_trajectory[end - 2 * window_size..end - window_size];
613
614            let current_avg =
615                current_window.iter().cloned().fold(T::zero(), |a, b| a + b) / window_len;
616            let prev_avg = prev_window.iter().cloned().fold(T::zero(), |a, b| a + b) / window_len;
617
618            let change = (current_avg - prev_avg).abs() / prev_avg.abs().max(floor);
619
620            if change < tolerance {
621                consecutive_hits += 1;
622                if consecutive_hits >= required_hits {
623                    return Ok((true, Some(end)));
624                }
625            } else {
626                consecutive_hits = 0;
627            }
628        }
629
630        Ok((false, None))
631    }
632
633    /// Compute efficiency metrics
634    fn compute_efficiency_metrics(
635        &self,
636        wall_time: T,
637        memory_usage: u64,
638        num_steps: usize,
639    ) -> Result<EfficiencyMetrics<T>> {
640        let flops = (num_steps as u64) * 1000; // Simplified FLOP estimation
641        let parameter_efficiency = T::one()
642            / (scirs2_core::numeric::NumCast::from(memory_usage as f64)
643                .unwrap_or_else(|| T::zero())
644                + T::one());
645        let sample_efficiency = scirs2_core::numeric::NumCast::from(num_steps as f64)
646            .unwrap_or_else(|| T::zero())
647            / (wall_time + T::one());
648        let energy_consumption = wall_time
649            * scirs2_core::numeric::NumCast::from(memory_usage as f64).unwrap_or_else(|| T::zero())
650            * scirs2_core::numeric::NumCast::from(1e-9).unwrap_or_else(|| T::zero());
651
652        Ok(EfficiencyMetrics {
653            wall_time,
654            flops,
655            peak_memory: memory_usage,
656            parameter_efficiency,
657            sample_efficiency,
658            energy_consumption,
659        })
660    }
661
662    /// Robustness metrics: re-score the trajectory under deterministic
663    /// multiplicative noise and report how much the convergence rate degrades.
664    fn robustness_metrics(&self, task_id: &str, loss_trajectory: &[T]) -> Result<Vec<(String, T)>> {
665        if loss_trajectory.is_empty() {
666            return Ok(Vec::new());
667        }
668
669        let severity = self.eval_params.robustness_severity.to_f64().unwrap_or(0.0);
670        let replicas = self.eval_params.robustness_replicas.max(1);
671        // Seed derived from the task so the perturbations are reproducible.
672        let seed = self
673            .eval_params
674            .robustness_seed
675            .wrapping_add(Self::task_seed(task_id));
676        let mut rng = scirs2_core::random::Random::seed(seed);
677
678        let clean = self.relative_improvement(loss_trajectory);
679        let mut degraded_sum = T::zero();
680        let mut converged_count = 0usize;
681
682        for _ in 0..replicas {
683            let perturbed: Vec<T> = loss_trajectory
684                .iter()
685                .map(|&x| {
686                    let noise = 1.0 + severity * (rng.gen_range(0.0..1.0) * 2.0 - 1.0);
687                    let factor: T =
688                        scirs2_core::numeric::NumCast::from(noise).unwrap_or_else(|| T::one());
689                    x * factor
690                })
691                .collect();
692            degraded_sum = degraded_sum + self.relative_improvement(&perturbed);
693            if self.detect_convergence(&perturbed)?.0 {
694                converged_count += 1;
695            }
696        }
697
698        let replicas_t: T =
699            scirs2_core::numeric::NumCast::from(replicas as f64).unwrap_or_else(|| T::one());
700        let degraded_mean = degraded_sum / replicas_t;
701        // 1 means "noise did not hurt"; smaller means more degradation.
702        let robustness_score = if clean.abs() > T::zero() {
703            T::one() - (clean - degraded_mean).abs() / clean.abs()
704        } else {
705            T::one()
706        };
707
708        Ok(vec![
709            ("robustness_score".to_string(), robustness_score),
710            (
711                "robustness_perturbed_improvement".to_string(),
712                degraded_mean,
713            ),
714            (
715                "robustness_converged_fraction".to_string(),
716                scirs2_core::numeric::NumCast::from(converged_count as f64 / replicas as f64)
717                    .unwrap_or_else(|| T::zero()),
718            ),
719        ])
720    }
721
722    /// Few-shot metrics: score the optimizer using only the first
723    /// `few_shot_samples` steps of the trajectory.
724    fn few_shot_metrics(&self, loss_trajectory: &[T]) -> Result<Vec<(String, T)>> {
725        if loss_trajectory.is_empty() {
726            return Ok(Vec::new());
727        }
728        let take = self
729            .eval_params
730            .few_shot_samples
731            .max(1)
732            .min(loss_trajectory.len());
733        let prefix = &loss_trajectory[..take];
734        let final_loss = prefix.last().copied().unwrap_or_else(T::zero);
735
736        Ok(vec![
737            ("few_shot_samples".to_string(), {
738                let value: T =
739                    scirs2_core::numeric::NumCast::from(take as f64).unwrap_or_else(|| T::zero());
740                value
741            }),
742            ("few_shot_final_loss".to_string(), final_loss),
743            (
744                "few_shot_improvement".to_string(),
745                self.relative_improvement(prefix),
746            ),
747        ])
748    }
749
750    /// Multi-task metrics: aggregate across every task seen so far.
751    fn multi_task_metrics(&self) -> Vec<(String, T)> {
752        let mut task_ids: Vec<&str> = self
753            .performance_history
754            .iter()
755            .map(|r| r.task_id.as_str())
756            .collect();
757        task_ids.sort_unstable();
758        task_ids.dedup();
759
760        if self.performance_history.is_empty() {
761            return Vec::new();
762        }
763
764        let count: T = scirs2_core::numeric::NumCast::from(self.performance_history.len() as f64)
765            .unwrap_or_else(|| T::one());
766        let mean_final_loss = self
767            .performance_history
768            .iter()
769            .map(|r| r.convergence_info.final_loss)
770            .fold(T::zero(), |a, b| a + b)
771            / count;
772
773        vec![
774            ("multi_task_mean_final_loss".to_string(), mean_final_loss),
775            ("multi_task_task_count".to_string(), {
776                let value: T = scirs2_core::numeric::NumCast::from(task_ids.len() as f64)
777                    .unwrap_or_else(|| T::zero());
778                value
779            }),
780        ]
781    }
782
783    /// Cross-domain metrics: spread of final losses across distinct tasks,
784    /// including the evaluation currently being computed.
785    fn cross_domain_metrics(&self, task_id: &str, current_loss: T) -> Vec<(String, T)> {
786        let mut per_task: HashMap<&str, (T, usize)> = HashMap::new();
787        for result in &self.performance_history {
788            let entry = per_task
789                .entry(result.task_id.as_str())
790                .or_insert((T::zero(), 0));
791            entry.0 = entry.0 + result.convergence_info.final_loss;
792            entry.1 += 1;
793        }
794        let entry = per_task.entry(task_id).or_insert((T::zero(), 0));
795        entry.0 = entry.0 + current_loss;
796        entry.1 += 1;
797
798        if per_task.len() < 2 {
799            return Vec::new();
800        }
801
802        let means: Vec<T> = per_task
803            .values()
804            .map(|(sum, n)| {
805                let n_t: T =
806                    scirs2_core::numeric::NumCast::from(*n as f64).unwrap_or_else(|| T::one());
807                *sum / n_t
808            })
809            .collect();
810        let n_t: T =
811            scirs2_core::numeric::NumCast::from(means.len() as f64).unwrap_or_else(|| T::one());
812        let mean = means.iter().cloned().fold(T::zero(), |a, b| a + b) / n_t;
813        let variance = means
814            .iter()
815            .map(|&m| (m - mean) * (m - mean))
816            .fold(T::zero(), |a, b| a + b)
817            / n_t;
818
819        vec![
820            ("cross_domain_mean_final_loss".to_string(), mean),
821            ("cross_domain_variance".to_string(), variance),
822        ]
823    }
824
825    /// Continual-learning metrics: forgetting relative to this task's previous
826    /// evaluation (positive means the loss got worse).
827    fn continual_learning_metrics(
828        &self,
829        task_id: &str,
830        convergence_info: &ConvergenceInfo<T>,
831    ) -> Vec<(String, T)> {
832        let previous = self
833            .performance_history
834            .iter()
835            .rev()
836            .find(|r| r.task_id == task_id);
837
838        match previous {
839            Some(previous) => vec![(
840                "forgetting".to_string(),
841                convergence_info.final_loss - previous.convergence_info.final_loss,
842            )],
843            None => vec![("forgetting".to_string(), T::zero())],
844        }
845    }
846
847    /// Efficiency-focused derived metrics.
848    fn efficiency_extra_metrics(efficiency: &EfficiencyMetrics<T>) -> Vec<(String, T)> {
849        let flops: T = scirs2_core::numeric::NumCast::from(efficiency.flops as f64)
850            .unwrap_or_else(|| T::zero());
851        let memory: T = scirs2_core::numeric::NumCast::from(efficiency.peak_memory as f64)
852            .unwrap_or_else(|| T::zero());
853        vec![
854            (
855                "flops_per_second".to_string(),
856                flops / (efficiency.wall_time + T::one()),
857            ),
858            ("peak_memory_bytes".to_string(), memory),
859            (
860                "energy_consumption".to_string(),
861                efficiency.energy_consumption,
862            ),
863        ]
864    }
865
866    /// Relative improvement of a loss trajectory (first vs last sample).
867    fn relative_improvement(&self, loss_trajectory: &[T]) -> T {
868        let (Some(&first), Some(&last)) = (loss_trajectory.first(), loss_trajectory.last()) else {
869            return T::zero();
870        };
871        let floor: T = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero());
872        (first - last) / first.abs().max(floor)
873    }
874
875    /// Deterministic seed contribution from a task identifier (FNV-1a).
876    fn task_seed(task_id: &str) -> u64 {
877        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
878        for byte in task_id.as_bytes() {
879            hash ^= u64::from(*byte);
880            hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
881        }
882        hash
883    }
884
885    /// Standard normal survival function via an Abramowitz-Stegun erfc approximation.
886    fn normal_survival(z: f64) -> f64 {
887        let x = z.abs() / std::f64::consts::SQRT_2;
888        let t = 1.0 / (1.0 + 0.327_591_1 * x);
889        let poly = t
890            * (0.254_829_592
891                + t * (-0.284_496_736
892                    + t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
893        let erf = 1.0 - poly * (-x * x).exp();
894        let tail = 0.5 * (1.0 - erf);
895        if z >= 0.0 {
896            tail
897        } else {
898            1.0 - tail
899        }
900    }
901
902    /// Compute statistical significance against the registered baselines.
903    ///
904    /// Returns `None` when no baseline supplies the metric under test, when
905    /// there are fewer than two observations, or when the observed sample has
906    /// zero spread (in which case no variance can be estimated). No fabricated
907    /// p-values are produced.
908    fn compute_statistical_significance(
909        &self,
910        metrics: &HashMap<String, T>,
911    ) -> Result<Option<StatisticalSignificance<T>>> {
912        let Some(&current) = metrics.get("final_loss") else {
913            return Ok(None);
914        };
915
916        let baseline_mean = self
917            .baseline_comparisons
918            .values()
919            .filter_map(|b| b.baseline_performance.get("final_loss").copied())
920            .fold(None::<(T, usize)>, |acc, value| match acc {
921                Some((sum, n)) => Some((sum + value, n + 1)),
922                None => Some((value, 1)),
923            });
924        let Some((baseline_sum, baseline_count)) = baseline_mean else {
925            return Ok(None);
926        };
927        let baseline_count_t: T =
928            scirs2_core::numeric::NumCast::from(baseline_count as f64).unwrap_or_else(|| T::one());
929        let baseline = baseline_sum / baseline_count_t;
930
931        // Sample statistics of the observed final losses.
932        let mut samples: Vec<T> = self
933            .performance_history
934            .iter()
935            .map(|r| r.convergence_info.final_loss)
936            .collect();
937        samples.push(current);
938        if samples.len() < 2 {
939            return Ok(None);
940        }
941
942        let n = samples.len();
943        let n_t: T = scirs2_core::numeric::NumCast::from(n as f64).unwrap_or_else(|| T::one());
944        let mean = samples.iter().cloned().fold(T::zero(), |a, b| a + b) / n_t;
945        let denominator: T =
946            scirs2_core::numeric::NumCast::from((n - 1) as f64).unwrap_or_else(|| T::one());
947        let variance = samples
948            .iter()
949            .map(|&x| (x - mean) * (x - mean))
950            .fold(T::zero(), |a, b| a + b)
951            / denominator;
952        let std_dev = variance.sqrt();
953
954        if std_dev <= T::zero() {
955            return Ok(None);
956        }
957
958        let effect_size = (baseline - mean) / std_dev;
959        let test_statistic = effect_size * n_t.sqrt();
960        let z = test_statistic.to_f64().unwrap_or(0.0);
961        let p_value: T = scirs2_core::numeric::NumCast::from(
962            (2.0 * Self::normal_survival(z.abs())).clamp(0.0, 1.0),
963        )
964        .unwrap_or_else(|| T::one());
965
966        // Two-sided normal confidence interval for the mean at the configured level.
967        let alpha = 1.0 - self.eval_params.confidence_level.to_f64().unwrap_or(0.95);
968        let critical = Self::normal_quantile(1.0 - alpha / 2.0);
969        let critical_t: T =
970            scirs2_core::numeric::NumCast::from(critical).unwrap_or_else(|| T::one());
971        let half_width = critical_t * std_dev / n_t.sqrt();
972
973        // Power of the two-sided test at the observed effect size.
974        let power: T = scirs2_core::numeric::NumCast::from(
975            (Self::normal_survival(critical - z.abs())).clamp(0.0, 1.0),
976        )
977        .unwrap_or_else(|| T::zero());
978
979        Ok(Some(StatisticalSignificance {
980            p_value,
981            effect_size,
982            confidence_interval: (mean - half_width, mean + half_width),
983            statistical_power: power,
984            test_statistic,
985        }))
986    }
987
988    /// Inverse standard normal CDF (Acklam's rational approximation).
989    fn normal_quantile(p: f64) -> f64 {
990        if p <= 0.0 {
991            return f64::NEG_INFINITY;
992        }
993        if p >= 1.0 {
994            return f64::INFINITY;
995        }
996        const A: [f64; 6] = [
997            -3.969_683_028_665_376e1,
998            2.209_460_984_245_205e2,
999            -2.759_285_104_469_687e2,
1000            1.383_577_518_672_69e2,
1001            -3.066_479_806_614_716e1,
1002            2.506_628_277_459_239,
1003        ];
1004        const B: [f64; 5] = [
1005            -5.447_609_879_822_406e1,
1006            1.615_858_368_580_409e2,
1007            -1.556_989_798_598_866e2,
1008            6.680_131_188_771_972e1,
1009            -1.328_068_155_288_572e1,
1010        ];
1011        const C: [f64; 6] = [
1012            -7.784_894_002_430_293e-3,
1013            -3.223_964_580_411_365e-1,
1014            -2.400_758_277_161_838e0,
1015            -2.549_732_539_343_734e0,
1016            4.374_664_141_464_968e0,
1017            2.938_163_982_698_783e0,
1018        ];
1019        const D: [f64; 4] = [
1020            7.784_695_709_041_462e-3,
1021            3.224_671_290_700_398e-1,
1022            2.445_134_137_142_996e0,
1023            3.754_408_661_907_416e0,
1024        ];
1025        let p_low = 0.02425;
1026        if p < p_low {
1027            let q = (-2.0 * p.ln()).sqrt();
1028            (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1029                / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1030        } else if p <= 1.0 - p_low {
1031            let q = p - 0.5;
1032            let r = q * q;
1033            (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
1034                / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
1035        } else {
1036            let q = (-2.0 * (1.0 - p).ln()).sqrt();
1037            -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1038                / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1039        }
1040    }
1041
1042    /// Add baseline for comparison
1043    pub fn add_baseline(
1044        &mut self,
1045        baseline_name: String,
1046        baseline_performance: HashMap<String, T>,
1047    ) -> Result<()> {
1048        let comparison = BaselineComparison {
1049            baseline_performance: baseline_performance.clone(),
1050        };
1051
1052        self.baseline_comparisons.insert(baseline_name, comparison);
1053        Ok(())
1054    }
1055
1056    /// Run robustness evaluation
1057    pub fn evaluate_robustness(
1058        &mut self,
1059        task_id: &str,
1060        robustness_tests: &RobustnessTestSuite<T>,
1061    ) -> Result<HashMap<String, T>> {
1062        let mut robustness_scores = HashMap::new();
1063
1064        // Evaluate noise robustness
1065        let mut noise_score = T::zero();
1066        for noise_test in &robustness_tests.noise_tests {
1067            noise_score = noise_score + (T::one() - noise_test.performance_degradation);
1068        }
1069        if !robustness_tests.noise_tests.is_empty() {
1070            noise_score = noise_score
1071                / scirs2_core::numeric::NumCast::from(robustness_tests.noise_tests.len() as f64)
1072                    .unwrap_or_else(|| T::one());
1073        }
1074        robustness_scores.insert("noise_robustness".to_string(), noise_score);
1075
1076        // Evaluate adversarial robustness
1077        let mut adversarial_score = T::zero();
1078        for adv_test in &robustness_tests.adversarial_tests {
1079            adversarial_score = adversarial_score + adv_test.robustness_score;
1080        }
1081        if !robustness_tests.adversarial_tests.is_empty() {
1082            adversarial_score = adversarial_score
1083                / scirs2_core::numeric::NumCast::from(
1084                    robustness_tests.adversarial_tests.len() as f64
1085                )
1086                .unwrap_or_else(|| T::one());
1087        }
1088        robustness_scores.insert("adversarial_robustness".to_string(), adversarial_score);
1089
1090        // Evaluate hyperparameter sensitivity
1091        let mut sensitivity_score = T::zero();
1092        for sens_test in &robustness_tests.sensitivity_tests {
1093            sensitivity_score =
1094                sensitivity_score + (T::one() / (T::one() + sens_test.sensitivity_score));
1095        }
1096        if !robustness_tests.sensitivity_tests.is_empty() {
1097            sensitivity_score = sensitivity_score
1098                / scirs2_core::numeric::NumCast::from(
1099                    robustness_tests.sensitivity_tests.len() as f64
1100                )
1101                .unwrap_or_else(|| T::one());
1102        }
1103        robustness_scores.insert("hyperparameter_robustness".to_string(), sensitivity_score);
1104
1105        // Retain the scores under the task they were measured for; a later call
1106        // for the same task replaces its entry rather than accumulating stale
1107        // suites.
1108        self.robustness_results
1109            .insert(task_id.to_string(), robustness_scores.clone());
1110
1111        Ok(robustness_scores)
1112    }
1113
1114    /// Aggregated value of a tracked metric, under that calculator's
1115    /// [`AggregationMethod`].
1116    ///
1117    /// Every [`Self::evaluate`] call feeds the per-metric calculators, but
1118    /// nothing could read the aggregate back: `MetricCalculator` was written to
1119    /// and never queried.
1120    pub fn aggregated_metric(&self, metric_name: &str) -> Option<Result<T>> {
1121        self.metric_calculators
1122            .get(metric_name)
1123            .map(|c| c.get_aggregated_value())
1124    }
1125
1126    /// Names of the metrics this evaluator aggregates.
1127    pub fn tracked_metrics(&self) -> impl Iterator<Item = &str> {
1128        self.metric_calculators.keys().map(String::as_str)
1129    }
1130
1131    /// Robustness scores recorded for `task_id` by a previous
1132    /// [`Self::evaluate_robustness`] call, or `None` if that task has not been
1133    /// evaluated for robustness.
1134    pub fn robustness_results(&self, task_id: &str) -> Option<&HashMap<String, T>> {
1135        self.robustness_results.get(task_id)
1136    }
1137
1138    /// Task ids that have a recorded robustness evaluation.
1139    pub fn robustness_evaluated_tasks(&self) -> impl Iterator<Item = &str> {
1140        self.robustness_results.keys().map(String::as_str)
1141    }
1142
1143    /// Get comprehensive evaluation summary
1144    pub fn get_evaluation_summary(&self) -> HashMap<String, T> {
1145        let mut summary = HashMap::new();
1146
1147        // Overall performance statistics
1148        if !self.performance_history.is_empty() {
1149            let history_len: T =
1150                scirs2_core::numeric::NumCast::from(self.performance_history.len() as f64)
1151                    .unwrap_or_else(|| T::one());
1152            // Average final performance
1153            let avg_final_loss = self
1154                .performance_history
1155                .iter()
1156                .map(|result| result.convergence_info.final_loss)
1157                .fold(T::zero(), |a, b| a + b)
1158                / history_len;
1159            summary.insert("average_final_loss".to_string(), avg_final_loss);
1160
1161            // Average convergence rate
1162            let avg_convergence_rate = self
1163                .performance_history
1164                .iter()
1165                .map(|result| result.convergence_info.convergence_rate)
1166                .fold(T::zero(), |a, b| a + b)
1167                / history_len;
1168            summary.insert("average_convergence_rate".to_string(), avg_convergence_rate);
1169
1170            // Success rate (convergence)
1171            let success_count = self
1172                .performance_history
1173                .iter()
1174                .filter(|result| result.convergence_info.converged)
1175                .count();
1176            let success_rate = scirs2_core::numeric::NumCast::from(success_count as f64)
1177                .unwrap_or_else(|| T::zero())
1178                / history_len;
1179            summary.insert("success_rate".to_string(), success_rate);
1180        }
1181
1182        summary.insert(
1183            "total_evaluations".to_string(),
1184            scirs2_core::numeric::NumCast::from(self.performance_history.len() as f64)
1185                .unwrap_or_else(|| T::zero()),
1186        );
1187        summary
1188    }
1189
1190    /// Reset evaluator state
1191    pub fn reset(&mut self) {
1192        self.performance_history.clear();
1193        self.baseline_comparisons.clear();
1194        self.robustness_results.clear();
1195
1196        for calculator in self.metric_calculators.values_mut() {
1197            calculator.reset();
1198        }
1199    }
1200
1201    /// Update evaluation parameters
1202    pub fn set_parameters(&mut self, params: EvaluationParams<T>) {
1203        self.eval_params = params;
1204    }
1205
1206    /// Get the active evaluation strategy
1207    pub fn strategy(&self) -> EvaluationStrategy {
1208        self.strategy
1209    }
1210
1211    /// Change the active evaluation strategy
1212    pub fn set_strategy(&mut self, strategy: EvaluationStrategy) {
1213        self.strategy = strategy;
1214    }
1215
1216    /// Get the recorded evaluation history
1217    pub fn performance_history(&self) -> &VecDeque<EvaluationResult<T>> {
1218        &self.performance_history
1219    }
1220}
1221
1222impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> MetricCalculator<T> {
1223    /// A calculator that aggregates its history with `aggregation_method`.
1224    ///
1225    /// The metric's name is the key it is stored under in
1226    /// `TransformerEvaluator::metric_calculators`; it used to be duplicated into
1227    /// a `metric_name` field that nothing ever read, so the two could disagree.
1228    fn new(aggregation_method: AggregationMethod) -> Result<Self> {
1229        Ok(Self {
1230            historical_values: VecDeque::new(),
1231            aggregation_method,
1232        })
1233    }
1234
1235    fn update(&mut self, value: T) -> Result<()> {
1236        self.historical_values.push_back(value);
1237        if self.historical_values.len() > 1000 {
1238            self.historical_values.pop_front();
1239        }
1240        Ok(())
1241    }
1242
1243    /// Aggregate the recorded history under this calculator's
1244    /// [`AggregationMethod`].
1245    pub fn get_aggregated_value(&self) -> Result<T> {
1246        if self.historical_values.is_empty() {
1247            return Ok(T::zero());
1248        }
1249
1250        match self.aggregation_method {
1251            AggregationMethod::Mean => {
1252                let sum = self
1253                    .historical_values
1254                    .iter()
1255                    .cloned()
1256                    .fold(T::zero(), |a, b| a + b);
1257                let count: T =
1258                    scirs2_core::numeric::NumCast::from(self.historical_values.len() as f64)
1259                        .unwrap_or_else(|| T::one());
1260                Ok(sum / count)
1261            }
1262            AggregationMethod::Max => Ok(self
1263                .historical_values
1264                .iter()
1265                .cloned()
1266                .fold(T::zero(), |a, b| a.max(b))),
1267            AggregationMethod::Min => Ok(self.historical_values.iter().cloned().fold(
1268                scirs2_core::numeric::NumCast::from(f64::INFINITY).unwrap_or_else(|| T::zero()),
1269                |a, b| a.min(b),
1270            )),
1271            _ => Ok(self.historical_values.back().copied().unwrap_or(T::zero())),
1272        }
1273    }
1274
1275    fn reset(&mut self) {
1276        self.historical_values.clear();
1277    }
1278}
1279
1280impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for EvaluationParams<T> {
1281    fn default() -> Self {
1282        Self {
1283            num_episodes: 10,
1284            eval_frequency: 100,
1285            convergence_tolerance: scirs2_core::numeric::NumCast::from(1e-6)
1286                .unwrap_or_else(|| T::zero()),
1287            max_eval_steps: 10000,
1288            confidence_level: scirs2_core::numeric::NumCast::from(0.95)
1289                .unwrap_or_else(|| T::zero()),
1290            bootstrap_samples: 1000,
1291            cv_folds: 5,
1292            robustness_severity: scirs2_core::numeric::NumCast::from(0.1)
1293                .unwrap_or_else(|| T::zero()),
1294            convergence_window: 10,
1295            convergence_consecutive_hits: 2,
1296            few_shot_samples: 5,
1297            robustness_replicas: 8,
1298            robustness_seed: 0x5EED_C0DE,
1299        }
1300    }
1301}
1302
1303#[cfg(test)]
1304mod tests {
1305    use super::*;
1306
1307    fn descending(n: usize) -> Vec<f64> {
1308        (0..n).map(|i| 1.0 / (1.0 + i as f64)).collect()
1309    }
1310
1311    #[test]
1312    fn short_trajectories_do_not_underflow() {
1313        let evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1314            .expect("evaluator creation");
1315        for len in 0..25 {
1316            let trajectory: Vec<f64> = descending(len);
1317            let detected = evaluator
1318                .detect_convergence(&trajectory)
1319                .expect("convergence detection must not panic");
1320            if len < 20 {
1321                assert!(!detected.0, "len {len} cannot be converged yet");
1322            }
1323        }
1324    }
1325
1326    #[test]
1327    fn descending_trajectory_is_not_converged() {
1328        let evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1329            .expect("evaluator creation");
1330        // Geometric decay by half each step: the moving averages keep changing.
1331        let trajectory: Vec<f64> = (0..64).map(|i| 0.5_f64.powi(i)).collect();
1332        let (converged, _) = evaluator
1333            .detect_convergence(&trajectory)
1334            .expect("convergence detection");
1335        assert!(
1336            !converged,
1337            "steadily descending trajectory is not converged"
1338        );
1339    }
1340
1341    #[test]
1342    fn flat_trajectory_is_converged() {
1343        let evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1344            .expect("evaluator creation");
1345        let trajectory = vec![1.0_f64; 64];
1346        let (converged, step) = evaluator
1347            .detect_convergence(&trajectory)
1348            .expect("convergence detection");
1349        assert!(converged);
1350        assert!(step.is_some());
1351    }
1352
1353    #[test]
1354    fn evaluation_strategies_produce_different_metric_sets() {
1355        let trajectory = descending(40);
1356        let grads = vec![0.1_f64; 40];
1357
1358        let mut single = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1359            .expect("evaluator creation");
1360        let single_result = single
1361            .evaluate("task", &trajectory, &grads, 1.0, 1024)
1362            .expect("single-task evaluation");
1363
1364        let mut robust = TransformerEvaluator::<f64>::new(EvaluationStrategy::Robustness)
1365            .expect("evaluator creation");
1366        let robust_result = robust
1367            .evaluate("task", &trajectory, &grads, 1.0, 1024)
1368            .expect("robustness evaluation");
1369
1370        let mut few_shot = TransformerEvaluator::<f64>::new(EvaluationStrategy::FewShot)
1371            .expect("evaluator creation");
1372        let few_shot_result = few_shot
1373            .evaluate("task", &trajectory, &grads, 1.0, 1024)
1374            .expect("few-shot evaluation");
1375
1376        assert!(!single_result.metrics().contains_key("robustness_score"));
1377        assert!(robust_result.metrics().contains_key("robustness_score"));
1378        assert!(few_shot_result
1379            .metrics()
1380            .contains_key("few_shot_final_loss"));
1381        assert!(!few_shot_result.metrics().contains_key("robustness_score"));
1382    }
1383
1384    #[test]
1385    fn comprehensive_strategy_covers_every_family() {
1386        let trajectory = descending(40);
1387        let grads = vec![0.1_f64; 40];
1388        let mut evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::Comprehensive)
1389            .expect("evaluator creation");
1390        let _ = evaluator
1391            .evaluate("task_a", &trajectory, &grads, 1.0, 1024)
1392            .expect("evaluation a");
1393        let result = evaluator
1394            .evaluate("task_b", &trajectory, &grads, 1.0, 1024)
1395            .expect("evaluation b");
1396
1397        for key in [
1398            "robustness_score",
1399            "few_shot_final_loss",
1400            "multi_task_mean_final_loss",
1401            "cross_domain_variance",
1402            "forgetting",
1403            "flops_per_second",
1404        ] {
1405            assert!(result.metrics().contains_key(key), "missing metric {key}");
1406        }
1407    }
1408
1409    #[test]
1410    fn robustness_metrics_are_deterministic() {
1411        let trajectory = descending(40);
1412        let grads = vec![0.1_f64; 40];
1413        let run = || {
1414            let mut evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::Robustness)
1415                .expect("evaluator creation");
1416            evaluator
1417                .evaluate("task", &trajectory, &grads, 1.0, 1024)
1418                .expect("evaluation")
1419                .metrics()["robustness_score"]
1420        };
1421        assert_eq!(run(), run());
1422    }
1423
1424    #[test]
1425    fn statistical_significance_requires_a_baseline() {
1426        let trajectory = descending(40);
1427        let grads = vec![0.1_f64; 40];
1428        let mut evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1429            .expect("evaluator creation");
1430
1431        let result = evaluator
1432            .evaluate("task", &trajectory, &grads, 1.0, 1024)
1433            .expect("evaluation");
1434        assert!(
1435            result.statistical_significance().is_none(),
1436            "no baseline means no p-value"
1437        );
1438
1439        let mut baseline = HashMap::new();
1440        baseline.insert("final_loss".to_string(), 0.5_f64);
1441        evaluator
1442            .add_baseline("adam".to_string(), baseline)
1443            .expect("baseline registration");
1444
1445        // A second, different trajectory gives the sample a non-zero spread.
1446        let other: Vec<f64> = descending(40).iter().map(|v| v * 0.5).collect();
1447        let result = evaluator
1448            .evaluate("task", &other, &grads, 1.0, 1024)
1449            .expect("evaluation");
1450        let significance = result
1451            .statistical_significance()
1452            .expect("baseline registered and variance estimable");
1453        assert!((0.0..=1.0).contains(&significance.p_value()));
1454        assert!(significance.effect_size().is_finite());
1455        let (low, high) = significance.confidence_interval();
1456        assert!(low <= high);
1457    }
1458
1459    #[test]
1460    fn degenerate_samples_yield_no_significance() {
1461        let trajectory = descending(40);
1462        let grads = vec![0.1_f64; 40];
1463        let mut evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1464            .expect("evaluator creation");
1465        let mut baseline = HashMap::new();
1466        baseline.insert("final_loss".to_string(), 0.5_f64);
1467        evaluator
1468            .add_baseline("adam".to_string(), baseline)
1469            .expect("baseline registration");
1470
1471        let _ = evaluator
1472            .evaluate("task", &trajectory, &grads, 1.0, 1024)
1473            .expect("evaluation");
1474        // Identical repeat: zero spread, so no variance can be estimated.
1475        let result = evaluator
1476            .evaluate("task", &trajectory, &grads, 1.0, 1024)
1477            .expect("evaluation");
1478        assert!(result.statistical_significance().is_none());
1479    }
1480
1481    #[test]
1482    fn empty_trajectory_is_rejected() {
1483        let mut evaluator = TransformerEvaluator::<f64>::new(EvaluationStrategy::SingleTask)
1484            .expect("evaluator creation");
1485        let result = evaluator.evaluate("task", &[], &[], 1.0, 0);
1486        assert!(result.is_err());
1487    }
1488}