use crate::eisenstein::{EisensteinConstraint, SnapResult, COVERING_RADIUS};
const HISTORY_SIZE: usize = 64;
pub struct TemporalAgent {
constraint: EisensteinConstraint,
history: [Option<SnapResult>; HISTORY_SIZE],
history_pos: usize,
history_count: usize,
pub decay_rate: f64,
pub prediction_horizon: usize,
pub anomaly_sigma: f64,
pub learning_rate: f64,
pub chirality_lock_threshold: u16,
pub merge_trust: f64,
error_mean: f64,
error_var: f64,
convergence_rate: f64,
precision_energy: f64,
predicted_error: f64,
prediction_error: f64,
chirality: ChiralityState,
phase: FunnelPhase,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChiralityState {
Exploring { chamber_hops: u32 },
Locking { dominant: u8, confidence_milli: u16 },
Locked { chamber: u8 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunnelPhase {
Approach,
Narrowing,
SnapImminent,
Crystallized,
Anomaly,
}
#[derive(Debug)]
pub struct TemporalUpdate {
pub snap: SnapResult,
pub phase: FunnelPhase,
pub chirality: ChiralityState,
pub predicted_error: f64,
pub prediction_error: f64,
pub convergence_rate: f64,
pub precision_energy: f64,
pub is_anomaly: bool,
pub action: AgentAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AgentAction {
Continue,
Converging,
HoldSteady,
WidenFunnel,
CommitChirality,
Diverging,
Satisfied,
}
impl Default for TemporalAgent {
fn default() -> Self {
Self::new()
}
}
impl TemporalAgent {
pub fn new() -> Self {
TemporalAgent {
constraint: EisensteinConstraint::new(),
history: [None; HISTORY_SIZE],
history_pos: 0,
history_count: 0,
decay_rate: 1.0,
prediction_horizon: 4,
anomaly_sigma: 2.0,
learning_rate: 0.1,
chirality_lock_threshold: 500,
merge_trust: 0.5,
error_mean: 0.0,
error_var: 0.0,
convergence_rate: 0.0,
precision_energy: 0.0,
predicted_error: COVERING_RADIUS,
prediction_error: 0.0,
chirality: ChiralityState::Exploring { chamber_hops: 0 },
phase: FunnelPhase::Approach,
}
}
pub fn observe(&mut self, x: f64, y: f64) -> TemporalUpdate {
let snap = self.constraint.snap(x, y);
let error_norm = snap.error / COVERING_RADIUS;
let _proportional = error_norm;
self.precision_energy += if snap.error > 0.0 { 1.0 / snap.error } else { 1000.0 };
self.update_convergence_rate(error_norm);
self.prediction_error = (error_norm - self.predicted_error).abs();
self.update_statistics(error_norm);
self.predicted_error = self.predict_next(error_norm);
self.update_chirality(snap.chamber);
self.update_phase(error_norm);
self.history[self.history_pos] = Some(snap.clone());
self.history_pos = (self.history_pos + 1) % HISTORY_SIZE;
self.history_count += 1;
let is_anomaly = self.prediction_error > self.anomaly_sigma * self.error_var.sqrt().max(0.01);
let action = self.determine_action(error_norm, is_anomaly);
if is_anomaly && self.decay_rate > 0.1 {
self.decay_rate *= 0.9;
} else if error_norm < 0.2 && self.decay_rate < 5.0 {
self.decay_rate *= 1.05;
}
TemporalUpdate {
snap,
phase: self.phase,
chirality: self.chirality,
predicted_error: self.predicted_error,
prediction_error: self.prediction_error,
convergence_rate: self.convergence_rate,
precision_energy: self.precision_energy,
is_anomaly,
action,
}
}
pub fn deadband(&self, t: f64) -> f64 {
COVERING_RADIUS * (1.0 - t).powf(1.0 / self.decay_rate).max(0.0)
}
fn predict_next(&self, current: f64) -> f64 {
if self.history_count < 2 {
return current;
}
let predicted = current + self.convergence_rate * self.prediction_horizon as f64;
predicted.max(0.0).min(1.0)
}
fn update_convergence_rate(&mut self, current: f64) {
if self.history_count < 2 {
return;
}
let prev_pos = if self.history_pos == 0 { HISTORY_SIZE - 1 } else { self.history_pos - 1 };
if let Some(prev) = &self.history[prev_pos] {
let prev_norm = prev.error / COVERING_RADIUS;
let rate = current - prev_norm;
self.convergence_rate = self.learning_rate * rate + (1.0 - self.learning_rate) * self.convergence_rate;
}
}
fn update_statistics(&mut self, value: f64) {
let n = self.history_count as f64 + 1.0;
let delta = value - self.error_mean;
self.error_mean += delta / n;
let delta2 = value - self.error_mean;
self.error_var += delta * delta2;
}
fn update_chirality(&mut self, chamber: u8) {
match self.chirality {
ChiralityState::Exploring { ref mut chamber_hops } => {
*chamber_hops += 1;
if *chamber_hops > 10 {
if let Some(d) = self.dominant_chamber() {
let conf = self.chamber_confidence_milli(d);
if conf > self.chirality_lock_threshold {
self.chirality = ChiralityState::Locking {
dominant: d,
confidence_milli: conf,
};
}
}
}
}
ChiralityState::Locking { dominant, ref mut confidence_milli } => {
if chamber == dominant {
*confidence_milli = confidence_milli.saturating_add(50);
if *confidence_milli > 900 {
self.chirality = ChiralityState::Locked { chamber: dominant };
}
} else {
*confidence_milli = confidence_milli.saturating_sub(100);
if *confidence_milli < 300 {
self.chirality = ChiralityState::Exploring { chamber_hops: 0 };
}
}
}
ChiralityState::Locked { .. } => {
}
}
}
fn update_phase(&mut self, error_norm: f64) {
self.phase = if error_norm > 0.9 {
FunnelPhase::Approach
} else if error_norm > 0.5 {
FunnelPhase::Narrowing
} else if error_norm > 0.15 {
FunnelPhase::SnapImminent
} else if error_norm < 0.05 {
FunnelPhase::Crystallized
} else if self.phase == FunnelPhase::Anomaly {
FunnelPhase::Anomaly
} else {
FunnelPhase::Narrowing
};
}
fn determine_action(&self, error_norm: f64, is_anomaly: bool) -> AgentAction {
if is_anomaly {
return AgentAction::WidenFunnel;
}
if error_norm < 0.05 {
return AgentAction::Satisfied;
}
if matches!(self.chirality, ChiralityState::Locked { .. }) {
if !matches!(self.phase, FunnelPhase::Crystallized) {
return AgentAction::CommitChirality;
}
}
if self.convergence_rate < -0.01 {
return AgentAction::Converging;
}
if self.convergence_rate > 0.01 {
return AgentAction::Diverging;
}
if error_norm < 0.2 {
return AgentAction::HoldSteady;
}
AgentAction::Continue
}
fn dominant_chamber(&self) -> Option<u8> {
let mut counts = [0u32; 6];
for slot in &self.history {
if let Some(s) = slot {
if (s.chamber as usize) < 6 {
counts[s.chamber as usize] += 1;
}
}
}
let max_count = *counts.iter().max()?;
if max_count == 0 {
return None;
}
Some(counts.iter().position(|&c| c == max_count)? as u8)
}
fn chamber_confidence_milli(&self, dominant: u8) -> u16 {
let mut dominant_count = 0u32;
let mut total = 0u32;
for slot in &self.history {
if let Some(s) = slot {
total += 1;
if s.chamber == dominant {
dominant_count += 1;
}
}
}
if total == 0 {
return 0;
}
((dominant_count as f64 / total as f64) * 1000.0) as u16
}
pub fn funnel_width(&self) -> f64 {
if self.history_count == 0 {
return 1.0;
}
self.error_mean
}
pub fn temperature(&self) -> f64 {
let mut chamber_counts = [0f64; 6];
let mut total = 0.0;
for slot in &self.history {
if let Some(s) = slot {
chamber_counts[s.chamber as usize] += 1.0;
total += 1.0;
}
}
if total == 0.0 {
return 1.0;
}
let entropy: f64 = chamber_counts
.iter()
.filter(|&&c| c > 0.0)
.map(|&c| {
let p = c / total;
-p * p.log2()
})
.sum();
entropy / 6f64.log2()
}
pub fn summary(&self) -> AgentSummary {
AgentSummary {
history_count: self.history_count,
error_mean: self.error_mean,
error_std: self.error_var.sqrt().max(0.0)
/ (self.history_count as f64).sqrt().max(1.0),
convergence_rate: self.convergence_rate,
precision_energy: self.precision_energy,
prediction_error: self.prediction_error,
temperature: self.temperature(),
phase: self.phase,
chirality: self.chirality,
decay_rate: self.decay_rate,
funnel_width: self.funnel_width(),
}
}
}
#[derive(Debug)]
pub struct AgentSummary {
pub history_count: usize,
pub error_mean: f64,
pub error_std: f64,
pub convergence_rate: f64,
pub precision_energy: f64,
pub prediction_error: f64,
pub temperature: f64,
pub phase: FunnelPhase,
pub chirality: ChiralityState,
pub decay_rate: f64,
pub funnel_width: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_creation() {
let agent = TemporalAgent::new();
assert_eq!(agent.history_count, 0);
assert_eq!(agent.phase, FunnelPhase::Approach);
assert_eq!(
agent.chirality,
ChiralityState::Exploring { chamber_hops: 0 }
);
}
#[test]
fn test_agent_observe_convergence() {
let mut agent = TemporalAgent::new();
let mut results = Vec::new();
for i in 0..20 {
let t = i as f64 / 20.0;
let r = COVERING_RADIUS * (1.0 - t * 0.9);
let angle: f64 = 0.5;
let x = r * angle.cos();
let y = r * angle.sin();
let update = agent.observe(x, y);
results.push(update);
}
let final_phase = results.last().unwrap().phase;
assert!(
final_phase == FunnelPhase::Narrowing || final_phase == FunnelPhase::SnapImminent,
"Expected convergence, got {:?}",
final_phase
);
}
#[test]
fn test_agent_prediction_improves() {
let mut agent = TemporalAgent::new();
let mut errors = Vec::new();
for i in 0..30 {
let r = COVERING_RADIUS * (1.0 - i as f64 / 40.0);
let x = r * 0.5;
let y = r * 0.866;
let update = agent.observe(x, y);
errors.push(update.prediction_error);
}
let early_avg: f64 = errors[..10].iter().sum::<f64>() / 10.0;
let late_avg: f64 = errors[20..].iter().sum::<f64>() / 10.0;
assert!(
late_avg < early_avg * 2.0,
"Prediction should not degrade: early={:.4} late={:.4}",
early_avg,
late_avg
);
}
#[test]
fn test_agent_anomaly_detection() {
let mut agent = TemporalAgent::new();
agent.anomaly_sigma = 1.5;
for _ in 0..20 {
agent.observe(0.01, 0.01);
}
for _ in 0..5 {
let update = agent.observe(0.01, 0.01);
assert!(!update.is_anomaly, "Should not be anomaly during steady state");
}
let update = agent.observe(3.0, 3.0);
assert!(update.is_anomaly || update.action == AgentAction::WidenFunnel || update.prediction_error > 0.5,
"Should detect anomaly on sudden jump: anomaly={}, action={:?}, pred_err={:.4}",
update.is_anomaly, update.action, update.prediction_error);
}
#[test]
fn test_agent_chirality_locking() {
let mut agent = TemporalAgent::new();
for _ in 0..40 {
agent.observe(0.1, 0.1);
}
match agent.chirality {
ChiralityState::Locked { .. }
| ChiralityState::Locking { .. }
| ChiralityState::Exploring { .. } => {} }
}
#[test]
fn test_agent_temperature() {
let mut agent = TemporalAgent::new();
agent.observe(0.1, 0.1);
let t1 = agent.temperature();
for _ in 0..20 {
agent.observe(0.1, 0.1);
}
let t2 = agent.temperature();
assert!(
t2 <= t1 + 0.1,
"Temperature should not increase with same-region observations"
);
}
#[test]
fn test_agent_summary() {
let mut agent = TemporalAgent::new();
for i in 0..10 {
let r = COVERING_RADIUS * (1.0 - i as f64 / 15.0);
agent.observe(r * 0.5, r * 0.866);
}
let summary = agent.summary();
assert_eq!(summary.history_count, 10);
assert!(summary.error_mean > 0.0);
assert!(summary.temperature >= 0.0 && summary.temperature <= 1.0);
}
#[test]
fn test_agent_satisfied() {
let mut agent = TemporalAgent::new();
let mut found_satisfied = false;
for _ in 0..20 {
let update = agent.observe(0.0, 0.0);
if update.snap.error < 0.001 {
if matches!(update.action, AgentAction::Satisfied | AgentAction::HoldSteady | AgentAction::Converging) {
found_satisfied = true;
break;
}
}
}
assert!(found_satisfied, "Should reach satisfied/converging at origin");
}
#[test]
fn test_agent_actions_cover() {
let mut agent = TemporalAgent::new();
let mut actions_seen = std::collections::HashSet::new();
for i in 0..30 {
let r = COVERING_RADIUS * (1.0 - i as f64 / 40.0);
let update = agent.observe(r * 0.5, r * 0.866);
actions_seen.insert(update.action);
}
let update = agent.observe(5.0, 5.0);
actions_seen.insert(update.action);
assert!(
actions_seen.len() >= 2,
"Should see multiple actions: {:?}",
actions_seen
);
}
}