BREAKING CHANGES: - Removed orphaned dqn.rs monolithic trainer (4,975 lines) - Removed orphaned dqn_ensemble.rs module (816 lines) - Removed orphaned tft.rs and tft_complete_int8_integration_test.rs - TFT trainer split into modular directory structure DQN Module Refactoring: - Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs) - Fixed hyperopt 39D search space (continuous params only) - Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions - use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues) Clean Module Structure: - ml/src/trainers/dqn/ directory with proper mod.rs exports - ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs - All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness Documentation: - Added comprehensive docs in docs/codebase-cleanup/ - ADR-001 for DQN refactoring decisions - Rainbow DQN component matrix and quick reference guides Build Status: Compiles with zero errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
348 lines
11 KiB
Rust
348 lines
11 KiB
Rust
// WAVE 26 Integration Tests for DQN Hyperopt Adapter
|
|
//
|
|
// Tests for all new parameters added in WAVE 26:
|
|
// - P0: td_error_clamp_max, batch_diversity_cooldown
|
|
// - P1: lr_decay_type, sharpe_weight, gae_lambda, noisy_sigma_initial, noisy_sigma_final
|
|
// - Network: use_spectral_norm, use_attention, use_residual, norm_type, activation_type
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_dqn_params_default_wave26() {
|
|
let params = DQNParams::default();
|
|
|
|
// P0 parameters
|
|
assert_eq!(params.td_error_clamp_max, 10.0);
|
|
assert_eq!(params.batch_diversity_cooldown, 50.0);
|
|
|
|
// P1 training parameters
|
|
assert_eq!(params.lr_decay_type, 0.0); // constant
|
|
assert_eq!(params.sharpe_weight, 0.3);
|
|
assert_eq!(params.gae_lambda, 0.95);
|
|
assert_eq!(params.noisy_sigma_initial, 0.6);
|
|
assert_eq!(params.noisy_sigma_final, 0.4);
|
|
|
|
// Network architecture parameters
|
|
assert!(!params.use_spectral_norm);
|
|
assert!(!params.use_attention);
|
|
assert!(!params.use_residual);
|
|
assert_eq!(params.norm_type, 0.0); // LayerNorm
|
|
assert_eq!(params.activation_type, 0.0); // ReLU
|
|
}
|
|
|
|
#[test]
|
|
fn test_continuous_bounds_dimension() {
|
|
let bounds = DQNParams::continuous_bounds();
|
|
// WAVE 26: 39D search space
|
|
// 30D (previous) + 2D (P0) + 5D (P1 training) + 2D (P1 network) = 39D
|
|
assert_eq!(
|
|
bounds.len(),
|
|
39,
|
|
"WAVE 26 should have 39 continuous parameters"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_p0_bounds() {
|
|
let bounds = DQNParams::continuous_bounds();
|
|
|
|
// td_error_clamp_max: [1.0, 100.0]
|
|
assert_eq!(bounds[30], (1.0, 100.0));
|
|
|
|
// batch_diversity_cooldown: [10.0, 100.0]
|
|
assert_eq!(bounds[31], (10.0, 100.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_p1_training_bounds() {
|
|
let bounds = DQNParams::continuous_bounds();
|
|
|
|
// lr_decay_type: [0.0, 2.0] (0=constant, 1=linear, 2=cosine)
|
|
assert_eq!(bounds[32], (0.0, 2.0));
|
|
|
|
// sharpe_weight: [0.0, 0.5]
|
|
assert_eq!(bounds[33], (0.0, 0.5));
|
|
|
|
// gae_lambda: [0.9, 0.99]
|
|
assert_eq!(bounds[34], (0.9, 0.99));
|
|
|
|
// noisy_sigma_initial: [0.4, 0.8]
|
|
assert_eq!(bounds[35], (0.4, 0.8));
|
|
|
|
// noisy_sigma_final: [0.2, 0.5]
|
|
assert_eq!(bounds[36], (0.2, 0.5));
|
|
}
|
|
|
|
#[test]
|
|
fn test_p1_network_bounds() {
|
|
let bounds = DQNParams::continuous_bounds();
|
|
|
|
// norm_type: [0.0, 2.0] (0=LayerNorm, 1=RMSNorm, 2=None)
|
|
assert_eq!(bounds[37], (0.0, 2.0));
|
|
|
|
// activation_type: [0.0, 3.0] (0=ReLU, 1=LeakyReLU, 2=GELU, 3=Mish)
|
|
assert_eq!(bounds[38], (0.0, 3.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_continuous_wave26_minimal() -> Result<(), MLError> {
|
|
// Create minimal valid parameter vector (39D)
|
|
let mut x = vec![0.0; 39];
|
|
|
|
// Set required parameters to valid values
|
|
x[0] = (1e-4_f64).ln(); // learning_rate
|
|
x[1] = 128.0; // batch_size
|
|
x[2] = 0.99; // gamma
|
|
x[3] = (100_000_f64).ln(); // buffer_size
|
|
|
|
// P0 parameters (indices 30-31)
|
|
x[30] = 10.0; // td_error_clamp_max
|
|
x[31] = 50.0; // batch_diversity_cooldown
|
|
|
|
// P1 training parameters (indices 32-36)
|
|
x[32] = 0.0; // lr_decay_type (constant)
|
|
x[33] = 0.3; // sharpe_weight
|
|
x[34] = 0.95; // gae_lambda
|
|
x[35] = 0.6; // noisy_sigma_initial
|
|
x[36] = 0.4; // noisy_sigma_final
|
|
|
|
// P1 network parameters (indices 37-38)
|
|
x[37] = 0.0; // norm_type (LayerNorm)
|
|
x[38] = 0.0; // activation_type (ReLU)
|
|
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
|
|
// Verify P0 parameters
|
|
assert_eq!(params.td_error_clamp_max, 10.0);
|
|
assert_eq!(params.batch_diversity_cooldown, 50.0);
|
|
|
|
// Verify P1 training parameters
|
|
assert_eq!(params.lr_decay_type, 0.0);
|
|
assert_eq!(params.sharpe_weight, 0.3);
|
|
assert_eq!(params.gae_lambda, 0.95);
|
|
assert_eq!(params.noisy_sigma_initial, 0.6);
|
|
assert_eq!(params.noisy_sigma_final, 0.4);
|
|
|
|
// Verify P1 network parameters
|
|
assert!(!params.use_spectral_norm);
|
|
assert!(!params.use_attention);
|
|
assert!(!params.use_residual);
|
|
assert_eq!(params.norm_type, 0.0);
|
|
assert_eq!(params.activation_type, 0.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_continuous_wave26_maximal() -> Result<(), MLError> {
|
|
// Test with maximum values in bounds
|
|
let mut x = vec![0.0; 39];
|
|
|
|
// Set required parameters
|
|
x[0] = (1e-4_f64).ln();
|
|
x[1] = 128.0;
|
|
x[2] = 0.99;
|
|
x[3] = (100_000_f64).ln();
|
|
|
|
// P0 parameters at maximum
|
|
x[30] = 100.0; // td_error_clamp_max (max)
|
|
x[31] = 100.0; // batch_diversity_cooldown (max)
|
|
|
|
// P1 training parameters at maximum
|
|
x[32] = 2.0; // lr_decay_type (cosine)
|
|
x[33] = 0.5; // sharpe_weight (max)
|
|
x[34] = 0.99; // gae_lambda (max)
|
|
x[35] = 0.8; // noisy_sigma_initial (max)
|
|
x[36] = 0.5; // noisy_sigma_final (max)
|
|
|
|
// P1 network parameters at maximum
|
|
x[37] = 2.0; // norm_type (None)
|
|
x[38] = 3.0; // activation_type (Mish)
|
|
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
|
|
// Verify P0 parameters
|
|
assert_eq!(params.td_error_clamp_max, 100.0);
|
|
assert_eq!(params.batch_diversity_cooldown, 100.0);
|
|
|
|
// Verify P1 training parameters
|
|
assert_eq!(params.lr_decay_type, 2.0);
|
|
assert_eq!(params.sharpe_weight, 0.5);
|
|
assert_eq!(params.gae_lambda, 0.99);
|
|
assert_eq!(params.noisy_sigma_initial, 0.8);
|
|
assert_eq!(params.noisy_sigma_final, 0.5);
|
|
|
|
// Verify P1 network parameters
|
|
assert_eq!(params.norm_type, 2.0);
|
|
assert_eq!(params.activation_type, 3.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_continuous_wave26_clamping() -> Result<(), MLError> {
|
|
// Test that values outside bounds are clamped
|
|
let mut x = vec![0.0; 39];
|
|
|
|
// Set required parameters
|
|
x[0] = (1e-4_f64).ln();
|
|
x[1] = 128.0;
|
|
x[2] = 0.99;
|
|
x[3] = (100_000_f64).ln();
|
|
|
|
// P0 parameters outside bounds
|
|
x[30] = 200.0; // td_error_clamp_max (should clamp to 100.0)
|
|
x[31] = 5.0; // batch_diversity_cooldown (should clamp to 10.0)
|
|
|
|
// P1 training parameters outside bounds
|
|
x[32] = 5.0; // lr_decay_type (should clamp to 2.0)
|
|
x[33] = 1.0; // sharpe_weight (should clamp to 0.5)
|
|
x[34] = 0.85; // gae_lambda (should clamp to 0.9)
|
|
x[35] = 1.0; // noisy_sigma_initial (should clamp to 0.8)
|
|
x[36] = 0.1; // noisy_sigma_final (should clamp to 0.2)
|
|
|
|
// P1 network parameters outside bounds
|
|
x[37] = 5.0; // norm_type (should clamp to 2.0)
|
|
x[38] = 10.0; // activation_type (should clamp to 3.0)
|
|
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
|
|
// Verify clamping for P0
|
|
assert_eq!(params.td_error_clamp_max, 100.0);
|
|
assert_eq!(params.batch_diversity_cooldown, 10.0);
|
|
|
|
// Verify clamping for P1 training
|
|
assert_eq!(params.lr_decay_type, 2.0);
|
|
assert_eq!(params.sharpe_weight, 0.5);
|
|
assert_eq!(params.gae_lambda, 0.9);
|
|
assert_eq!(params.noisy_sigma_initial, 0.8);
|
|
assert_eq!(params.noisy_sigma_final, 0.2);
|
|
|
|
// Verify clamping for P1 network
|
|
assert_eq!(params.norm_type, 2.0);
|
|
assert_eq!(params.activation_type, 3.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_continuous_wrong_dimension() {
|
|
// Test with wrong number of parameters
|
|
let x = vec![0.0; 30]; // Old dimension
|
|
|
|
let result = DQNParams::from_continuous(&x);
|
|
assert!(result.is_err());
|
|
|
|
if let Err(MLError::ConfigError { reason }) = result {
|
|
assert!(reason.contains("Expected 39"));
|
|
assert!(reason.contains("got 30"));
|
|
} else {
|
|
panic!("Expected ConfigError with dimension mismatch");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_lr_decay_type_rounding() -> Result<(), MLError> {
|
|
// Test that lr_decay_type is properly rounded to integer values
|
|
let mut x = vec![0.0; 39];
|
|
x[0] = (1e-4_f64).ln();
|
|
x[1] = 128.0;
|
|
x[2] = 0.99;
|
|
x[3] = (100_000_f64).ln();
|
|
|
|
// Test rounding to 0 (constant)
|
|
x[32] = 0.4;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.lr_decay_type, 0.0);
|
|
|
|
// Test rounding to 1 (linear)
|
|
x[32] = 0.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.lr_decay_type, 1.0);
|
|
|
|
// Test rounding to 2 (cosine)
|
|
x[32] = 1.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.lr_decay_type, 2.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_activation_type_rounding() -> Result<(), MLError> {
|
|
// Test that activation_type is properly rounded to integer values
|
|
let mut x = vec![0.0; 39];
|
|
x[0] = (1e-4_f64).ln();
|
|
x[1] = 128.0;
|
|
x[2] = 0.99;
|
|
x[3] = (100_000_f64).ln();
|
|
|
|
// Test rounding to 0 (ReLU)
|
|
x[38] = 0.4;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.activation_type, 0.0);
|
|
|
|
// Test rounding to 1 (LeakyReLU)
|
|
x[38] = 0.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.activation_type, 1.0);
|
|
|
|
// Test rounding to 2 (GELU)
|
|
x[38] = 1.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.activation_type, 2.0);
|
|
|
|
// Test rounding to 3 (Mish)
|
|
x[38] = 2.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.activation_type, 3.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_norm_type_rounding() -> Result<(), MLError> {
|
|
// Test that norm_type is properly rounded to integer values
|
|
let mut x = vec![0.0; 39];
|
|
x[0] = (1e-4_f64).ln();
|
|
x[1] = 128.0;
|
|
x[2] = 0.99;
|
|
x[3] = (100_000_f64).ln();
|
|
|
|
// Test rounding to 0 (LayerNorm)
|
|
x[37] = 0.4;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.norm_type, 0.0);
|
|
|
|
// Test rounding to 1 (RMSNorm)
|
|
x[37] = 0.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.norm_type, 1.0);
|
|
|
|
// Test rounding to 2 (None)
|
|
x[37] = 1.6;
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert_eq!(params.norm_type, 2.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_noisy_sigma_range_validation() -> Result<(), MLError> {
|
|
// Verify that noisy_sigma_final <= noisy_sigma_initial makes sense
|
|
let mut x = vec![0.0; 39];
|
|
x[0] = (1e-4_f64).ln();
|
|
x[1] = 128.0;
|
|
x[2] = 0.99;
|
|
x[3] = (100_000_f64).ln();
|
|
|
|
// Set initial > final (expected behavior)
|
|
x[35] = 0.7; // noisy_sigma_initial
|
|
x[36] = 0.3; // noisy_sigma_final
|
|
|
|
let params = DQNParams::from_continuous(&x)?;
|
|
assert!(params.noisy_sigma_initial > params.noisy_sigma_final);
|
|
|
|
Ok(())
|
|
}
|