1use crate::eisenstein::{EisensteinConstraint, SnapResult, COVERING_RADIUS};
32
33const HISTORY_SIZE: usize = 64;
35
36pub struct TemporalAgent {
38 constraint: EisensteinConstraint,
40
41 history: [Option<SnapResult>; HISTORY_SIZE],
43 history_pos: usize,
45 history_count: usize,
47
48 pub decay_rate: f64,
55
56 pub prediction_horizon: usize,
60
61 pub anomaly_sigma: f64,
65
66 pub learning_rate: f64,
70
71 pub chirality_lock_threshold: u16,
75
76 pub merge_trust: f64,
80
81 error_mean: f64,
85 error_var: f64,
87 convergence_rate: f64,
89 precision_energy: f64,
91 predicted_error: f64,
93 prediction_error: f64,
95 chirality: ChiralityState,
97 phase: FunnelPhase,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum ChiralityState {
104 Exploring { chamber_hops: u32 },
106 Locking { dominant: u8, confidence_milli: u16 },
108 Locked { chamber: u8 },
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum FunnelPhase {
115 Approach,
117 Narrowing,
119 SnapImminent,
121 Crystallized,
123 Anomaly,
125}
126
127#[derive(Debug)]
129pub struct TemporalUpdate {
130 pub snap: SnapResult,
132 pub phase: FunnelPhase,
134 pub chirality: ChiralityState,
136 pub predicted_error: f64,
138 pub prediction_error: f64,
140 pub convergence_rate: f64,
142 pub precision_energy: f64,
144 pub is_anomaly: bool,
146 pub action: AgentAction,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum AgentAction {
153 Continue,
155 Converging,
157 HoldSteady,
159 WidenFunnel,
161 CommitChirality,
163 Diverging,
165 Satisfied,
167}
168
169impl Default for TemporalAgent {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl TemporalAgent {
176 pub fn new() -> Self {
177 TemporalAgent {
178 constraint: EisensteinConstraint::new(),
179 history: [None; HISTORY_SIZE],
180 history_pos: 0,
181 history_count: 0,
182
183 decay_rate: 1.0,
184 prediction_horizon: 4,
185 anomaly_sigma: 2.0,
186 learning_rate: 0.1,
187 chirality_lock_threshold: 500,
188 merge_trust: 0.5,
189
190 error_mean: 0.0,
191 error_var: 0.0,
192 convergence_rate: 0.0,
193 precision_energy: 0.0,
194 predicted_error: COVERING_RADIUS,
195 prediction_error: 0.0,
196 chirality: ChiralityState::Exploring { chamber_hops: 0 },
197 phase: FunnelPhase::Approach,
198 }
199 }
200
201 pub fn observe(&mut self, x: f64, y: f64) -> TemporalUpdate {
210 let snap = self.constraint.snap(x, y);
212 let error_norm = snap.error / COVERING_RADIUS;
213
214 let _proportional = error_norm;
216 self.precision_energy += if snap.error > 0.0 { 1.0 / snap.error } else { 1000.0 };
217 self.update_convergence_rate(error_norm);
218
219 self.prediction_error = (error_norm - self.predicted_error).abs();
221
222 self.update_statistics(error_norm);
224
225 self.predicted_error = self.predict_next(error_norm);
227
228 self.update_chirality(snap.chamber);
230
231 self.update_phase(error_norm);
233
234 self.history[self.history_pos] = Some(snap.clone());
236 self.history_pos = (self.history_pos + 1) % HISTORY_SIZE;
237 self.history_count += 1;
238
239 let is_anomaly = self.prediction_error > self.anomaly_sigma * self.error_var.sqrt().max(0.01);
241 let action = self.determine_action(error_norm, is_anomaly);
242
243 if is_anomaly && self.decay_rate > 0.1 {
245 self.decay_rate *= 0.9;
246 } else if error_norm < 0.2 && self.decay_rate < 5.0 {
247 self.decay_rate *= 1.05;
248 }
249
250 TemporalUpdate {
251 snap,
252 phase: self.phase,
253 chirality: self.chirality,
254 predicted_error: self.predicted_error,
255 prediction_error: self.prediction_error,
256 convergence_rate: self.convergence_rate,
257 precision_energy: self.precision_energy,
258 is_anomaly,
259 action,
260 }
261 }
262
263 pub fn deadband(&self, t: f64) -> f64 {
265 COVERING_RADIUS * (1.0 - t).powf(1.0 / self.decay_rate).max(0.0)
266 }
267
268 fn predict_next(&self, current: f64) -> f64 {
270 if self.history_count < 2 {
271 return current;
272 }
273 let predicted = current + self.convergence_rate * self.prediction_horizon as f64;
274 predicted.max(0.0).min(1.0)
275 }
276
277 fn update_convergence_rate(&mut self, current: f64) {
279 if self.history_count < 2 {
280 return;
281 }
282 let prev_pos = if self.history_pos == 0 { HISTORY_SIZE - 1 } else { self.history_pos - 1 };
283 if let Some(prev) = &self.history[prev_pos] {
284 let prev_norm = prev.error / COVERING_RADIUS;
285 let rate = current - prev_norm;
286 self.convergence_rate = self.learning_rate * rate + (1.0 - self.learning_rate) * self.convergence_rate;
287 }
288 }
289
290 fn update_statistics(&mut self, value: f64) {
292 let n = self.history_count as f64 + 1.0;
293 let delta = value - self.error_mean;
294 self.error_mean += delta / n;
295 let delta2 = value - self.error_mean;
296 self.error_var += delta * delta2;
297 }
298
299 fn update_chirality(&mut self, chamber: u8) {
301 match self.chirality {
302 ChiralityState::Exploring { ref mut chamber_hops } => {
303 *chamber_hops += 1;
304 if *chamber_hops > 10 {
305 if let Some(d) = self.dominant_chamber() {
306 let conf = self.chamber_confidence_milli(d);
307 if conf > self.chirality_lock_threshold {
308 self.chirality = ChiralityState::Locking {
309 dominant: d,
310 confidence_milli: conf,
311 };
312 }
313 }
314 }
315 }
316 ChiralityState::Locking { dominant, ref mut confidence_milli } => {
317 if chamber == dominant {
318 *confidence_milli = confidence_milli.saturating_add(50);
319 if *confidence_milli > 900 {
320 self.chirality = ChiralityState::Locked { chamber: dominant };
321 }
322 } else {
323 *confidence_milli = confidence_milli.saturating_sub(100);
324 if *confidence_milli < 300 {
325 self.chirality = ChiralityState::Exploring { chamber_hops: 0 };
326 }
327 }
328 }
329 ChiralityState::Locked { .. } => {
330 }
332 }
333 }
334
335 fn update_phase(&mut self, error_norm: f64) {
337 self.phase = if error_norm > 0.9 {
338 FunnelPhase::Approach
339 } else if error_norm > 0.5 {
340 FunnelPhase::Narrowing
341 } else if error_norm > 0.15 {
342 FunnelPhase::SnapImminent
343 } else if error_norm < 0.05 {
344 FunnelPhase::Crystallized
345 } else if self.phase == FunnelPhase::Anomaly {
346 FunnelPhase::Anomaly
347 } else {
348 FunnelPhase::Narrowing
349 };
350 }
351
352 fn determine_action(&self, error_norm: f64, is_anomaly: bool) -> AgentAction {
354 if is_anomaly {
355 return AgentAction::WidenFunnel;
356 }
357 if error_norm < 0.05 {
358 return AgentAction::Satisfied;
359 }
360 if matches!(self.chirality, ChiralityState::Locked { .. }) {
361 if !matches!(self.phase, FunnelPhase::Crystallized) {
362 return AgentAction::CommitChirality;
363 }
364 }
365 if self.convergence_rate < -0.01 {
366 return AgentAction::Converging;
367 }
368 if self.convergence_rate > 0.01 {
369 return AgentAction::Diverging;
370 }
371 if error_norm < 0.2 {
372 return AgentAction::HoldSteady;
373 }
374 AgentAction::Continue
375 }
376
377 fn dominant_chamber(&self) -> Option<u8> {
379 let mut counts = [0u32; 6];
380 for slot in &self.history {
381 if let Some(s) = slot {
382 if (s.chamber as usize) < 6 {
383 counts[s.chamber as usize] += 1;
384 }
385 }
386 }
387 let max_count = *counts.iter().max()?;
388 if max_count == 0 {
389 return None;
390 }
391 Some(counts.iter().position(|&c| c == max_count)? as u8)
392 }
393
394 fn chamber_confidence_milli(&self, dominant: u8) -> u16 {
396 let mut dominant_count = 0u32;
397 let mut total = 0u32;
398 for slot in &self.history {
399 if let Some(s) = slot {
400 total += 1;
401 if s.chamber == dominant {
402 dominant_count += 1;
403 }
404 }
405 }
406 if total == 0 {
407 return 0;
408 }
409 ((dominant_count as f64 / total as f64) * 1000.0) as u16
410 }
411
412 pub fn funnel_width(&self) -> f64 {
414 if self.history_count == 0 {
415 return 1.0;
416 }
417 self.error_mean
418 }
419
420 pub fn temperature(&self) -> f64 {
423 let mut chamber_counts = [0f64; 6];
424 let mut total = 0.0;
425 for slot in &self.history {
426 if let Some(s) = slot {
427 chamber_counts[s.chamber as usize] += 1.0;
428 total += 1.0;
429 }
430 }
431 if total == 0.0 {
432 return 1.0;
433 }
434 let entropy: f64 = chamber_counts
435 .iter()
436 .filter(|&&c| c > 0.0)
437 .map(|&c| {
438 let p = c / total;
439 -p * p.log2()
440 })
441 .sum();
442 entropy / 6f64.log2()
444 }
445
446 pub fn summary(&self) -> AgentSummary {
448 AgentSummary {
449 history_count: self.history_count,
450 error_mean: self.error_mean,
451 error_std: self.error_var.sqrt().max(0.0)
452 / (self.history_count as f64).sqrt().max(1.0),
453 convergence_rate: self.convergence_rate,
454 precision_energy: self.precision_energy,
455 prediction_error: self.prediction_error,
456 temperature: self.temperature(),
457 phase: self.phase,
458 chirality: self.chirality,
459 decay_rate: self.decay_rate,
460 funnel_width: self.funnel_width(),
461 }
462 }
463}
464
465#[derive(Debug)]
467pub struct AgentSummary {
468 pub history_count: usize,
469 pub error_mean: f64,
470 pub error_std: f64,
471 pub convergence_rate: f64,
472 pub precision_energy: f64,
473 pub prediction_error: f64,
474 pub temperature: f64,
475 pub phase: FunnelPhase,
476 pub chirality: ChiralityState,
477 pub decay_rate: f64,
478 pub funnel_width: f64,
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_agent_creation() {
487 let agent = TemporalAgent::new();
488 assert_eq!(agent.history_count, 0);
489 assert_eq!(agent.phase, FunnelPhase::Approach);
490 assert_eq!(
491 agent.chirality,
492 ChiralityState::Exploring { chamber_hops: 0 }
493 );
494 }
495
496 #[test]
497 fn test_agent_observe_convergence() {
498 let mut agent = TemporalAgent::new();
499 let mut results = Vec::new();
500 for i in 0..20 {
501 let t = i as f64 / 20.0;
502 let r = COVERING_RADIUS * (1.0 - t * 0.9);
503 let angle: f64 = 0.5;
504 let x = r * angle.cos();
505 let y = r * angle.sin();
506 let update = agent.observe(x, y);
507 results.push(update);
508 }
509 let final_phase = results.last().unwrap().phase;
510 assert!(
511 final_phase == FunnelPhase::Narrowing || final_phase == FunnelPhase::SnapImminent,
512 "Expected convergence, got {:?}",
513 final_phase
514 );
515 }
516
517 #[test]
518 fn test_agent_prediction_improves() {
519 let mut agent = TemporalAgent::new();
520 let mut errors = Vec::new();
521 for i in 0..30 {
522 let r = COVERING_RADIUS * (1.0 - i as f64 / 40.0);
523 let x = r * 0.5;
524 let y = r * 0.866;
525 let update = agent.observe(x, y);
526 errors.push(update.prediction_error);
527 }
528 let early_avg: f64 = errors[..10].iter().sum::<f64>() / 10.0;
529 let late_avg: f64 = errors[20..].iter().sum::<f64>() / 10.0;
530 assert!(
531 late_avg < early_avg * 2.0,
532 "Prediction should not degrade: early={:.4} late={:.4}",
533 early_avg,
534 late_avg
535 );
536 }
537
538 #[test]
539 fn test_agent_anomaly_detection() {
540 let mut agent = TemporalAgent::new();
541 agent.anomaly_sigma = 1.5;
542 for _ in 0..20 {
544 agent.observe(0.01, 0.01);
545 }
546 for _ in 0..5 {
548 let update = agent.observe(0.01, 0.01);
549 assert!(!update.is_anomaly, "Should not be anomaly during steady state");
550 }
551 let update = agent.observe(3.0, 3.0);
553 assert!(update.is_anomaly || update.action == AgentAction::WidenFunnel || update.prediction_error > 0.5,
556 "Should detect anomaly on sudden jump: anomaly={}, action={:?}, pred_err={:.4}",
557 update.is_anomaly, update.action, update.prediction_error);
558 }
559
560 #[test]
561 fn test_agent_chirality_locking() {
562 let mut agent = TemporalAgent::new();
563 for _ in 0..40 {
564 agent.observe(0.1, 0.1);
565 }
566 match agent.chirality {
568 ChiralityState::Locked { .. }
569 | ChiralityState::Locking { .. }
570 | ChiralityState::Exploring { .. } => {} }
572 }
573
574 #[test]
575 fn test_agent_temperature() {
576 let mut agent = TemporalAgent::new();
577 agent.observe(0.1, 0.1);
578 let t1 = agent.temperature();
579 for _ in 0..20 {
580 agent.observe(0.1, 0.1);
581 }
582 let t2 = agent.temperature();
583 assert!(
584 t2 <= t1 + 0.1,
585 "Temperature should not increase with same-region observations"
586 );
587 }
588
589 #[test]
590 fn test_agent_summary() {
591 let mut agent = TemporalAgent::new();
592 for i in 0..10 {
593 let r = COVERING_RADIUS * (1.0 - i as f64 / 15.0);
594 agent.observe(r * 0.5, r * 0.866);
595 }
596 let summary = agent.summary();
597 assert_eq!(summary.history_count, 10);
598 assert!(summary.error_mean > 0.0);
599 assert!(summary.temperature >= 0.0 && summary.temperature <= 1.0);
600 }
601
602 #[test]
603 fn test_agent_satisfied() {
604 let mut agent = TemporalAgent::new();
605 let mut found_satisfied = false;
607 for _ in 0..20 {
608 let update = agent.observe(0.0, 0.0);
609 if update.snap.error < 0.001 {
610 if matches!(update.action, AgentAction::Satisfied | AgentAction::HoldSteady | AgentAction::Converging) {
611 found_satisfied = true;
612 break;
613 }
614 }
615 }
616 assert!(found_satisfied, "Should reach satisfied/converging at origin");
617 }
618
619 #[test]
620 fn test_agent_actions_cover() {
621 let mut agent = TemporalAgent::new();
622 let mut actions_seen = std::collections::HashSet::new();
623
624 for i in 0..30 {
625 let r = COVERING_RADIUS * (1.0 - i as f64 / 40.0);
626 let update = agent.observe(r * 0.5, r * 0.866);
627 actions_seen.insert(update.action);
628 }
629
630 let update = agent.observe(5.0, 5.0);
631 actions_seen.insert(update.action);
632
633 assert!(
634 actions_seen.len() >= 2,
635 "Should see multiple actions: {:?}",
636 actions_seen
637 );
638 }
639}