1use std::fmt::Debug;
2use scirs2_core::numeric::Float;
8use std::collections::{HashMap, VecDeque};
9
10use crate::error::{OptimError, Result};
11
12#[derive(Debug, Clone, Copy)]
14pub enum EvaluationStrategy {
15 SingleTask,
17 MultiTask,
19 CrossDomain,
21 FewShot,
23 ContinualLearning,
25 Robustness,
27 Efficiency,
29 Comprehensive,
31}
32
33#[derive(Debug, Clone)]
35pub struct TransformerEvaluator<T: Float + Debug + Send + Sync + 'static> {
36 strategy: EvaluationStrategy,
38
39 eval_params: EvaluationParams<T>,
41
42 metric_calculators: HashMap<String, MetricCalculator<T>>,
44
45 performance_history: VecDeque<EvaluationResult<T>>,
47
48 baseline_comparisons: HashMap<String, BaselineComparison<T>>,
50
51 robustness_results: HashMap<String, HashMap<String, T>>,
58}
59
60#[derive(Debug, Clone)]
62pub struct EvaluationParams<T: Float + Debug + Send + Sync + 'static> {
63 pub num_episodes: usize,
65
66 pub eval_frequency: usize,
68
69 pub convergence_tolerance: T,
71
72 pub max_eval_steps: usize,
74
75 pub confidence_level: T,
77
78 pub bootstrap_samples: usize,
80
81 pub cv_folds: usize,
83
84 pub robustness_severity: T,
86
87 pub convergence_window: usize,
89
90 pub convergence_consecutive_hits: usize,
92
93 pub few_shot_samples: usize,
95
96 pub robustness_replicas: usize,
98
99 pub robustness_seed: u64,
101}
102
103#[derive(Debug, Clone)]
105pub struct EvaluationResult<T: Float + Debug + Send + Sync + 'static> {
106 eval_id: String,
108
109 task_id: String,
111
112 metrics: HashMap<String, T>,
114
115 convergence_info: ConvergenceInfo<T>,
117
118 efficiency_metrics: EfficiencyMetrics<T>,
120
121 statistical_significance: Option<StatisticalSignificance<T>>,
123
124 timestamp: usize,
126}
127
128impl<T: Float + Debug + Send + Sync + 'static> EvaluationResult<T> {
129 pub fn eval_id(&self) -> &str {
131 &self.eval_id
132 }
133
134 pub fn task_id(&self) -> &str {
136 &self.task_id
137 }
138
139 pub fn metrics(&self) -> &HashMap<String, T> {
141 &self.metrics
142 }
143
144 pub fn convergence_info(&self) -> &ConvergenceInfo<T> {
146 &self.convergence_info
147 }
148
149 pub fn efficiency_metrics(&self) -> &EfficiencyMetrics<T> {
151 &self.efficiency_metrics
152 }
153
154 pub fn statistical_significance(&self) -> Option<&StatisticalSignificance<T>> {
156 self.statistical_significance.as_ref()
157 }
158
159 pub fn timestamp(&self) -> usize {
161 self.timestamp
162 }
163}
164
165impl<T: Float + Debug + Send + Sync + 'static> ConvergenceInfo<T> {
166 pub fn converged(&self) -> bool {
168 self.converged
169 }
170
171 pub fn steps_to_convergence(&self) -> Option<usize> {
173 self.steps_to_convergence
174 }
175
176 pub fn final_loss(&self) -> T {
178 self.final_loss
179 }
180
181 pub fn convergence_rate(&self) -> T {
183 self.convergence_rate
184 }
185
186 pub fn loss_trajectory(&self) -> &[T] {
188 &self.loss_trajectory
189 }
190
191 pub fn gradient_norms(&self) -> &[T] {
193 &self.gradient_norms
194 }
195}
196
197impl<T: Float + Debug + Send + Sync + 'static> EfficiencyMetrics<T> {
198 pub fn wall_time(&self) -> T {
200 self.wall_time
201 }
202
203 pub fn flops(&self) -> u64 {
205 self.flops
206 }
207
208 pub fn peak_memory(&self) -> u64 {
210 self.peak_memory
211 }
212
213 pub fn parameter_efficiency(&self) -> T {
215 self.parameter_efficiency
216 }
217
218 pub fn sample_efficiency(&self) -> T {
220 self.sample_efficiency
221 }
222
223 pub fn energy_consumption(&self) -> T {
225 self.energy_consumption
226 }
227}
228
229impl<T: Float + Debug + Send + Sync + 'static> StatisticalSignificance<T> {
230 pub fn p_value(&self) -> T {
232 self.p_value
233 }
234
235 pub fn effect_size(&self) -> T {
237 self.effect_size
238 }
239
240 pub fn confidence_interval(&self) -> (T, T) {
242 self.confidence_interval
243 }
244
245 pub fn statistical_power(&self) -> T {
247 self.statistical_power
248 }
249
250 pub fn test_statistic(&self) -> T {
252 self.test_statistic
253 }
254}
255
256#[derive(Debug, Clone)]
258pub struct ConvergenceInfo<T: Float + Debug + Send + Sync + 'static> {
259 converged: bool,
261
262 steps_to_convergence: Option<usize>,
264
265 final_loss: T,
267
268 convergence_rate: T,
270
271 loss_trajectory: Vec<T>,
273
274 gradient_norms: Vec<T>,
276}
277
278#[derive(Debug, Clone)]
280pub struct EfficiencyMetrics<T: Float + Debug + Send + Sync + 'static> {
281 wall_time: T,
283
284 flops: u64,
286
287 peak_memory: u64,
289
290 parameter_efficiency: T,
292
293 sample_efficiency: T,
295
296 energy_consumption: T,
298}
299
300#[derive(Debug, Clone)]
302pub struct StatisticalSignificance<T: Float + Debug + Send + Sync + 'static> {
303 p_value: T,
305
306 effect_size: T,
308
309 confidence_interval: (T, T),
311
312 statistical_power: T,
314
315 test_statistic: T,
317}
318
319#[derive(Debug, Clone)]
321pub struct MetricCalculator<T: Float + Debug + Send + Sync + 'static> {
322 historical_values: VecDeque<T>,
324
325 aggregation_method: AggregationMethod,
327}
328
329#[derive(Debug, Clone)]
331pub struct BaselineComparison<T: Float + Debug + Send + Sync + 'static> {
332 baseline_performance: HashMap<String, T>,
334}
335
336#[derive(Debug, Clone)]
338pub struct RobustnessTestSuite<T: Float + Debug + Send + Sync + 'static> {
339 pub noise_tests: Vec<NoiseTest<T>>,
341
342 pub adversarial_tests: Vec<AdversarialTest<T>>,
344
345 pub sensitivity_tests: Vec<SensitivityTest<T>>,
347
348 pub distribution_tests: Vec<DistributionTest<T>>,
350}
351
352#[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#[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#[derive(Debug, Clone, Copy)]
395pub enum NoiseType {
396 Gaussian,
397 Uniform,
398 SaltPepper,
399 Dropout,
400}
401
402#[derive(Debug, Clone, Copy)]
404pub enum AttackType {
405 FGSM,
406 PGD,
407 CarliniWagner,
408 DeepFool,
409}
410
411#[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 pub fn new(strategy: EvaluationStrategy) -> Result<Self> {
423 let mut metric_calculators = HashMap::new();
424
425 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 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 let convergence_info = self.compute_convergence_info(loss_trajectory, gradient_norms)?;
462
463 let efficiency_metrics =
465 self.compute_efficiency_metrics(wall_time, memory_usage, loss_trajectory.len())?;
466
467 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 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 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 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 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 let (converged, steps_to_convergence) = self.detect_convergence(loss_trajectory)?;
564
565 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 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 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 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; 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 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 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 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 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 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 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 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 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 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 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 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 fn compute_statistical_significance(
909 &self,
910 metrics: &HashMap<String, T>,
911 ) -> Result<Option<StatisticalSignificance<T>>> {
912 let Some(¤t) = 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 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 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 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 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 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 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 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 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 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 self.robustness_results
1109 .insert(task_id.to_string(), robustness_scores.clone());
1110
1111 Ok(robustness_scores)
1112 }
1113
1114 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 pub fn tracked_metrics(&self) -> impl Iterator<Item = &str> {
1128 self.metric_calculators.keys().map(String::as_str)
1129 }
1130
1131 pub fn robustness_results(&self, task_id: &str) -> Option<&HashMap<String, T>> {
1135 self.robustness_results.get(task_id)
1136 }
1137
1138 pub fn robustness_evaluated_tasks(&self) -> impl Iterator<Item = &str> {
1140 self.robustness_results.keys().map(String::as_str)
1141 }
1142
1143 pub fn get_evaluation_summary(&self) -> HashMap<String, T> {
1145 let mut summary = HashMap::new();
1146
1147 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 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 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 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 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 pub fn set_parameters(&mut self, params: EvaluationParams<T>) {
1203 self.eval_params = params;
1204 }
1205
1206 pub fn strategy(&self) -> EvaluationStrategy {
1208 self.strategy
1209 }
1210
1211 pub fn set_strategy(&mut self, strategy: EvaluationStrategy) {
1213 self.strategy = strategy;
1214 }
1215
1216 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 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 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 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 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 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}