//! Unit tests for Performance-Based Adaptive Temperature Decay //! //! Tests the adaptive temperature strategy where: //! - Temperature decays faster when validation loss improves (>0.1% change) //! - Temperature decays slower when validation loss plateaus (<0.1% change) //! - Temperature increases when stuck in local optimum (10+ epochs without improvement) use ml::dqn::{WorkingDQN, WorkingDQNConfig}; #[test] fn test_temperature_adaptive_improving_loss() -> anyhow::Result<()> { // Create DQN with adaptive temperature enabled let mut config = WorkingDQNConfig::emergency_safe_defaults(); config.use_adaptive_temperature = true; config.loss_improvement_threshold = 0.999; // 0.1% improvement config.plateau_window = 10; config.temp_increase_factor = 1.05; config.temperature_start = 1.0; config.temperature_min = 0.1; config.temperature_decay = 0.99; // Fast decay config.temperature_slow_decay = 0.998; // Slow decay let mut dqn = WorkingDQN::new(config)?; // Simulate improving loss over 6 epochs (first is baseline) let losses = vec![1.0, 0.95, 0.90, 0.85, 0.80, 0.75]; let initial_temp = dqn.get_temperature(); for loss in losses { dqn.update_temperature_adaptive(loss); } // Temperature should decay faster (loss improved) // First update establishes baseline, next 5 trigger fast decay // Expected: temp * 0.99^5 ≈ 0.9509 (close to 0.95) let final_temp = dqn.get_temperature(); assert!( final_temp < initial_temp * 0.952, // Slightly relaxed for floating point "Temperature should decay significantly when loss improves: {} -> {}", initial_temp, final_temp ); // Plateau counter should be reset assert_eq!( dqn.get_plateau_count(), 0, "Plateau count should reset on improvement" ); Ok(()) } #[test] fn test_temperature_adaptive_plateaued_loss() -> anyhow::Result<()> { // Create DQN with adaptive temperature enabled let mut config = WorkingDQNConfig::emergency_safe_defaults(); config.use_adaptive_temperature = true; config.loss_improvement_threshold = 0.999; // 0.1% improvement config.plateau_window = 10; config.temp_increase_factor = 1.05; config.temperature_start = 1.0; config.temperature_min = 0.1; config.temperature_decay = 0.99; // Fast decay config.temperature_slow_decay = 0.998; // Slow decay let mut dqn = WorkingDQN::new(config)?; // Simulate plateaued loss (no improvement) // First call establishes baseline, next 5 trigger plateau detection let initial_temp = dqn.get_temperature(); for _ in 0..6 { dqn.update_temperature_adaptive(1.0); // Same loss value } // Temperature should decay slowly (loss plateaued) let final_temp = dqn.get_temperature(); assert!( final_temp < initial_temp, "Temperature should decay slowly when loss plateaus: {} -> {}", initial_temp, final_temp ); assert!( final_temp > initial_temp * 0.98, "Slow decay should be minimal: {} -> {}", initial_temp, final_temp ); // Plateau counter should increase (5 epochs after baseline) assert_eq!( dqn.get_plateau_count(), 5, "Plateau count should track epochs without improvement" ); Ok(()) } #[test] fn test_temperature_adaptive_stuck_recovery() -> anyhow::Result<()> { // Create DQN with adaptive temperature enabled let mut config = WorkingDQNConfig::emergency_safe_defaults(); config.use_adaptive_temperature = true; config.loss_improvement_threshold = 0.999; // 0.1% improvement config.plateau_window = 10; config.temp_increase_factor = 1.05; config.temperature_start = 1.0; config.temperature_min = 0.1; config.temperature_decay = 0.99; // Fast decay config.temperature_slow_decay = 0.998; // Slow decay let mut dqn = WorkingDQN::new(config)?; // Simulate stuck in local optimum (13 epochs: 1 baseline + 12 plateau) // After 11 plateau epochs (window=10), temperature should increase and reset counter let initial_temp = dqn.get_temperature(); for _ in 0..13 { dqn.update_temperature_adaptive(1.0); // Same loss value } // Temperature should increase (escape local optimum) // First update: baseline (temp=1.0) // Next 10 updates: slow decay (temp = 1.0 * 0.998^10 ≈ 0.980) // Update 12: triggers increase (temp = 0.980 * 1.05 ≈ 1.029, plateau resets) // Update 13: slow decay again (temp = 1.029 * 0.998 ≈ 1.027) let final_temp = dqn.get_temperature(); assert!( final_temp > initial_temp * 1.02, // Should be ~1.027 (relaxed for floating point) "Temperature should increase when stuck: {} -> {} (expected >{})", initial_temp, final_temp, initial_temp * 1.02 ); // Plateau counter should reset after temperature increase // 13 total - 1 baseline - 11 that triggered increase = 1 remaining assert_eq!( dqn.get_plateau_count(), 1, "Plateau count should reset after temperature increase" ); Ok(()) } #[test] fn test_temperature_adaptive_bounds() -> anyhow::Result<()> { // Create DQN with adaptive temperature enabled let mut config = WorkingDQNConfig::emergency_safe_defaults(); config.use_adaptive_temperature = true; config.temperature_start = 1.0; config.temperature_min = 0.1; config.temperature_decay = 0.99; let mut dqn = WorkingDQN::new(config)?; // Decay temperature many times for _ in 0..1000 { dqn.update_temperature_adaptive(0.5); // Improving loss } // Temperature should not go below minimum assert!( dqn.get_temperature() >= 0.1, "Temperature should respect minimum bound: {}", dqn.get_temperature() ); // Try to increase temperature many times for _ in 0..100 { // Simulate stuck condition (plateau_window reached) for _ in 0..11 { dqn.update_temperature_adaptive(1.0); // Same loss } } // Temperature should not exceed start value assert!( dqn.get_temperature() <= 1.0, "Temperature should not exceed start value: {}", dqn.get_temperature() ); Ok(()) } #[test] fn test_temperature_adaptive_disabled() -> anyhow::Result<()> { // Create DQN with adaptive temperature DISABLED let mut config = WorkingDQNConfig::emergency_safe_defaults(); config.use_adaptive_temperature = false; // Disabled config.temperature_start = 1.0; config.temperature_decay = 0.995; let mut dqn = WorkingDQN::new(config)?; // Simulate improving loss let initial_temp = dqn.get_temperature(); for _ in 0..5 { dqn.update_temperature_adaptive(0.5); // Improving loss } // Temperature should use fixed decay (not adaptive) let expected_temp = initial_temp * 0.995_f64.powi(5); let actual_temp = dqn.get_temperature(); assert!( (actual_temp - expected_temp).abs() < 0.001, "Fixed decay should be used when adaptive disabled: {} vs {}", actual_temp, expected_temp ); Ok(()) } #[test] fn test_temperature_adaptive_loss_window_averaging() -> anyhow::Result<()> { // Create DQN with adaptive temperature enabled let mut config = WorkingDQNConfig::emergency_safe_defaults(); config.use_adaptive_temperature = true; config.loss_improvement_threshold = 0.999; // 0.1% improvement config.plateau_window = 10; let mut dqn = WorkingDQN::new(config)?; // Simulate noisy loss (oscillating but overall improving) let noisy_losses = vec![1.0, 1.1, 0.9, 1.0, 0.8, 0.9, 0.7, 0.85, 0.65, 0.8]; let initial_temp = dqn.get_temperature(); for loss in noisy_losses { dqn.update_temperature_adaptive(loss); } // Temperature should decay (loss averaged over window shows improvement) let final_temp = dqn.get_temperature(); assert!( final_temp < initial_temp, "Temperature should decay despite noisy loss: {} -> {}", initial_temp, final_temp ); // Plateau counter should be low (averaging smooths noise) assert!( dqn.get_plateau_count() < 5, "Plateau count should be low with noisy but improving loss: {}", dqn.get_plateau_count() ); Ok(()) }