Skip to main content

optirs_core/curriculum_optimization/
mod.rs

1// Curriculum optimization for adaptive training
2//
3// This module provides curriculum learning capabilities including task difficulty progression,
4// sample importance weighting, and adversarial training support.
5
6use crate::error::{OptimError, Result};
7use crate::utils::{scalar_or, try_scalar};
8use scirs2_core::ndarray::{Array, Dimension, ScalarOperand, Zip};
9use scirs2_core::numeric::Float;
10use std::collections::{HashMap, VecDeque};
11use std::fmt::Debug;
12use std::marker::PhantomData;
13
14/// Curriculum learning strategy
15#[derive(Debug, Clone)]
16pub enum CurriculumStrategy {
17    /// Linear difficulty progression
18    Linear {
19        /// Starting difficulty (0.0 to 1.0)
20        start_difficulty: f64,
21        /// Ending difficulty (0.0 to 1.0)
22        end_difficulty: f64,
23        /// Number of steps to reach end difficulty
24        num_steps: usize,
25    },
26    /// Exponential difficulty progression
27    Exponential {
28        /// Starting difficulty (0.0 to 1.0)
29        start_difficulty: f64,
30        /// Ending difficulty (0.0 to 1.0)
31        end_difficulty: f64,
32        /// Growth rate
33        growth_rate: f64,
34    },
35    /// Performance-based curriculum
36    PerformanceBased {
37        /// Threshold for advancing difficulty
38        advance_threshold: f64,
39        /// Threshold for reducing difficulty
40        reduce_threshold: f64,
41        /// Difficulty adjustment step size
42        adjustment_step: f64,
43        /// Window size for performance averaging
44        window_size: usize,
45    },
46    /// Custom curriculum with predefined schedule
47    Custom {
48        /// Difficulty schedule (step -> difficulty)
49        schedule: HashMap<usize, f64>,
50        /// Default difficulty for unspecified steps
51        default_difficulty: f64,
52    },
53}
54
55/// Sample importance weighting strategy
56#[derive(Debug, Clone)]
57pub enum ImportanceWeightingStrategy {
58    /// Uniform weighting (all samples equal)
59    Uniform,
60    /// Loss-based weighting (higher loss = higher weight)
61    LossBased {
62        /// Temperature parameter for softmax weighting
63        temperature: f64,
64        /// Minimum weight to avoid zero weights
65        min_weight: f64,
66    },
67    /// Gradient norm based weighting
68    GradientNormBased {
69        /// Temperature parameter
70        temperature: f64,
71        /// Minimum weight
72        min_weight: f64,
73    },
74    /// Uncertainty-based weighting
75    UncertaintyBased {
76        /// Temperature parameter
77        temperature: f64,
78        /// Minimum weight
79        min_weight: f64,
80    },
81    /// Age-based weighting (older samples get higher weight)
82    AgeBased {
83        /// Decay factor for age
84        decayfactor: f64,
85    },
86}
87
88/// Adversarial training configuration
89#[derive(Debug, Clone)]
90pub struct AdversarialConfig<A: Float> {
91    /// Adversarial perturbation magnitude
92    pub epsilon: A,
93    /// Number of adversarial steps
94    pub num_steps: usize,
95    /// Step size for adversarial perturbation
96    pub step_size: A,
97    /// Type of adversarial attack
98    pub attack_type: AdversarialAttack,
99    /// Regularization weight for adversarial loss
100    pub adversarial_weight: A,
101}
102
103/// Types of adversarial attacks
104#[derive(Debug, Clone, Copy)]
105pub enum AdversarialAttack {
106    /// Fast Gradient Sign Method (FGSM)
107    FGSM,
108    /// Projected Gradient Descent (PGD)
109    PGD,
110    /// Basic Iterative Method (BIM)
111    BIM,
112    /// Momentum Iterative Method (MIM)
113    MIM,
114}
115
116/// Curriculum learning manager
117#[derive(Debug)]
118pub struct CurriculumManager<A: Float, D: Dimension> {
119    /// Curriculum strategy
120    strategy: CurriculumStrategy,
121    /// Current difficulty level
122    current_difficulty: f64,
123    /// Current step count
124    step_count: usize,
125    /// Performance history
126    performance_history: VecDeque<A>,
127    /// Sample difficulty scores
128    sample_difficulties: HashMap<usize, f64>,
129    /// Importance weighting strategy
130    importance_strategy: ImportanceWeightingStrategy,
131    /// Sample weights
132    sample_weights: HashMap<usize, A>,
133    /// Adversarial training configuration
134    adversarial_config: Option<AdversarialConfig<A>>,
135    /// Phantom data for dimension
136    _phantom: PhantomData<D>,
137}
138
139impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> CurriculumManager<A, D> {
140    /// Create a new curriculum manager
141    pub fn new(
142        strategy: CurriculumStrategy,
143        importance_strategy: ImportanceWeightingStrategy,
144    ) -> Self {
145        let initial_difficulty = match &strategy {
146            CurriculumStrategy::Linear {
147                start_difficulty, ..
148            } => *start_difficulty,
149            CurriculumStrategy::Exponential {
150                start_difficulty, ..
151            } => *start_difficulty,
152            CurriculumStrategy::PerformanceBased { .. } => 0.1, // Start easy
153            CurriculumStrategy::Custom {
154                default_difficulty, ..
155            } => *default_difficulty,
156        };
157
158        Self {
159            strategy,
160            current_difficulty: initial_difficulty,
161            step_count: 0,
162            performance_history: VecDeque::new(),
163            sample_difficulties: HashMap::new(),
164            importance_strategy,
165            sample_weights: HashMap::new(),
166            adversarial_config: None,
167            _phantom: PhantomData,
168        }
169    }
170
171    /// Enable adversarial training
172    pub fn enable_adversarial_training(&mut self, config: AdversarialConfig<A>) {
173        self.adversarial_config = Some(config);
174    }
175
176    /// Disable adversarial training
177    pub fn disable_adversarial_training(&mut self) {
178        self.adversarial_config = None;
179    }
180
181    /// Update curriculum based on performance
182    pub fn update_curriculum(&mut self, performance: A) -> Result<()> {
183        self.performance_history.push_back(performance);
184        self.step_count += 1;
185
186        // Update difficulty based on strategy
187        match &self.strategy {
188            CurriculumStrategy::Linear {
189                start_difficulty,
190                end_difficulty,
191                num_steps,
192            } => {
193                let progress = (self.step_count as f64) / (*num_steps as f64);
194                let progress = progress.min(1.0);
195                self.current_difficulty =
196                    start_difficulty + progress * (end_difficulty - start_difficulty);
197            }
198            CurriculumStrategy::Exponential {
199                start_difficulty,
200                end_difficulty,
201                growth_rate,
202            } => {
203                let progress = 1.0 - (-growth_rate * self.step_count as f64).exp();
204                self.current_difficulty =
205                    start_difficulty + progress * (end_difficulty - start_difficulty);
206            }
207            CurriculumStrategy::PerformanceBased {
208                advance_threshold,
209                reduce_threshold,
210                adjustment_step,
211                window_size,
212            } => {
213                if self.performance_history.len() >= *window_size {
214                    // Keep only recent performance
215                    while self.performance_history.len() > *window_size {
216                        self.performance_history.pop_front();
217                    }
218
219                    // Calculate average performance
220                    let avg_performance = self
221                        .performance_history
222                        .iter()
223                        .fold(A::zero(), |acc, &perf| acc + perf)
224                        / try_scalar::<A, _>(self.performance_history.len())?;
225
226                    let avg_perf_f64 = avg_performance.to_f64().unwrap_or(0.0);
227
228                    // Adjust difficulty based on performance
229                    if avg_perf_f64 > *advance_threshold {
230                        self.current_difficulty =
231                            (self.current_difficulty + adjustment_step).min(1.0);
232                    } else if avg_perf_f64 < *reduce_threshold {
233                        self.current_difficulty =
234                            (self.current_difficulty - adjustment_step).max(0.0);
235                    }
236                }
237            }
238            CurriculumStrategy::Custom {
239                schedule,
240                default_difficulty,
241            } => {
242                self.current_difficulty = schedule
243                    .get(&self.step_count)
244                    .copied()
245                    .unwrap_or(*default_difficulty);
246            }
247        }
248
249        Ok(())
250    }
251
252    /// Set difficulty score for a sample
253    pub fn set_sample_difficulty(&mut self, sampleid: usize, difficulty: f64) {
254        self.sample_difficulties.insert(sampleid, difficulty);
255    }
256
257    /// Check if sample should be included based on current difficulty
258    pub fn should_include_sample(&self, sampleid: usize) -> bool {
259        if let Some(&sample_difficulty) = self.sample_difficulties.get(&sampleid) {
260            sample_difficulty <= self.current_difficulty
261        } else {
262            true // Include unknown samples
263        }
264    }
265
266    /// Get current difficulty level
267    pub fn get_current_difficulty(&self) -> f64 {
268        self.current_difficulty
269    }
270
271    /// Compute importance weights for samples
272    pub fn compute_sample_weights(
273        &mut self,
274        sampleids: &[usize],
275        losses: &[A],
276        gradient_norms: Option<&[A]>,
277        uncertainties: Option<&[A]>,
278    ) -> Result<()> {
279        if sampleids.len() != losses.len() {
280            return Err(OptimError::DimensionMismatch(
281                "Sample IDs and losses must have same length".to_string(),
282            ));
283        }
284
285        match &self.importance_strategy {
286            ImportanceWeightingStrategy::Uniform => {
287                let uniform_weight = A::one();
288                for &sampleid in sampleids {
289                    self.sample_weights.insert(sampleid, uniform_weight);
290                }
291            }
292            ImportanceWeightingStrategy::LossBased {
293                temperature,
294                min_weight,
295            } => {
296                self.compute_loss_based_weights(sampleids, losses, *temperature, *min_weight)?;
297            }
298            ImportanceWeightingStrategy::GradientNormBased {
299                temperature,
300                min_weight,
301            } => {
302                if let Some(grad_norms) = gradient_norms {
303                    self.compute_gradient_norm_weights(
304                        sampleids,
305                        grad_norms,
306                        *temperature,
307                        *min_weight,
308                    )?;
309                } else {
310                    // Fall back to uniform weights
311                    for &sampleid in sampleids {
312                        self.sample_weights.insert(sampleid, A::one());
313                    }
314                }
315            }
316            ImportanceWeightingStrategy::UncertaintyBased {
317                temperature,
318                min_weight,
319            } => {
320                if let Some(uncertainties_array) = uncertainties {
321                    self.compute_uncertainty_weights(
322                        sampleids,
323                        uncertainties_array,
324                        *temperature,
325                        *min_weight,
326                    )?;
327                } else {
328                    // Fall back to uniform weights
329                    for &sampleid in sampleids {
330                        self.sample_weights.insert(sampleid, A::one());
331                    }
332                }
333            }
334            ImportanceWeightingStrategy::AgeBased { decayfactor } => {
335                self.compute_age_based_weights(sampleids, *decayfactor)?;
336            }
337        }
338
339        Ok(())
340    }
341
342    /// Compute loss-based importance weights
343    fn compute_loss_based_weights(
344        &mut self,
345        sampleids: &[usize],
346        losses: &[A],
347        temperature: f64,
348        min_weight: f64,
349    ) -> Result<()> {
350        // Compute softmax weights based on losses
351        let temp = try_scalar::<A, _>(temperature)?;
352        let min_w = try_scalar::<A, _>(min_weight)?;
353
354        // Find max loss for numerical stability
355        let max_loss = losses.iter().fold(A::neg_infinity(), |a, &b| A::max(a, b));
356
357        // Compute unnormalized weights
358        let mut unnormalized_weights = Vec::new();
359        for &loss in losses {
360            let normalized_loss = (loss - max_loss) / temp;
361            unnormalized_weights.push(A::exp(normalized_loss));
362        }
363
364        // Normalize weights
365        let sum_weights: A = unnormalized_weights
366            .iter()
367            .fold(A::zero(), |acc, &w| acc + w);
368
369        for (i, &sampleid) in sampleids.iter().enumerate() {
370            let weight = A::max(min_w, unnormalized_weights[i] / sum_weights);
371            self.sample_weights.insert(sampleid, weight);
372        }
373
374        Ok(())
375    }
376
377    /// Compute gradient norm based weights
378    fn compute_gradient_norm_weights(
379        &mut self,
380        sampleids: &[usize],
381        gradient_norms: &[A],
382        temperature: f64,
383        min_weight: f64,
384    ) -> Result<()> {
385        let temp = try_scalar::<A, _>(temperature)?;
386        let min_w = try_scalar::<A, _>(min_weight)?;
387
388        // Find max gradient norm for numerical stability
389        let max_norm = gradient_norms
390            .iter()
391            .fold(A::neg_infinity(), |a, &b| A::max(a, b));
392
393        // Compute softmax weights
394        let mut unnormalized_weights = Vec::new();
395        for &norm in gradient_norms {
396            let normalized_norm = (norm - max_norm) / temp;
397            unnormalized_weights.push(A::exp(normalized_norm));
398        }
399
400        let sum_weights: A = unnormalized_weights
401            .iter()
402            .fold(A::zero(), |acc, &w| acc + w);
403
404        for (i, &sampleid) in sampleids.iter().enumerate() {
405            let weight = A::max(min_w, unnormalized_weights[i] / sum_weights);
406            self.sample_weights.insert(sampleid, weight);
407        }
408
409        Ok(())
410    }
411
412    /// Compute uncertainty-based weights
413    fn compute_uncertainty_weights(
414        &mut self,
415        sampleids: &[usize],
416        uncertainties: &[A],
417        temperature: f64,
418        min_weight: f64,
419    ) -> Result<()> {
420        let temp = try_scalar::<A, _>(temperature)?;
421        let min_w = try_scalar::<A, _>(min_weight)?;
422
423        // Find max uncertainty for numerical stability
424        let max_uncertainty = uncertainties
425            .iter()
426            .fold(A::neg_infinity(), |a, &b| A::max(a, b));
427
428        // Compute softmax weights (higher uncertainty = higher weight)
429        let mut unnormalized_weights = Vec::new();
430        for &uncertainty in uncertainties {
431            let normalized_uncertainty = (uncertainty - max_uncertainty) / temp;
432            unnormalized_weights.push(A::exp(normalized_uncertainty));
433        }
434
435        let sum_weights: A = unnormalized_weights
436            .iter()
437            .fold(A::zero(), |acc, &w| acc + w);
438
439        for (i, &sampleid) in sampleids.iter().enumerate() {
440            let weight = A::max(min_w, unnormalized_weights[i] / sum_weights);
441            self.sample_weights.insert(sampleid, weight);
442        }
443
444        Ok(())
445    }
446
447    /// Compute age-based weights
448    fn compute_age_based_weights(&mut self, sampleids: &[usize], decayfactor: f64) -> Result<()> {
449        let decay = try_scalar::<A, _>(decayfactor)?;
450
451        for &sampleid in sampleids {
452            // Simple age-based weighting (older samples get exponentially higher weight)
453            let age = try_scalar::<A, _>(self.step_count.saturating_sub(sampleid))?;
454            let weight = A::exp(decay * age);
455            self.sample_weights.insert(sampleid, weight);
456        }
457
458        Ok(())
459    }
460
461    /// Get importance weight for a sample
462    pub fn get_sample_weight(&self, sampleid: usize) -> A {
463        self.sample_weights
464            .get(&sampleid)
465            .copied()
466            .unwrap_or_else(|| A::one())
467    }
468
469    /// Generate adversarial examples
470    pub fn generate_adversarial_examples(
471        &self,
472        inputs: &Array<A, D>,
473        gradients: &Array<A, D>,
474    ) -> Result<Array<A, D>> {
475        if let Some(config) = &self.adversarial_config {
476            match config.attack_type {
477                AdversarialAttack::FGSM => self.fgsm_attack(inputs, gradients, config),
478                AdversarialAttack::PGD => self.pgd_attack(inputs, gradients, config),
479                AdversarialAttack::BIM => self.bim_attack(inputs, gradients, config),
480                AdversarialAttack::MIM => self.mim_attack(inputs, gradients, config),
481            }
482        } else {
483            Ok(inputs.clone()) // No adversarial training
484        }
485    }
486
487    /// Fast Gradient Sign Method (FGSM)
488    fn fgsm_attack(
489        &self,
490        inputs: &Array<A, D>,
491        gradients: &Array<A, D>,
492        config: &AdversarialConfig<A>,
493    ) -> Result<Array<A, D>> {
494        let mut adversarial = inputs.clone();
495
496        // Sign of gradients
497        let sign_gradients = gradients.mapv(|x| if x >= A::zero() { A::one() } else { -A::one() });
498
499        // Add perturbation
500        Zip::from(&mut adversarial)
501            .and(&sign_gradients)
502            .for_each(|x, &sign| {
503                *x = *x + config.epsilon * sign;
504            });
505
506        Ok(adversarial)
507    }
508
509    /// Projected Gradient Descent (PGD)
510    fn pgd_attack(
511        &self,
512        inputs: &Array<A, D>,
513        gradients: &Array<A, D>,
514        config: &AdversarialConfig<A>,
515    ) -> Result<Array<A, D>> {
516        let mut adversarial = inputs.clone();
517
518        // Multiple PGD steps
519        for _ in 0..config.num_steps {
520            // Gradient step
521            let sign_gradients =
522                gradients.mapv(|x| if x >= A::zero() { A::one() } else { -A::one() });
523
524            Zip::from(&mut adversarial)
525                .and(&sign_gradients)
526                .for_each(|x, &sign| {
527                    *x = *x + config.step_size * sign;
528                });
529
530            // Project back to epsilon ball
531            Zip::from(&mut adversarial)
532                .and(inputs)
533                .for_each(|adv, &orig| {
534                    let diff = *adv - orig;
535                    let clamped_diff = A::max(-config.epsilon, A::min(config.epsilon, diff));
536                    *adv = orig + clamped_diff;
537                });
538        }
539
540        Ok(adversarial)
541    }
542
543    /// Basic Iterative Method (BIM)
544    fn bim_attack(
545        &self,
546        inputs: &Array<A, D>,
547        gradients: &Array<A, D>,
548        config: &AdversarialConfig<A>,
549    ) -> Result<Array<A, D>> {
550        // BIM is similar to PGD but with smaller steps
551        let mut modified_config = config.clone();
552        modified_config.step_size = config.epsilon / try_scalar::<A, _>(config.num_steps)?;
553
554        self.pgd_attack(inputs, gradients, &modified_config)
555    }
556
557    /// Momentum Iterative Method (MIM)
558    fn mim_attack(
559        &self,
560        inputs: &Array<A, D>,
561        gradients: &Array<A, D>,
562        config: &AdversarialConfig<A>,
563    ) -> Result<Array<A, D>> {
564        let mut adversarial = inputs.clone();
565        let mut momentum = Array::zeros(inputs.raw_dim());
566        let decayfactor = try_scalar::<A, _>(1.0)?; // Momentum decay factor
567
568        for _ in 0..config.num_steps {
569            // Update momentum
570            let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
571            let normalized_gradients = if grad_norm > A::zero() {
572                gradients.mapv(|x| x / grad_norm)
573            } else {
574                gradients.clone()
575            };
576
577            Zip::from(&mut momentum)
578                .and(&normalized_gradients)
579                .for_each(|m, &g| {
580                    *m = decayfactor * *m + g;
581                });
582
583            // Apply momentum-based update
584            let momentum_signs =
585                momentum.mapv(|x| if x >= A::zero() { A::one() } else { -A::one() });
586
587            Zip::from(&mut adversarial)
588                .and(&momentum_signs)
589                .for_each(|x, &sign| {
590                    *x = *x + config.step_size * sign;
591                });
592
593            // Project back to epsilon ball
594            Zip::from(&mut adversarial)
595                .and(inputs)
596                .for_each(|adv, &orig| {
597                    let diff = *adv - orig;
598                    let clamped_diff = A::max(-config.epsilon, A::min(config.epsilon, diff));
599                    *adv = orig + clamped_diff;
600                });
601        }
602
603        Ok(adversarial)
604    }
605
606    /// Get filtered samples based on current curriculum
607    pub fn filter_samples(&self, sampleids: &[usize]) -> Vec<usize> {
608        sampleids
609            .iter()
610            .copied()
611            .filter(|&id| self.should_include_sample(id))
612            .collect()
613    }
614
615    /// Get performance history
616    pub fn get_performance_history(&self) -> &VecDeque<A> {
617        &self.performance_history
618    }
619
620    /// Get step count
621    pub fn step_count(&self) -> usize {
622        self.step_count
623    }
624
625    /// Reset curriculum state
626    pub fn reset(&mut self) {
627        self.step_count = 0;
628        self.performance_history.clear();
629        self.sample_weights.clear();
630        self.current_difficulty = match &self.strategy {
631            CurriculumStrategy::Linear {
632                start_difficulty, ..
633            } => *start_difficulty,
634            CurriculumStrategy::Exponential {
635                start_difficulty, ..
636            } => *start_difficulty,
637            CurriculumStrategy::PerformanceBased { .. } => 0.1,
638            CurriculumStrategy::Custom {
639                default_difficulty, ..
640            } => *default_difficulty,
641        };
642    }
643
644    /// Export curriculum state for analysis
645    pub fn export_state(&self) -> CurriculumState<A> {
646        CurriculumState {
647            current_difficulty: self.current_difficulty,
648            step_count: self.step_count,
649            performance_history: self.performance_history.clone(),
650            sample_weights: self.sample_weights.clone(),
651            has_adversarial: self.adversarial_config.is_some(),
652        }
653    }
654}
655
656/// Curriculum state for analysis and visualization
657#[derive(Debug, Clone)]
658pub struct CurriculumState<A: Float> {
659    /// Current difficulty level
660    pub current_difficulty: f64,
661    /// Current step count
662    pub step_count: usize,
663    /// Performance history
664    pub performance_history: VecDeque<A>,
665    /// Sample weights
666    pub sample_weights: HashMap<usize, A>,
667    /// Whether adversarial training is enabled
668    pub has_adversarial: bool,
669}
670
671/// Adaptive curriculum that automatically adjusts strategy
672#[derive(Debug)]
673pub struct AdaptiveCurriculum<A: Float, D: Dimension> {
674    /// Collection of curriculum managers with different strategies
675    curricula: Vec<CurriculumManager<A, D>>,
676    /// Current active curriculum index
677    active_curriculum: usize,
678    /// Performance tracking for each curriculum
679    curriculum_performance: Vec<VecDeque<A>>,
680    /// Switch threshold for changing curriculum
681    switchthreshold: A,
682    /// Minimum steps before switching
683    min_steps_before_switch: usize,
684    /// Steps since last switch
685    steps_since_switch: usize,
686    /// Phantom data for dimension
687    _phantom: PhantomData<D>,
688}
689
690impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> AdaptiveCurriculum<A, D> {
691    /// Create a new adaptive curriculum
692    pub fn new(curricula: Vec<CurriculumManager<A, D>>, switchthreshold: A) -> Self {
693        let num_curricula = curricula.len();
694        Self {
695            curricula,
696            active_curriculum: 0,
697            curriculum_performance: vec![VecDeque::new(); num_curricula],
698            switchthreshold,
699            min_steps_before_switch: 100,
700            steps_since_switch: 0,
701            _phantom: PhantomData,
702        }
703    }
704
705    /// Update with performance and potentially switch curriculum
706    pub fn update(&mut self, performance: A) -> Result<()> {
707        // Update current curriculum
708        self.curricula[self.active_curriculum].update_curriculum(performance)?;
709        self.curriculum_performance[self.active_curriculum].push_back(performance);
710        self.steps_since_switch += 1;
711
712        // Consider switching if enough steps have passed
713        if self.steps_since_switch >= self.min_steps_before_switch {
714            self.consider_curriculum_switch()?;
715        }
716
717        Ok(())
718    }
719
720    /// Consider switching to a better performing curriculum
721    fn consider_curriculum_switch(&mut self) -> Result<()> {
722        let current_performance = self.get_average_performance(self.active_curriculum);
723        let mut best_curriculum = self.active_curriculum;
724        let mut best_performance = current_performance;
725
726        // Find best performing curriculum
727        for (i, _) in self.curricula.iter().enumerate() {
728            if i != self.active_curriculum {
729                let perf = self.get_average_performance(i);
730                if perf > best_performance + self.switchthreshold {
731                    best_performance = perf;
732                    best_curriculum = i;
733                }
734            }
735        }
736
737        // Switch if a better curriculum is found
738        if best_curriculum != self.active_curriculum {
739            self.active_curriculum = best_curriculum;
740            self.steps_since_switch = 0;
741        }
742
743        Ok(())
744    }
745
746    /// Get average performance for a curriculum
747    fn get_average_performance(&self, curriculumidx: usize) -> A {
748        let perf_history = &self.curriculum_performance[curriculumidx];
749        if perf_history.is_empty() {
750            A::zero()
751        } else {
752            let sum = perf_history.iter().fold(A::zero(), |acc, &perf| acc + perf);
753            sum / scalar_or(perf_history.len(), A::one())
754        }
755    }
756
757    /// Get active curriculum manager
758    pub fn active_curriculum(&self) -> &CurriculumManager<A, D> {
759        &self.curricula[self.active_curriculum]
760    }
761
762    /// Get mutable active curriculum manager
763    pub fn active_curriculum_mut(&mut self) -> &mut CurriculumManager<A, D> {
764        &mut self.curricula[self.active_curriculum]
765    }
766
767    /// Get active curriculum index
768    pub fn active_curriculum_index(&self) -> usize {
769        self.active_curriculum
770    }
771
772    /// Get performance comparison across curricula
773    pub fn get_curriculum_comparison(&self) -> Vec<(usize, A)> {
774        (0..self.curricula.len())
775            .map(|i| (i, self.get_average_performance(i)))
776            .collect()
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783    use approx::assert_relative_eq;
784    use scirs2_core::ndarray::Array1;
785
786    #[test]
787    fn test_linear_curriculum() {
788        let strategy = CurriculumStrategy::Linear {
789            start_difficulty: 0.1,
790            end_difficulty: 1.0,
791            num_steps: 10,
792        };
793
794        let importance_strategy = ImportanceWeightingStrategy::Uniform;
795        let mut curriculum =
796            CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(strategy, importance_strategy);
797
798        // Test initial difficulty
799        assert_relative_eq!(curriculum.get_current_difficulty(), 0.1, epsilon = 1e-6);
800
801        // Update curriculum multiple times
802        for _ in 0..5 {
803            curriculum.update_curriculum(0.8).expect("unwrap failed");
804        }
805
806        // Difficulty should have increased
807        assert!(curriculum.get_current_difficulty() > 0.1);
808        assert!(curriculum.get_current_difficulty() <= 1.0);
809    }
810
811    #[test]
812    fn test_performance_based_curriculum() {
813        let strategy = CurriculumStrategy::PerformanceBased {
814            advance_threshold: 0.8,
815            reduce_threshold: 0.4,
816            adjustment_step: 0.1,
817            window_size: 3,
818        };
819
820        let importance_strategy = ImportanceWeightingStrategy::Uniform;
821        let mut curriculum =
822            CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(strategy, importance_strategy);
823
824        let initial_difficulty = curriculum.get_current_difficulty();
825
826        // Simulate good performance (should increase difficulty)
827        for _ in 0..5 {
828            curriculum.update_curriculum(0.9).expect("unwrap failed");
829        }
830
831        assert!(curriculum.get_current_difficulty() > initial_difficulty);
832    }
833
834    #[test]
835    fn test_sample_filtering() {
836        let strategy = CurriculumStrategy::Linear {
837            start_difficulty: 0.5,
838            end_difficulty: 0.5,
839            num_steps: 10,
840        };
841
842        let importance_strategy = ImportanceWeightingStrategy::Uniform;
843        let mut curriculum =
844            CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(strategy, importance_strategy);
845
846        // Set sample difficulties
847        curriculum.set_sample_difficulty(1, 0.3); // Easy
848        curriculum.set_sample_difficulty(2, 0.7); // Hard
849        curriculum.set_sample_difficulty(3, 0.5); // Medium
850
851        let sampleids = vec![1, 2, 3, 4]; // 4 has no difficulty set
852        let filtered = curriculum.filter_samples(&sampleids);
853
854        // Should include samples 1, 3, 4 (difficulty <= 0.5 or unknown)
855        assert_eq!(filtered.len(), 3);
856        assert!(filtered.contains(&1));
857        assert!(filtered.contains(&3));
858        assert!(filtered.contains(&4));
859        assert!(!filtered.contains(&2));
860    }
861
862    #[test]
863    fn test_loss_based_importance_weighting() {
864        let strategy = CurriculumStrategy::Linear {
865            start_difficulty: 0.5,
866            end_difficulty: 0.5,
867            num_steps: 10,
868        };
869
870        let importance_strategy = ImportanceWeightingStrategy::LossBased {
871            temperature: 1.0,
872            min_weight: 0.1,
873        };
874
875        let mut curriculum =
876            CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(strategy, importance_strategy);
877
878        let sampleids = vec![1, 2, 3];
879        let losses = vec![0.1, 1.0, 0.5]; // Low, high, medium loss
880
881        curriculum
882            .compute_sample_weights(&sampleids, &losses, None, None)
883            .expect("unwrap failed");
884
885        // Sample with highest loss should have highest weight
886        let weight1 = curriculum.get_sample_weight(1);
887        let weight2 = curriculum.get_sample_weight(2);
888        let weight3 = curriculum.get_sample_weight(3);
889
890        assert!(weight2 > weight3); // High loss > medium loss
891        assert!(weight3 > weight1); // Medium loss > low loss
892    }
893
894    #[test]
895    fn test_adversarial_config() {
896        let strategy = CurriculumStrategy::Linear {
897            start_difficulty: 0.5,
898            end_difficulty: 0.5,
899            num_steps: 10,
900        };
901
902        let importance_strategy = ImportanceWeightingStrategy::Uniform;
903        let mut curriculum =
904            CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(strategy, importance_strategy);
905
906        let adversarial_config = AdversarialConfig {
907            epsilon: 0.1,
908            num_steps: 5,
909            step_size: 0.02,
910            attack_type: AdversarialAttack::FGSM,
911            adversarial_weight: 0.5,
912        };
913
914        curriculum.enable_adversarial_training(adversarial_config);
915
916        let inputs = Array1::from_vec(vec![1.0, 2.0, 3.0]);
917        let gradients = Array1::from_vec(vec![0.1, -0.2, 0.3]);
918
919        let adversarial = curriculum
920            .generate_adversarial_examples(&inputs, &gradients)
921            .expect("unwrap failed");
922
923        // Adversarial examples should be different from original
924        assert_ne!(
925            adversarial.as_slice().expect("unwrap failed"),
926            inputs.as_slice().expect("unwrap failed")
927        );
928
929        // Check that perturbation is bounded
930        for (orig, adv) in inputs.iter().zip(adversarial.iter()) {
931            assert!((adv - orig).abs() <= 0.1 + 1e-6); // epsilon + small tolerance
932        }
933    }
934
935    #[test]
936    fn test_adaptive_curriculum() {
937        let strategy1 = CurriculumStrategy::Linear {
938            start_difficulty: 0.1,
939            end_difficulty: 0.5,
940            num_steps: 100,
941        };
942
943        let strategy2 = CurriculumStrategy::Linear {
944            start_difficulty: 0.2,
945            end_difficulty: 0.8,
946            num_steps: 100,
947        };
948
949        let importance_strategy = ImportanceWeightingStrategy::Uniform;
950        let curriculum1 = CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(
951            strategy1,
952            importance_strategy.clone(),
953        );
954        let curriculum2 = CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(
955            strategy2,
956            importance_strategy,
957        );
958
959        let mut adaptive = AdaptiveCurriculum::new(vec![curriculum1, curriculum2], 0.1);
960
961        assert_eq!(adaptive.active_curriculum_index(), 0);
962
963        // Update with some performance values
964        for _ in 0..150 {
965            adaptive.update(0.7).expect("unwrap failed");
966        }
967
968        // Should potentially have switched curriculum
969        let comparison = adaptive.get_curriculum_comparison();
970        assert_eq!(comparison.len(), 2);
971    }
972
973    #[test]
974    fn test_curriculum_state_export() {
975        let strategy = CurriculumStrategy::Linear {
976            start_difficulty: 0.1,
977            end_difficulty: 1.0,
978            num_steps: 10,
979        };
980
981        let importance_strategy = ImportanceWeightingStrategy::Uniform;
982        let mut curriculum =
983            CurriculumManager::<f64, scirs2_core::ndarray::Ix1>::new(strategy, importance_strategy);
984
985        curriculum.update_curriculum(0.8).expect("unwrap failed");
986        let state = curriculum.export_state();
987
988        assert_eq!(state.step_count, 1);
989        assert_eq!(state.performance_history.len(), 1);
990        assert!(!state.has_adversarial);
991    }
992}