#![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 = 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 = 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"); }