1use crate::config::FieldConfig;
11use crate::observer::{FieldEvent, FieldObserver, MonitoredRegion, TriggerConfig};
12use crate::vector::FieldVector;
13use std::ops::Range;
14use std::sync::Arc;
15use ternary_signal::Signal;
16
17pub struct TemporalField {
29 frames: Vec<FieldVector>,
31
32 config: FieldConfig,
34
35 write_head: usize,
37
38 tick_count: u64,
40
41 observers: Vec<Arc<dyn FieldObserver>>,
43
44 triggers: TriggerConfig,
46
47 was_active: Vec<bool>,
49}
50
51impl TemporalField {
52 pub fn new(config: FieldConfig) -> Self {
59 let frames = (0..config.frame_count)
60 .map(|_| FieldVector::new(config.dims))
61 .collect();
62
63 Self {
64 frames,
65 config,
66 write_head: 0,
67 tick_count: 0,
68 observers: Vec::new(),
69 triggers: TriggerConfig::default(),
70 was_active: Vec::new(),
71 }
72 }
73
74 pub fn monitor_region(&mut self, region: MonitoredRegion) {
76 self.triggers.regions.push(region);
77 self.was_active.push(false);
78 }
79
80 pub fn set_convergence_threshold(&mut self, threshold: usize) {
82 self.triggers.convergence_threshold = threshold;
83 }
84
85 pub fn subscribe(&mut self, observer: Arc<dyn FieldObserver>) {
91 self.observers.push(observer);
92 }
93
94 pub fn clear_observers(&mut self) {
96 self.observers.clear();
97 }
98
99 fn fire(&self, event: FieldEvent) {
101 for observer in &self.observers {
102 observer.on_event(event.clone());
103 }
104 }
105
106 fn check_and_fire(&mut self) {
113 if self.triggers.regions.is_empty() {
114 return;
115 }
116
117 let mut active_regions = Vec::new();
118 let mut total_energy: u64 = 0;
119
120 for (i, region) in self.triggers.regions.iter().enumerate() {
121 let energy = self.frames[self.write_head].range_energy(region.range.clone());
122 let was = self.was_active.get(i).copied().unwrap_or(false);
123
124 let is_active = if was {
128 energy >= region.off_threshold
130 } else {
131 energy > region.on_threshold
133 };
134
135 if is_active && !was {
137 self.fire(FieldEvent::RegionActive {
138 region: region.range.clone(),
139 energy,
140 threshold: region.on_threshold,
141 });
142 }
143
144 if !is_active && was {
146 self.fire(FieldEvent::RegionQuiet {
147 region: region.range.clone(),
148 energy,
149 threshold: region.off_threshold,
150 });
151 }
152
153 if is_active {
155 active_regions.push(region.range.clone());
156 total_energy += energy * region.weight as u64 / 100;
158 }
159
160 if i < self.was_active.len() {
162 self.was_active[i] = is_active;
163 }
164 }
165
166 if active_regions.len() >= self.triggers.convergence_threshold {
168 self.fire(FieldEvent::Convergence {
169 active_regions,
170 total_energy,
171 });
172 }
173 }
174
175 pub fn tick(&mut self) {
181 self.tick_count += 1;
182 for frame in &mut self.frames {
183 frame.decay(self.config.retention);
184 }
185 self.check_and_fire();
186 }
187
188 pub fn tick_n(&mut self, n: usize) {
190 for _ in 0..n {
191 self.tick();
192 }
193 }
194
195 pub fn advance_write_head(&mut self) {
197 self.write_head = (self.write_head + 1) % self.config.frame_count;
198 }
199
200 pub fn write_region(&mut self, signals: &[Signal], range: Range<usize>) {
206 self.frames[self.write_head].add_to_range(signals, range);
207 self.check_and_fire();
208 }
209
210 pub fn set_region(&mut self, signals: &[Signal], range: Range<usize>) {
212 self.frames[self.write_head].set_range(signals, range);
213 self.check_and_fire();
214 }
215
216 pub fn write_full(&mut self, vector: &FieldVector) {
218 self.frames[self.write_head].add(vector);
219 self.check_and_fire();
220 }
221
222 pub fn clear_current(&mut self) {
224 self.frames[self.write_head] = FieldVector::new(self.config.dims);
225 }
226
227 pub fn read_current(&self) -> &FieldVector {
233 &self.frames[self.write_head]
234 }
235
236 pub fn read_region(&self, range: Range<usize>) -> Vec<Signal> {
238 self.frames[self.write_head].get_range(range)
239 }
240
241 pub fn region_energy(&self, range: Range<usize>) -> u64 {
243 self.frames[self.write_head].range_energy(range)
244 }
245
246 pub fn region_active(&self, range: Range<usize>, threshold: u64) -> bool {
248 self.region_energy(range) > threshold
249 }
250
251 pub fn read_window(&self, n: usize) -> Vec<&FieldVector> {
253 let n = n.min(self.config.frame_count);
254 let mut result = Vec::with_capacity(n);
255
256 for i in 0..n {
257 let idx = (self.write_head + self.config.frame_count - n + i)
258 % self.config.frame_count;
259 result.push(&self.frames[idx]);
260 }
261
262 result
263 }
264
265 pub fn region_peak(&self, range: Range<usize>, window: usize) -> Vec<Signal> {
268 let frames = self.read_window(window);
269 if frames.is_empty() {
270 return vec![Signal::ZERO; range.len()];
271 }
272
273 let mut best_frame_idx = 0;
274 let mut best_energy = 0u64;
275
276 for (i, frame) in frames.iter().enumerate() {
277 let energy = frame.range_energy(range.clone());
278 if energy > best_energy {
279 best_energy = energy;
280 best_frame_idx = i;
281 }
282 }
283
284 frames[best_frame_idx].get_range(range)
285 }
286
287 pub fn region_mean(&self, range: Range<usize>, window: usize) -> Vec<Signal> {
290 let frames = self.read_window(window);
291 if frames.is_empty() {
292 return vec![Signal::ZERO; range.len()];
293 }
294
295 let len = range.len();
296 let mut sums: Vec<i64> = vec![0; len];
297
298 for frame in &frames {
299 for (i, idx) in range.clone().enumerate() {
300 sums[i] += frame.get_current(idx) as i64;
301 }
302 }
303
304 let n = frames.len() as i64;
305 sums.iter()
306 .map(|&sum| Signal::from_current((sum / n) as i32))
307 .collect()
308 }
309
310 pub fn config(&self) -> &FieldConfig {
316 &self.config
317 }
318
319 pub fn triggers(&self) -> &TriggerConfig {
321 &self.triggers
322 }
323
324 pub fn regions(&self) -> &[MonitoredRegion] {
326 &self.triggers.regions
327 }
328
329 pub fn tick_count(&self) -> u64 {
331 self.tick_count
332 }
333
334 pub fn write_head(&self) -> usize {
336 self.write_head
337 }
338
339 pub fn dims(&self) -> usize {
341 self.config.dims
342 }
343
344 pub fn frame_count(&self) -> usize {
346 self.config.frame_count
347 }
348
349 pub fn max_magnitude(&self) -> u16 {
351 self.frames
352 .iter()
353 .map(|f| f.max_magnitude())
354 .max()
355 .unwrap_or(0)
356 }
357
358 pub fn total_activity(&self) -> usize {
360 self.frames.iter().map(|f| f.non_zero_count()).sum()
361 }
362
363 pub fn clear(&mut self) {
365 for frame in &mut self.frames {
366 *frame = FieldVector::new(self.config.dims);
367 }
368 self.write_head = 0;
369 self.tick_count = 0;
370 self.was_active.fill(false);
371 }
372
373 pub fn ticks_to_ms(&self, ticks: u64) -> u32 {
375 ((ticks as u64 * 1000) / self.config.tick_rate_hz as u64) as u32
376 }
377
378 pub fn ms_to_ticks(&self, ms: u32) -> u64 {
380 (ms as u64 * self.config.tick_rate_hz as u64) / 1000
381 }
382}
383
384impl Clone for TemporalField {
385 fn clone(&self) -> Self {
388 Self {
389 frames: self.frames.clone(),
390 config: self.config.clone(),
391 write_head: self.write_head,
392 tick_count: self.tick_count,
393 observers: Vec::new(), triggers: self.triggers.clone(),
395 was_active: self.was_active.clone(),
396 }
397 }
398}
399
400impl std::fmt::Debug for TemporalField {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 f.debug_struct("TemporalField")
403 .field("dims", &self.config.dims)
404 .field("frame_count", &self.config.frame_count)
405 .field("retention", &self.config.retention)
406 .field("write_head", &self.write_head)
407 .field("tick_count", &self.tick_count)
408 .field("observers", &self.observers.len())
409 .field("regions", &self.triggers.regions.len())
410 .finish()
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use std::sync::atomic::{AtomicUsize, Ordering};
418
419 #[test]
420 fn test_new_field() {
421 let config = FieldConfig::new(64, 10, 242); let field = TemporalField::new(config);
423
424 assert_eq!(field.dims(), 64);
425 assert_eq!(field.frame_count(), 10);
426 assert_eq!(field.tick_count(), 0);
427 assert_eq!(field.total_activity(), 0);
428 }
429
430 #[test]
431 fn test_write_and_read_region() {
432 let config = FieldConfig::new(128, 10, 242);
433 let mut field = TemporalField::new(config);
434
435 let signals = vec![Signal::positive(128); 32];
436 field.write_region(&signals, 0..32);
437
438 assert!(field.region_active(0..32, 1000));
440 assert!(!field.region_active(32..64, 1000));
441 }
442
443 #[test]
444 fn test_decay() {
445 let config = FieldConfig::new(64, 10, 128); let mut field = TemporalField::new(config);
447
448 let signals = vec![Signal::positive(200); 64];
449 field.write_region(&signals, 0..64);
450 let initial = field.region_energy(0..64);
451
452 field.tick();
453 let after_tick = field.region_energy(0..64);
454
455 assert!(after_tick < initial / 2);
457 }
458
459 #[test]
460 fn test_region_active_fires_event() {
461 let config = FieldConfig::new(64, 10, 242);
462 let mut field = TemporalField::new(config);
463
464 field.monitor_region(MonitoredRegion::new("test", 0..32, 100_000));
467
468 let count = Arc::new(AtomicUsize::new(0));
470 let count_clone = count.clone();
471 field.subscribe(Arc::new(crate::observer::FnObserver(move |event| {
472 if matches!(event, FieldEvent::RegionActive { .. }) {
473 count_clone.fetch_add(1, Ordering::SeqCst);
474 }
475 })));
476
477 let signals = vec![Signal::positive(128); 32];
479 field.write_region(&signals, 0..32);
480
481 assert_eq!(count.load(Ordering::SeqCst), 1);
482 }
483
484 #[test]
485 fn test_convergence_fires() {
486 let config = FieldConfig::new(128, 10, 242);
487 let mut field = TemporalField::new(config);
488
489 field.monitor_region(MonitoredRegion::new("a", 0..32, 50_000));
491 field.monitor_region(MonitoredRegion::new("b", 32..64, 50_000));
492 field.monitor_region(MonitoredRegion::new("c", 64..96, 50_000));
493 field.set_convergence_threshold(2);
494
495 let convergence_count = Arc::new(AtomicUsize::new(0));
496 let cc = convergence_count.clone();
497
498 field.subscribe(Arc::new(crate::observer::FnObserver(move |event| {
499 if matches!(event, FieldEvent::Convergence { .. }) {
500 cc.fetch_add(1, Ordering::SeqCst);
501 }
502 })));
503
504 let signals = vec![Signal::positive(128); 32];
506 field.write_region(&signals, 0..32);
507 field.write_region(&signals, 32..64);
508
509 assert!(convergence_count.load(Ordering::SeqCst) >= 1);
510 }
511
512 #[test]
513 fn test_ring_buffer_wrap() {
514 let config = FieldConfig::new(64, 3, 255); let mut field = TemporalField::new(config);
516
517 for i in 0..5 {
518 field.clear_current();
519 let signals = vec![Signal::positive(((i + 1) * 25) as u8); 64];
520 field.write_region(&signals, 0..64);
521 field.advance_write_head();
522 }
523
524 assert_eq!(field.write_head(), 2);
525 }
526
527 #[test]
528 fn test_window_chronological() {
529 let config = FieldConfig::new(64, 5, 255); let mut field = TemporalField::new(config);
531
532 for i in 0..3 {
533 field.clear_current();
534 let signals = vec![Signal::positive(((i + 1) * 50) as u8); 1];
535 field.write_region(&signals, 0..1);
536 field.advance_write_head();
537 }
538
539 let window = field.read_window(3);
540 assert_eq!(window.len(), 3);
541
542 assert_eq!(window[0].get(0).magnitude, 50);
543 assert_eq!(window[1].get(0).magnitude, 100);
544 assert_eq!(window[2].get(0).magnitude, 150);
545 }
546
547 #[test]
548 fn test_hysteresis_prevents_chattering() {
549 let config = FieldConfig::new(1, 10, 255); let mut field = TemporalField::new(config);
553
554 field.monitor_region(MonitoredRegion::with_hysteresis("test", 0..1, 10000, 2500));
557
558 let active_count = Arc::new(AtomicUsize::new(0));
559 let quiet_count = Arc::new(AtomicUsize::new(0));
560 let ac = active_count.clone();
561 let qc = quiet_count.clone();
562
563 field.subscribe(Arc::new(crate::observer::FnObserver(move |event| {
564 match event {
565 FieldEvent::RegionActive { .. } => {
566 ac.fetch_add(1, Ordering::SeqCst);
567 }
568 FieldEvent::RegionQuiet { .. } => {
569 qc.fetch_add(1, Ordering::SeqCst);
570 }
571 _ => {}
572 }
573 })));
574
575 field.set_region(&[Signal::positive(120)], 0..1);
577 assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should fire RegionActive");
578 assert_eq!(quiet_count.load(Ordering::SeqCst), 0, "Should not fire RegionQuiet");
579
580 field.set_region(&[Signal::positive(70)], 0..1);
583 assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should not fire again (hysteresis)");
584 assert_eq!(quiet_count.load(Ordering::SeqCst), 0, "Should stay active (hysteresis)");
585
586 field.set_region(&[Signal::positive(40)], 0..1);
588 assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should not fire RegionActive");
589 assert_eq!(quiet_count.load(Ordering::SeqCst), 1, "Should fire RegionQuiet");
590
591 field.set_region(&[Signal::positive(70)], 0..1);
594 assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should not become active (hysteresis)");
595 assert_eq!(quiet_count.load(Ordering::SeqCst), 1, "Should stay quiet");
596
597 field.set_region(&[Signal::positive(120)], 0..1);
599 assert_eq!(active_count.load(Ordering::SeqCst), 2, "Should fire RegionActive again");
600 }
601
602 #[test]
603 fn test_region_mean() {
604 let config = FieldConfig::new(4, 5, 255); let mut field = TemporalField::new(config);
606
607 field.set_region(&[Signal::positive(60)], 0..1);
609 field.advance_write_head();
610 field.set_region(&[Signal::positive(120)], 0..1);
611 field.advance_write_head();
612 field.set_region(&[Signal::positive(180)], 0..1);
613 field.advance_write_head();
614
615 let mean = field.region_mean(0..1, 3);
616 assert_eq!(mean[0].magnitude, 120);
618 }
619}