Skip to main content

torsh_data/
online_transforms.rs

1//! Online data augmentation system for real-time transform application
2//!
3//! This module provides advanced online augmentation capabilities for dynamic,
4//! adaptive, and performance-aware data transformations during training.
5//!
6//! # Features
7//!
8//! - **Real-time augmentation**: OnlineAugmentationEngine for live transform application
9//! - **Intelligent caching**: Optional caching system with performance monitoring
10//! - **Dynamic strategies**: Epoch-based augmentation strategy switching
11//! - **Progressive augmentation**: Gradually increasing augmentation intensity
12//! - **Adaptive augmentation**: Performance-based intensity adjustment
13//! - **Async processing**: Queue-based augmentation for high-throughput scenarios
14
15use crate::transforms::Transform;
16use torsh_core::error::Result;
17use torsh_core::error::TorshError;
18
19#[cfg(not(feature = "std"))]
20use alloc::{boxed::Box, string::String, vec::Vec};
21
22use std::collections::HashMap;
23use std::sync::{Arc, RwLock};
24use std::time::Instant;
25
26#[cfg(feature = "std")]
27use scirs2_core::random::thread_rng;
28
29#[cfg(not(feature = "std"))]
30use scirs2_core::random::thread_rng;
31
32/// Online augmentation engine that applies transforms in real-time during data loading
33pub struct OnlineAugmentationEngine<T> {
34    transform_pipeline: Arc<dyn Transform<T, Output = T> + Send + Sync>,
35    cache: Arc<RwLock<HashMap<String, T>>>,
36    cache_enabled: bool,
37    max_cache_size: usize,
38    stats: Arc<RwLock<AugmentationStats>>,
39}
40
41/// Statistics for augmentation performance monitoring
42#[derive(Debug, Clone)]
43pub struct AugmentationStats {
44    pub total_transforms: u64,
45    pub cache_hits: u64,
46    pub cache_misses: u64,
47    pub total_time_ms: f64,
48    pub average_time_ms: f64,
49}
50
51impl Default for AugmentationStats {
52    fn default() -> Self {
53        Self {
54            total_transforms: 0,
55            cache_hits: 0,
56            cache_misses: 0,
57            total_time_ms: 0.0,
58            average_time_ms: 0.0,
59        }
60    }
61}
62
63impl<T: Clone + Send + Sync + 'static> OnlineAugmentationEngine<T> {
64    /// Create a new online augmentation engine
65    pub fn new<P: Transform<T, Output = T> + Send + Sync + 'static>(pipeline: P) -> Self {
66        Self {
67            transform_pipeline: Arc::new(pipeline),
68            cache: Arc::new(RwLock::new(HashMap::new())),
69            cache_enabled: false,
70            max_cache_size: 1000,
71            stats: Arc::new(RwLock::new(AugmentationStats::default())),
72        }
73    }
74
75    /// Enable caching of transformed data
76    pub fn with_cache(mut self, max_cache_size: usize) -> Self {
77        self.cache_enabled = true;
78        self.max_cache_size = max_cache_size;
79        self
80    }
81
82    /// Apply augmentation with optional caching
83    pub fn apply(&self, input: T, cache_key: Option<&str>) -> Result<T> {
84        let start_time = Instant::now();
85
86        // Check cache first if enabled
87        if self.cache_enabled {
88            if let Some(key) = cache_key {
89                let cache = self.cache.read().expect("lock should not be poisoned");
90                if let Some(cached_result) = cache.get(key) {
91                    self.update_stats(start_time, true);
92                    return Ok(cached_result.clone());
93                }
94            }
95        }
96
97        // Apply transformation
98        let result = self.transform_pipeline.transform(input)?;
99
100        // Cache result if enabled
101        if self.cache_enabled {
102            if let Some(key) = cache_key {
103                let mut cache = self.cache.write().expect("lock should not be poisoned");
104                if cache.len() < self.max_cache_size {
105                    cache.insert(key.to_string(), result.clone());
106                }
107            }
108        }
109
110        self.update_stats(start_time, false);
111        Ok(result)
112    }
113
114    /// Apply augmentation without caching
115    pub fn apply_uncached(&self, input: T) -> Result<T> {
116        let start_time = Instant::now();
117        let result = self.transform_pipeline.transform(input)?;
118        self.update_stats(start_time, false);
119        Ok(result)
120    }
121
122    /// Clear the cache
123    pub fn clear_cache(&self) {
124        if self.cache_enabled {
125            self.cache
126                .write()
127                .expect("lock should not be poisoned")
128                .clear();
129        }
130    }
131
132    /// Get cache size
133    pub fn cache_size(&self) -> usize {
134        if self.cache_enabled {
135            self.cache
136                .read()
137                .expect("lock should not be poisoned")
138                .len()
139        } else {
140            0
141        }
142    }
143
144    /// Get augmentation statistics
145    pub fn stats(&self) -> AugmentationStats {
146        self.stats
147            .read()
148            .expect("lock should not be poisoned")
149            .clone()
150    }
151
152    /// Reset statistics
153    pub fn reset_stats(&self) {
154        *self.stats.write().expect("lock should not be poisoned") = AugmentationStats::default();
155    }
156
157    fn update_stats(&self, start_time: Instant, was_cache_hit: bool) {
158        let duration = start_time.elapsed().as_secs_f64() * 1000.0;
159        let mut stats = self.stats.write().expect("lock should not be poisoned");
160
161        stats.total_transforms += 1;
162        stats.total_time_ms += duration;
163        stats.average_time_ms = stats.total_time_ms / stats.total_transforms as f64;
164
165        if was_cache_hit {
166            stats.cache_hits += 1;
167        } else {
168            stats.cache_misses += 1;
169        }
170    }
171}
172
173/// Dynamic augmentation strategy that can change parameters based on training progress
174pub struct DynamicAugmentationStrategy<T> {
175    strategies: Vec<StrategyConfig<T>>,
176    current_epoch: usize,
177    total_epochs: usize,
178}
179
180pub struct StrategyConfig<T> {
181    epoch_range: (usize, usize),
182    pipeline: Arc<dyn Transform<T, Output = T> + Send + Sync>,
183    weight: f32,
184}
185
186impl<T: Clone + Send + Sync + 'static> DynamicAugmentationStrategy<T> {
187    /// Create a new dynamic augmentation strategy
188    pub fn new(total_epochs: usize) -> Self {
189        Self {
190            strategies: Vec::new(),
191            current_epoch: 0,
192            total_epochs,
193        }
194    }
195
196    /// Add a strategy for specific epoch range
197    pub fn add_strategy<P: Transform<T, Output = T> + Send + Sync + 'static>(
198        mut self,
199        epoch_start: usize,
200        epoch_end: usize,
201        pipeline: P,
202        weight: f32,
203    ) -> Self {
204        self.strategies.push(StrategyConfig {
205            epoch_range: (epoch_start, epoch_end),
206            pipeline: Arc::new(pipeline),
207            weight,
208        });
209        self
210    }
211
212    /// Set current epoch
213    pub fn set_epoch(&mut self, epoch: usize) {
214        self.current_epoch = epoch;
215    }
216
217    /// Get current active strategies for the current epoch
218    fn get_active_strategies(&self) -> Vec<&StrategyConfig<T>> {
219        self.strategies
220            .iter()
221            .filter(|config| {
222                self.current_epoch >= config.epoch_range.0
223                    && self.current_epoch <= config.epoch_range.1
224            })
225            .collect()
226    }
227
228    /// Get training progress as a value between 0.0 and 1.0
229    pub fn get_progress(&self) -> f32 {
230        if self.total_epochs == 0 {
231            0.0
232        } else {
233            (self.current_epoch as f32 / self.total_epochs as f32).min(1.0)
234        }
235    }
236
237    /// Apply augmentation based on current epoch
238    pub fn apply(&self, input: T) -> Result<T> {
239        let active_strategies = self.get_active_strategies();
240
241        if active_strategies.is_empty() {
242            return Ok(input);
243        }
244
245        // Select strategy based on weights
246        let total_weight: f32 = active_strategies.iter().map(|s| s.weight).sum();
247        if total_weight == 0.0 {
248            return Ok(input);
249        }
250
251        let mut rng = thread_rng();
252        let random_value = rng.random::<f32>() * total_weight;
253        let mut cumulative_weight = 0.0;
254
255        for strategy in &active_strategies {
256            cumulative_weight += strategy.weight;
257            if random_value <= cumulative_weight {
258                return strategy.pipeline.transform(input);
259            }
260        }
261
262        // Fallback to last strategy
263        if let Some(last_strategy) = active_strategies.last() {
264            last_strategy.pipeline.transform(input)
265        } else {
266            Ok(input)
267        }
268    }
269}
270
271/// Progressive augmentation that gradually increases intensity
272#[derive(Clone)]
273pub struct ProgressiveAugmentation<T> {
274    light_pipeline: Arc<dyn Transform<T, Output = T> + Send + Sync>,
275    medium_pipeline: Arc<dyn Transform<T, Output = T> + Send + Sync>,
276    heavy_pipeline: Arc<dyn Transform<T, Output = T> + Send + Sync>,
277    current_epoch: usize,
278    total_epochs: usize,
279    progression_mode: ProgressionMode,
280}
281
282#[derive(Clone, Copy)]
283pub enum ProgressionMode {
284    Linear,
285    Exponential,
286    StepWise,
287}
288
289impl<T: Clone + Send + Sync + 'static> ProgressiveAugmentation<T> {
290    /// Create progressive augmentation with different intensity levels
291    pub fn new<L, M, H>(light: L, medium: M, heavy: H, total_epochs: usize) -> Self
292    where
293        L: Transform<T, Output = T> + Send + Sync + 'static,
294        M: Transform<T, Output = T> + Send + Sync + 'static,
295        H: Transform<T, Output = T> + Send + Sync + 'static,
296    {
297        Self {
298            light_pipeline: Arc::new(light),
299            medium_pipeline: Arc::new(medium),
300            heavy_pipeline: Arc::new(heavy),
301            current_epoch: 0,
302            total_epochs,
303            progression_mode: ProgressionMode::Linear,
304        }
305    }
306
307    /// Set progression mode
308    pub fn with_progression_mode(mut self, mode: ProgressionMode) -> Self {
309        self.progression_mode = mode;
310        self
311    }
312
313    /// Set current epoch
314    pub fn set_epoch(&mut self, epoch: usize) {
315        self.current_epoch = epoch;
316    }
317
318    /// Calculate current intensity based on epoch and progression mode
319    fn calculate_intensity(&self) -> f32 {
320        if self.total_epochs == 0 {
321            return 0.0;
322        }
323
324        let progress = self.current_epoch as f32 / self.total_epochs as f32;
325        let progress = progress.min(1.0);
326
327        match self.progression_mode {
328            ProgressionMode::Linear => progress,
329            ProgressionMode::Exponential => progress * progress,
330            ProgressionMode::StepWise => {
331                if progress < 0.33 {
332                    0.0
333                } else if progress < 0.66 {
334                    0.5
335                } else {
336                    1.0
337                }
338            }
339        }
340    }
341
342    /// Apply progressive augmentation
343    pub fn apply(&self, input: T) -> Result<T> {
344        let intensity = self.calculate_intensity();
345
346        if intensity < 0.33 {
347            self.light_pipeline.transform(input)
348        } else if intensity < 0.66 {
349            self.medium_pipeline.transform(input)
350        } else {
351            self.heavy_pipeline.transform(input)
352        }
353    }
354}
355
356/// Adaptive augmentation that adjusts based on model performance
357pub struct AdaptiveAugmentation<T> {
358    pipelines: Vec<(Arc<dyn Transform<T, Output = T> + Send + Sync>, f32)>, // (pipeline, intensity)
359    performance_history: Vec<f32>,
360    target_performance: f32,
361    adaptation_rate: f32,
362    current_intensity: f32,
363    min_intensity: f32,
364    max_intensity: f32,
365}
366
367impl<T: Clone + Send + Sync + 'static> AdaptiveAugmentation<T> {
368    /// Create adaptive augmentation system
369    pub fn new(target_performance: f32) -> Self {
370        Self {
371            pipelines: Vec::new(),
372            performance_history: Vec::new(),
373            target_performance,
374            adaptation_rate: 0.1,
375            current_intensity: 0.5,
376            min_intensity: 0.0,
377            max_intensity: 1.0,
378        }
379    }
380
381    /// Add augmentation pipeline with intensity level
382    pub fn add_pipeline<P: Transform<T, Output = T> + Send + Sync + 'static>(
383        mut self,
384        pipeline: P,
385        intensity: f32,
386    ) -> Self {
387        self.pipelines.push((Arc::new(pipeline), intensity));
388        self
389    }
390
391    /// Set adaptation parameters
392    pub fn with_adaptation_params(
393        mut self,
394        adaptation_rate: f32,
395        min_intensity: f32,
396        max_intensity: f32,
397    ) -> Self {
398        self.adaptation_rate = adaptation_rate;
399        self.min_intensity = min_intensity;
400        self.max_intensity = max_intensity;
401        self
402    }
403
404    /// Update with current model performance (e.g., validation accuracy)
405    pub fn update_performance(&mut self, performance: f32) {
406        self.performance_history.push(performance);
407
408        // Keep only recent history
409        if self.performance_history.len() > 10 {
410            self.performance_history.remove(0);
411        }
412
413        // Adapt intensity based on performance
414        if performance < self.target_performance {
415            // Performance is low, reduce augmentation intensity
416            self.current_intensity -= self.adaptation_rate;
417        } else {
418            // Performance is good, can increase augmentation intensity
419            self.current_intensity += self.adaptation_rate;
420        }
421
422        // Clamp intensity
423        self.current_intensity = self
424            .current_intensity
425            .max(self.min_intensity)
426            .min(self.max_intensity);
427    }
428
429    /// Apply adaptive augmentation
430    pub fn apply(&self, input: T) -> Result<T> {
431        if self.pipelines.is_empty() {
432            return Ok(input);
433        }
434
435        // Find pipeline with intensity closest to current intensity
436        let mut best_pipeline = &self.pipelines[0].0;
437        let mut best_distance = (self.pipelines[0].1 - self.current_intensity).abs();
438
439        for (pipeline, intensity) in &self.pipelines {
440            let distance = (intensity - self.current_intensity).abs();
441            if distance < best_distance {
442                best_distance = distance;
443                best_pipeline = pipeline;
444            }
445        }
446
447        best_pipeline.transform(input)
448    }
449
450    /// Get current intensity level
451    pub fn current_intensity(&self) -> f32 {
452        self.current_intensity
453    }
454
455    /// Get recent performance average
456    pub fn recent_performance(&self) -> Option<f32> {
457        if self.performance_history.is_empty() {
458            None
459        } else {
460            let sum: f32 = self.performance_history.iter().sum();
461            Some(sum / self.performance_history.len() as f32)
462        }
463    }
464}
465
466/// Simple message-passing based augmentation queue for async processing
467/// (Simplified version without external dependencies)
468pub struct AugmentationQueue<T> {
469    tasks: Arc<RwLock<Vec<AugmentationTask<T>>>>,
470    engine: Arc<OnlineAugmentationEngine<T>>,
471    max_queue_size: usize,
472}
473
474// Placeholder for future async task processing
475#[allow(dead_code)]
476struct AugmentationTask<T> {
477    input: T,
478    cache_key: Option<String>,
479    task_id: usize,
480}
481
482impl<T: Clone + Send + Sync + 'static> AugmentationQueue<T> {
483    /// Create a new augmentation queue
484    pub fn new(engine: OnlineAugmentationEngine<T>, max_queue_size: usize) -> Self {
485        Self {
486            tasks: Arc::new(RwLock::new(Vec::new())),
487            engine: Arc::new(engine),
488            max_queue_size,
489        }
490    }
491
492    /// Submit an augmentation task (simplified version)
493    pub fn submit(&self, input: T, cache_key: Option<String>) -> Result<T> {
494        let tasks = self.tasks.read().expect("lock should not be poisoned");
495        if tasks.len() >= self.max_queue_size {
496            return Err(TorshError::InvalidArgument(
497                "Augmentation queue is full".to_string(),
498            ));
499        }
500        drop(tasks);
501
502        // For this simplified version, process immediately
503        self.engine.apply(input, cache_key.as_deref())
504    }
505
506    /// Process pending tasks (placeholder for worker thread processing)
507    pub fn process_tasks(&self) -> usize {
508        let mut tasks = self.tasks.write().expect("lock should not be poisoned");
509        let processed_count = tasks.len();
510
511        // In a real implementation, this would process tasks asynchronously
512        // For now, we just clear the task list
513        tasks.clear();
514
515        processed_count
516    }
517
518    /// Get queue length
519    pub fn queue_length(&self) -> usize {
520        self.tasks
521            .read()
522            .expect("lock should not be poisoned")
523            .len()
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::transforms::lambda;
531
532    #[test]
533    fn test_augmentation_stats_default() {
534        let stats = AugmentationStats::default();
535        assert_eq!(stats.total_transforms, 0);
536        assert_eq!(stats.cache_hits, 0);
537        assert_eq!(stats.cache_misses, 0);
538        assert_eq!(stats.total_time_ms, 0.0);
539        assert_eq!(stats.average_time_ms, 0.0);
540    }
541
542    #[test]
543    fn test_online_augmentation_engine_creation() {
544        let transform = lambda(|x: i32| Ok(x * 2));
545        let engine = OnlineAugmentationEngine::new(transform);
546
547        assert!(!engine.cache_enabled);
548        assert_eq!(engine.max_cache_size, 1000);
549        assert_eq!(engine.cache_size(), 0);
550    }
551
552    #[test]
553    fn test_online_augmentation_engine_with_cache() {
554        let transform = lambda(|x: i32| Ok(x * 2));
555        let engine = OnlineAugmentationEngine::new(transform).with_cache(500);
556
557        assert!(engine.cache_enabled);
558        assert_eq!(engine.max_cache_size, 500);
559    }
560
561    #[test]
562    fn test_dynamic_augmentation_strategy() {
563        let strategy = DynamicAugmentationStrategy::<i32>::new(100);
564        assert_eq!(strategy.current_epoch, 0);
565        assert_eq!(strategy.total_epochs, 100);
566        assert_eq!(strategy.get_progress(), 0.0);
567    }
568
569    #[test]
570    fn test_dynamic_strategy_progress() {
571        let mut strategy = DynamicAugmentationStrategy::<i32>::new(100);
572        strategy.set_epoch(25);
573        assert_eq!(strategy.get_progress(), 0.25);
574
575        strategy.set_epoch(50);
576        assert_eq!(strategy.get_progress(), 0.5);
577
578        strategy.set_epoch(100);
579        assert_eq!(strategy.get_progress(), 1.0);
580    }
581
582    #[test]
583    fn test_progressive_augmentation_intensity() {
584        let light = lambda(|x: i32| Ok(x + 1));
585        let medium = lambda(|x: i32| Ok(x + 2));
586        let heavy = lambda(|x: i32| Ok(x + 3));
587
588        let mut progressive = ProgressiveAugmentation::new(light, medium, heavy, 100);
589
590        // Test different epochs
591        progressive.set_epoch(0);
592        assert_eq!(progressive.calculate_intensity(), 0.0);
593
594        progressive.set_epoch(25);
595        assert_eq!(progressive.calculate_intensity(), 0.25);
596
597        progressive.set_epoch(50);
598        assert_eq!(progressive.calculate_intensity(), 0.5);
599
600        progressive.set_epoch(100);
601        assert_eq!(progressive.calculate_intensity(), 1.0);
602    }
603
604    #[test]
605    fn test_progressive_augmentation_step_wise() {
606        let light = lambda(|x: i32| Ok(x + 1));
607        let medium = lambda(|x: i32| Ok(x + 2));
608        let heavy = lambda(|x: i32| Ok(x + 3));
609
610        let mut progressive = ProgressiveAugmentation::new(light, medium, heavy, 100)
611            .with_progression_mode(ProgressionMode::StepWise);
612
613        progressive.set_epoch(10);
614        assert_eq!(progressive.calculate_intensity(), 0.0);
615
616        progressive.set_epoch(40);
617        assert_eq!(progressive.calculate_intensity(), 0.5);
618
619        progressive.set_epoch(80);
620        assert_eq!(progressive.calculate_intensity(), 1.0);
621    }
622
623    #[test]
624    fn test_adaptive_augmentation() {
625        let mut adaptive = AdaptiveAugmentation::<i32>::new(0.85);
626        assert_eq!(adaptive.current_intensity(), 0.5);
627        assert_eq!(adaptive.recent_performance(), None);
628
629        // Update with good performance
630        adaptive.update_performance(0.9);
631        assert!(adaptive.current_intensity() > 0.5);
632        assert_eq!(adaptive.recent_performance(), Some(0.9));
633
634        // Update with poor performance
635        adaptive.update_performance(0.7);
636        assert!(adaptive.current_intensity() < 0.6);
637    }
638
639    #[test]
640    fn test_augmentation_queue() {
641        let transform = lambda(|x: i32| Ok(x * 2));
642        let engine = OnlineAugmentationEngine::new(transform);
643        let queue = AugmentationQueue::new(engine, 10);
644
645        assert_eq!(queue.queue_length(), 0);
646
647        let result = queue.submit(5, None).unwrap();
648        assert_eq!(result, 10);
649    }
650}