1use crate::base::{EdgeWeight, Graph, Node};
35use crate::error::{GraphError, Result};
36use scirs2_core::random::{Rng, RngExt};
37use std::collections::HashMap;
38use std::time::{Duration, Instant};
39
40#[derive(Debug, Clone, Default)]
46pub struct SimplePerformanceMonitor {
47 active: HashMap<String, Instant>,
49 completed: HashMap<String, (usize, Duration)>,
51}
52
53impl SimplePerformanceMonitor {
54 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn start_operation(&mut self, name: &str) {
63 self.active.insert(name.to_string(), Instant::now());
64 }
65
66 pub fn stop_operation(&mut self, name: &str) {
70 if let Some(start) = self.active.remove(name) {
71 let elapsed = start.elapsed();
72 let entry = self
73 .completed
74 .entry(name.to_string())
75 .or_insert((0, Duration::ZERO));
76 entry.0 += 1;
77 entry.1 += elapsed;
78 }
79 }
80
81 pub fn get_report(&self) -> SimplePerformanceReport {
83 let total_operations: usize = self.completed.values().map(|(count, _)| *count).sum();
84 let total_time: Duration = self.completed.values().map(|(_, dur)| *dur).sum();
85 SimplePerformanceReport {
86 total_operations,
87 total_time_ms: total_time.as_secs_f64() * 1000.0,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Default)]
94pub struct SimplePerformanceReport {
95 pub total_operations: usize,
97 pub total_time_ms: f64,
99}
100
101#[derive(Debug, Clone)]
103pub struct AdvancedConfig {
104 pub enable_neural_rl: bool,
106 pub enable_gpu_acceleration: bool,
109 pub enable_neuromorphic: bool,
112 pub enable_realtime_adaptation: bool,
114 pub enable_memory_optimization: bool,
117 pub learning_rate: f64,
121 pub memory_threshold_mb: usize,
123 pub gpu_memory_pool_mb: usize,
125 pub neural_hidden_size: usize,
127}
128
129impl Default for AdvancedConfig {
130 fn default() -> Self {
131 AdvancedConfig {
132 enable_neural_rl: true,
133 enable_gpu_acceleration: true,
134 enable_neuromorphic: true,
135 enable_realtime_adaptation: true,
136 enable_memory_optimization: true,
137 learning_rate: 0.001,
138 memory_threshold_mb: 1024,
139 gpu_memory_pool_mb: 2048,
140 neural_hidden_size: 128,
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
148pub enum ExplorationStrategy {
149 EpsilonGreedy {
151 epsilon: f64,
153 },
154 UCB {
156 c: f64,
158 },
159 ThompsonSampling {
161 alpha: f64,
163 beta: f64,
165 },
166 AdaptiveUncertainty {
168 uncertainty_threshold: f64,
170 },
171}
172
173impl Default for ExplorationStrategy {
174 fn default() -> Self {
175 ExplorationStrategy::EpsilonGreedy { epsilon: 0.1 }
176 }
177}
178
179pub struct AdvancedProcessor {
182 config: AdvancedConfig,
183 performance_monitor: SimplePerformanceMonitor,
184 stats: AdvancedStats,
185 rl_agent: NeuralRLAgent,
186 gpu_context: GPUAccelerationContext,
187}
188
189pub type CandidateOp<N, E, Ix, T> = fn(&Graph<N, E, Ix>) -> Result<T>;
193
194impl AdvancedProcessor {
195 pub fn new(config: AdvancedConfig) -> Self {
197 let gpu_context = if config.enable_gpu_acceleration {
198 GPUAccelerationContext::detect()
199 } else {
200 GPUAccelerationContext::default()
201 };
202 let rl_agent = NeuralRLAgent::new(config.clone(), ExplorationStrategy::default());
203
204 AdvancedProcessor {
205 config,
206 performance_monitor: SimplePerformanceMonitor::new(),
207 stats: AdvancedStats::default(),
208 rl_agent,
209 gpu_context,
210 }
211 }
212
213 pub fn execute<N, E, Ix, T, F>(&mut self, graph: &Graph<N, E, Ix>, operation: F) -> Result<T>
220 where
221 N: Node + std::fmt::Debug,
222 E: EdgeWeight,
223 Ix: petgraph::graph::IndexType,
224 F: FnOnce(&Graph<N, E, Ix>) -> Result<T>,
225 {
226 self.performance_monitor
227 .start_operation("advanced_execution");
228
229 let result = operation(graph);
230
231 self.performance_monitor
232 .stop_operation("advanced_execution");
233
234 let graph_bytes = structural_graph_memory_estimate(graph);
235 self.update_stats(graph_bytes);
236
237 result
238 }
239
240 pub fn execute_profiled<N, E, Ix, T, F>(
247 &mut self,
248 graph: &Graph<N, E, Ix>,
249 operation: F,
250 ) -> Result<T>
251 where
252 N: Node + std::fmt::Debug,
253 E: EdgeWeight,
254 Ix: petgraph::graph::IndexType,
255 F: FnOnce(&Graph<N, E, Ix>) -> Result<T>,
256 {
257 self.performance_monitor
258 .start_operation("advanced_execution");
259
260 let sample_interval = Duration::from_micros(200);
261 let (result, memory_metrics) =
262 crate::memory::AdvancedMemoryAnalyzer::analyze_operation_memory(
263 "advanced_execution",
264 || operation(graph),
265 sample_interval,
266 );
267
268 self.performance_monitor
269 .stop_operation("advanced_execution");
270
271 let memory_bytes = if memory_metrics.peak_memory > 0 {
276 memory_metrics.peak_memory as usize
277 } else {
278 structural_graph_memory_estimate(graph)
279 };
280 self.update_stats(memory_bytes);
281
282 result
283 }
284
285 pub fn execute_adaptive<N, E, Ix, T>(
296 &mut self,
297 graph: &Graph<N, E, Ix>,
298 candidates: &[CandidateOp<N, E, Ix, T>],
299 ) -> Result<T>
300 where
301 N: Node + std::fmt::Debug,
302 E: EdgeWeight,
303 Ix: petgraph::graph::IndexType,
304 {
305 if candidates.is_empty() {
306 return Err(GraphError::InvalidGraph(
307 "execute_adaptive: at least one candidate operation is required".to_string(),
308 ));
309 }
310
311 self.performance_monitor
312 .start_operation("advanced_execution");
313
314 let arm = self.rl_agent.select_arm(candidates.len());
315 let start = Instant::now();
316 let result = (candidates[arm])(graph);
317 let elapsed_secs = start.elapsed().as_secs_f64();
318
319 self.performance_monitor
320 .stop_operation("advanced_execution");
321
322 let reward = 1.0 / (1.0 + elapsed_secs);
326 self.rl_agent.record_reward(arm, reward);
327
328 let graph_bytes = structural_graph_memory_estimate(graph);
329 self.update_stats(graph_bytes);
330
331 result
332 }
333
334 fn update_stats(&mut self, latest_memory_estimate_bytes: usize) {
336 let report = self.performance_monitor.get_report();
337 self.stats.total_operations = report.total_operations;
338 self.stats.avg_execution_time_ms = if report.total_operations > 0 {
339 report.total_time_ms / report.total_operations as f64
340 } else {
341 0.0
342 };
343 self.stats.memory_usage_bytes = latest_memory_estimate_bytes;
344
345 const ASSUMED_FIXED_OVERHEAD_BYTES: f64 = 1024.0;
350 self.stats.memory_efficiency = if latest_memory_estimate_bytes == 0 {
351 1.0
352 } else {
353 let bytes = latest_memory_estimate_bytes as f64;
354 bytes / (bytes + ASSUMED_FIXED_OVERHEAD_BYTES)
355 };
356
357 self.stats.gpu_utilization_percent = 0.0;
361 }
362
363 pub fn get_performance_report(&self) -> SimplePerformanceReport {
365 self.performance_monitor.get_report()
366 }
367
368 pub fn get_optimization_stats(&self) -> AdvancedStats {
372 self.stats.clone()
373 }
374
375 pub fn gpu_context(&self) -> &GPUAccelerationContext {
380 &self.gpu_context
381 }
382
383 pub fn rl_agent(&self) -> &NeuralRLAgent {
385 &self.rl_agent
386 }
387
388 pub fn rl_agent_mut(&mut self) -> &mut NeuralRLAgent {
391 &mut self.rl_agent
392 }
393}
394
395fn structural_graph_memory_estimate<N, E, Ix>(graph: &Graph<N, E, Ix>) -> usize
403where
404 N: Node + std::fmt::Debug,
405 E: EdgeWeight,
406 Ix: petgraph::graph::IndexType,
407{
408 const BASE_OVERHEAD_BYTES: usize = 1024;
409 let node_size = std::mem::size_of::<N>() + std::mem::size_of::<Ix>();
410 let edge_size = std::mem::size_of::<E>() + 2 * std::mem::size_of::<Ix>();
411 BASE_OVERHEAD_BYTES + graph.node_count() * node_size + graph.edge_count() * edge_size
412}
413
414#[derive(Debug, Clone)]
417pub struct AdvancedStats {
418 pub total_operations: usize,
420 pub avg_execution_time_ms: f64,
422 pub memory_usage_bytes: usize,
426 pub gpu_utilization_percent: f64,
432 pub memory_efficiency: f64,
435}
436
437impl Default for AdvancedStats {
438 fn default() -> Self {
439 AdvancedStats {
440 total_operations: 0,
441 avg_execution_time_ms: 0.0,
442 memory_usage_bytes: 0,
443 gpu_utilization_percent: 0.0,
444 memory_efficiency: 1.0,
445 }
446 }
447}
448
449pub fn create_advanced_processor() -> AdvancedProcessor {
452 AdvancedProcessor::new(AdvancedConfig::default())
453}
454
455pub fn create_enhanced_advanced_processor() -> AdvancedProcessor {
457 let mut config = AdvancedConfig::default();
458 config.neural_hidden_size = 256;
459 config.gpu_memory_pool_mb = 4096;
460 AdvancedProcessor::new(config)
461}
462
463pub fn execute_with_advanced<N, E, Ix, T>(
465 graph: &Graph<N, E, Ix>,
466 operation: impl FnOnce(&Graph<N, E, Ix>) -> Result<T>,
467) -> Result<T>
468where
469 N: Node + std::fmt::Debug,
470 E: EdgeWeight,
471 Ix: petgraph::graph::IndexType,
472{
473 let mut processor = create_advanced_processor();
474 processor.execute(graph, operation)
475}
476
477pub fn execute_with_enhanced_advanced<N, E, Ix, T>(
479 graph: &Graph<N, E, Ix>,
480 operation: impl FnOnce(&Graph<N, E, Ix>) -> Result<T>,
481) -> Result<T>
482where
483 N: Node + std::fmt::Debug,
484 E: EdgeWeight,
485 Ix: petgraph::graph::IndexType,
486{
487 let mut processor = create_enhanced_advanced_processor();
488 processor.execute(graph, operation)
489}
490
491pub fn create_large_graph_advanced_processor() -> AdvancedProcessor {
493 let mut config = AdvancedConfig::default();
494 config.memory_threshold_mb = 8192;
495 config.gpu_memory_pool_mb = 8192;
496 config.enable_memory_optimization = true;
497 AdvancedProcessor::new(config)
498}
499
500pub fn create_realtime_advanced_processor() -> AdvancedProcessor {
502 let mut config = AdvancedConfig::default();
503 config.enable_realtime_adaptation = true;
504 config.learning_rate = 0.01;
505 AdvancedProcessor::new(config)
506}
507
508pub fn create_performance_advanced_processor() -> AdvancedProcessor {
510 let mut config = AdvancedConfig::default();
511 config.enable_gpu_acceleration = true;
512 config.enable_neuromorphic = true;
513 config.gpu_memory_pool_mb = 16384;
514 AdvancedProcessor::new(config)
515}
516
517pub fn create_memory_efficient_advanced_processor() -> AdvancedProcessor {
519 let mut config = AdvancedConfig::default();
520 config.enable_memory_optimization = true;
521 config.memory_threshold_mb = 512;
522 config.gpu_memory_pool_mb = 1024;
523 AdvancedProcessor::new(config)
524}
525
526pub fn create_adaptive_advanced_processor() -> AdvancedProcessor {
528 let mut config = AdvancedConfig::default();
529 config.enable_realtime_adaptation = true;
530 config.enable_neural_rl = true;
531 config.learning_rate = 0.005;
532 AdvancedProcessor::new(config)
533}
534
535#[derive(Debug, Clone)]
538pub struct AlgorithmMetrics {
539 pub algorithm_name: String,
541 pub execution_time_ms: f64,
543 pub memory_usage_bytes: usize,
545}
546
547impl Default for AlgorithmMetrics {
548 fn default() -> Self {
549 AlgorithmMetrics {
550 algorithm_name: String::new(),
551 execution_time_ms: 0.0,
552 memory_usage_bytes: 0,
553 }
554 }
555}
556
557#[derive(Debug, Default, Clone, Copy, PartialEq)]
563pub struct GPUAccelerationContext {
564 pub gpu_available: bool,
566 pub memory_pool_size: usize,
569}
570
571impl GPUAccelerationContext {
572 pub fn detect() -> Self {
580 #[cfg(feature = "cuda")]
581 {
582 if crate::gpu_cuda::cuda_is_available() {
583 return GPUAccelerationContext {
584 gpu_available: true,
585 memory_pool_size: 0,
586 };
587 }
588 }
589
590 GPUAccelerationContext {
591 gpu_available: false,
592 memory_pool_size: 0,
593 }
594 }
595}
596
597#[derive(Debug, Clone)]
611pub struct NeuralRLAgent {
612 pub config: AdvancedConfig,
614 pub learning_rate: f64,
619 pub strategy: ExplorationStrategy,
621 arm_stats: Vec<(u64, f64)>,
623}
624
625impl Default for NeuralRLAgent {
626 fn default() -> Self {
627 NeuralRLAgent {
628 config: AdvancedConfig::default(),
629 learning_rate: 0.001,
630 strategy: ExplorationStrategy::default(),
631 arm_stats: Vec::new(),
632 }
633 }
634}
635
636impl NeuralRLAgent {
637 pub fn new(config: AdvancedConfig, strategy: ExplorationStrategy) -> Self {
640 let learning_rate = config.learning_rate;
641 NeuralRLAgent {
642 config,
643 learning_rate,
644 strategy,
645 arm_stats: Vec::new(),
646 }
647 }
648
649 pub fn arm_count(&self) -> usize {
651 self.arm_stats.len()
652 }
653
654 pub fn arm_stats(&self, arm: usize) -> Option<(u64, f64)> {
657 self.arm_stats.get(arm).copied()
658 }
659
660 pub fn select_arm(&mut self, n_arms: usize) -> usize {
665 if n_arms == 0 {
666 return 0;
667 }
668 if self.arm_stats.len() < n_arms {
669 self.arm_stats.resize(n_arms, (0, 0.0));
670 }
671
672 if let Some(idx) = self.arm_stats[..n_arms]
673 .iter()
674 .position(|&(count, _)| count == 0)
675 {
676 return idx;
677 }
678
679 match &self.strategy {
680 ExplorationStrategy::EpsilonGreedy { epsilon } => {
681 let mut rng = scirs2_core::random::rng();
682 if rng.random::<f64>() < *epsilon {
683 rng.random_range(0..n_arms)
684 } else {
685 self.best_arm(n_arms)
686 }
687 }
688 ExplorationStrategy::UCB { c } => {
689 let total_pulls: u64 = self.arm_stats[..n_arms].iter().map(|&(cnt, _)| cnt).sum();
690 let ln_total = (total_pulls.max(1) as f64).ln();
691 (0..n_arms)
692 .max_by(|&a, &b| {
693 let score = |i: usize| {
694 let (count, mean) = self.arm_stats[i];
695 mean + c * (ln_total / (count.max(1) as f64)).sqrt()
696 };
697 score(a)
698 .partial_cmp(&score(b))
699 .unwrap_or(std::cmp::Ordering::Equal)
700 })
701 .unwrap_or(0)
702 }
703 ExplorationStrategy::ThompsonSampling { alpha, beta } => {
704 let mut rng = scirs2_core::random::rng();
711 let mut best = 0usize;
712 let mut best_score = f64::NEG_INFINITY;
713 for i in 0..n_arms {
714 let (count, mean) = self.arm_stats[i];
715 let mean = mean.clamp(0.0, 1.0);
716 let successes = alpha + mean * count as f64;
717 let failures = beta + (1.0 - mean) * count as f64;
718 let posterior_mean = successes / (successes + failures).max(1e-9);
719 let uncertainty = 1.0 / ((count as f64) + 1.0).sqrt();
720 let noise =
721 box_muller_standard_normal(rng.random::<f64>(), rng.random::<f64>())
722 * uncertainty
723 * 0.25;
724 let score = posterior_mean + noise;
725 if score > best_score {
726 best_score = score;
727 best = i;
728 }
729 }
730 best
731 }
732 ExplorationStrategy::AdaptiveUncertainty {
733 uncertainty_threshold,
734 } => {
735 let least_tried = (0..n_arms)
736 .min_by_key(|&i| self.arm_stats[i].0)
737 .unwrap_or(0);
738 let uncertainty = 1.0 / ((self.arm_stats[least_tried].0 as f64) + 1.0).sqrt();
739 if uncertainty > *uncertainty_threshold {
740 least_tried
741 } else {
742 self.best_arm(n_arms)
743 }
744 }
745 }
746 }
747
748 pub fn record_reward(&mut self, arm: usize, reward: f64) {
751 if arm >= self.arm_stats.len() {
752 self.arm_stats.resize(arm + 1, (0, 0.0));
753 }
754 let (count, mean) = &mut self.arm_stats[arm];
755 *count += 1;
756 if self.learning_rate > 0.0 {
757 *mean += self.learning_rate * (reward - *mean);
759 } else {
760 *mean += (reward - *mean) / (*count as f64);
762 }
763 }
764
765 fn best_arm(&self, n_arms: usize) -> usize {
768 (0..n_arms)
769 .max_by(|&a, &b| {
770 self.arm_stats[a]
771 .1
772 .partial_cmp(&self.arm_stats[b].1)
773 .unwrap_or(std::cmp::Ordering::Equal)
774 })
775 .unwrap_or(0)
776 }
777}
778
779fn box_muller_standard_normal(u1: f64, u2: f64) -> f64 {
782 let u1 = u1.max(1e-12); (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
784}
785
786#[derive(Debug, Clone, Copy, PartialEq)]
797pub struct NeuromorphicProcessor {
798 pub num_neurons: usize,
800 pub num_synapses: usize,
802}
803
804impl Default for NeuromorphicProcessor {
805 fn default() -> Self {
806 NeuromorphicProcessor {
807 num_neurons: 1000,
808 num_synapses: 10000,
809 }
810 }
811}
812
813impl NeuromorphicProcessor {
814 pub fn accelerate<T>(&self, operation_name: &str) -> Result<T> {
821 Err(GraphError::Unsupported(format!(
822 "NeuromorphicProcessor::accelerate({operation_name}): neuromorphic acceleration is \
823 not implemented (num_neurons={}, num_synapses={} are configuration only; no \
824 neuromorphic simulator or hardware backend exists in this crate)",
825 self.num_neurons, self.num_synapses
826 )))
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833 use crate::base::Graph;
834
835 fn small_graph() -> Graph<i32, f64> {
836 let mut graph: Graph<i32, f64> = Graph::new();
837 graph.add_edge(0, 1, 1.0).expect("add_edge failed");
838 graph.add_edge(1, 2, 1.0).expect("add_edge failed");
839 graph
840 }
841
842 fn bigger_graph() -> Graph<i32, f64> {
843 let mut graph: Graph<i32, f64> = Graph::new();
844 for i in 0..50i32 {
845 graph.add_edge(i, i + 1, 1.0).expect("add_edge failed");
846 }
847 graph
848 }
849
850 #[test]
851 fn test_performance_monitor_tracks_real_timing() {
852 let mut monitor = SimplePerformanceMonitor::new();
853
854 monitor.start_operation("op_a");
855 std::thread::sleep(Duration::from_millis(5));
856 monitor.stop_operation("op_a");
857
858 monitor.start_operation("op_a");
859 std::thread::sleep(Duration::from_millis(5));
860 monitor.stop_operation("op_a");
861
862 let report = monitor.get_report();
863 assert_eq!(report.total_operations, 2);
864 assert!(
867 report.total_time_ms >= 8.0,
868 "expected at least ~10ms of real elapsed time, got {}",
869 report.total_time_ms
870 );
871 }
872
873 #[test]
874 fn test_performance_monitor_stop_without_start_is_noop() {
875 let mut monitor = SimplePerformanceMonitor::new();
876 monitor.stop_operation("never_started");
877 let report = monitor.get_report();
878 assert_eq!(report.total_operations, 0);
879 assert_eq!(report.total_time_ms, 0.0);
880 }
881
882 #[test]
883 fn test_advanced_processor_execute_produces_real_stats() {
884 let mut processor = create_advanced_processor();
885
886 let before = processor.get_optimization_stats();
888 assert_eq!(before.total_operations, 0);
889
890 let graph = small_graph();
891 let result: Result<usize> = processor.execute(&graph, |g| Ok(g.node_count()));
892 assert_eq!(result.expect("execute failed"), 3);
893
894 let after = processor.get_optimization_stats();
895 assert_eq!(
896 after.total_operations, 1,
897 "total_operations must reflect the real call count, not stay at the old default 0"
898 );
899 assert!(
900 after.memory_usage_bytes > 0,
901 "memory_usage_bytes must be a real (nonzero) structural estimate"
902 );
903 assert_eq!(after.gpu_utilization_percent, 0.0);
905 }
906
907 #[test]
908 fn test_advanced_processor_memory_estimate_scales_with_graph_size() {
909 let mut small_processor = create_advanced_processor();
915 let mut big_processor = create_advanced_processor();
916
917 let small = small_graph();
918 let big = bigger_graph();
919
920 small_processor
921 .execute(&small, |g| Ok(g.node_count()))
922 .expect("execute failed");
923 big_processor
924 .execute(&big, |g| Ok(g.node_count()))
925 .expect("execute failed");
926
927 let small_stats = small_processor.get_optimization_stats();
928 let big_stats = big_processor.get_optimization_stats();
929
930 assert!(
931 big_stats.memory_usage_bytes > small_stats.memory_usage_bytes,
932 "a 51-node graph should report a larger memory estimate than a 3-node graph \
933 ({} vs {})",
934 big_stats.memory_usage_bytes,
935 small_stats.memory_usage_bytes
936 );
937 }
938
939 #[test]
940 fn test_advanced_processor_execute_profiled_runs_real_operation() {
941 let mut processor = create_large_graph_advanced_processor();
942 let graph = bigger_graph();
943
944 let result: Result<usize> = processor.execute_profiled(&graph, |g| Ok(g.edge_count()));
945 assert_eq!(result.expect("execute_profiled failed"), 50);
946
947 let stats = processor.get_optimization_stats();
948 assert_eq!(stats.total_operations, 1);
949 assert!(stats.memory_usage_bytes > 0);
950 }
951
952 #[test]
953 fn test_gpu_acceleration_context_detect_is_honest() {
954 let ctx = GPUAccelerationContext::detect();
955 assert_eq!(ctx.memory_pool_size, 0);
958
959 #[cfg(feature = "cuda")]
960 {
961 assert_eq!(ctx.gpu_available, crate::gpu_cuda::cuda_is_available());
965 }
966 #[cfg(not(feature = "cuda"))]
967 {
968 assert!(!ctx.gpu_available);
972 }
973 }
974
975 #[test]
976 fn test_neuromorphic_processor_is_honestly_unsupported() {
977 let processor = NeuromorphicProcessor::default();
978 let result: Result<()> = processor.accelerate("pagerank");
979 match result {
980 Err(GraphError::Unsupported(msg)) => {
981 assert!(msg.contains("neuromorphic"));
982 }
983 other => panic!("expected GraphError::Unsupported, got {other:?}"),
984 }
985 }
986
987 #[test]
988 fn test_neural_rl_agent_select_arm_tries_every_arm_before_repeating() {
989 let mut agent = NeuralRLAgent::new(
990 AdvancedConfig::default(),
991 ExplorationStrategy::EpsilonGreedy { epsilon: 0.0 },
992 );
993
994 let mut seen = std::collections::HashSet::new();
995 for _ in 0..4 {
996 let arm = agent.select_arm(4);
997 seen.insert(arm);
998 agent.record_reward(arm, 0.5);
999 }
1000 assert_eq!(
1001 seen.len(),
1002 4,
1003 "every arm must be tried at least once during warm-up"
1004 );
1005 }
1006
1007 #[test]
1008 fn test_neural_rl_agent_epsilon_greedy_converges_to_best_arm() {
1009 let mut agent = NeuralRLAgent::new(
1015 AdvancedConfig::default(),
1016 ExplorationStrategy::EpsilonGreedy { epsilon: 0.0 },
1017 );
1018
1019 const N_ARMS: usize = 5;
1020 for arm in 0..N_ARMS {
1022 agent.record_reward(arm, if arm == 0 { 1.0 } else { 0.0 });
1023 }
1024 for _ in 0..20 {
1028 let arm = agent.select_arm(N_ARMS);
1029 agent.record_reward(arm, if arm == 0 { 1.0 } else { 0.0 });
1030 }
1031
1032 let chosen = agent.select_arm(N_ARMS);
1033 assert_eq!(
1034 chosen, 0,
1035 "epsilon=0 agent must exploit the arm with the only nonzero reward"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_neural_rl_agent_ucb_prefers_less_explored_arms_when_tied() {
1041 let mut agent = NeuralRLAgent::new(
1046 AdvancedConfig {
1047 learning_rate: 0.0,
1048 ..AdvancedConfig::default()
1049 },
1050 ExplorationStrategy::UCB { c: 2.0 },
1051 );
1052
1053 agent.record_reward(0, 0.5);
1062 agent.record_reward(1, 0.5);
1063 for _ in 0..30 {
1064 agent.record_reward(0, 0.5);
1065 }
1066
1067 let (count0, mean0) = agent.arm_stats(0).expect("arm 0 stats");
1068 let (count1, mean1) = agent.arm_stats(1).expect("arm 1 stats");
1069 assert_eq!(count0, 31);
1070 assert_eq!(count1, 1);
1071 assert!((mean0 - mean1).abs() < 1e-9, "means must be tied by design");
1072
1073 let chosen = agent.select_arm(2);
1074 assert_eq!(
1075 chosen, 1,
1076 "with tied mean rewards, UCB must prefer the far-less-explored arm"
1077 );
1078 }
1079
1080 #[test]
1081 fn test_neural_rl_agent_record_reward_updates_mean() {
1082 let mut agent = NeuralRLAgent::new(
1083 AdvancedConfig {
1084 learning_rate: 0.0, ..AdvancedConfig::default()
1086 },
1087 ExplorationStrategy::default(),
1088 );
1089
1090 agent.record_reward(0, 0.0);
1091 agent.record_reward(0, 1.0);
1092 let (count, mean) = agent.arm_stats(0).expect("arm 0 should have stats");
1093 assert_eq!(count, 2);
1094 assert!(
1095 (mean - 0.5).abs() < 1e-9,
1096 "running mean of [0.0, 1.0] should be 0.5, got {mean}"
1097 );
1098 }
1099
1100 #[test]
1101 fn test_advanced_processor_execute_adaptive_learns_the_faster_candidate() {
1102 let mut processor = create_adaptive_advanced_processor();
1103 let graph = small_graph();
1104
1105 fn fast_op(g: &Graph<i32, f64>) -> Result<usize> {
1106 Ok(g.node_count())
1107 }
1108 fn slow_op(g: &Graph<i32, f64>) -> Result<usize> {
1109 std::thread::sleep(Duration::from_millis(2));
1110 Ok(g.node_count())
1111 }
1112
1113 let candidates: [CandidateOp<i32, f64, u32, usize>; 2] = [slow_op, fast_op];
1114
1115 processor.rl_agent_mut().strategy = ExplorationStrategy::EpsilonGreedy { epsilon: 0.0 };
1118
1119 for _ in 0..8 {
1120 processor
1121 .execute_adaptive(&graph, &candidates)
1122 .expect("execute_adaptive failed");
1123 }
1124
1125 let (_, slow_mean) = processor
1128 .rl_agent()
1129 .arm_stats(0)
1130 .expect("slow arm should have stats");
1131 let (_, fast_mean) = processor
1132 .rl_agent()
1133 .arm_stats(1)
1134 .expect("fast arm should have stats");
1135 assert!(
1136 fast_mean > slow_mean,
1137 "the genuinely faster candidate should have accumulated a higher reward \
1138 (fast={fast_mean}, slow={slow_mean})"
1139 );
1140 }
1141
1142 #[test]
1143 fn test_execute_adaptive_rejects_empty_candidates() {
1144 let mut processor = create_advanced_processor();
1145 let graph = small_graph();
1146 let candidates: [CandidateOp<i32, f64, u32, usize>; 0] = [];
1147 assert!(processor.execute_adaptive(&graph, &candidates).is_err());
1148 }
1149}