feat: vendor midstream and sublinear-time-solver libraries (#109)

Add ruvnet/midstream (AIMDS real-time inference) and
ruvnet/sublinear-time-solver (sublinear optimization algorithms)
as vendored dependencies under vendor/.
This commit is contained in:
rUv
2026-03-02 23:34:05 -05:00
committed by GitHub
parent 14902e6b4e
commit 407b46b206
1600 changed files with 1852646 additions and 0 deletions
@@ -0,0 +1,117 @@
//! Training binary for temporal neural networks
use clap::Parser;
use log::info;
use std::path::PathBuf;
use temporal_neural_net::{
config::Config,
data::TimeSeriesData,
models::{SystemA, SystemB},
training::Trainer,
error::Result,
};
#[derive(Parser)]
#[command(name = "train")]
#[command(about = "Train temporal neural network models")]
struct Args {
/// Configuration file path
#[arg(short, long)]
config: PathBuf,
/// Training data file
#[arg(short, long)]
data: PathBuf,
/// Output directory for trained model
#[arg(short, long, default_value = "output")]
output: PathBuf,
/// Verbose logging
#[arg(short, long)]
verbose: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
// Initialize logging
if args.verbose {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
} else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
}
temporal_neural_net::init()?;
info!("Loading configuration from {:?}", args.config);
let config = Config::from_file(&args.config)?;
info!("Loading training data from {:?}", args.data);
let data = TimeSeriesData::from_csv(&args.data)?;
info!("Creating temporal splits (70/15/15)");
let splits = data.temporal_split(0.7, 0.15, 0.15)?;
splits.validate()?;
let (train_size, val_size, test_size) = splits.get_sizes();
info!("Data split sizes - Train: {}, Val: {}, Test: {}", train_size, val_size, test_size);
// Create output directory
std::fs::create_dir_all(&args.output)?;
let mut trainer = Trainer::new(config.training.clone())?;
match config.system {
temporal_neural_net::config::SystemConfig::Traditional(_) => {
info!("Training System A (Traditional)");
let mut model = SystemA::new(&config.model)?;
let result = trainer.train_system_a(&mut model, &splits)?;
info!("Training completed:");
info!(" - Final loss: {:.6}", result.final_loss);
info!(" - Best val loss: {:.6}", result.best_val_loss);
info!(" - Converged: {}", result.converged);
info!(" - Training time: {:.2}s", result.total_time_seconds);
// Save model (simplified)
let model_path = args.output.join("system_a_model.json");
info!("Saving model to {:?}", model_path);
}
temporal_neural_net::config::SystemConfig::TemporalSolver(ref solver_config) => {
info!("Training System B (Temporal Solver)");
let mut model = SystemB::new(&config.model, solver_config)?;
let result = trainer.train_system_b(&mut model, &splits)?;
info!("Training completed:");
info!(" - Final loss: {:.6}", result.final_loss);
info!(" - Best val loss: {:.6}", result.best_val_loss);
info!(" - Converged: {}", result.converged);
info!(" - Training time: {:.2}s", result.total_time_seconds);
// Print System B specific metrics
if let Some(ref last_metrics) = result.history.metrics.last() {
if let Some(ref b_metrics) = last_metrics.system_b_metrics {
info!("System B metrics:");
info!(" - Gate pass rate: {:.3}", b_metrics.gate_pass_rate);
info!(" - Avg certificate error: {:.6}", b_metrics.avg_certificate_error);
info!(" - Kalman prediction error: {:.6}", b_metrics.kalman_prediction_error);
}
}
// Save model (simplified)
let model_path = args.output.join("system_b_model.json");
info!("Saving model to {:?}", model_path);
}
}
// Save training history
let history_path = args.output.join("training_history.json");
let history_json = serde_json::to_string_pretty(trainer.get_history())?;
std::fs::write(&history_path, history_json)?;
info!("Training history saved to {:?}", history_path);
info!("Training completed successfully!");
Ok(())
}
@@ -0,0 +1,520 @@
//! Configuration management for temporal neural network systems
use crate::error::{Result, TemporalNeuralError};
use serde::{Deserialize, Serialize};
use std::path::Path;
/// Main configuration structure containing all system settings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Common settings shared between systems
pub common: CommonConfig,
/// Model architecture configuration
pub model: ModelConfig,
/// Training configuration
pub training: TrainingConfig,
/// Inference configuration
pub inference: InferenceConfig,
/// System-specific configuration
pub system: SystemConfig,
}
/// Common configuration shared between System A and B
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommonConfig {
/// Prediction horizon in milliseconds
pub horizon_ms: u32,
/// Input window size in milliseconds
pub window_ms: u32,
/// Sample rate in Hz
pub sample_rate_hz: u32,
/// Feature names and order
pub features: Vec<String>,
/// Whether to use INT8 quantization for inference
pub quantize: bool,
/// Random seed for reproducibility
pub random_seed: Option<u64>,
/// Enable detailed logging
pub verbose: bool,
}
/// Model architecture configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
/// Model type: "micro_gru" or "micro_tcn"
pub model_type: String,
/// Hidden layer size
pub hidden_size: u32,
/// Number of layers
pub num_layers: u32,
/// Dropout rate during training
pub dropout: f64,
/// Whether to use residual connections
pub residual: bool,
/// Activation function: "relu", "tanh", "gelu"
pub activation: String,
/// Layer normalization
pub layer_norm: bool,
}
/// Training configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingConfig {
/// Optimizer type: "adam", "sgd", "rmsprop"
pub optimizer: String,
/// Learning rate
pub learning_rate: f64,
/// Batch size
pub batch_size: u32,
/// Number of training epochs
pub epochs: u32,
/// Early stopping patience
pub patience: u32,
/// Validation frequency (epochs)
pub val_frequency: u32,
/// Gradient clipping threshold
pub grad_clip: Option<f64>,
/// Weight decay / L2 regularization
pub weight_decay: f64,
/// Smoothness penalty weight for velocity
pub smoothness_weight: f64,
/// Checkpoint saving frequency
pub checkpoint_frequency: u32,
}
/// Inference configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceConfig {
/// Target P99.9 latency in milliseconds
pub target_latency_ms: f64,
/// Enable SIMD optimizations
pub enable_simd: bool,
/// Number of inference threads
pub num_threads: u32,
/// Memory pinning for performance
pub pin_memory: bool,
/// CPU affinity settings
pub cpu_affinity: Option<Vec<u32>>,
/// Batch size for inference (usually 1 for real-time)
pub batch_size: u32,
}
/// System-specific configuration (A or B)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SystemConfig {
/// Traditional micro-net (System A)
Traditional(TraditionalConfig),
/// Temporal solver net (System B)
TemporalSolver(TemporalSolverConfig),
}
/// Configuration for traditional system (System A)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraditionalConfig {
/// Whether traditional system is enabled
pub enabled: bool,
}
/// Configuration for temporal solver system (System B)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalSolverConfig {
/// Kalman filter prior configuration
pub prior: KalmanConfig,
/// Sublinear solver gate configuration
pub solver_gate: SolverGateConfig,
/// Active sample selection configuration
pub active_selection: ActiveSelectionConfig,
}
/// Kalman filter configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KalmanConfig {
/// Process noise covariance
pub process_noise: f64,
/// Measurement noise covariance
pub measurement_noise: f64,
/// Initial state uncertainty
pub initial_uncertainty: f64,
/// State transition model: "constant_velocity", "constant_acceleration"
pub transition_model: String,
/// Update frequency in Hz
pub update_frequency: f64,
}
/// Sublinear solver gate configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverGateConfig {
/// Solver algorithm: "neumann", "random_walk", "forward_push"
pub algorithm: String,
/// Convergence tolerance
pub epsilon: f64,
/// Computational budget
pub budget: u64,
/// Maximum certificate error to allow passage
pub max_cert_error: f64,
/// Fallback strategy: "hold_last", "kalman_only", "disable_gate"
pub fallback_strategy: String,
}
/// Active sample selection configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveSelectionConfig {
/// k-NN graph construction parameter
pub k: u32,
/// PageRank tolerance for convergence
pub pagerank_eps: f64,
/// Number of active samples per epoch
pub samples_per_epoch: u32,
/// Error weight for PageRank scoring
pub error_weight: f64,
/// Diversity weight to avoid clustering
pub diversity_weight: f64,
}
impl Config {
/// Load configuration from YAML file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
TemporalNeuralError::IoError {
message: format!("Failed to read config file: {}", e),
path: Some(path.as_ref().to_string_lossy().to_string()),
source: Some(e),
}
})?;
let config: Self = serde_yaml::from_str(&content).map_err(|e| {
TemporalNeuralError::ConfigurationError {
message: format!("Failed to parse config: {}", e),
field: None,
}
})?;
config.validate()?;
Ok(config)
}
/// Save configuration to YAML file
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = serde_yaml::to_string(self).map_err(|e| {
TemporalNeuralError::SerializationError {
message: format!("Failed to serialize config: {}", e),
format: Some("yaml".to_string()),
}
})?;
std::fs::write(path.as_ref(), content).map_err(|e| {
TemporalNeuralError::IoError {
message: format!("Failed to write config file: {}", e),
path: Some(path.as_ref().to_string_lossy().to_string()),
source: Some(e),
}
})?;
Ok(())
}
/// Validate configuration parameters
pub fn validate(&self) -> Result<()> {
// Validate common config
if self.common.horizon_ms == 0 {
return Err(TemporalNeuralError::config_field_error(
"horizon_ms", "Must be greater than 0"
));
}
if self.common.window_ms == 0 {
return Err(TemporalNeuralError::config_field_error(
"window_ms", "Must be greater than 0"
));
}
if self.common.sample_rate_hz == 0 {
return Err(TemporalNeuralError::config_field_error(
"sample_rate_hz", "Must be greater than 0"
));
}
if self.common.features.is_empty() {
return Err(TemporalNeuralError::config_field_error(
"features", "Must specify at least one feature"
));
}
// Validate model config
if self.model.hidden_size == 0 {
return Err(TemporalNeuralError::config_field_error(
"hidden_size", "Must be greater than 0"
));
}
if self.model.num_layers == 0 {
return Err(TemporalNeuralError::config_field_error(
"num_layers", "Must be greater than 0"
));
}
if !(0.0..=1.0).contains(&self.model.dropout) {
return Err(TemporalNeuralError::config_field_error(
"dropout", "Must be between 0.0 and 1.0"
));
}
// Validate training config
if self.training.learning_rate <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"learning_rate", "Must be positive"
));
}
if self.training.batch_size == 0 {
return Err(TemporalNeuralError::config_field_error(
"batch_size", "Must be greater than 0"
));
}
if self.training.epochs == 0 {
return Err(TemporalNeuralError::config_field_error(
"epochs", "Must be greater than 0"
));
}
// Validate inference config
if self.inference.target_latency_ms <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"target_latency_ms", "Must be positive"
));
}
if self.inference.num_threads == 0 {
return Err(TemporalNeuralError::config_field_error(
"num_threads", "Must be greater than 0"
));
}
// Validate system-specific config
match &self.system {
SystemConfig::Traditional(_) => {
// No additional validation needed
}
SystemConfig::TemporalSolver(config) => {
self.validate_temporal_solver_config(config)?;
}
}
Ok(())
}
fn validate_temporal_solver_config(&self, config: &TemporalSolverConfig) -> Result<()> {
// Validate Kalman config
if config.prior.process_noise <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"process_noise", "Must be positive"
));
}
if config.prior.measurement_noise <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"measurement_noise", "Must be positive"
));
}
if config.prior.update_frequency <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"update_frequency", "Must be positive"
));
}
// Validate solver gate config
if config.solver_gate.epsilon <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"epsilon", "Must be positive"
));
}
if config.solver_gate.budget == 0 {
return Err(TemporalNeuralError::config_field_error(
"budget", "Must be greater than 0"
));
}
if config.solver_gate.max_cert_error < 0.0 {
return Err(TemporalNeuralError::config_field_error(
"max_cert_error", "Must be non-negative"
));
}
// Validate active selection config
if config.active_selection.k == 0 {
return Err(TemporalNeuralError::config_field_error(
"k", "Must be greater than 0"
));
}
if config.active_selection.pagerank_eps <= 0.0 {
return Err(TemporalNeuralError::config_field_error(
"pagerank_eps", "Must be positive"
));
}
Ok(())
}
/// Get the expected input window size in samples
pub fn window_samples(&self) -> usize {
((self.common.window_ms as f64 / 1000.0) * self.common.sample_rate_hz as f64) as usize
}
/// Get the prediction horizon in samples
pub fn horizon_samples(&self) -> usize {
((self.common.horizon_ms as f64 / 1000.0) * self.common.sample_rate_hz as f64) as usize
}
/// Get the feature count
pub fn feature_count(&self) -> usize {
self.common.features.len()
}
/// Get input shape for the neural network
pub fn input_shape(&self) -> (usize, usize) {
(self.window_samples(), self.feature_count())
}
/// Check if this is a temporal solver system
pub fn is_temporal_solver(&self) -> bool {
matches!(self.system, SystemConfig::TemporalSolver(_))
}
}
impl Default for Config {
fn default() -> Self {
Self {
common: CommonConfig {
horizon_ms: 500,
window_ms: 128,
sample_rate_hz: 2000,
features: vec!["x".to_string(), "y".to_string(), "vx".to_string(), "vy".to_string()],
quantize: true,
random_seed: Some(42),
verbose: false,
},
model: ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 32,
num_layers: 1,
dropout: 0.1,
residual: true,
activation: "tanh".to_string(),
layer_norm: false,
},
training: TrainingConfig {
optimizer: "adam".to_string(),
learning_rate: 1e-3,
batch_size: 256,
epochs: 15,
patience: 5,
val_frequency: 1,
grad_clip: Some(1.0),
weight_decay: 1e-4,
smoothness_weight: 0.1,
checkpoint_frequency: 5,
},
inference: InferenceConfig {
target_latency_ms: 0.9,
enable_simd: true,
num_threads: 1,
pin_memory: true,
cpu_affinity: None,
batch_size: 1,
},
system: SystemConfig::Traditional(TraditionalConfig { enabled: true }),
}
}
}
impl Default for TemporalSolverConfig {
fn default() -> Self {
Self {
prior: KalmanConfig {
process_noise: 0.01,
measurement_noise: 0.1,
initial_uncertainty: 1.0,
transition_model: "constant_velocity".to_string(),
update_frequency: 2000.0,
},
solver_gate: SolverGateConfig {
algorithm: "neumann".to_string(),
epsilon: 0.02,
budget: 200000,
max_cert_error: 0.02,
fallback_strategy: "kalman_only".to_string(),
},
active_selection: ActiveSelectionConfig {
k: 15,
pagerank_eps: 0.03,
samples_per_epoch: 1000,
error_weight: 0.8,
diversity_weight: 0.2,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_default_config_validation() {
let config = Config::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let yaml = serde_yaml::to_string(&config).unwrap();
let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(config.common.horizon_ms, deserialized.common.horizon_ms);
}
#[test]
fn test_file_operations() {
let config = Config::default();
let temp_file = NamedTempFile::new().unwrap();
// Test save
config.to_file(temp_file.path()).unwrap();
// Test load
let loaded_config = Config::from_file(temp_file.path()).unwrap();
assert_eq!(config.common.horizon_ms, loaded_config.common.horizon_ms);
}
#[test]
fn test_validation_errors() {
let mut config = Config::default();
// Test invalid horizon
config.common.horizon_ms = 0;
assert!(config.validate().is_err());
config.common.horizon_ms = 500; // Reset
// Test invalid learning rate
config.training.learning_rate = -1.0;
assert!(config.validate().is_err());
}
#[test]
fn test_helper_methods() {
let config = Config::default();
assert_eq!(config.window_samples(), 256); // 128ms at 2kHz
assert_eq!(config.horizon_samples(), 1000); // 500ms at 2kHz
assert_eq!(config.feature_count(), 4);
assert_eq!(config.input_shape(), (256, 4));
assert!(!config.is_temporal_solver());
}
}
@@ -0,0 +1,29 @@
//! Data augmentation for temporal neural networks
use crate::{data::TimeSeriesData, error::Result};
use serde::{Deserialize, Serialize};
/// Configuration for data augmentation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AugmentationConfig {
pub noise_std: f64,
pub time_warp_strength: f64,
pub magnitude_warp_strength: f64,
}
/// Data augmentor
pub struct DataAugmentor {
config: AugmentationConfig,
}
impl DataAugmentor {
pub fn new(config: AugmentationConfig) -> Self {
Self { config }
}
pub fn augment(&self, _data: &TimeSeriesData) -> Result<TimeSeriesData> {
// Placeholder for data augmentation
// Would implement time warping, noise addition, etc.
todo!("Data augmentation not yet implemented")
}
}
@@ -0,0 +1,140 @@
//! Data loading utilities for temporal neural networks
use crate::{
data::{TimeSeriesData, DataMetadata},
error::{Result, TemporalNeuralError},
};
use nalgebra::DMatrix;
use std::path::Path;
/// Trait for data loaders
pub trait DataLoader {
/// Load data from the specified path
fn load<P: AsRef<Path>>(path: P) -> Result<TimeSeriesData>;
}
/// CSV data loader
pub struct CsvLoader;
impl DataLoader for CsvLoader {
fn load<P: AsRef<Path>>(path: P) -> Result<TimeSeriesData> {
let path = path.as_ref();
let mut reader = csv::Reader::from_path(path)?;
// Get headers
let headers = reader.headers()?.clone();
let feature_names: Vec<String> = headers.iter().map(|h| h.to_string()).collect();
// Read all records
let mut records = Vec::new();
for result in reader.records() {
let record = result?;
let values: Result<Vec<f64>, _> = record.iter()
.map(|field| field.parse::<f64>())
.collect();
match values {
Ok(vals) => records.push(vals),
Err(e) => {
return Err(TemporalNeuralError::DataError {
message: format!("Failed to parse CSV record: {}", e),
context: Some(path.to_string_lossy().to_string()),
});
}
}
}
if records.is_empty() {
return Err(TemporalNeuralError::DataError {
message: "No data records found in CSV file".to_string(),
context: Some(path.to_string_lossy().to_string()),
});
}
let num_features = records[0].len();
let num_samples = records.len();
// Create matrix (features x samples)
let mut features = DMatrix::zeros(num_features, num_samples);
for (sample_idx, record) in records.iter().enumerate() {
if record.len() != num_features {
return Err(TemporalNeuralError::DataError {
message: format!(
"Inconsistent number of features at sample {}: expected {}, got {}",
sample_idx, num_features, record.len()
),
context: Some(path.to_string_lossy().to_string()),
});
}
for (feature_idx, &value) in record.iter().enumerate() {
features[(feature_idx, sample_idx)] = value;
}
}
// Assume 1kHz sample rate by default (can be overridden)
let sample_rate = 1000.0;
let metadata = DataMetadata {
name: path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string(),
num_samples,
num_features,
duration_seconds: num_samples as f64 / sample_rate,
source: path.to_string_lossy().to_string(),
created_at: chrono::Utc::now(),
preprocessing_history: Vec::new(),
};
Ok(TimeSeriesData {
features,
feature_names,
timestamps: None,
sample_rate,
metadata,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_csv_loading() {
// Create temporary CSV file
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "x,y,vx,vy").unwrap();
writeln!(temp_file, "1.0,2.0,0.1,0.2").unwrap();
writeln!(temp_file, "1.1,2.1,0.15,0.25").unwrap();
writeln!(temp_file, "1.2,2.2,0.2,0.3").unwrap();
let data = CsvLoader::load(temp_file.path()).unwrap();
assert_eq!(data.features.nrows(), 4); // 4 features
assert_eq!(data.features.ncols(), 3); // 3 samples
assert_eq!(data.feature_names.len(), 4);
assert_eq!(data.feature_names[0], "x");
assert_eq!(data.features[(0, 0)], 1.0);
assert_eq!(data.features[(1, 1)], 2.1);
}
#[test]
fn test_csv_loading_errors() {
// Test with non-existent file
let result = CsvLoader::load("non_existent_file.csv");
assert!(result.is_err());
// Test with malformed CSV
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "x,y").unwrap();
writeln!(temp_file, "1.0,invalid_number").unwrap();
let result = CsvLoader::load(temp_file.path());
assert!(result.is_err());
}
}
@@ -0,0 +1,629 @@
//! Data processing and management for temporal neural networks
//!
//! This module provides data loading, preprocessing, and batching functionality
//! specifically designed for temporal trajectory prediction tasks.
use crate::{
config::Config,
error::{Result, TemporalNeuralError},
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use std::path::Path;
pub mod loader;
pub mod preprocessing;
pub mod augmentation;
pub use loader::{CsvLoader, DataLoader};
pub use preprocessing::{Preprocessor, NormalizationStrategy};
pub use augmentation::{DataAugmentor, AugmentationConfig};
/// Time series data structure for temporal neural networks
#[derive(Debug, Clone)]
pub struct TimeSeriesData {
/// Raw feature data (features x time)
pub features: DMatrix<f64>,
/// Feature names
pub feature_names: Vec<String>,
/// Timestamps (optional)
pub timestamps: Option<Vec<f64>>,
/// Sample rate in Hz
pub sample_rate: f64,
/// Data metadata
pub metadata: DataMetadata,
}
/// Metadata about the dataset
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataMetadata {
/// Dataset name
pub name: String,
/// Number of samples
pub num_samples: usize,
/// Number of features
pub num_features: usize,
/// Duration in seconds
pub duration_seconds: f64,
/// Data source
pub source: String,
/// Creation timestamp
pub created_at: chrono::DateTime<chrono::Utc>,
/// Preprocessing applied
pub preprocessing_history: Vec<String>,
}
/// Windowed sample for training/evaluation
#[derive(Debug, Clone)]
pub struct WindowedSample {
/// Input window (features x window_length)
pub input: DMatrix<f64>,
/// Target value (typically 2D position)
pub target: DVector<f64>,
/// Sample metadata
pub metadata: SampleMetadata,
}
/// Metadata for individual samples
#[derive(Debug, Clone)]
pub struct SampleMetadata {
/// Original sample index
pub original_index: usize,
/// Start time in seconds
pub start_time: f64,
/// End time in seconds
pub end_time: f64,
/// Target time (prediction horizon)
pub target_time: f64,
/// Data quality score (0.0 to 1.0)
pub quality_score: f64,
}
/// Data splits for training, validation, and testing
#[derive(Debug, Clone)]
pub struct DataSplits {
/// Training data
pub train: Vec<WindowedSample>,
/// Validation data
pub val: Vec<WindowedSample>,
/// Test data
pub test: Vec<WindowedSample>,
/// Split configuration
pub config: SplitConfig,
}
/// Configuration for data splitting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitConfig {
/// Training set fraction
pub train_fraction: f64,
/// Validation set fraction
pub val_fraction: f64,
/// Test set fraction
pub test_fraction: f64,
/// Whether to shuffle data
pub shuffle: bool,
/// Random seed for reproducibility
pub random_seed: Option<u64>,
/// Stratification strategy
pub stratify_by: Option<String>,
}
impl TimeSeriesData {
/// Create new time series data
pub fn new(
features: DMatrix<f64>,
feature_names: Vec<String>,
sample_rate: f64,
name: String,
) -> Self {
let num_samples = features.ncols();
let num_features = features.nrows();
let duration_seconds = num_samples as f64 / sample_rate;
let metadata = DataMetadata {
name: name.clone(),
num_samples,
num_features,
duration_seconds,
source: "unknown".to_string(),
created_at: chrono::Utc::now(),
preprocessing_history: Vec::new(),
};
Self {
features,
feature_names,
timestamps: None,
sample_rate,
metadata,
}
}
/// Load from CSV file
pub fn from_csv<P: AsRef<Path>>(path: P) -> Result<Self> {
CsvLoader::load(path)
}
/// Create windowed samples for training
pub fn create_windowed_samples(
&self,
window_length: usize,
horizon_length: usize,
stride: usize,
) -> Result<Vec<WindowedSample>> {
if window_length == 0 || horizon_length == 0 {
return Err(TemporalNeuralError::DataError {
message: "Window length and horizon length must be positive".to_string(),
context: None,
});
}
if window_length + horizon_length > self.features.ncols() {
return Err(TemporalNeuralError::DataError {
message: "Window + horizon exceeds data length".to_string(),
context: Some(format!(
"window: {}, horizon: {}, data: {}",
window_length, horizon_length, self.features.ncols()
)),
});
}
let mut samples = Vec::new();
let num_samples = self.features.ncols();
let max_start = num_samples - window_length - horizon_length + 1;
for start_idx in (0..max_start).step_by(stride) {
let end_idx = start_idx + window_length;
let target_idx = end_idx + horizon_length - 1;
// Extract input window
let input = self.features.view((0, start_idx), (self.features.nrows(), window_length)).into();
// Extract target (assuming we predict position)
let target = if self.features.nrows() >= 2 {
DVector::from_vec(vec![
self.features[(0, target_idx)], // x
self.features[(1, target_idx)], // y
])
} else {
return Err(TemporalNeuralError::DataError {
message: "Need at least 2 features for position prediction".to_string(),
context: None,
});
};
// Create sample metadata
let start_time = start_idx as f64 / self.sample_rate;
let end_time = end_idx as f64 / self.sample_rate;
let target_time = target_idx as f64 / self.sample_rate;
let metadata = SampleMetadata {
original_index: start_idx,
start_time,
end_time,
target_time,
quality_score: self.compute_sample_quality(&input),
};
samples.push(WindowedSample {
input,
target,
metadata,
});
}
Ok(samples)
}
/// Perform temporal split (no shuffling across time)
pub fn temporal_split(
&self,
train_fraction: f64,
val_fraction: f64,
test_fraction: f64,
) -> Result<DataSplits> {
self.temporal_split_with_config(&SplitConfig {
train_fraction,
val_fraction,
test_fraction,
shuffle: false, // Never shuffle temporal data
random_seed: None,
stratify_by: None,
})
}
/// Perform temporal split with configuration
pub fn temporal_split_with_config(&self, config: &SplitConfig) -> Result<DataSplits> {
// Validate fractions
let total_fraction = config.train_fraction + config.val_fraction + config.test_fraction;
if (total_fraction - 1.0).abs() > 1e-6 {
return Err(TemporalNeuralError::DataError {
message: format!("Split fractions must sum to 1.0, got {}", total_fraction),
context: None,
});
}
// Create windowed samples (using default parameters)
let window_length = 256; // Default: 128ms at 2kHz
let horizon_length = 1000; // Default: 500ms at 2kHz
let stride = 10; // Overlap samples
let all_samples = self.create_windowed_samples(window_length, horizon_length, stride)?;
if all_samples.is_empty() {
return Err(TemporalNeuralError::DataError {
message: "No samples created from data".to_string(),
context: None,
});
}
// Temporal split (no shuffling)
let n_total = all_samples.len();
let n_train = (n_total as f64 * config.train_fraction) as usize;
let n_val = (n_total as f64 * config.val_fraction) as usize;
let train = all_samples[0..n_train].to_vec();
let val = all_samples[n_train..n_train + n_val].to_vec();
let test = all_samples[n_train + n_val..].to_vec();
Ok(DataSplits {
train,
val,
test,
config: config.clone(),
})
}
/// Compute quality score for a sample
fn compute_sample_quality(&self, input: &DMatrix<f64>) -> f64 {
// Simple quality metrics:
// 1. No NaN or infinite values
// 2. Reasonable variance (not flat line)
// 3. No extreme outliers
let mut quality = 1.0;
// Check for invalid values
for &val in input.iter() {
if !val.is_finite() {
quality *= 0.1; // Heavy penalty for invalid data
}
}
// Check variance
for row in 0..input.nrows() {
let row_data: Vec<f64> = input.row(row).iter().cloned().collect();
let mean = row_data.iter().sum::<f64>() / row_data.len() as f64;
let variance = row_data.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / row_data.len() as f64;
if variance < 1e-6 {
quality *= 0.5; // Penalty for flat data
}
}
quality.clamp(0.0, 1.0)
}
/// Get statistics about the data
pub fn get_statistics(&self) -> DataStatistics {
let n_samples = self.features.ncols();
let n_features = self.features.nrows();
let mut feature_stats = Vec::new();
for i in 0..n_features {
let row_data: Vec<f64> = self.features.row(i).iter().cloned().collect();
let mean = row_data.iter().sum::<f64>() / row_data.len() as f64;
let variance = row_data.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / row_data.len() as f64;
let min_val = row_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = row_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
feature_stats.push(FeatureStatistics {
name: self.feature_names.get(i).cloned().unwrap_or_else(|| format!("feature_{}", i)),
mean,
std_dev: variance.sqrt(),
min_value: min_val,
max_value: max_val,
num_samples: n_samples,
});
}
DataStatistics {
num_samples: n_samples,
num_features: n_features,
duration_seconds: self.metadata.duration_seconds,
sample_rate: self.sample_rate,
feature_stats,
has_missing_values: self.has_missing_values(),
data_quality_score: self.compute_overall_quality(),
}
}
/// Check if data has missing values
fn has_missing_values(&self) -> bool {
self.features.iter().any(|&x| !x.is_finite())
}
/// Compute overall data quality score
fn compute_overall_quality(&self) -> f64 {
if self.has_missing_values() {
return 0.5;
}
// Check for reasonable data ranges and variance
let mut quality_score = 1.0;
for i in 0..self.features.nrows() {
let row_data: Vec<f64> = self.features.row(i).iter().cloned().collect();
let variance = {
let mean = row_data.iter().sum::<f64>() / row_data.len() as f64;
row_data.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / row_data.len() as f64
};
if variance < 1e-6 {
quality_score *= 0.8; // Penalty for low variance
}
}
quality_score.clamp(0.0, 1.0)
}
}
/// Statistics about the dataset
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataStatistics {
/// Number of samples
pub num_samples: usize,
/// Number of features
pub num_features: usize,
/// Duration in seconds
pub duration_seconds: f64,
/// Sample rate in Hz
pub sample_rate: f64,
/// Per-feature statistics
pub feature_stats: Vec<FeatureStatistics>,
/// Whether data has missing values
pub has_missing_values: bool,
/// Overall data quality score
pub data_quality_score: f64,
}
/// Statistics for individual features
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureStatistics {
/// Feature name
pub name: String,
/// Mean value
pub mean: f64,
/// Standard deviation
pub std_dev: f64,
/// Minimum value
pub min_value: f64,
/// Maximum value
pub max_value: f64,
/// Number of samples
pub num_samples: usize,
}
impl DataSplits {
/// Get split sizes
pub fn get_sizes(&self) -> (usize, usize, usize) {
(self.train.len(), self.val.len(), self.test.len())
}
/// Validate splits
pub fn validate(&self) -> Result<()> {
if self.train.is_empty() {
return Err(TemporalNeuralError::DataError {
message: "Training set is empty".to_string(),
context: None,
});
}
if self.val.is_empty() {
return Err(TemporalNeuralError::DataError {
message: "Validation set is empty".to_string(),
context: None,
});
}
if self.test.is_empty() {
return Err(TemporalNeuralError::DataError {
message: "Test set is empty".to_string(),
context: None,
});
}
// Check input/output consistency
let first_train = &self.train[0];
let input_shape = (first_train.input.nrows(), first_train.input.ncols());
let output_dim = first_train.target.len();
for split_name in ["train", "val", "test"] {
let samples = match split_name {
"train" => &self.train,
"val" => &self.val,
"test" => &self.test,
_ => unreachable!(),
};
for (i, sample) in samples.iter().enumerate() {
let sample_input_shape = (sample.input.nrows(), sample.input.ncols());
if sample_input_shape != input_shape {
return Err(TemporalNeuralError::DataError {
message: format!(
"Input shape mismatch in {} set, sample {}: expected {:?}, got {:?}",
split_name, i, input_shape, sample_input_shape
),
context: None,
});
}
if sample.target.len() != output_dim {
return Err(TemporalNeuralError::DataError {
message: format!(
"Output dimension mismatch in {} set, sample {}: expected {}, got {}",
split_name, i, output_dim, sample.target.len()
),
context: None,
});
}
}
}
Ok(())
}
/// Get summary statistics
pub fn get_summary(&self) -> SplitSummary {
SplitSummary {
train_size: self.train.len(),
val_size: self.val.len(),
test_size: self.test.len(),
total_size: self.train.len() + self.val.len() + self.test.len(),
input_shape: self.train.first().map(|s| (s.input.nrows(), s.input.ncols())),
output_dim: self.train.first().map(|s| s.target.len()),
train_duration: self.get_duration(&self.train),
val_duration: self.get_duration(&self.val),
test_duration: self.get_duration(&self.test),
}
}
fn get_duration(&self, samples: &[WindowedSample]) -> f64 {
if samples.is_empty() {
0.0
} else {
let first = &samples[0].metadata;
let last = &samples[samples.len() - 1].metadata;
last.target_time - first.start_time
}
}
}
/// Summary of data splits
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitSummary {
/// Training set size
pub train_size: usize,
/// Validation set size
pub val_size: usize,
/// Test set size
pub test_size: usize,
/// Total size
pub total_size: usize,
/// Input shape (features, time_steps)
pub input_shape: Option<(usize, usize)>,
/// Output dimension
pub output_dim: Option<usize>,
/// Training duration in seconds
pub train_duration: f64,
/// Validation duration in seconds
pub val_duration: f64,
/// Test duration in seconds
pub test_duration: f64,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_data() -> TimeSeriesData {
// Create synthetic trajectory data: spiral motion
let n_samples = 1000;
let sample_rate = 100.0;
let mut features = DMatrix::zeros(4, n_samples); // [x, y, vx, vy]
for i in 0..n_samples {
let t = i as f64 / sample_rate;
let radius = 1.0 + 0.1 * t;
let angle = 2.0 * std::f64::consts::PI * t;
features[(0, i)] = radius * angle.cos(); // x
features[(1, i)] = radius * angle.sin(); // y
features[(2, i)] = -radius * angle.sin() * 2.0 * std::f64::consts::PI; // vx
features[(3, i)] = radius * angle.cos() * 2.0 * std::f64::consts::PI; // vy
}
TimeSeriesData::new(
features,
vec!["x".to_string(), "y".to_string(), "vx".to_string(), "vy".to_string()],
sample_rate,
"test_spiral".to_string(),
)
}
#[test]
fn test_data_creation() {
let data = create_test_data();
assert_eq!(data.features.nrows(), 4);
assert_eq!(data.features.ncols(), 1000);
assert_eq!(data.sample_rate, 100.0);
assert_eq!(data.feature_names.len(), 4);
}
#[test]
fn test_windowed_samples() {
let data = create_test_data();
let samples = data.create_windowed_samples(50, 10, 5).unwrap();
assert!(!samples.is_empty());
let first_sample = &samples[0];
assert_eq!(first_sample.input.shape(), (4, 50));
assert_eq!(first_sample.target.len(), 2);
assert!(first_sample.metadata.quality_score > 0.0);
}
#[test]
fn test_temporal_split() {
let data = create_test_data();
let splits = data.temporal_split(0.7, 0.15, 0.15).unwrap();
let (train_size, val_size, test_size) = splits.get_sizes();
assert!(train_size > 0);
assert!(val_size > 0);
assert!(test_size > 0);
// Check temporal ordering
let train_start_time = splits.train.first().unwrap().metadata.start_time;
let val_start_time = splits.val.first().unwrap().metadata.start_time;
let test_start_time = splits.test.first().unwrap().metadata.start_time;
assert!(train_start_time <= val_start_time);
assert!(val_start_time <= test_start_time);
splits.validate().unwrap();
}
#[test]
fn test_data_statistics() {
let data = create_test_data();
let stats = data.get_statistics();
assert_eq!(stats.num_features, 4);
assert_eq!(stats.num_samples, 1000);
assert!(!stats.has_missing_values);
assert!(stats.data_quality_score > 0.8);
assert_eq!(stats.feature_stats.len(), 4);
}
#[test]
fn test_split_validation() {
let data = create_test_data();
let splits = data.temporal_split(0.8, 0.1, 0.1).unwrap();
// Should pass validation
assert!(splits.validate().is_ok());
let summary = splits.get_summary();
assert!(summary.total_size > 0);
assert_eq!(summary.input_shape, Some((4, 256))); // Default window size
assert_eq!(summary.output_dim, Some(2)); // x, y position
}
}
@@ -0,0 +1,66 @@
//! Data preprocessing utilities
use crate::{data::TimeSeriesData, error::Result};
use nalgebra::DMatrix;
/// Normalization strategies
#[derive(Debug, Clone)]
pub enum NormalizationStrategy {
ZScore,
MinMax,
Robust,
}
/// Data preprocessor
pub struct Preprocessor {
strategy: NormalizationStrategy,
}
impl Preprocessor {
pub fn new(strategy: NormalizationStrategy) -> Self {
Self { strategy }
}
pub fn fit_transform(&self, data: &mut TimeSeriesData) -> Result<()> {
match self.strategy {
NormalizationStrategy::ZScore => self.z_score_normalize(&mut data.features),
NormalizationStrategy::MinMax => self.min_max_normalize(&mut data.features),
NormalizationStrategy::Robust => self.robust_normalize(&mut data.features),
}
}
fn z_score_normalize(&self, features: &mut DMatrix<f64>) -> Result<()> {
for i in 0..features.nrows() {
let row_data: Vec<f64> = features.row(i).iter().cloned().collect();
let mean = row_data.iter().sum::<f64>() / row_data.len() as f64;
let std_dev = (row_data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / row_data.len() as f64).sqrt();
if std_dev > 1e-8 {
for j in 0..features.ncols() {
features[(i, j)] = (features[(i, j)] - mean) / std_dev;
}
}
}
Ok(())
}
fn min_max_normalize(&self, features: &mut DMatrix<f64>) -> Result<()> {
for i in 0..features.nrows() {
let row_data: Vec<f64> = features.row(i).iter().cloned().collect();
let min_val = row_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = row_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
if (max_val - min_val).abs() > 1e-8 {
for j in 0..features.ncols() {
features[(i, j)] = (features[(i, j)] - min_val) / (max_val - min_val);
}
}
}
Ok(())
}
fn robust_normalize(&self, _features: &mut DMatrix<f64>) -> Result<()> {
// Placeholder for robust normalization (median-based)
Ok(())
}
}
@@ -0,0 +1,424 @@
//! Error types for the temporal neural network system
use thiserror::Error;
/// Result type alias for temporal neural network operations
pub type Result<T> = std::result::Result<T, TemporalNeuralError>;
/// Comprehensive error types for the temporal neural network system
#[derive(Error, Debug, Clone)]
pub enum TemporalNeuralError {
/// Library initialization failed
#[error("Initialization error: {reason}")]
InitializationError {
/// Reason for the failure
reason: String,
},
/// Invalid configuration provided
#[error("Configuration error: {message}")]
ConfigurationError {
/// Error message
message: String,
/// Configuration field that caused the error
field: Option<String>,
},
/// Data processing or validation error
#[error("Data error: {message}")]
DataError {
/// Error message
message: String,
/// Data context (e.g., file path, sample index)
context: Option<String>,
},
/// Model architecture or parameter error
#[error("Model error in {component}: {message}")]
ModelError {
/// Model component that failed
component: String,
/// Error message
message: String,
/// Additional context
context: Vec<(String, String)>,
},
/// Training process error
#[error("Training error at epoch {epoch}: {message}")]
TrainingError {
/// Training epoch when error occurred
epoch: usize,
/// Error message
message: String,
/// Training metrics at time of failure
metrics: Option<TrainingMetrics>,
},
/// Inference or prediction error
#[error("Inference error: {message}")]
InferenceError {
/// Error message
message: String,
/// Input that caused the error
input_shape: Option<Vec<usize>>,
/// Latency budget exceeded flag
latency_exceeded: bool,
},
/// Sublinear solver integration error
#[error("Solver error: {message}")]
SolverError {
/// Error message
message: String,
/// Solver algorithm that failed
algorithm: Option<String>,
/// Certificate error if available
certificate_error: Option<f64>,
},
/// Kalman filter error
#[error("Kalman filter error: {message}")]
KalmanError {
/// Error message
message: String,
/// Filter state when error occurred
state_dimension: Option<usize>,
},
/// Quantization or optimization error
#[error("Quantization error: {message}")]
QuantizationError {
/// Error message
message: String,
/// Quantization scheme that failed
scheme: Option<String>,
/// Accuracy loss if measured
accuracy_loss: Option<f64>,
},
/// I/O operation error
#[error("IO error: {message}")]
IoError {
/// Error message
message: String,
/// File path if applicable
path: Option<String>,
/// Underlying IO error
source: Option<std::io::Error>,
},
/// Serialization/deserialization error
#[error("Serialization error: {message}")]
SerializationError {
/// Error message
message: String,
/// Format being serialized/deserialized
format: Option<String>,
},
/// Numerical computation error
#[error("Numerical error: {message}")]
NumericalError {
/// Error message
message: String,
/// Value that caused the error
problematic_value: Option<f64>,
/// Operation that failed
operation: Option<String>,
},
/// Memory allocation or management error
#[error("Memory error: {message}")]
MemoryError {
/// Error message
message: String,
/// Requested memory size in bytes
requested_bytes: Option<usize>,
/// Available memory in bytes
available_bytes: Option<usize>,
},
/// Performance or latency constraint violation
#[error("Performance error: {message}")]
PerformanceError {
/// Error message
message: String,
/// Actual latency measured
actual_latency_ms: Option<f64>,
/// Target latency constraint
target_latency_ms: Option<f64>,
/// Performance metric that was violated
metric: Option<String>,
},
/// Validation or verification error
#[error("Validation error: {message}")]
ValidationError {
/// Error message
message: String,
/// Expected value or range
expected: Option<String>,
/// Actual value received
actual: Option<String>,
/// Validation rule that failed
rule: Option<String>,
},
/// External dependency error
#[error("External error: {message}")]
ExternalError {
/// Error message
message: String,
/// External library or service
external_source: String,
/// Original error if available
original_error: Option<String>,
},
/// Dimension mismatch error
#[error("Dimension mismatch: {message}")]
DimensionMismatch {
/// Error message
message: String,
/// Expected dimensions
expected: Option<String>,
/// Actual dimensions
actual: Option<String>,
/// Context of the operation
context: Option<String>,
},
}
/// Training metrics for error reporting
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TrainingMetrics {
/// Training loss
pub train_loss: f64,
/// Validation loss
pub val_loss: Option<f64>,
/// Learning rate
pub learning_rate: f64,
/// Number of samples processed
pub samples_processed: usize,
/// Wall clock time elapsed
pub elapsed_ms: f64,
}
impl TemporalNeuralError {
/// Create a configuration error with field context
pub fn config_field_error(field: &str, message: &str) -> Self {
Self::ConfigurationError {
message: message.to_string(),
field: Some(field.to_string()),
}
}
/// Create a data error with context
pub fn data_context_error(message: &str, context: &str) -> Self {
Self::DataError {
message: message.to_string(),
context: Some(context.to_string()),
}
}
/// Create a model error with component and context
pub fn model_component_error(component: &str, message: &str) -> Self {
Self::ModelError {
component: component.to_string(),
message: message.to_string(),
context: vec![],
}
}
/// Create a training error with metrics
pub fn training_with_metrics(epoch: usize, message: &str, metrics: TrainingMetrics) -> Self {
Self::TrainingError {
epoch,
message: message.to_string(),
metrics: Some(metrics),
}
}
/// Create an inference error with latency flag
pub fn inference_latency_error(message: &str, latency_exceeded: bool) -> Self {
Self::InferenceError {
message: message.to_string(),
input_shape: None,
latency_exceeded,
}
}
/// Create a solver error with certificate context
pub fn solver_certificate_error(message: &str, certificate_error: f64) -> Self {
Self::SolverError {
message: message.to_string(),
algorithm: None,
certificate_error: Some(certificate_error),
}
}
/// Create a performance error with latency metrics
pub fn performance_latency_error(
message: &str,
actual_ms: f64,
target_ms: f64
) -> Self {
Self::PerformanceError {
message: message.to_string(),
actual_latency_ms: Some(actual_ms),
target_latency_ms: Some(target_ms),
metric: Some("latency".to_string()),
}
}
/// Check if error is related to latency constraints
pub fn is_latency_error(&self) -> bool {
match self {
Self::InferenceError { latency_exceeded, .. } => *latency_exceeded,
Self::PerformanceError { metric, .. } => {
metric.as_ref().map_or(false, |m| m.contains("latency"))
}
_ => false,
}
}
/// Check if error is recoverable (can retry)
pub fn is_recoverable(&self) -> bool {
match self {
Self::InitializationError { .. } => false,
Self::ConfigurationError { .. } => false,
Self::DataError { .. } => false,
Self::ModelError { .. } => false,
Self::TrainingError { .. } => true, // Can retry with different params
Self::InferenceError { .. } => true, // Can retry inference
Self::SolverError { .. } => true, // Can fallback or retry
Self::KalmanError { .. } => true, // Can reset filter
Self::QuantizationError { .. } => false,
Self::IoError { .. } => true, // Can retry I/O
Self::SerializationError { .. } => false,
Self::NumericalError { .. } => true, // Can adjust parameters
Self::MemoryError { .. } => true, // Can reduce batch size
Self::PerformanceError { .. } => true, // Can optimize
Self::ValidationError { .. } => false,
Self::ExternalError { .. } => true, // Can retry external calls
Self::DimensionMismatch { .. } => false, // Usually indicates programming error
}
}
/// Get error category for logging and monitoring
pub fn category(&self) -> &'static str {
match self {
Self::InitializationError { .. } => "initialization",
Self::ConfigurationError { .. } => "configuration",
Self::DataError { .. } => "data",
Self::ModelError { .. } => "model",
Self::TrainingError { .. } => "training",
Self::InferenceError { .. } => "inference",
Self::SolverError { .. } => "solver",
Self::KalmanError { .. } => "kalman",
Self::QuantizationError { .. } => "quantization",
Self::IoError { .. } => "io",
Self::SerializationError { .. } => "serialization",
Self::NumericalError { .. } => "numerical",
Self::MemoryError { .. } => "memory",
Self::PerformanceError { .. } => "performance",
Self::ValidationError { .. } => "validation",
Self::ExternalError { .. } => "external",
Self::DimensionMismatch { .. } => "dimension_mismatch",
}
}
}
// Implement conversions from common error types
impl From<std::io::Error> for TemporalNeuralError {
fn from(err: std::io::Error) -> Self {
Self::IoError {
message: err.to_string(),
path: None,
source: Some(err),
}
}
}
impl From<serde_json::Error> for TemporalNeuralError {
fn from(err: serde_json::Error) -> Self {
Self::SerializationError {
message: err.to_string(),
format: Some("json".to_string()),
}
}
}
impl From<csv::Error> for TemporalNeuralError {
fn from(err: csv::Error) -> Self {
Self::SerializationError {
message: err.to_string(),
format: Some("csv".to_string()),
}
}
}
// Temporarily comment out until sublinear integration is fixed
// impl From<sublinear::SolverError> for TemporalNeuralError {
// fn from(err: sublinear::SolverError) -> Self {
// Self::SolverError {
// message: err.to_string(),
// algorithm: None,
// certificate_error: None,
// }
// }
// }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_categorization() {
let config_err = TemporalNeuralError::ConfigurationError {
message: "Invalid parameter".to_string(),
field: Some("learning_rate".to_string()),
};
assert_eq!(config_err.category(), "configuration");
assert!(!config_err.is_recoverable());
assert!(!config_err.is_latency_error());
let latency_err = TemporalNeuralError::inference_latency_error(
"Exceeded budget", true
);
assert_eq!(latency_err.category(), "inference");
assert!(latency_err.is_recoverable());
assert!(latency_err.is_latency_error());
}
#[test]
fn test_error_creation_helpers() {
let err = TemporalNeuralError::config_field_error(
"batch_size", "Must be positive"
);
match err {
TemporalNeuralError::ConfigurationError { field, .. } => {
assert_eq!(field, Some("batch_size".to_string()));
}
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_training_metrics_serialization() {
let metrics = TrainingMetrics {
train_loss: 0.1,
val_loss: Some(0.12),
learning_rate: 1e-3,
samples_processed: 1000,
elapsed_ms: 5000.0,
};
let json = serde_json::to_string(&metrics).unwrap();
let deserialized: TrainingMetrics = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.train_loss, metrics.train_loss);
assert_eq!(deserialized.val_loss, metrics.val_loss);
}
}
@@ -0,0 +1,34 @@
//! Memory pool for zero-allocation inference
use crate::error::{Result, TemporalNeuralError};
/// Memory pool for efficient allocation
pub struct MemoryPool {
size: usize,
used: usize,
}
impl MemoryPool {
pub fn new(size: usize) -> Result<Self> {
Ok(Self { size, used: 0 })
}
pub fn acquire(&mut self) -> Result<PreallocatedBuffer> {
Ok(PreallocatedBuffer { size: 1024 })
}
pub fn current_usage(&self) -> usize {
self.used
}
}
/// Preallocated buffer
pub struct PreallocatedBuffer {
size: usize,
}
impl Drop for PreallocatedBuffer {
fn drop(&mut self) {
// Would return buffer to pool
}
}
@@ -0,0 +1,722 @@
//! High-performance inference engine for temporal neural networks
//!
//! This module provides optimized inference capabilities with sub-millisecond
//! latency guarantees and comprehensive performance monitoring.
use crate::{
config::{Config, InferenceConfig},
error::{Result, TemporalNeuralError},
models::{ModelTrait, SystemA, SystemB},
solvers::Certificate,
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use std::time::Instant;
pub mod quantization;
pub mod simd_ops;
pub mod memory_pool;
pub use quantization::{QuantizedInference, Int8Quantizer};
pub use simd_ops::{SimdAccelerator, VectorOps};
pub use memory_pool::{MemoryPool, PreallocatedBuffer};
/// High-performance predictor with latency guarantees
pub struct Predictor {
/// Model being used for prediction
model: PredictorModel,
/// Inference configuration
config: InferenceConfig,
/// Performance monitor
monitor: PerformanceMonitor,
/// Memory pool for zero-allocation inference
memory_pool: MemoryPool,
/// SIMD accelerator
simd_accelerator: SimdAccelerator,
/// Quantization engine (if enabled)
quantizer: Option<Int8Quantizer>,
/// Inference statistics
stats: InferenceStatistics,
}
/// Model wrapper for unified inference interface
enum PredictorModel {
/// System A (traditional)
SystemA(SystemA),
/// System B (temporal solver)
SystemB(SystemB),
}
/// Prediction result with comprehensive metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prediction {
/// Predicted values
pub values: DVector<f64>,
/// Confidence score (0.0 to 1.0)
pub confidence: f64,
/// Prediction latency in microseconds
pub latency_us: f64,
/// Certificate (for System B)
pub certificate: Option<Certificate>,
/// Prediction metadata
pub metadata: PredictionMetadata,
}
/// Metadata about the prediction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictionMetadata {
/// Model type used
pub model_type: String,
/// Prediction timestamp
pub timestamp: chrono::DateTime<chrono::Utc>,
/// Input data quality score
pub input_quality: f64,
/// Whether quantization was used
pub quantized: bool,
/// Whether SIMD was used
pub simd_used: bool,
/// Memory usage for this prediction
pub memory_used_bytes: usize,
/// Detailed timing breakdown
pub timing: TimingBreakdown,
}
/// Detailed timing breakdown for performance analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingBreakdown {
/// Input preprocessing time (microseconds)
pub preprocessing_us: f64,
/// Core model inference time (microseconds)
pub inference_us: f64,
/// Post-processing time (microseconds)
pub postprocessing_us: f64,
/// Solver verification time (microseconds, System B only)
pub verification_us: Option<f64>,
/// Memory allocation time (microseconds)
pub allocation_us: f64,
/// Total time (microseconds)
pub total_us: f64,
}
/// Performance monitoring and latency tracking
#[derive(Debug)]
struct PerformanceMonitor {
/// Recent latency measurements
recent_latencies: Vec<f64>,
/// Maximum number of recent measurements to keep
max_recent: usize,
/// Target latency threshold
target_latency_us: f64,
/// Latency violations counter
violations: u64,
/// Total predictions made
total_predictions: u64,
}
/// Comprehensive inference statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceStatistics {
/// Total predictions made
pub total_predictions: u64,
/// Average latency in microseconds
pub avg_latency_us: f64,
/// P50 latency in microseconds
pub p50_latency_us: f64,
/// P99 latency in microseconds
pub p99_latency_us: f64,
/// P99.9 latency in microseconds
pub p99_9_latency_us: f64,
/// Maximum latency observed
pub max_latency_us: f64,
/// Minimum latency observed
pub min_latency_us: f64,
/// Latency target violations
pub latency_violations: u64,
/// Latency violation rate
pub violation_rate: f64,
/// Average throughput (predictions per second)
pub throughput_pred_per_sec: f64,
/// Memory usage statistics
pub memory_stats: MemoryStatistics,
/// System B specific statistics
pub system_b_stats: Option<SystemBInferenceStats>,
}
/// Memory usage statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryStatistics {
/// Current memory usage in bytes
pub current_usage_bytes: usize,
/// Peak memory usage in bytes
pub peak_usage_bytes: usize,
/// Average memory per prediction
pub avg_memory_per_prediction: f64,
/// Memory pool utilization
pub pool_utilization: f64,
}
/// System B specific inference statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemBInferenceStats {
/// Gate pass rate
pub gate_pass_rate: f64,
/// Average certificate error
pub avg_certificate_error: f64,
/// Fallback usage rate
pub fallback_rate: f64,
/// Average solver work performed
pub avg_solver_work: f64,
}
impl Predictor {
/// Create a new predictor from a trained model
pub fn new_system_a(model: SystemA, config: InferenceConfig) -> Result<Self> {
let monitor = PerformanceMonitor::new(config.target_latency_ms * 1000.0);
let memory_pool = MemoryPool::new(1024 * 1024)?; // 1MB pool
let simd_accelerator = SimdAccelerator::new(config.enable_simd);
let quantizer = if config.enable_simd {
Some(Int8Quantizer::new()?)
} else {
None
};
Ok(Self {
model: PredictorModel::SystemA(model),
config,
monitor,
memory_pool,
simd_accelerator,
quantizer,
stats: InferenceStatistics::new(),
})
}
/// Create a new predictor for System B
pub fn new_system_b(mut model: SystemB, config: InferenceConfig) -> Result<Self> {
// Prepare model for inference
model.prepare_for_inference()?;
let monitor = PerformanceMonitor::new(config.target_latency_ms * 1000.0);
let memory_pool = MemoryPool::new(2 * 1024 * 1024)?; // 2MB pool for System B
let simd_accelerator = SimdAccelerator::new(config.enable_simd);
let quantizer = if config.enable_simd {
Some(Int8Quantizer::new()?)
} else {
None
};
Ok(Self {
model: PredictorModel::SystemB(model),
config,
monitor,
memory_pool,
simd_accelerator,
quantizer,
stats: InferenceStatistics::new(),
})
}
/// Perform prediction with comprehensive monitoring
pub fn predict(&mut self, input: &DMatrix<f64>) -> Result<Prediction> {
let start_time = Instant::now();
// Pre-allocate memory from pool
let _buffer = self.memory_pool.acquire()?;
let allocation_time = start_time.elapsed().as_micros() as f64;
// Validate input
self.validate_input(input)?;
// Preprocessing
let preprocessing_start = Instant::now();
let processed_input = self.preprocess_input(input)?;
let preprocessing_time = preprocessing_start.elapsed().as_micros() as f64;
// Core inference
let inference_start = Instant::now();
let (prediction_values, certificate) = match &mut self.model {
PredictorModel::SystemA(model) => {
let pred = model.forward(&processed_input)?;
(pred, None)
}
PredictorModel::SystemB(model) => {
let pred_result = model.predict_with_solver(&processed_input)?;
// Create certificate from gate result
let certificate = Certificate {
error_bound: pred_result.gate_result.certificate_error,
confidence: pred_result.gate_result.confidence,
work_performed: pred_result.gate_result.work_performed,
algorithm: "temporal_solver".to_string(),
is_valid: pred_result.gate_result.passed,
metadata: crate::solvers::CertificateMetadata {
condition_number: None,
diagonally_dominant: false,
iterations: 0,
residual_norm: pred_result.gate_result.certificate_error,
computation_time_us: pred_result.gate_result.verification_time_us,
},
};
(pred_result.prediction, Some(certificate))
}
};
let inference_time = inference_start.elapsed().as_micros() as f64;
// Post-processing
let postprocessing_start = Instant::now();
let final_prediction = self.postprocess_prediction(&prediction_values)?;
let postprocessing_time = postprocessing_start.elapsed().as_micros() as f64;
let total_time = start_time.elapsed().as_micros() as f64;
// Update performance monitoring
self.monitor.record_latency(total_time);
// Compute confidence score
let confidence = self.compute_confidence(&final_prediction, certificate.as_ref());
// Create timing breakdown
let timing = TimingBreakdown {
preprocessing_us: preprocessing_time,
inference_us: inference_time,
postprocessing_us: postprocessing_time,
verification_us: certificate.as_ref().map(|c| c.metadata.computation_time_us),
allocation_us: allocation_time,
total_us: total_time,
};
// Create metadata
let metadata = PredictionMetadata {
model_type: match &self.model {
PredictorModel::SystemA(_) => "SystemA".to_string(),
PredictorModel::SystemB(_) => "SystemB".to_string(),
},
timestamp: chrono::Utc::now(),
input_quality: self.assess_input_quality(input),
quantized: self.quantizer.is_some(),
simd_used: self.config.enable_simd,
memory_used_bytes: self.memory_pool.current_usage(),
timing,
};
// Update statistics
self.update_statistics(total_time, &metadata, certificate.as_ref());
// Check latency constraints
if total_time > self.config.target_latency_ms * 1000.0 {
log::warn!(
"Latency constraint violated: {:.2}μs > {:.2}μs",
total_time, self.config.target_latency_ms * 1000.0
);
}
Ok(Prediction {
values: final_prediction,
confidence,
latency_us: total_time,
certificate,
metadata,
})
}
/// Batch prediction for higher throughput
pub fn predict_batch(&mut self, inputs: &[DMatrix<f64>]) -> Result<Vec<Prediction>> {
let mut predictions = Vec::with_capacity(inputs.len());
for input in inputs {
let prediction = self.predict(input)?;
predictions.push(prediction);
}
Ok(predictions)
}
/// Validate input data
fn validate_input(&self, input: &DMatrix<f64>) -> Result<()> {
let expected_shape = match &self.model {
PredictorModel::SystemA(model) => model.input_shape(),
PredictorModel::SystemB(model) => model.input_shape(),
};
let actual_shape = (input.nrows(), input.ncols());
if actual_shape != expected_shape {
return Err(TemporalNeuralError::InferenceError {
message: format!(
"Input shape mismatch: expected {:?}, got {:?}",
expected_shape, actual_shape
),
input_shape: Some(vec![actual_shape.0, actual_shape.1]),
latency_exceeded: false,
});
}
// Check for invalid values
for &val in input.iter() {
if !val.is_finite() {
return Err(TemporalNeuralError::InferenceError {
message: "Input contains invalid values (NaN or Inf)".to_string(),
input_shape: Some(vec![actual_shape.0, actual_shape.1]),
latency_exceeded: false,
});
}
}
Ok(())
}
/// Preprocess input for inference
fn preprocess_input(&self, input: &DMatrix<f64>) -> Result<DMatrix<f64>> {
// Apply SIMD optimizations if available
if self.config.enable_simd {
self.simd_accelerator.optimize_matrix(input)
} else {
Ok(input.clone())
}
}
/// Post-process prediction results
fn postprocess_prediction(&self, prediction: &DVector<f64>) -> Result<DVector<f64>> {
// Apply any final transformations
Ok(prediction.clone())
}
/// Compute confidence score for prediction
fn compute_confidence(&self, prediction: &DVector<f64>, certificate: Option<&Certificate>) -> f64 {
match certificate {
Some(cert) => cert.confidence,
None => {
// For System A, use prediction magnitude as rough confidence measure
let mag = prediction.norm();
if mag < 10.0 { 0.9 } else { 0.7 } // Simple heuristic
}
}
}
/// Assess input data quality
fn assess_input_quality(&self, input: &DMatrix<f64>) -> f64 {
// Check for reasonable variance and no outliers
let mut quality: f64 = 1.0;
for i in 0..input.nrows() {
let row_data: Vec<f64> = input.row(i).iter().cloned().collect();
let mean = row_data.iter().sum::<f64>() / row_data.len() as f64;
let variance = row_data.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / row_data.len() as f64;
// Penalize very low variance (flat signals)
if variance < 1e-6 {
quality *= 0.5;
}
// Penalize extreme values
for &val in &row_data {
if val.abs() > 100.0 {
quality *= 0.8;
}
}
}
quality.clamp(0.0, 1.0)
}
/// Update inference statistics
fn update_statistics(&mut self, latency_us: f64, metadata: &PredictionMetadata, certificate: Option<&Certificate>) {
self.stats.total_predictions += 1;
// Update latency statistics
let prev_avg = self.stats.avg_latency_us;
let n = self.stats.total_predictions as f64;
self.stats.avg_latency_us = (prev_avg * (n - 1.0) + latency_us) / n;
// Update min/max
if latency_us > self.stats.max_latency_us {
self.stats.max_latency_us = latency_us;
}
if latency_us < self.stats.min_latency_us || self.stats.min_latency_us == 0.0 {
self.stats.min_latency_us = latency_us;
}
// Update memory statistics
self.stats.memory_stats.current_usage_bytes = metadata.memory_used_bytes;
if metadata.memory_used_bytes > self.stats.memory_stats.peak_usage_bytes {
self.stats.memory_stats.peak_usage_bytes = metadata.memory_used_bytes;
}
// Update System B specific stats
if let Some(cert) = certificate {
if self.stats.system_b_stats.is_none() {
self.stats.system_b_stats = Some(SystemBInferenceStats {
gate_pass_rate: 0.0,
avg_certificate_error: 0.0,
fallback_rate: 0.0,
avg_solver_work: 0.0,
});
}
if let Some(ref mut b_stats) = self.stats.system_b_stats {
let prev_avg_error = b_stats.avg_certificate_error;
b_stats.avg_certificate_error = (prev_avg_error * (n - 1.0) + cert.error_bound) / n;
let prev_avg_work = b_stats.avg_solver_work;
b_stats.avg_solver_work = (prev_avg_work * (n - 1.0) + cert.work_performed as f64) / n;
}
}
// Update percentile statistics periodically
if self.stats.total_predictions % 100 == 0 {
self.update_percentile_statistics();
}
}
/// Update percentile statistics (P50, P99, P99.9)
fn update_percentile_statistics(&mut self) {
let mut latencies = self.monitor.recent_latencies.clone();
if latencies.is_empty() {
return;
}
latencies.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = latencies.len();
self.stats.p50_latency_us = latencies[len / 2];
self.stats.p99_latency_us = latencies[(len as f64 * 0.99) as usize];
self.stats.p99_9_latency_us = latencies[(len as f64 * 0.999) as usize];
// Update violation statistics
self.stats.latency_violations = self.monitor.violations;
self.stats.violation_rate = self.monitor.violations as f64 / self.stats.total_predictions as f64;
// Estimate throughput
if let (Some(&first), Some(&last)) = (latencies.first(), latencies.last()) {
let time_span = last - first;
if time_span > 0.0 {
self.stats.throughput_pred_per_sec = (len as f64) / (time_span / 1_000_000.0);
}
}
}
/// Get current inference statistics
pub fn get_statistics(&self) -> &InferenceStatistics {
&self.stats
}
/// Check if performance targets are being met
pub fn meets_performance_targets(&self) -> bool {
self.stats.p99_9_latency_us <= self.config.target_latency_ms * 1000.0 &&
self.stats.violation_rate <= 0.001 // Less than 0.1% violations
}
/// Reset statistics
pub fn reset_statistics(&mut self) {
self.stats = InferenceStatistics::new();
self.monitor.reset();
}
/// Warm up the predictor (important for latency-critical applications)
pub fn warmup(&mut self, warmup_iterations: usize) -> Result<()> {
log::info!("Warming up predictor with {} iterations", warmup_iterations);
// Create dummy input of the correct shape
let input_shape = match &self.model {
PredictorModel::SystemA(model) => model.input_shape(),
PredictorModel::SystemB(model) => model.input_shape(),
};
let dummy_input = DMatrix::zeros(input_shape.0, input_shape.1);
for _ in 0..warmup_iterations {
let _ = self.predict(&dummy_input)?;
}
// Reset statistics after warmup
self.reset_statistics();
log::info!("Warmup completed");
Ok(())
}
}
impl PerformanceMonitor {
fn new(target_latency_us: f64) -> Self {
Self {
recent_latencies: Vec::with_capacity(1000),
max_recent: 1000,
target_latency_us,
violations: 0,
total_predictions: 0,
}
}
fn record_latency(&mut self, latency_us: f64) {
self.recent_latencies.push(latency_us);
if self.recent_latencies.len() > self.max_recent {
self.recent_latencies.remove(0);
}
if latency_us > self.target_latency_us {
self.violations += 1;
}
self.total_predictions += 1;
}
fn reset(&mut self) {
self.recent_latencies.clear();
self.violations = 0;
self.total_predictions = 0;
}
}
impl InferenceStatistics {
fn new() -> Self {
Self {
total_predictions: 0,
avg_latency_us: 0.0,
p50_latency_us: 0.0,
p99_latency_us: 0.0,
p99_9_latency_us: 0.0,
max_latency_us: 0.0,
min_latency_us: 0.0,
latency_violations: 0,
violation_rate: 0.0,
throughput_pred_per_sec: 0.0,
memory_stats: MemoryStatistics {
current_usage_bytes: 0,
peak_usage_bytes: 0,
avg_memory_per_prediction: 0.0,
pool_utilization: 0.0,
},
system_b_stats: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
config::{ModelConfig, TemporalSolverConfig},
models::{SystemA, SystemB},
};
fn create_test_inference_config() -> InferenceConfig {
InferenceConfig {
target_latency_ms: 0.9,
enable_simd: false, // Disable for tests
num_threads: 1,
pin_memory: false,
cpu_affinity: None,
batch_size: 1,
}
}
fn create_test_model_config() -> ModelConfig {
ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 8,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
}
}
#[test]
fn test_system_a_predictor() {
let model_config = create_test_model_config();
let inference_config = create_test_inference_config();
let model = SystemA::new(&model_config).unwrap();
let mut predictor = Predictor::new_system_a(model, inference_config).unwrap();
let input = DMatrix::from_element(4, 256, 1.0);
let prediction = predictor.predict(&input).unwrap();
assert_eq!(prediction.values.len(), 2);
assert!(prediction.latency_us > 0.0);
assert!(prediction.confidence >= 0.0 && prediction.confidence <= 1.0);
assert!(prediction.certificate.is_none());
}
#[test]
fn test_system_b_predictor() {
let model_config = create_test_model_config();
let solver_config = TemporalSolverConfig::default();
let inference_config = create_test_inference_config();
let model = SystemB::new(&model_config, &solver_config).unwrap();
let mut predictor = Predictor::new_system_b(model, inference_config).unwrap();
let input = DMatrix::from_element(4, 256, 1.0);
let prediction = predictor.predict(&input).unwrap();
assert_eq!(prediction.values.len(), 2);
assert!(prediction.latency_us > 0.0);
assert!(prediction.confidence >= 0.0 && prediction.confidence <= 1.0);
assert!(prediction.certificate.is_some());
}
#[test]
fn test_batch_prediction() {
let model_config = create_test_model_config();
let inference_config = create_test_inference_config();
let model = SystemA::new(&model_config).unwrap();
let mut predictor = Predictor::new_system_a(model, inference_config).unwrap();
let inputs = vec![
DMatrix::from_element(4, 256, 1.0),
DMatrix::from_element(4, 256, 2.0),
DMatrix::from_element(4, 256, 3.0),
];
let predictions = predictor.predict_batch(&inputs).unwrap();
assert_eq!(predictions.len(), 3);
for prediction in predictions {
assert_eq!(prediction.values.len(), 2);
assert!(prediction.latency_us > 0.0);
}
}
#[test]
fn test_statistics_tracking() {
let model_config = create_test_model_config();
let inference_config = create_test_inference_config();
let model = SystemA::new(&model_config).unwrap();
let mut predictor = Predictor::new_system_a(model, inference_config).unwrap();
let input = DMatrix::from_element(4, 256, 1.0);
// Make several predictions
for _ in 0..10 {
let _ = predictor.predict(&input).unwrap();
}
let stats = predictor.get_statistics();
assert_eq!(stats.total_predictions, 10);
assert!(stats.avg_latency_us > 0.0);
}
#[test]
fn test_input_validation() {
let model_config = create_test_model_config();
let inference_config = create_test_inference_config();
let model = SystemA::new(&model_config).unwrap();
let mut predictor = Predictor::new_system_a(model, inference_config).unwrap();
// Wrong shape
let wrong_input = DMatrix::from_element(3, 100, 1.0);
assert!(predictor.predict(&wrong_input).is_err());
// Invalid values
let mut invalid_input = DMatrix::from_element(4, 256, 1.0);
invalid_input[(0, 0)] = f64::NAN;
assert!(predictor.predict(&invalid_input).is_err());
}
}
@@ -0,0 +1,33 @@
//! Quantized inference optimizations
use crate::error::{Result, TemporalNeuralError};
use nalgebra::DMatrix;
/// INT8 quantizer for inference optimization
pub struct Int8Quantizer {
initialized: bool,
}
impl Int8Quantizer {
pub fn new() -> Result<Self> {
Ok(Self { initialized: true })
}
}
/// Quantized inference engine
pub struct QuantizedInference {
quantizer: Int8Quantizer,
}
impl QuantizedInference {
pub fn new() -> Result<Self> {
Ok(Self {
quantizer: Int8Quantizer::new()?,
})
}
pub fn quantize_input(&self, _input: &DMatrix<f64>) -> Result<Vec<i8>> {
// Placeholder for quantization
Ok(vec![])
}
}
@@ -0,0 +1,41 @@
//! SIMD-accelerated operations for inference
use crate::error::Result;
use nalgebra::DMatrix;
/// SIMD accelerator for vector operations
pub struct SimdAccelerator {
enabled: bool,
}
impl SimdAccelerator {
pub fn new(enabled: bool) -> Self {
Self { enabled }
}
pub fn optimize_matrix(&self, input: &DMatrix<f64>) -> Result<DMatrix<f64>> {
if self.enabled {
// Would apply SIMD optimizations
Ok(input.clone())
} else {
Ok(input.clone())
}
}
}
/// Vector operations trait
pub trait VectorOps {
fn dot_product_simd(&self, a: &[f64], b: &[f64]) -> f64;
fn vector_add_simd(&self, a: &[f64], b: &[f64]) -> Vec<f64>;
}
impl VectorOps for SimdAccelerator {
fn dot_product_simd(&self, a: &[f64], b: &[f64]) -> f64 {
// Fallback to regular implementation
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
fn vector_add_simd(&self, a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(&x, &y)| x + y).collect()
}
}
@@ -0,0 +1,224 @@
//! # Temporal Micro-Neural Network with Sublinear Solver Integration
//!
//! This crate implements a novel temporal prediction neural network system that combines
//! traditional micro-nets with sublinear solver gating for improved latency and stability
//! in short-horizon predictions.
//!
//! ## Key Features
//!
//! - **Ultra-Low Latency**: Target <0.9ms P99.9 latency on single CPU core
//! - **Mathematical Certificates**: Sublinear solver gating with error bounds
//! - **Kalman Filter Priors**: Combine physics-based priors with residual learning
//! - **Active Selection**: PageRank-based sample selection for training efficiency
//! - **INT8 Quantization**: SIMD-optimized inference with minimal accuracy loss
//! - **Dual System A/B Testing**: Compare traditional vs temporal solver approaches
//!
//! ## Quick Start
//!
//! ```rust
//! use temporal_neural_net::{
//! models::{SystemA, SystemB},
//! data::TimeSeriesData,
//! training::Trainer,
//! inference::Predictor,
//! config::Config,
//! };
//!
//! // Load configuration
//! let config = Config::from_file("configs/B_temporal_solver.yaml")?;
//!
//! // Create data pipeline
//! let data = TimeSeriesData::from_csv("data/trajectory_data.csv")?;
//! let splits = data.temporal_split(0.7, 0.15, 0.15)?;
//!
//! // Train System B (temporal solver)
//! let mut trainer = Trainer::new(config.training);
//! let system_b = SystemB::new(config.model)?;
//! let trained_model = trainer.train(system_b, &splits.train, &splits.val)?;
//!
//! // Run inference with sub-millisecond latency
//! let predictor = Predictor::new(trained_model, config.inference)?;
//! let prediction = predictor.predict(&input_window)?;
//!
//! println!("Prediction: {:?}", prediction);
//! println!("Certificate error: {:.6}", prediction.certificate.error);
//! println!("Latency: {:.3}ms", prediction.latency_ms);
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! ## System Architecture
//!
//! ### System A - Traditional Micro-Net
//! - Residual GRU or TCN architecture
//! - Direct end-to-end prediction
//! - Standard backpropagation training
//! - FP32 training, INT8 inference
//!
//! ### System B - Temporal Solver Net
//! - Same neural architecture as System A
//! - Kalman filter prior integration
//! - Residual learning approach (net predicts residual from prior)
//! - Sublinear solver gate for mathematical verification
//! - PageRank-based active sample selection
//!
//! ## Performance Targets
//!
//! - **Latency Budget (per tick)**:
//! - Ingest: 0.10ms
//! - Prior: 0.10ms
//! - Network: 0.30ms
//! - Gate: 0.20ms
//! - Actuation: 0.10ms
//! - **Total P99.9 ≤ 0.90ms**
//!
//! ## Success Criteria
//!
//! 1. System B reduces P99.9 latency by ≥20% OR
//! 2. System B reduces P99 error by ≥15% with equal latency
//! 3. Gate pass rate ≥90% with avg cert.error ≤0.02
#![warn(missing_docs, clippy::all)]
#![allow(clippy::float_cmp)] // Numerical code often requires exact comparisons
use log::info;
/// Current version of the temporal neural network system
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// System description
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
// Re-export commonly used types and modules
pub use error::{Result, TemporalNeuralError};
pub use config::Config;
// Core modules
pub mod config;
pub mod error;
pub mod models;
pub mod solvers;
pub mod data;
pub mod training;
pub mod inference;
pub mod utils;
// Optional modules
#[cfg(feature = "benchmarks")]
pub mod benchmarks;
// WASM module (when targeting WASM)
#[cfg(target_arch = "wasm32")]
pub mod wasm;
// Re-exports for convenience
pub mod prelude {
//! Convenient re-exports for common usage patterns
pub use crate::{
config::{Config, ModelConfig, TrainingConfig, InferenceConfig},
models::{SystemA, SystemB, ModelTrait},
data::{TimeSeriesData, DataSplits, WindowedSample},
training::{Trainer, TrainingResult},
inference::{Predictor, Prediction, Certificate},
error::{Result, TemporalNeuralError},
};
// Re-export sublinear solver types
pub use ::sublinear::{
SolverAlgorithm, SolverOptions, SolverResult,
NeumannSolver, Precision,
};
// Re-export common external types
pub use nalgebra::{DVector, DMatrix};
pub use rand::Rng;
}
/// Initialize the temporal neural network library
///
/// This function should be called once at the start of your application
/// to set up proper logging and initialize any global state.
pub fn init() -> Result<()> {
#[cfg(feature = "std")]
env_logger::try_init().map_err(|e| {
TemporalNeuralError::InitializationError {
reason: format!("Failed to initialize logger: {}", e),
}
})?;
info!("Temporal Neural Network v{} initialized", VERSION);
info!("Features: {}", get_enabled_features().join(", "));
Ok(())
}
/// Get list of enabled features for this build
pub fn get_enabled_features() -> Vec<&'static str> {
let mut features = vec!["std"];
#[cfg(feature = "plots")]
features.push("plots");
#[cfg(feature = "huggingface")]
features.push("huggingface");
#[cfg(feature = "onnx")]
features.push("onnx");
#[cfg(feature = "benchmarks")]
features.push("benchmarks");
features
}
/// Build information for debugging and compatibility checks
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BuildInfo {
/// Library version
pub version: &'static str,
/// Enabled feature flags
pub features: Vec<&'static str>,
/// Whether SIMD optimizations are available
pub simd_support: bool,
/// Target architecture
pub target_arch: &'static str,
/// Build timestamp
pub build_timestamp: &'static str,
}
/// Get comprehensive build information
pub fn build_info() -> BuildInfo {
BuildInfo {
version: VERSION,
features: get_enabled_features(),
simd_support: false, // TODO: Check SIMD support properly
target_arch: std::env::consts::ARCH,
build_timestamp: option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init() {
// Should not panic and should be idempotent
let _ = init();
let _ = init();
}
#[test]
fn test_build_info() {
let info = build_info();
assert_eq!(info.version, VERSION);
assert!(!info.features.is_empty());
assert!(info.features.contains(&"std"));
}
#[test]
fn test_version_info() {
assert!(!VERSION.is_empty());
assert!(!DESCRIPTION.is_empty());
}
}
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
@@ -0,0 +1,484 @@
//! Neural network layer implementations optimized for temporal prediction
use crate::error::{Result, TemporalNeuralError};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use rand::{Rng, distributions::{Distribution, Uniform}};
use std::f64::consts::PI;
/// GRU (Gated Recurrent Unit) layer optimized for micro-networks
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GruLayer {
/// Input size
pub input_size: usize,
/// Hidden state size
pub hidden_size: usize,
/// Reset gate weights (input)
pub weight_ir: DMatrix<f64>,
/// Reset gate weights (hidden)
pub weight_hr: DMatrix<f64>,
/// Reset gate bias
pub bias_r: DVector<f64>,
/// Update gate weights (input)
pub weight_iz: DMatrix<f64>,
/// Update gate weights (hidden)
pub weight_hz: DMatrix<f64>,
/// Update gate bias
pub bias_z: DVector<f64>,
/// New gate weights (input)
pub weight_in: DMatrix<f64>,
/// New gate weights (hidden)
pub weight_hn: DMatrix<f64>,
/// New gate bias
pub bias_n: DVector<f64>,
/// Current hidden state
hidden_state: Option<DVector<f64>>,
}
impl GruLayer {
/// Create a new GRU layer
pub fn new(input_size: usize, hidden_size: usize) -> Self {
let mut layer = Self {
input_size,
hidden_size,
weight_ir: DMatrix::zeros(hidden_size, input_size),
weight_hr: DMatrix::zeros(hidden_size, hidden_size),
bias_r: DVector::zeros(hidden_size),
weight_iz: DMatrix::zeros(hidden_size, input_size),
weight_hz: DMatrix::zeros(hidden_size, hidden_size),
bias_z: DVector::zeros(hidden_size),
weight_in: DMatrix::zeros(hidden_size, input_size),
weight_hn: DMatrix::zeros(hidden_size, hidden_size),
bias_n: DVector::zeros(hidden_size),
hidden_state: None,
};
layer.initialize_weights();
layer
}
/// Initialize weights using Xavier/Glorot initialization
pub fn initialize_weights(&mut self) {
let mut rng = rand::thread_rng();
// Xavier initialization scale
let input_scale = (6.0 / (self.input_size + self.hidden_size) as f64).sqrt();
let hidden_scale = (6.0 / (2.0 * self.hidden_size) as f64).sqrt();
// Initialize input weights
let uniform = Uniform::new(-input_scale, input_scale);
self.weight_ir = DMatrix::from_fn(self.hidden_size, self.input_size, |_, _| {
uniform.sample(&mut rng)
});
self.weight_iz = DMatrix::from_fn(self.hidden_size, self.input_size, |_, _| {
uniform.sample(&mut rng)
});
self.weight_in = DMatrix::from_fn(self.hidden_size, self.input_size, |_, _| {
uniform.sample(&mut rng)
});
// Initialize hidden weights
let hidden_uniform = Uniform::new(-hidden_scale, hidden_scale);
self.weight_hr = DMatrix::from_fn(self.hidden_size, self.hidden_size, |_, _| {
hidden_uniform.sample(&mut rng)
});
self.weight_hz = DMatrix::from_fn(self.hidden_size, self.hidden_size, |_, _| {
hidden_uniform.sample(&mut rng)
});
self.weight_hn = DMatrix::from_fn(self.hidden_size, self.hidden_size, |_, _| {
hidden_uniform.sample(&mut rng)
});
// Initialize biases to small positive values for forget gates
self.bias_z.fill(1.0); // Update gate bias - helps with gradient flow
}
/// Forward pass through GRU layer
pub fn forward(&mut self, input: &DVector<f64>) -> Result<DVector<f64>> {
if input.len() != self.input_size {
return Err(TemporalNeuralError::ModelError {
component: "GruLayer".to_string(),
message: format!(
"Input size mismatch: expected {}, got {}",
self.input_size, input.len()
),
context: vec![],
});
}
// Initialize hidden state if needed
if self.hidden_state.is_none() {
self.hidden_state = Some(DVector::zeros(self.hidden_size));
}
let h_prev = self.hidden_state.as_ref().unwrap().clone();
// Reset gate: r = sigmoid(W_ir @ x + W_hr @ h + b_r)
let r = sigmoid(&(&self.weight_ir * input + &self.weight_hr * &h_prev + &self.bias_r));
// Update gate: z = sigmoid(W_iz @ x + W_hz @ h + b_z)
let z = sigmoid(&(&self.weight_iz * input + &self.weight_hz * &h_prev + &self.bias_z));
// New gate: n = tanh(W_in @ x + W_hn @ (r ⊙ h) + b_n)
let r_h = r.component_mul(&h_prev);
let n = tanh(&(&self.weight_in * input + &self.weight_hn * &r_h + &self.bias_n));
// Hidden state: h = (1 - z) ⊙ n + z ⊙ h_prev
let one_minus_z = DVector::from_element(self.hidden_size, 1.0) - &z;
let h_new = one_minus_z.component_mul(&n) + z.component_mul(&h_prev);
self.hidden_state = Some(h_new.clone());
Ok(h_new)
}
/// Process a sequence of inputs
pub fn forward_sequence(&mut self, inputs: &DMatrix<f64>) -> Result<DMatrix<f64>> {
let seq_len = inputs.ncols();
let mut outputs = DMatrix::zeros(self.hidden_size, seq_len);
for t in 0..seq_len {
let input = inputs.column(t);
let output = self.forward(&input.into())?;
outputs.set_column(t, &output);
}
Ok(outputs)
}
/// Reset hidden state
pub fn reset_state(&mut self) {
self.hidden_state = None;
}
/// Get current hidden state
pub fn get_state(&self) -> Option<&DVector<f64>> {
self.hidden_state.as_ref()
}
/// Set hidden state (for initialization or transfer)
pub fn set_state(&mut self, state: DVector<f64>) -> Result<()> {
if state.len() != self.hidden_size {
return Err(TemporalNeuralError::ModelError {
component: "GruLayer".to_string(),
message: format!(
"State size mismatch: expected {}, got {}",
self.hidden_size, state.len()
),
context: vec![],
});
}
self.hidden_state = Some(state);
Ok(())
}
/// Get parameter count
pub fn parameter_count(&self) -> usize {
self.weight_ir.len() + self.weight_hr.len() + self.bias_r.len() +
self.weight_iz.len() + self.weight_hz.len() + self.bias_z.len() +
self.weight_in.len() + self.weight_hn.len() + self.bias_n.len()
}
}
/// Temporal Convolutional Network (TCN) layer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TcnLayer {
/// Number of input channels
pub input_channels: usize,
/// Number of output channels
pub output_channels: usize,
/// Kernel size
pub kernel_size: usize,
/// Dilation factor
pub dilation: usize,
/// Convolution weights
pub weight: DMatrix<f64>,
/// Bias terms
pub bias: DVector<f64>,
/// Whether to use residual connections
pub residual: bool,
/// Residual projection weights (if needed)
pub residual_weight: Option<DMatrix<f64>>,
}
impl TcnLayer {
/// Create a new TCN layer
pub fn new(
input_channels: usize,
output_channels: usize,
kernel_size: usize,
dilation: usize,
residual: bool,
) -> Self {
let weight_size = output_channels * input_channels * kernel_size;
let mut layer = Self {
input_channels,
output_channels,
kernel_size,
dilation,
weight: DMatrix::zeros(output_channels, input_channels * kernel_size),
bias: DVector::zeros(output_channels),
residual,
residual_weight: None,
};
// Create residual projection if channel sizes don't match
if residual && input_channels != output_channels {
layer.residual_weight = Some(DMatrix::zeros(output_channels, input_channels));
}
layer.initialize_weights();
layer
}
/// Initialize weights
pub fn initialize_weights(&mut self) {
let mut rng = rand::thread_rng();
let fan_in = self.input_channels * self.kernel_size;
let fan_out = self.output_channels * self.kernel_size;
let scale = (2.0 / (fan_in + fan_out) as f64).sqrt();
let uniform = Uniform::new(-scale, scale);
self.weight = DMatrix::from_fn(self.output_channels, self.input_channels * self.kernel_size, |_, _| {
uniform.sample(&mut rng)
});
if let Some(ref mut res_weight) = self.residual_weight {
let res_scale = (2.0 / (self.input_channels + self.output_channels) as f64).sqrt();
let res_uniform = Uniform::new(-res_scale, res_scale);
*res_weight = DMatrix::from_fn(self.output_channels, self.input_channels, |_, _| {
res_uniform.sample(&mut rng)
});
}
}
/// Forward pass with causal convolution
pub fn forward(&self, input: &DMatrix<f64>) -> Result<DMatrix<f64>> {
let (channels, seq_len) = (input.nrows(), input.ncols());
if channels != self.input_channels {
return Err(TemporalNeuralError::ModelError {
component: "TcnLayer".to_string(),
message: format!(
"Input channel mismatch: expected {}, got {}",
self.input_channels, channels
),
context: vec![],
});
}
// Calculate output sequence length (causal convolution doesn't reduce length)
let output_len = seq_len;
let mut output = DMatrix::zeros(self.output_channels, output_len);
// Perform causal dilated convolution
for t in 0..output_len {
for out_ch in 0..self.output_channels {
let mut sum = self.bias[out_ch];
for k in 0..self.kernel_size {
let input_t = t as i64 - (k * self.dilation) as i64;
if input_t >= 0 {
let input_t = input_t as usize;
for in_ch in 0..self.input_channels {
let weight_idx = in_ch * self.kernel_size + k;
sum += self.weight[(out_ch, weight_idx)] * input[(in_ch, input_t)];
}
}
}
output[(out_ch, t)] = sum;
}
}
// Apply residual connection if enabled
if self.residual {
if let Some(ref res_weight) = self.residual_weight {
// Project input to match output channels
let residual = res_weight * input;
output += residual;
} else if self.input_channels == self.output_channels {
// Direct residual connection
output += input;
}
}
Ok(output)
}
/// Get parameter count
pub fn parameter_count(&self) -> usize {
let mut count = self.weight.len() + self.bias.len();
if let Some(ref res_weight) = self.residual_weight {
count += res_weight.len();
}
count
}
}
/// Dense (fully connected) layer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DenseLayer {
/// Input dimension
pub input_dim: usize,
/// Output dimension
pub output_dim: usize,
/// Weight matrix
pub weight: DMatrix<f64>,
/// Bias vector
pub bias: DVector<f64>,
/// Activation function
pub activation: ActivationFunction,
}
/// Activation function types
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ActivationFunction {
/// Linear (no activation)
Linear,
/// ReLU activation
Relu,
/// Tanh activation
Tanh,
/// Sigmoid activation
Sigmoid,
/// GELU activation
Gelu,
}
impl DenseLayer {
/// Create a new dense layer
pub fn new(input_dim: usize, output_dim: usize, activation: ActivationFunction) -> Self {
let mut layer = Self {
input_dim,
output_dim,
weight: DMatrix::zeros(output_dim, input_dim),
bias: DVector::zeros(output_dim),
activation,
};
layer.initialize_weights();
layer
}
/// Initialize weights
pub fn initialize_weights(&mut self) {
let mut rng = rand::thread_rng();
let scale = (2.0 / (self.input_dim + self.output_dim) as f64).sqrt();
let uniform = Uniform::new(-scale, scale);
self.weight = DMatrix::from_fn(self.output_dim, self.input_dim, |_, _| {
uniform.sample(&mut rng)
});
}
/// Forward pass
pub fn forward(&self, input: &DVector<f64>) -> Result<DVector<f64>> {
if input.len() != self.input_dim {
return Err(TemporalNeuralError::ModelError {
component: "DenseLayer".to_string(),
message: format!(
"Input dimension mismatch: expected {}, got {}",
self.input_dim, input.len()
),
context: vec![],
});
}
let linear_output = &self.weight * input + &self.bias;
let activated_output = apply_activation(&linear_output, self.activation);
Ok(activated_output)
}
/// Get parameter count
pub fn parameter_count(&self) -> usize {
self.weight.len() + self.bias.len()
}
}
// Activation functions
fn sigmoid(x: &DVector<f64>) -> DVector<f64> {
x.map(|val| 1.0 / (1.0 + (-val).exp()))
}
fn tanh(x: &DVector<f64>) -> DVector<f64> {
x.map(|val| val.tanh())
}
fn relu(x: &DVector<f64>) -> DVector<f64> {
x.map(|val| val.max(0.0))
}
fn gelu(x: &DVector<f64>) -> DVector<f64> {
x.map(|val| 0.5 * val * (1.0 + (val * (2.0 / PI).sqrt()).tanh()))
}
fn apply_activation(x: &DVector<f64>, activation: ActivationFunction) -> DVector<f64> {
match activation {
ActivationFunction::Linear => x.clone(),
ActivationFunction::Relu => relu(x),
ActivationFunction::Tanh => tanh(x),
ActivationFunction::Sigmoid => sigmoid(x),
ActivationFunction::Gelu => gelu(x),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gru_forward() {
let mut gru = GruLayer::new(4, 8);
let input = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let output = gru.forward(&input).unwrap();
assert_eq!(output.len(), 8);
// Test state persistence
let output2 = gru.forward(&input).unwrap();
assert_ne!(output, output2); // Should be different due to state
}
#[test]
fn test_gru_sequence() {
let mut gru = GruLayer::new(2, 4);
let inputs = DMatrix::from_row_slice(2, 3, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let outputs = gru.forward_sequence(&inputs).unwrap();
assert_eq!(outputs.shape(), (4, 3));
}
#[test]
fn test_tcn_forward() {
let tcn = TcnLayer::new(2, 4, 3, 1, true);
let input = DMatrix::from_row_slice(2, 5, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
let output = tcn.forward(&input).unwrap();
assert_eq!(output.shape(), (4, 5));
}
#[test]
fn test_dense_forward() {
let dense = DenseLayer::new(4, 2, ActivationFunction::Relu);
let input = DVector::from_vec(vec![1.0, -2.0, 3.0, -4.0]);
let output = dense.forward(&input).unwrap();
assert_eq!(output.len(), 2);
}
#[test]
fn test_activation_functions() {
let x = DVector::from_vec(vec![-2.0, -1.0, 0.0, 1.0, 2.0]);
let relu_out = apply_activation(&x, ActivationFunction::Relu);
assert_eq!(relu_out[0], 0.0); // ReLU of negative should be 0
assert_eq!(relu_out[3], 1.0); // ReLU of positive should be unchanged
let tanh_out = apply_activation(&x, ActivationFunction::Tanh);
assert!(tanh_out[2] == 0.0); // tanh(0) = 0
let sigmoid_out = apply_activation(&x, ActivationFunction::Sigmoid);
assert!(sigmoid_out[2] == 0.5); // sigmoid(0) = 0.5
}
}
@@ -0,0 +1,322 @@
//! Neural network model implementations for temporal prediction
use crate::{
config::{Config, ModelConfig},
error::{Result, TemporalNeuralError},
};
use nalgebra::{DVector, DMatrix};
use serde::{Deserialize, Serialize};
pub mod system_a;
pub mod system_b;
pub mod layers;
pub mod quantization;
pub use system_a::SystemA;
pub use system_b::SystemB;
pub use layers::{GruLayer, TcnLayer, DenseLayer};
pub use quantization::{QuantizedModel, QuantizationScheme};
/// Common trait for all neural network models
pub trait ModelTrait: Send + Sync {
/// Model parameter type
type Params: ModelParams;
/// Create a new model with the given configuration
fn new(config: &ModelConfig) -> Result<Self> where Self: Sized;
/// Forward pass through the network
fn forward(&self, input: &DMatrix<f64>) -> Result<DVector<f64>>;
/// Get model parameters for training/serialization
fn parameters(&self) -> &Self::Params;
/// Get mutable model parameters for training
fn parameters_mut(&mut self) -> &mut Self::Params;
/// Load parameters from another model (for transfer learning)
fn load_parameters(&mut self, params: Self::Params) -> Result<()>;
/// Get the number of parameters in the model
fn parameter_count(&self) -> usize;
/// Get model memory usage in bytes
fn memory_usage(&self) -> usize;
/// Get the expected input shape
fn input_shape(&self) -> (usize, usize);
/// Get the output dimension
fn output_dim(&self) -> usize;
/// Model name for identification
fn model_name(&self) -> &'static str;
/// Prepare model for inference (e.g., quantization)
fn prepare_for_inference(&mut self) -> Result<()> {
Ok(()) // Default implementation does nothing
}
/// Check if model is ready for inference
fn is_inference_ready(&self) -> bool {
true // Default implementation always ready
}
/// Get model configuration
fn config(&self) -> &ModelConfig;
/// Validate input dimensions
fn validate_input(&self, input: &DMatrix<f64>) -> Result<()> {
let expected_shape = self.input_shape();
let actual_shape = (input.nrows(), input.ncols());
if actual_shape != expected_shape {
return Err(TemporalNeuralError::ModelError {
component: self.model_name().to_string(),
message: format!(
"Input shape mismatch: expected {:?}, got {:?}",
expected_shape, actual_shape
),
context: vec![
("expected_rows".to_string(), expected_shape.0.to_string()),
("expected_cols".to_string(), expected_shape.1.to_string()),
("actual_rows".to_string(), actual_shape.0.to_string()),
("actual_cols".to_string(), actual_shape.1.to_string()),
],
});
}
Ok(())
}
}
/// Trait for model parameters
pub trait ModelParams: Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> {
/// Initialize parameters with the given configuration
fn initialize(config: &ModelConfig, rng: &mut impl rand::Rng) -> Self;
/// Get the number of parameters
fn parameter_count(&self) -> usize;
/// Get parameter memory usage in bytes
fn memory_usage(&self) -> usize {
self.parameter_count() * std::mem::size_of::<f64>()
}
/// Apply L2 regularization to parameters
fn apply_l2_regularization(&mut self, weight_decay: f64);
/// Clip gradients to prevent explosion
fn clip_gradients(&mut self, max_norm: f64);
/// Zero out gradients
fn zero_gradients(&mut self);
/// Update parameters using gradients
fn update_parameters(&mut self, learning_rate: f64);
/// Get parameter statistics for monitoring
fn parameter_stats(&self) -> ParameterStats;
}
/// Statistics about model parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterStats {
/// Mean absolute value of parameters
pub mean_abs_value: f64,
/// Standard deviation of parameters
pub std_dev: f64,
/// Minimum parameter value
pub min_value: f64,
/// Maximum parameter value
pub max_value: f64,
/// Mean absolute gradient (if available)
pub mean_abs_gradient: Option<f64>,
/// Gradient norm (if available)
pub gradient_norm: Option<f64>,
}
/// Model metadata for serialization and identification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelMetadata {
/// Model name and version
pub name: String,
/// Configuration used to create the model
pub config: ModelConfig,
/// Training information
pub training_info: Option<TrainingInfo>,
/// Performance metrics
pub performance_metrics: Option<PerformanceMetrics>,
/// Creation timestamp
pub created_at: chrono::DateTime<chrono::Utc>,
/// Last modified timestamp
pub modified_at: chrono::DateTime<chrono::Utc>,
}
/// Training information for model provenance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingInfo {
/// Number of training epochs completed
pub epochs_trained: u32,
/// Training dataset size
pub training_samples: usize,
/// Validation dataset size
pub validation_samples: usize,
/// Final training loss
pub final_train_loss: f64,
/// Final validation loss
pub final_val_loss: f64,
/// Training time in seconds
pub training_time_sec: f64,
/// Optimizer used
pub optimizer: String,
/// Learning rate schedule
pub learning_rate_schedule: Vec<(u32, f64)>,
}
/// Performance metrics for model evaluation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
/// Mean squared error on test set
pub mse: f64,
/// Mean absolute error on test set
pub mae: f64,
/// P90 absolute error
pub p90_error: f64,
/// P99 absolute error
pub p99_error: f64,
/// Average inference latency in milliseconds
pub avg_latency_ms: f64,
/// P50 inference latency in milliseconds
pub p50_latency_ms: f64,
/// P99.9 inference latency in milliseconds
pub p99_9_latency_ms: f64,
/// Model memory usage in bytes
pub memory_usage_bytes: usize,
/// Throughput in predictions per second
pub throughput_pred_per_sec: f64,
}
/// Factory function to create models from configuration
pub fn create_model(config: &Config) -> Result<Box<dyn ModelTrait<Params = Box<dyn ModelParams>>>> {
match config.system {
crate::config::SystemConfig::Traditional(_) => {
let system_a = SystemA::new(&config.model)?;
Ok(Box::new(system_a) as Box<dyn ModelTrait<Params = Box<dyn ModelParams>>>)
}
crate::config::SystemConfig::TemporalSolver(_) => {
let system_b = SystemB::new(&config.model)?;
Ok(Box::new(system_b) as Box<dyn ModelTrait<Params = Box<dyn ModelParams>>>)
}
}
}
/// Load a model from file
pub fn load_model(path: &std::path::Path) -> Result<(Box<dyn ModelTrait<Params = Box<dyn ModelParams>>>, ModelMetadata)> {
let content = std::fs::read_to_string(path)?;
let data: serde_json::Value = serde_json::from_str(&content)?;
let metadata: ModelMetadata = serde_json::from_value(data["metadata"].clone())?;
let model = match metadata.name.as_str() {
"SystemA" => {
let system_a = SystemA::new(&metadata.config)?;
Box::new(system_a) as Box<dyn ModelTrait<Params = Box<dyn ModelParams>>>
}
"SystemB" => {
let system_b = SystemB::new(&metadata.config)?;
Box::new(system_b) as Box<dyn ModelTrait<Params = Box<dyn ModelParams>>>
}
_ => {
return Err(TemporalNeuralError::ModelError {
component: "model_loader".to_string(),
message: format!("Unknown model type: {}", metadata.name),
context: vec![],
});
}
};
Ok((model, metadata))
}
/// Save a model to file
pub fn save_model(
model: &dyn ModelTrait<Params = Box<dyn ModelParams>>,
metadata: &ModelMetadata,
path: &std::path::Path,
) -> Result<()> {
let data = serde_json::json!({
"metadata": metadata,
"parameters": model.parameters(),
});
let content = serde_json::to_string_pretty(&data)?;
std::fs::write(path, content)?;
Ok(())
}
/// Model comparison utilities
pub mod comparison {
use super::*;
/// Compare two models and return similarity metrics
pub fn compare_models(
model1: &dyn ModelTrait<Params = Box<dyn ModelParams>>,
model2: &dyn ModelTrait<Params = Box<dyn ModelParams>>,
) -> ModelComparison {
let params1 = model1.parameters();
let params2 = model2.parameters();
ModelComparison {
parameter_count_diff: (model1.parameter_count() as i64 - model2.parameter_count() as i64).abs() as usize,
memory_usage_diff: (model1.memory_usage() as i64 - model2.memory_usage() as i64).abs() as usize,
architecture_match: model1.model_name() == model2.model_name(),
config_match: model1.config() == model2.config(),
}
}
/// Result of model comparison
#[derive(Debug, Clone)]
pub struct ModelComparison {
/// Difference in parameter count
pub parameter_count_diff: usize,
/// Difference in memory usage
pub memory_usage_diff: usize,
/// Whether architectures match
pub architecture_match: bool,
/// Whether configurations match
pub config_match: bool,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
#[test]
fn test_model_creation() {
let config = Config::default();
let model = create_model(&config);
assert!(model.is_ok());
}
#[test]
fn test_model_validation() {
let config = Config::default();
let model = create_model(&config).unwrap();
let input = DMatrix::zeros(10, 4); // Wrong size
assert!(model.validate_input(&input).is_err());
let correct_input = DMatrix::zeros(256, 4); // Correct size
assert!(model.validate_input(&correct_input).is_ok());
}
#[test]
fn test_parameter_stats() {
// This would be implemented once we have concrete parameter types
// For now, just test that the trait compiles
}
}
@@ -0,0 +1,191 @@
//! INT8 quantization implementation for neural network optimization
use crate::error::{Result, TemporalNeuralError};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
/// Quantization scheme for model optimization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum QuantizationScheme {
/// 8-bit integer quantization
Int8,
/// 4-bit integer quantization
Int4,
/// Binary quantization
Binary,
}
/// Quantized model wrapper
#[derive(Debug, Clone)]
pub struct QuantizedModel {
/// Quantization scheme used
pub scheme: QuantizationScheme,
/// Quantized weights
pub quantized_weights: Vec<i8>,
/// Scale factors for dequantization
pub scales: Vec<f32>,
/// Zero points for symmetric quantization
pub zero_points: Vec<i8>,
/// Original model shape information
pub shape_info: ModelShapeInfo,
}
/// Information about model structure for quantization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelShapeInfo {
/// Layer dimensions
pub layer_shapes: Vec<(usize, usize)>,
/// Total parameter count
pub total_params: usize,
/// Memory savings achieved
pub memory_savings_ratio: f64,
}
impl QuantizedModel {
/// Create quantized model from floating point weights
pub fn quantize_int8(weights: &[f64]) -> Result<Self> {
if weights.is_empty() {
return Err(TemporalNeuralError::QuantizationError {
message: "Cannot quantize empty weight vector".to_string(),
scheme: Some("int8".to_string()),
accuracy_loss: None,
});
}
let mut quantized_weights = Vec::with_capacity(weights.len());
let mut scales = Vec::new();
let mut zero_points = Vec::new();
// For simplicity, quantize the entire weight vector with single scale/zero_point
// In practice, you'd quantize per-layer or per-channel
let min_val = weights.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = weights.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let scale = (max_val - min_val) / 255.0; // 8-bit range
let zero_point = (-min_val / scale).round() as i8;
scales.push(scale as f32);
zero_points.push(zero_point);
for &weight in weights {
let quantized = ((weight / scale) + zero_point as f64).round().clamp(-128.0, 127.0) as i8;
quantized_weights.push(quantized);
}
let shape_info = ModelShapeInfo {
layer_shapes: vec![(weights.len(), 1)], // Simplified
total_params: weights.len(),
memory_savings_ratio: 4.0, // 32-bit -> 8-bit
};
Ok(Self {
scheme: QuantizationScheme::Int8,
quantized_weights,
scales,
zero_points,
shape_info,
})
}
/// Dequantize weights back to floating point
pub fn dequantize(&self) -> Result<Vec<f64>> {
if self.quantized_weights.len() != self.shape_info.total_params {
return Err(TemporalNeuralError::QuantizationError {
message: "Weight count mismatch during dequantization".to_string(),
scheme: Some(format!("{:?}", self.scheme)),
accuracy_loss: None,
});
}
let mut dequantized = Vec::with_capacity(self.quantized_weights.len());
let scale = self.scales[0] as f64; // Using first scale for simplicity
let zero_point = self.zero_points[0] as f64;
for &quantized_weight in &self.quantized_weights {
let dequantized_weight = (quantized_weight as f64 - zero_point) * scale;
dequantized.push(dequantized_weight);
}
Ok(dequantized)
}
/// Get memory usage in bytes
pub fn memory_usage(&self) -> usize {
self.quantized_weights.len() * std::mem::size_of::<i8>() +
self.scales.len() * std::mem::size_of::<f32>() +
self.zero_points.len() * std::mem::size_of::<i8>()
}
/// Estimate accuracy loss from quantization
pub fn estimate_accuracy_loss(&self, original_weights: &[f64]) -> Result<f64> {
let dequantized = self.dequantize()?;
if dequantized.len() != original_weights.len() {
return Err(TemporalNeuralError::QuantizationError {
message: "Length mismatch for accuracy estimation".to_string(),
scheme: Some(format!("{:?}", self.scheme)),
accuracy_loss: None,
});
}
// Compute MSE between original and dequantized weights
let mse = original_weights.iter()
.zip(dequantized.iter())
.map(|(&orig, &deq)| (orig - deq).powi(2))
.sum::<f64>() / original_weights.len() as f64;
Ok(mse.sqrt()) // Return RMSE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_int8_quantization() {
let weights = vec![1.0, 2.5, -1.5, 0.0, 3.2, -2.1];
let quantized = QuantizedModel::quantize_int8(&weights).unwrap();
assert_eq!(quantized.quantized_weights.len(), weights.len());
assert_eq!(quantized.scales.len(), 1);
assert_eq!(quantized.zero_points.len(), 1);
}
#[test]
fn test_dequantization() {
let weights = vec![1.0, 2.5, -1.5, 0.0, 3.2, -2.1];
let quantized = QuantizedModel::quantize_int8(&weights).unwrap();
let dequantized = quantized.dequantize().unwrap();
assert_eq!(dequantized.len(), weights.len());
// Check that dequantized values are reasonably close to originals
for (orig, deq) in weights.iter().zip(dequantized.iter()) {
assert!((orig - deq).abs() < 0.1); // Should be within quantization error
}
}
#[test]
fn test_accuracy_loss_estimation() {
let weights = vec![1.0, 2.5, -1.5, 0.0, 3.2, -2.1];
let quantized = QuantizedModel::quantize_int8(&weights).unwrap();
let accuracy_loss = quantized.estimate_accuracy_loss(&weights).unwrap();
assert!(accuracy_loss >= 0.0);
assert!(accuracy_loss < 1.0); // Should be reasonable for this simple case
}
#[test]
fn test_memory_usage() {
let weights = vec![1.0; 1000];
let quantized = QuantizedModel::quantize_int8(&weights).unwrap();
let memory_usage = quantized.memory_usage();
assert!(memory_usage > 0);
// Should use roughly 1/4 the memory (32-bit -> 8-bit)
let original_memory = weights.len() * std::mem::size_of::<f64>();
assert!(memory_usage < original_memory / 2);
}
}
@@ -0,0 +1,549 @@
//! System A: Traditional micro-neural network implementation
//!
//! This module implements the baseline traditional micro-network for comparison
//! against the temporal solver approach. It uses standard neural architectures
//! without mathematical solver integration.
use crate::{
config::ModelConfig,
error::{Result, TemporalNeuralError},
models::{
layers::{GruLayer, TcnLayer, DenseLayer, ActivationFunction},
ModelTrait, ModelParams, ParameterStats,
},
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
/// System A: Traditional micro-neural network
///
/// This system implements a standard approach using either GRU or TCN
/// for sequence modeling followed by a dense output layer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemA {
/// Model configuration
config: ModelConfig,
/// Parameters for the system
params: SystemAParams,
/// Current architecture type
architecture: ArchitectureType,
}
/// Architecture variants for System A
#[derive(Debug, Clone, Serialize, Deserialize)]
enum ArchitectureType {
/// GRU-based architecture
Gru {
/// GRU layers
layers: Vec<GruLayer>,
/// Output projection layer
output_layer: DenseLayer,
},
/// TCN-based architecture
Tcn {
/// TCN layers
layers: Vec<TcnLayer>,
/// Global pooling type
pooling: PoolingType,
/// Output projection layer
output_layer: DenseLayer,
},
}
/// Pooling types for TCN architecture
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
enum PoolingType {
/// Take the last timestep
Last,
/// Global average pooling
Average,
/// Global max pooling
Max,
/// Attention-based pooling
Attention,
}
/// Parameters for System A
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemAParams {
/// All parameter values flattened
values: Vec<f64>,
/// Gradients for backpropagation
gradients: Vec<f64>,
/// Parameter structure metadata
structure: ParameterStructure,
}
/// Metadata about parameter structure for serialization
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ParameterStructure {
/// Total parameter count
total_params: usize,
/// Parameter layout (layer_name -> (start_idx, count))
layout: Vec<(String, usize, usize)>,
}
impl SystemA {
/// Create a new System A model
pub fn new(config: &ModelConfig) -> Result<Self> {
let architecture = match config.model_type.as_str() {
"micro_gru" => Self::create_gru_architecture(config)?,
"micro_tcn" => Self::create_tcn_architecture(config)?,
_ => {
return Err(TemporalNeuralError::ConfigurationError {
message: format!("Unsupported model type: {}", config.model_type),
field: Some("model_type".to_string()),
});
}
};
let params = SystemAParams::initialize(config, &mut rand::thread_rng());
Ok(Self {
config: config.clone(),
params,
architecture,
})
}
fn create_gru_architecture(config: &ModelConfig) -> Result<ArchitectureType> {
let mut layers = Vec::new();
// First layer takes input features
let first_layer = GruLayer::new(config.hidden_size, config.hidden_size);
layers.push(first_layer);
// Additional layers if specified
for _ in 1..config.num_layers {
let layer = GruLayer::new(config.hidden_size, config.hidden_size);
layers.push(layer);
}
// Output layer - predict 2D position (x, y) at horizon
let activation = match config.activation.as_str() {
"relu" => ActivationFunction::Relu,
"tanh" => ActivationFunction::Tanh,
"gelu" => ActivationFunction::Gelu,
"linear" => ActivationFunction::Linear,
_ => ActivationFunction::Tanh, // Default
};
let output_layer = DenseLayer::new(config.hidden_size, 2, activation);
Ok(ArchitectureType::Gru {
layers,
output_layer,
})
}
fn create_tcn_architecture(config: &ModelConfig) -> Result<ArchitectureType> {
let mut layers = Vec::new();
let mut channels = config.hidden_size as usize;
// Create dilated TCN layers
for layer_idx in 0..config.num_layers as usize {
let dilation = 2_usize.pow(layer_idx as u32);
let layer = TcnLayer::new(
channels,
config.hidden_size as usize,
3, // kernel size
dilation,
config.residual,
);
layers.push(layer);
channels = config.hidden_size as usize;
}
// Global pooling strategy
let pooling = PoolingType::Last; // Simple approach - take last timestep
// Output layer
let activation = match config.activation.as_str() {
"relu" => ActivationFunction::Relu,
"tanh" => ActivationFunction::Tanh,
"gelu" => ActivationFunction::Gelu,
"linear" => ActivationFunction::Linear,
_ => ActivationFunction::Tanh,
};
let output_layer = DenseLayer::new(config.hidden_size as usize, 2, activation);
Ok(ArchitectureType::Tcn {
layers,
pooling,
output_layer,
})
}
/// Apply pooling to TCN output
fn apply_pooling(output: &DMatrix<f64>, pooling: PoolingType) -> DVector<f64> {
match pooling {
PoolingType::Last => {
// Take the last timestep
output.column(output.ncols() - 1).into()
}
PoolingType::Average => {
// Global average pooling
let mut result = DVector::zeros(output.nrows());
for i in 0..output.nrows() {
result[i] = output.row(i).mean();
}
result
}
PoolingType::Max => {
// Global max pooling
let mut result = DVector::zeros(output.nrows());
for i in 0..output.nrows() {
result[i] = output.row(i).max();
}
result
}
PoolingType::Attention => {
// Simple attention: uniform weights for now
// In a full implementation, this would be learned
let seq_len = output.ncols();
let weights = DVector::from_element(seq_len, 1.0 / seq_len as f64);
output * weights
}
}
}
}
impl ModelTrait for SystemA {
type Params = SystemAParams;
fn new(config: &ModelConfig) -> Result<Self> {
Self::new(config)
}
fn forward(&self, input: &DMatrix<f64>) -> Result<DVector<f64>> {
self.validate_input(input)?;
match &self.architecture {
ArchitectureType::Gru { layers, output_layer } => {
// Process through GRU layers
let mut current_input = input.clone();
// For GRU, we need to process the sequence through each layer
// Input shape: (features, sequence_length)
// We need to process timestep by timestep
let seq_len = input.ncols();
let mut hidden_states = Vec::new();
// Create temporary GRU layers for forward pass (clone to avoid mutation issues)
let mut temp_layers: Vec<GruLayer> = layers.iter().cloned().collect();
// Process sequence through all GRU layers
for t in 0..seq_len {
let timestep_input = current_input.column(t).into();
// Process through each layer for this timestep
let mut layer_input = timestep_input;
for layer in temp_layers.iter_mut() {
layer_input = layer.forward(&layer_input)?;
}
hidden_states.push(layer_input);
}
// Take the final hidden state
let final_hidden = hidden_states.last().unwrap();
// Project to output
output_layer.forward(final_hidden)
}
ArchitectureType::Tcn { layers, pooling, output_layer } => {
// Process through TCN layers
let mut current_output = input.clone();
for layer in layers {
current_output = layer.forward(&current_output)?;
}
// Apply pooling to get fixed-size representation
let pooled = Self::apply_pooling(&current_output, *pooling);
// Project to output
output_layer.forward(&pooled)
}
}
}
fn parameters(&self) -> &Self::Params {
&self.params
}
fn parameters_mut(&mut self) -> &mut Self::Params {
&mut self.params
}
fn load_parameters(&mut self, params: Self::Params) -> Result<()> {
if params.values.len() != self.params.values.len() {
return Err(TemporalNeuralError::ModelError {
component: "SystemA".to_string(),
message: format!(
"Parameter count mismatch: expected {}, got {}",
self.params.values.len(),
params.values.len()
),
context: vec![],
});
}
self.params = params;
Ok(())
}
fn parameter_count(&self) -> usize {
self.params.parameter_count()
}
fn memory_usage(&self) -> usize {
self.params.memory_usage()
}
fn input_shape(&self) -> (usize, usize) {
// Returns (sequence_length, features)
// This will be set based on configuration in a real implementation
(256, 4) // Default for 128ms window at 2kHz with 4 features
}
fn output_dim(&self) -> usize {
2 // Predicting 2D position (x, y)
}
fn model_name(&self) -> &'static str {
"SystemA"
}
fn config(&self) -> &ModelConfig {
&self.config
}
fn prepare_for_inference(&mut self) -> Result<()> {
// For System A, we might apply quantization here
// For now, just mark as ready
Ok(())
}
}
impl ModelParams for SystemAParams {
fn initialize(config: &ModelConfig, rng: &mut impl rand::Rng) -> Self {
// Calculate total parameter count based on architecture
let total_params = Self::calculate_param_count(config);
// Initialize with small random values
let values: Vec<f64> = (0..total_params)
.map(|_| (rng.sample(rand::distributions::Standard) as f64 - 0.5) * 0.02)
.collect();
let gradients = vec![0.0; total_params];
let structure = Self::build_structure(config);
Self {
values,
gradients,
structure,
}
}
fn parameter_count(&self) -> usize {
self.values.len()
}
fn apply_l2_regularization(&mut self, weight_decay: f64) {
for (param, grad) in self.values.iter().zip(self.gradients.iter_mut()) {
*grad += weight_decay * param;
}
}
fn clip_gradients(&mut self, max_norm: f64) {
let grad_norm: f64 = self.gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
if grad_norm > max_norm {
let scale = max_norm / grad_norm;
for grad in self.gradients.iter_mut() {
*grad *= scale;
}
}
}
fn zero_gradients(&mut self) {
self.gradients.fill(0.0);
}
fn update_parameters(&mut self, learning_rate: f64) {
for (param, grad) in self.values.iter_mut().zip(self.gradients.iter()) {
*param -= learning_rate * grad;
}
}
fn parameter_stats(&self) -> ParameterStats {
let n = self.values.len() as f64;
let mean = self.values.iter().sum::<f64>() / n;
let variance = self.values.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / n;
let mean_abs_value = self.values.iter().map(|x| x.abs()).sum::<f64>() / n;
let min_value = self.values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_value = self.values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let mean_abs_gradient = if !self.gradients.is_empty() {
Some(self.gradients.iter().map(|g| g.abs()).sum::<f64>() / self.gradients.len() as f64)
} else {
None
};
let gradient_norm = if !self.gradients.is_empty() {
Some(self.gradients.iter().map(|g| g * g).sum::<f64>().sqrt())
} else {
None
};
ParameterStats {
mean_abs_value,
std_dev: variance.sqrt(),
min_value,
max_value,
mean_abs_gradient,
gradient_norm,
}
}
}
impl SystemAParams {
fn calculate_param_count(config: &ModelConfig) -> usize {
match config.model_type.as_str() {
"micro_gru" => {
// GRU parameters: 3 * (input_size * hidden + hidden * hidden + hidden) per layer
// Plus output layer: hidden * output + output
let hidden = config.hidden_size as usize;
let input_size = hidden; // Simplified assumption
let gru_params_per_layer = 3 * (input_size * hidden + hidden * hidden + hidden);
let gru_params = gru_params_per_layer * config.num_layers as usize;
let output_params = hidden * 2 + 2; // 2D output
gru_params + output_params
}
"micro_tcn" => {
// TCN parameters: kernel_size * input_channels * output_channels + output_channels per layer
// Plus output layer
let channels = config.hidden_size as usize;
let kernel_size = 3;
let tcn_params_per_layer = kernel_size * channels * channels + channels;
let tcn_params = tcn_params_per_layer * config.num_layers as usize;
let output_params = channels * 2 + 2; // 2D output
tcn_params + output_params
}
_ => 1000, // Default fallback
}
}
fn build_structure(config: &ModelConfig) -> ParameterStructure {
let mut layout = Vec::new();
let mut offset = 0;
match config.model_type.as_str() {
"micro_gru" => {
for i in 0..config.num_layers {
let layer_params = Self::gru_layer_param_count(config.hidden_size as usize);
layout.push((format!("gru_layer_{}", i), offset, layer_params));
offset += layer_params;
}
}
"micro_tcn" => {
for i in 0..config.num_layers {
let layer_params = Self::tcn_layer_param_count(config.hidden_size as usize);
layout.push((format!("tcn_layer_{}", i), offset, layer_params));
offset += layer_params;
}
}
_ => {}
}
// Output layer
let output_params = config.hidden_size as usize * 2 + 2;
layout.push(("output_layer".to_string(), offset, output_params));
ParameterStructure {
total_params: Self::calculate_param_count(config),
layout,
}
}
fn gru_layer_param_count(hidden_size: usize) -> usize {
3 * (hidden_size * hidden_size + hidden_size * hidden_size + hidden_size)
}
fn tcn_layer_param_count(channels: usize) -> usize {
3 * channels * channels + channels // kernel_size=3
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_system_a_creation() {
let mut config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 16,
num_layers: 2,
dropout: 0.1,
residual: true,
activation: "tanh".to_string(),
layer_norm: false,
};
let system = SystemA::new(&config).unwrap();
assert_eq!(system.model_name(), "SystemA");
assert_eq!(system.output_dim(), 2);
// Test TCN variant
config.model_type = "micro_tcn".to_string();
let system_tcn = SystemA::new(&config).unwrap();
assert_eq!(system_tcn.model_name(), "SystemA");
}
#[test]
fn test_forward_pass() {
let config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 8,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let system = SystemA::new(&config).unwrap();
let input = DMatrix::from_element(4, 10, 1.0); // 4 features, 10 timesteps
let output = system.forward(&input).unwrap();
assert_eq!(output.len(), 2);
}
#[test]
fn test_parameter_operations() {
let config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 4,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let system = SystemA::new(&config).unwrap();
let param_count = system.parameter_count();
assert!(param_count > 0);
let stats = system.parameters().parameter_stats();
assert!(stats.mean_abs_value >= 0.0);
}
}
@@ -0,0 +1,480 @@
//! System B: Temporal solver neural network implementation
//!
//! This module implements the novel temporal solver approach that combines
//! neural networks with Kalman filter priors and sublinear solver gating
//! for improved latency and mathematical verification.
use crate::{
config::{ModelConfig, TemporalSolverConfig},
error::{Result, TemporalNeuralError},
models::{
layers::{GruLayer, TcnLayer, DenseLayer, ActivationFunction},
ModelTrait, ModelParams, ParameterStats,
system_a::{SystemA, SystemAParams}, // Reuse SystemA architecture
},
solvers::{KalmanFilter, SolverGate, PageRankSelector},
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
/// System B: Temporal solver neural network
///
/// This system combines a neural network (same architecture as System A)
/// with Kalman filter priors and sublinear solver verification.
/// The key innovation is residual learning: the network predicts the
/// residual between the Kalman prior and the true target.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemB {
/// Base neural network (same as System A)
base_network: SystemA,
/// Kalman filter for prior predictions
kalman_filter: KalmanFilter,
/// Sublinear solver gate for verification
solver_gate: SolverGate,
/// PageRank-based active selector for training
active_selector: Option<PageRankSelector>,
/// Temporal solver configuration
solver_config: TemporalSolverConfig,
/// Whether the system is in training or inference mode
inference_mode: bool,
}
/// Prediction result from System B
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemBPrediction {
/// Kalman filter prior prediction
pub prior: DVector<f64>,
/// Neural network residual prediction
pub residual: DVector<f64>,
/// Final combined prediction (prior + residual)
pub prediction: DVector<f64>,
/// Solver gate verification result
pub gate_result: GateResult,
/// Computation timing breakdown
pub timing: PredictionTiming,
}
/// Result from the solver gate verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateResult {
/// Whether the gate passed (prediction is verified)
pub passed: bool,
/// Certificate error from sublinear solver
pub certificate_error: f64,
/// Computational work performed
pub work_performed: u64,
/// Fallback strategy used if gate failed
pub fallback_used: Option<String>,
}
/// Timing breakdown for performance analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictionTiming {
/// Time spent in Kalman filter update (microseconds)
pub kalman_us: f64,
/// Time spent in neural network forward pass (microseconds)
pub network_us: f64,
/// Time spent in solver gate verification (microseconds)
pub gate_us: f64,
/// Total prediction time (microseconds)
pub total_us: f64,
}
impl SystemB {
/// Create a new System B model
pub fn new(config: &ModelConfig, solver_config: &TemporalSolverConfig) -> Result<Self> {
// Create base network (same architecture as System A)
let base_network = SystemA::new(config)?;
// Initialize Kalman filter
let kalman_filter = KalmanFilter::new(&solver_config.prior)?;
// Initialize solver gate
let solver_gate = SolverGate::new(&solver_config.solver_gate)?;
// Initialize active selector for training
let active_selector = Some(PageRankSelector::new(&solver_config.active_selection)?);
Ok(Self {
base_network,
kalman_filter,
solver_gate,
active_selector,
solver_config: solver_config.clone(),
inference_mode: false,
})
}
/// Set inference mode (disables active selector)
pub fn set_inference_mode(&mut self, inference_mode: bool) {
self.inference_mode = inference_mode;
if inference_mode {
self.active_selector = None; // Save memory in inference
}
}
/// Predict with full temporal solver pipeline
pub fn predict_with_solver(&mut self, input: &DMatrix<f64>) -> Result<SystemBPrediction> {
let start_time = std::time::Instant::now();
// Step 1: Update Kalman filter and get prior
let kalman_start = std::time::Instant::now();
let prior = self.kalman_filter.predict(input)?;
let kalman_time = kalman_start.elapsed().as_micros() as f64;
// Step 2: Neural network predicts residual
let network_start = std::time::Instant::now();
let residual = self.base_network.forward(input)?;
let network_time = network_start.elapsed().as_micros() as f64;
// Step 3: Combine prior and residual
let prediction = &prior + &residual;
// Step 4: Verify with solver gate
let gate_start = std::time::Instant::now();
let gate_result = self.solver_gate.verify(&prior, &residual, &prediction)?;
let gate_time = gate_start.elapsed().as_micros() as f64;
let total_time = start_time.elapsed().as_micros() as f64;
// Apply fallback if gate failed
let final_prediction = if gate_result.passed {
prediction
} else {
self.apply_fallback(&prior, &residual, &gate_result)?
};
Ok(SystemBPrediction {
prior,
residual,
prediction: final_prediction,
gate_result,
timing: PredictionTiming {
kalman_us: kalman_time,
network_us: network_time,
gate_us: gate_time,
total_us: total_time,
},
})
}
/// Apply fallback strategy when solver gate fails
fn apply_fallback(
&self,
prior: &DVector<f64>,
residual: &DVector<f64>,
gate_result: &GateResult,
) -> Result<DVector<f64>> {
match self.solver_config.solver_gate.fallback_strategy.as_str() {
"kalman_only" => Ok(prior.clone()),
"hold_last" => {
// In a real implementation, this would hold the last verified prediction
// For now, use the prior
Ok(prior.clone())
}
"disable_gate" => {
// Use the combined prediction anyway
Ok(prior + residual)
}
"weighted_blend" => {
// Blend based on certificate error
let cert_error = gate_result.certificate_error;
let weight = 1.0 / (1.0 + cert_error * 10.0); // Sigmoid-like weighting
Ok(prior * (1.0 - weight) + (prior + residual) * weight)
}
_ => {
return Err(TemporalNeuralError::ConfigurationError {
message: format!(
"Unknown fallback strategy: {}",
self.solver_config.solver_gate.fallback_strategy
),
field: Some("fallback_strategy".to_string()),
});
}
}
}
/// Update Kalman filter state with ground truth (for training)
pub fn update_kalman_state(&mut self, measurement: &DVector<f64>) -> Result<()> {
self.kalman_filter.update(measurement)
}
/// Get active sample selector for training
pub fn active_selector(&mut self) -> Option<&mut PageRankSelector> {
self.active_selector.as_mut()
}
/// Reset all internal states
pub fn reset_states(&mut self) -> Result<()> {
self.kalman_filter.reset()?;
self.solver_gate.reset()?;
if let Some(ref mut selector) = self.active_selector {
selector.reset()?;
}
Ok(())
}
/// Get solver statistics for monitoring
pub fn get_solver_stats(&self) -> SolverStats {
SolverStats {
gate_pass_rate: self.solver_gate.get_pass_rate(),
avg_certificate_error: self.solver_gate.get_avg_certificate_error(),
avg_computational_work: self.solver_gate.get_avg_work(),
kalman_prediction_error: self.kalman_filter.get_prediction_error(),
total_predictions: self.solver_gate.get_prediction_count(),
}
}
/// Configure solver parameters dynamically
pub fn configure_solver(&mut self, epsilon: Option<f64>, budget: Option<u64>) -> Result<()> {
if let Some(eps) = epsilon {
self.solver_gate.set_epsilon(eps)?;
}
if let Some(b) = budget {
self.solver_gate.set_budget(b)?;
}
Ok(())
}
}
/// Statistics from the solver components
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverStats {
/// Percentage of predictions that passed the gate
pub gate_pass_rate: f64,
/// Average certificate error across all predictions
pub avg_certificate_error: f64,
/// Average computational work performed
pub avg_computational_work: f64,
/// Kalman filter prediction error
pub kalman_prediction_error: f64,
/// Total number of predictions made
pub total_predictions: u64,
}
impl ModelTrait for SystemB {
type Params = SystemAParams; // Reuse SystemA parameters
fn new(config: &ModelConfig) -> Result<Self> {
// This is a simplified constructor - in practice would need full solver config
let default_solver_config = TemporalSolverConfig::default();
Self::new(config, &default_solver_config)
}
fn forward(&self, input: &DMatrix<f64>) -> Result<DVector<f64>> {
// For the ModelTrait interface, we provide a simplified forward pass
// without solver verification (for compatibility)
let prior = self.kalman_filter.predict_const(input)?;
let residual = self.base_network.forward(input)?;
Ok(&prior + &residual)
}
fn parameters(&self) -> &Self::Params {
self.base_network.parameters()
}
fn parameters_mut(&mut self) -> &mut Self::Params {
self.base_network.parameters_mut()
}
fn load_parameters(&mut self, params: Self::Params) -> Result<()> {
self.base_network.load_parameters(params)
}
fn parameter_count(&self) -> usize {
self.base_network.parameter_count()
}
fn memory_usage(&self) -> usize {
self.base_network.memory_usage() +
self.kalman_filter.memory_usage() +
self.solver_gate.memory_usage() +
self.active_selector.as_ref().map_or(0, |s| s.memory_usage())
}
fn input_shape(&self) -> (usize, usize) {
self.base_network.input_shape()
}
fn output_dim(&self) -> usize {
self.base_network.output_dim()
}
fn model_name(&self) -> &'static str {
"SystemB"
}
fn config(&self) -> &ModelConfig {
self.base_network.config()
}
fn prepare_for_inference(&mut self) -> Result<()> {
self.set_inference_mode(true);
self.base_network.prepare_for_inference()?;
self.kalman_filter.prepare_for_inference()?;
self.solver_gate.prepare_for_inference()?;
Ok(())
}
fn is_inference_ready(&self) -> bool {
self.inference_mode &&
self.base_network.is_inference_ready() &&
self.kalman_filter.is_inference_ready() &&
self.solver_gate.is_inference_ready()
}
}
/// Training utilities specific to System B
impl SystemB {
/// Compute residual learning loss
pub fn compute_residual_loss(
&self,
predictions: &[SystemBPrediction],
targets: &[DVector<f64>],
) -> Result<f64> {
if predictions.len() != targets.len() {
return Err(TemporalNeuralError::TrainingError {
epoch: 0,
message: "Predictions and targets length mismatch".to_string(),
metrics: None,
});
}
let mut total_loss = 0.0;
let mut valid_samples = 0;
for (pred, target) in predictions.iter().zip(targets.iter()) {
// Residual learning: target - prior should equal residual
let expected_residual = target - &pred.prior;
let residual_error = &pred.residual - &expected_residual;
// MSE loss
let sample_loss: f64 = residual_error.iter().map(|x| x * x).sum();
total_loss += sample_loss;
valid_samples += 1;
}
if valid_samples == 0 {
return Err(TemporalNeuralError::TrainingError {
epoch: 0,
message: "No valid samples for loss computation".to_string(),
metrics: None,
});
}
Ok(total_loss / valid_samples as f64)
}
/// Compute additional regularization terms
pub fn compute_regularization_loss(&self, predictions: &[SystemBPrediction]) -> f64 {
let mut reg_loss = 0.0;
let smoothness_weight = 0.1; // From config
// Smoothness penalty on velocity predictions
for pred in predictions {
if pred.prediction.len() >= 2 {
// Assume prediction is [x, y] - penalize large velocities
let velocity_mag = pred.prediction[0].powi(2) + pred.prediction[1].powi(2);
reg_loss += smoothness_weight * velocity_mag;
}
}
// Gate verification penalty
let gate_penalty = 1.0;
for pred in predictions {
if !pred.gate_result.passed {
reg_loss += gate_penalty * pred.gate_result.certificate_error;
}
}
reg_loss / predictions.len() as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_system_b_creation() {
let config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 8,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let solver_config = TemporalSolverConfig::default();
let system = SystemB::new(&config, &solver_config).unwrap();
assert_eq!(system.model_name(), "SystemB");
assert_eq!(system.output_dim(), 2);
}
#[test]
fn test_inference_mode() {
let config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 4,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let solver_config = TemporalSolverConfig::default();
let mut system = SystemB::new(&config, &solver_config).unwrap();
assert!(!system.inference_mode);
assert!(system.active_selector.is_some());
system.set_inference_mode(true);
assert!(system.inference_mode);
assert!(system.active_selector.is_none());
}
#[test]
fn test_forward_pass() {
let config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 4,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let solver_config = TemporalSolverConfig::default();
let system = SystemB::new(&config, &solver_config).unwrap();
let input = DMatrix::from_element(4, 10, 1.0);
let output = system.forward(&input).unwrap();
assert_eq!(output.len(), 2);
}
#[test]
fn test_solver_stats() {
let config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 4,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let solver_config = TemporalSolverConfig::default();
let system = SystemB::new(&config, &solver_config).unwrap();
let stats = system.get_solver_stats();
assert!(stats.gate_pass_rate >= 0.0 && stats.gate_pass_rate <= 1.0);
assert!(stats.avg_certificate_error >= 0.0);
}
}
@@ -0,0 +1,463 @@
//! Kalman filter implementation for temporal prior predictions
//!
//! This module provides a Kalman filter implementation optimized for
//! providing high-quality prior predictions for the temporal neural network.
use crate::{
config::KalmanConfig,
error::{Result, TemporalNeuralError},
solvers::InferenceReadyTrait,
};
use nalgebra::{DMatrix, DVector, Matrix2, Vector2};
use serde::{Deserialize, Serialize};
/// Kalman filter for providing temporal priors
///
/// This filter tracks position and velocity for 2D trajectory prediction,
/// providing physics-based priors that the neural network can refine.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KalmanFilter {
/// Configuration
config: KalmanConfig,
/// Current state estimate [x, y, vx, vy]
state: DVector<f64>,
/// State covariance matrix
covariance: DMatrix<f64>,
/// State transition matrix
transition_matrix: DMatrix<f64>,
/// Process noise covariance
process_noise: DMatrix<f64>,
/// Measurement noise covariance
measurement_noise: DMatrix<f64>,
/// Measurement matrix (maps state to observations)
measurement_matrix: DMatrix<f64>,
/// Whether filter is initialized
initialized: bool,
/// Last prediction for error tracking
last_prediction: Option<DVector<f64>>,
/// Prediction error history
prediction_errors: Vec<f64>,
/// Time of last update
last_update_time: Option<std::time::Instant>,
/// Ready for inference flag
inference_ready: bool,
}
impl KalmanFilter {
/// Create a new Kalman filter
pub fn new(config: &KalmanConfig) -> Result<Self> {
let state_dim = 4; // [x, y, vx, vy]
let obs_dim = 2; // [x, y]
let state = DVector::zeros(state_dim);
let covariance = DMatrix::identity(state_dim, state_dim) * config.initial_uncertainty;
// Create state transition matrix based on model type
let transition_matrix = match config.transition_model.as_str() {
"constant_velocity" => Self::create_constant_velocity_matrix(1.0 / config.update_frequency),
"constant_acceleration" => Self::create_constant_acceleration_matrix(1.0 / config.update_frequency),
_ => {
return Err(TemporalNeuralError::ConfigurationError {
message: format!("Unknown transition model: {}", config.transition_model),
field: Some("transition_model".to_string()),
});
}
};
// Process noise (uncertainty in dynamics)
let dt = 1.0 / config.update_frequency;
let process_noise = Self::create_process_noise_matrix(config.process_noise, dt);
// Measurement noise
let measurement_noise = DMatrix::identity(obs_dim, obs_dim) * config.measurement_noise;
// Measurement matrix (observe position only)
let measurement_matrix = DMatrix::from_row_slice(obs_dim, state_dim, &[
1.0, 0.0, 0.0, 0.0, // x
0.0, 1.0, 0.0, 0.0, // y
]);
Ok(Self {
config: config.clone(),
state,
covariance,
transition_matrix,
process_noise,
measurement_noise,
measurement_matrix,
initialized: false,
last_prediction: None,
prediction_errors: Vec::new(),
last_update_time: None,
inference_ready: false,
})
}
/// Create constant velocity transition matrix
fn create_constant_velocity_matrix(dt: f64) -> DMatrix<f64> {
DMatrix::from_row_slice(4, 4, &[
1.0, 0.0, dt, 0.0, // x = x + vx*dt
0.0, 1.0, 0.0, dt, // y = y + vy*dt
0.0, 0.0, 1.0, 0.0, // vx = vx
0.0, 0.0, 0.0, 1.0, // vy = vy
])
}
/// Create constant acceleration transition matrix
fn create_constant_acceleration_matrix(dt: f64) -> DMatrix<f64> {
let dt2 = dt * dt / 2.0;
DMatrix::from_row_slice(4, 4, &[
1.0, 0.0, dt, 0.0, // x = x + vx*dt
0.0, 1.0, 0.0, dt, // y = y + vy*dt
0.0, 0.0, 0.9, 0.0, // vx = 0.9*vx (decay)
0.0, 0.0, 0.0, 0.9, // vy = 0.9*vy (decay)
])
}
/// Create process noise covariance matrix
fn create_process_noise_matrix(noise_level: f64, dt: f64) -> DMatrix<f64> {
let dt2 = dt * dt;
let dt3 = dt * dt2 / 2.0;
let dt4 = dt2 * dt2 / 4.0;
// Q matrix for constant velocity model
DMatrix::from_row_slice(4, 4, &[
dt4, 0.0, dt3, 0.0, // x variance and x-vx covariance
0.0, dt4, 0.0, dt3, // y variance and y-vy covariance
dt3, 0.0, dt2, 0.0, // vx-x covariance and vx variance
0.0, dt3, 0.0, dt2, // vy-y covariance and vy variance
]) * noise_level
}
/// Predict next state (time update)
pub fn predict(&self, _input: &DMatrix<f64>) -> Result<DVector<f64>> {
if !self.initialized {
// Return zero prediction if not initialized
return Ok(DVector::zeros(2));
}
// Predict state: x_k|k-1 = F * x_k-1|k-1
let predicted_state = &self.transition_matrix * &self.state;
// Extract position prediction [x, y]
Ok(DVector::from_vec(vec![predicted_state[0], predicted_state[1]]))
}
/// Const version of predict for immutable contexts
pub fn predict_const(&self, _input: &DMatrix<f64>) -> Result<DVector<f64>> {
self.predict(_input)
}
/// Update filter with measurement (measurement update)
pub fn update(&mut self, measurement: &DVector<f64>) -> Result<()> {
if measurement.len() != 2 {
return Err(TemporalNeuralError::KalmanError {
message: format!("Expected 2D measurement, got {}", measurement.len()),
state_dimension: Some(self.state.len()),
});
}
if !self.initialized {
// Initialize state with first measurement
self.state[0] = measurement[0]; // x
self.state[1] = measurement[1]; // y
self.state[2] = 0.0; // vx = 0
self.state[3] = 0.0; // vy = 0
self.initialized = true;
self.last_update_time = Some(std::time::Instant::now());
return Ok(());
}
// Time update (predict)
let predicted_state = &self.transition_matrix * &self.state;
let predicted_covariance = &self.transition_matrix * &self.covariance * self.transition_matrix.transpose() + &self.process_noise;
// Measurement update (correct)
let innovation = measurement - &self.measurement_matrix * &predicted_state;
let innovation_covariance = &self.measurement_matrix * &predicted_covariance * self.measurement_matrix.transpose() + &self.measurement_noise;
// Kalman gain
let kalman_gain = &predicted_covariance * self.measurement_matrix.transpose() * innovation_covariance.try_inverse().ok_or_else(|| {
TemporalNeuralError::KalmanError {
message: "Innovation covariance matrix is not invertible".to_string(),
state_dimension: Some(self.state.len()),
}
})?;
// Update state and covariance
self.state = predicted_state + &kalman_gain * innovation;
let identity = DMatrix::identity(self.state.len(), self.state.len());
self.covariance = (identity - &kalman_gain * &self.measurement_matrix) * predicted_covariance;
// Track prediction error if we had a previous prediction
if let Some(ref last_pred) = self.last_prediction {
let error = (measurement - last_pred).norm();
self.prediction_errors.push(error);
// Keep only recent errors (for memory efficiency)
if self.prediction_errors.len() > 1000 {
self.prediction_errors.remove(0);
}
}
self.last_prediction = Some(measurement.clone());
self.last_update_time = Some(std::time::Instant::now());
Ok(())
}
/// Get current state estimate
pub fn get_state(&self) -> &DVector<f64> {
&self.state
}
/// Get current covariance estimate
pub fn get_covariance(&self) -> &DMatrix<f64> {
&self.covariance
}
/// Get prediction uncertainty (position covariance)
pub fn get_prediction_uncertainty(&self) -> Matrix2<f64> {
if !self.initialized {
return Matrix2::identity() * 1000.0; // High uncertainty
}
// Extract position covariance [x, y]
Matrix2::new(
self.covariance[(0, 0)], self.covariance[(0, 1)],
self.covariance[(1, 0)], self.covariance[(1, 1)],
)
}
/// Get average prediction error
pub fn get_prediction_error(&self) -> f64 {
if self.prediction_errors.is_empty() {
0.0
} else {
self.prediction_errors.iter().sum::<f64>() / self.prediction_errors.len() as f64
}
}
/// Check if filter is well-conditioned
pub fn is_well_conditioned(&self) -> bool {
if !self.initialized {
return false;
}
// Check covariance matrix condition
let max_eigenvalue = self.covariance.diagonal().max();
let min_eigenvalue = self.covariance.diagonal().min();
if min_eigenvalue <= 0.0 {
return false;
}
let condition_number = max_eigenvalue / min_eigenvalue;
condition_number < 1e6 // Reasonable condition number
}
/// Predict at specific time horizon
pub fn predict_at_horizon(&self, horizon_seconds: f64) -> Result<Vector2<f64>> {
if !self.initialized {
return Ok(Vector2::zeros());
}
// Create transition matrix for the specific horizon
let transition = match self.config.transition_model.as_str() {
"constant_velocity" => Self::create_constant_velocity_matrix(horizon_seconds),
"constant_acceleration" => Self::create_constant_acceleration_matrix(horizon_seconds),
_ => self.transition_matrix.clone(),
};
// Predict state at horizon
let predicted_state = &transition * &self.state;
Ok(Vector2::new(predicted_state[0], predicted_state[1]))
}
/// Adaptive tuning based on recent performance
pub fn adapt_parameters(&mut self) -> Result<()> {
if self.prediction_errors.len() < 10 {
return Ok(()); // Need enough data
}
let recent_errors: Vec<f64> = self.prediction_errors
.iter()
.rev()
.take(10)
.cloned()
.collect();
let avg_error = recent_errors.iter().sum::<f64>() / recent_errors.len() as f64;
// Adapt process noise based on error
if avg_error > 0.1 {
// High error - increase process noise
self.process_noise *= 1.1;
} else if avg_error < 0.01 {
// Low error - decrease process noise
self.process_noise *= 0.95;
}
// Clamp process noise to reasonable bounds
let min_noise = 1e-6;
let max_noise = 1.0;
for element in self.process_noise.iter_mut() {
*element = element.clamp(min_noise, max_noise);
}
Ok(())
}
}
impl InferenceReadyTrait for KalmanFilter {
fn prepare_for_inference(&mut self) -> Result<()> {
// Ensure filter is in a good state for inference
if !self.initialized {
return Err(TemporalNeuralError::KalmanError {
message: "Kalman filter not initialized".to_string(),
state_dimension: Some(self.state.len()),
});
}
if !self.is_well_conditioned() {
return Err(TemporalNeuralError::KalmanError {
message: "Kalman filter is poorly conditioned".to_string(),
state_dimension: Some(self.state.len()),
});
}
// Clear prediction error history to save memory
self.prediction_errors.clear();
self.inference_ready = true;
Ok(())
}
fn is_inference_ready(&self) -> bool {
self.inference_ready && self.initialized && self.is_well_conditioned()
}
fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>() +
self.state.len() * std::mem::size_of::<f64>() +
self.covariance.len() * std::mem::size_of::<f64>() +
self.transition_matrix.len() * std::mem::size_of::<f64>() +
self.process_noise.len() * std::mem::size_of::<f64>() +
self.measurement_noise.len() * std::mem::size_of::<f64>() +
self.measurement_matrix.len() * std::mem::size_of::<f64>() +
self.prediction_errors.len() * std::mem::size_of::<f64>()
}
fn reset(&mut self) -> Result<()> {
self.state.fill(0.0);
self.covariance = DMatrix::identity(self.state.len(), self.state.len()) * self.config.initial_uncertainty;
self.initialized = false;
self.last_prediction = None;
self.prediction_errors.clear();
self.last_update_time = None;
self.inference_ready = false;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> KalmanConfig {
KalmanConfig {
process_noise: 0.01,
measurement_noise: 0.1,
initial_uncertainty: 1.0,
transition_model: "constant_velocity".to_string(),
update_frequency: 100.0,
}
}
#[test]
fn test_kalman_creation() {
let config = create_test_config();
let filter = KalmanFilter::new(&config).unwrap();
assert!(!filter.initialized);
assert_eq!(filter.state.len(), 4);
assert_eq!(filter.covariance.shape(), (4, 4));
}
#[test]
fn test_kalman_initialization() {
let config = create_test_config();
let mut filter = KalmanFilter::new(&config).unwrap();
let measurement = DVector::from_vec(vec![1.0, 2.0]);
filter.update(&measurement).unwrap();
assert!(filter.initialized);
assert_eq!(filter.state[0], 1.0);
assert_eq!(filter.state[1], 2.0);
}
#[test]
fn test_prediction_tracking() {
let config = create_test_config();
let mut filter = KalmanFilter::new(&config).unwrap();
// Initialize
let measurement1 = DVector::from_vec(vec![0.0, 0.0]);
filter.update(&measurement1).unwrap();
// Update with moving trajectory
let measurement2 = DVector::from_vec(vec![1.0, 1.0]);
filter.update(&measurement2).unwrap();
// Predict should show movement
let input = DMatrix::zeros(4, 10); // Dummy input
let prediction = filter.predict(&input).unwrap();
assert!(prediction[0] > 0.5); // Should predict continued movement
assert!(prediction[1] > 0.5);
}
#[test]
fn test_horizon_prediction() {
let config = create_test_config();
let mut filter = KalmanFilter::new(&config).unwrap();
// Initialize with trajectory
filter.update(&DVector::from_vec(vec![0.0, 0.0])).unwrap();
filter.update(&DVector::from_vec(vec![1.0, 0.0])).unwrap(); // Moving right
let horizon_pred = filter.predict_at_horizon(0.5).unwrap(); // 0.5 seconds
assert!(horizon_pred[0] > 1.0); // Should be further right
assert!(horizon_pred[1].abs() < 0.1); // Should stay near y=0
}
#[test]
fn test_condition_checking() {
let config = create_test_config();
let mut filter = KalmanFilter::new(&config).unwrap();
assert!(!filter.is_well_conditioned()); // Not initialized
filter.update(&DVector::from_vec(vec![0.0, 0.0])).unwrap();
assert!(filter.is_well_conditioned()); // Should be well-conditioned after init
}
#[test]
fn test_inference_preparation() {
let config = create_test_config();
let mut filter = KalmanFilter::new(&config).unwrap();
// Should fail before initialization
assert!(filter.prepare_for_inference().is_err());
// Initialize
filter.update(&DVector::from_vec(vec![0.0, 0.0])).unwrap();
// Should succeed after initialization
assert!(filter.prepare_for_inference().is_ok());
assert!(filter.is_inference_ready());
}
}
@@ -0,0 +1,341 @@
//! Sublinear solver integration and supporting components
//!
//! This module provides the key innovation of the temporal neural network:
//! integration with sublinear-time mathematical solvers for prediction
//! verification and Kalman filter priors.
use crate::error::{Result, TemporalNeuralError};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
pub mod kalman;
// pub mod solver_gate; // Temporarily disabled
pub mod solver_gate_simple;
pub mod pagerank_selector;
pub use kalman::KalmanFilter;
// pub use solver_gate::SolverGate;
pub use solver_gate_simple::{SolverGate, SolverGateConfig, GateResult, SolverGateStats};
pub use pagerank_selector::PageRankSelector;
/// Trait for components that can be prepared for inference
pub trait InferenceReadyTrait {
/// Prepare the component for inference mode
fn prepare_for_inference(&mut self) -> Result<()>;
/// Check if the component is ready for inference
fn is_inference_ready(&self) -> bool;
/// Get memory usage in bytes
fn memory_usage(&self) -> usize;
/// Reset component state
fn reset(&mut self) -> Result<()>;
}
/// Common mathematical utilities used by solver components
pub mod math_utils {
use nalgebra::{DMatrix, DVector};
/// Compute Jacobian matrix numerically using finite differences
pub fn compute_jacobian<F>(
f: F,
x: &DVector<f64>,
h: f64,
) -> nalgebra::DMatrix<f64>
where
F: Fn(&DVector<f64>) -> DVector<f64>,
{
let n = x.len();
let fx = f(x);
let m = fx.len();
let mut jacobian = DMatrix::zeros(m, n);
for j in 0..n {
let mut x_plus = x.clone();
x_plus[j] += h;
let fx_plus = f(&x_plus);
for i in 0..m {
jacobian[(i, j)] = (fx_plus[i] - fx[i]) / h;
}
}
jacobian
}
/// Compute matrix condition number estimate
pub fn condition_number_estimate(matrix: &DMatrix<f64>) -> f64 {
// Simple estimate using ratio of max to min singular values
// In practice, use proper SVD
let max_elem = matrix.iter().map(|x| x.abs()).fold(0.0, f64::max);
let min_elem = matrix.iter()
.filter(|&&x| x.abs() > 1e-12)
.map(|x| x.abs())
.fold(f64::INFINITY, f64::min);
if min_elem.is_infinite() || min_elem == 0.0 {
f64::INFINITY
} else {
max_elem / min_elem
}
}
/// Check if matrix is diagonally dominant
pub fn is_diagonally_dominant(matrix: &DMatrix<f64>) -> bool {
let (rows, cols) = matrix.shape();
if rows != cols {
return false;
}
for i in 0..rows {
let diagonal_elem = matrix[(i, i)].abs();
let off_diagonal_sum: f64 = (0..cols)
.filter(|&j| j != i)
.map(|j| matrix[(i, j)].abs())
.sum();
if diagonal_elem <= off_diagonal_sum {
return false;
}
}
true
}
/// Create a simple test matrix that is diagonally dominant
pub fn create_test_dd_matrix(size: usize) -> DMatrix<f64> {
let mut matrix = DMatrix::zeros(size, size);
for i in 0..size {
// Set diagonal elements to be larger than sum of off-diagonal
let mut off_diag_sum = 0.0;
for j in 0..size {
if i != j {
let val = (i + j + 1) as f64 * 0.1;
matrix[(i, j)] = val;
off_diag_sum += val.abs();
}
}
matrix[(i, i)] = off_diag_sum * 1.5 + 1.0; // Ensure diagonal dominance
}
matrix
}
/// Spectral radius estimation using power iteration
pub fn spectral_radius_estimate(matrix: &DMatrix<f64>, max_iterations: usize) -> f64 {
let n = matrix.nrows();
if n != matrix.ncols() {
return f64::NAN;
}
let mut v = DVector::from_vec((0..n).map(|_| rand::random::<f64>()).collect());
v /= v.norm();
let mut lambda = 0.0;
for _ in 0..max_iterations {
let new_v = matrix * &v;
lambda = v.dot(&new_v);
v = new_v;
if v.norm() > 1e-10 {
v /= v.norm();
} else {
break;
}
}
lambda.abs()
}
}
/// Certificate information from solver verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Certificate {
/// Estimated error bound
pub error_bound: f64,
/// Confidence level (0.0 to 1.0)
pub confidence: f64,
/// Computational work performed
pub work_performed: u64,
/// Solver algorithm used
pub algorithm: String,
/// Whether the certificate is valid
pub is_valid: bool,
/// Additional metadata
pub metadata: CertificateMetadata,
}
/// Additional metadata for certificates
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificateMetadata {
/// Matrix condition number
pub condition_number: Option<f64>,
/// Whether matrix was diagonally dominant
pub diagonally_dominant: bool,
/// Convergence iterations performed
pub iterations: u32,
/// Final residual norm
pub residual_norm: f64,
/// Computation time in microseconds
pub computation_time_us: f64,
}
impl Certificate {
/// Create a new certificate
pub fn new(
error_bound: f64,
confidence: f64,
work_performed: u64,
algorithm: String,
) -> Self {
Self {
error_bound,
confidence,
work_performed,
algorithm,
is_valid: error_bound >= 0.0 && confidence >= 0.0 && confidence <= 1.0,
metadata: CertificateMetadata {
condition_number: None,
diagonally_dominant: false,
iterations: 0,
residual_norm: 0.0,
computation_time_us: 0.0,
},
}
}
/// Check if certificate passes given tolerance
pub fn passes_tolerance(&self, tolerance: f64) -> bool {
self.is_valid && self.error_bound <= tolerance
}
/// Get quality score (0.0 to 1.0, higher is better)
pub fn quality_score(&self) -> f64 {
if !self.is_valid {
return 0.0;
}
// Combine error bound (lower is better) and confidence (higher is better)
let error_score = 1.0 / (1.0 + self.error_bound);
let confidence_score = self.confidence;
(error_score + confidence_score) / 2.0
}
}
/// Factory for creating solver components
pub struct SolverFactory;
impl SolverFactory {
/// Create a Kalman filter with the given configuration
pub fn create_kalman_filter(config: &crate::config::KalmanConfig) -> Result<KalmanFilter> {
KalmanFilter::new(config)
}
/// Create a solver gate with the given configuration
pub fn create_solver_gate(config: &SolverGateConfig) -> Result<SolverGate> {
SolverGate::new(config)
}
/// Create a PageRank selector with the given configuration
pub fn create_pagerank_selector(
config: &crate::config::ActiveSelectionConfig,
) -> Result<PageRankSelector> {
PageRankSelector::new(config)
}
}
/// Performance monitoring for solver components
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverPerformanceMetrics {
/// Average prediction latency in microseconds
pub avg_latency_us: f64,
/// P50 latency in microseconds
pub p50_latency_us: f64,
/// P99 latency in microseconds
pub p99_latency_us: f64,
/// P99.9 latency in microseconds
pub p99_9_latency_us: f64,
/// Success rate (0.0 to 1.0)
pub success_rate: f64,
/// Memory usage in bytes
pub memory_usage_bytes: usize,
/// Total predictions made
pub total_predictions: u64,
/// Average certificate error
pub avg_certificate_error: f64,
/// Gate pass rate
pub gate_pass_rate: f64,
}
impl Default for SolverPerformanceMetrics {
fn default() -> Self {
Self {
avg_latency_us: 0.0,
p50_latency_us: 0.0,
p99_latency_us: 0.0,
p99_9_latency_us: 0.0,
success_rate: 1.0,
memory_usage_bytes: 0,
total_predictions: 0,
avg_certificate_error: 0.0,
gate_pass_rate: 1.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::math_utils::*;
#[test]
fn test_diagonal_dominance() {
let dd_matrix = create_test_dd_matrix(3);
assert!(is_diagonally_dominant(&dd_matrix));
// Test non-diagonally dominant matrix
let non_dd = DMatrix::from_row_slice(2, 2, &[1.0, 2.0, 3.0, 1.0]);
assert!(!is_diagonally_dominant(&non_dd));
}
#[test]
fn test_certificate_validation() {
let cert = Certificate::new(0.01, 0.95, 1000, "neumann".to_string());
assert!(cert.is_valid);
assert!(cert.passes_tolerance(0.02));
assert!(!cert.passes_tolerance(0.005));
let quality = cert.quality_score();
assert!(quality > 0.0 && quality <= 1.0);
}
#[test]
fn test_condition_number() {
let well_conditioned = DMatrix::identity(3, 3);
let cond = condition_number_estimate(&well_conditioned);
assert!(cond < 2.0); // Should be close to 1.0
let ill_conditioned = DMatrix::from_row_slice(2, 2, &[1.0, 1.0, 1.0, 1.0001]);
let cond_ill = condition_number_estimate(&ill_conditioned);
assert!(cond_ill > 1000.0);
}
#[test]
fn test_jacobian_computation() {
// Test with simple linear function f(x) = Ax
let a = DMatrix::from_row_slice(2, 2, &[2.0, 1.0, 0.5, 3.0]);
let f = |x: &DVector<f64>| &a * x;
let x = DVector::from_vec(vec![1.0, 2.0]);
let jac = compute_jacobian(f, &x, 1e-6);
// Jacobian should be approximately equal to A
assert!((jac[(0, 0)] - 2.0).abs() < 1e-4);
assert!((jac[(0, 1)] - 1.0).abs() < 1e-4);
assert!((jac[(1, 0)] - 0.5).abs() < 1e-4);
assert!((jac[(1, 1)] - 3.0).abs() < 1e-4);
}
}
@@ -0,0 +1,645 @@
//! PageRank-based active sample selection for training
//!
//! This module implements the PageRank-based active learning strategy
//! that selects the most valuable training samples for the neural network.
use crate::{
config::ActiveSelectionConfig,
error::{Result, TemporalNeuralError},
solvers::InferenceReadyTrait,
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
/// PageRank-based active sample selector
///
/// Uses k-NN graphs and PageRank scoring to identify the most valuable
/// training samples, focusing on regions where the model is uncertain
/// or making large errors.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageRankSelector {
/// Configuration
config: ActiveSelectionConfig,
/// k-NN graph adjacency matrix
graph: Option<DMatrix<f64>>,
/// Sample embeddings (features from last layer)
embeddings: Vec<DVector<f64>>,
/// Sample errors for scoring
sample_errors: Vec<f64>,
/// Sample importance scores
importance_scores: Vec<f64>,
/// Selected sample indices
selected_indices: HashSet<usize>,
/// PageRank scores
pagerank_scores: Vec<f64>,
/// Statistics
stats: SelectorStatistics,
/// Ready for inference flag
inference_ready: bool,
}
/// Statistics tracked by the selector
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectorStatistics {
/// Total number of samples processed
pub total_samples: usize,
/// Number of selections made
pub selections_made: usize,
/// Average error of selected samples
pub avg_selected_error: f64,
/// Average error of non-selected samples
pub avg_nonselected_error: f64,
/// Graph construction time in milliseconds
pub graph_construction_time_ms: f64,
/// PageRank computation time in milliseconds
pub pagerank_computation_time_ms: f64,
/// Selection time in milliseconds
pub selection_time_ms: f64,
}
impl PageRankSelector {
/// Create a new PageRank selector
pub fn new(config: &ActiveSelectionConfig) -> Result<Self> {
Ok(Self {
config: config.clone(),
graph: None,
embeddings: Vec::new(),
sample_errors: Vec::new(),
importance_scores: Vec::new(),
selected_indices: HashSet::new(),
pagerank_scores: Vec::new(),
stats: SelectorStatistics {
total_samples: 0,
selections_made: 0,
avg_selected_error: 0.0,
avg_nonselected_error: 0.0,
graph_construction_time_ms: 0.0,
pagerank_computation_time_ms: 0.0,
selection_time_ms: 0.0,
},
inference_ready: false,
})
}
/// Add samples with their embeddings and errors
pub fn add_samples(
&mut self,
embeddings: &[DVector<f64>],
errors: &[f64],
) -> Result<()> {
if embeddings.len() != errors.len() {
return Err(TemporalNeuralError::DataError {
message: "Embeddings and errors length mismatch".to_string(),
context: Some(format!("embeddings: {}, errors: {}", embeddings.len(), errors.len())),
});
}
// Add to internal storage
self.embeddings.extend_from_slice(embeddings);
self.sample_errors.extend_from_slice(errors);
self.stats.total_samples = self.embeddings.len();
// Invalidate graph since we have new samples
self.graph = None;
self.pagerank_scores.clear();
self.importance_scores.clear();
Ok(())
}
/// Build k-NN graph from current embeddings
pub fn build_graph(&mut self) -> Result<()> {
if self.embeddings.is_empty() {
return Err(TemporalNeuralError::DataError {
message: "No embeddings available for graph construction".to_string(),
context: None,
});
}
let start_time = std::time::Instant::now();
let n = self.embeddings.len();
let k = self.config.k as usize;
// Initialize adjacency matrix
let mut adjacency = DMatrix::zeros(n, n);
// Build k-NN graph
for i in 0..n {
// Compute distances to all other samples
let mut distances: Vec<(usize, f64)> = (0..n)
.filter(|&j| j != i)
.map(|j| {
let dist = self.compute_distance(&self.embeddings[i], &self.embeddings[j]);
(j, dist)
})
.collect();
// Sort by distance and take k nearest neighbors
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let neighbors: Vec<usize> = distances
.into_iter()
.take(k)
.map(|(idx, _)| idx)
.collect();
// Add edges to adjacency matrix
for &neighbor in &neighbors {
// Use Gaussian similarity as edge weight
let dist = self.compute_distance(&self.embeddings[i], &self.embeddings[neighbor]);
let weight = (-dist * dist / (2.0 * 0.1)).exp(); // σ = 0.1
adjacency[(i, neighbor)] = weight;
}
}
// Make graph symmetric
for i in 0..n {
for j in 0..n {
let avg_weight = (adjacency[(i, j)] + adjacency[(j, i)]) / 2.0;
adjacency[(i, j)] = avg_weight;
adjacency[(j, i)] = avg_weight;
}
}
self.graph = Some(adjacency);
self.stats.graph_construction_time_ms = start_time.elapsed().as_millis() as f64;
Ok(())
}
/// Compute PageRank scores with error-based personalization
pub fn compute_pagerank(&mut self) -> Result<()> {
if self.graph.is_none() {
self.build_graph()?;
}
let graph = self.graph.as_ref().unwrap();
let n = graph.nrows();
if n == 0 {
return Ok(());
}
let start_time = std::time::Instant::now();
// Create personalization vector based on recent errors
let personalization = self.create_error_personalization_vector()?;
// Compute PageRank using power iteration
let pagerank_scores = self.power_iteration_pagerank(graph, &personalization)?;
self.pagerank_scores = pagerank_scores;
self.stats.pagerank_computation_time_ms = start_time.elapsed().as_millis() as f64;
Ok(())
}
/// Select active samples based on PageRank scores
pub fn select_samples(&mut self) -> Result<Vec<usize>> {
if self.pagerank_scores.is_empty() {
self.compute_pagerank()?;
}
let start_time = std::time::Instant::now();
let n_samples = self.config.samples_per_epoch as usize;
let total_samples = self.embeddings.len();
if n_samples >= total_samples {
// Select all samples if we need more than available
let selected: Vec<usize> = (0..total_samples).collect();
self.selected_indices = selected.iter().cloned().collect();
return Ok(selected);
}
// Combine PageRank scores with diversity to avoid clustering
let mut combined_scores = Vec::new();
for i in 0..total_samples {
let pagerank_score = self.pagerank_scores.get(i).copied().unwrap_or(0.0);
let error_score = self.sample_errors.get(i).copied().unwrap_or(0.0);
let diversity_score = self.compute_diversity_score(i)?;
let combined_score =
self.config.error_weight * error_score +
(1.0 - self.config.error_weight) * pagerank_score +
self.config.diversity_weight * diversity_score;
combined_scores.push((i, combined_score));
}
// Sort by combined score (descending)
combined_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
// Select top samples with diversity constraints
let selected = self.select_with_diversity_constraint(&combined_scores, n_samples)?;
self.selected_indices = selected.iter().cloned().collect();
self.stats.selections_made += 1;
self.stats.selection_time_ms = start_time.elapsed().as_millis() as f64;
// Update statistics
self.update_selection_statistics(&selected)?;
Ok(selected)
}
/// Create error-based personalization vector for PageRank
fn create_error_personalization_vector(&self) -> Result<DVector<f64>> {
let n = self.sample_errors.len();
if n == 0 {
return Err(TemporalNeuralError::DataError {
message: "No sample errors available".to_string(),
context: None,
});
}
// Create personalization vector: higher error = higher probability
let mut personalization = DVector::zeros(n);
let max_error = self.sample_errors.iter().fold(0.0f64, |a, &b| a.max(b));
if max_error > 0.0 {
for (i, &error) in self.sample_errors.iter().enumerate() {
personalization[i] = error / max_error;
}
} else {
// Uniform if no errors
personalization.fill(1.0 / n as f64);
}
// Normalize
let sum = personalization.sum();
if sum > 0.0 {
personalization /= sum;
} else {
personalization.fill(1.0 / n as f64);
}
Ok(personalization)
}
/// Compute PageRank using power iteration
fn power_iteration_pagerank(
&self,
graph: &DMatrix<f64>,
personalization: &DVector<f64>,
) -> Result<Vec<f64>> {
let n = graph.nrows();
let damping = 0.85;
let tolerance = self.config.pagerank_eps;
let max_iterations = 100;
// Normalize graph to transition matrix
let mut transition = graph.clone();
for i in 0..n {
let row_sum: f64 = transition.row(i).sum();
if row_sum > 1e-12 {
for j in 0..n {
transition[(i, j)] /= row_sum;
}
} else {
// Uniform transition for isolated nodes
for j in 0..n {
transition[(i, j)] = 1.0 / n as f64;
}
}
}
// Initialize PageRank vector
let mut pagerank = DVector::from_element(n, 1.0 / n as f64);
// Power iteration
for _ in 0..max_iterations {
let old_pagerank = pagerank.clone();
// PageRank update: PR = (1-d)/N + d * T^T * PR + (1-d) * personalization
pagerank = &transition.transpose() * &old_pagerank * damping +
personalization * (1.0 - damping);
// Check convergence
let diff = (&pagerank - &old_pagerank).norm();
if diff < tolerance {
break;
}
}
Ok(pagerank.data.as_vec().clone())
}
/// Compute diversity score for a sample
fn compute_diversity_score(&self, sample_idx: usize) -> Result<f64> {
if self.selected_indices.is_empty() {
return Ok(1.0); // Maximum diversity if no samples selected yet
}
let sample_embedding = &self.embeddings[sample_idx];
let mut min_distance = f64::INFINITY;
// Find minimum distance to already selected samples
for &selected_idx in &self.selected_indices {
let distance = self.compute_distance(sample_embedding, &self.embeddings[selected_idx]);
min_distance = min_distance.min(distance);
}
// Diversity score is minimum distance (higher = more diverse)
Ok(min_distance)
}
/// Select samples with diversity constraint
fn select_with_diversity_constraint(
&self,
scored_samples: &[(usize, f64)],
n_samples: usize,
) -> Result<Vec<usize>> {
let mut selected = Vec::new();
let mut selected_set = HashSet::new();
for &(idx, _score) in scored_samples {
if selected.len() >= n_samples {
break;
}
// Check diversity constraint
if self.meets_diversity_constraint(idx, &selected_set)? {
selected.push(idx);
selected_set.insert(idx);
}
}
// Fill remaining slots if we don't have enough diverse samples
for &(idx, _score) in scored_samples {
if selected.len() >= n_samples {
break;
}
if !selected_set.contains(&idx) {
selected.push(idx);
selected_set.insert(idx);
}
}
Ok(selected)
}
/// Check if sample meets diversity constraint
fn meets_diversity_constraint(
&self,
sample_idx: usize,
selected_indices: &HashSet<usize>,
) -> Result<bool> {
if selected_indices.is_empty() {
return Ok(true);
}
let min_diversity_distance = 0.1; // Minimum distance threshold
let sample_embedding = &self.embeddings[sample_idx];
for &selected_idx in selected_indices {
let distance = self.compute_distance(sample_embedding, &self.embeddings[selected_idx]);
if distance < min_diversity_distance {
return Ok(false);
}
}
Ok(true)
}
/// Compute distance between two embeddings
fn compute_distance(&self, a: &DVector<f64>, b: &DVector<f64>) -> f64 {
(a - b).norm()
}
/// Update selection statistics
fn update_selection_statistics(&mut self, selected: &[usize]) -> Result<()> {
if selected.is_empty() {
return Ok(());
}
// Compute average error of selected samples
let selected_errors: Vec<f64> = selected
.iter()
.map(|&idx| self.sample_errors.get(idx).copied().unwrap_or(0.0))
.collect();
self.stats.avg_selected_error = selected_errors.iter().sum::<f64>() / selected_errors.len() as f64;
// Compute average error of non-selected samples
let non_selected_errors: Vec<f64> = (0..self.sample_errors.len())
.filter(|idx| !selected.contains(idx))
.map(|idx| self.sample_errors[idx])
.collect();
if !non_selected_errors.is_empty() {
self.stats.avg_nonselected_error = non_selected_errors.iter().sum::<f64>() / non_selected_errors.len() as f64;
}
Ok(())
}
/// Get selection statistics
pub fn get_statistics(&self) -> &SelectorStatistics {
&self.stats
}
/// Clear all stored data
pub fn clear_data(&mut self) {
self.embeddings.clear();
self.sample_errors.clear();
self.importance_scores.clear();
self.selected_indices.clear();
self.pagerank_scores.clear();
self.graph = None;
self.stats.total_samples = 0;
}
/// Get memory usage estimate
pub fn estimate_memory_usage(&self) -> usize {
let embeddings_size = self.embeddings.len() *
self.embeddings.get(0).map_or(0, |e| e.len()) *
std::mem::size_of::<f64>();
let graph_size = self.graph.as_ref().map_or(0, |g| g.len() * std::mem::size_of::<f64>());
let other_vecs_size = (self.sample_errors.len() +
self.importance_scores.len() +
self.pagerank_scores.len()) * std::mem::size_of::<f64>();
std::mem::size_of::<Self>() + embeddings_size + graph_size + other_vecs_size
}
}
impl InferenceReadyTrait for PageRankSelector {
fn prepare_for_inference(&mut self) -> Result<()> {
// Clear training-specific data to save memory
self.clear_data();
self.inference_ready = true;
Ok(())
}
fn is_inference_ready(&self) -> bool {
self.inference_ready
}
fn memory_usage(&self) -> usize {
self.estimate_memory_usage()
}
fn reset(&mut self) -> Result<()> {
self.clear_data();
self.stats = SelectorStatistics {
total_samples: 0,
selections_made: 0,
avg_selected_error: 0.0,
avg_nonselected_error: 0.0,
graph_construction_time_ms: 0.0,
pagerank_computation_time_ms: 0.0,
selection_time_ms: 0.0,
};
self.inference_ready = false;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> ActiveSelectionConfig {
ActiveSelectionConfig {
k: 5,
pagerank_eps: 0.01,
samples_per_epoch: 10,
error_weight: 0.7,
diversity_weight: 0.3,
}
}
fn create_test_embeddings() -> Vec<DVector<f64>> {
(0..20)
.map(|i| {
DVector::from_vec(vec![
i as f64 / 10.0,
(i as f64 / 10.0).sin(),
(i as f64 / 10.0).cos(),
])
})
.collect()
}
fn create_test_errors() -> Vec<f64> {
(0..20).map(|i| (i as f64 / 20.0) + 0.1).collect()
}
#[test]
fn test_selector_creation() {
let config = create_test_config();
let selector = PageRankSelector::new(&config).unwrap();
assert_eq!(selector.config.k, 5);
assert_eq!(selector.config.samples_per_epoch, 10);
assert!(selector.embeddings.is_empty());
}
#[test]
fn test_add_samples() {
let config = create_test_config();
let mut selector = PageRankSelector::new(&config).unwrap();
let embeddings = create_test_embeddings();
let errors = create_test_errors();
selector.add_samples(&embeddings, &errors).unwrap();
assert_eq!(selector.stats.total_samples, 20);
assert_eq!(selector.embeddings.len(), 20);
assert_eq!(selector.sample_errors.len(), 20);
}
#[test]
fn test_graph_construction() {
let config = create_test_config();
let mut selector = PageRankSelector::new(&config).unwrap();
let embeddings = create_test_embeddings();
let errors = create_test_errors();
selector.add_samples(&embeddings, &errors).unwrap();
selector.build_graph().unwrap();
assert!(selector.graph.is_some());
let graph = selector.graph.as_ref().unwrap();
assert_eq!(graph.shape(), (20, 20));
assert!(selector.stats.graph_construction_time_ms > 0.0);
}
#[test]
fn test_pagerank_computation() {
let config = create_test_config();
let mut selector = PageRankSelector::new(&config).unwrap();
let embeddings = create_test_embeddings();
let errors = create_test_errors();
selector.add_samples(&embeddings, &errors).unwrap();
selector.compute_pagerank().unwrap();
assert_eq!(selector.pagerank_scores.len(), 20);
assert!(selector.stats.pagerank_computation_time_ms > 0.0);
// Check that scores sum approximately to 1
let sum: f64 = selector.pagerank_scores.iter().sum();
assert!((sum - 1.0).abs() < 0.1);
}
#[test]
fn test_sample_selection() {
let config = create_test_config();
let mut selector = PageRankSelector::new(&config).unwrap();
let embeddings = create_test_embeddings();
let errors = create_test_errors();
selector.add_samples(&embeddings, &errors).unwrap();
let selected = selector.select_samples().unwrap();
assert_eq!(selected.len(), 10); // Should select requested number
assert!(selector.stats.selection_time_ms > 0.0);
assert!(selector.stats.avg_selected_error >= 0.0);
}
#[test]
fn test_diversity_constraint() {
let config = create_test_config();
let mut selector = PageRankSelector::new(&config).unwrap();
// Create embeddings with some very similar ones
let mut embeddings = vec![DVector::from_vec(vec![0.0, 0.0, 0.0])];
embeddings.push(DVector::from_vec(vec![0.001, 0.001, 0.001])); // Very similar
embeddings.push(DVector::from_vec(vec![1.0, 1.0, 1.0])); // Different
let errors = vec![1.0, 1.0, 0.1]; // First two have high error
selector.add_samples(&embeddings, &errors).unwrap();
let selected = selector.select_samples().unwrap();
// Should not select both very similar samples
assert!(selected.len() <= 2);
}
#[test]
fn test_inference_preparation() {
let config = create_test_config();
let mut selector = PageRankSelector::new(&config).unwrap();
let embeddings = create_test_embeddings();
let errors = create_test_errors();
selector.add_samples(&embeddings, &errors).unwrap();
let memory_before = selector.memory_usage();
selector.prepare_for_inference().unwrap();
assert!(selector.is_inference_ready());
assert!(selector.embeddings.is_empty()); // Should clear training data
let memory_after = selector.memory_usage();
assert!(memory_after < memory_before); // Should use less memory
}
}
@@ -0,0 +1,650 @@
//! Sublinear solver gate for mathematical verification of predictions
//!
//! This module provides the core innovation: using sublinear-time mathematical
//! solvers to verify neural network predictions with mathematical certificates.
use crate::{
config::SolverGateConfig,
error::{Result, TemporalNeuralError},
solvers::{InferenceReadyTrait, Certificate, CertificateMetadata, math_utils},
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
// Temporarily commented out until sublinear integration is fixed
// use ::sublinear::{SolverAlgorithm, SolverOptions, NeumannSolver, Precision};
// Temporary type aliases for compilation
type SolverAlgorithm = ();
type SolverOptions = ();
type NeumannSolver = ();
type Precision = f64;
/// Gate result from solver verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateResult {
/// Whether the prediction passed verification
pub passed: bool,
/// Certificate error bound
pub certificate_error: f64,
/// Computational work performed
pub work_performed: u64,
/// Fallback strategy used (if any)
pub fallback_used: Option<String>,
/// Full certificate details
pub certificate: Certificate,
/// Computation time in microseconds
pub computation_time_us: f64,
}
/// Sublinear solver gate for prediction verification
///
/// The gate works by formulating the prediction problem as a linear system
/// and using sublinear solvers to verify the mathematical consistency.
#[derive(Debug)]
pub struct SolverGate {
/// Configuration
config: SolverGateConfig,
/// Sublinear solver instance
// Temporarily disabled solver: Box<dyn SolverAlgorithm<State = Box<dyn sublinear::solver::SolverState>>>,
solver_placeholder: bool,
/// Solver options
solver_options: SolverOptions,
/// Gate statistics
stats: GateStatistics,
/// Ready for inference flag
inference_ready: bool,
/// Recent verification times for latency tracking
recent_times: Vec<f64>,
/// Maximum number of recent times to keep
max_recent_times: usize,
}
/// Statistics tracked by the solver gate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateStatistics {
/// Total number of verifications performed
pub total_verifications: u64,
/// Number of verifications that passed
pub passed_verifications: u64,
/// Sum of all certificate errors
pub total_certificate_error: f64,
/// Sum of all computational work
pub total_work: u64,
/// Average verification time in microseconds
pub avg_verification_time_us: f64,
/// P99.9 verification time in microseconds
pub p99_9_verification_time_us: f64,
}
impl SolverGate {
/// Create a new solver gate
pub fn new(config: &SolverGateConfig) -> Result<Self> {
// Temporarily disabled solver integration for compilation
// TODO: Re-enable once sublinear crate integration is fixed
Ok(Self {
solver_placeholder: true,
config: config.clone(),
verification_history: Vec::new(),
certificate_cache: std::collections::HashMap::new(),
})
}
/// Temporarily simplified verify method
pub fn verify_placeholder(
&mut self,
_prior: &DMatrix<f64>,
_residual: &DMatrix<f64>,
_prediction: &DMatrix<f64>,
) -> Result<GateResult> {
// Placeholder implementation - always passes for now
Ok(GateResult {
passed: true,
confidence: 0.95,
certificate_error: 0.001,
verification_time_us: 10.0,
work_performed: 100,
certificate: Some(Certificate {
error_bound: 0.001,
algorithm: "placeholder".to_string(),
verification_id: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now(),
computational_work: 100,
confidence_level: 0.95,
}),
})
}
// Temporary placeholder for the rest of the implementation
fn _disabled_new(config: &SolverGateConfig) -> Result<Self> {
return Err(TemporalNeuralError::ConfigurationError(
message: "Random walk solver not yet implemented".to_string(),
field: Some("algorithm".to_string()),
});
}
"forward_push" => {
return Err(TemporalNeuralError::ConfigurationError {
message: "Forward push solver not yet implemented".to_string(),
field: Some("algorithm".to_string()),
});
}
_ => {
return Err(TemporalNeuralError::ConfigurationError {
message: format!("Unknown solver algorithm: {}", config.algorithm),
field: Some("algorithm".to_string()),
});
}
};
let solver_options = SolverOptions {
tolerance: config.epsilon,
max_iterations: (config.budget / 1000).min(10000) as usize, // Convert budget to iterations
..SolverOptions::default()
};
let stats = GateStatistics {
total_verifications: 0,
passed_verifications: 0,
total_certificate_error: 0.0,
total_work: 0,
avg_verification_time_us: 0.0,
p99_9_verification_time_us: 0.0,
};
Ok(Self {
config: config.clone(),
solver,
solver_options,
stats,
inference_ready: false,
recent_times: Vec::new(),
max_recent_times: 1000,
})
}
/// Verify a prediction using the sublinear solver
pub fn verify(
&mut self,
prior: &DVector<f64>,
residual: &DVector<f64>,
prediction: &DVector<f64>,
) -> Result<GateResult> {
let start_time = std::time::Instant::now();
// Formulate the verification problem as a linear system
let (matrix, rhs) = self.formulate_verification_problem(prior, residual, prediction)?;
// Solve using sublinear solver
let solver_result = self.solver.solve(&matrix, &rhs, &self.solver_options)
.map_err(|e| TemporalNeuralError::SolverError {
message: format!("Solver verification failed: {}", e),
algorithm: Some(self.config.algorithm.clone()),
certificate_error: None,
})?;
let computation_time = start_time.elapsed().as_micros() as f64;
// Create certificate from solver result
let certificate = self.create_certificate(&solver_result, &matrix, computation_time)?;
// Determine if gate passes
let passed = certificate.passes_tolerance(self.config.max_cert_error);
// Determine fallback strategy if failed
let fallback_used = if !passed {
Some(self.config.fallback_strategy.clone())
} else {
None
};
// Update statistics
self.update_statistics(passed, certificate.error_bound, solver_result.iterations as u64, computation_time);
Ok(GateResult {
passed,
certificate_error: certificate.error_bound,
work_performed: solver_result.iterations as u64,
fallback_used,
certificate,
computation_time_us: computation_time,
})
}
/// Formulate the verification problem as a linear system
///
/// The key insight is to verify that the prediction is mathematically
/// consistent with the dynamics model implied by the Kalman filter.
fn formulate_verification_problem(
&self,
prior: &DVector<f64>,
residual: &DVector<f64>,
prediction: &DVector<f64>,
) -> Result<(Box<dyn sublinear::Matrix>, Vec<Precision>)> {
let dim = prior.len();
// Create a verification matrix that encodes the consistency constraint:
// prediction = prior + residual
// We formulate this as: [I -I] * [prediction; residual] = prior
// For a 2D problem, create a 2x4 system
let matrix_data = vec![
vec![1.0, 0.0, -1.0, 0.0], // prediction_x - residual_x = prior_x
vec![0.0, 1.0, 0.0, -1.0], // prediction_y - residual_y = prior_y
];
// Convert to sparse matrix format expected by solver
let mut triplets = Vec::new();
for (i, row) in matrix_data.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
if val.abs() > 1e-12 {
triplets.push((i, j, val));
}
}
}
let sparse_matrix = sublinear::SparseMatrix::from_triplets(
triplets,
dim,
dim * 2, // [prediction; residual]
);
// Make the matrix diagonally dominant for solver compatibility
let dd_matrix = self.make_diagonally_dominant(sparse_matrix)?;
// Right-hand side is the prior
let rhs: Vec<Precision> = prior.iter().cloned().collect();
Ok((Box::new(dd_matrix), rhs))
}
/// Make matrix diagonally dominant for sublinear solver compatibility
fn make_diagonally_dominant(
&self,
mut matrix: sublinear::SparseMatrix,
) -> Result<sublinear::SparseMatrix> {
// For the solver to work, we need diagonal dominance
// Add regularization to diagonal elements
let regularization = 1.1; // Ensure diagonal dominance
// This is a simplified approach - in practice, we'd need to modify
// the underlying sparse matrix structure
// For now, create a simple diagonally dominant test matrix
let size = matrix.rows().min(matrix.cols());
let test_matrix = math_utils::create_test_dd_matrix(size);
// Convert back to sparse format
let mut triplets = Vec::new();
for i in 0..size {
for j in 0..size {
let val = test_matrix[(i, j)];
if val.abs() > 1e-12 {
triplets.push((i, j, val));
}
}
}
Ok(sublinear::SparseMatrix::from_triplets(
triplets,
size,
size,
))
}
/// Create certificate from solver result
fn create_certificate(
&self,
solver_result: &sublinear::SolverResult,
matrix: &dyn sublinear::Matrix,
computation_time_us: f64,
) -> Result<Certificate> {
let error_bound = solver_result.residual_norm;
let confidence = if solver_result.converged { 0.95 } else { 0.5 };
let work_performed = solver_result.iterations as u64;
let mut certificate = Certificate::new(
error_bound,
confidence,
work_performed,
self.config.algorithm.clone(),
);
// Add metadata
certificate.metadata = CertificateMetadata {
condition_number: Some(math_utils::condition_number_estimate(
&nalgebra::DMatrix::identity(2, 2) // Placeholder
)),
diagonally_dominant: true, // We ensure this in formulation
iterations: solver_result.iterations as u32,
residual_norm: solver_result.residual_norm,
computation_time_us,
};
Ok(certificate)
}
/// Update internal statistics
fn update_statistics(
&mut self,
passed: bool,
certificate_error: f64,
work: u64,
time_us: f64,
) {
self.stats.total_verifications += 1;
if passed {
self.stats.passed_verifications += 1;
}
self.stats.total_certificate_error += certificate_error;
self.stats.total_work += work;
// Update timing statistics
self.recent_times.push(time_us);
if self.recent_times.len() > self.max_recent_times {
self.recent_times.remove(0);
}
// Recompute average
self.stats.avg_verification_time_us =
self.recent_times.iter().sum::<f64>() / self.recent_times.len() as f64;
// Compute P99.9
if self.recent_times.len() > 10 {
let mut sorted_times = self.recent_times.clone();
sorted_times.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p99_9_index = ((sorted_times.len() as f64) * 0.999) as usize;
self.stats.p99_9_verification_time_us = sorted_times.get(p99_9_index)
.copied()
.unwrap_or(time_us);
}
}
/// Get gate pass rate
pub fn get_pass_rate(&self) -> f64 {
if self.stats.total_verifications == 0 {
1.0
} else {
self.stats.passed_verifications as f64 / self.stats.total_verifications as f64
}
}
/// Get average certificate error
pub fn get_avg_certificate_error(&self) -> f64 {
if self.stats.total_verifications == 0 {
0.0
} else {
self.stats.total_certificate_error / self.stats.total_verifications as f64
}
}
/// Get average computational work
pub fn get_avg_work(&self) -> f64 {
if self.stats.total_verifications == 0 {
0.0
} else {
self.stats.total_work as f64 / self.stats.total_verifications as f64
}
}
/// Get total prediction count
pub fn get_prediction_count(&self) -> u64 {
self.stats.total_verifications
}
/// Set epsilon tolerance dynamically
pub fn set_epsilon(&mut self, epsilon: f64) -> Result<()> {
if epsilon <= 0.0 {
return Err(TemporalNeuralError::ConfigurationError {
message: "Epsilon must be positive".to_string(),
field: Some("epsilon".to_string()),
});
}
self.config.epsilon = epsilon;
self.solver_options.tolerance = epsilon;
Ok(())
}
/// Set computational budget dynamically
pub fn set_budget(&mut self, budget: u64) -> Result<()> {
if budget == 0 {
return Err(TemporalNeuralError::ConfigurationError {
message: "Budget must be positive".to_string(),
field: Some("budget".to_string()),
});
}
self.config.budget = budget;
self.solver_options.max_iterations = (budget / 1000).min(10000) as usize;
Ok(())
}
/// Get current performance metrics
pub fn get_performance_metrics(&self) -> SolverGateMetrics {
SolverGateMetrics {
pass_rate: self.get_pass_rate(),
avg_certificate_error: self.get_avg_certificate_error(),
avg_verification_time_us: self.stats.avg_verification_time_us,
p99_9_verification_time_us: self.stats.p99_9_verification_time_us,
total_verifications: self.stats.total_verifications,
memory_usage_bytes: self.memory_usage(),
}
}
/// Check if gate is meeting performance targets
pub fn meets_performance_targets(&self, target_latency_us: f64, target_pass_rate: f64) -> bool {
self.stats.p99_9_verification_time_us <= target_latency_us &&
self.get_pass_rate() >= target_pass_rate
}
}
/// Performance metrics for the solver gate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverGateMetrics {
/// Gate pass rate (0.0 to 1.0)
pub pass_rate: f64,
/// Average certificate error
pub avg_certificate_error: f64,
/// Average verification time in microseconds
pub avg_verification_time_us: f64,
/// P99.9 verification time in microseconds
pub p99_9_verification_time_us: f64,
/// Total verifications performed
pub total_verifications: u64,
/// Memory usage in bytes
pub memory_usage_bytes: usize,
}
impl InferenceReadyTrait for SolverGate {
fn prepare_for_inference(&mut self) -> Result<()> {
// Validate configuration
if self.config.epsilon <= 0.0 {
return Err(TemporalNeuralError::ConfigurationError {
message: "Invalid epsilon for inference".to_string(),
field: Some("epsilon".to_string()),
});
}
if self.config.budget == 0 {
return Err(TemporalNeuralError::ConfigurationError {
message: "Invalid budget for inference".to_string(),
field: Some("budget".to_string()),
});
}
// Clear statistics to save memory
self.recent_times.clear();
self.recent_times.reserve(100); // Keep small buffer for recent metrics
self.inference_ready = true;
Ok(())
}
fn is_inference_ready(&self) -> bool {
self.inference_ready &&
self.config.epsilon > 0.0 &&
self.config.budget > 0
}
fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>() +
self.recent_times.len() * std::mem::size_of::<f64>() +
1024 // Estimated solver overhead
}
fn reset(&mut self) -> Result<()> {
self.stats = GateStatistics {
total_verifications: 0,
passed_verifications: 0,
total_certificate_error: 0.0,
total_work: 0,
avg_verification_time_us: 0.0,
p99_9_verification_time_us: 0.0,
};
self.recent_times.clear();
self.inference_ready = false;
Ok(())
}
}
impl Clone for SolverGate {
fn clone(&self) -> Self {
// Create a new solver instance for the clone
Self::new(&self.config).unwrap()
}
}
impl Serialize for SolverGate {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Serialize only the essential data
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("SolverGate", 3)?;
state.serialize_field("config", &self.config)?;
state.serialize_field("stats", &self.stats)?;
state.serialize_field("inference_ready", &self.inference_ready)?;
state.end()
}
}
impl<'de> Deserialize<'de> for SolverGate {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct SolverGateData {
config: SolverGateConfig,
stats: GateStatistics,
inference_ready: bool,
}
let data = SolverGateData::deserialize(deserializer)?;
let mut gate = Self::new(&data.config).map_err(serde::de::Error::custom)?;
gate.stats = data.stats;
gate.inference_ready = data.inference_ready;
Ok(gate)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> SolverGateConfig {
SolverGateConfig {
algorithm: "neumann".to_string(),
epsilon: 0.02,
budget: 10000,
max_cert_error: 0.05,
fallback_strategy: "kalman_only".to_string(),
}
}
#[test]
fn test_solver_gate_creation() {
let config = create_test_config();
let gate = SolverGate::new(&config).unwrap();
assert_eq!(gate.config.algorithm, "neumann");
assert_eq!(gate.config.epsilon, 0.02);
assert!(!gate.inference_ready);
}
#[test]
fn test_verification_process() {
let config = create_test_config();
let mut gate = SolverGate::new(&config).unwrap();
let prior = DVector::from_vec(vec![1.0, 2.0]);
let residual = DVector::from_vec(vec![0.1, -0.1]);
let prediction = DVector::from_vec(vec![1.1, 1.9]);
let result = gate.verify(&prior, &residual, &prediction).unwrap();
assert!(result.certificate_error >= 0.0);
assert!(result.work_performed > 0);
assert!(result.computation_time_us > 0.0);
}
#[test]
fn test_statistics_tracking() {
let config = create_test_config();
let mut gate = SolverGate::new(&config).unwrap();
// Perform several verifications
for i in 0..5 {
let prior = DVector::from_vec(vec![i as f64, i as f64]);
let residual = DVector::from_vec(vec![0.1, 0.1]);
let prediction = &prior + &residual;
let _ = gate.verify(&prior, &residual, &prediction);
}
assert_eq!(gate.stats.total_verifications, 5);
assert!(gate.get_pass_rate() >= 0.0 && gate.get_pass_rate() <= 1.0);
assert!(gate.get_avg_certificate_error() >= 0.0);
}
#[test]
fn test_dynamic_configuration() {
let config = create_test_config();
let mut gate = SolverGate::new(&config).unwrap();
// Test epsilon update
gate.set_epsilon(0.01).unwrap();
assert_eq!(gate.config.epsilon, 0.01);
// Test budget update
gate.set_budget(50000).unwrap();
assert_eq!(gate.config.budget, 50000);
// Test invalid values
assert!(gate.set_epsilon(-1.0).is_err());
assert!(gate.set_budget(0).is_err());
}
#[test]
fn test_inference_preparation() {
let config = create_test_config();
let mut gate = SolverGate::new(&config).unwrap();
assert!(gate.prepare_for_inference().is_ok());
assert!(gate.is_inference_ready());
let metrics = gate.get_performance_metrics();
assert_eq!(metrics.total_verifications, 0);
}
#[test]
fn test_performance_targets() {
let config = create_test_config();
let gate = SolverGate::new(&config).unwrap();
// Should meet initial targets (no data yet)
assert!(gate.meets_performance_targets(1000.0, 0.9));
}
}
@@ -0,0 +1,206 @@
//! Simplified solver gate for compilation - will be replaced with full implementation
use crate::error::{Result, TemporalNeuralError};
use crate::solvers::InferenceReadyTrait;
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// Simplified solver gate configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverGateConfig {
pub algorithm: String,
pub epsilon: f64,
pub max_iterations: usize,
pub budget: u64,
pub max_cert_error: f64,
}
/// Gate result from solver verification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateResult {
pub passed: bool,
pub confidence: f64,
pub certificate_error: f64,
pub verification_time_us: f64,
pub work_performed: u64,
pub certificate: Option<Certificate>,
}
/// Mathematical certificate from solver
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Certificate {
pub error_bound: f64,
pub algorithm: String,
pub verification_id: String,
pub timestamp: DateTime<Utc>,
pub computational_work: u64,
pub confidence_level: f64,
}
/// Simplified solver gate
#[derive(Debug)]
pub struct SolverGate {
config: SolverGateConfig,
verification_history: Vec<GateResult>,
}
impl SolverGate {
/// Create a new solver gate
pub fn new(config: &SolverGateConfig) -> Result<Self> {
Ok(Self {
config: config.clone(),
verification_history: Vec::new(),
})
}
/// Get pass rate of verifications
pub fn get_pass_rate(&self) -> f64 {
if self.verification_history.is_empty() {
return 1.0;
}
let passed = self.verification_history.iter().filter(|r| r.passed).count();
passed as f64 / self.verification_history.len() as f64
}
/// Get average certificate error
pub fn get_avg_certificate_error(&self) -> f64 {
if self.verification_history.is_empty() {
return 0.0;
}
let total_error: f64 = self.verification_history.iter()
.map(|r| r.certificate_error)
.sum();
total_error / self.verification_history.len() as f64
}
/// Get average computational work
pub fn get_avg_work(&self) -> f64 {
if self.verification_history.is_empty() {
return 0.0;
}
let total_work: u64 = self.verification_history.iter()
.map(|r| r.work_performed)
.sum();
total_work as f64 / self.verification_history.len() as f64
}
/// Get total prediction count
pub fn get_prediction_count(&self) -> u64 {
self.verification_history.len() as u64
}
/// Set epsilon parameter
pub fn set_epsilon(&mut self, epsilon: f64) -> Result<()> {
if epsilon <= 0.0 {
return Err(TemporalNeuralError::ConfigurationError {
message: "Epsilon must be positive".to_string(),
field: Some("epsilon".to_string()),
});
}
self.config.epsilon = epsilon;
Ok(())
}
/// Set budget parameter
pub fn set_budget(&mut self, budget: u64) -> Result<()> {
if budget == 0 {
return Err(TemporalNeuralError::ConfigurationError {
message: "Budget must be positive".to_string(),
field: Some("budget".to_string()),
});
}
self.config.budget = budget;
Ok(())
}
/// Verify a prediction using simplified logic
pub fn verify(
&mut self,
_prior: &DMatrix<f64>,
_residual: &DMatrix<f64>,
_prediction: &DMatrix<f64>,
) -> Result<GateResult> {
// Simplified verification - always passes for now
// TODO: Implement actual solver-based verification
let result = GateResult {
passed: true,
confidence: 0.95,
certificate_error: 0.001,
verification_time_us: 10.0,
work_performed: 100,
certificate: Some(Certificate {
error_bound: 0.001,
algorithm: self.config.algorithm.clone(),
verification_id: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now(),
computational_work: 100,
confidence_level: 0.95,
}),
};
self.verification_history.push(result.clone());
Ok(result)
}
/// Get verification statistics
pub fn get_stats(&self) -> SolverGateStats {
let total_verifications = self.verification_history.len() as u64;
let passed_verifications = self.verification_history.iter()
.filter(|r| r.passed)
.count() as u64;
let avg_verification_time = if total_verifications > 0 {
self.verification_history.iter()
.map(|r| r.verification_time_us)
.sum::<f64>() / total_verifications as f64
} else {
0.0
};
SolverGateStats {
total_verifications,
passed_verifications,
average_confidence: 0.95,
total_certificate_error: 0.001,
total_work: total_verifications * 100,
avg_verification_time_us: avg_verification_time,
p99_9_verification_time_us: avg_verification_time * 1.1,
}
}
}
/// Statistics from solver gate operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverGateStats {
pub total_verifications: u64,
pub passed_verifications: u64,
pub average_confidence: f64,
pub total_certificate_error: f64,
pub total_work: u64,
pub avg_verification_time_us: f64,
pub p99_9_verification_time_us: f64,
}
impl InferenceReadyTrait for SolverGate {
fn prepare_for_inference(&mut self) -> Result<()> {
// Clear verification history to save memory
self.verification_history.clear();
Ok(())
}
fn is_inference_ready(&self) -> bool {
true // Simple implementation is always ready
}
fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>() +
self.verification_history.len() * std::mem::size_of::<GateResult>()
}
fn reset(&mut self) -> Result<()> {
self.verification_history.clear();
Ok(())
}
}
@@ -0,0 +1,99 @@
//! Training callbacks for monitoring and control
use crate::error::Result;
/// Trait for training callbacks
pub trait Callback: Send + Sync {
/// Called at the start of training
fn on_train_begin(&mut self) -> Result<()> { Ok(()) }
/// Called at the end of training
fn on_train_end(&mut self) -> Result<()> { Ok(()) }
/// Called at the start of each epoch
fn on_epoch_begin(&mut self, epoch: u32) -> Result<()> { let _ = epoch; Ok(()) }
/// Called at the end of each epoch
fn on_epoch_end(&mut self, epoch: u32, train_loss: f64, val_loss: f64) -> Result<bool> {
let _ = (epoch, train_loss, val_loss);
Ok(true) // Continue training
}
}
/// Early stopping callback
pub struct EarlyStoppingCallback {
patience: u32,
min_delta: f64,
best_loss: f64,
patience_counter: u32,
}
impl EarlyStoppingCallback {
pub fn new(patience: u32, min_delta: f64) -> Self {
Self {
patience,
min_delta,
best_loss: f64::INFINITY,
patience_counter: 0,
}
}
}
impl Callback for EarlyStoppingCallback {
fn on_epoch_end(&mut self, _epoch: u32, _train_loss: f64, val_loss: f64) -> Result<bool> {
if val_loss < self.best_loss - self.min_delta {
self.best_loss = val_loss;
self.patience_counter = 0;
} else {
self.patience_counter += 1;
}
Ok(self.patience_counter < self.patience)
}
}
/// Checkpoint saving callback
pub struct CheckpointCallback {
frequency: u32,
checkpoint_dir: String,
}
impl CheckpointCallback {
pub fn new(frequency: u32, checkpoint_dir: String) -> Self {
Self { frequency, checkpoint_dir }
}
}
impl Callback for CheckpointCallback {
fn on_epoch_end(&mut self, epoch: u32, _train_loss: f64, _val_loss: f64) -> Result<bool> {
if epoch % self.frequency == 0 {
// Would save checkpoint here
log::info!("Checkpoint saved at epoch {}", epoch);
}
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_early_stopping() {
let mut callback = EarlyStoppingCallback::new(3, 0.001);
// Should continue initially
assert!(callback.on_epoch_end(0, 1.0, 1.0).unwrap());
// Improvement should reset counter
assert!(callback.on_epoch_end(1, 0.8, 0.8).unwrap());
// No improvement should increment counter
assert!(callback.on_epoch_end(2, 0.9, 0.9).unwrap());
assert!(callback.on_epoch_end(3, 0.9, 0.9).unwrap());
assert!(callback.on_epoch_end(4, 0.9, 0.9).unwrap());
// Should stop after patience is exhausted
assert!(!callback.on_epoch_end(5, 0.9, 0.9).unwrap());
}
}
@@ -0,0 +1,119 @@
//! Loss functions for training temporal neural networks
use crate::error::{Result, TemporalNeuralError};
use nalgebra::DVector;
/// Trait for loss functions
pub trait LossFunction: Send + Sync {
/// Compute loss between prediction and target
fn compute_loss(&self, prediction: &DVector<f64>, target: &DVector<f64>) -> Result<f64>;
/// Compute gradient of loss with respect to prediction
fn compute_gradient(&self, prediction: &DVector<f64>, target: &DVector<f64>) -> Result<DVector<f64>>;
}
/// Mean Squared Error loss with optional smoothness penalty
pub struct MseLoss {
smoothness_weight: f64,
}
impl MseLoss {
pub fn new(smoothness_weight: f64) -> Self {
Self { smoothness_weight }
}
}
impl LossFunction for MseLoss {
fn compute_loss(&self, prediction: &DVector<f64>, target: &DVector<f64>) -> Result<f64> {
if prediction.len() != target.len() {
return Err(TemporalNeuralError::TrainingError {
epoch: 0,
message: "Prediction and target dimension mismatch".to_string(),
metrics: None,
});
}
let diff = prediction - target;
let mse = diff.norm_squared() / prediction.len() as f64;
// Add smoothness penalty if enabled
let smoothness_penalty = if self.smoothness_weight > 0.0 && prediction.len() >= 2 {
// Penalize large velocities (assuming prediction is [x, y])
let velocity_penalty = prediction[0].powi(2) + prediction[1].powi(2);
self.smoothness_weight * velocity_penalty
} else {
0.0
};
Ok(mse + smoothness_penalty)
}
fn compute_gradient(&self, prediction: &DVector<f64>, target: &DVector<f64>) -> Result<DVector<f64>> {
if prediction.len() != target.len() {
return Err(TemporalNeuralError::TrainingError {
epoch: 0,
message: "Prediction and target dimension mismatch".to_string(),
metrics: None,
});
}
let mut grad = 2.0 * (prediction - target) / prediction.len() as f64;
// Add smoothness gradient
if self.smoothness_weight > 0.0 && prediction.len() >= 2 {
grad[0] += 2.0 * self.smoothness_weight * prediction[0];
grad[1] += 2.0 * self.smoothness_weight * prediction[1];
}
Ok(grad)
}
}
/// Smoothness penalty for temporal predictions
pub struct SmoothnessPenalty {
weight: f64,
}
impl SmoothnessPenalty {
pub fn new(weight: f64) -> Self {
Self { weight }
}
pub fn compute_penalty(&self, prediction: &DVector<f64>) -> f64 {
if prediction.len() < 2 {
return 0.0;
}
// Penalize large magnitudes (velocity penalty)
self.weight * prediction.norm_squared()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mse_loss() {
let loss_fn = MseLoss::new(0.0);
let prediction = DVector::from_vec(vec![1.0, 2.0]);
let target = DVector::from_vec(vec![1.5, 1.5]);
let loss = loss_fn.compute_loss(&prediction, &target).unwrap();
assert!(loss > 0.0);
let grad = loss_fn.compute_gradient(&prediction, &target).unwrap();
assert_eq!(grad.len(), 2);
}
#[test]
fn test_mse_with_smoothness() {
let loss_fn = MseLoss::new(0.1);
let prediction = DVector::from_vec(vec![1.0, 2.0]);
let target = DVector::from_vec(vec![1.0, 2.0]);
// Even with perfect prediction, smoothness penalty should add to loss
let loss = loss_fn.compute_loss(&prediction, &target).unwrap();
assert!(loss > 0.0);
}
}
@@ -0,0 +1,662 @@
//! Training pipeline for temporal neural networks
//!
//! This module implements the training logic for both System A and System B,
//! including active sample selection, residual learning, and performance monitoring.
use crate::{
config::{Config, TrainingConfig},
data::{DataSplits, WindowedSample},
error::{Result, TemporalNeuralError, TrainingMetrics},
models::{ModelTrait, ModelParams, SystemA, SystemB},
solvers::PageRankSelector,
};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use std::time::Instant;
pub mod optimizer;
pub mod losses;
pub mod callbacks;
pub use optimizer::{Optimizer, AdamOptimizer, SgdOptimizer};
pub use losses::{LossFunction, MseLoss, SmoothnessPenalty};
pub use callbacks::{Callback, EarlyStoppingCallback, CheckpointCallback};
/// Training result containing model and metrics
#[derive(Debug, Clone)]
pub struct TrainingResult {
/// Training history
pub history: TrainingHistory,
/// Final model state
pub final_loss: f64,
/// Whether training converged
pub converged: bool,
/// Total training time in seconds
pub total_time_seconds: f64,
/// Best validation loss achieved
pub best_val_loss: f64,
/// Epoch at which best validation loss was achieved
pub best_epoch: u32,
}
/// Training history tracking
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingHistory {
/// Training loss per epoch
pub train_losses: Vec<f64>,
/// Validation loss per epoch
pub val_losses: Vec<f64>,
/// Learning rate per epoch
pub learning_rates: Vec<f64>,
/// Training time per epoch (seconds)
pub epoch_times: Vec<f64>,
/// Additional metrics per epoch
pub metrics: Vec<EpochMetrics>,
}
/// Metrics tracked per epoch
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpochMetrics {
/// Epoch number
pub epoch: u32,
/// Training samples processed
pub samples_processed: usize,
/// Average gradient norm
pub avg_gradient_norm: f64,
/// Parameter update magnitude
pub param_update_norm: f64,
/// Memory usage in bytes
pub memory_usage_bytes: usize,
/// System B specific metrics
pub system_b_metrics: Option<SystemBMetrics>,
}
/// System B specific training metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemBMetrics {
/// Gate pass rate during training
pub gate_pass_rate: f64,
/// Average certificate error
pub avg_certificate_error: f64,
/// Kalman filter prediction error
pub kalman_prediction_error: f64,
/// Active selection efficiency
pub active_selection_efficiency: f64,
/// Residual learning loss
pub residual_loss: f64,
}
/// Main trainer for temporal neural networks
pub struct Trainer {
/// Training configuration
config: TrainingConfig,
/// Optimizer
optimizer: Box<dyn Optimizer>,
/// Loss function
loss_fn: Box<dyn LossFunction>,
/// Callbacks
callbacks: Vec<Box<dyn Callback>>,
/// Training history
history: TrainingHistory,
/// Current epoch
current_epoch: u32,
/// Best validation loss
best_val_loss: f64,
/// Early stopping patience counter
patience_counter: u32,
}
impl Trainer {
/// Create a new trainer
pub fn new(config: TrainingConfig) -> Result<Self> {
// Create optimizer
let optimizer: Box<dyn Optimizer> = match config.optimizer.as_str() {
"adam" => Box::new(AdamOptimizer::new(config.learning_rate)),
"sgd" => Box::new(SgdOptimizer::new(config.learning_rate)),
"rmsprop" => {
return Err(TemporalNeuralError::ConfigurationError {
message: "RMSprop optimizer not yet implemented".to_string(),
field: Some("optimizer".to_string()),
});
}
_ => {
return Err(TemporalNeuralError::ConfigurationError {
message: format!("Unknown optimizer: {}", config.optimizer),
field: Some("optimizer".to_string()),
});
}
};
// Create loss function
let loss_fn: Box<dyn LossFunction> = Box::new(MseLoss::new(config.smoothness_weight));
// Create callbacks
let mut callbacks: Vec<Box<dyn Callback>> = Vec::new();
// Add early stopping
callbacks.push(Box::new(EarlyStoppingCallback::new(
config.patience,
1e-6, // min_delta
)));
// Add checkpointing
if config.checkpoint_frequency > 0 {
callbacks.push(Box::new(CheckpointCallback::new(
config.checkpoint_frequency,
"checkpoints".to_string(),
)));
}
Ok(Self {
config,
optimizer,
loss_fn,
callbacks,
history: TrainingHistory {
train_losses: Vec::new(),
val_losses: Vec::new(),
learning_rates: Vec::new(),
epoch_times: Vec::new(),
metrics: Vec::new(),
},
current_epoch: 0,
best_val_loss: f64::INFINITY,
patience_counter: 0,
})
}
/// Train System A (traditional approach)
pub fn train_system_a(
&mut self,
model: &mut SystemA,
data: &DataSplits,
) -> Result<TrainingResult> {
log::info!("Starting System A training");
let start_time = Instant::now();
data.validate()?;
for epoch in 0..self.config.epochs {
self.current_epoch = epoch;
let epoch_start = Instant::now();
// Training phase
let train_loss = self.train_epoch_system_a(model, &data.train)?;
// Validation phase
let val_loss = self.evaluate_system_a(model, &data.val)?;
// Update learning rate
let current_lr = self.optimizer.get_learning_rate();
// Create epoch metrics
let metrics = EpochMetrics {
epoch,
samples_processed: data.train.len(),
avg_gradient_norm: 0.0, // Would be computed during training
param_update_norm: 0.0, // Would be computed during optimization
memory_usage_bytes: model.memory_usage(),
system_b_metrics: None,
};
// Update history
self.history.train_losses.push(train_loss);
self.history.val_losses.push(val_loss);
self.history.learning_rates.push(current_lr);
self.history.epoch_times.push(epoch_start.elapsed().as_secs_f64());
self.history.metrics.push(metrics);
// Check for improvement
if val_loss < self.best_val_loss {
self.best_val_loss = val_loss;
self.patience_counter = 0;
} else {
self.patience_counter += 1;
}
// Early stopping check
if self.patience_counter >= self.config.patience {
log::info!("Early stopping triggered at epoch {}", epoch);
break;
}
// Progress logging
if epoch % self.config.val_frequency == 0 {
log::info!(
"Epoch {}: train_loss={:.6}, val_loss={:.6}, lr={:.6}",
epoch, train_loss, val_loss, current_lr
);
}
}
let total_time = start_time.elapsed().as_secs_f64();
let converged = self.patience_counter < self.config.patience;
Ok(TrainingResult {
history: self.history.clone(),
final_loss: self.history.train_losses.last().copied().unwrap_or(f64::INFINITY),
converged,
total_time_seconds: total_time,
best_val_loss: self.best_val_loss,
best_epoch: self.find_best_epoch(),
})
}
/// Train System B (temporal solver approach)
pub fn train_system_b(
&mut self,
model: &mut SystemB,
data: &DataSplits,
) -> Result<TrainingResult> {
log::info!("Starting System B training with temporal solver");
let start_time = Instant::now();
data.validate()?;
for epoch in 0..self.config.epochs {
self.current_epoch = epoch;
let epoch_start = Instant::now();
// Training phase with active selection
let (train_loss, system_b_metrics) = if epoch < 2 {
// First 2 epochs: use all data like System A
let loss = self.train_epoch_system_b_full(model, &data.train)?;
(loss, self.compute_system_b_metrics(model)?)
} else {
// From epoch 3: use active selection
let (loss, metrics) = self.train_epoch_system_b_active(model, &data.train)?;
(loss, metrics)
};
// Validation phase
let val_loss = self.evaluate_system_b(model, &data.val)?;
// Update learning rate
let current_lr = self.optimizer.get_learning_rate();
// Create epoch metrics
let metrics = EpochMetrics {
epoch,
samples_processed: data.train.len(),
avg_gradient_norm: 0.0, // Would be computed during training
param_update_norm: 0.0, // Would be computed during optimization
memory_usage_bytes: model.memory_usage(),
system_b_metrics: Some(system_b_metrics),
};
// Update history
self.history.train_losses.push(train_loss);
self.history.val_losses.push(val_loss);
self.history.learning_rates.push(current_lr);
self.history.epoch_times.push(epoch_start.elapsed().as_secs_f64());
self.history.metrics.push(metrics);
// Check for improvement
if val_loss < self.best_val_loss {
self.best_val_loss = val_loss;
self.patience_counter = 0;
} else {
self.patience_counter += 1;
}
// Early stopping check
if self.patience_counter >= self.config.patience {
log::info!("Early stopping triggered at epoch {}", epoch);
break;
}
// Progress logging
if epoch % self.config.val_frequency == 0 {
log::info!(
"Epoch {}: train_loss={:.6}, val_loss={:.6}, gate_pass_rate={:.3}, lr={:.6}",
epoch, train_loss, val_loss,
self.history.metrics.last().unwrap().system_b_metrics.as_ref()
.map_or(0.0, |m| m.gate_pass_rate),
current_lr
);
}
}
let total_time = start_time.elapsed().as_secs_f64();
let converged = self.patience_counter < self.config.patience;
Ok(TrainingResult {
history: self.history.clone(),
final_loss: self.history.train_losses.last().copied().unwrap_or(f64::INFINITY),
converged,
total_time_seconds: total_time,
best_val_loss: self.best_val_loss,
best_epoch: self.find_best_epoch(),
})
}
/// Train one epoch for System A
fn train_epoch_system_a(&mut self, model: &mut SystemA, samples: &[WindowedSample]) -> Result<f64> {
let mut total_loss = 0.0;
let mut sample_count = 0;
// Process samples in batches
for batch in samples.chunks(self.config.batch_size as usize) {
let batch_loss = self.process_batch_system_a(model, batch)?;
total_loss += batch_loss;
sample_count += batch.len();
}
Ok(total_loss / sample_count as f64)
}
/// Train one epoch for System B (full data)
fn train_epoch_system_b_full(&mut self, model: &mut SystemB, samples: &[WindowedSample]) -> Result<f64> {
let mut total_loss = 0.0;
let mut sample_count = 0;
// Process samples in batches
for batch in samples.chunks(self.config.batch_size as usize) {
let batch_loss = self.process_batch_system_b(model, batch)?;
total_loss += batch_loss;
sample_count += batch.len();
}
Ok(total_loss / sample_count as f64)
}
/// Train one epoch for System B with active selection
fn train_epoch_system_b_active(
&mut self,
model: &mut SystemB,
samples: &[WindowedSample],
) -> Result<(f64, SystemBMetrics)> {
// Get active selector
let selector = model.active_selector()
.ok_or_else(|| TemporalNeuralError::TrainingError {
epoch: self.current_epoch as usize,
message: "Active selector not available".to_string(),
metrics: None,
})?;
// Extract embeddings and compute errors for all samples
let (embeddings, errors) = self.extract_embeddings_and_errors(model, samples)?;
// Add samples to selector
selector.add_samples(&embeddings, &errors)?;
// Select active samples
let selected_indices = selector.select_samples()?;
// Train on selected samples
let selected_samples: Vec<&WindowedSample> = selected_indices
.iter()
.map(|&idx| &samples[idx])
.collect();
let mut total_loss = 0.0;
let mut sample_count = 0;
for batch in selected_samples.chunks(self.config.batch_size as usize) {
let batch_loss = self.process_batch_system_b(model, batch)?;
total_loss += batch_loss;
sample_count += batch.len();
}
let avg_loss = total_loss / sample_count as f64;
let metrics = self.compute_system_b_metrics(model)?;
Ok((avg_loss, metrics))
}
/// Process a batch of samples for System A
fn process_batch_system_a(&mut self, model: &mut SystemA, batch: &[&WindowedSample]) -> Result<f64> {
let mut batch_loss = 0.0;
for &sample in batch {
// Forward pass
let prediction = model.forward(&sample.input)?;
// Compute loss
let loss = self.loss_fn.compute_loss(&prediction, &sample.target)?;
batch_loss += loss;
// Backward pass (simplified - in practice would compute gradients)
// This would involve computing gradients and updating parameters
}
// Apply optimizer (simplified)
self.optimizer.step(model.parameters_mut())?;
Ok(batch_loss / batch.len() as f64)
}
/// Process a batch of samples for System B
fn process_batch_system_b(&mut self, model: &mut SystemB, batch: &[&WindowedSample]) -> Result<f64> {
let mut batch_loss = 0.0;
let mut predictions = Vec::new();
let mut targets = Vec::new();
for &sample in batch {
// Forward pass with solver verification
let prediction_result = model.predict_with_solver(&sample.input)?;
// Update Kalman filter with ground truth
model.update_kalman_state(&sample.target)?;
// Store for batch loss computation
predictions.push(prediction_result);
targets.push(sample.target.clone());
}
// Compute residual learning loss
let residual_loss = model.compute_residual_loss(&predictions, &targets)?;
// Compute regularization terms
let reg_loss = model.compute_regularization_loss(&predictions);
batch_loss = residual_loss + reg_loss;
// Apply optimizer (simplified)
self.optimizer.step(model.parameters_mut())?;
Ok(batch_loss)
}
/// Evaluate System A on validation/test data
fn evaluate_system_a(&self, model: &SystemA, samples: &[WindowedSample]) -> Result<f64> {
let mut total_loss = 0.0;
let mut sample_count = 0;
for sample in samples {
let prediction = model.forward(&sample.input)?;
let loss = self.loss_fn.compute_loss(&prediction, &sample.target)?;
total_loss += loss;
sample_count += 1;
}
Ok(total_loss / sample_count as f64)
}
/// Evaluate System B on validation/test data
fn evaluate_system_b(&self, model: &SystemB, samples: &[WindowedSample]) -> Result<f64> {
let mut total_loss = 0.0;
let mut sample_count = 0;
for sample in samples {
// Use simple forward pass for evaluation (without solver verification for speed)
let prediction = model.forward(&sample.input)?;
let loss = self.loss_fn.compute_loss(&prediction, &sample.target)?;
total_loss += loss;
sample_count += 1;
}
Ok(total_loss / sample_count as f64)
}
/// Extract embeddings and compute errors for active selection
fn extract_embeddings_and_errors(
&self,
model: &SystemB,
samples: &[WindowedSample],
) -> Result<(Vec<DVector<f64>>, Vec<f64>)> {
let mut embeddings = Vec::new();
let mut errors = Vec::new();
for sample in samples {
// Get prediction
let prediction = model.forward(&sample.input)?;
// Compute error
let error = (&prediction - &sample.target).norm();
errors.push(error);
// For embeddings, we'd extract hidden layer activations
// For simplicity, use a hash of the input as embedding
let embedding = self.compute_simple_embedding(&sample.input);
embeddings.push(embedding);
}
Ok((embeddings, errors))
}
/// Compute simple embedding (placeholder)
fn compute_simple_embedding(&self, input: &DMatrix<f64>) -> DVector<f64> {
// Simplified: use mean and std of each feature as embedding
let mut embedding = Vec::new();
for i in 0..input.nrows() {
let row_data: Vec<f64> = input.row(i).iter().cloned().collect();
let mean = row_data.iter().sum::<f64>() / row_data.len() as f64;
let variance = row_data.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / row_data.len() as f64;
embedding.push(mean);
embedding.push(variance.sqrt());
}
DVector::from_vec(embedding)
}
/// Compute System B specific metrics
fn compute_system_b_metrics(&self, model: &SystemB) -> Result<SystemBMetrics> {
let solver_stats = model.get_solver_stats();
Ok(SystemBMetrics {
gate_pass_rate: solver_stats.gate_pass_rate,
avg_certificate_error: solver_stats.avg_certificate_error,
kalman_prediction_error: solver_stats.kalman_prediction_error,
active_selection_efficiency: 1.0, // Would be computed from selector stats
residual_loss: 0.0, // Would be tracked during training
})
}
/// Find the epoch with the best validation loss
fn find_best_epoch(&self) -> u32 {
self.history.val_losses
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(idx, _)| idx as u32)
.unwrap_or(0)
}
/// Get training history
pub fn get_history(&self) -> &TrainingHistory {
&self.history
}
/// Reset trainer for new training run
pub fn reset(&mut self) {
self.history = TrainingHistory {
train_losses: Vec::new(),
val_losses: Vec::new(),
learning_rates: Vec::new(),
epoch_times: Vec::new(),
metrics: Vec::new(),
};
self.current_epoch = 0;
self.best_val_loss = f64::INFINITY;
self.patience_counter = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
config::{Config, ModelConfig, TrainingConfig},
data::TimeSeriesData,
};
fn create_test_config() -> TrainingConfig {
TrainingConfig {
optimizer: "adam".to_string(),
learning_rate: 1e-3,
batch_size: 32,
epochs: 5,
patience: 10,
val_frequency: 1,
grad_clip: Some(1.0),
weight_decay: 1e-4,
smoothness_weight: 0.1,
checkpoint_frequency: 0,
}
}
fn create_test_data() -> DataSplits {
// Create minimal test data
let n_samples = 1000;
let features = nalgebra::DMatrix::from_fn(4, n_samples, |i, j| {
(i as f64 + j as f64 * 0.01).sin()
});
let data = TimeSeriesData::new(
features,
vec!["x".to_string(), "y".to_string(), "vx".to_string(), "vy".to_string()],
100.0,
"test".to_string(),
);
data.temporal_split(0.8, 0.1, 0.1).unwrap()
}
#[test]
fn test_trainer_creation() {
let config = create_test_config();
let trainer = Trainer::new(config).unwrap();
assert_eq!(trainer.current_epoch, 0);
assert_eq!(trainer.best_val_loss, f64::INFINITY);
}
#[test]
fn test_system_a_training() {
let training_config = create_test_config();
let mut trainer = Trainer::new(training_config).unwrap();
let model_config = ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 8,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "tanh".to_string(),
layer_norm: false,
};
let mut model = SystemA::new(&model_config).unwrap();
let data = create_test_data();
// This is a simplified test - full training would require gradient computation
// For now, just test that the training loop runs without errors
let result = trainer.train_system_a(&mut model, &data);
assert!(result.is_ok());
}
#[test]
fn test_training_history() {
let config = create_test_config();
let trainer = Trainer::new(config).unwrap();
let history = trainer.get_history();
assert!(history.train_losses.is_empty());
assert!(history.val_losses.is_empty());
}
}
@@ -0,0 +1,237 @@
//! Optimizers for neural network training
use crate::{
error::{Result, TemporalNeuralError},
models::ModelParams,
};
/// Trait for optimization algorithms
pub trait Optimizer: Send + Sync {
/// Perform one optimization step
fn step(&mut self, params: &mut dyn ModelParams) -> Result<()>;
/// Get current learning rate
fn get_learning_rate(&self) -> f64;
/// Set learning rate
fn set_learning_rate(&mut self, lr: f64);
/// Reset optimizer state
fn reset(&mut self);
}
/// Adam optimizer implementation
pub struct AdamOptimizer {
learning_rate: f64,
beta1: f64,
beta2: f64,
epsilon: f64,
step_count: usize,
}
impl AdamOptimizer {
/// Create new Adam optimizer
pub fn new(learning_rate: f64) -> Self {
Self {
learning_rate,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
step_count: 0,
}
}
/// Create Adam optimizer with custom parameters
pub fn with_params(learning_rate: f64, beta1: f64, beta2: f64, epsilon: f64) -> Self {
Self {
learning_rate,
beta1,
beta2,
epsilon,
step_count: 0,
}
}
}
impl Optimizer for AdamOptimizer {
fn step(&mut self, params: &mut dyn ModelParams) -> Result<()> {
self.step_count += 1;
// In a full implementation, this would:
// 1. Compute bias-corrected first and second moment estimates
// 2. Update parameters using adaptive learning rates
// For now, just apply basic gradient update
params.update_parameters(self.learning_rate);
Ok(())
}
fn get_learning_rate(&self) -> f64 {
self.learning_rate
}
fn set_learning_rate(&mut self, lr: f64) {
self.learning_rate = lr;
}
fn reset(&mut self) {
self.step_count = 0;
}
}
/// SGD optimizer implementation
pub struct SgdOptimizer {
learning_rate: f64,
momentum: f64,
weight_decay: f64,
}
impl SgdOptimizer {
/// Create new SGD optimizer
pub fn new(learning_rate: f64) -> Self {
Self {
learning_rate,
momentum: 0.0,
weight_decay: 0.0,
}
}
/// Create SGD optimizer with momentum
pub fn with_momentum(learning_rate: f64, momentum: f64) -> Self {
Self {
learning_rate,
momentum,
weight_decay: 0.0,
}
}
}
impl Optimizer for SgdOptimizer {
fn step(&mut self, params: &mut dyn ModelParams) -> Result<()> {
// Apply weight decay
if self.weight_decay > 0.0 {
params.apply_l2_regularization(self.weight_decay);
}
// Update parameters
params.update_parameters(self.learning_rate);
Ok(())
}
fn get_learning_rate(&self) -> f64 {
self.learning_rate
}
fn set_learning_rate(&mut self, lr: f64) {
self.learning_rate = lr;
}
fn reset(&mut self) {
// No state to reset for basic SGD
}
}
#[cfg(test)]
mod tests {
use super::*;
// Mock implementation for testing
struct MockParams {
values: Vec<f64>,
gradients: Vec<f64>,
}
impl ModelParams for MockParams {
fn initialize(_config: &crate::config::ModelConfig, _rng: &mut impl rand::Rng) -> Self {
Self {
values: vec![1.0, 2.0, 3.0],
gradients: vec![0.1, 0.2, 0.3],
}
}
fn parameter_count(&self) -> usize {
self.values.len()
}
fn apply_l2_regularization(&mut self, weight_decay: f64) {
for (grad, &param) in self.gradients.iter_mut().zip(self.values.iter()) {
*grad += weight_decay * param;
}
}
fn clip_gradients(&mut self, _max_norm: f64) {
// Simple implementation
}
fn zero_gradients(&mut self) {
self.gradients.fill(0.0);
}
fn update_parameters(&mut self, learning_rate: f64) {
for (param, &grad) in self.values.iter_mut().zip(self.gradients.iter()) {
*param -= learning_rate * grad;
}
}
fn parameter_stats(&self) -> crate::models::ParameterStats {
crate::models::ParameterStats {
mean_abs_value: 0.0,
std_dev: 0.0,
min_value: 0.0,
max_value: 0.0,
mean_abs_gradient: None,
gradient_norm: None,
}
}
}
#[test]
fn test_adam_optimizer() {
let mut optimizer = AdamOptimizer::new(0.01);
let mut params = MockParams::initialize(
&crate::config::ModelConfig {
model_type: "test".to_string(),
hidden_size: 1,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "linear".to_string(),
layer_norm: false,
},
&mut rand::thread_rng(),
);
let initial_values = params.values.clone();
optimizer.step(&mut params).unwrap();
// Parameters should have changed
assert_ne!(params.values, initial_values);
assert_eq!(optimizer.get_learning_rate(), 0.01);
}
#[test]
fn test_sgd_optimizer() {
let mut optimizer = SgdOptimizer::new(0.1);
let mut params = MockParams::initialize(
&crate::config::ModelConfig {
model_type: "test".to_string(),
hidden_size: 1,
num_layers: 1,
dropout: 0.0,
residual: false,
activation: "linear".to_string(),
layer_norm: false,
},
&mut rand::thread_rng(),
);
let initial_values = params.values.clone();
optimizer.step(&mut params).unwrap();
// Parameters should have changed
assert_ne!(params.values, initial_values);
assert_eq!(optimizer.get_learning_rate(), 0.1);
}
}
@@ -0,0 +1,183 @@
//! Utility functions and helpers
use crate::error::{Result, TemporalNeuralError};
use nalgebra::DMatrix;
use std::time::{Duration, Instant};
/// Timer for performance measurement
pub struct Timer {
start: Instant,
name: String,
}
impl Timer {
pub fn new(name: &str) -> Self {
Self {
start: Instant::now(),
name: name.to_string(),
}
}
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
pub fn elapsed_ms(&self) -> f64 {
self.elapsed().as_secs_f64() * 1000.0
}
pub fn elapsed_micros(&self) -> f64 {
self.elapsed().as_secs_f64() * 1_000_000.0
}
}
impl Drop for Timer {
fn drop(&mut self) {
println!("{}: {:.3}ms", self.name, self.elapsed_ms());
}
}
/// Mathematical utilities
pub mod math {
use super::*;
/// Compute softmax activation
pub fn softmax(input: &[f64]) -> Vec<f64> {
let max_val = input.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let exp_vals: Vec<f64> = input.iter().map(|&x| (x - max_val).exp()).collect();
let sum: f64 = exp_vals.iter().sum();
exp_vals.iter().map(|&x| x / sum).collect()
}
/// Compute ReLU activation
pub fn relu(x: f64) -> f64 {
x.max(0.0)
}
/// Compute tanh activation
pub fn tanh_activation(x: f64) -> f64 {
x.tanh()
}
/// Compute sigmoid activation
pub fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
/// Compute mean squared error
pub fn mse(predicted: &[f64], actual: &[f64]) -> Result<f64> {
if predicted.len() != actual.len() {
return Err(TemporalNeuralError::DimensionMismatch {
message: "Predicted and actual arrays have different lengths".to_string(),
expected: Some(actual.len().to_string()),
actual: Some(predicted.len().to_string()),
context: Some("mse computation".to_string()),
});
}
let sum_sq_error: f64 = predicted
.iter()
.zip(actual.iter())
.map(|(&p, &a)| (p - a).powi(2))
.sum();
Ok(sum_sq_error / predicted.len() as f64)
}
/// Compute mean absolute error
pub fn mae(predicted: &[f64], actual: &[f64]) -> Result<f64> {
if predicted.len() != actual.len() {
return Err(TemporalNeuralError::DimensionMismatch {
message: "Predicted and actual arrays have different lengths".to_string(),
expected: Some(actual.len().to_string()),
actual: Some(predicted.len().to_string()),
context: Some("mae computation".to_string()),
});
}
let sum_abs_error: f64 = predicted
.iter()
.zip(actual.iter())
.map(|(&p, &a)| (p - a).abs())
.sum();
Ok(sum_abs_error / predicted.len() as f64)
}
}
/// Memory utilities
pub mod memory {
use super::*;
/// Get current memory usage in bytes
pub fn get_memory_usage() -> Result<usize> {
// Placeholder for memory monitoring
// In real implementation, would use system calls
Ok(0)
}
/// Calculate matrix memory footprint
pub fn matrix_memory_size(matrix: &DMatrix<f64>) -> usize {
matrix.nrows() * matrix.ncols() * std::mem::size_of::<f64>()
}
}
/// Validation utilities
pub mod validation {
use super::*;
/// Validate matrix dimensions for operations
pub fn validate_matrix_dims(
a: &DMatrix<f64>,
b: &DMatrix<f64>,
operation: &str,
) -> Result<()> {
match operation {
"multiply" => {
if a.ncols() != b.nrows() {
return Err(TemporalNeuralError::DimensionMismatch {
message: "Matrix dimensions incompatible for multiplication".to_string(),
expected: Some(a.ncols().to_string()),
actual: Some(b.nrows().to_string()),
context: Some("matrix multiplication".to_string()),
});
}
}
"add" | "subtract" => {
if a.shape() != b.shape() {
return Err(TemporalNeuralError::DimensionMismatch {
message: "Matrix shapes must match for addition/subtraction".to_string(),
expected: Some(format!("{}x{}", a.nrows(), a.ncols())),
actual: Some(format!("{}x{}", b.nrows(), b.ncols())),
context: Some(operation.to_string()),
});
}
}
_ => {
return Err(TemporalNeuralError::ConfigurationError {
message: format!("Unknown operation: {}", operation),
field: Some("operation".to_string()),
});
}
}
Ok(())
}
/// Validate prediction bounds
pub fn validate_prediction_bounds(prediction: &[f64], bounds: (f64, f64)) -> Result<()> {
for &value in prediction {
if value < bounds.0 || value > bounds.1 {
return Err(TemporalNeuralError::ValidationError {
message: format!(
"Prediction value {} outside bounds [{}, {}]",
value, bounds.0, bounds.1
),
expected: Some(format!("[{}, {}]", bounds.0, bounds.1)),
actual: Some(value.to_string()),
rule: Some("bounds_check".to_string()),
});
}
}
Ok(())
}
}
@@ -0,0 +1,618 @@
//! WASM bindings for temporal neural network
//!
//! This module provides WebAssembly bindings for the temporal neural network,
//! enabling sub-millisecond neural inference in web browsers and Node.js.
use wasm_bindgen::prelude::*;
use js_sys::{Array, Object, Reflect};
use web_sys::console;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{
config::{Config, ModelConfig, TrainingConfig, InferenceConfig},
error::{Result, TemporalNeuralError},
};
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
console::log_1(&"Temporal Neural Solver initialized".into());
}
/// WASM wrapper for temporal neural network configuration
#[wasm_bindgen]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmConfig {
inner: Config,
}
#[wasm_bindgen]
impl WasmConfig {
/// Create a new configuration from JSON
#[wasm_bindgen(constructor)]
pub fn new(json_config: &str) -> Result<WasmConfig, JsValue> {
let config: Config = serde_json::from_str(json_config)
.map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
Ok(WasmConfig { inner: config })
}
/// Create default configuration for System A
#[wasm_bindgen(js_name = systemA)]
pub fn system_a() -> WasmConfig {
// Create default System A config - simplified for WASM demo
let config = Config {
common: crate::config::CommonConfig {
horizon_ms: 100,
window_ms: 256,
sample_rate_hz: 1000,
features: vec!["x".to_string(), "y".to_string(), "vx".to_string(), "vy".to_string()],
quantize: true,
random_seed: Some(42),
verbose: false,
},
model: ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 32,
num_layers: 2,
dropout: 0.1,
residual: true,
activation: "tanh".to_string(),
layer_norm: false,
},
training: TrainingConfig {
optimizer: "adam".to_string(),
learning_rate: 0.001,
batch_size: 32,
epochs: 100,
patience: 10,
val_frequency: 5,
grad_clip: Some(1.0),
weight_decay: 0.0001,
smoothness_weight: 0.1,
checkpoint_frequency: 10,
},
inference: InferenceConfig {
target_latency_ms: 0.9,
enable_simd: false, // Disabled for WASM compatibility
num_threads: 1,
pin_memory: false,
cpu_affinity: None,
batch_size: 1,
},
system: crate::config::SystemConfig::Traditional(crate::config::TraditionalConfig {
enabled: true,
}),
};
WasmConfig { inner: config }
}
/// Create default configuration for System B (temporal solver)
#[wasm_bindgen(js_name = systemB)]
pub fn system_b() -> WasmConfig {
// Create default System B config - simplified for WASM demo
let config = Config {
common: crate::config::CommonConfig {
horizon_ms: 100,
window_ms: 256,
sample_rate_hz: 1000,
features: vec!["x".to_string(), "y".to_string(), "vx".to_string(), "vy".to_string()],
quantize: true,
random_seed: Some(42),
verbose: false,
},
model: ModelConfig {
model_type: "micro_gru".to_string(),
hidden_size: 32,
num_layers: 2,
dropout: 0.1,
residual: true,
activation: "tanh".to_string(),
layer_norm: false,
},
training: TrainingConfig {
optimizer: "adam".to_string(),
learning_rate: 0.001,
batch_size: 32,
epochs: 100,
patience: 10,
val_frequency: 5,
grad_clip: Some(1.0),
weight_decay: 0.0001,
smoothness_weight: 0.1,
checkpoint_frequency: 10,
},
inference: InferenceConfig {
target_latency_ms: 0.9,
enable_simd: false, // Disabled for WASM compatibility
num_threads: 1,
pin_memory: false,
cpu_affinity: None,
batch_size: 1,
},
system: crate::config::SystemConfig::TemporalSolver(crate::config::TemporalSolverConfig {
prior: crate::config::KalmanConfig {
process_noise: 0.01,
measurement_noise: 0.1,
initial_uncertainty: 1.0,
transition_model: "constant_velocity".to_string(),
update_frequency: 100.0,
},
solver_gate: crate::config::SolverGateConfig {
algorithm: "neumann".to_string(),
error_threshold: 0.01,
max_iterations: 100,
confidence_threshold: 0.9,
fallback_enabled: true,
},
active_selection: crate::config::ActiveSelectionConfig {
enabled: true,
selection_ratio: 0.1,
embedding_dim: 16,
pagerank_damping: 0.85,
update_frequency: 10,
},
}),
};
WasmConfig { inner: config }
}
/// Export configuration as JSON
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(&self.inner).unwrap_or_default()
}
}
/// WASM wrapper for temporal neural network models
#[wasm_bindgen]
pub struct WasmTemporalSolver {
predictor: Option<Box<dyn PredictorTrait>>,
model_type: String,
config: Config,
is_trained: bool,
}
/// Trait for unified predictor interface in WASM
trait PredictorTrait {
fn predict(&mut self, input: &[f64]) -> Result<Prediction>;
fn predict_batch(&mut self, inputs: &[Vec<f64>]) -> Result<Vec<Prediction>>;
fn get_latency_stats(&self) -> LatencyStats;
fn warmup(&mut self, iterations: u32) -> Result<()>;
}
/// Wrapper for System A predictor
struct SystemAPredictor {
predictor: Predictor,
}
impl PredictorTrait for SystemAPredictor {
fn predict(&mut self, input: &[f64]) -> Result<Prediction> {
let matrix = nalgebra::DMatrix::from_row_slice(4, input.len() / 4, input);
self.predictor.predict(&matrix)
}
fn predict_batch(&mut self, inputs: &[Vec<f64>]) -> Result<Vec<Prediction>> {
let matrices: Result<Vec<_>> = inputs.iter()
.map(|input| {
Ok(nalgebra::DMatrix::from_row_slice(4, input.len() / 4, input))
})
.collect();
self.predictor.predict_batch(&matrices?)
}
fn get_latency_stats(&self) -> LatencyStats {
let stats = self.predictor.get_statistics();
LatencyStats {
avg_latency_us: stats.avg_latency_us,
p50_latency_us: stats.p50_latency_us,
p99_latency_us: stats.p99_latency_us,
p99_9_latency_us: stats.p99_9_latency_us,
violation_rate: stats.violation_rate,
throughput_pred_per_sec: stats.throughput_pred_per_sec,
}
}
fn warmup(&mut self, iterations: u32) -> Result<()> {
self.predictor.warmup(iterations as usize)
}
}
/// Wrapper for System B predictor
struct SystemBPredictor {
predictor: Predictor,
}
impl PredictorTrait for SystemBPredictor {
fn predict(&mut self, input: &[f64]) -> Result<Prediction> {
let matrix = nalgebra::DMatrix::from_row_slice(4, input.len() / 4, input);
self.predictor.predict(&matrix)
}
fn predict_batch(&mut self, inputs: &[Vec<f64>]) -> Result<Vec<Prediction>> {
let matrices: Result<Vec<_>> = inputs.iter()
.map(|input| {
Ok(nalgebra::DMatrix::from_row_slice(4, input.len() / 4, input))
})
.collect();
self.predictor.predict_batch(&matrices?)
}
fn get_latency_stats(&self) -> LatencyStats {
let stats = self.predictor.get_statistics();
LatencyStats {
avg_latency_us: stats.avg_latency_us,
p50_latency_us: stats.p50_latency_us,
p99_latency_us: stats.p99_latency_us,
p99_9_latency_us: stats.p99_9_latency_us,
violation_rate: stats.violation_rate,
throughput_pred_per_sec: stats.throughput_pred_per_sec,
}
}
fn warmup(&mut self, iterations: u32) -> Result<()> {
self.predictor.warmup(iterations as usize)
}
}
/// Latency statistics for WASM export
#[wasm_bindgen]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatencyStats {
pub avg_latency_us: f64,
pub p50_latency_us: f64,
pub p99_latency_us: f64,
pub p99_9_latency_us: f64,
pub violation_rate: f64,
pub throughput_pred_per_sec: f64,
}
/// Prediction result for WASM export
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct WasmPrediction {
prediction: Prediction,
}
#[wasm_bindgen]
impl WasmPrediction {
/// Get predicted values as array
#[wasm_bindgen(getter = values)]
pub fn values(&self) -> Vec<f64> {
self.prediction.values.as_slice().to_vec()
}
/// Get confidence score (0.0 to 1.0)
#[wasm_bindgen(getter = confidence)]
pub fn confidence(&self) -> f64 {
self.prediction.confidence
}
/// Get prediction latency in microseconds
#[wasm_bindgen(getter = latency_us)]
pub fn latency_us(&self) -> f64 {
self.prediction.latency_us
}
/// Get certificate error bound (System B only)
#[wasm_bindgen(getter = certificate_error)]
pub fn certificate_error(&self) -> Option<f64> {
self.prediction.certificate.as_ref().map(|c| c.error_bound)
}
/// Check if solver gate passed (System B only)
#[wasm_bindgen(getter = gate_passed)]
pub fn gate_passed(&self) -> Option<bool> {
self.prediction.certificate.as_ref().map(|c| c.is_valid)
}
/// Get prediction metadata as JSON
#[wasm_bindgen(js_name = getMetadataJSON)]
pub fn get_metadata_json(&self) -> String {
serde_json::to_string(&self.prediction.metadata).unwrap_or_default()
}
}
#[wasm_bindgen]
impl WasmTemporalSolver {
/// Create a new temporal solver instance
#[wasm_bindgen(constructor)]
pub fn new(config: &WasmConfig) -> WasmTemporalSolver {
WasmTemporalSolver {
predictor: None,
model_type: "uninitialized".to_string(),
config: config.inner.clone(),
is_trained: false,
}
}
/// Initialize System A (traditional neural network)
#[wasm_bindgen(js_name = initSystemA)]
pub fn init_system_a(&mut self) -> Result<(), JsValue> {
let model = SystemA::new(&self.config.model)
.map_err(|e| JsValue::from_str(&format!("Failed to create System A: {}", e)))?;
let predictor = Predictor::new_system_a(model, self.config.inference.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to create predictor: {}", e)))?;
self.predictor = Some(Box::new(SystemAPredictor { predictor }));
self.model_type = "SystemA".to_string();
Ok(())
}
/// Initialize System B (temporal solver neural network)
#[wasm_bindgen(js_name = initSystemB)]
pub fn init_system_b(&mut self) -> Result<(), JsValue> {
let temporal_config = match &self.config.system {
crate::config::SystemConfig::TemporalSolver(config) => config.clone(),
_ => return Err(JsValue::from_str("Config must be for temporal solver system")),
};
let model = SystemB::new(&self.config.model, &temporal_config)
.map_err(|e| JsValue::from_str(&format!("Failed to create System B: {}", e)))?;
let predictor = Predictor::new_system_b(model, self.config.inference.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to create predictor: {}", e)))?;
self.predictor = Some(Box::new(SystemBPredictor { predictor }));
self.model_type = "SystemB".to_string();
Ok(())
}
/// Perform single prediction
#[wasm_bindgen]
pub fn predict(&mut self, input: &[f64]) -> Result<WasmPrediction, JsValue> {
let predictor = self.predictor.as_mut()
.ok_or_else(|| JsValue::from_str("Model not initialized"))?;
let prediction = predictor.predict(input)
.map_err(|e| JsValue::from_str(&format!("Prediction failed: {}", e)))?;
Ok(WasmPrediction { prediction })
}
/// Perform batch predictions
#[wasm_bindgen(js_name = predictBatch)]
pub fn predict_batch(&mut self, inputs: &JsValue) -> Result<Array, JsValue> {
let predictor = self.predictor.as_mut()
.ok_or_else(|| JsValue::from_str("Model not initialized"))?;
// Convert JS array to Vec<Vec<f64>>
let js_array = Array::from(inputs);
let mut input_vectors = Vec::new();
for i in 0..js_array.length() {
let js_input = js_array.get(i);
let input_array = Array::from(&js_input);
let mut input_vec = Vec::new();
for j in 0..input_array.length() {
let val = input_array.get(j).as_f64()
.ok_or_else(|| JsValue::from_str("Input must be numeric"))?;
input_vec.push(val);
}
input_vectors.push(input_vec);
}
let predictions = predictor.predict_batch(&input_vectors)
.map_err(|e| JsValue::from_str(&format!("Batch prediction failed: {}", e)))?;
let result_array = Array::new();
for prediction in predictions {
let wasm_pred = WasmPrediction { prediction };
result_array.push(&JsValue::from(wasm_pred));
}
Ok(result_array)
}
/// Get current latency statistics
#[wasm_bindgen(js_name = getStats)]
pub fn get_stats(&self) -> Result<LatencyStats, JsValue> {
let predictor = self.predictor.as_ref()
.ok_or_else(|| JsValue::from_str("Model not initialized"))?;
Ok(predictor.get_latency_stats())
}
/// Warm up the model for consistent latency
#[wasm_bindgen]
pub fn warmup(&mut self, iterations: u32) -> Result<(), JsValue> {
let predictor = self.predictor.as_mut()
.ok_or_else(|| JsValue::from_str("Model not initialized"))?;
predictor.warmup(iterations)
.map_err(|e| JsValue::from_str(&format!("Warmup failed: {}", e)))?;
Ok(())
}
/// Check if model meets performance targets
#[wasm_bindgen(js_name = meetsTargets)]
pub fn meets_targets(&self) -> bool {
if let Some(predictor) = &self.predictor {
let stats = predictor.get_latency_stats();
stats.p99_9_latency_us <= 900.0 && stats.violation_rate <= 0.001
} else {
false
}
}
/// Get model type
#[wasm_bindgen(getter = model_type)]
pub fn model_type(&self) -> String {
self.model_type.clone()
}
/// Check if model is trained
#[wasm_bindgen(getter = is_trained)]
pub fn is_trained(&self) -> bool {
self.is_trained
}
/// Get build information
#[wasm_bindgen(js_name = getBuildInfo)]
pub fn get_build_info() -> String {
let build_info = crate::build_info();
serde_json::to_string(&build_info).unwrap_or_default()
}
/// Benchmark the solver
#[wasm_bindgen]
pub fn benchmark(&mut self, num_predictions: u32) -> Result<String, JsValue> {
let predictor = self.predictor.as_mut()
.ok_or_else(|| JsValue::from_str("Model not initialized"))?;
let start_time = js_sys::Date::now();
let test_input = vec![1.0; 1024]; // 4x256 test input
for _ in 0..num_predictions {
predictor.predict(&test_input)
.map_err(|e| JsValue::from_str(&format!("Benchmark prediction failed: {}", e)))?;
}
let end_time = js_sys::Date::now();
let total_time_ms = end_time - start_time;
let avg_latency_ms = total_time_ms / num_predictions as f64;
let throughput = num_predictions as f64 / (total_time_ms / 1000.0);
let stats = predictor.get_latency_stats();
let benchmark_result = serde_json::json!({
"num_predictions": num_predictions,
"total_time_ms": total_time_ms,
"avg_latency_ms": avg_latency_ms,
"avg_latency_us": avg_latency_ms * 1000.0,
"throughput_pred_per_sec": throughput,
"p99_9_latency_us": stats.p99_9_latency_us,
"meets_target": avg_latency_ms * 1000.0 <= 900.0,
"model_type": self.model_type,
});
Ok(benchmark_result.to_string())
}
}
/// Utilities for WASM
#[wasm_bindgen]
pub struct WasmUtils;
#[wasm_bindgen]
impl WasmUtils {
/// Get version information
#[wasm_bindgen(js_name = getVersion)]
pub fn get_version() -> String {
crate::VERSION.to_string()
}
/// Check if SIMD is supported
#[wasm_bindgen(js_name = hasSIMD)]
pub fn has_simd() -> bool {
// In WASM, SIMD support would need to be detected at runtime
false // Conservative default
}
/// Log message to console
#[wasm_bindgen(js_name = log)]
pub fn log(message: &str) {
console::log_1(&message.into());
}
/// Generate sample trajectory data for testing
#[wasm_bindgen(js_name = generateSampleData)]
pub fn generate_sample_data(length: u32) -> Array {
let mut data = Array::new();
for i in 0..length {
let t = i as f64 * 0.01;
let trajectory = Array::new();
// Simple circular trajectory
trajectory.push(&JsValue::from(t.cos())); // x
trajectory.push(&JsValue::from(t.sin())); // y
trajectory.push(&JsValue::from(-t.sin())); // vx
trajectory.push(&JsValue::from(t.cos())); // vy
data.push(&trajectory);
}
data
}
/// Calculate temporal lead for given distance
#[wasm_bindgen(js_name = calculateTemporalLead)]
pub fn calculate_temporal_lead(distance_km: f64, computation_us: f64) -> f64 {
let light_speed_km_per_s = 299_792.458; // km/ms in vacuum
let light_travel_us = (distance_km / light_speed_km_per_s) * 1000.0;
light_travel_us - computation_us
}
}
/// Training interface for WASM (simplified)
#[wasm_bindgen]
pub struct WasmTrainer {
trainer: Option<Trainer>,
config: TrainingConfig,
}
#[wasm_bindgen]
impl WasmTrainer {
/// Create new trainer
#[wasm_bindgen(constructor)]
pub fn new(config_json: &str) -> Result<WasmTrainer, JsValue> {
let config: TrainingConfig = serde_json::from_str(config_json)
.map_err(|e| JsValue::from_str(&format!("Invalid training config: {}", e)))?;
let trainer = Trainer::new(config.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to create trainer: {}", e)))?;
Ok(WasmTrainer {
trainer: Some(trainer),
config,
})
}
/// Train model with provided data
#[wasm_bindgen]
pub fn train(&mut self, data_json: &str) -> Result<String, JsValue> {
let _trainer = self.trainer.as_mut()
.ok_or_else(|| JsValue::from_str("Trainer not initialized"))?;
// For WASM, we'll provide a simplified training interface
// Full training would require streaming data support
let result = serde_json::json!({
"message": "Training interface available - use full Rust API for production training",
"epochs_completed": 0,
"final_loss": 0.0,
"training_time_seconds": 0.0
});
Ok(result.to_string())
}
}
// Error handling utilities
impl From<TemporalNeuralError> for JsValue {
fn from(error: TemporalNeuralError) -> Self {
JsValue::from_str(&format!("{}", error))
}
}
// Export main initialization function
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// Macro for logging from WASM
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
pub(crate) use console_log;