//! PPO-Specific Edge Case Tests for Hyperparameter Optimization //! //! This test suite covers PPO-specific edge cases: //! 1. Dual learning rate constraints (policy vs value) //! 2. Clip epsilon boundaries //! 3. Value loss coefficient edge cases //! 4. Entropy coefficient constraints //! 5. Synthetic trajectory generation edge cases //! //! Purpose: Ensure PPO adapter handles actor-critic specific edge cases robustly use ml::hyperopt::adapters::ppo::{PPOParams, PPOTrainer}; use ml::hyperopt::traits::{HyperparameterOptimizable, ParameterSpace}; // ============================================================================ // DUAL LEARNING RATE CONSTRAINTS // ============================================================================ #[test] fn test_policy_lr_bounds() { let bounds = PPOParams::continuous_bounds(); // Policy LR bounds: [ln(1e-6), ln(1e-3)] let min_lr = bounds[0].0.exp(); let max_lr = bounds[0].1.exp(); assert!((min_lr - 1e-6).abs() < 1e-10); assert!((max_lr - 1e-3).abs() < 1e-10); } #[test] fn test_value_lr_bounds() { let bounds = PPOParams::continuous_bounds(); // Value LR bounds: [ln(1e-5), ln(1e-3)] let min_lr = bounds[1].0.exp(); let max_lr = bounds[1].1.exp(); assert!((min_lr - 1e-5).abs() < 1e-10); assert!((max_lr - 1e-3).abs() < 1e-10); } #[test] fn test_policy_value_lr_relationship() { // Typically policy_lr < value_lr, but not enforced let params = PPOParams::default(); assert!(params.policy_learning_rate > 0.0); assert!(params.value_learning_rate > 0.0); } #[test] fn test_extreme_lr_difference() { // Test very different learning rates let params = PPOParams { policy_learning_rate: 1e-6, // Very small value_learning_rate: 1e-3, // Large ..Default::default() }; let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Extreme LR difference should be valid"); assert!((recovered.policy_learning_rate - 1e-6).abs() < 1e-10); assert!((recovered.value_learning_rate - 1e-3).abs() < 1e-10); } // ============================================================================ // CLIP EPSILON BOUNDARIES // ============================================================================ #[test] fn test_clip_epsilon_bounds() { let bounds = PPOParams::continuous_bounds(); // Clip epsilon bounds: [0.1, 0.3] assert_eq!(bounds[2], (0.1, 0.3)); } #[test] fn test_clip_epsilon_min() { let mut params = PPOParams::default(); params.clip_epsilon = 0.1; // Conservative clipping let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Min clip epsilon should be valid"); assert!((recovered.clip_epsilon - 0.1).abs() < 1e-10); } #[test] fn test_clip_epsilon_max() { let mut params = PPOParams::default(); params.clip_epsilon = 0.3; // Aggressive clipping let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Max clip epsilon should be valid"); assert!((recovered.clip_epsilon - 0.3).abs() < 1e-10); } #[test] fn test_clip_epsilon_clamping() { // Test values outside [0.1, 0.3] are clamped let too_small = vec![ (-5.0_f64).ln(), // policy_lr (-4.0_f64).ln(), // value_lr 0.05, // clip_epsilon (below min) 1.0, // value_loss_coeff (0.05_f64).ln(), // entropy_coeff ]; let params_small = PPOParams::from_continuous(&too_small).expect("Should clamp clip epsilon"); assert!( (params_small.clip_epsilon - 0.1).abs() < 1e-6, "Should clamp to 0.1" ); let too_large = vec![ (-5.0_f64).ln(), // policy_lr (-4.0_f64).ln(), // value_lr 0.5, // clip_epsilon (above max) 1.0, // value_loss_coeff (0.05_f64).ln(), // entropy_coeff ]; let params_large = PPOParams::from_continuous(&too_large).expect("Should clamp clip epsilon"); assert!( (params_large.clip_epsilon - 0.3).abs() < 1e-6, "Should clamp to 0.3" ); } // ============================================================================ // VALUE LOSS COEFFICIENT EDGE CASES // ============================================================================ #[test] fn test_value_loss_coeff_bounds() { let bounds = PPOParams::continuous_bounds(); // Value loss coeff bounds: [0.5, 2.0] assert_eq!(bounds[3], (0.5, 2.0)); } #[test] fn test_value_loss_coeff_min() { let mut params = PPOParams::default(); params.value_loss_coeff = 0.5; // Minimal value loss weight let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Min value loss coeff should be valid"); assert!((recovered.value_loss_coeff - 0.5).abs() < 1e-10); } #[test] fn test_value_loss_coeff_max() { let mut params = PPOParams::default(); params.value_loss_coeff = 2.0; // High value loss weight let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Max value loss coeff should be valid"); assert!((recovered.value_loss_coeff - 2.0).abs() < 1e-10); } // ============================================================================ // ENTROPY COEFFICIENT CONSTRAINTS // ============================================================================ #[test] fn test_entropy_coeff_bounds() { let bounds = PPOParams::continuous_bounds(); // Entropy coeff bounds: [ln(0.001), ln(0.1)] let min_entropy = bounds[4].0.exp(); let max_entropy = bounds[4].1.exp(); assert!((min_entropy - 0.001).abs() < 1e-6); assert!((max_entropy - 0.1).abs() < 1e-6); } #[test] fn test_entropy_coeff_min() { let mut params = PPOParams::default(); params.entropy_coeff = 0.001; // Minimal exploration let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Min entropy coeff should be valid"); assert!((recovered.entropy_coeff - 0.001).abs() < 1e-6); } #[test] fn test_entropy_coeff_max() { let mut params = PPOParams::default(); params.entropy_coeff = 0.1; // High exploration let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Max entropy coeff should be valid"); assert!((recovered.entropy_coeff - 0.1).abs() < 1e-6); } // ============================================================================ // PARAMETER ROUNDTRIP TESTS // ============================================================================ #[test] fn test_ppo_params_roundtrip() { let params = PPOParams { policy_learning_rate: 3e-5, value_learning_rate: 1e-4, clip_epsilon: 0.2, value_loss_coeff: 1.0, entropy_coeff: 0.05, }; let continuous = params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Roundtrip should succeed"); assert!((recovered.policy_learning_rate - params.policy_learning_rate).abs() < 1e-10); assert!((recovered.value_learning_rate - params.value_learning_rate).abs() < 1e-10); assert!((recovered.clip_epsilon - params.clip_epsilon).abs() < 1e-10); assert!((recovered.value_loss_coeff - params.value_loss_coeff).abs() < 1e-10); assert!((recovered.entropy_coeff - params.entropy_coeff).abs() < 1e-10); } #[test] fn test_extreme_values_roundtrip() { // Test boundary values let extreme_params = PPOParams { policy_learning_rate: 1e-6, value_learning_rate: 1e-5, clip_epsilon: 0.1, value_loss_coeff: 0.5, entropy_coeff: 0.001, }; let continuous = extreme_params.to_continuous(); let recovered = PPOParams::from_continuous(&continuous).expect("Extreme values should roundtrip"); assert!((recovered.policy_learning_rate - extreme_params.policy_learning_rate).abs() < 1e-10); assert!((recovered.value_learning_rate - extreme_params.value_learning_rate).abs() < 1e-10); } // ============================================================================ // PARAMETER NAMES // ============================================================================ #[test] fn test_param_names() { let names = PPOParams::param_names(); assert_eq!(names.len(), 5); assert_eq!(names[0], "policy_learning_rate"); assert_eq!(names[1], "value_learning_rate"); assert_eq!(names[2], "clip_epsilon"); assert_eq!(names[3], "value_loss_coeff"); assert_eq!(names[4], "entropy_coeff"); } // ============================================================================ // TRAINER CREATION // ============================================================================ #[test] fn test_ppo_trainer_creation() { let result = PPOTrainer::new(1000); assert!(result.is_ok(), "PPOTrainer creation should succeed"); } #[test] fn test_ppo_trainer_zero_episodes() { let result = PPOTrainer::new(0); // Zero episodes should either error or handle gracefully assert!(result.is_ok(), "Should handle zero episodes"); } // ============================================================================ // INTEGRATION TESTS // ============================================================================ #[test] fn test_default_params_valid() { let params = PPOParams::default(); // Verify default values are reasonable assert!(params.policy_learning_rate > 0.0); assert!(params.value_learning_rate > 0.0); assert!(params.clip_epsilon > 0.0 && params.clip_epsilon < 1.0); assert!(params.value_loss_coeff > 0.0); assert!(params.entropy_coeff > 0.0); } #[test] fn test_parameter_space_coverage() { let bounds = PPOParams::continuous_bounds(); // Sample midpoint of parameter space let midpoint: Vec = bounds.iter().map(|(min, max)| (min + max) / 2.0).collect(); let params = PPOParams::from_continuous(&midpoint).expect("Midpoint should be valid"); // Verify all params are in valid ranges assert!(params.policy_learning_rate > 0.0); assert!(params.value_learning_rate > 0.0); assert!(params.clip_epsilon >= 0.1 && params.clip_epsilon <= 0.3); assert!(params.value_loss_coeff >= 0.5 && params.value_loss_coeff <= 2.0); assert!(params.entropy_coeff > 0.0); } #[test] fn test_log_scale_parameters() { // Verify learning rates and entropy use log scale let params1 = PPOParams { policy_learning_rate: 1e-6, value_learning_rate: 1e-5, entropy_coeff: 0.001, ..Default::default() }; let params2 = PPOParams { policy_learning_rate: 1e-3, value_learning_rate: 1e-3, entropy_coeff: 0.1, ..Default::default() }; let cont1 = params1.to_continuous(); let cont2 = params2.to_continuous(); // Log scale differences should be consistent assert!(cont1[0] < cont2[0]); // policy_lr assert!(cont1[1] < cont2[1]); // value_lr assert!(cont1[4] < cont2[4]); // entropy_coeff } #[test] fn test_combined_loss_calculation() { // Test that combined loss formula is correct let params = PPOParams::default(); let policy_loss = 0.5; let value_loss = 0.3; let combined_loss = policy_loss + params.value_loss_coeff * value_loss; // Verify formula let expected = policy_loss + params.value_loss_coeff * value_loss; assert!((combined_loss - expected).abs() < 1e-10); } #[test] fn test_all_params_positive() { // All PPO parameters should be positive let params = PPOParams::default(); assert!(params.policy_learning_rate > 0.0); assert!(params.value_learning_rate > 0.0); assert!(params.clip_epsilon > 0.0); assert!(params.value_loss_coeff > 0.0); assert!(params.entropy_coeff > 0.0); } #[test] fn test_parameter_relationships() { // Test that parameter relationships make sense let params = PPOParams::default(); // Clip epsilon should be reasonable (typically 0.1-0.3) assert!(params.clip_epsilon >= 0.1 && params.clip_epsilon <= 0.3); // Value loss coeff should be reasonable (typically 0.5-2.0) assert!(params.value_loss_coeff >= 0.5 && params.value_loss_coeff <= 2.0); // Entropy coeff should be small (typically 0.001-0.1) assert!(params.entropy_coeff >= 0.001 && params.entropy_coeff <= 0.1); }