#![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 Uncertainty Fields in Hyperopt //! //! Validates that ensemble uncertainty parameters are properly set as fixed defaults //! in the current 39D search space (ensemble params were removed from search space, //! they are now fixed in from_continuous). use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::ParameterSpace; use ml::trainers::dqn::DQNHyperparameters; #[test] fn test_dqn_hyperparameters_has_ensemble_fields() { // WAVE 26 P1.4: Verify DQNHyperparameters struct has 6 new ensemble fields let hyperparams = DQNHyperparameters::conservative(); // Check default values exist (compilation test - if fields don't exist, this won't compile) 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; // Verify conservative defaults are reasonable 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_fields() { // WAVE 26 P1.4: Verify DQNParams struct has 5 new ensemble fields let params = DQNParams::default(); // Check default values exist (compilation test) let _use_ensemble = params.use_ensemble_uncertainty; let _ensemble_size = params.ensemble_size; let _beta_variance = params.beta_variance; let _beta_disagreement = params.beta_disagreement; let _beta_entropy = params.beta_entropy; // Verify defaults match conservative config assert!(!params.use_ensemble_uncertainty, "Default should be disabled"); assert_eq!(params.ensemble_size, 5.0, "Default ensemble size should be 5.0"); assert!(params.beta_variance > 0.0, "Beta variance should be positive"); assert!(params.beta_disagreement > 0.0, "Beta disagreement should be positive"); assert!(params.beta_entropy > 0.0, "Beta entropy should be positive"); } #[test] fn test_ensemble_search_space_bounds() { // Ensemble params (ensemble_size, beta_variance, etc.) are no longer in the // continuous search space — they are fixed in from_continuous(). Verify that // the 39D search space is properly sized and ensemble defaults are set. let bounds = DQNParams::continuous_bounds(); assert_eq!(bounds.len(), 39, "Search space should have 39 continuous parameters total (C8: +8 reward/policy weights)"); // Ensemble params are NOT in bounds — they're fixed in from_continuous. // Verify a DQNParams default still has correct ensemble defaults. let params = DQNParams::default(); assert!(!params.use_ensemble_uncertainty, "Default: ensemble disabled"); assert_eq!(params.ensemble_size, 5.0, "Default ensemble size should be 5.0"); assert!(params.beta_variance > 0.0, "Beta variance should be positive"); assert!(params.beta_disagreement > 0.0, "Beta disagreement should be positive"); assert!(params.beta_entropy > 0.0, "Beta entropy should be positive"); } #[test] fn test_from_continuous_validates_ensemble_params() { // Ensemble params are now fixed in from_continuous() (not in 39D search space). // Verify that from_continuous produces correct fixed ensemble defaults. let bounds = DQNParams::continuous_bounds(); let x: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); assert_eq!(x.len(), 39, "Should produce 39D midpoint vector"); let params = DQNParams::from_continuous(&x).expect("Should convert valid parameters"); // Ensemble parameters are fixed to conservative defaults assert_eq!(params.ensemble_size, 5.0, "Ensemble size should be fixed to 5.0"); assert_eq!(params.beta_variance, 0.5, "Beta variance should be fixed to 0.5"); assert_eq!(params.beta_disagreement, 0.5, "Beta disagreement should be fixed to 0.5"); assert_eq!(params.beta_entropy, 0.2, "Beta entropy should be fixed to 0.2"); assert!(!params.use_ensemble_uncertainty, "Ensemble uncertainty should be disabled"); } #[test] fn test_from_continuous_clamps_ensemble_params() { // Ensemble parameters are now fixed (not in search space), so clamping // is not relevant. Instead verify that from_continuous rejects wrong-length input. let x = vec![0.0; 40]; // Wrong length (should be 39) let result = DQNParams::from_continuous(&x); assert!(result.is_err(), "Should reject wrong-length (40) input"); let x2 = vec![0.0; 31]; // Also wrong length let result2 = DQNParams::from_continuous(&x2); assert!(result2.is_err(), "Should reject wrong-length (31) input"); // Correct length (39) with valid midpoints should succeed let bounds = DQNParams::continuous_bounds(); let x38: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); let params = DQNParams::from_continuous(&x38).expect("Should accept 39D input"); // Ensemble params are fixed regardless of input assert_eq!(params.ensemble_size, 5.0, "Ensemble size fixed to 5.0"); } #[test] fn test_to_continuous_includes_reward_weights() { // C8: Verify that to_continuous() includes 7 composite reward weights (indices 31-37) let mut params = DQNParams::default(); // Set reward weights to specific values params.w_dsr = 1.5; params.w_pnl = 0.5; params.w_dd = 2.0; params.w_idle = 0.05; params.dd_threshold = 0.04; params.loss_aversion = 2.0; params.time_decay_rate = 0.001; let continuous = params.to_continuous(); assert_eq!(continuous.len(), 39, "Continuous representation should have 39 values"); // Verify reward weight positions (indices 31-37) assert_eq!(continuous[31], 1.5, "w_dsr should be at position 31"); assert_eq!(continuous[32], 0.5, "w_pnl should be at position 32"); assert_eq!(continuous[33], 2.0, "w_dd should be at position 33"); assert_eq!(continuous[34], 0.05, "w_idle should be at position 34"); assert_eq!(continuous[35], 0.04, "dd_threshold should be at position 35"); assert_eq!(continuous[36], 2.0, "loss_aversion should be at position 36"); assert_eq!(continuous[37], 0.001, "time_decay_rate should be at position 37"); // Ensemble params are NOT in the continuous vector (they're fixed) // Verify the vector doesn't include them assert_eq!(continuous.len(), 39); } #[test] fn test_ensemble_size_fixed_in_from_continuous() { // Ensemble size is now fixed at 5.0 in from_continuous() — not in search space. // Verify that from_continuous always produces ensemble_size=5.0 regardless of input. let bounds = DQNParams::continuous_bounds(); let x: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); let params = DQNParams::from_continuous(&x).expect("Should convert"); assert_eq!(params.ensemble_size, 5.0, "Ensemble size should always be fixed to 5.0"); assert!(!params.use_ensemble_uncertainty, "Ensemble uncertainty should be disabled"); }