temporal_field/
field.rs

1//! Temporal Field - Floating-point ternary 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
8use crate::config::FieldConfig;
9use crate::observer::{FieldEvent, FieldObserver, MonitoredRegion, TriggerConfig};
10use crate::vector::FieldVector;
11use std::ops::Range;
12use std::sync::Arc;
13
14/// The temporal field - ring buffer with decay and pub/sub events.
15///
16/// Every write and tick checks thresholds and fires events to observers.
17/// The brain does not poll - one spark cascades.
18///
19/// See crate-level documentation for full architecture explanation and examples.
20///
21/// # Clone behavior
22///
23/// Cloning a TemporalField copies the field state (frames, config, triggers)
24/// but NOT the observers. The clone starts with no subscribers.
25pub struct TemporalField {
26    /// Ring buffer of frames.
27    frames: Vec<FieldVector>,
28
29    /// Configuration.
30    config: FieldConfig,
31
32    /// Current write position.
33    write_head: usize,
34
35    /// Total ticks elapsed.
36    tick_count: u64,
37
38    /// Registered observers for pub/sub.
39    observers: Vec<Arc<dyn FieldObserver>>,
40
41    /// What triggers notifications.
42    triggers: TriggerConfig,
43
44    /// Previous active state per region (for edge detection).
45    was_active: Vec<bool>,
46}
47
48impl TemporalField {
49    /// Create a new temporal field.
50    ///
51    /// After creation, configure the field:
52    /// 1. Add monitored regions with `monitor_region()`
53    /// 2. Subscribe observers with `subscribe()`
54    /// 3. Writers write with `write_region()`, readers receive events
55    pub fn new(config: FieldConfig) -> Self {
56        let frames = (0..config.frame_count)
57            .map(|_| FieldVector::new(config.dims))
58            .collect();
59
60        Self {
61            frames,
62            config,
63            write_head: 0,
64            tick_count: 0,
65            observers: Vec::new(),
66            triggers: TriggerConfig::default(),
67            was_active: Vec::new(),
68        }
69    }
70
71    /// Add a monitored region after construction.
72    pub fn monitor_region(&mut self, region: MonitoredRegion) {
73        self.triggers.regions.push(region);
74        self.was_active.push(false);
75    }
76
77    /// Set convergence threshold.
78    pub fn set_convergence_threshold(&mut self, threshold: usize) {
79        self.triggers.convergence_threshold = threshold;
80    }
81
82    // =========================================================================
83    // PUB/SUB - The brain does not poll
84    // =========================================================================
85
86    /// Subscribe an observer to receive field events.
87    pub fn subscribe(&mut self, observer: Arc<dyn FieldObserver>) {
88        self.observers.push(observer);
89    }
90
91    /// Remove all observers.
92    pub fn clear_observers(&mut self) {
93        self.observers.clear();
94    }
95
96    /// Fire an event to all observers.
97    fn fire(&self, event: FieldEvent) {
98        for observer in &self.observers {
99            observer.on_event(event.clone());
100        }
101    }
102
103    /// Check regions and fire events for state changes.
104    ///
105    /// Uses hysteresis to prevent chattering:
106    /// - To become active: energy must exceed on_threshold
107    /// - To become quiet: energy must drop below off_threshold
108    /// - Between thresholds: maintain previous state
109    fn check_and_fire(&mut self) {
110        if self.triggers.regions.is_empty() {
111            return;
112        }
113
114        let mut active_regions = Vec::new();
115        let mut total_energy = 0.0;
116
117        for (i, region) in self.triggers.regions.iter().enumerate() {
118            let energy = self.frames[self.write_head].range_energy(region.range.clone());
119            let was = self.was_active.get(i).copied().unwrap_or(false);
120
121            // Hysteresis logic:
122            // - If already active, stay active until energy drops below off_threshold
123            // - If not active, only become active if energy exceeds on_threshold
124            let is_active = if was {
125                // Already active - use lower threshold to leave
126                energy >= region.off_threshold
127            } else {
128                // Not active - use higher threshold to enter
129                energy > region.on_threshold
130            };
131
132            // Edge detection: became active (crossed on_threshold from below)
133            if is_active && !was {
134                self.fire(FieldEvent::RegionActive {
135                    region: region.range.clone(),
136                    energy,
137                    threshold: region.on_threshold,
138                });
139            }
140
141            // Edge detection: became quiet (dropped below off_threshold)
142            if !is_active && was {
143                self.fire(FieldEvent::RegionQuiet {
144                    region: region.range.clone(),
145                    energy,
146                    threshold: region.off_threshold,
147                });
148            }
149
150            // Track for convergence
151            if is_active {
152                active_regions.push(region.range.clone());
153                total_energy += energy * region.weight;
154            }
155
156            // Update state
157            if i < self.was_active.len() {
158                self.was_active[i] = is_active;
159            }
160        }
161
162        // Check for convergence (multiple regions active)
163        if active_regions.len() >= self.triggers.convergence_threshold {
164            self.fire(FieldEvent::Convergence {
165                active_regions,
166                total_energy,
167            });
168        }
169    }
170
171    // =========================================================================
172    // TIME ADVANCEMENT
173    // =========================================================================
174
175    /// Advance time by one tick - decay all frames, may fire RegionQuiet events.
176    pub fn tick(&mut self) {
177        self.tick_count += 1;
178        for frame in &mut self.frames {
179            frame.decay(self.config.retention);
180        }
181        self.check_and_fire();
182    }
183
184    /// Advance multiple ticks.
185    pub fn tick_n(&mut self, n: usize) {
186        for _ in 0..n {
187            self.tick();
188        }
189    }
190
191    /// Advance write head to next frame.
192    pub fn advance_write_head(&mut self) {
193        self.write_head = (self.write_head + 1) % self.config.frame_count;
194    }
195
196    // =========================================================================
197    // WRITING - triggers event checks after mutation
198    // =========================================================================
199
200    /// Write values to a region of the current frame (additive) - may fire events.
201    pub fn write_region(&mut self, values: &[f32], range: Range<usize>) {
202        self.frames[self.write_head].add_to_range(values, range);
203        self.check_and_fire();
204    }
205
206    /// Set values in a region of the current frame (replace) - may fire events.
207    pub fn set_region(&mut self, values: &[f32], range: Range<usize>) {
208        self.frames[self.write_head].set_range(values, range);
209        self.check_and_fire();
210    }
211
212    /// Add a full vector to current frame - may fire events.
213    pub fn write_full(&mut self, vector: &FieldVector) {
214        self.frames[self.write_head].add(vector);
215        self.check_and_fire();
216    }
217
218    /// Clear the current frame.
219    pub fn clear_current(&mut self) {
220        self.frames[self.write_head] = FieldVector::new(self.config.dims);
221    }
222
223    // =========================================================================
224    // READING
225    // =========================================================================
226
227    /// Read the current frame.
228    pub fn read_current(&self) -> &FieldVector {
229        &self.frames[self.write_head]
230    }
231
232    /// Read a specific region from current frame.
233    pub fn read_region(&self, range: Range<usize>) -> Vec<f32> {
234        self.frames[self.write_head].get_range(range)
235    }
236
237    /// Get energy in a region of current frame.
238    pub fn region_energy(&self, range: Range<usize>) -> f32 {
239        self.frames[self.write_head].range_energy(range)
240    }
241
242    /// Check if region is active (energy above threshold).
243    pub fn region_active(&self, range: Range<usize>, threshold: f32) -> bool {
244        self.region_energy(range) > threshold
245    }
246
247    /// Read the last N frames in chronological order (oldest first).
248    pub fn read_window(&self, n: usize) -> Vec<&FieldVector> {
249        let n = n.min(self.config.frame_count);
250        let mut result = Vec::with_capacity(n);
251
252        for i in 0..n {
253            let idx = (self.write_head + self.config.frame_count - n + i)
254                % self.config.frame_count;
255            result.push(&self.frames[idx]);
256        }
257
258        result
259    }
260
261    /// Get peak values in a region over the last N frames.
262    pub fn region_peak(&self, range: Range<usize>, window: usize) -> Vec<f32> {
263        let frames = self.read_window(window);
264        if frames.is_empty() {
265            return vec![0.0; range.len()];
266        }
267
268        let mut best_frame_idx = 0;
269        let mut best_energy = 0.0f32;
270
271        for (i, frame) in frames.iter().enumerate() {
272            let energy = frame.range_energy(range.clone());
273            if energy > best_energy {
274                best_energy = energy;
275                best_frame_idx = i;
276            }
277        }
278
279        frames[best_frame_idx].get_range(range)
280    }
281
282    /// Get mean values in a region over the last N frames.
283    pub fn region_mean(&self, range: Range<usize>, window: usize) -> Vec<f32> {
284        let frames = self.read_window(window);
285        if frames.is_empty() {
286            return vec![0.0; range.len()];
287        }
288
289        let len = range.len();
290        let mut avg = vec![0.0; len];
291
292        for frame in &frames {
293            for (i, idx) in range.clone().enumerate() {
294                avg[i] += frame.get(idx);
295            }
296        }
297
298        let n = frames.len() as f32;
299        for v in &mut avg {
300            *v /= n;
301        }
302
303        avg
304    }
305
306    // =========================================================================
307    // METRICS
308    // =========================================================================
309
310    /// Get configuration.
311    pub fn config(&self) -> &FieldConfig {
312        &self.config
313    }
314
315    /// Get trigger configuration.
316    pub fn triggers(&self) -> &TriggerConfig {
317        &self.triggers
318    }
319
320    /// Get monitored regions.
321    pub fn regions(&self) -> &[MonitoredRegion] {
322        &self.triggers.regions
323    }
324
325    /// Get current tick count.
326    pub fn tick_count(&self) -> u64 {
327        self.tick_count
328    }
329
330    /// Get write head position.
331    pub fn write_head(&self) -> usize {
332        self.write_head
333    }
334
335    /// Get total dimensions.
336    pub fn dims(&self) -> usize {
337        self.config.dims
338    }
339
340    /// Get frame count.
341    pub fn frame_count(&self) -> usize {
342        self.config.frame_count
343    }
344
345    /// Get maximum absolute value in field.
346    pub fn max_energy(&self) -> f32 {
347        self.frames
348            .iter()
349            .map(|f| f.max_abs())
350            .fold(0.0f32, f32::max)
351    }
352
353    /// Get total non-zero count.
354    pub fn total_activity(&self) -> usize {
355        self.frames.iter().map(|f| f.non_zero_count()).sum()
356    }
357
358    /// Clear entire field.
359    pub fn clear(&mut self) {
360        for frame in &mut self.frames {
361            *frame = FieldVector::new(self.config.dims);
362        }
363        self.write_head = 0;
364        self.tick_count = 0;
365        self.was_active.fill(false);
366    }
367
368    /// Convert tick difference to milliseconds.
369    pub fn ticks_to_ms(&self, ticks: u64) -> f32 {
370        (ticks as f32 * 1000.0) / self.config.tick_rate_hz as f32
371    }
372
373    /// Convert milliseconds to ticks.
374    pub fn ms_to_ticks(&self, ms: f32) -> u64 {
375        ((ms * self.config.tick_rate_hz as f32) / 1000.0).round() as u64
376    }
377}
378
379impl Clone for TemporalField {
380    /// Clone the field state but NOT the observers.
381    /// The clone starts with no subscribers.
382    fn clone(&self) -> Self {
383        Self {
384            frames: self.frames.clone(),
385            config: self.config.clone(),
386            write_head: self.write_head,
387            tick_count: self.tick_count,
388            observers: Vec::new(), // Observers are not cloned
389            triggers: self.triggers.clone(),
390            was_active: self.was_active.clone(),
391        }
392    }
393}
394
395impl std::fmt::Debug for TemporalField {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        f.debug_struct("TemporalField")
398            .field("dims", &self.config.dims)
399            .field("frame_count", &self.config.frame_count)
400            .field("retention", &self.config.retention)
401            .field("write_head", &self.write_head)
402            .field("tick_count", &self.tick_count)
403            .field("observers", &self.observers.len())
404            .field("regions", &self.triggers.regions.len())
405            .finish()
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use std::sync::atomic::{AtomicUsize, Ordering};
413
414    #[test]
415    fn test_new_field() {
416        let config = FieldConfig::new(64, 10, 0.95);
417        let field = TemporalField::new(config);
418
419        assert_eq!(field.dims(), 64);
420        assert_eq!(field.frame_count(), 10);
421        assert_eq!(field.tick_count(), 0);
422        assert_eq!(field.total_activity(), 0);
423    }
424
425    #[test]
426    fn test_write_and_read_region() {
427        let config = FieldConfig::new(128, 10, 0.95);
428        let mut field = TemporalField::new(config);
429
430        let values = vec![0.5; 32];
431        field.write_region(&values, 0..32);
432
433        assert!(field.region_active(0..32, 0.1));
434        assert!(!field.region_active(32..64, 0.1));
435    }
436
437    #[test]
438    fn test_decay() {
439        let config = FieldConfig::new(64, 10, 0.5);
440        let mut field = TemporalField::new(config);
441
442        field.write_region(&vec![1.0; 64], 0..64);
443        let initial = field.region_energy(0..64);
444
445        field.tick();
446        let after_tick = field.region_energy(0..64);
447
448        assert!(after_tick < initial * 0.5);
449    }
450
451    #[test]
452    fn test_region_active_fires_event() {
453        let config = FieldConfig::new(64, 10, 0.95);
454        let mut field = TemporalField::new(config);
455
456        // Configure: add monitored region
457        field.monitor_region(MonitoredRegion::new("test", 0..32, 0.1));
458
459        // Subscribe reader
460        let count = Arc::new(AtomicUsize::new(0));
461        let count_clone = count.clone();
462        field.subscribe(Arc::new(crate::observer::FnObserver(move |event| {
463            if matches!(event, FieldEvent::RegionActive { .. }) {
464                count_clone.fetch_add(1, Ordering::SeqCst);
465            }
466        })));
467
468        // Writer writes - fires event to reader
469        field.write_region(&vec![0.5; 32], 0..32);
470
471        assert_eq!(count.load(Ordering::SeqCst), 1);
472    }
473
474    #[test]
475    fn test_convergence_fires() {
476        let config = FieldConfig::new(128, 10, 0.95);
477        let mut field = TemporalField::new(config);
478
479        // Configure: add monitored regions
480        field.monitor_region(MonitoredRegion::new("a", 0..32, 0.1));
481        field.monitor_region(MonitoredRegion::new("b", 32..64, 0.1));
482        field.monitor_region(MonitoredRegion::new("c", 64..96, 0.1));
483        field.set_convergence_threshold(2);
484
485        let convergence_count = Arc::new(AtomicUsize::new(0));
486        let cc = convergence_count.clone();
487
488        field.subscribe(Arc::new(crate::observer::FnObserver(move |event| {
489            if matches!(event, FieldEvent::Convergence { .. }) {
490                cc.fetch_add(1, Ordering::SeqCst);
491            }
492        })));
493
494        // Write to two regions - should trigger convergence
495        field.write_region(&vec![0.5; 32], 0..32);
496        field.write_region(&vec![0.5; 32], 32..64);
497
498        assert!(convergence_count.load(Ordering::SeqCst) >= 1);
499    }
500
501    #[test]
502    fn test_ring_buffer_wrap() {
503        let config = FieldConfig::new(64, 3, 1.0);
504        let mut field = TemporalField::new(config);
505
506        for i in 0..5 {
507            field.clear_current();
508            field.write_region(&vec![(i + 1) as f32 * 0.1; 64], 0..64);
509            field.advance_write_head();
510        }
511
512        assert_eq!(field.write_head(), 2);
513    }
514
515    #[test]
516    fn test_window_chronological() {
517        let config = FieldConfig::new(64, 5, 1.0);
518        let mut field = TemporalField::new(config);
519
520        for i in 0..3 {
521            field.clear_current();
522            field.write_region(&vec![(i + 1) as f32 * 0.25; 1], 0..1);
523            field.advance_write_head();
524        }
525
526        let window = field.read_window(3);
527        assert_eq!(window.len(), 3);
528
529        assert!((window[0].get(0) - 0.25).abs() < 0.01);
530        assert!((window[1].get(0) - 0.50).abs() < 0.01);
531        assert!((window[2].get(0) - 0.75).abs() < 0.01);
532    }
533
534    #[test]
535    fn test_hysteresis_prevents_chattering() {
536        // Use single dimension for simpler energy calculation
537        // Energy = value^2 for single dimension
538        let config = FieldConfig::new(1, 10, 1.0); // No decay for clarity
539        let mut field = TemporalField::new(config);
540
541        // Region with explicit hysteresis: on=0.25 (value=0.5), off=0.09 (value=0.3)
542        // Energy = value^2, so:
543        //   value=0.6 → energy=0.36 (above on)
544        //   value=0.4 → energy=0.16 (between)
545        //   value=0.2 → energy=0.04 (below off)
546        field.monitor_region(MonitoredRegion::with_hysteresis("test", 0..1, 0.25, 0.09));
547
548        let active_count = Arc::new(AtomicUsize::new(0));
549        let quiet_count = Arc::new(AtomicUsize::new(0));
550        let ac = active_count.clone();
551        let qc = quiet_count.clone();
552
553        field.subscribe(Arc::new(crate::observer::FnObserver(move |event| {
554            match event {
555                FieldEvent::RegionActive { .. } => {
556                    ac.fetch_add(1, Ordering::SeqCst);
557                }
558                FieldEvent::RegionQuiet { .. } => {
559                    qc.fetch_add(1, Ordering::SeqCst);
560                }
561                _ => {}
562            }
563        })));
564
565        // Write value=0.6 → energy=0.36 (above on_threshold 0.25) → should fire RegionActive
566        field.set_region(&[0.6], 0..1);
567        assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should fire RegionActive");
568        assert_eq!(quiet_count.load(Ordering::SeqCst), 0, "Should not fire RegionQuiet");
569
570        // Write value=0.4 → energy=0.16 (between thresholds: below on=0.25 but above off=0.09)
571        // Should NOT fire any event due to hysteresis
572        field.set_region(&[0.4], 0..1);
573        assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should not fire again (hysteresis)");
574        assert_eq!(quiet_count.load(Ordering::SeqCst), 0, "Should stay active (hysteresis)");
575
576        // Write value=0.2 → energy=0.04 (below off_threshold 0.09) → should fire RegionQuiet
577        field.set_region(&[0.2], 0..1);
578        assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should not fire RegionActive");
579        assert_eq!(quiet_count.load(Ordering::SeqCst), 1, "Should fire RegionQuiet");
580
581        // Write value=0.4 → energy=0.16 (above off=0.09 but below on=0.25)
582        // Should NOT fire any event (need to exceed on_threshold to become active again)
583        field.set_region(&[0.4], 0..1);
584        assert_eq!(active_count.load(Ordering::SeqCst), 1, "Should not become active (hysteresis)");
585        assert_eq!(quiet_count.load(Ordering::SeqCst), 1, "Should stay quiet");
586
587        // Write value=0.6 → energy=0.36 (above on_threshold 0.25) → should fire RegionActive again
588        field.set_region(&[0.6], 0..1);
589        assert_eq!(active_count.load(Ordering::SeqCst), 2, "Should fire RegionActive again");
590    }
591
592    #[test]
593    fn test_default_hysteresis_gap() {
594        // Default hysteresis gap is 20%
595        let region = MonitoredRegion::new("test", 0..32, 0.5);
596        assert!((region.on_threshold - 0.5).abs() < 0.001);
597        assert!((region.off_threshold - 0.4).abs() < 0.001); // 0.5 * 0.8 = 0.4
598        assert!((region.hysteresis_gap() - 0.2).abs() < 0.001);
599    }
600
601    #[test]
602    fn test_custom_hysteresis_gap() {
603        // Custom gap of 30%
604        let region = MonitoredRegion::new("test", 0..32, 1.0).with_gap(0.3);
605        assert!((region.on_threshold - 1.0).abs() < 0.001);
606        assert!((region.off_threshold - 0.7).abs() < 0.001); // 1.0 * 0.7 = 0.7
607        assert!((region.hysteresis_gap() - 0.3).abs() < 0.001);
608    }
609}