Files
foxhunt/crates/ml/tests/ensemble_hyperopt_tests.rs
jgrusewski 3d7d9c3d03 fix: update 9 test files for 14D DQNParams restructure
Tests referenced old DQNParams fields (learning_rate, batch_size,
ensemble_size, etc.) that were absorbed into family intensity scalars.
Rewrote all affected tests to validate the 14D search space layout,
intensity bounds [0.0, 2.0], and round-trip serialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 01:30:33 +02:00

207 lines
7.8 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,
)]
//! TDD Tests for Ensemble Intensity in the 14D Hyperopt Search Space
//!
//! Validates that ensemble_intensity is properly included in the 14D search space
//! and that from_continuous / to_continuous round-trip correctly.
//! Individual ensemble params (ensemble_size, beta_variance, etc.) are now fixed
//! in DQNHyperparameters, not in DQNParams.
use ml::hyperopt::adapters::dqn::DQNParams;
use ml::hyperopt::ParameterSpace;
use ml::trainers::dqn::DQNHyperparameters;
#[test]
fn test_dqn_hyperparameters_has_ensemble_fields() {
// Verify DQNHyperparameters struct has ensemble fields (these live here, not in DQNParams)
let hyperparams = DQNHyperparameters::conservative();
let _use_ensemble = hyperparams.use_ensemble_uncertainty;
let _ensemble_size = hyperparams.ensemble_size;
let _beta_variance = hyperparams.beta_variance;
let _beta_disagreement = hyperparams.beta_disagreement;
let _beta_entropy = hyperparams.beta_entropy;
let _variance_cap = hyperparams.variance_cap;
assert!(!hyperparams.use_ensemble_uncertainty, "Default should be disabled for conservative config");
assert_eq!(hyperparams.ensemble_size, 5, "Default ensemble size should be 5");
assert!(hyperparams.beta_variance >= 0.0 && hyperparams.beta_variance <= 1.0, "Beta variance should be in [0, 1]");
assert!(hyperparams.beta_disagreement >= 0.0 && hyperparams.beta_disagreement <= 1.0, "Beta disagreement should be in [0, 1]");
assert!(hyperparams.beta_entropy >= 0.0 && hyperparams.beta_entropy <= 1.0, "Beta entropy should be in [0, 1]");
assert!(hyperparams.variance_cap > 0.0, "Variance cap should be positive");
}
#[test]
fn test_dqn_params_has_ensemble_intensity() {
// DQNParams now has ensemble_intensity (a single scalar) instead of individual ensemble fields
let params = DQNParams::default();
assert!(
params.ensemble_intensity >= 0.0 && params.ensemble_intensity <= 2.0,
"ensemble_intensity should be in [0.0, 2.0], got {}",
params.ensemble_intensity
);
assert!(
(params.ensemble_intensity - 1.0).abs() < 1e-6,
"Default ensemble_intensity should be 1.0 (neutral)"
);
}
#[test]
fn test_ensemble_search_space_bounds() {
// 14D search space: 3 breakouts + 5 core families + 6 gen families
let bounds = DQNParams::continuous_bounds();
assert_eq!(bounds.len(), 14, "Search space should have 14 continuous parameters");
// ensemble_intensity is at index 12
let names = DQNParams::param_names();
let ens_idx = names.iter().position(|&n| n == "ensemble_intensity")
.expect("ensemble_intensity should be in param_names");
assert_eq!(ens_idx, 12, "ensemble_intensity should be at index 12");
let (lo, hi) = bounds[ens_idx];
assert_eq!(lo, 0.0, "ensemble_intensity lower bound should be 0.0");
assert_eq!(hi, 2.0, "ensemble_intensity upper bound should be 2.0");
}
#[test]
fn test_from_continuous_sets_ensemble_intensity() {
// from_continuous with midpoint values
let bounds = DQNParams::continuous_bounds();
let x: Vec<f64> = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect();
assert_eq!(x.len(), 14, "Should produce 14D midpoint vector");
let params = DQNParams::from_continuous(&x).expect("Should convert valid parameters");
// ensemble_intensity should be the midpoint of [0.0, 2.0] = 1.0
assert!(
(params.ensemble_intensity - 1.0).abs() < 1e-6,
"ensemble_intensity should be 1.0 at midpoint, got {}",
params.ensemble_intensity
);
}
#[test]
fn test_from_continuous_rejects_wrong_length() {
// from_continuous rejects wrong-length input
let x = vec![0.0; 15]; // Wrong length (should be 14)
let result = DQNParams::from_continuous(&x);
assert!(result.is_err(), "Should reject wrong-length (15) input");
let x2 = vec![0.0; 13]; // Also wrong length
let result2 = DQNParams::from_continuous(&x2);
assert!(result2.is_err(), "Should reject wrong-length (13) input");
// Correct length (14) with valid midpoints should succeed
let bounds = DQNParams::continuous_bounds();
let x14: Vec<f64> = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect();
let params = DQNParams::from_continuous(&x14).expect("Should accept 14D input");
assert!(
(params.ensemble_intensity - 1.0).abs() < 1e-6,
"ensemble_intensity should be 1.0 at midpoint"
);
}
#[test]
fn test_to_continuous_roundtrip() {
// Verify to_continuous includes all 14 fields and round-trips correctly
let mut params = DQNParams::default();
params.ensemble_intensity = 1.5;
params.causal_intensity = 0.8;
let continuous = params.to_continuous();
assert_eq!(continuous.len(), 14, "Continuous representation should have 14 values");
let recovered = DQNParams::from_continuous(&continuous).expect("Should recover from continuous");
assert!(
(recovered.ensemble_intensity - 1.5).abs() < 1e-6,
"ensemble_intensity should survive roundtrip: expected 1.5, got {}",
recovered.ensemble_intensity
);
assert!(
(recovered.causal_intensity - 0.8).abs() < 1e-6,
"causal_intensity should survive roundtrip: expected 0.8, got {}",
recovered.causal_intensity
);
}
#[test]
fn test_ensemble_intensity_in_param_names() {
// Verify ensemble_intensity is listed in param_names at position 12
let names = DQNParams::param_names();
assert_eq!(names.len(), 14, "param_names should have 14 entries");
assert_eq!(names[12], "ensemble_intensity", "Index 12 should be ensemble_intensity");
assert_eq!(names[13], "causal_intensity", "Index 13 should be causal_intensity");
}