#![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, )] //! Liquid NN Training Pipeline TDD Test Suite //! //! Comprehensive E2E tests for Liquid Neural Network training with CPU-only //! fixed-point arithmetic. Tests cover forward/backward passes, training loop //! convergence, checkpoint persistence, inference determinism, and memory usage. //! //! Architecture: //! - CPU-ONLY (fixed-point arithmetic for <100μs inference) //! - No CUDA dependencies (by design, not a limitation) //! - Fixed-point precision: 8 decimal places (PRECISION = 100_000_000) //! - Training: CPU-based gradient descent with MSE loss //! - Inference: Deterministic fixed-point computation //! //! Test Coverage: //! 1. Forward pass: Fixed-point computation correctness //! 2. Backward pass: Gradient calculation (CPU only) //! 3. Training loop: Loss convergence over epochs //! 4. Checkpoint save/load: Safetensors persistence //! 5. Inference determinism: Same input → same output //! 6. Memory usage: CPU memory within limits #![allow(unused_crate_dependencies)] use ml::liquid::{ ActivationType, FixedPoint, LTCConfig, LayerConfig, LiquidNetwork, LiquidNetworkConfig, LiquidTrainer, LiquidTrainingConfig, NetworkType, OutputLayerConfig, SolverType, TrainingBatch, TrainingSample, TrainingUtils, PRECISION, }; use std::time::Instant; use tracing::info; // ============================================================================ // Test 1: Forward Pass - Fixed-Point Computation // ============================================================================ #[test] fn test_liquid_nn_forward_pass() -> anyhow::Result<()> { info!("=== Test 1: Forward Pass - Fixed-Point Computation ==="); // Create minimal Liquid NN (16 input → 8 hidden → 3 output) let ltc_config = LTCConfig { input_size: 16, hidden_size: 8, tau_min: FixedPoint::from_f64(0.1), tau_max: FixedPoint::from_f64(1.0), use_bias: true, solver_type: SolverType::Euler, // Simplest solver for testing activation: ActivationType::Tanh, }; let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 16, output_size: 3, layer_configs: vec![LayerConfig::LTC(ltc_config)], 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)?; info!(parameter_count = network.parameter_count(), "Created network: 16 inputs -> 8 hidden (LTC) -> 3 outputs"); // Create input with fixed-point values let input: Vec = (0..16) .map(|i| FixedPoint::from_f64(0.5 + (i as f64) * 0.01)) .collect(); info!(?input, "Input features (first 5 shown in debug)"); // Forward pass let start = Instant::now(); let output = network.forward(&input)?; let duration = start.elapsed(); info!(?duration, output_len = output.len(), ?output, "Forward pass completed"); // Assertions assert_eq!(output.len(), 3, "Output should have 3 values"); assert!( duration.as_micros() < 1000, "Forward pass should be <1ms (target: <100μs in production)" ); // Verify fixed-point arithmetic correctness for &val in &output { assert!( val.is_finite(), "Output values should be finite (no overflow)" ); } info!("Forward pass completed successfully"); Ok(()) } // ============================================================================ // Test 2: Backward Pass - Gradient Computation (CPU Only) // ============================================================================ #[test] fn test_liquid_nn_backward_pass() -> anyhow::Result<()> { info!("=== Test 2: Backward Pass - Gradient Computation ==="); // Create network let ltc_config = LTCConfig { input_size: 4, hidden_size: 4, tau_min: FixedPoint::from_f64(0.1), tau_max: FixedPoint::from_f64(1.0), use_bias: true, solver_type: SolverType::Euler, activation: ActivationType::Tanh, }; let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 4, output_size: 2, layer_configs: vec![LayerConfig::LTC(ltc_config)], 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.clone())?; let trainer_config = LiquidTrainingConfig::default(); let mut trainer = LiquidTrainer::new(trainer_config); info!("Created network: 4 -> 4 (LTC) -> 2"); // Create training sample let input = vec![ FixedPoint::from_f64(0.5), FixedPoint::from_f64(0.3), FixedPoint::from_f64(0.7), FixedPoint::from_f64(0.2), ]; let target = vec![FixedPoint::one(), FixedPoint::zero()]; let sample = TrainingSample { input: input.clone(), target: target.clone(), timestamp: None, market_regime: None, volatility: None, }; info!(?input, ?target, "Training sample"); // Forward pass to get predictions let predictions = network.forward(&input)?; info!(?predictions, "Predictions before training"); // Calculate loss manually (MSE) let loss_before: f64 = predictions .iter() .zip(target.iter()) .map(|(pred, tgt)| { let diff = pred.to_f64() - tgt.to_f64(); diff * diff }) .sum::() / predictions.len() as f64; info!(loss_before, "Loss before training"); // Train to verify gradient computation (use public train method) let batches = vec![TrainingBatch::new(vec![sample])]; trainer.train(&mut network, &batches, None)?; let history = trainer.get_training_history(); let batch_loss = history.last().map(|m| m.training_loss).unwrap_or(0.0); let gradient_history_len = trainer.gradient_history.len(); info!(batch_loss, gradient_history_len, "Training results"); // Verify gradient was computed assert!( !trainer.gradient_history.is_empty(), "Gradient history should contain gradients after training" ); // Verify gradient is finite let last_gradient = trainer.gradient_history.last().unwrap(); assert!( last_gradient.is_finite(), "Gradient should be finite (no overflow)" ); info!(last_gradient = last_gradient.to_f64(), "Backward pass completed successfully"); Ok(()) } // ============================================================================ // Test 3: Training Loop Convergence - Loss Decreases Over Epochs // ============================================================================ #[test] fn test_training_loop_convergence() -> anyhow::Result<()> { info!("=== Test 3: Training Loop Convergence ==="); // Create small network for fast convergence test let ltc_config = LTCConfig { input_size: 3, hidden_size: 4, tau_min: FixedPoint::from_f64(0.1), tau_max: FixedPoint::from_f64(1.0), use_bias: true, solver_type: SolverType::Euler, activation: ActivationType::Tanh, }; let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 3, output_size: 2, layer_configs: vec![LayerConfig::LTC(ltc_config)], 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)?; info!("Created network: 3 -> 4 (LTC) -> 2"); // Create synthetic training data (simple XOR-like problem) let mut training_samples = Vec::new(); for i in 0..20 { let x = (i % 4) as f64; let input = vec![ FixedPoint::from_f64(x / 4.0), FixedPoint::from_f64((x * 2.0) / 4.0), FixedPoint::from_f64((x * 3.0) / 4.0), ]; let target = if i % 2 == 0 { vec![FixedPoint::one(), FixedPoint::zero()] } else { vec![FixedPoint::zero(), FixedPoint::one()] }; training_samples.push(TrainingSample { input, target, timestamp: None, market_regime: None, volatility: None, }); } info!(sample_count = training_samples.len(), "Created training samples"); // Create batches let batches = TrainingUtils::create_batches(training_samples, 4); info!(batch_count = batches.len(), "Created batches (batch size: 4)"); // Configure training (10 epochs for convergence test) let training_config = LiquidTrainingConfig { learning_rate: FixedPoint(PRECISION / 100), // 0.01 batch_size: 4, max_epochs: 10, early_stopping_patience: 5, gradient_clip_threshold: FixedPoint::one(), l2_regularization: FixedPoint::zero(), adaptive_learning_rate: false, market_regime_adaptation: false, validation_split: 0.0, }; let mut trainer = LiquidTrainer::new(training_config); info!("Training configuration: learning_rate=0.01, max_epochs=10, batch_size=4"); // Train network info!("Starting training"); let start = Instant::now(); trainer.train(&mut network, &batches, None)?; let training_time = start.elapsed(); info!(?training_time, "Training completed"); // Verify loss convergence let history = trainer.get_training_history(); assert!( history.len() >= 2, "Training history should have at least 2 epochs" ); let first_loss = history[0].training_loss; let last_loss = history.last().unwrap().training_loss; info!(first_loss, "Epoch 0 loss"); for (i, metrics) in history.iter().enumerate().skip(1) { info!(epoch = i, loss = metrics.training_loss, "Epoch loss"); } info!(last_loss, "Final loss"); // Assert loss decreased (convergence) assert!( last_loss < first_loss, "Loss should decrease during training (first={:.6}, last={:.6})", first_loss, last_loss ); let loss_reduction = ((first_loss - last_loss) / first_loss) * 100.0; info!(loss_reduction, "Training converged successfully"); Ok(()) } // ============================================================================ // Test 4: Checkpoint Save/Load - Safetensors Persistence // ============================================================================ #[test] fn test_checkpoint_save_load() -> anyhow::Result<()> { info!("=== Test 4: Checkpoint Save/Load ==="); // Create network let ltc_config = LTCConfig { input_size: 5, hidden_size: 6, tau_min: FixedPoint::from_f64(0.1), tau_max: FixedPoint::from_f64(1.0), use_bias: true, solver_type: SolverType::Euler, activation: ActivationType::Tanh, }; let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 5, output_size: 3, layer_configs: vec![LayerConfig::LTC(ltc_config)], 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 network = LiquidNetwork::new(network_config.clone())?; info!("Created original network: 5 -> 6 (LTC) -> 3"); // Create test input let input: Vec = (0..5) .map(|i| FixedPoint::from_f64(0.1 + (i as f64) * 0.1)) .collect(); // Save checkpoint BEFORE running forward pass to preserve initial state info!("Saving checkpoint"); let original_network = network.clone(); let checkpoint_json = serde_json::to_string(&original_network)?; info!(checkpoint_size_bytes = checkpoint_json.len(), "Checkpoint saved"); // Run forward pass on original network let mut original_network_mut = original_network.clone(); let original_output = original_network_mut.forward(&input)?; info!(?original_output, "Original predictions"); // Load checkpoint info!("Loading checkpoint"); let mut loaded_network: LiquidNetwork = serde_json::from_str(&checkpoint_json)?; info!("Checkpoint loaded successfully"); // Verify predictions match let loaded_output = loaded_network.forward(&input)?; info!(?loaded_output, "Loaded predictions"); // Compare outputs for (i, (&orig, &loaded)) in original_output.iter().zip(loaded_output.iter()).enumerate() { let diff = (orig.0 - loaded.0).abs(); info!( output_idx = i, orig = orig.to_f64(), loaded = loaded.to_f64(), diff, "Output comparison" ); assert_eq!( orig, loaded, "Output {} should match exactly after checkpoint reload", i ); } info!("Checkpoint save/load verified (deterministic)"); Ok(()) } // ============================================================================ // Test 5: Inference Determinism - Same Input → Same Output // ============================================================================ #[test] fn test_inference_determinism() -> anyhow::Result<()> { info!("=== Test 5: Inference Determinism ==="); // Create network let ltc_config = LTCConfig { input_size: 8, hidden_size: 8, tau_min: FixedPoint::from_f64(0.1), tau_max: FixedPoint::from_f64(1.0), use_bias: true, solver_type: SolverType::RK4, // Higher-order solver activation: ActivationType::Tanh, }; let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 8, output_size: 4, layer_configs: vec![LayerConfig::LTC(ltc_config)], 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)?; info!("Created network: 8 -> 8 (LTC, RK4) -> 4"); // Create test input let input: Vec = vec![ FixedPoint::from_f64(0.123), FixedPoint::from_f64(0.456), FixedPoint::from_f64(0.789), FixedPoint::from_f64(0.234), FixedPoint::from_f64(0.567), FixedPoint::from_f64(0.890), FixedPoint::from_f64(0.345), FixedPoint::from_f64(0.678), ]; info!("Running 10 inference passes with identical input"); // Run inference 10 times with same input let mut outputs = Vec::new(); for i in 0..10 { // Reset network state before each inference network.reset_states(); let output = network.forward(&input)?; outputs.push(output); if i == 0 { info!(output = ?outputs[0], run = i, "First run output"); } } // Verify all outputs are identical let first_output = &outputs[0]; for (run_idx, output) in outputs.iter().enumerate().skip(1) { for (i, (&expected, &actual)) in first_output.iter().zip(output.iter()).enumerate() { assert_eq!( expected, actual, "Run {} output[{}] should match run 0 (deterministic)", run_idx, i ); } } info!(?first_output, "Inference is deterministic (10/10 runs identical)"); Ok(()) } // ============================================================================ // Test 6: Memory Usage - CPU Memory Within Limits // ============================================================================ #[test] fn test_memory_usage() -> anyhow::Result<()> { info!("=== Test 6: Memory Usage ==="); // Create realistic-sized network (16 → 128 → 3) let ltc_config = LTCConfig { input_size: 16, hidden_size: 128, tau_min: FixedPoint::from_f64(0.1), tau_max: FixedPoint::from_f64(1.0), use_bias: true, solver_type: SolverType::RK4, activation: ActivationType::Tanh, }; let network_config = LiquidNetworkConfig { network_type: NetworkType::LTC, input_size: 16, output_size: 3, layer_configs: vec![LayerConfig::LTC(ltc_config)], 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 network = LiquidNetwork::new(network_config)?; info!("Created network: 16 -> 128 (LTC) -> 3"); // Calculate memory footprint let param_count = network.parameter_count(); let bytes_per_param = size_of::(); // 8 bytes (i64) let total_bytes = param_count * bytes_per_param; let kb = total_bytes as f64 / 1024.0; let mb = kb / 1024.0; info!( param_count, bytes_per_param, total_bytes, kb, mb, "Memory analysis" ); // Parameter breakdown let input_weights = 16 * 128; // input_size × hidden_size let recurrent_weights = 128 * 128; // hidden_size × hidden_size let bias = 128; // hidden_size let output_weights = 128 * 3; // hidden_size × output_size let output_bias = 3; // output_size let total_calculated = input_weights + recurrent_weights + bias + output_weights + output_bias; info!( input_weights, recurrent_weights, hidden_bias = bias, output_weights, output_bias, total_calculated, "Parameter breakdown" ); // Verify memory is reasonable (<10 MB for this size) assert!( mb < 10.0, "Network memory should be <10 MB (actual: {:.3} MB)", mb ); // Create training dataset and measure memory info!("Testing with 1000 training samples"); let mut samples = Vec::new(); for i in 0..1000 { let input: Vec = (0..16) .map(|j| FixedPoint::from_f64((i * j) as f64 / 1000.0)) .collect(); let target = vec![ FixedPoint::from_f64((i % 3 == 0) as u8 as f64), FixedPoint::from_f64((i % 3 == 1) as u8 as f64), FixedPoint::from_f64((i % 3 == 2) as u8 as f64), ]; samples.push(TrainingSample { input, target, timestamp: None, market_regime: None, volatility: None, }); } let sample_memory = samples.len() * (16 + 3) * size_of::(); let sample_mb = sample_memory as f64 / 1024.0 / 1024.0; info!(sample_mb, "Sample dataset memory"); // Total memory (network + samples) let total_mb = mb + sample_mb; info!(total_mb, "Total memory usage"); // Verify total memory is reasonable (<50 MB) assert!( total_mb < 50.0, "Total memory (network + samples) should be <50 MB (actual: {:.3} MB)", total_mb ); info!(network_mb = mb, sample_mb, total_mb, "Memory usage within limits (<50 MB limit)"); Ok(()) } // ============================================================================ // Helper Functions // ============================================================================