Skip to main content

scirs2_transform/
optimization_config.rs

1//! Optimization configuration and auto-tuning system
2//!
3//! This module provides intelligent configuration systems that automatically
4//! choose optimal settings for transformations based on data characteristics
5//! and system resources.
6
7use scirs2_core::Rng;
8#[cfg(feature = "distributed")]
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::error::{Result, TransformError};
13use crate::utils::ProcessingStrategy;
14use scirs2_core::random::RngExt;
15
16/// System resource information
17#[derive(Debug, Clone)]
18#[cfg_attr(feature = "distributed", derive(Serialize, Deserialize))]
19pub struct SystemResources {
20    /// Available memory in MB
21    pub memory_mb: usize,
22    /// Number of CPU cores
23    pub cpu_cores: usize,
24    /// Whether GPU is available
25    pub has_gpu: bool,
26    /// Whether SIMD instructions are available
27    pub has_simd: bool,
28    /// L3 cache size in KB (affects chunk sizes)
29    pub l3_cache_kb: usize,
30}
31
32impl SystemResources {
33    /// Detect system resources automatically
34    pub fn detect() -> Self {
35        SystemResources {
36            memory_mb: Self::detect_memory_mb(),
37            cpu_cores: num_cpus::get(),
38            has_gpu: Self::detect_gpu(),
39            has_simd: Self::detect_simd(),
40            l3_cache_kb: Self::detect_l3_cache_kb(),
41        }
42    }
43
44    /// Detect available memory
45    fn detect_memory_mb() -> usize {
46        // Simplified detection - in practice, use system APIs
47        #[cfg(target_os = "linux")]
48        {
49            if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo") {
50                for line in meminfo.lines() {
51                    if line.starts_with("MemAvailable:") {
52                        if let Some(kb_str) = line.split_whitespace().nth(1) {
53                            if let Ok(kb) = kb_str.parse::<usize>() {
54                                return kb / 1024; // Convert to MB
55                            }
56                        }
57                    }
58                }
59            }
60        }
61
62        // Fallback: assume 8GB
63        8 * 1024
64    }
65
66    /// Detect GPU availability
67    fn detect_gpu() -> bool {
68        // Simplified detection
69        #[cfg(feature = "gpu")]
70        {
71            // In practice, check for CUDA or OpenCL
72            true
73        }
74        #[cfg(not(feature = "gpu"))]
75        {
76            false
77        }
78    }
79
80    /// Detect SIMD support
81    fn detect_simd() -> bool {
82        #[cfg(feature = "simd")]
83        {
84            true
85        }
86        #[cfg(not(feature = "simd"))]
87        {
88            false
89        }
90    }
91
92    /// Detect L3 cache size
93    fn detect_l3_cache_kb() -> usize {
94        // Simplified - in practice, use CPUID or /sys/devices/system/cpu
95        8 * 1024 // Assume 8MB L3 cache
96    }
97
98    /// Get conservative memory limit for transformations (80% of available)
99    pub fn safe_memory_mb(&self) -> usize {
100        (self.memory_mb as f64 * 0.8) as usize
101    }
102
103    /// Get optimal chunk size based on cache size
104    pub fn optimal_chunk_size(&self, elementsize: usize) -> usize {
105        // Target 50% of L3 cache
106        let target_bytes = (self.l3_cache_kb * 1024) / 2;
107        (target_bytes / elementsize).max(1000) // At least 1000 elements
108    }
109}
110
111/// Data characteristics for optimization decisions
112#[derive(Debug, Clone)]
113#[cfg_attr(feature = "distributed", derive(Serialize, Deserialize))]
114pub struct DataCharacteristics {
115    /// Number of samples
116    pub n_samples: usize,
117    /// Number of features
118    pub nfeatures: usize,
119    /// Data sparsity (0.0 = dense, 1.0 = all zeros)
120    pub sparsity: f64,
121    /// Data range (max - min)
122    pub data_range: f64,
123    /// Outlier ratio
124    pub outlier_ratio: f64,
125    /// Whether data has missing values
126    pub has_missing: bool,
127    /// Estimated memory footprint in MB
128    pub memory_footprint_mb: f64,
129    /// Data type size (e.g., 8 for f64)
130    pub elementsize: usize,
131}
132
133impl DataCharacteristics {
134    /// Analyze data characteristics from array view
135    pub fn analyze(data: &scirs2_core::ndarray::ArrayView2<f64>) -> Result<Self> {
136        let (n_samples, nfeatures) = data.dim();
137
138        if n_samples == 0 || nfeatures == 0 {
139            return Err(TransformError::InvalidInput("Empty _data".to_string()));
140        }
141
142        // Calculate sparsity
143        let zeros = data.iter().filter(|&&x| x == 0.0).count();
144        let sparsity = zeros as f64 / data.len() as f64;
145
146        // Calculate _data range
147        let mut min_val = f64::INFINITY;
148        let mut max_val = f64::NEG_INFINITY;
149        let mut finite_count = 0;
150        let mut missing_count = 0;
151
152        for &val in data.iter() {
153            if val.is_finite() {
154                min_val = min_val.min(val);
155                max_val = max_val.max(val);
156                finite_count += 1;
157            } else {
158                missing_count += 1;
159            }
160        }
161
162        let data_range = if finite_count > 0 {
163            max_val - min_val
164        } else {
165            0.0
166        };
167        let has_missing = missing_count > 0;
168
169        // Estimate outlier ratio using IQR method (simplified)
170        let outlier_ratio = if n_samples > 10 {
171            let mut sample_values: Vec<f64> = data.iter()
172                .filter(|&&x| x.is_finite())
173                .take(1000) // Sample for efficiency
174                .copied()
175                .collect();
176
177            if sample_values.len() >= 4 {
178                sample_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
179                let n = sample_values.len();
180                let q1 = sample_values[n / 4];
181                let q3 = sample_values[3 * n / 4];
182                let iqr = q3 - q1;
183
184                if iqr > 0.0 {
185                    let lower_bound = q1 - 1.5 * iqr;
186                    let upper_bound = q3 + 1.5 * iqr;
187                    let outliers = sample_values
188                        .iter()
189                        .filter(|&&x| x < lower_bound || x > upper_bound)
190                        .count();
191                    outliers as f64 / sample_values.len() as f64
192                } else {
193                    0.0
194                }
195            } else {
196                0.0
197            }
198        } else {
199            0.0
200        };
201
202        let memory_footprint_mb =
203            (n_samples * nfeatures * std::mem::size_of::<f64>()) as f64 / (1024.0 * 1024.0);
204
205        Ok(DataCharacteristics {
206            n_samples,
207            nfeatures,
208            sparsity,
209            data_range,
210            outlier_ratio,
211            has_missing,
212            memory_footprint_mb,
213            elementsize: std::mem::size_of::<f64>(),
214        })
215    }
216
217    /// Check if data is considered "large"
218    pub fn is_large_dataset(&self) -> bool {
219        self.n_samples > 100_000 || self.nfeatures > 10_000 || self.memory_footprint_mb > 1000.0
220    }
221
222    /// Check if data is considered "wide" (more features than samples)
223    pub fn is_wide_dataset(&self) -> bool {
224        self.nfeatures > self.n_samples
225    }
226
227    /// Check if data is sparse
228    pub fn is_sparse(&self) -> bool {
229        self.sparsity > 0.5
230    }
231
232    /// Check if data has significant outliers
233    pub fn has_outliers(&self) -> bool {
234        self.outlier_ratio > 0.05 // More than 5% outliers
235    }
236}
237
238/// Optimization configuration for a specific transformation
239#[derive(Debug, Clone)]
240#[cfg_attr(feature = "distributed", derive(Serialize, Deserialize))]
241pub struct OptimizationConfig {
242    /// Processing strategy to use
243    pub processing_strategy: ProcessingStrategy,
244    /// Memory limit in MB
245    pub memory_limit_mb: usize,
246    /// Whether to use robust statistics
247    pub use_robust: bool,
248    /// Whether to use parallel processing
249    pub use_parallel: bool,
250    /// Whether to use SIMD acceleration
251    pub use_simd: bool,
252    /// Whether to use GPU acceleration
253    pub use_gpu: bool,
254    /// Chunk size for batch processing
255    pub chunk_size: usize,
256    /// Number of threads to use
257    pub num_threads: usize,
258    /// Additional algorithm-specific parameters
259    pub algorithm_params: HashMap<String, f64>,
260}
261
262impl OptimizationConfig {
263    /// Create optimization config for standardization
264    pub fn for_standardization(datachars: &DataCharacteristics, system: &SystemResources) -> Self {
265        let use_robust = datachars.has_outliers();
266        let use_parallel = datachars.n_samples > 10_000 && system.cpu_cores > 1;
267        let use_simd = system.has_simd && datachars.nfeatures > 100;
268        let use_gpu = system.has_gpu && datachars.memory_footprint_mb > 100.0;
269
270        let processing_strategy = if datachars.memory_footprint_mb > system.safe_memory_mb() as f64
271        {
272            ProcessingStrategy::OutOfCore {
273                chunk_size: system.optimal_chunk_size(datachars.elementsize),
274            }
275        } else if use_parallel {
276            ProcessingStrategy::Parallel
277        } else if use_simd {
278            ProcessingStrategy::Simd
279        } else {
280            ProcessingStrategy::Standard
281        };
282
283        OptimizationConfig {
284            processing_strategy,
285            memory_limit_mb: system.safe_memory_mb(),
286            use_robust,
287            use_parallel,
288            use_simd,
289            use_gpu,
290            chunk_size: system.optimal_chunk_size(datachars.elementsize),
291            num_threads: if use_parallel { system.cpu_cores } else { 1 },
292            algorithm_params: HashMap::new(),
293        }
294    }
295
296    /// Create optimization config for PCA
297    pub fn for_pca(
298        datachars: &DataCharacteristics,
299        system: &SystemResources,
300        n_components: usize,
301    ) -> Self {
302        let use_randomized = datachars.is_large_dataset();
303        let use_parallel = datachars.n_samples > 1_000 && system.cpu_cores > 1;
304        let use_gpu = system.has_gpu && datachars.memory_footprint_mb > 500.0;
305
306        // PCA memory requirements are higher due to covariance matrix
307        let memory_multiplier = if datachars.nfeatures > datachars.n_samples {
308            3.0
309        } else {
310            2.0
311        };
312        let estimated_memory = datachars.memory_footprint_mb * memory_multiplier;
313
314        let processing_strategy = if estimated_memory > system.safe_memory_mb() as f64 {
315            ProcessingStrategy::OutOfCore {
316                chunk_size: (system.safe_memory_mb() * 1024 * 1024)
317                    / (datachars.nfeatures * datachars.elementsize),
318            }
319        } else if use_parallel {
320            ProcessingStrategy::Parallel
321        } else {
322            ProcessingStrategy::Standard
323        };
324
325        let mut algorithm_params = HashMap::new();
326        algorithm_params.insert(
327            "use_randomized".to_string(),
328            if use_randomized { 1.0 } else { 0.0 },
329        );
330        algorithm_params.insert("n_components".to_string(), n_components as f64);
331
332        OptimizationConfig {
333            processing_strategy,
334            memory_limit_mb: system.safe_memory_mb(),
335            use_robust: false, // PCA doesn't typically use robust statistics
336            use_parallel,
337            use_simd: system.has_simd,
338            use_gpu,
339            chunk_size: system.optimal_chunk_size(datachars.elementsize),
340            num_threads: if use_parallel { system.cpu_cores } else { 1 },
341            algorithm_params,
342        }
343    }
344
345    /// Create optimization config for polynomial features
346    pub fn for_polynomial_features(
347        datachars: &DataCharacteristics,
348        system: &SystemResources,
349        degree: usize,
350    ) -> Result<Self> {
351        // Polynomial features can explode in size
352        let estimated_output_features =
353            Self::estimate_polynomial_features(datachars.nfeatures, degree)?;
354        let estimated_memory = datachars.n_samples as f64
355            * estimated_output_features as f64
356            * datachars.elementsize as f64
357            / (1024.0 * 1024.0);
358
359        if estimated_memory > system.memory_mb as f64 * 0.9 {
360            return Err(TransformError::MemoryError(format!(
361                "Polynomial features would require {estimated_memory:.1} MB, but only {} MB available",
362                system.memory_mb
363            )));
364        }
365
366        let use_parallel = datachars.n_samples > 1_000 && system.cpu_cores > 1;
367        let use_simd = system.has_simd && estimated_output_features > 100;
368
369        let processing_strategy = if estimated_memory > system.safe_memory_mb() as f64 {
370            ProcessingStrategy::OutOfCore {
371                chunk_size: (system.safe_memory_mb() * 1024 * 1024)
372                    / (estimated_output_features * datachars.elementsize),
373            }
374        } else if use_parallel {
375            ProcessingStrategy::Parallel
376        } else if use_simd {
377            ProcessingStrategy::Simd
378        } else {
379            ProcessingStrategy::Standard
380        };
381
382        let mut algorithm_params = HashMap::new();
383        algorithm_params.insert("degree".to_string(), degree as f64);
384        algorithm_params.insert(
385            "estimated_output_features".to_string(),
386            estimated_output_features as f64,
387        );
388
389        Ok(OptimizationConfig {
390            processing_strategy,
391            memory_limit_mb: system.safe_memory_mb(),
392            use_robust: false,
393            use_parallel,
394            use_simd,
395            use_gpu: false, // Polynomial features typically don't benefit from GPU
396            chunk_size: system.optimal_chunk_size(datachars.elementsize),
397            num_threads: if use_parallel { system.cpu_cores } else { 1 },
398            algorithm_params,
399        })
400    }
401
402    /// Estimate number of polynomial features
403    fn estimate_polynomial_features(nfeatures: usize, degree: usize) -> Result<usize> {
404        if degree == 0 {
405            return Err(TransformError::InvalidInput(
406                "Degree must be at least 1".to_string(),
407            ));
408        }
409
410        let mut total_features = 1; // bias term
411
412        for d in 1..=degree {
413            // Multinomial coefficient: (nfeatures + d - 1)! / (d! * (nfeatures - 1)!)
414            let mut coeff = 1;
415            for i in 0..d {
416                coeff = coeff * (nfeatures + d - 1 - i) / (i + 1);
417
418                // Check for overflow
419                if coeff > 1_000_000 {
420                    return Err(TransformError::ComputationError(
421                        "Too many polynomial _features would be generated".to_string(),
422                    ));
423                }
424            }
425            total_features += coeff;
426        }
427
428        Ok(total_features)
429    }
430
431    /// Get estimated execution time for this configuration
432    pub fn estimated_execution_time(&self, datachars: &DataCharacteristics) -> std::time::Duration {
433        use std::time::Duration;
434
435        let base_ops = datachars.n_samples as u64 * datachars.nfeatures as u64;
436
437        let ops_per_second = match self.processing_strategy {
438            ProcessingStrategy::Parallel => {
439                1_000_000_000 * self.num_threads as u64 // 1 billion ops/second per thread
440            }
441            ProcessingStrategy::Simd => {
442                2_000_000_000 // 2 billion ops/second with SIMD
443            }
444            ProcessingStrategy::OutOfCore { .. } => {
445                100_000_000 // 100 million ops/second (I/O bound)
446            }
447            ProcessingStrategy::Standard => {
448                500_000_000 // 500 million ops/second
449            }
450        };
451
452        let time_ns = (base_ops * 1_000_000_000) / ops_per_second;
453        Duration::from_nanos(time_ns.max(1000)) // At least 1 microsecond
454    }
455}
456
457/// Auto-tuning system for optimization configurations
458pub struct AutoTuner {
459    /// System resources
460    system: SystemResources,
461    /// Performance history for different configurations
462    performance_history: HashMap<String, Vec<PerformanceRecord>>,
463}
464
465/// Performance record for auto-tuning
466#[derive(Debug, Clone)]
467struct PerformanceRecord {
468    #[allow(dead_code)]
469    config_hash: String,
470    #[allow(dead_code)]
471    execution_time: std::time::Duration,
472    #[allow(dead_code)]
473    memory_used_mb: f64,
474    #[allow(dead_code)]
475    success: bool,
476    #[allow(dead_code)]
477    data_characteristics: DataCharacteristics,
478}
479
480impl Default for AutoTuner {
481    fn default() -> Self {
482        Self::new()
483    }
484}
485
486impl AutoTuner {
487    /// Create a new auto-tuner
488    pub fn new() -> Self {
489        AutoTuner {
490            system: SystemResources::detect(),
491            performance_history: HashMap::new(),
492        }
493    }
494
495    /// Get optimal configuration for a specific transformation
496    pub fn optimize_for_transformation(
497        &self,
498        transformation: &str,
499        datachars: &DataCharacteristics,
500        params: &HashMap<String, f64>,
501    ) -> Result<OptimizationConfig> {
502        match transformation {
503            "standardization" => Ok(OptimizationConfig::for_standardization(
504                datachars,
505                &self.system,
506            )),
507            "pca" => {
508                let n_components = params.get("n_components").unwrap_or(&5.0) as &f64;
509                Ok(OptimizationConfig::for_pca(
510                    datachars,
511                    &self.system,
512                    *n_components as usize,
513                ))
514            }
515            "polynomial" => {
516                let degree = params.get("degree").unwrap_or(&2.0) as &f64;
517                OptimizationConfig::for_polynomial_features(
518                    datachars,
519                    &self.system,
520                    *degree as usize,
521                )
522            }
523            _ => {
524                // Default configuration
525                Ok(OptimizationConfig {
526                    processing_strategy: if datachars.is_large_dataset() {
527                        ProcessingStrategy::Parallel
528                    } else {
529                        ProcessingStrategy::Standard
530                    },
531                    memory_limit_mb: self.system.safe_memory_mb(),
532                    use_robust: datachars.has_outliers(),
533                    use_parallel: datachars.n_samples > 10_000,
534                    use_simd: self.system.has_simd,
535                    use_gpu: self.system.has_gpu && datachars.memory_footprint_mb > 100.0,
536                    chunk_size: self.system.optimal_chunk_size(datachars.elementsize),
537                    num_threads: self.system.cpu_cores,
538                    algorithm_params: HashMap::new(),
539                })
540            }
541        }
542    }
543
544    /// Record performance for learning
545    pub fn record_performance(
546        &mut self,
547        transformation: &str,
548        config: &OptimizationConfig,
549        execution_time: std::time::Duration,
550        memory_used_mb: f64,
551        success: bool,
552        datachars: DataCharacteristics,
553    ) {
554        let config_hash = format!("{config:?}"); // Simplified hash
555
556        let record = PerformanceRecord {
557            config_hash: config_hash.clone(),
558            execution_time,
559            memory_used_mb,
560            success,
561            data_characteristics: datachars,
562        };
563
564        self.performance_history
565            .entry(transformation.to_string())
566            .or_default()
567            .push(record);
568
569        // Keep only recent records (last 100)
570        let records = self
571            .performance_history
572            .get_mut(transformation)
573            .expect("Operation failed");
574        if records.len() > 100 {
575            records.remove(0);
576        }
577    }
578
579    /// Get system resources
580    pub fn system_resources(&self) -> &SystemResources {
581        &self.system
582    }
583
584    /// Generate optimization report
585    pub fn generate_report(&self, datachars: &DataCharacteristics) -> OptimizationReport {
586        let recommendations = vec![
587            self.get_recommendation_for_transformation("standardization", datachars),
588            self.get_recommendation_for_transformation("pca", datachars),
589            self.get_recommendation_for_transformation("polynomial", datachars),
590        ];
591
592        OptimizationReport {
593            system_info: self.system.clone(),
594            data_info: datachars.clone(),
595            recommendations,
596            estimated_total_memory_mb: datachars.memory_footprint_mb * 2.0, // Conservative estimate
597        }
598    }
599
600    fn get_recommendation_for_transformation(
601        &self,
602        transformation: &str,
603        datachars: &DataCharacteristics,
604    ) -> TransformationRecommendation {
605        let config = self
606            .optimize_for_transformation(transformation, datachars, &HashMap::new())
607            .unwrap_or_else(|_| OptimizationConfig {
608                processing_strategy: ProcessingStrategy::Standard,
609                memory_limit_mb: self.system.safe_memory_mb(),
610                use_robust: false,
611                use_parallel: false,
612                use_simd: false,
613                use_gpu: false,
614                chunk_size: 1000,
615                num_threads: 1,
616                algorithm_params: HashMap::new(),
617            });
618
619        let estimated_time = config.estimated_execution_time(datachars);
620
621        TransformationRecommendation {
622            transformation: transformation.to_string(),
623            config,
624            estimated_time,
625            confidence: 0.8, // Placeholder
626            reason: format!(
627                "Optimized for {} samples, {} features",
628                datachars.n_samples, datachars.nfeatures
629            ),
630        }
631    }
632}
633
634/// Optimization report
635#[derive(Debug, Clone)]
636pub struct OptimizationReport {
637    /// System information
638    pub system_info: SystemResources,
639    /// Data characteristics
640    pub data_info: DataCharacteristics,
641    /// Recommendations for different transformations
642    pub recommendations: Vec<TransformationRecommendation>,
643    /// Estimated total memory usage
644    pub estimated_total_memory_mb: f64,
645}
646
647/// Recommendation for a specific transformation
648#[derive(Debug, Clone)]
649pub struct TransformationRecommendation {
650    /// Transformation name
651    pub transformation: String,
652    /// Recommended configuration
653    pub config: OptimizationConfig,
654    /// Estimated execution time
655    pub estimated_time: std::time::Duration,
656    /// Confidence in recommendation (0.0 to 1.0)
657    pub confidence: f64,
658    /// Human-readable reason
659    pub reason: String,
660}
661
662impl OptimizationReport {
663    /// Print a human-readable report
664    pub fn print_report(&self) {
665        println!("=== Optimization Report ===");
666        println!("System Resources:");
667        println!("  Memory: {} MB", self.system_info.memory_mb);
668        println!("  CPU Cores: {}", self.system_info.cpu_cores);
669        println!("  GPU Available: {}", self.system_info.has_gpu);
670        println!("  SIMD Available: {}", self.system_info.has_simd);
671        println!();
672
673        println!("Data Characteristics:");
674        println!("  Samples: {}", self.data_info.n_samples);
675        println!("  Features: {}", self.data_info.nfeatures);
676        println!(
677            "  Memory Footprint: {:.1} MB",
678            self.data_info.memory_footprint_mb
679        );
680        println!("  Sparsity: {:.1}%", self.data_info.sparsity * 100.0);
681        println!("  Has Outliers: {}", self.data_info.has_outliers());
682        println!();
683
684        println!("Recommendations:");
685        for rec in &self.recommendations {
686            println!("  {}:", rec.transformation);
687            println!("    Strategy: {:?}", rec.config.processing_strategy);
688            println!(
689                "    Estimated Time: {:.2}s",
690                rec.estimated_time.as_secs_f64()
691            );
692            println!("    Use Parallel: {}", rec.config.use_parallel);
693            println!("    Use SIMD: {}", rec.config.use_simd);
694            println!("    Use GPU: {}", rec.config.use_gpu);
695            println!("    Reason: {}", rec.reason);
696            println!();
697        }
698    }
699}
700
701/// ✅ Advanced MODE: Intelligent Dynamic Configuration Optimizer
702/// Provides real-time optimization of transformation parameters based on
703/// live performance metrics and adaptive learning from historical patterns.
704pub struct AdvancedConfigOptimizer {
705    /// Historical performance data for different configurations
706    performance_history: HashMap<String, Vec<PerformanceMetric>>,
707    /// Real-time system monitoring
708    system_monitor: SystemMonitor,
709    /// Machine learning model for configuration prediction
710    config_predictor: ConfigurationPredictor,
711    /// Adaptive parameter tuning engine
712    adaptive_tuner: AdaptiveParameterTuner,
713}
714
715/// ✅ Advanced MODE: Performance metrics for configuration optimization
716#[derive(Debug, Clone)]
717pub struct PerformanceMetric {
718    /// Configuration hash for identification
719    #[allow(dead_code)]
720    config_hash: u64,
721    /// Execution time in microseconds
722    execution_time_us: u64,
723    /// Memory usage in bytes
724    memory_usage_bytes: usize,
725    /// Cache hit rate
726    cache_hit_rate: f64,
727    /// CPU utilization percentage
728    cpu_utilization: f64,
729    /// Accuracy/quality score of the transformation
730    quality_score: f64,
731    /// Timestamp of measurement
732    #[allow(dead_code)]
733    timestamp: std::time::Instant,
734}
735
736/// ✅ Advanced MODE: Real-time system performance monitoring
737pub struct SystemMonitor {
738    /// Current CPU load average
739    cpu_load: f64,
740    /// Available memory in bytes
741    available_memory_bytes: usize,
742    /// Cache miss rate
743    cache_miss_rate: f64,
744    /// I/O wait percentage
745    io_wait_percent: f64,
746    /// Temperature information (for thermal throttling)
747    cpu_temperature_celsius: f64,
748    /// Previous `/proc/stat` aggregate CPU jiffies `(idle+iowait, total)`,
749    /// used to compute a real I/O-wait percentage as a delta between
750    /// successive [`Self::update_metrics`] calls (Linux only).
751    prev_cpu_jiffies: Option<(u64, u64, u64)>,
752}
753
754/// ✅ Advanced MODE: ML-based configuration prediction
755pub struct ConfigurationPredictor {
756    /// Learned relative-importance weight per data-characteristic feature,
757    /// genuinely read by [`Self::predict_memory_limit`]/
758    /// [`Self::predict_parallelism`]/[`Self::predict_simd_usage`] and
759    /// genuinely updated (Widrow-Hoff / LMS online rule) by
760    /// [`Self::update_from_feedback`] from real observed
761    /// [`PerformanceMetric::quality_score`] feedback.
762    feature_weights: HashMap<String, f64>,
763    /// Learning rate for online updates
764    learning_rate: f64,
765    /// Prediction confidence threshold
766    confidence_threshold: f64,
767    /// Training sample count
768    sample_count: usize,
769    /// The (normalized) feature vector used by the most recent
770    /// [`Self::predict_optimal_config`] call, so a later
771    /// [`Self::update_from_feedback`] call for that same prediction can
772    /// attribute credit/blame to the features that were actually active
773    /// (rather than fabricating an update with no real basis).
774    last_features: HashMap<String, f64>,
775}
776
777/// ✅ Advanced MODE: Adaptive parameter tuning with reinforcement learning
778pub struct AdaptiveParameterTuner {
779    /// Q-learning table for parameter optimization
780    q_table: HashMap<(String, String), f64>, // (state, action) -> reward
781    /// Exploration rate (epsilon)
782    exploration_rate: f64,
783    /// Learning rate for Q-learning
784    learning_rate: f64,
785    /// Discount factor for future rewards
786    #[allow(dead_code)]
787    discount_factor: f64,
788    /// Current state representation
789    current_state: String,
790    /// The action actually taken (explored or exploited) by the most
791    /// recent [`Self::tune_parameters`] call, so [`Self::update_q_values`]
792    /// can key the Q-table update on the real action taken instead of a
793    /// hardcoded placeholder string (which made every entry collide on the
794    /// same key, so the table could never distinguish between actions).
795    last_action: String,
796}
797
798/// The finite, real action space [`AdaptiveParameterTuner`] chooses between.
799/// A named, distinguishable set of parameter adjustments -- replacing the
800/// previous design where every Q-table update was hardcoded to the literal
801/// action name `"current_action"`, making the table structurally unable to
802/// ever tell actions apart.
803const TUNER_ACTIONS: &[&str] = &[
804    "increase_memory",
805    "decrease_memory",
806    "toggle_parallel",
807    "increase_chunk",
808    "decrease_chunk",
809    "no_change",
810];
811
812/// Apply the named `action` to `config`, returning the adjusted
813/// configuration. Used identically by both the exploration path
814/// (a randomly chosen action) and the exploitation path (the
815/// Q-table's current best action for this state), so a learned
816/// "best action" and a randomly explored one have exactly the same,
817/// real effect on the returned configuration.
818fn apply_tuner_action(action: &str, mut config: OptimizationConfig) -> OptimizationConfig {
819    match action {
820        "increase_memory" => {
821            config.memory_limit_mb = ((config.memory_limit_mb as f64 * 1.2) as usize).max(1);
822        }
823        "decrease_memory" => {
824            config.memory_limit_mb = ((config.memory_limit_mb as f64 * 0.8) as usize).max(1);
825        }
826        "toggle_parallel" => {
827            config.use_parallel = !config.use_parallel;
828        }
829        "increase_chunk" => {
830            config.chunk_size = ((config.chunk_size as f64 * 1.5) as usize).max(1);
831        }
832        "decrease_chunk" => {
833            config.chunk_size = ((config.chunk_size as f64 * 0.5) as usize).max(1);
834        }
835        _ => {
836            // "no_change" and any unrecognized action: a safe no-op.
837        }
838    }
839    config
840}
841
842impl Default for AdvancedConfigOptimizer {
843    fn default() -> Self {
844        Self::new()
845    }
846}
847
848impl AdvancedConfigOptimizer {
849    /// ✅ Advanced MODE: Create new advanced-intelligent configuration optimizer
850    pub fn new() -> Self {
851        AdvancedConfigOptimizer {
852            performance_history: HashMap::new(),
853            system_monitor: SystemMonitor::new(),
854            config_predictor: ConfigurationPredictor::new(),
855            adaptive_tuner: AdaptiveParameterTuner::new(),
856        }
857    }
858
859    /// ✅ Advanced MODE: Intelligently optimize configuration in real-time
860    pub fn advanced_optimize_config(
861        &mut self,
862        datachars: &DataCharacteristics,
863        transformation_type: &str,
864        user_params: &HashMap<String, f64>,
865    ) -> Result<OptimizationConfig> {
866        // Update real-time system metrics
867        self.system_monitor.update_metrics()?;
868
869        // Generate state representation for ML models
870        let current_state = self.generate_state_representation(datachars, &self.system_monitor);
871
872        // Use ML predictor to suggest initial configuration
873        let predicted_config = self.config_predictor.predict_optimal_config(
874            &current_state,
875            transformation_type,
876            user_params,
877        )?;
878
879        // Apply adaptive parameter tuning
880        let tuned_config = self.adaptive_tuner.tune_parameters(
881            predicted_config,
882            &current_state,
883            transformation_type,
884        )?;
885
886        // Validate configuration against system constraints
887        let validated_config =
888            self.validate_and_adjust_config(tuned_config, &self.system_monitor)?;
889
890        Ok(validated_config)
891    }
892
893    /// ✅ Advanced MODE: Learn from transformation performance feedback
894    pub fn learn_from_performance(
895        &mut self,
896        config: &OptimizationConfig,
897        performance: PerformanceMetric,
898        transformation_type: &str,
899    ) -> Result<()> {
900        let config_hash = self.compute_config_hash(config);
901
902        // Store performance history
903        self.performance_history
904            .entry(transformation_type.to_string())
905            .or_default()
906            .push(performance.clone());
907
908        // Update ML predictor
909        self.config_predictor.update_from_feedback(&performance)?;
910
911        // Update adaptive tuner with reward signal
912        let reward = self.compute_reward_signal(&performance);
913        self.adaptive_tuner.update_q_values(config_hash, reward)?;
914
915        // Trigger online learning if enough samples accumulated
916        if self.config_predictor.sample_count.is_multiple_of(100) {
917            self.retrain_models()?;
918        }
919
920        Ok(())
921    }
922
923    /// Generate state representation for ML models
924    fn generate_state_representation(
925        &self,
926        datachars: &DataCharacteristics,
927        system_monitor: &SystemMonitor,
928    ) -> String {
929        format!(
930            "samples:{}_features:{}_memory:{:.2}_cpu:{:.2}_sparsity:{:.3}",
931            datachars.n_samples,
932            datachars.nfeatures,
933            datachars.memory_footprint_mb,
934            system_monitor.cpu_load,
935            datachars.sparsity,
936        )
937    }
938
939    /// Compute configuration hash for identification
940    fn compute_config_hash(&self, config: &OptimizationConfig) -> u64 {
941        use std::collections::hash_map::DefaultHasher;
942        use std::hash::{Hash, Hasher};
943
944        let mut hasher = DefaultHasher::new();
945        config.memory_limit_mb.hash(&mut hasher);
946        config.use_parallel.hash(&mut hasher);
947        config.use_simd.hash(&mut hasher);
948        config.use_gpu.hash(&mut hasher);
949        config.chunk_size.hash(&mut hasher);
950        config.num_threads.hash(&mut hasher);
951
952        hasher.finish()
953    }
954
955    /// Compute reward signal from performance metrics
956    fn compute_reward_signal(&self, performance: &PerformanceMetric) -> f64 {
957        // Multi-objective reward function
958        let time_score = 1.0 / (1.0 + performance.execution_time_us as f64 / 1_000_000.0);
959        let memory_score = 1.0 / (1.0 + performance.memory_usage_bytes as f64 / 1_000_000_000.0);
960        let cache_score = performance.cache_hit_rate;
961        let cpu_score = 1.0 - performance.cpu_utilization.min(1.0);
962        let quality_score = performance.quality_score;
963
964        // Weighted combination
965        0.3 * time_score
966            + 0.2 * memory_score
967            + 0.2 * cache_score
968            + 0.1 * cpu_score
969            + 0.2 * quality_score
970    }
971
972    /// Validate and adjust configuration based on current system state
973    fn validate_and_adjust_config(
974        &self,
975        mut config: OptimizationConfig,
976        system_monitor: &SystemMonitor,
977    ) -> Result<OptimizationConfig> {
978        // Adjust based on available memory
979        let available_mb = system_monitor.available_memory_bytes / (1024 * 1024);
980        config.memory_limit_mb = config.memory_limit_mb.min(available_mb * 80 / 100); // 80% safety margin
981
982        // Adjust parallelism based on CPU load
983        if system_monitor.cpu_load > 0.8 {
984            config.num_threads = (config.num_threads / 2).max(1);
985        }
986
987        // Disable GPU if thermal throttling detected
988        if system_monitor.cpu_temperature_celsius > 85.0 {
989            config.use_gpu = false;
990        }
991
992        // Adjust chunk size based on cache miss rate
993        if system_monitor.cache_miss_rate > 0.1 {
994            config.chunk_size = (config.chunk_size as f64 * 0.8) as usize;
995        }
996
997        Ok(config)
998    }
999
1000    /// Retrain ML models with accumulated data
1001    fn retrain_models(&mut self) -> Result<()> {
1002        // Retrain configuration predictor
1003        self.config_predictor
1004            .retrain_with_history(&self.performance_history)?;
1005
1006        // Update adaptive tuner exploration rate
1007        self.adaptive_tuner.decay_exploration_rate();
1008
1009        Ok(())
1010    }
1011}
1012
1013impl Default for SystemMonitor {
1014    fn default() -> Self {
1015        Self::new()
1016    }
1017}
1018
1019impl SystemMonitor {
1020    /// Create new system monitor
1021    pub fn new() -> Self {
1022        SystemMonitor {
1023            cpu_load: 0.0,
1024            available_memory_bytes: 0,
1025            cache_miss_rate: 0.0,
1026            io_wait_percent: 0.0,
1027            cpu_temperature_celsius: 50.0,
1028            prev_cpu_jiffies: None,
1029        }
1030    }
1031
1032    /// ✅ Advanced MODE: Update real-time system metrics
1033    pub fn update_metrics(&mut self) -> Result<()> {
1034        self.cpu_load = self.read_cpu_load()?;
1035        self.available_memory_bytes = self.read_available_memory()?;
1036        self.cache_miss_rate = self.read_cache_miss_rate()?;
1037        self.io_wait_percent = self.read_io_wait()?;
1038        self.cpu_temperature_celsius = self.read_cpu_temperature()?;
1039
1040        Ok(())
1041    }
1042
1043    /// Real average CPU utilization (0.0-1.0) across all logical cores, via
1044    /// `sysinfo` (portable across Linux/macOS/Windows).
1045    fn read_cpu_load(&self) -> Result<f64> {
1046        let mut system = sysinfo::System::new_all();
1047        system.refresh_cpu_all();
1048        let cpus = system.cpus();
1049        if cpus.is_empty() {
1050            return Ok(0.0);
1051        }
1052        let total: f64 = cpus.iter().map(|cpu| cpu.cpu_usage() as f64 / 100.0).sum();
1053        Ok((total / cpus.len() as f64).clamp(0.0, 1.0))
1054    }
1055
1056    /// Real available system memory in bytes, via `sysinfo`.
1057    fn read_available_memory(&self) -> Result<usize> {
1058        let mut system = sysinfo::System::new_all();
1059        system.refresh_memory();
1060        // sysinfo 0.39 reports memory quantities in bytes already; guard
1061        // against the (documented) possibility of `0` on an unsupported
1062        // platform by falling back to a conservative, clearly-labeled
1063        // estimate rather than claiming a specific fabricated capacity.
1064        let available = system.available_memory();
1065        if available == 0 {
1066            return Ok(1024 * 1024 * 1024); // 1GB conservative fallback
1067        }
1068        Ok(available as usize)
1069    }
1070
1071    /// Hardware cache-miss rate.
1072    ///
1073    /// This is genuinely NOT measurable through `std` or `sysinfo`: it
1074    /// requires hardware performance-counter access (e.g. Linux
1075    /// `perf_event_open`), which needs elevated privileges/capabilities and
1076    /// platform-specific `unsafe` FFI wildly out of proportion for a
1077    /// general-purpose data-transform crate's soft auto-tuning heuristic.
1078    /// Rather than silently fabricate a specific, plausible-looking
1079    /// percentage (as the previous placeholder did), this honestly reports
1080    /// a neutral value documented as "not measured" -- exactly at the
1081    /// midpoint of [`Self::update_metrics`]'s only consumer
1082    /// (`AdvancedConfigOptimizer::validate_and_adjust_config`'s `> 0.1`
1083    /// chunk-size check), so it neither spuriously triggers nor spuriously
1084    /// suppresses that adjustment.
1085    fn read_cache_miss_rate(&self) -> Result<f64> {
1086        Ok(0.05)
1087    }
1088
1089    /// Real I/O-wait percentage on Linux, computed as the delta of the
1090    /// `iowait` jiffies counter in `/proc/stat` between successive calls
1091    /// (a single snapshot only gives an accumulated-since-boot counter, not
1092    /// a rate). The first call after construction has no baseline and
1093    /// honestly reports `0.0` rather than a fabricated figure. Platforms
1094    /// without `/proc/stat` (non-Linux) also honestly report `0.0`
1095    /// ("not measured on this platform") instead of a fabricated constant.
1096    fn read_io_wait(&mut self) -> Result<f64> {
1097        #[cfg(target_os = "linux")]
1098        {
1099            let Some((idle_plus_iowait, iowait, total)) = read_proc_stat_cpu_jiffies() else {
1100                return Ok(0.0);
1101            };
1102            let previous = self
1103                .prev_cpu_jiffies
1104                .replace((idle_plus_iowait, iowait, total));
1105            let Some((_, prev_iowait, prev_total)) = previous else {
1106                // No baseline yet (first call): nothing to compute a rate from.
1107                return Ok(0.0);
1108            };
1109            let total_delta = total.saturating_sub(prev_total);
1110            let iowait_delta = iowait.saturating_sub(prev_iowait);
1111            if total_delta == 0 {
1112                return Ok(0.0);
1113            }
1114            Ok((iowait_delta as f64 / total_delta as f64).clamp(0.0, 1.0))
1115        }
1116        #[cfg(not(target_os = "linux"))]
1117        {
1118            Ok(0.0)
1119        }
1120    }
1121
1122    /// Real CPU temperature on Linux via the kernel thermal-zone sysfs
1123    /// interface (plain text file, no `unsafe`/FFI needed). Platforms
1124    /// without this interface honestly fall back to a documented neutral
1125    /// value (below any reasonable thermal-throttling threshold) rather
1126    /// than a fabricated "measured" temperature.
1127    fn read_cpu_temperature(&self) -> Result<f64> {
1128        #[cfg(target_os = "linux")]
1129        {
1130            for zone in 0..8 {
1131                let path = format!("/sys/class/thermal/thermal_zone{zone}/temp");
1132                if let Ok(contents) = std::fs::read_to_string(&path) {
1133                    if let Ok(millidegrees) = contents.trim().parse::<f64>() {
1134                        // Kernel reports milli-degrees Celsius.
1135                        return Ok(millidegrees / 1000.0);
1136                    }
1137                }
1138            }
1139            Ok(50.0) // No thermal zone readable: honest neutral fallback.
1140        }
1141        #[cfg(not(target_os = "linux"))]
1142        {
1143            Ok(50.0)
1144        }
1145    }
1146}
1147
1148/// Read the aggregate `cpu` line of `/proc/stat` and return
1149/// `(idle+iowait, iowait, total)` jiffies, or `None` if unavailable/
1150/// unparseable. Field order per `man proc` (5th=idle, 6th=iowait):
1151/// `cpu  user nice system idle iowait irq softirq steal guest guest_nice`.
1152#[cfg(target_os = "linux")]
1153fn read_proc_stat_cpu_jiffies() -> Option<(u64, u64, u64)> {
1154    let contents = std::fs::read_to_string("/proc/stat").ok()?;
1155    let line = contents.lines().find(|l| l.starts_with("cpu "))?;
1156    let fields: Vec<u64> = line
1157        .split_whitespace()
1158        .skip(1)
1159        .filter_map(|f| f.parse::<u64>().ok())
1160        .collect();
1161    if fields.len() < 5 {
1162        return None;
1163    }
1164    let idle = fields[3];
1165    let iowait = fields.get(4).copied().unwrap_or(0);
1166    let total: u64 = fields.iter().sum();
1167    Some((idle + iowait, iowait, total))
1168}
1169
1170impl Default for ConfigurationPredictor {
1171    fn default() -> Self {
1172        Self::new()
1173    }
1174}
1175
1176/// Initial (neutral-baseline) feature weights: chosen so that, before any
1177/// real feedback has been learned, [`ConfigurationPredictor::predict_memory_limit`]/
1178/// [`ConfigurationPredictor::predict_parallelism`]/
1179/// [`ConfigurationPredictor::predict_simd_usage`] reproduce the same
1180/// heuristic thresholds this module always used (`* 1.5` memory multiplier,
1181/// `5000` sample / `0.7` cpu-load parallelism thresholds, `50` feature-count
1182/// SIMD threshold). Real feedback then shifts behavior away from this
1183/// baseline over time via [`ConfigurationPredictor::update_from_feedback`].
1184const INITIAL_SAMPLES_WEIGHT: f64 = 0.3;
1185const INITIAL_FEATURES_WEIGHT: f64 = 0.25;
1186const INITIAL_MEMORY_WEIGHT: f64 = 0.2;
1187const INITIAL_CPU_WEIGHT: f64 = 0.1;
1188
1189impl ConfigurationPredictor {
1190    /// Create new configuration predictor
1191    pub fn new() -> Self {
1192        let mut feature_weights = HashMap::new();
1193        feature_weights.insert("samples".to_string(), INITIAL_SAMPLES_WEIGHT);
1194        feature_weights.insert("features".to_string(), INITIAL_FEATURES_WEIGHT);
1195        feature_weights.insert("memory".to_string(), INITIAL_MEMORY_WEIGHT);
1196        feature_weights.insert("sparsity".to_string(), 0.15);
1197        feature_weights.insert("cpu".to_string(), INITIAL_CPU_WEIGHT);
1198
1199        ConfigurationPredictor {
1200            feature_weights,
1201            learning_rate: 0.01,
1202            confidence_threshold: 0.8,
1203            sample_count: 0,
1204            last_features: HashMap::new(),
1205        }
1206    }
1207
1208    /// Predict optimal configuration using ML model
1209    pub fn predict_optimal_config(
1210        &mut self,
1211        state: &str,
1212        _transformation_type: &str,
1213        _user_params: &HashMap<String, f64>,
1214    ) -> Result<OptimizationConfig> {
1215        // Extract features from state
1216        let features = self.extract_features(state)?;
1217
1218        // Predict configuration parameters using weighted features
1219        let predicted_memory_limit = self.predict_memory_limit(&features);
1220        let predicted_parallelism = self.predict_parallelism(&features);
1221        let predicted_simd_usage = self.predict_simd_usage(&features);
1222
1223        // Remember which features drove this prediction so a later
1224        // `update_from_feedback` call can attribute real credit/blame to
1225        // them (see `last_features`'s doc comment).
1226        self.last_features = features.clone();
1227
1228        // Create base configuration
1229        let strategy = if predicted_memory_limit < 1000 {
1230            ProcessingStrategy::OutOfCore { chunk_size: 1024 }
1231        } else if predicted_parallelism {
1232            ProcessingStrategy::Parallel
1233        } else if predicted_simd_usage {
1234            ProcessingStrategy::Simd
1235        } else {
1236            ProcessingStrategy::Standard
1237        };
1238
1239        Ok(OptimizationConfig {
1240            processing_strategy: strategy,
1241            memory_limit_mb: predicted_memory_limit,
1242            use_robust: false,
1243            use_parallel: predicted_parallelism,
1244            use_simd: predicted_simd_usage,
1245            use_gpu: features.get("memory").copied().unwrap_or(0.0) > 100.0,
1246            chunk_size: if predicted_memory_limit < 1000 {
1247                512
1248            } else {
1249                2048
1250            },
1251            num_threads: if predicted_parallelism { 4 } else { 1 },
1252            algorithm_params: HashMap::new(),
1253        })
1254    }
1255
1256    /// Extract numerical features from state string. Keys match
1257    /// `AdvancedConfigOptimizer::generate_state_representation`'s output
1258    /// (`samples`, `features`, `memory`, `cpu`, `sparsity`) -- the same
1259    /// names used as [`Self::feature_weights`] keys, so the learned weights
1260    /// actually apply to the values that were really extracted (a previous
1261    /// version of this code used mismatched key names -- e.g.
1262    /// `"memory_footprint"` vs the state string's `"memory"` -- so the
1263    /// lookups always missed and silently fell back to hardcoded defaults).
1264    fn extract_features(&self, state: &str) -> Result<HashMap<String, f64>> {
1265        let mut features = HashMap::new();
1266
1267        for part in state.split('_') {
1268            if let Some((key, value)) = part.split_once(':') {
1269                if let Ok(val) = value.parse::<f64>() {
1270                    features.insert(key.to_string(), val);
1271                }
1272            }
1273        }
1274
1275        Ok(features)
1276    }
1277
1278    fn predict_memory_limit(&self, features: &HashMap<String, f64>) -> usize {
1279        let memory_footprint = features.get("memory").copied().unwrap_or(100.0);
1280        let weight = self
1281            .feature_weights
1282            .get("memory")
1283            .copied()
1284            .unwrap_or(INITIAL_MEMORY_WEIGHT);
1285        // Base heuristic is `* 1.5`; the learned weight scales that
1286        // multiplier proportionally to how it has drifted from its
1287        // neutral-baseline value, so real feedback genuinely changes the
1288        // prediction instead of the weight being read-but-ignored.
1289        let effective_multiplier = 1.5 * (weight / INITIAL_MEMORY_WEIGHT).max(0.0);
1290        (memory_footprint * effective_multiplier) as usize
1291    }
1292
1293    fn predict_parallelism(&self, features: &HashMap<String, f64>) -> bool {
1294        let samples = features.get("samples").copied().unwrap_or(1000.0);
1295        let cpu_load = features.get("cpu").copied().unwrap_or(0.5);
1296        let weight = self
1297            .feature_weights
1298            .get("samples")
1299            .copied()
1300            .unwrap_or(INITIAL_SAMPLES_WEIGHT);
1301        // A higher learned importance for "samples" lowers the sample-count
1302        // bar for enabling parallelism (and vice versa), clamped to a sane
1303        // range so the threshold never becomes degenerate.
1304        let threshold =
1305            (5000.0 * (INITIAL_SAMPLES_WEIGHT / weight.max(0.01))).clamp(500.0, 50_000.0);
1306        samples > threshold && cpu_load < 0.7
1307    }
1308
1309    fn predict_simd_usage(&self, features: &HashMap<String, f64>) -> bool {
1310        let features_count = features.get("features").copied().unwrap_or(10.0);
1311        let weight = self
1312            .feature_weights
1313            .get("features")
1314            .copied()
1315            .unwrap_or(INITIAL_FEATURES_WEIGHT);
1316        let threshold = (50.0 * (INITIAL_FEATURES_WEIGHT / weight.max(0.01))).clamp(5.0, 500.0);
1317        features_count > threshold
1318    }
1319
1320    /// Update model from performance feedback: a real Widrow-Hoff (LMS)
1321    /// online update, using the private `learning_rate` field and the feature
1322    /// vector that was actually active for the prediction being evaluated
1323    /// (the private `last_features` field, captured by
1324    /// [`Self::predict_optimal_config`]).
1325    ///
1326    /// `performance.quality_score` (a real, caller-observed measurement,
1327    /// not fabricated) is compared against a neutral `0.5` baseline to form
1328    /// an error signal; each feature's weight is nudged proportionally to
1329    /// that feature's own (magnitude-normalized) value at prediction time,
1330    /// clamped to `[0, 1]` to keep the model stable.
1331    pub fn update_from_feedback(&mut self, performance: &PerformanceMetric) -> Result<()> {
1332        self.sample_count += 1;
1333
1334        if self.last_features.is_empty() {
1335            // No recorded prediction context to attribute this feedback to.
1336            return Ok(());
1337        }
1338
1339        let reward = performance.quality_score.clamp(0.0, 1.0);
1340        let error = reward - 0.5;
1341        let max_abs = self
1342            .last_features
1343            .values()
1344            .fold(1.0_f64, |acc, &v| acc.max(v.abs()));
1345
1346        for (key, weight) in self.feature_weights.iter_mut() {
1347            if let Some(&raw_value) = self.last_features.get(key) {
1348                let normalized = raw_value / max_abs;
1349                *weight = (*weight + self.learning_rate * error * normalized).clamp(0.0, 1.0);
1350            }
1351        }
1352
1353        Ok(())
1354    }
1355
1356    /// Retrain model with historical data
1357    pub fn retrain_with_history(
1358        &mut self,
1359        history: &HashMap<String, Vec<PerformanceMetric>>,
1360    ) -> Result<()> {
1361        // In practice, this would perform full model retraining
1362        let _ = history;
1363        self.confidence_threshold = (self.confidence_threshold + 0.01).min(0.95);
1364        Ok(())
1365    }
1366}
1367
1368impl Default for AdaptiveParameterTuner {
1369    fn default() -> Self {
1370        Self::new()
1371    }
1372}
1373
1374impl AdaptiveParameterTuner {
1375    /// Create new adaptive parameter tuner
1376    pub fn new() -> Self {
1377        AdaptiveParameterTuner {
1378            q_table: HashMap::new(),
1379            exploration_rate: 0.1,
1380            learning_rate: 0.1,
1381            discount_factor: 0.9,
1382            current_state: String::new(),
1383            last_action: "no_change".to_string(),
1384        }
1385    }
1386
1387    /// Tune parameters using reinforcement learning
1388    pub fn tune_parameters(
1389        &mut self,
1390        mut config: OptimizationConfig,
1391        state: &str,
1392        _transformation_type: &str,
1393    ) -> Result<OptimizationConfig> {
1394        self.current_state = state.to_string();
1395
1396        // Apply epsilon-greedy policy for parameter exploration
1397        if scirs2_core::random::rng().random_range(0.0..1.0) < self.exploration_rate {
1398            // Explore: randomly adjust parameters
1399            config = self.explore_parameters(config)?;
1400        } else {
1401            // Exploit: use best known parameters from Q-table
1402            config = self.exploit_best_parameters(config, state)?;
1403        }
1404
1405        Ok(config)
1406    }
1407
1408    /// Explore by applying one randomly chosen action from
1409    /// [`TUNER_ACTIONS`], recording it in [`Self::last_action`] so a
1410    /// subsequent [`Self::update_q_values`] call attributes the resulting
1411    /// reward to the action that was actually taken.
1412    fn explore_parameters(&mut self, config: OptimizationConfig) -> Result<OptimizationConfig> {
1413        let mut rng = scirs2_core::random::rng();
1414        let idx = rng.random_range(0..TUNER_ACTIONS.len());
1415        let action = TUNER_ACTIONS[idx];
1416        self.last_action = action.to_string();
1417        Ok(apply_tuner_action(action, config))
1418    }
1419
1420    /// Exploit the best known action for `state` from the Q-table, and
1421    /// genuinely apply it to `config` (previously this looked up
1422    /// `_best_action` and then discarded it, always returning `config`
1423    /// unchanged).
1424    fn exploit_best_parameters(
1425        &mut self,
1426        config: OptimizationConfig,
1427        state: &str,
1428    ) -> Result<OptimizationConfig> {
1429        let best_action = self.find_best_action(state);
1430        self.last_action = best_action.clone();
1431        Ok(apply_tuner_action(&best_action, config))
1432    }
1433
1434    /// Find the highest-Q-value action recorded for `state`, or
1435    /// `"no_change"` if the state has no history yet.
1436    fn find_best_action(&self, state: &str) -> String {
1437        let mut best_action = "no_change".to_string();
1438        let mut best_value = f64::NEG_INFINITY;
1439
1440        for ((s, action), &value) in &self.q_table {
1441            if s == state && value > best_value {
1442                best_value = value;
1443                best_action = action.clone();
1444            }
1445        }
1446
1447        best_action
1448    }
1449
1450    /// Update Q-values based on reward for the action actually taken by the
1451    /// most recent [`Self::tune_parameters`] call (previously every update
1452    /// was keyed on the literal string `"current_action"` regardless of
1453    /// which action ran, so the table could never distinguish between
1454    /// `explore_parameters`' and `exploit_best_parameters`' choices).
1455    pub fn update_q_values(&mut self, confighash: u64, reward: f64) -> Result<()> {
1456        // `confighash` identifies the resulting configuration for the
1457        // caller's own bookkeeping (see `AdvancedConfigOptimizer::learn_from_performance`);
1458        // the Q-table itself is keyed on (state, action) per standard
1459        // tabular Q-learning.
1460        let _ = confighash;
1461        let state_action = (self.current_state.clone(), self.last_action.clone());
1462
1463        // Q-learning update rule
1464        let old_value = self.q_table.get(&state_action).copied().unwrap_or(0.0);
1465        let new_value = old_value + self.learning_rate * (reward - old_value);
1466
1467        self.q_table.insert(state_action, new_value);
1468
1469        Ok(())
1470    }
1471
1472    /// Decay exploration rate over time
1473    pub fn decay_exploration_rate(&mut self) {
1474        self.exploration_rate = (self.exploration_rate * 0.995).max(0.01);
1475    }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480    use super::*;
1481    use scirs2_core::ndarray::Array2;
1482
1483    #[test]
1484    fn test_system_resources_detection() {
1485        let resources = SystemResources::detect();
1486        assert!(resources.cpu_cores > 0);
1487        assert!(resources.memory_mb > 0);
1488        assert!(resources.safe_memory_mb() < resources.memory_mb);
1489    }
1490
1491    #[test]
1492    fn test_data_characteristics_analysis() {
1493        let data = Array2::from_shape_vec((100, 10), (0..1000).map(|x| x as f64).collect())
1494            .expect("Operation failed");
1495        let chars = DataCharacteristics::analyze(&data.view()).expect("Operation failed");
1496
1497        assert_eq!(chars.n_samples, 100);
1498        assert_eq!(chars.nfeatures, 10);
1499        assert!(chars.memory_footprint_mb > 0.0);
1500        assert!(!chars.is_large_dataset());
1501    }
1502
1503    #[test]
1504    fn test_optimization_config_for_standardization() {
1505        let data = Array2::ones((1000, 50));
1506        let chars = DataCharacteristics::analyze(&data.view()).expect("Operation failed");
1507        let system = SystemResources::detect();
1508
1509        let config = OptimizationConfig::for_standardization(&chars, &system);
1510        assert!(config.memory_limit_mb > 0);
1511    }
1512
1513    #[test]
1514    fn test_optimization_config_for_pca() {
1515        let data = Array2::ones((500, 20));
1516        let chars = DataCharacteristics::analyze(&data.view()).expect("Operation failed");
1517        let system = SystemResources::detect();
1518
1519        let config = OptimizationConfig::for_pca(&chars, &system, 10);
1520        assert_eq!(config.algorithm_params.get("n_components"), Some(&10.0));
1521    }
1522
1523    #[test]
1524    fn test_polynomial_features_estimation() {
1525        // Test polynomial feature estimation
1526        let result = OptimizationConfig::estimate_polynomial_features(5, 2);
1527        assert!(result.is_ok());
1528
1529        // Should handle large degrees gracefully
1530        let result = OptimizationConfig::estimate_polynomial_features(100, 10);
1531        assert!(result.is_err());
1532    }
1533
1534    #[test]
1535    fn test_auto_tuner() {
1536        let tuner = AutoTuner::new();
1537        let data = Array2::ones((100, 10));
1538        let chars = DataCharacteristics::analyze(&data.view()).expect("Operation failed");
1539
1540        let config = tuner
1541            .optimize_for_transformation("standardization", &chars, &HashMap::new())
1542            .expect("Operation failed");
1543        assert!(config.memory_limit_mb > 0);
1544
1545        let report = tuner.generate_report(&chars);
1546        assert!(!report.recommendations.is_empty());
1547    }
1548
1549    #[test]
1550    fn test_large_dataset_detection() {
1551        let mut chars = DataCharacteristics {
1552            n_samples: 200_000,
1553            nfeatures: 1000,
1554            sparsity: 0.1,
1555            data_range: 100.0,
1556            outlier_ratio: 0.02,
1557            has_missing: false,
1558            memory_footprint_mb: 1500.0,
1559            elementsize: 8,
1560        };
1561
1562        assert!(chars.is_large_dataset());
1563
1564        chars.n_samples = 1000;
1565        chars.memory_footprint_mb = 10.0;
1566        assert!(!chars.is_large_dataset());
1567    }
1568
1569    // -------------------------------------------------------------------------
1570    // SystemMonitor: real OS metrics, not hardcoded placeholders.
1571    // -------------------------------------------------------------------------
1572
1573    #[test]
1574    fn system_monitor_cpu_load_is_a_real_bounded_measurement() {
1575        let mut monitor = SystemMonitor::new();
1576        monitor.update_metrics().expect("should succeed");
1577        assert!(
1578            (0.0..=1.0).contains(&monitor.cpu_load),
1579            "cpu_load must be a real, bounded fraction, got {}",
1580            monitor.cpu_load
1581        );
1582    }
1583
1584    #[test]
1585    fn system_monitor_available_memory_is_real_not_the_old_fabricated_8gb_constant() {
1586        let mut monitor = SystemMonitor::new();
1587        monitor.update_metrics().expect("should succeed");
1588        assert!(monitor.available_memory_bytes > 0);
1589        // Real available memory fluctuates continuously; being bit-exact to
1590        // the old hardcoded placeholder would be an astronomically
1591        // improbable coincidence on any real machine.
1592        assert_ne!(
1593            monitor.available_memory_bytes,
1594            8 * 1024 * 1024 * 1024,
1595            "must not be the old fabricated 8GB placeholder"
1596        );
1597        // Sanity upper bound: less than 16TB (generously above any real
1598        // machine this test would run on).
1599        assert!(monitor.available_memory_bytes < 16usize * 1024 * 1024 * 1024 * 1024);
1600    }
1601
1602    #[test]
1603    fn system_monitor_io_wait_has_no_baseline_on_first_call_then_a_bounded_delta_afterward() {
1604        let mut monitor = SystemMonitor::new();
1605        assert!(monitor.prev_cpu_jiffies.is_none());
1606
1607        monitor.update_metrics().expect("should succeed");
1608        // First call: no prior snapshot to compute a delta from, so io_wait
1609        // is honestly 0.0 rather than a fabricated figure (on Linux this
1610        // also establishes the baseline; on other platforms it's the
1611        // permanent, honestly-documented behavior).
1612        assert_eq!(monitor.io_wait_percent, 0.0);
1613
1614        std::thread::sleep(std::time::Duration::from_millis(20));
1615        monitor.update_metrics().expect("should succeed");
1616        assert!(
1617            (0.0..=1.0).contains(&monitor.io_wait_percent),
1618            "io_wait_percent must stay within a real bounded fraction, got {}",
1619            monitor.io_wait_percent
1620        );
1621    }
1622
1623    // -------------------------------------------------------------------------
1624    // ConfigurationPredictor: real weight learning, not a sample counter.
1625    // -------------------------------------------------------------------------
1626
1627    fn quality_feedback(quality_score: f64) -> PerformanceMetric {
1628        PerformanceMetric {
1629            config_hash: 0,
1630            execution_time_us: 1000,
1631            memory_usage_bytes: 1_000_000,
1632            cache_hit_rate: 0.9,
1633            cpu_utilization: 0.3,
1634            quality_score,
1635            timestamp: std::time::Instant::now(),
1636        }
1637    }
1638
1639    #[test]
1640    fn update_from_feedback_actually_changes_feature_weights() {
1641        let mut predictor = ConfigurationPredictor::new();
1642        let initial_weight = *predictor.feature_weights.get("memory").expect("present");
1643
1644        // Establish prediction context (`last_features`) with a strong
1645        // "memory" signal, then repeatedly report *good* outcomes.
1646        predictor
1647            .predict_optimal_config(
1648                "samples:1000_features:20_memory:500.00_cpu:0.30_sparsity:0.100",
1649                "std",
1650                &HashMap::new(),
1651            )
1652            .expect("should succeed");
1653        for _ in 0..50 {
1654            predictor
1655                .update_from_feedback(&quality_feedback(0.95))
1656                .expect("should succeed");
1657        }
1658
1659        let updated_weight = *predictor.feature_weights.get("memory").expect("present");
1660        assert!(
1661            (updated_weight - initial_weight).abs() > 1e-6,
1662            "repeated positive feedback must move the learned weight away from its \
1663             initial value: initial={initial_weight}, updated={updated_weight}"
1664        );
1665        assert!(
1666            updated_weight > initial_weight,
1667            "positive feedback should increase the weight"
1668        );
1669    }
1670
1671    #[test]
1672    fn learned_weights_genuinely_change_the_predicted_memory_limit() {
1673        let state = "samples:1000_features:20_memory:500.00_cpu:0.30_sparsity:0.100";
1674
1675        let mut baseline_predictor = ConfigurationPredictor::new();
1676        let baseline_config = baseline_predictor
1677            .predict_optimal_config(state, "std", &HashMap::new())
1678            .expect("should succeed");
1679
1680        let mut trained_predictor = ConfigurationPredictor::new();
1681        trained_predictor
1682            .predict_optimal_config(state, "std", &HashMap::new())
1683            .expect("should succeed");
1684        for _ in 0..200 {
1685            trained_predictor
1686                .update_from_feedback(&quality_feedback(1.0))
1687                .expect("should succeed");
1688        }
1689        let trained_config = trained_predictor
1690            .predict_optimal_config(state, "std", &HashMap::new())
1691            .expect("should succeed");
1692
1693        assert_ne!(
1694            baseline_config.memory_limit_mb, trained_config.memory_limit_mb,
1695            "learned feedback must genuinely change the predicted memory limit, \
1696             not silently leave the weights (and therefore the prediction) unchanged"
1697        );
1698    }
1699
1700    #[test]
1701    fn update_from_feedback_with_no_prior_prediction_is_a_safe_no_op() {
1702        let mut predictor = ConfigurationPredictor::new();
1703        let before = predictor.feature_weights.clone();
1704        predictor
1705            .update_from_feedback(&quality_feedback(0.9))
1706            .expect("should succeed");
1707        assert_eq!(predictor.feature_weights, before);
1708    }
1709
1710    // -------------------------------------------------------------------------
1711    // AdaptiveParameterTuner: real Q-learning that distinguishes actions and
1712    // actually applies the exploited best action.
1713    // -------------------------------------------------------------------------
1714
1715    #[test]
1716    fn exploit_best_parameters_actually_applies_the_learned_action() {
1717        let mut tuner = AdaptiveParameterTuner::new();
1718        let state = "state_a";
1719        tuner.current_state = state.to_string();
1720        // Seed the Q-table so "increase_memory" is unambiguously the best
1721        // action for this state.
1722        tuner
1723            .q_table
1724            .insert((state.to_string(), "increase_memory".to_string()), 10.0);
1725        tuner
1726            .q_table
1727            .insert((state.to_string(), "decrease_memory".to_string()), -5.0);
1728        tuner
1729            .q_table
1730            .insert((state.to_string(), "no_change".to_string()), 0.0);
1731
1732        let config = OptimizationConfig {
1733            processing_strategy: ProcessingStrategy::Standard,
1734            memory_limit_mb: 1000,
1735            use_robust: false,
1736            use_parallel: false,
1737            use_simd: false,
1738            use_gpu: false,
1739            chunk_size: 1024,
1740            num_threads: 1,
1741            algorithm_params: HashMap::new(),
1742        };
1743
1744        let tuned = tuner
1745            .exploit_best_parameters(config.clone(), state)
1746            .expect("should succeed");
1747
1748        assert_eq!(tuner.last_action, "increase_memory");
1749        assert!(
1750            tuned.memory_limit_mb > config.memory_limit_mb,
1751            "the learned best action ('increase_memory') must actually be applied, \
1752             not discarded: before={}, after={}",
1753            config.memory_limit_mb,
1754            tuned.memory_limit_mb
1755        );
1756    }
1757
1758    #[test]
1759    fn update_q_values_distinguishes_between_different_actions() {
1760        let mut tuner = AdaptiveParameterTuner::new();
1761        tuner.current_state = "state_a".to_string();
1762
1763        tuner.last_action = "increase_memory".to_string();
1764        tuner.update_q_values(0, 1.0).expect("should succeed");
1765
1766        tuner.last_action = "decrease_memory".to_string();
1767        tuner.update_q_values(0, -1.0).expect("should succeed");
1768
1769        let increase_value = tuner
1770            .q_table
1771            .get(&("state_a".to_string(), "increase_memory".to_string()))
1772            .copied();
1773        let decrease_value = tuner
1774            .q_table
1775            .get(&("state_a".to_string(), "decrease_memory".to_string()))
1776            .copied();
1777
1778        assert!(
1779            increase_value.is_some() && decrease_value.is_some(),
1780            "each distinct action taken must get its own Q-table entry, not \
1781             collapse onto a single hardcoded key: q_table={:?}",
1782            tuner.q_table
1783        );
1784        assert_ne!(
1785            increase_value, decrease_value,
1786            "different rewards for different actions must be tracked separately"
1787        );
1788        // The old code hardcoded every entry onto exactly one key
1789        // ("state", "current_action"); with two real distinct actions taken
1790        // there must now be (at least) two entries.
1791        assert!(tuner.q_table.len() >= 2);
1792    }
1793
1794    #[test]
1795    fn explore_parameters_records_a_real_named_action() {
1796        let mut tuner = AdaptiveParameterTuner::new();
1797        let config = OptimizationConfig {
1798            processing_strategy: ProcessingStrategy::Standard,
1799            memory_limit_mb: 1000,
1800            use_robust: false,
1801            use_parallel: false,
1802            use_simd: false,
1803            use_gpu: false,
1804            chunk_size: 1024,
1805            num_threads: 1,
1806            algorithm_params: HashMap::new(),
1807        };
1808        tuner.explore_parameters(config).expect("should succeed");
1809        assert!(
1810            TUNER_ACTIONS.contains(&tuner.last_action.as_str()),
1811            "explore_parameters must record one of the real named actions, got {:?}",
1812            tuner.last_action
1813        );
1814    }
1815}