#![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, )] //! Comprehensive edge case tests for ML model training loops //! //! This module tests edge cases across DQN, PPO, and Liquid Neural Networks: //! - Training iteration with NaN/Inf loss values //! - Gradient explosion/vanishing scenarios //! - Convergence check boundary conditions //! - Batch size edge cases (size=1, size=max) //! - Learning rate edge cases (zero, negative, very large) //! - Training loop early stopping conditions //! - Model state save/load during training //! //! Target: +20% coverage increase for ml crate #![allow(unused_crate_dependencies)] use common::trading::MarketRegime; use ml_core::common::action::{ExposureLevel, FactoredAction, OrderType, Urgency}; use ml::liquid::network::{LiquidNetwork, OutputLayerConfig}; use ml::liquid::training::{LiquidTrainer, LiquidTrainingConfig, TrainingBatch, TrainingSample}; use ml::liquid::{ActivationType, FixedPoint, LiquidNetworkConfig, NetworkType, PRECISION}; use ml::ppo::ppo::{PPOConfig, PPO}; use ml::ppo::trajectories::{Trajectory, TrajectoryBatch, TrajectoryStep}; // ============================================================================ // PPO Training Edge Cases // ============================================================================ #[tokio::test] async fn test_ppo_training_with_empty_trajectory_batch() -> Result<(), Box> { let config = PPOConfig::default(); let mut ppo = PPO::new(config)?; let mut empty_batch = TrajectoryBatch::from_trajectories(vec![], vec![], vec![]); // Training with empty batch should handle gracefully let result = ppo.update(&mut empty_batch); // Empty batch should either succeed with zero updates or fail gracefully match result { Ok((policy_loss, value_loss)) => { assert!(policy_loss.is_finite()); assert!(value_loss.is_finite()); }, Err(_) => { // Empty batch error is acceptable }, } Ok(()) } #[tokio::test] async fn test_ppo_training_with_single_step_trajectory() -> Result<(), Box> { let config = PPOConfig { state_dim: 2, mini_batch_size: 1, batch_size: 1, ..Default::default() }; let _ppo = PPO::new(config)?; // Create trajectory with single step let mut trajectory = Trajectory::new(); trajectory.add_step(TrajectoryStep::new( vec![1.0, 2.0], FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), -0.5, 10.0, 1.0, true, )); assert_eq!(trajectory.steps.len(), 1); Ok(()) } #[tokio::test] async fn test_ppo_training_with_all_terminal_states() -> Result<(), Box> { let config = PPOConfig { state_dim: 2, ..Default::default() }; let _ppo = PPO::new(config)?; // Create trajectory with all terminal states let mut trajectory = Trajectory::new(); for i in 0..10 { trajectory.add_step(TrajectoryStep::new( vec![i as f32, i as f32 + 1.0], FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), -0.5, 5.0, 0.0, true, // All steps are terminal )); } // Compute returns with all terminal states let returns = trajectory.compute_returns(0.99); assert_eq!(returns.len(), 10); // All returns should be zero since all states are terminal for ret in returns { assert_eq!(ret, 0.0); } Ok(()) } #[tokio::test] async fn test_ppo_clip_epsilon_boundary() -> Result<(), Box> { // Test with very small epsilon (should still clip) let config_small = PPOConfig { clip_epsilon: 0.001, ..Default::default() }; let _ppo_small = PPO::new(config_small)?; // Test with large epsilon (less aggressive clipping) let config_large = PPOConfig { clip_epsilon: 0.9, ..Default::default() }; let _ppo_large = PPO::new(config_large)?; Ok(()) } #[tokio::test] async fn test_ppo_zero_entropy_coefficient() -> Result<(), Box> { let config = PPOConfig { entropy_coeff: 0.0, ..Default::default() }; let _ppo = PPO::new(config)?; // PPO with zero entropy should work (deterministic policy) Ok(()) } #[tokio::test] async fn test_ppo_high_entropy_coefficient() -> Result<(), Box> { let config = PPOConfig { entropy_coeff: 1.0, // Very high entropy = more exploration ..Default::default() }; let _ppo = PPO::new(config)?; Ok(()) } #[tokio::test] async fn test_ppo_training_epochs_boundary() -> Result<(), Box> { // Test with 1 epoch let config_single = PPOConfig { num_epochs: 1, ..Default::default() }; let _ppo_single = PPO::new(config_single)?; // Test with many epochs let config_many = PPOConfig { num_epochs: 100, ..Default::default() }; let _ppo_many = PPO::new(config_many)?; Ok(()) } // ============================================================================ // Liquid Neural Network Training Edge Cases // ============================================================================ #[tokio::test] async fn test_liquid_training_with_nan_loss() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { batch_size: 2, max_epochs: 5, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); // Create batch with extreme values that could cause NaN let inputs = vec![ vec![FixedPoint(i64::MAX / 2), FixedPoint(i64::MAX / 2)], vec![FixedPoint(i64::MIN / 2), FixedPoint(i64::MIN / 2)], ]; let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]]; let batch = TrainingBatch::from_arrays(&inputs, &targets)?; let training_data = vec![batch]; // Training should handle extreme values gracefully let result = trainer.train(&mut network, &training_data, None); // Either succeeds or fails with TrainingError (not panic) match result { Ok(_) => {}, Err(ml::liquid::LiquidError::TrainingError(_)) => {}, Err(e) => panic!("Unexpected error type: {:?}", e), } Ok(()) } #[tokio::test] async fn test_liquid_training_with_zero_learning_rate() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { learning_rate: FixedPoint(0), // Zero learning rate batch_size: 2, max_epochs: 3, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); let inputs = vec![ vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)], vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)], ]; let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]]; let batch = TrainingBatch::from_arrays(&inputs, &targets)?; let training_data = vec![batch]; // Training with zero learning rate should succeed but not learn trainer.train(&mut network, &training_data, None)?; // Learning rate should remain zero assert_eq!(trainer.get_current_learning_rate(), 0.0); Ok(()) } #[tokio::test] async fn test_liquid_training_early_stopping() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { batch_size: 2, max_epochs: 100, early_stopping_patience: 3, // Stop if no improvement for 3 epochs ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); // Training data let train_inputs = vec![ vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)], vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)], ]; let train_targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]]; let train_batch = TrainingBatch::from_arrays(&train_inputs, &train_targets)?; // Validation data let val_inputs = vec![vec![FixedPoint(PRECISION / 6), FixedPoint(PRECISION / 7)]]; let val_targets = vec![vec![FixedPoint(PRECISION / 3)]]; let val_batch = TrainingBatch::from_arrays(&val_inputs, &val_targets)?; let training_data = vec![train_batch]; let validation_data = vec![val_batch]; trainer.train(&mut network, &training_data, Some(&validation_data))?; // Training should have stopped early (less than max epochs) let history = trainer.get_training_history(); assert!( history.len() < 100, "Early stopping should trigger before max epochs" ); Ok(()) } #[tokio::test] async fn test_liquid_training_batch_size_one() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { batch_size: 1, // Single sample per batch max_epochs: 5, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); let inputs = vec![vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)]]; let targets = vec![vec![FixedPoint(PRECISION)]]; let batch = TrainingBatch::from_arrays(&inputs, &targets)?; let training_data = vec![batch]; // Training with batch size 1 should succeed trainer.train(&mut network, &training_data, None)?; Ok(()) } #[tokio::test] async fn test_liquid_training_gradient_clipping() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { gradient_clip_threshold: FixedPoint(PRECISION / 10), // Small threshold = aggressive clipping batch_size: 2, max_epochs: 3, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); let inputs = vec![ vec![FixedPoint(PRECISION), FixedPoint(PRECISION)], vec![FixedPoint(-PRECISION), FixedPoint(-PRECISION)], ]; let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]]; let batch = TrainingBatch::from_arrays(&inputs, &targets)?; let training_data = vec![batch]; // Training should apply gradient clipping trainer.train(&mut network, &training_data, None)?; Ok(()) } #[tokio::test] async fn test_liquid_training_adaptive_learning_rate() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { learning_rate: FixedPoint(PRECISION / 100), // 0.01 adaptive_learning_rate: true, batch_size: 2, max_epochs: 50, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); let initial_lr = trainer.get_current_learning_rate(); let inputs = vec![ vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)], vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)], ]; let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]]; let batch = TrainingBatch::from_arrays(&inputs, &targets)?; let training_data = vec![batch]; trainer.train(&mut network, &training_data, None)?; let final_lr = trainer.get_current_learning_rate(); // Learning rate should have decayed assert!(final_lr < initial_lr, "Adaptive learning rate should decay"); Ok(()) } #[tokio::test] async fn test_liquid_training_market_regime_adaptation() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { market_regime_adaptation: true, batch_size: 2, max_epochs: 5, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); // Create samples with different market regimes let sample1 = TrainingSample { input: vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)], target: vec![FixedPoint(PRECISION)], timestamp: Some(1000), market_regime: Some(MarketRegime::Trending), volatility: Some(FixedPoint(PRECISION * 2)), // High volatility }; let sample2 = TrainingSample { input: vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)], target: vec![FixedPoint(PRECISION / 2)], timestamp: Some(2000), market_regime: Some(MarketRegime::Sideways), volatility: Some(FixedPoint(PRECISION / 10)), // Low volatility }; let batch = TrainingBatch::new(vec![sample1, sample2]); let training_data = vec![batch]; // Training should adapt to market regimes trainer.train(&mut network, &training_data, None)?; Ok(()) } #[tokio::test] async fn test_liquid_training_l2_regularization() -> Result<(), Box> { let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 2, output_size: 1, layer_configs: vec![], output_layer: OutputLayerConfig { use_linear_output: true, output_activation: Some(ActivationType::Linear), dropout_rate: None, }, default_dt: FixedPoint::from_f64(0.01), market_regime_adaptation: false, }; let mut network = LiquidNetwork::new(network_config)?; let training_config = LiquidTrainingConfig { l2_regularization: FixedPoint(PRECISION / 100), // 0.01 L2 penalty batch_size: 2, max_epochs: 5, ..Default::default() }; let mut trainer = LiquidTrainer::new(training_config); let inputs = vec![ vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)], vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)], ]; let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]]; let batch = TrainingBatch::from_arrays(&inputs, &targets)?; let training_data = vec![batch]; // Training with L2 regularization should succeed trainer.train(&mut network, &training_data, None)?; Ok(()) }