//! DQN-Specific Edge Case Tests for Hyperparameter Optimization //! //! This test suite covers DQN-specific edge cases: //! 1. Replay buffer size constraints //! 2. Epsilon decay edge cases //! 3. Gamma (discount factor) boundaries //! 4. Batch size vs buffer size constraints //! //! Purpose: Ensure DQN adapter handles RL-specific edge cases robustly use ml::hyperopt::adapters::dqn::{DQNParams, DQNTrainer}; use ml::hyperopt::traits::{HyperparameterOptimizable, ParameterSpace}; // ============================================================================ // REPLAY BUFFER CONSTRAINTS // ============================================================================ #[test] fn test_buffer_size_min_bound() { let mut params = DQNParams::default(); params.buffer_size = 10_000; // Minimum bound let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Min buffer size should be valid"); assert_eq!(recovered.buffer_size, 10_000); } #[test] fn test_buffer_size_max_bound() { let mut params = DQNParams::default(); params.buffer_size = 1_000_000; // Maximum bound let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Max buffer size should be valid"); assert_eq!(recovered.buffer_size, 1_000_000); } #[test] fn test_batch_size_vs_buffer_size() { // Batch size should be <= buffer size let params = DQNParams { learning_rate: 1e-4, batch_size: 128, gamma: 0.99, epsilon_decay: 0.995, buffer_size: 100_000, }; assert!( params.batch_size <= params.buffer_size, "Batch size {} should be <= buffer size {}", params.batch_size, params.buffer_size ); } // ============================================================================ // EPSILON DECAY EDGE CASES // ============================================================================ #[test] fn test_epsilon_decay_bounds() { let bounds = DQNParams::continuous_bounds(); // Epsilon decay bounds: [ln(0.990), ln(0.999)] let min_decay = bounds[3].0.exp(); let max_decay = bounds[3].1.exp(); assert!((min_decay - 0.990).abs() < 1e-6); assert!((max_decay - 0.999).abs() < 1e-6); } #[test] fn test_epsilon_decay_min() { let mut params = DQNParams::default(); params.epsilon_decay = 0.990; // Fast decay let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Min epsilon decay should be valid"); assert!((recovered.epsilon_decay - 0.990).abs() < 1e-6); } #[test] fn test_epsilon_decay_max() { let mut params = DQNParams::default(); params.epsilon_decay = 0.999; // Slow decay let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Max epsilon decay should be valid"); assert!((recovered.epsilon_decay - 0.999).abs() < 1e-6); } // ============================================================================ // GAMMA (DISCOUNT FACTOR) BOUNDARIES // ============================================================================ #[test] fn test_gamma_bounds() { let bounds = DQNParams::continuous_bounds(); // Gamma bounds: [0.95, 0.99] assert_eq!(bounds[2], (0.95, 0.99)); } #[test] fn test_gamma_min() { let mut params = DQNParams::default(); params.gamma = 0.95; // Short-term focused let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Min gamma should be valid"); assert!((recovered.gamma - 0.95).abs() < 1e-10); } #[test] fn test_gamma_max() { let mut params = DQNParams::default(); params.gamma = 0.99; // Long-term focused let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Max gamma should be valid"); assert!((recovered.gamma - 0.99).abs() < 1e-10); } // ============================================================================ // BATCH SIZE CONSTRAINTS // ============================================================================ #[test] fn test_batch_size_min_bound() { let bounds = DQNParams::continuous_bounds(); // Batch size bounds: [32, 230] assert_eq!(bounds[1], (32.0, 230.0)); } #[test] fn test_batch_size_rtx_3050_ti_constraint() { // RTX 3050 Ti max batch size = 230 let mut params = DQNParams::default(); params.batch_size = 230; // Maximum for RTX 3050 Ti let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Max batch size should be valid"); assert_eq!(recovered.batch_size, 230); } #[test] fn test_batch_size_clamping() { // Test that batch sizes outside [32, 230] are clamped let too_small = vec![ (-4.0_f64).ln(), // learning_rate 10.0, // batch_size (below min) 0.99, // gamma (0.995_f64).ln(), // epsilon_decay (100_000_f64).ln(), // buffer_size ]; let params_small = DQNParams::from_continuous(&too_small).expect("Should clamp batch size"); assert!(params_small.batch_size >= 32, "Should clamp to min"); let too_large = vec![ (-4.0_f64).ln(), // learning_rate 1000.0, // batch_size (above max) 0.99, // gamma (0.995_f64).ln(), // epsilon_decay (100_000_f64).ln(), // buffer_size ]; let params_large = DQNParams::from_continuous(&too_large).expect("Should clamp batch size"); assert!(params_large.batch_size <= 230, "Should clamp to max"); } // ============================================================================ // PARAMETER ROUNDTRIP TESTS // ============================================================================ #[test] fn test_dqn_params_roundtrip() { let params = DQNParams { learning_rate: 1e-4, batch_size: 128, gamma: 0.99, epsilon_decay: 0.995, buffer_size: 100_000, }; let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Roundtrip should succeed"); assert!((recovered.learning_rate - params.learning_rate).abs() < 1e-10); assert_eq!(recovered.batch_size, params.batch_size); assert!((recovered.gamma - params.gamma).abs() < 1e-10); assert!((recovered.epsilon_decay - params.epsilon_decay).abs() < 1e-6); assert_eq!(recovered.buffer_size, params.buffer_size); } #[test] fn test_extreme_values_roundtrip() { // Test boundary values let extreme_params = DQNParams { learning_rate: 1e-5, batch_size: 32, gamma: 0.95, epsilon_decay: 0.990, buffer_size: 10_000, }; let continuous = extreme_params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).expect("Extreme values should roundtrip"); assert!((recovered.learning_rate - extreme_params.learning_rate).abs() < 1e-10); assert_eq!(recovered.batch_size, extreme_params.batch_size); assert!((recovered.gamma - extreme_params.gamma).abs() < 1e-10); } // ============================================================================ // PARAMETER NAMES // ============================================================================ #[test] fn test_param_names() { let names = DQNParams::param_names(); assert_eq!(names.len(), 5); assert_eq!(names[0], "learning_rate"); assert_eq!(names[1], "batch_size"); assert_eq!(names[2], "gamma"); assert_eq!(names[3], "epsilon_decay"); assert_eq!(names[4], "buffer_size"); } // ============================================================================ // TRAINER CREATION // ============================================================================ #[test] fn test_dqn_trainer_invalid_path() { let result = DQNTrainer::new("nonexistent_directory", 100); assert!(result.is_err(), "Should error on nonexistent directory"); let err_msg = format!("{:?}", result.unwrap_err()); assert!( err_msg.contains("not found") || err_msg.contains("Config"), "Should mention directory not found" ); } // ============================================================================ // INTEGRATION TESTS // ============================================================================ #[test] fn test_default_params_valid() { let params = DQNParams::default(); // Verify default values are reasonable assert!(params.learning_rate > 0.0); assert!(params.batch_size > 0); assert!(params.gamma > 0.0 && params.gamma <= 1.0); assert!(params.epsilon_decay > 0.0 && params.epsilon_decay <= 1.0); assert!(params.buffer_size > 0); assert!(params.batch_size <= params.buffer_size); } #[test] fn test_parameter_space_coverage() { let bounds = DQNParams::continuous_bounds(); // Sample midpoint of parameter space let midpoint: Vec = bounds.iter().map(|(min, max)| (min + max) / 2.0).collect(); let params = DQNParams::from_continuous(&midpoint).expect("Midpoint should be valid"); // Verify all params are in valid ranges assert!(params.learning_rate > 0.0); assert!(params.batch_size >= 32 && params.batch_size <= 230); assert!(params.gamma >= 0.95 && params.gamma <= 0.99); assert!(params.epsilon_decay >= 0.990 && params.epsilon_decay <= 0.999); assert!(params.buffer_size >= 10_000 && params.buffer_size <= 1_000_000); } #[test] fn test_learning_rate_log_scale() { // Verify learning rate uses log scale let params1 = DQNParams { learning_rate: 1e-5, ..Default::default() }; let params2 = DQNParams { learning_rate: 1e-3, ..Default::default() }; let cont1 = params1.to_continuous(); let cont2 = params2.to_continuous(); // Log scale: ln(1e-5) vs ln(1e-3) assert!(cont1[0] < cont2[0]); // Difference should be log(100) ≈ 4.6 let log_diff = (cont2[0] - cont1[0]).abs(); assert!((log_diff - (100.0_f64).ln()).abs() < 0.1); }