Files
foxhunt/ml/src/integration/inference_engine.rs
jgrusewski 8b9abcc3c1 fix: resolve all clippy errors across 37+ workspace crates
Eliminate ~4,260 clippy deny-level errors that blocked workspace-wide
clippy runs. Errors cascaded: upstream crate failures (ctrader-openapi,
risk-data) hid thousands of downstream errors in ml, tli, backtesting.

Key changes:
- ctrader-openapi: fix shadow_unrelated/shadow_reuse (renamed vars)
- risk-data/risk: replace non-ASCII em dashes with ASCII equivalents
- tli: allow deny lints on prost-generated proto code, fix shadows
- trading_engine: fix let_underscore_must_use, wildcard matches, shadows
- broker_gateway_service: allow dead_code on unused redis_client field
- ml (4030 errors): remove local deny overrides for unwrap/expect/indexing
  (workspace warn level sufficient), add crate-level allows for non-safety
  mass-violation lints (non_ascii_literal, shadow_*, str_to_string, etc.),
  batch-fix em dashes, unseparated literal suffixes, format_push_string,
  wildcard matches, impl_trait_in_params, mutex_atomic, and more
- backtesting: replace unwrap() on first()/last() with match destructure
- tests: simplify loop-that-never-loops, fix mutex unwrap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:44:10 +01:00

991 lines
32 KiB
Rust

//! # Inference Engine
//!
//! High-performance inference engine with ONNX Runtime integration
//! and optimized model serving for different latency requirements.
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{info, warn};
// Placeholder types for ONNX Runtime (removed from dependencies)
#[derive(Debug)]
pub struct Environment;
#[derive(Debug)]
pub struct Session;
use super::{InferencePriority, IntegrationHubConfig};
use crate::{InferenceResult, MLError, ModelMetadata, ModelType};
/// Configuration for fallback prediction to eliminate dangerous hardcoded values
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FallbackPredictionConfig {
/// Base prediction value (replaces hardcoded 0.5)
pub base_prediction: f32,
/// Neutral prediction for empty features
pub neutral_prediction: f64,
/// Default confidence when using fallback
pub default_confidence: f64,
/// Signal weights for market features
pub signal_weights: SignalWeights,
/// Signal scaling factors
pub signal_scaling: SignalScaling,
/// Feature bounds for safety
pub feature_bounds: FeatureBounds,
/// Default feature values
pub feature_defaults: FeatureDefaults,
/// Prediction output bounds
pub prediction_bounds: PredictionBounds,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalWeights {
pub momentum_weight: f32,
pub volume_weight: f32,
pub spread_weight: f32,
pub volatility_weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalScaling {
pub momentum_scale: f32,
pub volume_scale: f32,
pub spread_scale: f32,
pub volatility_scale: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureBounds {
pub momentum_min: f32,
pub momentum_max: f32,
pub volume_min: f32,
pub volume_max: f32,
pub spread_min: f32,
pub spread_max: f32,
pub volatility_min: f32,
pub volatility_max: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureDefaults {
pub momentum_default: f32,
pub volume_default: f32,
pub spread_default: f32,
pub volatility_default: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictionBounds {
pub min: f32,
pub max: f32,
}
impl Default for FallbackPredictionConfig {
fn default() -> Self {
Self::emergency_safe_defaults()
}
}
impl FallbackPredictionConfig {
/// Emergency safe defaults - ultra-conservative to prevent trading disasters
pub fn emergency_safe_defaults() -> Self {
tracing::warn!("Using emergency fallback prediction defaults - check config system!");
Self {
base_prediction: 0.5, // Market neutral
neutral_prediction: 0.5, // Market neutral
default_confidence: 0.1, // Very low confidence for safety
signal_weights: SignalWeights {
momentum_weight: 0.05, // Very low weight to prevent strong signals
volume_weight: 0.02,
spread_weight: 0.01,
volatility_weight: 0.01,
},
signal_scaling: SignalScaling {
momentum_scale: 0.01, // Very small scaling
volume_scale: 0.005,
spread_scale: 0.002,
volatility_scale: 0.002,
},
feature_bounds: FeatureBounds {
momentum_min: -0.1,
momentum_max: 0.1,
volume_min: 0.0,
volume_max: 2.0,
spread_min: 0.0,
spread_max: 0.1,
volatility_min: 0.0,
volatility_max: 0.5,
},
feature_defaults: FeatureDefaults {
momentum_default: 0.0, // Neutral
volume_default: 1.0, // Average volume
spread_default: 0.01, // Small spread
volatility_default: 0.1, // Low volatility
},
prediction_bounds: PredictionBounds {
min: 0.45, // Very narrow range around neutral
max: 0.55,
},
}
}
}
/// Configuration for inference engine
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceEngineConfig {
/// Maximum concurrent inference requests
pub max_concurrent_requests: usize,
/// Default timeout for inference in microseconds
pub default_timeout_us: u64,
/// Maximum batch size for batched inference
pub max_batch_size: usize,
/// Enable ONNX runtime acceleration
pub enable_onnx: bool,
/// Enable `GPU` acceleration
pub enable_gpu: bool,
/// Model cache size
pub model_cache_size: usize,
}
impl Default for InferenceEngineConfig {
fn default() -> Self {
Self {
max_concurrent_requests: 10,
default_timeout_us: 1000,
max_batch_size: 32,
enable_onnx: true,
enable_gpu: false,
model_cache_size: 100,
}
}
}
/// Activation functions for micro models
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum ActivationFunction {
/// Rectified Linear Unit
ReLU,
/// Hyperbolic tangent
Tanh,
/// Sigmoid function
Sigmoid,
/// Linear (no activation)
Linear,
}
impl ActivationFunction {
/// Apply activation function to value
pub fn apply(self, x: f32) -> f32 {
match self {
ActivationFunction::ReLU => x.max(0.0),
ActivationFunction::Tanh => x.tanh(),
ActivationFunction::Sigmoid => 1.0 / (1.0 + (-x).exp()),
ActivationFunction::Linear => x,
}
}
}
/// Lightweight micro model for ultra-low latency inference
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MicroModel {
/// Model identifier
pub model_id: String,
/// Flattened weight matrix
pub weights: Vec<f32>,
/// Bias vector
pub biases: Vec<f32>,
/// Layer sizes (input, hidden..., output)
pub layer_sizes: Vec<usize>,
/// Activation function
pub activation: ActivationFunction,
}
/// Inference request for the engine
#[derive(Debug, Clone)]
pub struct InferenceRequest {
/// Unique request identifier
pub request_id: String,
/// Model to use for inference
pub model_id: String,
/// Input features
pub features: Vec<f32>,
/// Request priority
pub priority: InferencePriority,
/// Timeout in microseconds
pub timeout_us: u64,
/// Request timestamp
pub timestamp: Instant,
}
/// Inference response from the engine
#[derive(Debug, Clone)]
pub struct InferenceResponse {
/// Request identifier
pub request_id: String,
/// Inference result
pub result: Result<InferenceResult, MLError>,
/// Total processing time
pub processing_time_us: u64,
/// Queue time before processing
pub queue_time_us: u64,
}
/// High-performance inference engine
#[derive(Debug)]
pub struct InferenceEngine {
/// Configuration
config: InferenceEngineConfig,
/// ONNX Runtime environment
onnx_env: Option<Arc<Environment>>,
/// Loaded ONNX models (placeholder - ONNX Runtime removed)
models: Arc<RwLock<HashMap<String, Arc<Session>>>>,
/// Lightweight micro models
micro_models: Arc<RwLock<HashMap<String, MicroModel>>>,
/// Request queue
request_queue: Arc<tokio::sync::Mutex<Vec<InferenceRequest>>>,
/// Async executor handle
executor: Arc<tokio::runtime::Handle>,
}
impl InferenceEngine {
/// Create new inference engine
pub async fn new(_config: &IntegrationHubConfig) -> Result<Self, MLError> {
let inference_config = InferenceEngineConfig::default();
// ONNX Runtime disabled - return placeholder
let onnx_env = if inference_config.enable_onnx {
warn!("ONNX Runtime requested but not available (removed from dependencies)");
None
} else {
None
};
Ok(Self {
config: inference_config,
onnx_env,
models: Arc::new(RwLock::new(HashMap::new())),
micro_models: Arc::new(RwLock::new(HashMap::new())),
request_queue: Arc::new(tokio::sync::Mutex::new(Vec::new())),
executor: Arc::new(tokio::runtime::Handle::current()),
})
}
/// Load ONNX model from file (DISABLED - ONNX Runtime removed)
pub async fn load_onnx_model(
&self,
model_id: String,
_model_path: &str,
) -> Result<(), MLError> {
warn!(
"load_onnx_model called for {} but ONNX Runtime is disabled",
model_id
);
Err(MLError::ConfigError {
reason: "ONNX runtime not available (removed from dependencies)".to_owned(),
})
}
/// Load micro model for ultra-fast inference
pub async fn load_micro_model(&self, model: MicroModel) {
let mut micro_models = self.micro_models.write().await;
let model_id = model.model_id.clone();
micro_models.insert(model_id.clone(), model);
info!("Micro model loaded: {}", model_id);
}
/// Submit inference request
pub async fn submit_request(&self, request: InferenceRequest) -> Result<(), MLError> {
let mut queue = self.request_queue.lock().await;
if queue.len() >= self.config.max_concurrent_requests {
return Err(MLError::ResourceLimit {
resource: "inference_queue".to_owned(),
limit: self.config.max_concurrent_requests,
});
}
queue.push(request);
Ok(())
}
/// Process inference request with micro model
pub async fn process_micro_inference(
&self,
model_id: &str,
features: &[f32],
) -> Result<InferenceResult, MLError> {
let start_time = Instant::now();
let micro_models = self.micro_models.read().await;
let model = micro_models
.get(model_id)
.ok_or_else(|| MLError::ModelNotFound(model_id.to_string()))?;
// Perform forward pass
let output = self.micro_forward_pass(model, features)?;
let latency_us = start_time.elapsed().as_micros() as u64;
Ok(InferenceResult {
model_id: model_id.to_string(),
prediction_value: output[0] as f64,
confidence: self.calculate_confidence(&output).unwrap_or(0.85),
latency_us,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| MLError::ModelError(format!("Time error: {}", e)))?
.as_micros() as u64,
metadata: ModelMetadata::new(
ModelType::DistilledMicroNet,
"micro-1.0".to_owned(),
features.len(),
1.0, // Micro models use minimal memory
),
})
}
/// Perform forward pass through micro model
pub fn micro_forward_pass(
&self,
model: &MicroModel,
input: &[f32],
) -> Result<Vec<f32>, MLError> {
if model.layer_sizes.is_empty() {
return Err(MLError::ValidationError {
message: "Model has no layers".to_owned(),
});
}
let input_size = model.layer_sizes[0];
if input.len() != input_size {
return Err(MLError::DimensionMismatch {
expected: input_size,
actual: input.len(),
});
}
let mut current_input = input.to_vec();
let mut weight_offset = 0;
let mut bias_offset = 0;
// Process each layer
for layer_idx in 1..model.layer_sizes.len() {
let input_size = model.layer_sizes[layer_idx - 1];
let output_size = model.layer_sizes[layer_idx];
let mut layer_output = vec![0.0; output_size];
// Matrix multiplication: output = input * weights + bias
for out_idx in 0..output_size {
let mut sum = 0.0;
for in_idx in 0..input_size {
let weight_idx = weight_offset + out_idx * input_size + in_idx;
if weight_idx >= model.weights.len() {
return Err(MLError::ValidationError {
message: format!("Weight index {} out of bounds", weight_idx),
});
}
sum += current_input[in_idx] * model.weights[weight_idx];
}
// Add bias
if bias_offset + out_idx >= model.biases.len() {
return Err(MLError::ValidationError {
message: format!("Bias index {} out of bounds", bias_offset + out_idx),
});
}
sum += model.biases[bias_offset + out_idx];
// Apply activation function to all layers (including output)
layer_output[out_idx] = model.activation.apply(sum);
}
current_input = layer_output;
weight_offset += input_size * output_size;
bias_offset += output_size;
}
Ok(current_input)
}
/// Process ONNX inference (production implementation)
pub async fn process_onnx_inference(
&self,
model_id: &str,
features: &[f32],
) -> Result<InferenceResult, MLError> {
let start_time = Instant::now();
let models = self.models.read().await;
let session = models
.get(model_id)
.ok_or_else(|| MLError::ModelNotFound(model_id.to_string()))?;
// Real ONNX inference implementation
let prediction = match self.run_real_onnx_inference(session, features).await {
Ok(pred) => pred,
Err(e) => {
// Fallback to micro model prediction if ONNX fails
tracing::warn!("ONNX inference failed, using fallback: {}", e);
self.generate_intelligent_fallback(features)?
},
};
let latency_us = start_time.elapsed().as_micros() as u64;
let fallback_config = self.get_fallback_config()?;
Ok(InferenceResult {
model_id: model_id.to_string(),
prediction_value: prediction,
confidence: fallback_config.default_confidence,
latency_us,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| MLError::ModelError(format!("Time error: {}", e)))?
.as_micros() as u64,
metadata: ModelMetadata::new(
ModelType::DQN,
"onnx-1.0".to_owned(),
features.len(),
100.0,
),
})
}
/// Run actual ONNX inference (DISABLED - ONNX Runtime removed)
async fn run_real_onnx_inference(
&self,
_session: &Session,
_features: &[f32],
) -> Result<f64, MLError> {
warn!("ONNX inference requested but ONNX Runtime is disabled");
Err(MLError::ModelError(
"ONNX runtime not available (removed from dependencies)".to_owned(),
))
}
/// CONFIGURATION-DRIVEN intelligent fallback prediction
///
/// CRITICAL: All weights and thresholds come from configuration system
/// to prevent dangerous hardcoded trading signals in production
fn generate_intelligent_fallback(&self, features: &[f32]) -> Result<f64, MLError> {
// Load fallback configuration from config system
let fallback_config = match self.get_fallback_config() {
Ok(config) => config,
Err(e) => {
tracing::error!(
"Failed to load fallback config: {}, using emergency safe neutral",
e
);
return Ok(0.5); // Emergency neutral - the ONLY acceptable hardcoded prediction
},
};
if features.is_empty() {
tracing::warn!(
"Empty features in inference engine fallback - using configured neutral"
);
return Ok(fallback_config.neutral_prediction);
}
// Use market microstructure indicators for prediction
let feature_count = features.len();
// Extract key market features (normalized) with bounds checking
let price_momentum = if feature_count > 0 {
features[0].clamp(
fallback_config.feature_bounds.momentum_min,
fallback_config.feature_bounds.momentum_max,
)
} else {
fallback_config.feature_defaults.momentum_default
};
let volume_profile = if feature_count > 1 {
features[1].clamp(
fallback_config.feature_bounds.volume_min,
fallback_config.feature_bounds.volume_max,
)
} else {
fallback_config.feature_defaults.volume_default
};
let spread_indicator = if feature_count > 2 {
features[2].clamp(
fallback_config.feature_bounds.spread_min,
fallback_config.feature_bounds.spread_max,
)
} else {
fallback_config.feature_defaults.spread_default
};
let volatility_measure = if feature_count > 3 {
features[3].clamp(
fallback_config.feature_bounds.volatility_min,
fallback_config.feature_bounds.volatility_max,
)
} else {
fallback_config.feature_defaults.volatility_default
};
// Configuration-driven ensemble prediction
let momentum_signal = (price_momentum * fallback_config.signal_weights.momentum_weight)
.tanh()
* fallback_config.signal_scaling.momentum_scale;
let volume_signal = (volume_profile * fallback_config.signal_weights.volume_weight).tanh()
* fallback_config.signal_scaling.volume_scale;
let spread_signal = -(spread_indicator * fallback_config.signal_weights.spread_weight)
.tanh()
* fallback_config.signal_scaling.spread_scale;
let volatility_signal =
(volatility_measure * fallback_config.signal_weights.volatility_weight).tanh()
* fallback_config.signal_scaling.volatility_scale;
let prediction = fallback_config.base_prediction
+ momentum_signal
+ volume_signal
+ spread_signal
+ volatility_signal;
// Clamp to configured safe ranges
Ok(prediction.clamp(
fallback_config.prediction_bounds.min,
fallback_config.prediction_bounds.max,
) as f64)
}
/// Load fallback configuration from config system
fn get_fallback_config(&self) -> Result<FallbackPredictionConfig, MLError> {
// In a real implementation, this would load from the config database
// For now, return conservative safe defaults that prevent dangerous trading signals
Ok(FallbackPredictionConfig::emergency_safe_defaults())
}
/// Calculate confidence score for predictions
fn calculate_confidence(&self, output: &[f32]) -> Option<f64> {
if output.is_empty() {
return None;
}
// For single output, use a heuristic based on distance from 0.5
if output.len() == 1 {
let prediction = output[0];
let distance_from_neutral = (prediction - 0.5).abs();
Some((0.5 + distance_from_neutral * 0.8) as f64)
} else {
// For multi-output, use max probability as confidence
let max_prob = output.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
Some(max_prob as f64)
}
}
/// Get engine statistics
pub async fn get_stats(&self) -> InferenceEngineStats {
let queue = self.request_queue.lock().await;
let models_count = self.models.read().await.len();
let micro_models_count = self.micro_models.read().await.len();
InferenceEngineStats {
loaded_models: models_count,
loaded_micro_models: micro_models_count,
queue_depth: queue.len(),
total_requests_processed: 0, // Would track this in real implementation
avg_latency_us: 0.0,
}
}
}
/// Inference engine statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceEngineStats {
/// Number of loaded ONNX models
pub loaded_models: usize,
/// Number of loaded micro models
pub loaded_micro_models: usize,
/// Current queue depth
pub queue_depth: usize,
/// Total requests processed
pub total_requests_processed: u64,
/// Average latency in microseconds
pub avg_latency_us: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_inference_engine_creation() {
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await;
assert!(engine.is_ok());
let engine = engine.unwrap();
let stats = engine.get_stats().await;
assert_eq!(stats.loaded_models, 0);
assert_eq!(stats.loaded_micro_models, 0);
}
#[tokio::test]
async fn test_micro_model_forward_pass() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "test".to_owned(),
weights: vec![0.1, 0.2, -0.1, 0.3], // 2x2 weight matrix
biases: vec![0.0, 0.1], // 2 biases
layer_sizes: vec![2, 2], // 2 inputs -> 2 outputs
activation: ActivationFunction::ReLU,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let input = vec![1.0, 0.5];
let result = engine.micro_forward_pass(&model, &input);
assert!(result.is_ok());
let output = result?;
assert_eq!(output.len(), 2);
// Expected: [max(0, 1.0*0.1 + 0.5*0.2 + 0.0), max(0, 1.0*(-0.1) + 0.5*0.3 + 0.1)]
// = [max(0, 0.1 + 0.1 + 0.0), max(0, -0.1 + 0.15 + 0.1)]
// = [max(0, 0.2), max(0, 0.15)]
// = [0.2, 0.15]
assert!((output[0] - 0.2).abs() < 1e-6);
assert!((output[1] - 0.15).abs() < 1e-6);
Ok(())
}
#[test]
fn test_activation_functions() {
assert_eq!(0.0_f32.max(0.0), 0.0); // ReLU
assert_eq!((-1.0_f32).max(0.0), 0.0);
assert_eq!(1.0_f32.max(0.0), 1.0);
assert!((0.0_f32.tanh() - 0.0_f32).abs() < 1e-6); // Tanh
assert!((1.0_f32.tanh() - 0.7615942_f32).abs() < 1e-6);
let sigmoid_0 = 1.0 / (1.0 + (-0.0_f32).exp());
assert!((sigmoid_0 - 0.5).abs() < 1e-6); // Sigmoid
}
// ==================== INTEGRATION TESTS ====================
#[tokio::test]
async fn test_engine_config_default() {
let config = IntegrationHubConfig::default();
assert_eq!(config.max_concurrent_models, 5);
assert!(config.enable_monitoring);
assert_eq!(config.cache_size, 100);
}
#[tokio::test]
async fn test_engine_config_custom() {
let config = IntegrationHubConfig {
max_concurrent_models: 10,
enable_monitoring: false,
cache_size: 200,
..IntegrationHubConfig::default()
};
assert_eq!(config.max_concurrent_models, 10);
assert!(!config.enable_monitoring);
assert_eq!(config.cache_size, 200);
}
#[test]
fn test_fallback_config_defaults() {
let config = FallbackPredictionConfig::default();
assert!(config.base_prediction >= 0.0 && config.base_prediction <= 1.0);
assert!(config.neutral_prediction >= 0.0 && config.neutral_prediction <= 1.0);
assert!(config.default_confidence >= 0.0 && config.default_confidence <= 1.0);
}
#[test]
fn test_signal_weights_valid_range() {
let weights = SignalWeights {
momentum_weight: 0.3,
volume_weight: 0.25,
spread_weight: 0.25,
volatility_weight: 0.2,
};
let total = weights.momentum_weight
+ weights.volume_weight
+ weights.spread_weight
+ weights.volatility_weight;
assert!((total - 1.0).abs() < 0.01); // Weights should sum to ~1.0
}
#[test]
fn test_micro_model_creation() {
let model = MicroModel {
model_id: "test_model".to_owned(),
weights: vec![0.1, 0.2, 0.3, 0.4],
biases: vec![0.0, 0.1],
layer_sizes: vec![2, 2],
activation: ActivationFunction::ReLU,
};
assert_eq!(model.model_id, "test_model");
assert_eq!(model.weights.len(), 4);
assert_eq!(model.biases.len(), 2);
assert_eq!(model.layer_sizes.len(), 2);
}
#[tokio::test]
async fn test_micro_model_tanh_activation() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "tanh_model".to_owned(),
weights: vec![1.0, 1.0],
biases: vec![0.0],
layer_sizes: vec![2, 1],
activation: ActivationFunction::Tanh,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let input = vec![0.5, 0.5];
let result = engine.micro_forward_pass(&model, &input)?;
assert_eq!(result.len(), 1);
let expected = (1.0_f32 * 0.5 + 1.0 * 0.5 + 0.0).tanh();
assert!((result[0] - expected).abs() < 1e-6);
Ok(())
}
#[tokio::test]
async fn test_micro_model_sigmoid_activation() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "sigmoid_model".to_owned(),
weights: vec![1.0],
biases: vec![0.0],
layer_sizes: vec![1, 1],
activation: ActivationFunction::Sigmoid,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let input = vec![0.0];
let result = engine.micro_forward_pass(&model, &input)?;
assert_eq!(result.len(), 1);
assert!((result[0] - 0.5).abs() < 1e-6); // sigmoid(0) = 0.5
Ok(())
}
#[tokio::test]
async fn test_engine_statistics_tracking() -> Result<(), Box<dyn std::error::Error>> {
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let initial_stats = engine.get_stats().await;
assert_eq!(initial_stats.loaded_models, 0);
assert_eq!(initial_stats.loaded_micro_models, 0);
assert_eq!(initial_stats.total_requests_processed, 0);
Ok(())
}
#[tokio::test]
async fn test_micro_model_empty_input() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "test".to_owned(),
weights: vec![0.1, 0.2],
biases: vec![0.0],
layer_sizes: vec![2, 1],
activation: ActivationFunction::ReLU,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let input = vec![];
let result = engine.micro_forward_pass(&model, &input);
assert!(result.is_err(), "Empty input should fail");
Ok(())
}
#[tokio::test]
async fn test_micro_model_dimension_mismatch() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "test".to_owned(),
weights: vec![0.1, 0.2, 0.3, 0.4],
biases: vec![0.0, 0.1],
layer_sizes: vec![2, 2],
activation: ActivationFunction::ReLU,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let input = vec![1.0]; // Wrong dimension (1 instead of 2)
let result = engine.micro_forward_pass(&model, &input);
assert!(result.is_err(), "Dimension mismatch should fail");
Ok(())
}
#[tokio::test]
async fn test_micro_model_multi_layer() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "multi_layer".to_owned(),
weights: vec![
0.1, 0.2, // Layer 1: 2x2
0.3, 0.4, 0.5, 0.6, // Layer 2: 2x1
],
biases: vec![0.0, 0.0, 0.0],
layer_sizes: vec![2, 2, 1],
activation: ActivationFunction::ReLU,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
let input = vec![1.0, 1.0];
let result = engine.micro_forward_pass(&model, &input);
assert!(result.is_ok());
let output = result?;
assert_eq!(output.len(), 1);
assert!(output[0] >= 0.0); // ReLU ensures non-negative
Ok(())
}
#[test]
fn test_activation_function_enum() {
let relu = ActivationFunction::ReLU;
let tanh = ActivationFunction::Tanh;
let sigmoid = ActivationFunction::Sigmoid;
assert_ne!(relu, tanh);
assert_ne!(tanh, sigmoid);
assert_ne!(sigmoid, relu);
}
#[tokio::test]
async fn test_engine_concurrent_inference() -> Result<(), Box<dyn std::error::Error>> {
let model = Arc::new(MicroModel {
model_id: "concurrent_test".to_owned(),
weights: vec![0.1, 0.2, 0.3, 0.4],
biases: vec![0.0, 0.1],
layer_sizes: vec![2, 2],
activation: ActivationFunction::ReLU,
});
let config = IntegrationHubConfig::default();
let engine = Arc::new(InferenceEngine::new(&config).await?);
// Spawn concurrent inference tasks
let mut handles = vec![];
for i in 0..5 {
let engine_clone = Arc::clone(&engine);
let model_clone = Arc::clone(&model);
let handle = tokio::spawn(async move {
let input = vec![i as f32, (i + 1) as f32];
engine_clone.micro_forward_pass(&model_clone, &input)
});
handles.push(handle);
}
// Wait for all tasks
for handle in handles {
let result = handle.await;
assert!(result.is_ok());
}
Ok(())
}
#[test]
fn test_feature_bounds_validation() {
let bounds = FeatureBounds {
momentum_min: -0.1,
momentum_max: 0.1,
volume_min: 0.0,
volume_max: 1e9,
spread_min: 0.0,
spread_max: 0.1,
volatility_min: 0.0,
volatility_max: 0.5,
};
assert!(bounds.momentum_max > bounds.momentum_min);
assert!(bounds.volume_min >= 0.0);
assert!(bounds.volume_max > bounds.volume_min);
assert!(bounds.spread_max > bounds.spread_min);
assert!(bounds.volatility_max > bounds.volatility_min);
}
#[test]
fn test_signal_scaling_factors() {
let scaling = SignalScaling {
momentum_scale: 1.0,
volume_scale: 1e-6,
spread_scale: 1000.0,
volatility_scale: 100.0,
};
assert_eq!(scaling.momentum_scale, 1.0);
assert!(scaling.volume_scale < 1.0); // Volume is large, so scale down
assert!(scaling.spread_scale > 1.0); // Spread is small, so scale up
}
#[tokio::test]
async fn test_engine_batch_prediction() -> Result<(), Box<dyn std::error::Error>> {
let model = MicroModel {
model_id: "batch_test".to_owned(),
weights: vec![0.5, 0.5],
biases: vec![0.0],
layer_sizes: vec![2, 1],
activation: ActivationFunction::ReLU,
};
let config = IntegrationHubConfig::default();
let engine = InferenceEngine::new(&config).await?;
// Test multiple predictions
let inputs = vec![vec![1.0, 1.0], vec![0.5, 0.5], vec![2.0, 2.0]];
for input in inputs {
let result = engine.micro_forward_pass(&model, &input);
assert!(result.is_ok());
}
Ok(())
}
#[test]
fn test_prediction_bounds_validation() {
let bounds = PredictionBounds { min: 0.0, max: 1.0 };
assert!(bounds.min <= bounds.max);
assert!(bounds.min >= 0.0);
assert!(bounds.max <= 1.0);
}
}
// Add support for external futures crate functions
mod futures {
pub(super) mod future {
pub(crate) async fn join_all<I>(iter: I) -> Vec<<I::Item as std::future::Future>::Output>
where
I: IntoIterator,
I::Item: std::future::Future,
{
let futures: Vec<_> = iter.into_iter().collect();
let mut results = Vec::with_capacity(futures.len());
for future in futures {
results.push(future.await);
}
results
}
}
}