#![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, unused_crate_dependencies, )] //! Integration test: 4-model inference ensemble (DQN + PPO + Mamba2 + TFT) //! //! Constructs all four adapters with small configs, pre-warms the //! sequence-based models (Mamba2, TFT) by feeding feature vectors into //! their buffers, then runs the full ensemble and verifies bounded //! predictions from all 4 models. use ml::dqn::DQNConfig; use ml::ensemble::adapters::{ DqnInferenceAdapter, Mamba2InferenceAdapter, PpoInferenceAdapter, TftInferenceAdapter, }; use ml::ensemble::inference_ensemble::InferenceEnsemble; use ml::ensemble::{FeatureVector, ModelInferenceAdapter}; use ml::mamba::Mamba2Config; use ml::ppo::PPOConfig; use ml::tft::TFTConfig; /// Small DQN config -- 51 input features, 45 actions, two small hidden layers. fn small_dqn_config() -> DQNConfig { DQNConfig { state_dim: 51, num_actions: 45, hidden_dims: vec![64, 64], ..Default::default() } } /// Small PPO config -- 64 input (zero-padded from 51), 45 actions, small MLPs. fn small_ppo_config() -> PPOConfig { PPOConfig { state_dim: 64, num_actions: 45, policy_hidden_dims: vec![64, 64], value_hidden_dims: vec![64, 64], ..Default::default() } } /// Small Mamba2 config -- d_model=64 (>51 features), 1 layer, seq_len=4. fn small_mamba2_config() -> Mamba2Config { Mamba2Config { d_model: 64, d_state: 16, d_head: 16, num_heads: 2, expand: 2, num_layers: 1, max_seq_len: 8, dropout: 0.0, ..Default::default() } } /// Small TFT config -- input_dim=20, 1 layer, seq_len=4. /// input_dim = num_static(6) + num_known(6) + num_unknown(8) = 20 fn small_tft_config() -> TFTConfig { TFTConfig { input_dim: 20, hidden_dim: 32, num_heads: 2, num_layers: 1, prediction_horizon: 5, sequence_length: 4, num_quantiles: 9, num_static_features: 6, num_known_features: 6, num_unknown_features: 8, dropout_rate: 0.0, ..Default::default() } } use tracing::info; const SEQ_LEN: usize = 4; const NUM_FEATURE_VECTORS: usize = 5; /// Build a 51-dimensional feature vector with a distinguishing pattern. fn make_feature_vector(index: usize) -> FeatureVector { let base = 0.01 * (index as f64 + 1.0); FeatureVector { values: vec![base; 51], timestamp: 1_700_000_000_000_000 + index as i64, } } #[test] fn test_four_model_ensemble_integration() { // ---- construct all 4 adapters ---- let dqn = DqnInferenceAdapter::new(small_dqn_config()) .expect("DqnInferenceAdapter::new should succeed with small config"); let ppo = PpoInferenceAdapter::new(small_ppo_config()) .expect("PpoInferenceAdapter::new should succeed with small config"); let mamba2 = Mamba2InferenceAdapter::new(small_mamba2_config(), SEQ_LEN) .expect("Mamba2InferenceAdapter::new should succeed with small config"); let tft = TftInferenceAdapter::new(small_tft_config(), SEQ_LEN) .expect("TftInferenceAdapter::new should succeed with small config"); // ---- DQN + PPO are ready immediately; Mamba2 + TFT need buffer fill ---- assert!(dqn.is_ready(), "DQN should be ready immediately"); assert!(ppo.is_ready(), "PPO should be ready immediately"); assert!( !mamba2.is_ready(), "Mamba2 should NOT be ready (empty buffer)" ); assert!(!tft.is_ready(), "TFT should NOT be ready (empty buffer)"); // ---- pre-warm sequence-based adapters (Mamba2, TFT) ---- // The ensemble only calls predict on ready adapters, so we must fill // the internal sequence buffers before assembling the ensemble. for i in 0..SEQ_LEN { let fv = make_feature_vector(i); let mamba2_pred = mamba2 .predict(&fv) .unwrap_or_else(|e| panic!("Mamba2 warm-up {} should not error: {}", i, e)); let tft_pred = tft .predict(&fv) .unwrap_or_else(|e| panic!("TFT warm-up {} should not error: {}", i, e)); // Before the buffer is full, direction and confidence should be neutral (0.0) if i < SEQ_LEN - 1 { assert_eq!( mamba2_pred.direction, 0.0, "Mamba2 should return neutral direction during warm-up (step {})", i ); assert_eq!( tft_pred.direction, 0.0, "TFT should return neutral direction during warm-up (step {})", i ); } else { // Buffer is now full -- should produce real predictions assert!( mamba2_pred.direction >= -1.0 && mamba2_pred.direction <= 1.0, "Mamba2 warm-up final: direction {} out of [-1, 1]", mamba2_pred.direction ); assert!( tft_pred.direction >= -1.0 && tft_pred.direction <= 1.0, "TFT warm-up final: direction {} out of [-1, 1]", tft_pred.direction ); } info!( i, mamba2_direction = mamba2_pred.direction, mamba2_confidence = mamba2_pred.confidence, tft_direction = tft_pred.direction, tft_confidence = tft_pred.confidence, "Warm-up step" ); } // All 4 adapters should now be ready assert!(mamba2.is_ready(), "Mamba2 should be ready after warm-up"); assert!(tft.is_ready(), "TFT should be ready after warm-up"); // ---- build the ensemble with all 4 pre-warmed adapters ---- let adapters: Vec> = vec![Box::new(dqn), Box::new(ppo), Box::new(mamba2), Box::new(tft)]; let ensemble = InferenceEnsemble::new(adapters); // All 4 models should be ready assert_eq!( ensemble.ready_count(), 4, "All 4 adapters should be ready after warm-up" ); // ---- feed 5 feature vectors through the full ensemble ---- for i in 0..NUM_FEATURE_VECTORS { let fv = make_feature_vector(SEQ_LEN + i); let pred = ensemble .predict(&fv) .unwrap_or_else(|e| panic!("Ensemble prediction {} should succeed: {}", i, e)); // Direction must be in [-1.0, 1.0] assert!( pred.direction >= -1.0 && pred.direction <= 1.0, "FV {}: direction {} out of [-1, 1]", i, pred.direction ); // Confidence must be in [0.0, 1.0] assert!( pred.confidence >= 0.0 && pred.confidence <= 1.0, "FV {}: confidence {} out of [0, 1]", i, pred.confidence ); // All 4 models should contribute (ensemble name includes all) assert!( pred.model_name.contains("DQN"), "FV {}: ensemble name should include DQN, got: {}", i, pred.model_name ); assert!( pred.model_name.contains("PPO"), "FV {}: ensemble name should include PPO, got: {}", i, pred.model_name ); assert!( pred.model_name.contains("MAMBA-2"), "FV {}: ensemble name should include MAMBA-2, got: {}", i, pred.model_name ); assert!( pred.model_name.contains("TFT"), "FV {}: ensemble name should include TFT, got: {}", i, pred.model_name ); info!( i, direction = pred.direction, confidence = pred.confidence, model_name = %pred.model_name, "Ensemble prediction" ); } // ---- confirm ready_count is still 4 after all predictions ---- assert_eq!( ensemble.ready_count(), 4, "All 4 adapters should remain ready after {} ensemble predictions", NUM_FEATURE_VECTORS ); info!("All 4-model ensemble integration checks passed"); }