Skip to main content

temporal_field/
vector.rs

1//! Field Vector - Signal-based storage for temporal fields
2//!
3//! ASTRO_004 compliant: Uses Signal (polarity × magnitude × multiplier) throughout.
4//! No floats in neural computation paths.
5
6use std::ops::Range;
7use ternary_signal::Signal;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12/// Signal-based vector for temporal fields.
13///
14/// Each element is a Signal (polarity: i8, magnitude: u8, multiplier: u8).
15/// Total size: 3 bytes per element.
16///
17/// Arithmetic operations use the full effective value: `polarity × magnitude × multiplier`
18/// (range ±65,025). Results are decomposed back into (p, m, k) via `Signal::from_current`.
19#[derive(Clone, Debug)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub struct FieldVector {
22    signals: Vec<Signal>,
23}
24
25impl FieldVector {
26    /// Create a new zero-initialized vector.
27    pub fn new(dims: usize) -> Self {
28        Self {
29            signals: vec![Signal::ZERO; dims],
30        }
31    }
32
33    /// Create from raw Signal slice.
34    pub fn from_signals(signals: Vec<Signal>) -> Self {
35        Self { signals }
36    }
37
38    /// Get dimensions.
39    #[inline]
40    pub fn dims(&self) -> usize {
41        self.signals.len()
42    }
43
44    /// Get Signal at index.
45    #[inline]
46    pub fn get(&self, idx: usize) -> Signal {
47        self.signals[idx]
48    }
49
50    /// Set Signal at index.
51    #[inline]
52    pub fn set(&mut self, idx: usize, signal: Signal) {
53        self.signals[idx] = signal;
54    }
55
56    /// Get the full effective value: `polarity × magnitude × multiplier` (±65,025).
57    #[inline]
58    pub fn get_current(&self, idx: usize) -> i32 {
59        self.signals[idx].current()
60    }
61
62    /// Set from a signed i32 value using the full p×m×k range (±65,025).
63    #[inline]
64    pub fn set_current(&mut self, idx: usize, value: i32) {
65        self.signals[idx] = Signal::from_current(value);
66    }
67
68    /// Get as signed i16 (polarity × magnitude only, ignores multiplier).
69    ///
70    /// **Deprecated in favor of [`get_current`]** which uses the full range.
71    /// Retained for backward compatibility with code that needs the narrow range.
72    #[inline]
73    pub fn get_i16(&self, idx: usize) -> i16 {
74        let s = self.signals[idx];
75        (s.polarity as i16) * (s.magnitude as i16)
76    }
77
78    /// Set from signed i16 value (clamped to ±255, multiplier=1).
79    ///
80    /// **Deprecated in favor of [`set_current`]** which uses the full range.
81    /// Retained for backward compatibility.
82    #[inline]
83    pub fn set_i16(&mut self, idx: usize, value: i16) {
84        self.signals[idx] = Signal::from_signed_i32(value as i32);
85    }
86
87    /// Decay all values toward zero.
88    /// retention: u8 where 255 = 1.0 (no decay), 230 ≈ 0.90
89    ///
90    /// Decays the effective value (p×m×k), then re-encodes into Signal.
91    /// This preserves the full dynamic range during decay.
92    pub fn decay(&mut self, retention: u8) {
93        for s in &mut self.signals {
94            let current = s.current();
95            if current == 0 {
96                continue;
97            }
98            // Apply retention to the effective value
99            let decayed = (current as i64 * retention as i64 / 255) as i32;
100            if decayed == 0 {
101                *s = Signal::ZERO;
102            } else {
103                *s = Signal::from_current(decayed);
104            }
105        }
106    }
107
108    /// Add another vector (saturating at ±65,025).
109    pub fn add(&mut self, other: &FieldVector) {
110        debug_assert_eq!(self.dims(), other.dims());
111        for i in 0..self.signals.len() {
112            let a = self.get_current(i);
113            let b = other.get_current(i);
114            let sum = (a as i64 + b as i64).clamp(-65025, 65025) as i32;
115            self.set_current(i, sum);
116        }
117    }
118
119    /// Add Signals to a range (saturating at ±65,025).
120    pub fn add_to_range(&mut self, signals: &[Signal], range: Range<usize>) {
121        let range_len = range.len();
122        for (i, &s) in signals.iter().take(range_len).enumerate() {
123            let idx = range.start + i;
124            if idx < self.signals.len() {
125                let current = self.get_current(idx);
126                let delta = s.current();
127                let sum = (current as i64 + delta as i64).clamp(-65025, 65025) as i32;
128                self.set_current(idx, sum);
129            }
130        }
131    }
132
133    /// Set Signals in a range.
134    pub fn set_range(&mut self, signals: &[Signal], range: Range<usize>) {
135        let range_len = range.len();
136        for (i, &s) in signals.iter().take(range_len).enumerate() {
137            let idx = range.start + i;
138            if idx < self.signals.len() {
139                self.signals[idx] = s;
140            }
141        }
142    }
143
144    /// Get Signals from a range.
145    pub fn get_range(&self, range: Range<usize>) -> Vec<Signal> {
146        (range.start..range.end.min(self.dims()))
147            .map(|i| self.signals[i])
148            .collect()
149    }
150
151    /// Compute energy (sum of squared effective magnitudes) in a range.
152    /// Returns u64 to prevent overflow (max per element: 65025² ≈ 4.2B).
153    pub fn range_energy(&self, range: Range<usize>) -> u64 {
154        (range.start..range.end.min(self.dims()))
155            .map(|i| {
156                let eff = self.signals[i].effective_magnitude() as u64;
157                eff * eff
158            })
159            .sum()
160    }
161
162    /// Check if range is active (energy above threshold).
163    pub fn range_active(&self, range: Range<usize>, threshold: u64) -> bool {
164        self.range_energy(range) > threshold
165    }
166
167    /// Check if all signals are zero.
168    pub fn is_zero(&self) -> bool {
169        self.signals.iter().all(|s| s.magnitude == 0)
170    }
171
172    /// Count non-zero signals.
173    pub fn non_zero_count(&self) -> usize {
174        self.signals.iter().filter(|s| s.magnitude > 0).count()
175    }
176
177    /// Get maximum effective magnitude.
178    pub fn max_magnitude(&self) -> u16 {
179        self.signals.iter().map(|s| s.effective_magnitude()).max().unwrap_or(0)
180    }
181
182    /// Scale all values by factor (u8 where 255 = 1.0).
183    pub fn scale(&mut self, factor: u8) {
184        for s in &mut self.signals {
185            let current = s.current();
186            let scaled = (current as i64 * factor as i64 / 255) as i32;
187            *s = Signal::from_current(scaled);
188        }
189    }
190
191    /// Get slice reference for direct access.
192    pub fn as_slice(&self) -> &[Signal] {
193        &self.signals
194    }
195
196    /// Get mutable slice reference.
197    pub fn as_mut_slice(&mut self) -> &mut [Signal] {
198        &mut self.signals
199    }
200}
201
202impl Default for FieldVector {
203    fn default() -> Self {
204        Self::new(64)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_new_is_zero() {
214        let v = FieldVector::new(128);
215        assert!(v.is_zero());
216        assert_eq!(v.non_zero_count(), 0);
217    }
218
219    #[test]
220    fn test_set_get_signal() {
221        let mut v = FieldVector::new(64);
222        v.set(0, Signal::positive(200));
223        v.set(10, Signal::negative(128));
224
225        assert_eq!(v.get(0).polarity, 1);
226        assert_eq!(v.get(0).magnitude, 200);
227        assert_eq!(v.get(10).polarity, -1);
228        assert_eq!(v.get(10).magnitude, 128);
229    }
230
231    #[test]
232    fn test_current_interface() {
233        let mut v = FieldVector::new(64);
234        v.set_current(0, 5000);  // should decompose into p=1, m×k≈5000
235        v.set_current(1, -3000); // should decompose into p=-1, m×k≈3000
236
237        let val0 = v.get_current(0);
238        let val1 = v.get_current(1);
239        // Allow small rounding error from decomposition
240        assert!((val0 - 5000).abs() < 20, "expected ~5000, got {}", val0);
241        assert!((val1 + 3000).abs() < 20, "expected ~-3000, got {}", val1);
242    }
243
244    #[test]
245    fn test_full_range_add() {
246        let mut v = FieldVector::new(4);
247        // Set a value > 255 — requires multiplier
248        v.set(0, Signal::positive_amplified(200, 100)); // 20,000
249        let mut other = FieldVector::new(4);
250        other.set(0, Signal::positive_amplified(100, 50)); // 5,000
251
252        v.add(&other);
253        let result = v.get_current(0);
254        // 20000 + 5000 = 25000
255        assert!((result - 25000).abs() < 100, "expected ~25000, got {}", result);
256    }
257
258    #[test]
259    fn test_decay_full_range() {
260        let mut v = FieldVector::new(64);
261        v.set(0, Signal::positive_amplified(255, 100)); // 25,500
262        v.set(1, Signal::negative_amplified(200, 50));  // -10,000
263
264        v.decay(128); // ~50% retention
265
266        let val0 = v.get_current(0);
267        let val1 = v.get_current(1);
268        // 25500 * 128/255 ≈ 12800
269        assert!((val0 - 12800).abs() < 200, "expected ~12800, got {}", val0);
270        // -10000 * 128/255 ≈ -5020
271        assert!((val1 + 5020).abs() < 200, "expected ~-5020, got {}", val1);
272    }
273
274    #[test]
275    fn test_range_energy_full() {
276        let mut v = FieldVector::new(128);
277        // Set signals with multiplier
278        for i in 0..4 {
279            v.set(i, Signal::positive_amplified(100, 50)); // effective = 5000
280        }
281
282        let energy = v.range_energy(0..4);
283        // 4 * 5000² = 4 * 25,000,000 = 100,000,000
284        assert_eq!(energy, 100_000_000);
285    }
286
287    #[test]
288    fn test_add_to_range_full() {
289        let mut v = FieldVector::new(64);
290        let signals = vec![Signal::positive_amplified(100, 10); 4]; // 1000 each
291        v.add_to_range(&signals, 0..4);
292
293        let val = v.get_current(0);
294        assert!((val - 1000).abs() < 10, "expected ~1000, got {}", val);
295    }
296
297    #[test]
298    fn test_set_range() {
299        let mut v = FieldVector::new(64);
300        let signals = vec![Signal::negative(50); 4];
301        v.set_range(&signals, 10..14);
302
303        assert_eq!(v.get(9).magnitude, 0);
304        assert_eq!(v.get(10).magnitude, 50);
305        assert_eq!(v.get(10).polarity, -1);
306        assert_eq!(v.get(13).magnitude, 50);
307        assert_eq!(v.get(14).magnitude, 0);
308    }
309
310    #[test]
311    fn test_get_range() {
312        let mut v = FieldVector::new(64);
313        v.set(5, Signal::positive(100));
314        v.set(6, Signal::negative(200));
315
316        let range = v.get_range(5..7);
317        assert_eq!(range.len(), 2);
318        assert_eq!(range[0].magnitude, 100);
319        assert_eq!(range[1].magnitude, 200);
320    }
321
322    #[test]
323    fn test_scale_full_range() {
324        let mut v = FieldVector::new(4);
325        v.set(0, Signal::positive_amplified(200, 100)); // 20000
326
327        v.scale(128); // ~50%
328
329        let val = v.get_current(0);
330        // 20000 * 128/255 ≈ 10039
331        assert!((val - 10039).abs() < 200, "expected ~10039, got {}", val);
332    }
333
334    #[test]
335    fn test_saturation_at_max() {
336        let mut v = FieldVector::new(4);
337        v.set(0, Signal::positive_amplified(255, 255)); // 65025
338
339        let mut other = FieldVector::new(4);
340        other.set(0, Signal::positive_amplified(255, 255)); // 65025
341
342        v.add(&other);
343        let result = v.get_current(0);
344        // 65025 + 65025 = 130050 → clamped to 65025
345        assert!(result <= 65025, "should clamp to 65025, got {}", result);
346    }
347
348    // Backward compat: i16 interface still works for narrow-range code
349    #[test]
350    fn test_i16_interface() {
351        let mut v = FieldVector::new(64);
352        v.set_i16(0, 200);
353        v.set_i16(1, -128);
354
355        assert_eq!(v.get_i16(0), 200);
356        assert_eq!(v.get_i16(1), -128);
357    }
358}