Skip to main content

temporal_field/
field.rs

1//! Temporal Field - Signal-based ring buffer with pub/sub
2//!
3//! The brain does not poll - one spark cascades.
4//!
5//! This is THE temporal field: ring buffer + decay + observer events.
6//! Writes trigger downstream processing automatically.
7//!
8//! ASTRO_004 compliant: No floats. Signals throughout.
9
10use 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
17/// The temporal field - ring buffer with decay and pub/sub events.
18///
19/// Every write and tick checks thresholds and fires events to observers.
20/// The brain does not poll - one spark cascades.
21///
22/// See crate-level documentation for full architecture explanation and examples.
23///
24/// # Clone behavior
25///
26/// Cloning a TemporalField copies the field state (frames, config, triggers)
27/// but NOT the observers. The clone starts with no subscribers.
28pub struct TemporalField {
29    /// Ring buffer of frames.
30    frames: Vec<FieldVector>,
31
32    /// Configuration.
33    config: FieldConfig,
34
35    /// Current write position.
36    write_head: usize,
37
38    /// Total ticks elapsed.
39    tick_count: u64,
40
41    /// Registered observers for pub/sub.
42    observers: Vec<Arc<dyn FieldObserver>>,
43
44    /// What triggers notifications.
45    triggers: TriggerConfig,
46
47    /// Previous active state per region (for edge detection).
48    was_active: Vec<bool>,
49}
50
51impl TemporalField {
52    /// Create a new temporal field.
53    ///
54    /// After creation, configure the field:
55    /// 1. Add monitored regions with `monitor_region()`
56    /// 2. Subscribe observers with `subscribe()`
57    /// 3. Writers write with `write_region()`, readers receive events
58    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    /// Add a monitored region after construction.
75    pub fn monitor_region(&mut self, region: MonitoredRegion) {
76        self.triggers.regions.push(region);
77        self.was_active.push(false);
78    }
79
80    /// Set convergence threshold.
81    pub fn set_convergence_threshold(&mut self, threshold: usize) {
82        self.triggers.convergence_threshold = threshold;
83    }
84
85    // =========================================================================
86    // PUB/SUB - The brain does not poll
87    // =========================================================================
88
89    /// Subscribe an observer to receive field events.
90    pub fn subscribe(&mut self, observer: Arc<dyn FieldObserver>) {
91        self.observers.push(observer);
92    }
93
94    /// Remove all observers.
95    pub fn clear_observers(&mut self) {
96        self.observers.clear();
97    }
98
99    /// Fire an event to all observers.
100    fn fire(&self, event: FieldEvent) {
101        for observer in &self.observers {
102            observer.on_event(event.clone());
103        }
104    }
105
106    /// Check regions and fire events for state changes.
107    ///
108    /// Uses hysteresis to prevent chattering:
109    /// - To become active: energy must exceed on_threshold
110    /// - To become quiet: energy must drop below off_threshold
111    /// - Between thresholds: maintain previous state
112    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            // Hysteresis logic:
125            // - If already active, stay active until energy drops below off_threshold
126            // - If not active, only become active if energy exceeds on_threshold
127            let is_active = if was {
128                // Already active - use lower threshold to leave
129                energy >= region.off_threshold
130            } else {
131                // Not active - use higher threshold to enter
132                energy > region.on_threshold
133            };
134
135            // Edge detection: became active (crossed on_threshold from below)
136            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            // Edge detection: became quiet (dropped below off_threshold)
145            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            // Track for convergence
154            if is_active {
155                active_regions.push(region.range.clone());
156                // Weighted energy: energy × weight / 100 (since weight 100 = 1.0×)
157                total_energy += energy * region.weight as u64 / 100;
158            }
159
160            // Update state
161            if i < self.was_active.len() {
162                self.was_active[i] = is_active;
163            }
164        }
165
166        // Check for convergence (multiple regions active)
167        if active_regions.len() >= self.triggers.convergence_threshold {
168            self.fire(FieldEvent::Convergence {
169                active_regions,
170                total_energy,
171            });
172        }
173    }
174
175    // =========================================================================
176    // TIME ADVANCEMENT
177    // =========================================================================
178
179    /// Advance time by one tick - decay all frames, may fire RegionQuiet events.
180    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    /// Advance multiple ticks.
189    pub fn tick_n(&mut self, n: usize) {
190        for _ in 0..n {
191            self.tick();
192        }
193    }
194
195    /// Advance write head to next frame.
196    pub fn advance_write_head(&mut self) {
197        self.write_head = (self.write_head + 1) % self.config.frame_count;
198    }
199
200    // =========================================================================
201    // WRITING - triggers event checks after mutation
202    // =========================================================================
203
204    /// Write Signals to a region of the current frame (additive) - may fire events.
205    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    /// Set Signals in a region of the current frame (replace) - may fire events.
211    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    /// Add a full vector to current frame - may fire events.
217    pub fn write_full(&mut self, vector: &FieldVector) {
218        self.frames[self.write_head].add(vector);
219        self.check_and_fire();
220    }
221
222    /// Clear the current frame.
223    pub fn clear_current(&mut self) {
224        self.frames[self.write_head] = FieldVector::new(self.config.dims);
225    }
226
227    // =========================================================================
228    // READING
229    // =========================================================================
230
231    /// Read the current frame.
232    pub fn read_current(&self) -> &FieldVector {
233        &self.frames[self.write_head]
234    }
235
236    /// Read a specific region from current frame.
237    pub fn read_region(&self, range: Range<usize>) -> Vec<Signal> {
238        self.frames[self.write_head].get_range(range)
239    }
240
241    /// Get energy in a region of current frame.
242    pub fn region_energy(&self, range: Range<usize>) -> u64 {
243        self.frames[self.write_head].range_energy(range)
244    }
245
246    /// Check if region is active (energy above threshold).
247    pub fn region_active(&self, range: Range<usize>, threshold: u64) -> bool {
248        self.region_energy(range) > threshold
249    }
250
251    /// Read the last N frames in chronological order (oldest first).
252    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    /// Get peak values in a region over the last N frames.
266    /// Returns the frame with highest energy.
267    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    /// Get mean values in a region over the last N frames.
288    /// Returns averaged Signal values using the full p×m×k range.
289    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    // =========================================================================
311    // METRICS
312    // =========================================================================
313
314    /// Get configuration.
315    pub fn config(&self) -> &FieldConfig {
316        &self.config
317    }
318
319    /// Get trigger configuration.
320    pub fn triggers(&self) -> &TriggerConfig {
321        &self.triggers
322    }
323
324    /// Get monitored regions.
325    pub fn regions(&self) -> &[MonitoredRegion] {
326        &self.triggers.regions
327    }
328
329    /// Get current tick count.
330    pub fn tick_count(&self) -> u64 {
331        self.tick_count
332    }
333
334    /// Get write head position.
335    pub fn write_head(&self) -> usize {
336        self.write_head
337    }
338
339    /// Get total dimensions.
340    pub fn dims(&self) -> usize {
341        self.config.dims
342    }
343
344    /// Get frame count.
345    pub fn frame_count(&self) -> usize {
346        self.config.frame_count
347    }
348
349    /// Get maximum effective magnitude in field.
350    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    /// Get total non-zero count.
359    pub fn total_activity(&self) -> usize {
360        self.frames.iter().map(|f| f.non_zero_count()).sum()
361    }
362
363    /// Clear entire field.
364    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    /// Convert tick difference to milliseconds.
374    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    /// Convert milliseconds to ticks.
379    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    /// Clone the field state but NOT the observers.
386    /// The clone starts with no subscribers.
387    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(), // Observers are not cloned
394            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); // 242 ≈ 0.95
422        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        // Energy = 32 * 128^2 = 524288
439        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); // 50% retention
446        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        // After 50% decay, energy should be ~25% (magnitude halved, energy = mag^2)
456        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        // Configure: add monitored region
465        // Threshold: 32 * 100^2 = 320000
466        field.monitor_region(MonitoredRegion::new("test", 0..32, 100_000));
467
468        // Subscribe reader
469        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        // Writer writes - fires event to reader (magnitude 128, energy = 32 * 16384 = 524288)
478        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        // Configure: add monitored regions (threshold = 50000)
490        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        // Write to two regions - should trigger convergence
505        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); // No decay
515        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); // No decay
530        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        // Use single dimension for simpler energy calculation
550        // Energy = magnitude^2 for single dimension
551        let config = FieldConfig::new(1, 10, 255); // No decay for clarity
552        let mut field = TemporalField::new(config);
553
554        // Region with explicit hysteresis:
555        // on_threshold = 10000 (mag ~100), off_threshold = 2500 (mag ~50)
556        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        // Write magnitude 120 → energy = 14400 (above on_threshold 10000) → should fire RegionActive
576        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        // Write magnitude 70 → energy = 4900 (between thresholds: below on=10000 but above off=2500)
581        // Should NOT fire any event due to hysteresis
582        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        // Write magnitude 40 → energy = 1600 (below off_threshold 2500) → should fire RegionQuiet
587        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        // Write magnitude 70 → energy = 4900 (above off=2500 but below on=10000)
592        // Should NOT fire any event (need to exceed on_threshold to become active again)
593        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        // Write magnitude 120 → energy = 14400 (above on_threshold 10000) → should fire RegionActive again
598        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); // No decay
605        let mut field = TemporalField::new(config);
606
607        // Write 3 frames with different values
608        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        // (60 + 120 + 180) / 3 = 120
617        assert_eq!(mean[0].magnitude, 120);
618    }
619}