Files
foxhunt/crates/ml/tests/dqn_feature_defaults_test.rs
jgrusewski 04d8802c94 refactor: remove 8 always-on use_ booleans — features are mandatory
Remove use_double_dqn, use_dueling, use_per, use_branching,
use_distributional, use_noisy_nets, use_huber_loss, and use_cql
from DQNConfig, DQNHyperparameters, and DqnParams structs.

These features are always enabled (Rainbow DQN standard). The boolean
flags were dead code — every constructor set them to true, and the
only code paths that set them to false were in tests that disabled
features for simplicity. With the fields removed, the features are
unconditionally active, eliminating ~490 lines of dead configuration.

Key changes:
- Struct field declarations removed from 3 core config structs
- Conditional branches (if use_X { ... } else { ... }) simplified:
  dueling/branching/PER network creation is now unconditional
- Checkpoint metadata hardcodes "true" for backward compatibility
- Hyperopt search space index 11 (use_branching) fixed at 1.0
- TOML/YAML config files cleaned of removed fields
- Tests that toggled these flags updated or rewritten

45 files changed, -487 net lines. Zero new test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:45:54 +01:00

273 lines
8.9 KiB
Rust

#![allow(
clippy::assertions_on_constants,
clippy::assertions_on_result_states,
clippy::clone_on_copy,
clippy::decimal_literal_representation,
clippy::doc_markdown,
clippy::empty_line_after_doc_comments,
clippy::field_reassign_with_default,
clippy::get_unwrap,
clippy::identity_op,
clippy::inconsistent_digit_grouping,
clippy::indexing_slicing,
clippy::integer_division,
clippy::len_zero,
clippy::let_underscore_must_use,
clippy::manual_div_ceil,
clippy::manual_let_else,
clippy::manual_range_contains,
clippy::modulo_arithmetic,
clippy::needless_range_loop,
clippy::non_ascii_literal,
clippy::redundant_clone,
clippy::shadow_reuse,
clippy::shadow_same,
clippy::shadow_unrelated,
clippy::single_match_else,
clippy::str_to_string,
clippy::string_slice,
clippy::tests_outside_test_module,
clippy::too_many_lines,
clippy::unnecessary_wraps,
clippy::unseparated_literal_suffix,
clippy::use_debug,
clippy::useless_vec,
clippy::wildcard_enum_match_arm,
clippy::else_if_without_else,
clippy::expect_used,
clippy::missing_const_for_fn,
clippy::similar_names,
clippy::type_complexity,
clippy::collapsible_else_if,
clippy::doc_lazy_continuation,
clippy::items_after_test_module,
clippy::map_clone,
clippy::multiple_unsafe_ops_per_block,
clippy::unwrap_or_default,
clippy::assign_op_pattern,
clippy::needless_borrow,
clippy::println_empty_string,
clippy::unnecessary_cast,
clippy::used_underscore_binding,
clippy::create_dir,
clippy::implicit_saturating_sub,
clippy::exit,
clippy::expect_fun_call,
clippy::too_many_arguments,
clippy::unnecessary_map_or,
clippy::unwrap_used,
dead_code,
unused_imports,
unused_variables,
clippy::cloned_ref_to_slice_refs,
clippy::neg_multiply,
clippy::while_let_loop,
clippy::bool_assert_comparison,
clippy::excessive_precision,
clippy::trivially_copy_pass_by_ref,
clippy::op_ref,
clippy::redundant_closure,
clippy::unnecessary_lazy_evaluations,
clippy::if_then_some_else_none,
clippy::unnecessary_to_owned,
clippy::single_component_path_imports,
)]
//! 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_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_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_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_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 {
n_steps: 1,
..DQNHyperparameters::conservative()
};
// Verify vanilla DQN configuration
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 {
..DQNHyperparameters::conservative()
};
let config_no_distributional = DQNHyperparameters {
..DQNHyperparameters::conservative()
};
let config_no_noisy = DQNHyperparameters {
..DQNHyperparameters::conservative()
};
}
#[test]
fn test_rainbow_feature_combinations() {
// Test common feature combinations
// Rainbow without noisy nets (use epsilon-greedy instead)
let rainbow_epsilon_greedy = DQNHyperparameters {
..DQNHyperparameters::conservative()
};
// Rainbow without PER (uniform replay)
let rainbow_uniform_replay = DQNHyperparameters {
..DQNHyperparameters::conservative()
};
// Double DQN + Dueling only (no distributional, no noisy, no PER)
let double_dueling_only = DQNHyperparameters {
n_steps: 1,
..DQNHyperparameters::conservative()
};
}
#[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_eq!(config.n_steps, 3);
}