Files
foxhunt/ml/tests/dqn_feature_defaults_test.rs
jgrusewski c645e6222d Wave 11: Rainbow DQN integration + 23/23 tests passing
CRITICAL FINDINGS from 3-trial validation:
- 85,120 gradient clipping warnings (81.6% of logs) - REGRESSION
- Rainbow features DISABLED: use_dueling=false, use_distributional=false, use_noisy_nets=false
- Negative Q-values confirmed: HOLD -1000 to -3250
- Performance: Sharpe 0.29 (target 0.77)

Changes:
- Fixed N-Step compilation (7/7 tests passing)
- Fixed Distributional compilation (6/6 tests passing)
- Fixed Dueling CUDA errors (10/10 tests passing)
- Added TDD validation for state_dim=225
- Total: 23/23 Wave 11 tests passing (100%)

Issues requiring investigation:
1. Why are Dueling/Distributional/Noisy disabled in hyperopt?
2. Why gradient explosion despite previous fixes?
3. Test coverage gaps - unit tests pass but integration fails

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:53:59 +01:00

251 lines
9.2 KiB
Rust

//! DQN Rainbow Feature Toggle Tests (Wave 6.4)
//!
//! Validates that all Rainbow DQN components are enabled by default
//! and can be toggled via configuration.
//!
//! Test Coverage:
//! - All Rainbow features enabled by default in DQNHyperparameters::conservative()
//! - Default parameter values match Rainbow DQN paper standards
//! - Individual features can be disabled without breaking the system
//! - Vanilla DQN configuration (all Rainbow features disabled) still works
use ml::trainers::dqn::DQNHyperparameters;
#[test]
fn test_all_rainbow_features_enabled_by_default() {
let hyperparams = DQNHyperparameters::conservative();
// All Rainbow components should be enabled by default
assert!(hyperparams.use_dueling, "Dueling should be enabled by default");
assert!(hyperparams.use_distributional, "Distributional should be enabled by default");
assert!(hyperparams.use_noisy_nets, "Noisy nets should be enabled by default");
assert!(hyperparams.use_per, "PER should be enabled by default");
assert_eq!(hyperparams.n_steps, 3, "Multi-step (n=3) should be enabled by default");
}
#[test]
fn test_rainbow_defaults_match_best_practice() {
let hyperparams = DQNHyperparameters::conservative();
// Validate default values match Rainbow DQN paper
assert_eq!(hyperparams.num_atoms, 51, "C51 standard is 51 atoms");
assert_eq!(hyperparams.noisy_sigma_init, 0.5, "Rainbow standard sigma_init");
assert_eq!(hyperparams.per_alpha, 0.6, "Standard PER alpha");
assert_eq!(hyperparams.tau, 0.001, "Standard soft update rate");
assert_eq!(hyperparams.dueling_hidden_dim, 128, "Dueling hidden dimension should be 128");
}
#[test]
fn test_distributional_parameters_valid() {
let hyperparams = DQNHyperparameters::conservative();
// Validate C51 distributional RL parameters
assert_eq!(hyperparams.num_atoms, 51, "C51 standard: 51 atoms");
assert_eq!(hyperparams.v_min, -1000.0, "V_min should be -1000.0");
assert_eq!(hyperparams.v_max, 1000.0, "V_max should be 1000.0");
assert!(hyperparams.v_max > hyperparams.v_min, "V_max must be greater than V_min");
assert!(hyperparams.num_atoms > 1, "Must have at least 2 atoms");
assert!(hyperparams.num_atoms % 2 == 1, "Odd number of atoms is standard (51)");
}
#[test]
fn test_multi_step_returns_valid() {
let hyperparams = DQNHyperparameters::conservative();
// Validate n-step returns parameter
assert_eq!(hyperparams.n_steps, 3, "Default should be 3 (Rainbow standard)");
assert!(hyperparams.n_steps >= 1, "n_steps must be at least 1");
assert!(hyperparams.n_steps <= 10, "n_steps should be <= 10 to avoid high variance");
}
#[test]
fn test_dueling_parameters_valid() {
let hyperparams = DQNHyperparameters::conservative();
// Validate dueling network parameters
assert!(hyperparams.use_dueling, "Dueling should be enabled");
assert_eq!(hyperparams.dueling_hidden_dim, 128, "Hidden dim should be 128");
assert!(hyperparams.dueling_hidden_dim >= 32, "Hidden dim should be at least 32");
assert!(hyperparams.dueling_hidden_dim <= 512, "Hidden dim should be reasonable (<= 512)");
}
#[test]
fn test_noisy_nets_parameters_valid() {
let hyperparams = DQNHyperparameters::conservative();
// Validate noisy networks parameters
assert!(hyperparams.use_noisy_nets, "Noisy nets should be enabled");
assert_eq!(hyperparams.noisy_sigma_init, 0.5, "Sigma init should be 0.5 (Rainbow standard)");
assert!(hyperparams.noisy_sigma_init > 0.0, "Sigma must be positive");
assert!(hyperparams.noisy_sigma_init <= 1.0, "Sigma should be reasonable (<= 1.0)");
}
#[test]
fn test_per_parameters_valid() {
let hyperparams = DQNHyperparameters::conservative();
// Validate PER parameters
assert!(hyperparams.use_per, "PER should be enabled");
assert_eq!(hyperparams.per_alpha, 0.6, "PER alpha should be 0.6");
assert_eq!(hyperparams.per_beta_start, 0.4, "PER beta start should be 0.4");
assert!(hyperparams.per_alpha >= 0.0 && hyperparams.per_alpha <= 1.0, "Alpha must be in [0, 1]");
assert!(hyperparams.per_beta_start >= 0.0 && hyperparams.per_beta_start <= 1.0, "Beta must be in [0, 1]");
}
#[test]
fn test_can_create_vanilla_dqn_config() {
// Test that we can create a vanilla DQN config with all Rainbow features disabled
let vanilla_config = DQNHyperparameters {
use_dueling: false,
use_distributional: false,
use_noisy_nets: false,
use_per: false,
n_steps: 1,
..DQNHyperparameters::conservative()
};
// Verify vanilla DQN configuration
assert!(!vanilla_config.use_dueling, "Dueling should be disabled");
assert!(!vanilla_config.use_distributional, "Distributional should be disabled");
assert!(!vanilla_config.use_noisy_nets, "Noisy nets should be disabled");
assert!(!vanilla_config.use_per, "PER should be disabled");
assert_eq!(vanilla_config.n_steps, 1, "n_steps should be 1 for vanilla DQN");
}
#[test]
fn test_can_disable_individual_rainbow_features() {
// Test that we can selectively disable Rainbow features
let config_no_dueling = DQNHyperparameters {
use_dueling: false,
..DQNHyperparameters::conservative()
};
assert!(!config_no_dueling.use_dueling);
assert!(config_no_dueling.use_distributional); // Others still enabled
assert!(config_no_dueling.use_noisy_nets);
assert!(config_no_dueling.use_per);
let config_no_distributional = DQNHyperparameters {
use_distributional: false,
..DQNHyperparameters::conservative()
};
assert!(config_no_distributional.use_dueling); // Others still enabled
assert!(!config_no_distributional.use_distributional);
assert!(config_no_distributional.use_noisy_nets);
assert!(config_no_distributional.use_per);
let config_no_noisy = DQNHyperparameters {
use_noisy_nets: false,
..DQNHyperparameters::conservative()
};
assert!(config_no_noisy.use_dueling); // Others still enabled
assert!(config_no_noisy.use_distributional);
assert!(!config_no_noisy.use_noisy_nets);
assert!(config_no_noisy.use_per);
}
#[test]
fn test_rainbow_feature_combinations() {
// Test common feature combinations
// Rainbow without noisy nets (use epsilon-greedy instead)
let rainbow_epsilon_greedy = DQNHyperparameters {
use_noisy_nets: false,
..DQNHyperparameters::conservative()
};
assert!(rainbow_epsilon_greedy.use_dueling);
assert!(rainbow_epsilon_greedy.use_distributional);
assert!(!rainbow_epsilon_greedy.use_noisy_nets);
assert!(rainbow_epsilon_greedy.use_per);
// Rainbow without PER (uniform replay)
let rainbow_uniform_replay = DQNHyperparameters {
use_per: false,
..DQNHyperparameters::conservative()
};
assert!(rainbow_uniform_replay.use_dueling);
assert!(rainbow_uniform_replay.use_distributional);
assert!(rainbow_uniform_replay.use_noisy_nets);
assert!(!rainbow_uniform_replay.use_per);
// Double DQN + Dueling only (no distributional, no noisy, no PER)
let double_dueling_only = DQNHyperparameters {
use_dueling: true,
use_distributional: false,
use_noisy_nets: false,
use_per: false,
n_steps: 1,
..DQNHyperparameters::conservative()
};
assert!(double_dueling_only.use_dueling);
assert!(!double_dueling_only.use_distributional);
assert!(!double_dueling_only.use_noisy_nets);
assert!(!double_dueling_only.use_per);
}
#[test]
fn test_n_step_parameter_range() {
// Test valid n_steps range (1-10)
for n in 1..=10 {
let config = DQNHyperparameters {
n_steps: n,
..DQNHyperparameters::conservative()
};
assert_eq!(config.n_steps, n);
}
}
#[test]
fn test_num_atoms_parameter_range() {
// Test various num_atoms values (odd numbers recommended)
for &atoms in &[11, 21, 31, 41, 51, 61, 71, 81, 101] {
let config = DQNHyperparameters {
num_atoms: atoms,
..DQNHyperparameters::conservative()
};
assert_eq!(config.num_atoms, atoms);
assert!(atoms % 2 == 1, "Odd number of atoms is standard");
}
}
#[test]
fn test_v_min_v_max_parameter_ranges() {
// Test various v_min/v_max ranges
let test_ranges = [
(-100.0, 100.0),
(-500.0, 500.0),
(-1000.0, 1000.0),
(-2000.0, 2000.0),
];
for (v_min, v_max) in test_ranges {
let config = DQNHyperparameters {
v_min,
v_max,
..DQNHyperparameters::conservative()
};
assert_eq!(config.v_min, v_min);
assert_eq!(config.v_max, v_max);
assert!(config.v_max > config.v_min, "v_max must be greater than v_min");
}
}
#[test]
fn test_backward_compatibility() {
// Ensure conservative() produces a valid configuration
let config = DQNHyperparameters::conservative();
// All required fields should have valid values
assert!(config.learning_rate > 0.0);
assert!(config.batch_size > 0);
assert!(config.gamma >= 0.0 && config.gamma <= 1.0);
assert!(config.buffer_size > 0);
assert!(config.epochs > 0);
// Rainbow features should all be enabled
assert!(config.use_dueling);
assert!(config.use_distributional);
assert!(config.use_noisy_nets);
assert!(config.use_per);
assert_eq!(config.n_steps, 3);
}