wifi-densepose/vendor/midstream/crates/temporal-attractor-studio/src/lib.rs

483 lines
14 KiB
Rust

//! # Temporal-Attractor-Studio
//!
//! Dynamical systems and strange attractors analysis.
//!
//! ## Features
//! - Attractor classification (point, limit cycle, strange)
//! - Lyapunov exponent calculation
//! - Phase space analysis
//! - Trajectory visualization data
//! - Stability detection
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use thiserror::Error;
/// Attractor analysis errors
#[derive(Debug, Error)]
pub enum AttractorError {
#[error("Insufficient data: need at least {0} points")]
InsufficientData(usize),
#[error("Invalid dimension: {0}")]
InvalidDimension(usize),
#[error("Computation error: {0}")]
ComputationError(String),
}
/// Types of attractors
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttractorType {
/// Point attractor (stable equilibrium)
PointAttractor,
/// Limit cycle (periodic behavior)
LimitCycle,
/// Strange attractor (chaotic behavior)
StrangeAttractor,
/// No clear attractor detected
Unknown,
}
/// A point in phase space
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhasePoint {
pub coordinates: Vec<f64>,
pub timestamp: u64,
}
impl PhasePoint {
pub fn new(coordinates: Vec<f64>, timestamp: u64) -> Self {
Self { coordinates, timestamp }
}
pub fn dimension(&self) -> usize {
self.coordinates.len()
}
}
/// A trajectory in phase space
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trajectory {
pub points: VecDeque<PhasePoint>,
pub max_length: usize,
}
impl Trajectory {
pub fn new(max_length: usize) -> Self {
Self {
points: VecDeque::new(),
max_length,
}
}
pub fn push(&mut self, point: PhasePoint) {
if self.points.len() >= self.max_length {
self.points.pop_front();
}
self.points.push_back(point);
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn clear(&mut self) {
self.points.clear();
}
}
/// Information about a detected attractor
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttractorInfo {
pub attractor_type: AttractorType,
pub dimension: usize,
pub lyapunov_exponents: Vec<f64>,
pub is_stable: bool,
pub confidence: f64,
}
impl AttractorInfo {
pub fn is_chaotic(&self) -> bool {
matches!(self.attractor_type, AttractorType::StrangeAttractor)
}
pub fn max_lyapunov_exponent(&self) -> Option<f64> {
self.lyapunov_exponents.iter().copied().max_by(|a, b| {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
})
}
}
/// Behavior summary statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorSummary {
pub total_points: usize,
pub dimension: usize,
pub attractor_info: Option<AttractorInfo>,
pub mean_velocity: f64,
pub trajectory_length: f64,
}
/// Attractor analyzer
pub struct AttractorAnalyzer {
embedding_dimension: usize,
min_points_for_analysis: usize,
trajectory: Trajectory,
}
impl AttractorAnalyzer {
/// Create a new attractor analyzer
pub fn new(embedding_dimension: usize, max_trajectory_length: usize) -> Self {
Self {
embedding_dimension,
min_points_for_analysis: 100,
trajectory: Trajectory::new(max_trajectory_length),
}
}
/// Add a point to the trajectory
pub fn add_point(&mut self, point: PhasePoint) -> Result<(), AttractorError> {
if point.dimension() != self.embedding_dimension {
return Err(AttractorError::InvalidDimension(point.dimension()));
}
self.trajectory.push(point);
Ok(())
}
/// Analyze the current trajectory
pub fn analyze(&self) -> Result<AttractorInfo, AttractorError> {
if self.trajectory.len() < self.min_points_for_analysis {
return Err(AttractorError::InsufficientData(self.min_points_for_analysis));
}
// Calculate Lyapunov exponents
let lyapunov_exponents = self.calculate_lyapunov_exponents()?;
// Classify attractor type based on Lyapunov exponents
let attractor_type = self.classify_attractor(&lyapunov_exponents);
// Determine stability
let is_stable = lyapunov_exponents.iter().all(|&l| l < 0.0);
// Calculate confidence based on data quality
let confidence = self.calculate_confidence();
Ok(AttractorInfo {
attractor_type,
dimension: self.embedding_dimension,
lyapunov_exponents,
is_stable,
confidence,
})
}
/// Calculate Lyapunov exponents for the trajectory
fn calculate_lyapunov_exponents(&self) -> Result<Vec<f64>, AttractorError> {
if self.trajectory.len() < 2 {
return Ok(vec![0.0; self.embedding_dimension]);
}
let mut exponents = vec![0.0; self.embedding_dimension];
// Simplified Lyapunov calculation
// In production, this would use more sophisticated methods
let points: Vec<&PhasePoint> = self.trajectory.points.iter().collect();
for dim in 0..self.embedding_dimension {
let mut sum_log_divergence = 0.0;
let mut count = 0;
for i in 1..points.len() {
let diff = points[i].coordinates[dim] - points[i-1].coordinates[dim];
if diff.abs() > 1e-10 {
sum_log_divergence += diff.abs().ln();
count += 1;
}
}
if count > 0 {
exponents[dim] = sum_log_divergence / count as f64;
}
}
Ok(exponents)
}
/// Classify attractor based on Lyapunov exponents
fn classify_attractor(&self, lyapunov_exponents: &[f64]) -> AttractorType {
let max_exponent = lyapunov_exponents.iter()
.copied()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
if max_exponent > 0.1 {
// Positive Lyapunov exponent indicates chaos
AttractorType::StrangeAttractor
} else if max_exponent > -0.1 && self.detect_periodicity() {
// Near-zero with periodicity indicates limit cycle
AttractorType::LimitCycle
} else if max_exponent < -0.1 {
// Negative Lyapunov exponent indicates stable point
AttractorType::PointAttractor
} else {
AttractorType::Unknown
}
}
/// Detect if trajectory shows periodic behavior
fn detect_periodicity(&self) -> bool {
if self.trajectory.len() < 20 {
return false;
}
// Simple autocorrelation check
let points: Vec<&PhasePoint> = self.trajectory.points.iter().collect();
let n = points.len();
// Check for repeating patterns
for lag in 5..n/4 {
let mut correlation = 0.0;
let mut count = 0;
for i in 0..n-lag {
for dim in 0..self.embedding_dimension {
let diff = (points[i].coordinates[dim] - points[i+lag].coordinates[dim]).abs();
correlation += diff;
count += 1;
}
}
let avg_diff = correlation / count as f64;
if avg_diff < 0.1 {
return true; // Found periodic pattern
}
}
false
}
/// Calculate confidence in the analysis
fn calculate_confidence(&self) -> f64 {
let data_ratio = self.trajectory.len() as f64 / self.min_points_for_analysis as f64;
data_ratio.min(1.0)
}
/// Get trajectory statistics
pub fn get_trajectory_stats(&self) -> BehaviorSummary {
let total_points = self.trajectory.len();
let mut trajectory_length = 0.0;
let mut velocity_sum = 0.0;
let points: Vec<&PhasePoint> = self.trajectory.points.iter().collect();
for i in 1..points.len() {
let mut distance = 0.0;
for dim in 0..self.embedding_dimension {
let diff = points[i].coordinates[dim] - points[i-1].coordinates[dim];
distance += diff * diff;
}
let segment_length = distance.sqrt();
trajectory_length += segment_length;
let time_diff = (points[i].timestamp - points[i-1].timestamp) as f64;
if time_diff > 0.0 {
velocity_sum += segment_length / time_diff;
}
}
let mean_velocity = if points.len() > 1 {
velocity_sum / (points.len() - 1) as f64
} else {
0.0
};
let attractor_info = if total_points >= self.min_points_for_analysis {
self.analyze().ok()
} else {
None
};
BehaviorSummary {
total_points,
dimension: self.embedding_dimension,
attractor_info,
mean_velocity,
trajectory_length,
}
}
/// Clear the trajectory
pub fn clear(&mut self) {
self.trajectory.clear();
}
/// Get current trajectory length
pub fn trajectory_length(&self) -> usize {
self.trajectory.len()
}
}
impl Default for AttractorAnalyzer {
fn default() -> Self {
Self::new(3, 10000)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_point() {
let point = PhasePoint::new(vec![1.0, 2.0, 3.0], 100);
assert_eq!(point.dimension(), 3);
}
#[test]
fn test_trajectory() {
let mut traj = Trajectory::new(10);
assert!(traj.is_empty());
traj.push(PhasePoint::new(vec![1.0, 2.0], 1));
assert_eq!(traj.len(), 1);
// Fill to capacity
for i in 2..=11 {
traj.push(PhasePoint::new(vec![i as f64, i as f64 * 2.0], i as u64));
}
// Should maintain max length
assert_eq!(traj.len(), 10);
}
#[test]
fn test_attractor_analyzer() {
let mut analyzer = AttractorAnalyzer::new(2, 1000);
// Add some points
for i in 0..150 {
let point = PhasePoint::new(
vec![i as f64, (i * 2) as f64],
i as u64 * 1000,
);
analyzer.add_point(point).unwrap();
}
assert_eq!(analyzer.trajectory_length(), 150);
let result = analyzer.analyze();
assert!(result.is_ok());
let info = result.unwrap();
assert_eq!(info.dimension, 2);
assert!(!info.lyapunov_exponents.is_empty());
}
#[test]
fn test_invalid_dimension() {
let mut analyzer = AttractorAnalyzer::new(3, 1000);
let point = PhasePoint::new(vec![1.0, 2.0], 100); // Only 2D
let result = analyzer.add_point(point);
assert!(result.is_err());
}
#[test]
fn test_insufficient_data() {
let analyzer = AttractorAnalyzer::new(2, 1000);
// Not enough points for analysis
let result = analyzer.analyze();
assert!(result.is_err());
}
#[test]
fn test_behavior_summary() {
let mut analyzer = AttractorAnalyzer::new(2, 1000);
for i in 0..50 {
let point = PhasePoint::new(
vec![i as f64, i as f64],
i as u64 * 100,
);
analyzer.add_point(point).unwrap();
}
let summary = analyzer.get_trajectory_stats();
assert_eq!(summary.total_points, 50);
assert_eq!(summary.dimension, 2);
assert!(summary.trajectory_length > 0.0);
}
#[test]
fn test_nan_handling_in_lyapunov_exponents() {
// Test that NaN values don't cause panics in max_lyapunov_exponent
let info = AttractorInfo {
attractor_type: AttractorType::StrangeAttractor,
dimension: 3,
lyapunov_exponents: vec![1.0, f64::NAN, -0.5],
is_stable: false,
confidence: 0.95,
};
// Should not panic, should handle NaN gracefully
let max_exp = info.max_lyapunov_exponent();
assert!(max_exp.is_some());
// With NaN handling, should return one of the valid values
let val = max_exp.unwrap();
assert!(val.is_finite(), "Should not return NaN");
}
#[test]
fn test_nan_handling_in_trajectory() {
// Test that NaN values in trajectory don't cause panics during analysis
let mut analyzer = AttractorAnalyzer::new(2, 1000);
// Add points including some with NaN to trigger edge cases
for i in 0..150 {
let coords = if i == 50 {
// Insert a point that could lead to NaN in calculations
vec![f64::NAN, i as f64]
} else {
vec![i as f64, (i * 2) as f64]
};
let point = PhasePoint::new(coords, i as u64 * 1000);
// Should not panic when adding or analyzing
let _ = analyzer.add_point(point);
}
// Analysis should complete without panicking
let result = analyzer.analyze();
assert!(result.is_ok(), "Analysis should handle NaN gracefully");
let info = result.unwrap();
// Verify the result is usable
assert_eq!(info.dimension, 2);
}
#[test]
fn test_all_nan_lyapunov_exponents() {
// Edge case: all NaN values
let info = AttractorInfo {
attractor_type: AttractorType::Unknown,
dimension: 2,
lyapunov_exponents: vec![f64::NAN, f64::NAN],
is_stable: false,
confidence: 0.5,
};
// Should not panic even with all NaN
let max_exp = info.max_lyapunov_exponent();
assert!(max_exp.is_some());
}
}