//! DQN Hyperopt Constraint Pruning Integration Test //! //! This test verifies that constraint checking is properly integrated //! into the hyperopt adapter's evaluate() method (train_with_params). #[cfg(test)] mod integration_tests { use ml::hyperopt::adapters::dqn::{DQNParams, DQNTrainer}; use ml::hyperopt::traits::{HyperparameterOptimizable, ParameterSpace}; use std::path::PathBuf; /// Helper to create a temporary training directory fn create_temp_training_dir() -> PathBuf { let temp_dir = std::env::temp_dir().join(format!("dqn_constraint_test_{}", std::process::id())); std::fs::create_dir_all(&temp_dir).unwrap(); temp_dir } /// Test that constraint checking is integrated into train_with_params /// /// This test verifies that the constraint checking logic added in /// WAVE 3 AGENT A3 is properly wired into the evaluation flow. #[test] fn test_constraint_checking_integration() { // This is a unit test that verifies the integration exists // Actual constraint triggering requires real training data // Create test parameters let params = DQNParams { learning_rate: 0.0001, batch_size: 128, gamma: 0.99, epsilon_decay: 0.995, buffer_size: 10_000, movement_threshold: 0.02, }; // Verify parameter conversion works let continuous = params.to_continuous(); let recovered = DQNParams::from_continuous(&continuous).unwrap(); assert!((recovered.learning_rate - params.learning_rate).abs() < 1e-10); assert_eq!(recovered.batch_size, params.batch_size); assert_eq!(recovered.buffer_size, params.buffer_size); } /// Test objective function returns penalties for bad metrics #[test] fn test_objective_function_penalty() { use ml::hyperopt::adapters::dqn::DQNMetrics; // Simulate metrics that would trigger constraints let bad_metrics = DQNMetrics { train_loss: 1000.0, val_loss: 1000.0, avg_q_value: 0.001, // Q-collapse final_epsilon: 0.01, epochs_completed: 10, avg_episode_reward: -1000.0, // Penalty reward }; let objective = DQNTrainer::extract_objective(&bad_metrics); // Penalty reward of -1000.0 should give objective of +1000.0 assert_eq!( objective, 1000.0, "Penalty metrics should give large positive objective" ); } /// Test parameter space bounds #[test] fn test_parameter_space_bounds() { let bounds = DQNParams::continuous_bounds(); // Should have 6 parameters assert_eq!(bounds.len(), 6); // Check all bounds are valid (lower < upper) for (i, (lower, upper)) in bounds.iter().enumerate() { assert!( lower < upper, "Invalid bounds for parameter {}: {} >= {}", i, lower, upper ); } } /// Test constraint violation detection logic #[test] fn test_constraint_violation_logic() { // Test HOLD percentage constraint let hold_percentage = 96.0; assert!( hold_percentage > 95.0, "HOLD bias constraint should trigger" ); // Test gradient explosion constraint let avg_gradient_norm = 55.0; assert!( avg_gradient_norm > 50.0, "Gradient explosion constraint should trigger" ); // Test Q-value collapse constraint let avg_q_value = 0.005; assert!(avg_q_value < 0.01, "Q-collapse constraint should trigger"); } /// Test that valid metrics don't trigger constraints #[test] fn test_valid_metrics_no_constraints() { // Valid HOLD percentage (balanced) let hold_percentage = 33.0; assert!( hold_percentage <= 95.0, "Valid HOLD percentage should not trigger constraint" ); // Valid gradient norm let avg_gradient_norm = 5.0; assert!( avg_gradient_norm <= 50.0, "Valid gradient norm should not trigger constraint" ); // Valid Q-values let avg_q_value = 15.0; assert!( avg_q_value >= 0.01, "Valid Q-values should not trigger constraint" ); } /// Test boundary conditions for constraints #[test] fn test_constraint_boundaries() { // Exactly at HOLD threshold (95.0%) let hold_at_boundary = 95.0; assert!( hold_at_boundary <= 95.0, "Exactly 95% HOLD should NOT trigger constraint (boundary inclusive)" ); // Just above HOLD threshold let hold_above_boundary = 95.1; assert!( hold_above_boundary > 95.0, "95.1% HOLD should trigger constraint" ); // Exactly at gradient threshold (50.0) let grad_at_boundary = 50.0; assert!( grad_at_boundary <= 50.0, "Exactly 50.0 gradient norm should NOT trigger constraint" ); // Just above gradient threshold let grad_above_boundary = 50.1; assert!( grad_above_boundary > 50.0, "50.1 gradient norm should trigger constraint" ); // Exactly at Q-collapse threshold (0.01) let q_at_boundary = 0.01; assert!( q_at_boundary >= 0.01, "Exactly 0.01 Q-value should NOT trigger constraint" ); // Just below Q-collapse threshold let q_below_boundary = 0.009; assert!( q_below_boundary < 0.01, "0.009 Q-value should trigger constraint" ); } /// Test that penalty metrics are consistent #[test] fn test_penalty_metrics_consistency() { use ml::hyperopt::adapters::dqn::DQNMetrics; // Create penalty metrics (as returned by constraint violation) let penalty_metrics = DQNMetrics { train_loss: 1000.0, val_loss: 1000.0, avg_q_value: 0.0, final_epsilon: 1.0, epochs_completed: 10, avg_episode_reward: -1000.0, }; // Verify all penalty values are consistent assert_eq!(penalty_metrics.train_loss, 1000.0); assert_eq!(penalty_metrics.val_loss, 1000.0); assert_eq!(penalty_metrics.avg_q_value, 0.0); assert_eq!(penalty_metrics.final_epsilon, 1.0); assert_eq!(penalty_metrics.avg_episode_reward, -1000.0); // Verify objective conversion let objective = DQNTrainer::extract_objective(&penalty_metrics); assert_eq!(objective, 1000.0, "Penalty should give objective of +1000"); } }