//! DQN Hyperparameter Tests //! //! Test suite for DQN hyperparameter presets, validation, and CLI configuration. //! //! Coverage: //! - Test 1: Conservative preset values //! - Test 2: Aggressive preset values (to be implemented) //! - Test 3: Production preset values (Trial #35 optimal) //! - Test 4: CLI configurability of all hyperparameters //! - Test 5: Hyperopt adapter uses correct ranges //! - Test 6: Hyperparameter validation (reject invalid values) use ml::hyperopt::adapters::dqn::{DQNParams, DQNTrainer}; use ml::hyperopt::traits::ParameterSpace; use ml::trainers::dqn::DQNHyperparameters; /// Test 1: Verify conservative() preset has correct values /// /// Conservative preset should be suitable for testing and development. #[test] fn test_conservative_preset() { let params = DQNHyperparameters::conservative(); // Learning rate: moderate value assert_eq!(params.learning_rate, 0.0001); // Batch size: medium value (GPU compatible) assert_eq!(params.batch_size, 128); // Gamma: high value for long-term rewards assert_eq!(params.gamma, 0.99); // Epsilon parameters: standard exploration schedule assert_eq!(params.epsilon_start, 1.0); assert_eq!(params.epsilon_end, 0.01); assert_eq!(params.epsilon_decay, 0.995); // Buffer size: moderate capacity assert_eq!(params.buffer_size, 100_000); assert_eq!(params.min_replay_size, 1_000); // Training config assert_eq!(params.epochs, 100); assert_eq!(params.checkpoint_frequency, 10); // Early stopping: enabled with conservative thresholds assert!(params.early_stopping_enabled); assert_eq!(params.q_value_floor, 0.5); assert_eq!(params.min_loss_improvement_pct, 2.0); assert_eq!(params.plateau_window, 30); assert_eq!(params.min_epochs_before_stopping, 50); // Advanced features: enabled assert_eq!(params.gradient_clip_norm, Some(1.0)); assert!(params.use_huber_loss); assert_eq!(params.huber_delta, 1.0); assert!(params.use_double_dqn); // HOLD penalty assert_eq!(params.hold_penalty_weight, 0.01); // Target network updates assert_eq!(params.target_update_frequency, 500); assert_eq!(params.target_update_tau, None); // Validation configuration assert!(!params.skip_validation); assert_eq!(params.validation_split, 0.2); assert_eq!(params.validation_log_frequency, 1); assert_eq!(params.movement_threshold, 0.02); } /// Test 2: Verify aggressive() preset has correct values /// /// Aggressive preset should use higher learning rates and larger batch sizes /// for faster convergence (at the risk of instability). #[test] fn test_aggressive_preset() { let params = DQNHyperparameters::aggressive(); // Learning rate: higher for faster learning assert_eq!(params.learning_rate, 0.0005); // Batch size: larger for more stable gradients assert_eq!(params.batch_size, 230); // Max for RTX 3050 Ti // Gamma: lower for more short-term focus assert_eq!(params.gamma, 0.95); // Epsilon: faster decay for exploitation assert_eq!(params.epsilon_start, 1.0); assert_eq!(params.epsilon_end, 0.01); assert_eq!(params.epsilon_decay, 0.99); // Faster decay than conservative // Buffer size: larger for more diverse experiences assert_eq!(params.buffer_size, 200_000); assert_eq!(params.min_replay_size, 2_000); // Training config assert_eq!(params.epochs, 500); assert_eq!(params.checkpoint_frequency, 50); // Early stopping: disabled for aggressive training assert!(!params.early_stopping_enabled); // Advanced features assert_eq!(params.gradient_clip_norm, Some(1.0)); // Target network updates assert_eq!(params.target_update_frequency, 1000); assert_eq!(params.target_update_tau, Some(0.005)); assert!(params.use_huber_loss); assert!(params.use_double_dqn); } /// Test 3: Verify production() preset has optimal Trial #35 values /// /// Production preset should use hyperopt-optimized values from Trial #35: /// - Learning rate: 5e-6 (more stable than 1e-5) /// - Batch size: 64 (better than 110) /// - Gamma: 0.92 (less long-term bias than 0.9775) /// - Epsilon decay: 0.997 (slower than 0.9394) /// - Buffer size: 50,000 (larger than 34,000) #[test] fn test_production_preset() { let params = DQNHyperparameters::production(); // Optimized hyperparameters from Trial #35 backtesting analysis assert_eq!(params.learning_rate, 5e-6); assert_eq!(params.batch_size, 64); assert_eq!(params.gamma, 0.92); // Epsilon schedule: slower decay for more exploration assert_eq!(params.epsilon_start, 1.0); assert_eq!(params.epsilon_end, 0.01); assert_eq!(params.epsilon_decay, 0.997); // Buffer size: larger for better experience diversity assert_eq!(params.buffer_size, 50_000); assert_eq!(params.min_replay_size, 128); // 2x batch size // Training config: production defaults assert_eq!(params.epochs, 1000); // Longer training for production assert_eq!(params.checkpoint_frequency, 100); // Early stopping: enabled with production thresholds assert!(params.early_stopping_enabled); assert_eq!(params.q_value_floor, 0.5); assert_eq!(params.min_loss_improvement_pct, 0.1); // More sensitive assert_eq!(params.plateau_window, 5); assert_eq!(params.min_epochs_before_stopping, 50); // Advanced features: all enabled for production assert_eq!(params.gradient_clip_norm, Some(1.0)); assert!(params.use_huber_loss); assert_eq!(params.huber_delta, 1.0); assert!(params.use_double_dqn); // HOLD penalty: default values assert_eq!(params.hold_penalty_weight, 0.01); assert_eq!(params.movement_threshold, 0.02); } /// Test 4: Verify all hyperparameters are accessible and configurable /// /// This test ensures all DQNHyperparameters fields can be set and retrieved. #[test] // Target network updates assert_eq!(params.target_update_frequency, 500); assert_eq!(params.target_update_tau, None); // Validation configuration assert!(!params.skip_validation); assert_eq!(params.validation_split, 0.2); assert_eq!(params.validation_log_frequency, 1); fn test_all_hyperparameters_configurable() { // Create custom hyperparameters by modifying production preset let mut params = DQNHyperparameters::production(); // Verify all fields are mutable and accessible params.learning_rate = 1e-4; params.batch_size = 128; params.gamma = 0.99; params.epsilon_start = 0.5; params.epsilon_end = 0.05; params.epsilon_decay = 0.995; params.buffer_size = 100_000; params.min_replay_size = 500; params.epochs = 200; params.checkpoint_frequency = 20; params.early_stopping_enabled = false; params.q_value_floor = 1.0; params.min_loss_improvement_pct = 5.0; params.plateau_window = 10; params.min_epochs_before_stopping = 100; params.hold_penalty_weight = 0.02; params.movement_threshold = 0.01; params.gradient_clip_norm = Some(0.5); params.use_huber_loss = false; params.huber_delta = 2.0; params.use_double_dqn = false; // Verify values were set correctly assert_eq!(params.learning_rate, 1e-4); assert_eq!(params.batch_size, 128); assert_eq!(params.gamma, 0.99); assert_eq!(params.epsilon_start, 0.5); assert_eq!(params.epsilon_end, 0.05); assert_eq!(params.epsilon_decay, 0.995); assert_eq!(params.buffer_size, 100_000); assert_eq!(params.min_replay_size, 500); assert_eq!(params.epochs, 200); assert_eq!(params.checkpoint_frequency, 20); assert!(!params.early_stopping_enabled); assert_eq!(params.q_value_floor, 1.0); assert_eq!(params.min_loss_improvement_pct, 5.0); assert_eq!(params.plateau_window, 10); assert_eq!(params.min_epochs_before_stopping, 100); assert_eq!(params.hold_penalty_weight, 0.02); assert_eq!(params.movement_threshold, 0.01); assert_eq!(params.gradient_clip_norm, Some(0.5)); assert!(!params.use_huber_loss); assert_eq!(params.huber_delta, 2.0); assert!(!params.use_double_dqn); } /// Test 5: Verify hyperopt adapter uses correct parameter ranges /// /// Updated ranges based on Trial #35 backtesting analysis: /// - Learning rate: 1e-6 to 1e-4 (narrower, more conservative) /// - Gamma: 0.90 to 0.95 (lower values, less long-term bias) /// - Epsilon decay: 0.995 to 0.9995 (slower decay) /// - Batch size: 32 to 128 (GPU optimized) /// - Buffer size: 10,000 to 100,000 (expanded range) #[test] fn test_hyperopt_ranges() { let bounds = DQNParams::continuous_bounds(); // 5 continuous parameters: [learning_rate, batch_size, gamma, epsilon_decay, buffer_size] assert_eq!(bounds.len(), 5); // Learning rate: log-scale, 1e-6 to 1e-4 (updated from 1e-5 to 1e-3) let lr_bounds = bounds[0]; assert!((lr_bounds.0 - (1e-6_f64).ln()).abs() < 1e-10); assert!((lr_bounds.1 - (1e-4_f64).ln()).abs() < 1e-10); // Batch size: linear scale, 32 to 128 (updated from 64 to 230) let batch_bounds = bounds[1]; assert_eq!(batch_bounds, (32.0, 128.0)); // Gamma: linear scale, 0.90 to 0.95 (updated from 0.95 to 0.99) let gamma_bounds = bounds[2]; assert_eq!(gamma_bounds, (0.90, 0.95)); // Epsilon decay: log-scale, 0.995 to 0.9995 (updated from 0.88 to 0.95) let epsilon_bounds = bounds[3]; assert!((epsilon_bounds.0 - (0.995_f64).ln()).abs() < 1e-10); assert!((epsilon_bounds.1 - (0.9995_f64).ln()).abs() < 1e-10); // Buffer size: log-scale, 10k to 100k (updated from 10k to 1M) let buffer_bounds = bounds[4]; assert!((buffer_bounds.0 - (10_000_f64).ln()).abs() < 1e-10); assert!((buffer_bounds.1 - (100_000_f64).ln()).abs() < 1e-10); } /// Test 6: Verify hyperparameter validation rejects invalid values /// /// Validation should catch: /// - Learning rate outside (0, 1] /// - Gamma outside (0, 1) /// - Epsilon decay outside (0, 1) /// - Batch size < 1 /// - Buffer size < batch size #[test] fn test_hyperparameter_validation() { // Valid production parameters should pass let valid_params = DQNHyperparameters::production(); assert!(valid_params.validate().is_ok()); // Test 1: Invalid learning rate (too low) let mut invalid_lr_low = valid_params.clone(); invalid_lr_low.learning_rate = 0.0; assert!(invalid_lr_low.validate().is_err()); assert_eq!( invalid_lr_low.validate().unwrap_err(), "learning_rate must be in (0, 1]" ); // Test 2: Invalid learning rate (too high) let mut invalid_lr_high = valid_params.clone(); invalid_lr_high.learning_rate = 1.5; assert!(invalid_lr_high.validate().is_err()); assert_eq!( invalid_lr_high.validate().unwrap_err(), "learning_rate must be in (0, 1]" ); // Test 3: Invalid gamma (too low) let mut invalid_gamma_low = valid_params.clone(); invalid_gamma_low.gamma = 0.0; assert!(invalid_gamma_low.validate().is_err()); assert_eq!( invalid_gamma_low.validate().unwrap_err(), "gamma must be in (0, 1)" ); // Test 4: Invalid gamma (too high) let mut invalid_gamma_high = valid_params.clone(); invalid_gamma_high.gamma = 1.0; assert!(invalid_gamma_high.validate().is_err()); assert_eq!( invalid_gamma_high.validate().unwrap_err(), "gamma must be in (0, 1)" ); // Test 5: Invalid epsilon_decay (too low) let mut invalid_epsilon_low = valid_params.clone(); invalid_epsilon_low.epsilon_decay = 0.0; assert!(invalid_epsilon_low.validate().is_err()); assert_eq!( invalid_epsilon_low.validate().unwrap_err(), "epsilon_decay must be in (0, 1)" ); // Test 6: Invalid epsilon_decay (too high) let mut invalid_epsilon_high = valid_params.clone(); invalid_epsilon_high.epsilon_decay = 1.0; assert!(invalid_epsilon_high.validate().is_err()); assert_eq!( invalid_epsilon_high.validate().unwrap_err(), "epsilon_decay must be in (0, 1)" ); // Test 7: Invalid batch size (< 1) let mut invalid_batch = valid_params.clone(); invalid_batch.batch_size = 0; assert!(invalid_batch.validate().is_err()); assert_eq!( invalid_batch.validate().unwrap_err(), "batch_size must be >= 1" ); // Test 8: Invalid buffer size (< batch size) let mut invalid_buffer = valid_params.clone(); invalid_buffer.buffer_size = invalid_buffer.batch_size - 1; assert!(invalid_buffer.validate().is_err()); assert_eq!( invalid_buffer.validate().unwrap_err(), "buffer_size must be >= batch_size" ); // Test 9: Invalid target_update_frequency (< 1) let mut invalid_target_freq = valid_params.clone(); invalid_target_freq.target_update_frequency = 0; assert!(invalid_target_freq.validate().is_err()); assert_eq!( invalid_target_freq.validate().unwrap_err(), "target_update_frequency must be >= 1" ); } /// Test 7: Verify parameter space conversions are accurate /// /// Test that DQNParams can correctly convert to/from continuous representation /// and that bounds are enforced. #[test] fn test_parameter_space_conversions() { // Create params with known values let original = DQNParams { learning_rate: 5e-6, batch_size: 64, gamma: 0.92, epsilon_decay: 0.997, buffer_size: 50_000, }; // Convert to continuous and back let continuous = original.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).unwrap(); // Verify values match (with small floating point tolerance) assert!((recovered.learning_rate - original.learning_rate).abs() < 1e-10); assert_eq!(recovered.batch_size, original.batch_size); assert!((recovered.gamma - original.gamma).abs() < 1e-10); assert!((recovered.epsilon_decay - original.epsilon_decay).abs() < 1e-6); assert_eq!(recovered.buffer_size, original.buffer_size); }