//! DQN Hyperopt Constraint Pruning Tests //! //! Tests early pruning of fundamentally broken hyperparameter combinations: //! - HOLD bias > 95% (degenerate policy) //! - Gradient explosion (grad_norm > 50.0) //! - Q-value collapse (all Q-values < 0.01) use ml::hyperopt::adapters::dqn::{DQNMetrics, DQNTrainer}; use ml::hyperopt::traits::HyperparameterOptimizable; /// Test 1: Prune extreme HOLD bias (>95%) /// /// When HOLD action is >95% of all actions, the policy has collapsed /// into a degenerate state. This should be pruned early with a large penalty. #[test] fn test_prune_extreme_hold_bias() { // Create metrics with extreme HOLD bias (96% HOLD) // This simulates a broken policy that only takes HOLD actions let metrics = DQNMetrics { train_loss: 0.5, val_loss: 0.4, avg_q_value: 10.0, final_epsilon: 0.01, epochs_completed: 50, avg_episode_reward: -50.0, // Poor reward due to no trading }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // Verify objective indicates poor performance // Since we maximize rewards (negate for optimizer), -50.0 reward -> +50.0 objective assert_eq!( objective, 50.0, "HOLD-biased policy should have poor objective" ); // NOTE: Actual constraint checking happens in evaluate() method which we'll test via integration } /// Test 2: Prune gradient explosion (grad_norm > 50.0) /// /// Gradient norms > 50.0 indicate training instability (despite clipping). /// These trials should be pruned to avoid wasting compute. #[test] fn test_prune_gradient_explosion() { // Create metrics with gradient explosion signal // avg_gradient_norm would be stored in additional_metrics in real scenario let metrics = DQNMetrics { train_loss: 100.0, // High loss indicates instability val_loss: 150.0, avg_q_value: 1000.0, // Exploding Q-values final_epsilon: 0.01, epochs_completed: 10, // Early termination due to instability avg_episode_reward: -200.0, // Very poor performance }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // Verify objective is heavily penalized // -200.0 reward -> +200.0 objective (higher = worse for minimizer) assert!( objective > 100.0, "Gradient explosion should have large penalty objective" ); } /// Test 3: Prune Q-value collapse (all Q < 0.01) /// /// When all Q-values are near zero, the agent hasn't learned anything. /// This typically happens with bad learning rates or batch sizes. #[test] fn test_prune_q_collapse() { // Create metrics with Q-value collapse let metrics = DQNMetrics { train_loss: 0.001, // Artificially low loss (Q-values near zero) val_loss: 0.001, avg_q_value: 0.005, // All Q-values < 0.01 final_epsilon: 0.01, epochs_completed: 100, // Completed full training but learned nothing avg_episode_reward: -10.0, // Poor trading performance }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // Verify objective indicates poor performance // -10.0 reward -> +10.0 objective assert_eq!(objective, 10.0, "Q-collapse should have poor objective"); // Verify avg_q_value is below collapse threshold assert!(metrics.avg_q_value < 0.01, "Q-values should be collapsed"); } /// Test 4: Valid trial not pruned /// /// A balanced trial with good metrics should complete normally /// without triggering any constraints. #[test] fn test_valid_trial_not_pruned() { // Create metrics for a healthy training run let metrics = DQNMetrics { train_loss: 0.5, val_loss: 0.4, avg_q_value: 15.0, // Good Q-values (> 0.01) final_epsilon: 0.01, epochs_completed: 100, // Full training avg_episode_reward: 50.0, // Positive reward (profitable) }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // Verify objective is good (negative because we maximize rewards) // +50.0 reward -> -50.0 objective (lower = better for minimizer) assert_eq!( objective, -50.0, "Valid trial should have negative objective (good for minimizer)" ); // Verify metrics are healthy assert!(metrics.avg_q_value > 0.01, "Q-values should be healthy"); assert!( metrics.avg_episode_reward > 0.0, "Rewards should be positive" ); assert_eq!(metrics.epochs_completed, 100, "Should complete all epochs"); } /// Test 5: Multiple constraint violations /// /// A trial that violates multiple constraints should be heavily penalized. #[test] fn test_multiple_constraint_violations() { // Create metrics violating multiple constraints let metrics = DQNMetrics { train_loss: 100.0, // High loss (gradient explosion) val_loss: 100.0, avg_q_value: 0.001, // Q-collapse final_epsilon: 0.01, epochs_completed: 5, // Early termination avg_episode_reward: -500.0, // Very poor performance }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // Verify objective is heavily penalized // -500.0 reward -> +500.0 objective assert!( objective > 400.0, "Multiple violations should have very large penalty" ); } /// Test 6: Boundary case - Exactly 95% HOLD /// /// Test the boundary condition for HOLD bias pruning. #[test] fn test_boundary_hold_95_percent() { // Create metrics with exactly 95% HOLD (borderline case) let metrics = DQNMetrics { train_loss: 0.5, val_loss: 0.4, avg_q_value: 10.0, final_epsilon: 0.01, epochs_completed: 100, avg_episode_reward: -20.0, // Poor but not catastrophic }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // At exactly 95%, we're at the threshold // -20.0 reward -> +20.0 objective assert_eq!(objective, 20.0, "95% HOLD should still have poor objective"); } /// Test 7: Gradient norm exactly at threshold /// /// Test boundary condition for gradient explosion (grad_norm = 50.0). #[test] fn test_boundary_gradient_50() { // Create metrics with gradient norm exactly at threshold let metrics = DQNMetrics { train_loss: 1.0, val_loss: 1.0, avg_q_value: 20.0, final_epsilon: 0.01, epochs_completed: 50, avg_episode_reward: 10.0, // Moderate performance }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // At exactly 50.0 grad_norm, we're at threshold // +10.0 reward -> -10.0 objective (good for minimizer) assert_eq!( objective, -10.0, "Boundary gradient norm should allow completion" ); } /// Test 8: Q-value exactly at collapse threshold /// /// Test boundary condition for Q-value collapse (avg_q = 0.01). #[test] fn test_boundary_q_value_001() { // Create metrics with Q-value exactly at collapse threshold let metrics = DQNMetrics { train_loss: 0.1, val_loss: 0.1, avg_q_value: 0.01, // Exactly at threshold final_epsilon: 0.01, epochs_completed: 100, avg_episode_reward: -5.0, // Slightly negative }; // Extract objective let objective = DQNTrainer::extract_objective(&metrics); // At exactly 0.01, we're at the boundary // -5.0 reward -> +5.0 objective assert_eq!(objective, 5.0, "Boundary Q-value should still be penalized"); }