1use 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#[derive(Debug, Clone)]
18#[cfg_attr(feature = "distributed", derive(Serialize, Deserialize))]
19pub struct SystemResources {
20 pub memory_mb: usize,
22 pub cpu_cores: usize,
24 pub has_gpu: bool,
26 pub has_simd: bool,
28 pub l3_cache_kb: usize,
30}
31
32impl SystemResources {
33 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 fn detect_memory_mb() -> usize {
46 #[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; }
56 }
57 }
58 }
59 }
60 }
61
62 8 * 1024
64 }
65
66 fn detect_gpu() -> bool {
68 #[cfg(feature = "gpu")]
70 {
71 true
73 }
74 #[cfg(not(feature = "gpu"))]
75 {
76 false
77 }
78 }
79
80 fn detect_simd() -> bool {
82 #[cfg(feature = "simd")]
83 {
84 true
85 }
86 #[cfg(not(feature = "simd"))]
87 {
88 false
89 }
90 }
91
92 fn detect_l3_cache_kb() -> usize {
94 8 * 1024 }
97
98 pub fn safe_memory_mb(&self) -> usize {
100 (self.memory_mb as f64 * 0.8) as usize
101 }
102
103 pub fn optimal_chunk_size(&self, elementsize: usize) -> usize {
105 let target_bytes = (self.l3_cache_kb * 1024) / 2;
107 (target_bytes / elementsize).max(1000) }
109}
110
111#[derive(Debug, Clone)]
113#[cfg_attr(feature = "distributed", derive(Serialize, Deserialize))]
114pub struct DataCharacteristics {
115 pub n_samples: usize,
117 pub nfeatures: usize,
119 pub sparsity: f64,
121 pub data_range: f64,
123 pub outlier_ratio: f64,
125 pub has_missing: bool,
127 pub memory_footprint_mb: f64,
129 pub elementsize: usize,
131}
132
133impl DataCharacteristics {
134 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 let zeros = data.iter().filter(|&&x| x == 0.0).count();
144 let sparsity = zeros as f64 / data.len() as f64;
145
146 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 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) .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 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 pub fn is_wide_dataset(&self) -> bool {
224 self.nfeatures > self.n_samples
225 }
226
227 pub fn is_sparse(&self) -> bool {
229 self.sparsity > 0.5
230 }
231
232 pub fn has_outliers(&self) -> bool {
234 self.outlier_ratio > 0.05 }
236}
237
238#[derive(Debug, Clone)]
240#[cfg_attr(feature = "distributed", derive(Serialize, Deserialize))]
241pub struct OptimizationConfig {
242 pub processing_strategy: ProcessingStrategy,
244 pub memory_limit_mb: usize,
246 pub use_robust: bool,
248 pub use_parallel: bool,
250 pub use_simd: bool,
252 pub use_gpu: bool,
254 pub chunk_size: usize,
256 pub num_threads: usize,
258 pub algorithm_params: HashMap<String, f64>,
260}
261
262impl OptimizationConfig {
263 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 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 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, 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 pub fn for_polynomial_features(
347 datachars: &DataCharacteristics,
348 system: &SystemResources,
349 degree: usize,
350 ) -> Result<Self> {
351 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, 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 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; for d in 1..=degree {
413 let mut coeff = 1;
415 for i in 0..d {
416 coeff = coeff * (nfeatures + d - 1 - i) / (i + 1);
417
418 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 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 }
441 ProcessingStrategy::Simd => {
442 2_000_000_000 }
444 ProcessingStrategy::OutOfCore { .. } => {
445 100_000_000 }
447 ProcessingStrategy::Standard => {
448 500_000_000 }
450 };
451
452 let time_ns = (base_ops * 1_000_000_000) / ops_per_second;
453 Duration::from_nanos(time_ns.max(1000)) }
455}
456
457pub struct AutoTuner {
459 system: SystemResources,
461 performance_history: HashMap<String, Vec<PerformanceRecord>>,
463}
464
465#[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 pub fn new() -> Self {
489 AutoTuner {
490 system: SystemResources::detect(),
491 performance_history: HashMap::new(),
492 }
493 }
494
495 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 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 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:?}"); 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 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 pub fn system_resources(&self) -> &SystemResources {
581 &self.system
582 }
583
584 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, }
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, reason: format!(
627 "Optimized for {} samples, {} features",
628 datachars.n_samples, datachars.nfeatures
629 ),
630 }
631 }
632}
633
634#[derive(Debug, Clone)]
636pub struct OptimizationReport {
637 pub system_info: SystemResources,
639 pub data_info: DataCharacteristics,
641 pub recommendations: Vec<TransformationRecommendation>,
643 pub estimated_total_memory_mb: f64,
645}
646
647#[derive(Debug, Clone)]
649pub struct TransformationRecommendation {
650 pub transformation: String,
652 pub config: OptimizationConfig,
654 pub estimated_time: std::time::Duration,
656 pub confidence: f64,
658 pub reason: String,
660}
661
662impl OptimizationReport {
663 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
701pub struct AdvancedConfigOptimizer {
705 performance_history: HashMap<String, Vec<PerformanceMetric>>,
707 system_monitor: SystemMonitor,
709 config_predictor: ConfigurationPredictor,
711 adaptive_tuner: AdaptiveParameterTuner,
713}
714
715#[derive(Debug, Clone)]
717pub struct PerformanceMetric {
718 #[allow(dead_code)]
720 config_hash: u64,
721 execution_time_us: u64,
723 memory_usage_bytes: usize,
725 cache_hit_rate: f64,
727 cpu_utilization: f64,
729 quality_score: f64,
731 #[allow(dead_code)]
733 timestamp: std::time::Instant,
734}
735
736pub struct SystemMonitor {
738 cpu_load: f64,
740 available_memory_bytes: usize,
742 cache_miss_rate: f64,
744 io_wait_percent: f64,
746 cpu_temperature_celsius: f64,
748 prev_cpu_jiffies: Option<(u64, u64, u64)>,
752}
753
754pub struct ConfigurationPredictor {
756 feature_weights: HashMap<String, f64>,
763 learning_rate: f64,
765 confidence_threshold: f64,
767 sample_count: usize,
769 last_features: HashMap<String, f64>,
775}
776
777pub struct AdaptiveParameterTuner {
779 q_table: HashMap<(String, String), f64>, exploration_rate: f64,
783 learning_rate: f64,
785 #[allow(dead_code)]
787 discount_factor: f64,
788 current_state: String,
790 last_action: String,
796}
797
798const TUNER_ACTIONS: &[&str] = &[
804 "increase_memory",
805 "decrease_memory",
806 "toggle_parallel",
807 "increase_chunk",
808 "decrease_chunk",
809 "no_change",
810];
811
812fn 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 }
838 }
839 config
840}
841
842impl Default for AdvancedConfigOptimizer {
843 fn default() -> Self {
844 Self::new()
845 }
846}
847
848impl AdvancedConfigOptimizer {
849 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 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 self.system_monitor.update_metrics()?;
868
869 let current_state = self.generate_state_representation(datachars, &self.system_monitor);
871
872 let predicted_config = self.config_predictor.predict_optimal_config(
874 ¤t_state,
875 transformation_type,
876 user_params,
877 )?;
878
879 let tuned_config = self.adaptive_tuner.tune_parameters(
881 predicted_config,
882 ¤t_state,
883 transformation_type,
884 )?;
885
886 let validated_config =
888 self.validate_and_adjust_config(tuned_config, &self.system_monitor)?;
889
890 Ok(validated_config)
891 }
892
893 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 self.performance_history
904 .entry(transformation_type.to_string())
905 .or_default()
906 .push(performance.clone());
907
908 self.config_predictor.update_from_feedback(&performance)?;
910
911 let reward = self.compute_reward_signal(&performance);
913 self.adaptive_tuner.update_q_values(config_hash, reward)?;
914
915 if self.config_predictor.sample_count.is_multiple_of(100) {
917 self.retrain_models()?;
918 }
919
920 Ok(())
921 }
922
923 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 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 fn compute_reward_signal(&self, performance: &PerformanceMetric) -> f64 {
957 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 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 fn validate_and_adjust_config(
974 &self,
975 mut config: OptimizationConfig,
976 system_monitor: &SystemMonitor,
977 ) -> Result<OptimizationConfig> {
978 let available_mb = system_monitor.available_memory_bytes / (1024 * 1024);
980 config.memory_limit_mb = config.memory_limit_mb.min(available_mb * 80 / 100); if system_monitor.cpu_load > 0.8 {
984 config.num_threads = (config.num_threads / 2).max(1);
985 }
986
987 if system_monitor.cpu_temperature_celsius > 85.0 {
989 config.use_gpu = false;
990 }
991
992 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 fn retrain_models(&mut self) -> Result<()> {
1002 self.config_predictor
1004 .retrain_with_history(&self.performance_history)?;
1005
1006 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 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 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 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 fn read_available_memory(&self) -> Result<usize> {
1058 let mut system = sysinfo::System::new_all();
1059 system.refresh_memory();
1060 let available = system.available_memory();
1065 if available == 0 {
1066 return Ok(1024 * 1024 * 1024); }
1068 Ok(available as usize)
1069 }
1070
1071 fn read_cache_miss_rate(&self) -> Result<f64> {
1086 Ok(0.05)
1087 }
1088
1089 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 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 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 return Ok(millidegrees / 1000.0);
1136 }
1137 }
1138 }
1139 Ok(50.0) }
1141 #[cfg(not(target_os = "linux"))]
1142 {
1143 Ok(50.0)
1144 }
1145 }
1146}
1147
1148#[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
1176const 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 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 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 let features = self.extract_features(state)?;
1217
1218 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 self.last_features = features.clone();
1227
1228 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 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 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 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 pub fn update_from_feedback(&mut self, performance: &PerformanceMetric) -> Result<()> {
1332 self.sample_count += 1;
1333
1334 if self.last_features.is_empty() {
1335 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 pub fn retrain_with_history(
1358 &mut self,
1359 history: &HashMap<String, Vec<PerformanceMetric>>,
1360 ) -> Result<()> {
1361 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 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 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 if scirs2_core::random::rng().random_range(0.0..1.0) < self.exploration_rate {
1398 config = self.explore_parameters(config)?;
1400 } else {
1401 config = self.exploit_best_parameters(config, state)?;
1403 }
1404
1405 Ok(config)
1406 }
1407
1408 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 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 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 pub fn update_q_values(&mut self, confighash: u64, reward: f64) -> Result<()> {
1456 let _ = confighash;
1461 let state_action = (self.current_state.clone(), self.last_action.clone());
1462
1463 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 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 let result = OptimizationConfig::estimate_polynomial_features(5, 2);
1527 assert!(result.is_ok());
1528
1529 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 #[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 assert_ne!(
1593 monitor.available_memory_bytes,
1594 8 * 1024 * 1024 * 1024,
1595 "must not be the old fabricated 8GB placeholder"
1596 );
1597 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 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 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 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 #[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 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 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}