Skip to main content

ternary_signal/
lib.rs

1//! # ternary-signal
2//!
3//! The fundamental unit of ternary neural communication: `Signal = polarity × magnitude × multiplier`
4//!
5//! Compact 3-byte representation combining:
6//! - **Polarity**: excitatory (+1), inhibitory (-1), or silent (0)
7//! - **Magnitude**: base intensity 0–255 (intrinsic neuron drive)
8//! - **Multiplier**: contextual scaling 0–255 (metabolic state, arousal, burst energy)
9//!
10//! Effective magnitude = `magnitude × multiplier` (0–65,025 range).
11//!
12//! This is the canonical Signal type shared across neuromorphic crates
13//! (ternsig, thermogram, astromind-snn, etc.).
14//!
15//! # Example
16//! ```
17//! use ternary_signal::{Signal, Polarity};
18//!
19//! let excited = Signal::positive(200);
20//! assert!(excited.is_positive());
21//! assert_eq!(excited.effective_magnitude(), 200); // multiplier defaults to 1
22//!
23//! let burst = Signal::positive_amplified(200, 100);
24//! assert_eq!(burst.effective_magnitude(), 20_000); // 200 × 100
25//!
26//! let inhibited = Signal::with_polarity(Polarity::Negative, 50);
27//! assert!(inhibited.is_negative());
28//!
29//! let neutral = Signal::zero();
30//! assert!(!neutral.is_active());
31//! ```
32
33#![no_std]
34
35#[cfg(feature = "serde")]
36use serde::{Deserialize, Serialize};
37
38// ---------------------------------------------------------------------------
39// Polarity
40// ---------------------------------------------------------------------------
41
42/// Polarity of a neural signal — strictly {-1, 0, +1}
43///
44/// Using this enum instead of raw `i8` prevents invalid states like polarity=2.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47#[repr(i8)]
48pub enum Polarity {
49    /// Inhibitory signal
50    Negative = -1,
51    /// No signal / silent
52    #[default]
53    Zero = 0,
54    /// Excitatory signal
55    Positive = 1,
56}
57
58impl Polarity {
59    /// Convert to i8
60    #[inline]
61    pub const fn as_i8(self) -> i8 {
62        self as i8
63    }
64
65    /// Try to convert from i8, returns None for invalid values
66    #[inline]
67    pub const fn from_i8(value: i8) -> Option<Self> {
68        match value {
69            -1 => Some(Self::Negative),
70            0 => Some(Self::Zero),
71            1 => Some(Self::Positive),
72            _ => None,
73        }
74    }
75
76    /// Convert from i8, clamping invalid values to the nearest valid polarity
77    #[inline]
78    pub const fn from_i8_clamped(value: i8) -> Self {
79        if value > 0 {
80            Self::Positive
81        } else if value < 0 {
82            Self::Negative
83        } else {
84            Self::Zero
85        }
86    }
87
88    /// Is this an active (non-zero) polarity?
89    #[inline]
90    pub const fn is_active(self) -> bool {
91        !matches!(self, Self::Zero)
92    }
93}
94
95impl From<Polarity> for i8 {
96    fn from(p: Polarity) -> i8 {
97        p.as_i8()
98    }
99}
100
101impl TryFrom<i8> for Polarity {
102    type Error = &'static str;
103
104    fn try_from(value: i8) -> Result<Self, Self::Error> {
105        Polarity::from_i8(value).ok_or("polarity must be -1, 0, or +1")
106    }
107}
108
109// ---------------------------------------------------------------------------
110// Signal
111// ---------------------------------------------------------------------------
112
113/// Signal: polarity × magnitude × multiplier (`s = p × m × k`)
114///
115/// The fundamental unit of neural communication.
116/// Compact 3-byte `#[repr(C)]` representation:
117/// - `polarity`: -1 (inhibited), 0 (neutral), +1 (excited)
118/// - `magnitude`: 0–255 (base intensity — intrinsic neuron drive)
119/// - `multiplier`: 0–255 (contextual scaling — metabolic state, arousal, burst energy)
120///
121/// Effective magnitude = `magnitude × multiplier` (range 0–65,025).
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124#[repr(C)]
125pub struct Signal {
126    /// Polarity: -1 (inhibited), 0 (neutral), +1 (excited)
127    pub polarity: i8,
128    /// Magnitude: 0–255 (base intensity — intrinsic neuron drive)
129    pub magnitude: u8,
130    /// Multiplier: 0–255 (contextual scaling — metabolic state, arousal, burst energy)
131    ///
132    /// Default: 1 (neutral — effective magnitude equals base magnitude).
133    /// 0 = silenced (effective magnitude is zero regardless of base magnitude).
134    pub multiplier: u8,
135}
136
137impl Signal {
138    // -- Constants -----------------------------------------------------------
139
140    /// Zero signal (no activity)
141    pub const ZERO: Self = Self {
142        polarity: 0,
143        magnitude: 0,
144        multiplier: 0,
145    };
146    /// Maximum positive signal
147    pub const MAX_POSITIVE: Self = Self {
148        polarity: 1,
149        magnitude: 255,
150        multiplier: 255,
151    };
152    /// Maximum negative signal
153    pub const MAX_NEGATIVE: Self = Self {
154        polarity: -1,
155        magnitude: 255,
156        multiplier: 255,
157    };
158
159    // -- Effective magnitude -------------------------------------------------
160
161    /// Compute effective magnitude: `magnitude × multiplier` (0–65,025)
162    #[inline]
163    pub const fn effective_magnitude(&self) -> u16 {
164        self.magnitude as u16 * self.multiplier as u16
165    }
166
167    /// Compute signed effective current: `polarity × magnitude × multiplier`
168    #[inline]
169    pub const fn current(&self) -> i32 {
170        self.polarity as i32 * self.effective_magnitude() as i32
171    }
172
173    /// Baseline multiplier constant — use when k=1 is intentional.
174    pub const BASELINE_MULTIPLIER: u8 = 1;
175
176    // -- Constructors --------------------------------------------------------
177
178    /// Create a zero / neutral signal
179    #[inline]
180    pub const fn zero() -> Self {
181        Self::ZERO
182    }
183
184    /// Create a positive (excitatory) signal with multiplier = 1
185    #[deprecated(note = "use Signal::positive_amplified(mag, mul) — s = p × m × k requires all three values")]
186    #[inline]
187    pub const fn positive(magnitude: u8) -> Self {
188        Self {
189            polarity: 1,
190            magnitude,
191            multiplier: 1,
192        }
193    }
194
195    /// Create a negative (inhibitory) signal with multiplier = 1
196    #[deprecated(note = "use Signal::negative_amplified(mag, mul) — s = p × m × k requires all three values")]
197    #[inline]
198    pub const fn negative(magnitude: u8) -> Self {
199        Self {
200            polarity: -1,
201            magnitude,
202            multiplier: 1,
203        }
204    }
205
206    /// Create a positive signal with explicit multiplier
207    #[inline]
208    pub const fn positive_amplified(magnitude: u8, multiplier: u8) -> Self {
209        Self {
210            polarity: 1,
211            magnitude,
212            multiplier,
213        }
214    }
215
216    /// Create a negative signal with explicit multiplier
217    #[inline]
218    pub const fn negative_amplified(magnitude: u8, multiplier: u8) -> Self {
219        Self {
220            polarity: -1,
221            magnitude,
222            multiplier,
223        }
224    }
225
226    /// Create from [`Polarity`] enum and magnitude (type-safe, multiplier = 1)
227    #[deprecated(note = "use Signal::with_polarity_amplified(pol, mag, mul) — s = p × m × k requires all three values")]
228    #[inline]
229    pub const fn with_polarity(polarity: Polarity, magnitude: u8) -> Self {
230        Self {
231            polarity: polarity.as_i8(),
232            magnitude,
233            multiplier: 1,
234        }
235    }
236
237    /// Create from [`Polarity`] enum, magnitude, and multiplier (type-safe)
238    #[inline]
239    pub const fn with_polarity_amplified(
240        polarity: Polarity,
241        magnitude: u8,
242        multiplier: u8,
243    ) -> Self {
244        Self {
245            polarity: polarity.as_i8(),
246            magnitude,
247            multiplier,
248        }
249    }
250
251    /// Create from raw `i8` polarity and magnitude (**unchecked**, multiplier = 1)
252    #[deprecated(note = "use Signal::new_raw(pol, mag, mul) — s = p × m × k requires all three values")]
253    #[inline]
254    pub const fn new(polarity: i8, magnitude: u8) -> Self {
255        Self {
256            polarity,
257            magnitude,
258            multiplier: 1,
259        }
260    }
261
262    /// Create from raw components (**unchecked**)
263    #[inline]
264    pub const fn new_raw(polarity: i8, magnitude: u8, multiplier: u8) -> Self {
265        Self {
266            polarity,
267            magnitude,
268            multiplier,
269        }
270    }
271
272    /// Create from raw `i8` polarity with validation (multiplier = 1)
273    #[deprecated(note = "use Signal::new_checked_full(pol, mag, mul) — s = p × m × k requires all three values")]
274    #[inline]
275    pub const fn new_checked(polarity: i8, magnitude: u8) -> Option<Self> {
276        match Polarity::from_i8(polarity) {
277            Some(_) => Some(Self {
278                polarity,
279                magnitude,
280                multiplier: 1,
281            }),
282            None => None,
283        }
284    }
285
286    /// Create from raw `i8` polarity, magnitude, and multiplier with validation
287    #[inline]
288    pub const fn new_checked_full(polarity: i8, magnitude: u8, multiplier: u8) -> Option<Self> {
289        match Polarity::from_i8(polarity) {
290            Some(_) => Some(Self {
291                polarity,
292                magnitude,
293                multiplier,
294            }),
295            None => None,
296        }
297    }
298
299    /// Create from separate float components (multiplier = 1)
300    ///
301    /// `polarity_f`: quantised to {-1, 0, +1} (dead-zone ±0.1)
302    /// `magnitude_f`: clamped to 0.0–1.0, scaled to 0–255
303    #[deprecated(note = "defaults multiplier to 1 — construct with explicit multiplier instead")]
304    #[inline]
305    pub fn from_floats(polarity_f: f32, magnitude_f: f32) -> Self {
306        let polarity = if polarity_f > 0.1 {
307            1
308        } else if polarity_f < -0.1 {
309            -1
310        } else {
311            0
312        };
313        let magnitude = (clamp_f32(magnitude_f, 0.0, 1.0) * 255.0) as u8;
314        Self {
315            polarity,
316            magnitude,
317            multiplier: 1,
318        }
319    }
320
321    /// Create from a single signed float (-1.0 to 1.0), multiplier = 1
322    ///
323    /// Sign → polarity, absolute value → magnitude.
324    #[deprecated(note = "defaults multiplier to 1 — construct with explicit multiplier instead")]
325    #[inline]
326    pub fn from_signed(value: f32) -> Self {
327        let polarity = if value > 0.01 {
328            1
329        } else if value < -0.01 {
330            -1
331        } else {
332            0
333        };
334        let abs = if value < 0.0 { -value } else { value };
335        let clamped = if abs > 1.0 { 1.0 } else { abs };
336        let magnitude = (clamped * 255.0) as u8;
337        Self {
338            polarity,
339            magnitude,
340            multiplier: 1,
341        }
342    }
343
344    /// Create from a signed `i32` (multiplier = 1, clamps magnitude to 255).
345    ///
346    /// **Warning**: This discards values above ±255. For the full ±65,025 range,
347    /// use [`from_current`] instead.
348    #[deprecated(note = "defaults multiplier to 1 — use Signal::from_current(val) for full p × m × k range")]
349    #[inline]
350    pub fn from_signed_i32(value: i32) -> Self {
351        if value == 0 {
352            Self::ZERO
353        } else if value > 0 {
354            Self {
355                polarity: 1,
356                magnitude: (if value > 255 { 255 } else { value }) as u8,
357                multiplier: 1,
358            }
359        } else {
360            let abs = if value == i32::MIN { 255 } else { -value };
361            Self {
362                polarity: -1,
363                magnitude: (if abs > 255 { 255 } else { abs }) as u8,
364                multiplier: 1,
365            }
366        }
367    }
368
369    /// Create from a signed `i32` using the full `p × m × k` range (±65,025).
370    ///
371    /// Decomposes the absolute value into `magnitude × multiplier` where both
372    /// are 0–255. Values above 65,025 are clamped. Values ≤ 255 get multiplier=1.
373    /// Values > 255 use magnitude=255 and multiplier=ceil(abs/255).
374    #[inline]
375    pub fn from_current(value: i32) -> Self {
376        if value == 0 {
377            return Self::ZERO;
378        }
379
380        let polarity: i8 = if value > 0 { 1 } else { -1 };
381        let abs = if value == i32::MIN {
382            65025u32
383        } else if value < 0 {
384            (-value) as u32
385        } else {
386            value as u32
387        };
388
389        if abs <= 255 {
390            Self {
391                polarity,
392                magnitude: abs as u8,
393                multiplier: 1,
394            }
395        } else {
396            // abs > 255: use magnitude=255, multiplier = abs/255 (rounded)
397            let clamped = if abs > 65025 { 65025 } else { abs };
398            let mul = ((clamped + 127) / 255).min(255) as u8;
399            let mul = if mul == 0 { 1 } else { mul };
400            // Recompute magnitude to minimize rounding error: mag = clamped / mul
401            let mag = (clamped / mul as u32).min(255) as u8;
402            Self {
403                polarity,
404                magnitude: mag,
405                multiplier: mul,
406            }
407        }
408    }
409
410    /// Create from a signed `i16` (multiplier = 1)
411    #[deprecated(note = "defaults multiplier to 1 — use Signal::from_signed_i32(val) for large ranges or construct with explicit multiplier")]
412    #[inline]
413    pub fn from_i16(value: i16) -> Self {
414        if value == 0 {
415            Self::ZERO
416        } else if value > 0 {
417            Self {
418                polarity: 1,
419                magnitude: (if value > 255 { 255 } else { value }) as u8,
420                multiplier: 1,
421            }
422        } else {
423            let abs = -(value as i32);
424            Self {
425                polarity: -1,
426                magnitude: (if abs > 255 { 255 } else { abs }) as u8,
427                multiplier: 1,
428            }
429        }
430    }
431
432    /// Create from u8 bipolar representation (128 = baseline)
433    #[deprecated(note = "defaults multiplier to 1 — construct Signal with explicit multiplier instead")]
434    #[inline]
435    pub fn from_u8_bipolar(level: u8) -> Self {
436        let delta = level as i16 - 128;
437        Self::from_i16(delta)
438    }
439
440    /// Create from spike rate (Hz)
441    #[deprecated(note = "defaults multiplier to 1 — construct Signal with explicit multiplier instead")]
442    #[inline]
443    pub fn from_spike_rate(rate_hz: f32, max_rate_hz: f32) -> Self {
444        if rate_hz <= 0.0 || max_rate_hz <= 0.0 {
445            return Self::ZERO;
446        }
447        let normalized = clamp_f32(rate_hz / max_rate_hz, 0.0, 1.0);
448        let magnitude = (normalized * 255.0) as u8;
449        if magnitude == 0 {
450            Self::ZERO
451        } else {
452            Self::positive(magnitude)
453        }
454    }
455
456    /// Create from spike count in a time window
457    #[deprecated(note = "defaults multiplier to 1 — construct Signal with explicit multiplier instead")]
458    #[inline]
459    pub fn from_spike_count(count: u32, window_ms: f32, max_rate_hz: f32) -> Self {
460        if count == 0 || window_ms <= 0.0 {
461            return Self::ZERO;
462        }
463        let rate_hz = count as f32 * 1000.0 / window_ms;
464        Self::from_spike_rate(rate_hz, max_rate_hz)
465    }
466
467    // -- Conversion ----------------------------------------------------------
468
469    /// Get as signed `i32` using effective magnitude (`polarity × magnitude × multiplier`)
470    #[inline]
471    pub fn as_signed_i32(&self) -> i32 {
472        self.current()
473    }
474
475    /// Get base magnitude as float (0.0–1.0)
476    #[inline]
477    pub fn magnitude_f32(&self) -> f32 {
478        self.magnitude as f32 / 255.0
479    }
480
481    /// Get effective magnitude as float (0.0–1.0, normalized to max 65025)
482    #[inline]
483    pub fn effective_magnitude_f32(&self) -> f32 {
484        self.effective_magnitude() as f32 / 65025.0
485    }
486
487    /// Get as signed float using effective magnitude (-1.0 to 1.0)
488    #[inline]
489    pub fn as_signed_f32(&self) -> f32 {
490        self.polarity as f32 * self.effective_magnitude_f32()
491    }
492
493    /// Convert to u8 bipolar (128 = baseline), using base magnitude only
494    #[inline]
495    pub fn to_u8_bipolar(&self) -> u8 {
496        let signed = self.polarity as i16 * self.magnitude as i16;
497        let result = signed + 128;
498        (if result < 0 {
499            0
500        } else if result > 255 {
501            255
502        } else {
503            result
504        }) as u8
505    }
506
507    /// Convert to spike rate (Hz), using base magnitude
508    #[inline]
509    pub fn to_spike_rate(&self, max_rate_hz: f32) -> f32 {
510        if self.polarity == 0 || self.magnitude == 0 {
511            return 0.0;
512        }
513        let normalized = self.magnitude as f32 / 255.0;
514        normalized * max_rate_hz * self.polarity.signum() as f32
515    }
516
517    /// Get polarity as [`Polarity`] enum (type-safe)
518    #[inline]
519    pub fn get_polarity(&self) -> Polarity {
520        Polarity::from_i8(self.polarity).unwrap_or(Polarity::Zero)
521    }
522
523    // -- Queries -------------------------------------------------------------
524
525    /// Is this signal active (non-zero)?
526    #[inline]
527    pub fn is_active(&self) -> bool {
528        self.polarity != 0 && self.magnitude > 0
529    }
530
531    /// Is this a positive / excitatory signal?
532    #[inline]
533    pub fn is_positive(&self) -> bool {
534        self.polarity > 0 && self.magnitude > 0
535    }
536
537    /// Is this a negative / inhibitory signal?
538    #[inline]
539    pub fn is_negative(&self) -> bool {
540        self.polarity < 0 && self.magnitude > 0
541    }
542
543    // -- Multiplier operations -----------------------------------------------
544
545    /// Set the multiplier (contextual scaling)
546    #[inline]
547    pub const fn with_multiplier(self, multiplier: u8) -> Self {
548        Self {
549            polarity: self.polarity,
550            magnitude: self.magnitude,
551            multiplier,
552        }
553    }
554
555    /// Amplify: increase multiplier by delta (saturating)
556    #[inline]
557    pub const fn amplify(self, delta: u8) -> Self {
558        Self {
559            polarity: self.polarity,
560            magnitude: self.magnitude,
561            multiplier: self.multiplier.saturating_add(delta),
562        }
563    }
564
565    /// Attenuate: decrease multiplier by delta (saturating)
566    #[inline]
567    pub const fn attenuate(self, delta: u8) -> Self {
568        Self {
569            polarity: self.polarity,
570            magnitude: self.magnitude,
571            multiplier: self.multiplier.saturating_sub(delta),
572        }
573    }
574
575    // -- Arithmetic ----------------------------------------------------------
576
577    /// Add two signals (same polarity adds, opposite cancels).
578    /// Operates on base magnitude. Result gets multiplier = 1.
579    #[inline]
580    pub fn add(&self, other: &Self) -> Self {
581        let a = self.polarity as i32 * self.magnitude as i32;
582        let b = other.polarity as i32 * other.magnitude as i32;
583        Self::from_signed_i32(a + b)
584    }
585
586    /// Scale base magnitude by a factor (multiplier unchanged)
587    #[inline]
588    pub fn scale(&self, factor: f32) -> Self {
589        let new_mag = clamp_f32(self.magnitude as f32 * factor, 0.0, 255.0) as u8;
590        Self {
591            polarity: if new_mag > 0 { self.polarity } else { 0 },
592            magnitude: new_mag,
593            multiplier: self.multiplier,
594        }
595    }
596
597    /// Apply decay to base magnitude (in-place, for temporal fields)
598    #[inline]
599    pub fn decay(&mut self, retention: f32) {
600        self.magnitude = (self.magnitude as f32 * retention) as u8;
601        if self.magnitude == 0 {
602            self.polarity = 0;
603        }
604    }
605
606    /// Return a decayed copy
607    #[inline]
608    pub fn decayed(&self, retention: f32) -> Self {
609        let mut copy = *self;
610        copy.decay(retention);
611        copy
612    }
613
614    // -- Smooth transitions --------------------------------------------------
615
616    /// Step toward target by one unit (base magnitude)
617    #[inline]
618    pub fn step_toward(&self, target: &Self) -> Self {
619        self.step_toward_by(target, 1)
620    }
621
622    /// Step toward target by specified delta (base magnitude)
623    #[inline]
624    pub fn step_toward_by(&self, target: &Self, delta: u8) -> Self {
625        let current = self.polarity as i16 * self.magnitude as i16;
626        let target_val = target.polarity as i16 * target.magnitude as i16;
627        let diff = target_val - current;
628
629        if diff.unsigned_abs() <= delta as u16 {
630            *target
631        } else if diff > 0 {
632            let mut result = Self::from_signed_i32((current + delta as i16) as i32);
633            result.multiplier = self.multiplier;
634            result
635        } else {
636            let mut result = Self::from_signed_i32((current - delta as i16) as i32);
637            result.multiplier = self.multiplier;
638            result
639        }
640    }
641
642    /// Step toward target by fractional amount (0.0–1.0)
643    #[inline]
644    pub fn step_toward_ratio(&self, target: &Self, ratio: f32) -> Self {
645        let current = self.polarity as i16 * self.magnitude as i16;
646        let target_val = target.polarity as i16 * target.magnitude as i16;
647        let diff = target_val - current;
648        let abs_diff = if diff < 0 { -diff } else { diff } as f32;
649        let clamped = clamp_f32(ratio, 0.0, 1.0);
650        let delta = {
651            let raw = (abs_diff * clamped + 0.999) as i16; // ceil
652            if raw < 1 {
653                1
654            } else {
655                raw
656            }
657        };
658        self.step_toward_by(target, delta as u8)
659    }
660
661    /// Check if this signal has reached the target (within tolerance, base magnitude)
662    #[inline]
663    pub fn reached(&self, target: &Self, tolerance: u8) -> bool {
664        let current = self.polarity as i16 * self.magnitude as i16;
665        let target_val = target.polarity as i16 * target.magnitude as i16;
666        let diff = current - target_val;
667        let abs_diff = if diff < 0 { -diff } else { diff };
668        abs_diff <= tolerance as i16
669    }
670}
671
672// ---------------------------------------------------------------------------
673// PackedSignal — 1-byte compact format
674// ---------------------------------------------------------------------------
675
676/// Log-scale lookup table: 3-bit code → effective value (0–255).
677///
678/// Shared by both magnitude and multiplier fields — same encoding, single table.
679/// Logarithmic spacing preserves perceptual significance:
680/// fine granularity at low intensities, coarser at high.
681/// This table is frozen and will never change.
682pub const LOG_LUT: [u8; 8] = [0, 1, 4, 16, 32, 64, 128, 255];
683
684/// Shift table: 3-bit multiplier code → bit-shift for i16-safe integration.
685///
686/// Low multiplier attenuates (right-shift), high multiplier amplifies (left-shift).
687/// Maximum single-signal contribution: `255 << 3 = 2040`, well within i16 range.
688const SHIFT_TABLE: [i8; 8] = [
689    0,  // code 0: mul=0, signal dead (magnitude also 0 for this code)
690    -2, // code 1: mul=1, attenuate >>2
691    -1, // code 2: mul=4, attenuate >>1
692    0,  // code 3: mul=16, neutral
693    0,  // code 4: mul=32, neutral
694    1,  // code 5: mul=64, amplify <<1
695    2,  // code 6: mul=128, amplify <<2
696    3,  // code 7: mul=255, amplify <<3
697];
698
699/// Precomputed `polarity × magnitude × multiplier` for every possible packed byte.
700///
701/// 256 entries × 4 bytes = 1 KB — fits in L1 cache.
702/// Use this for operations that need the true scalar current (similarity, TERNARY_MATMUL,
703/// metabolic cost) outside the integration hot path.
704const CURRENT_LUT: [i32; 256] = {
705    let mut lut = [0i32; 256];
706    let mut i = 0u16;
707    while i < 256 {
708        let byte = i as u8;
709        let pol: i32 = match byte >> 6 {
710            1 => 1,
711            2 => -1,
712            _ => 0,
713        };
714        let mag = LOG_LUT[((byte >> 3) & 0x07) as usize] as i32;
715        let mul = LOG_LUT[(byte & 0x07) as usize] as i32;
716        lut[i as usize] = pol * mag * mul;
717        i += 1;
718    }
719    lut
720};
721
722/// Quantize a u8 value to the nearest 3-bit log-scale code.
723#[inline]
724const fn quantize(v: u8) -> u8 {
725    if v == 0 {
726        return 0;
727    }
728    // Thresholds are midpoints between adjacent LUT values
729    if v <= 2 {
730        return 1;
731    }
732    if v <= 10 {
733        return 2;
734    }
735    if v <= 24 {
736        return 3;
737    }
738    if v <= 48 {
739        return 4;
740    }
741    if v <= 96 {
742        return 5;
743    }
744    if v <= 191 {
745        return 6;
746    }
747    7
748}
749
750/// Compact 1-byte signal: `[pol:2|mag:3|mul:3]`
751///
752/// Storage and wire format for ternary signals. Reduces memory from 3 bytes to 1 byte
753/// per signal — a 67% reduction across the connectome.
754///
755/// ```text
756/// Bit:  7  6  5  4  3  2  1  0
757///       [pol ] [magnitude] [multiplier]
758///        2 bit   3 bit       3 bit
759/// ```
760///
761/// - Polarity: `00` = zero, `01` = +1, `10` = -1
762/// - Magnitude/Multiplier: 3-bit codes into log-scale LUT `[0, 1, 4, 16, 32, 64, 128, 255]`
763///
764/// Unpacking to `(i8, u8, u8)` happens only at the soma for integration.
765/// Shift-weighted integration keeps accumulators at i16.
766#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
767#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
768#[repr(transparent)]
769pub struct PackedSignal(pub u8);
770
771impl PackedSignal {
772    /// Zero / silent packed signal.
773    pub const ZERO: Self = Self(0);
774
775    /// Maximum excitatory: pol=+1, mag=255, mul=255
776    pub const MAX_POSITIVE: Self = Self(0b01_111_111);
777
778    /// Maximum inhibitory: pol=-1, mag=255, mul=255
779    pub const MAX_NEGATIVE: Self = Self(0b10_111_111);
780
781    /// Pack from raw polarity, magnitude, and multiplier.
782    ///
783    /// Magnitude and multiplier are quantized to the nearest 3-bit log-scale code.
784    #[inline]
785    pub const fn pack(polarity: i8, magnitude: u8, multiplier: u8) -> Self {
786        let p: u8 = if polarity > 0 {
787            1
788        } else if polarity < 0 {
789            2
790        } else {
791            0
792        };
793        Self((p << 6) | (quantize(magnitude) << 3) | quantize(multiplier))
794    }
795
796    /// Pack from a [`Polarity`] enum.
797    #[inline]
798    pub const fn pack_typed(polarity: Polarity, magnitude: u8, multiplier: u8) -> Self {
799        Self::pack(polarity.as_i8(), magnitude, multiplier)
800    }
801
802    /// Get the raw byte value.
803    #[inline]
804    pub const fn as_u8(self) -> u8 {
805        self.0
806    }
807
808    /// Construct directly from a raw byte (no validation).
809    #[inline]
810    pub const fn from_raw(byte: u8) -> Self {
811        Self(byte)
812    }
813
814    // -- Unpack components ---------------------------------------------------
815
816    /// Extract polarity as i8.
817    #[inline]
818    pub const fn polarity(self) -> i8 {
819        match self.0 >> 6 {
820            1 => 1,
821            2 => -1,
822            _ => 0,
823        }
824    }
825
826    /// Extract polarity as [`Polarity`] enum.
827    #[inline]
828    pub const fn get_polarity(self) -> Polarity {
829        match self.0 >> 6 {
830            1 => Polarity::Positive,
831            2 => Polarity::Negative,
832            _ => Polarity::Zero,
833        }
834    }
835
836    /// Extract the 3-bit magnitude code (0–7).
837    #[inline]
838    pub const fn mag_code(self) -> u8 {
839        (self.0 >> 3) & 0x07
840    }
841
842    /// Extract the 3-bit multiplier code (0–7).
843    #[inline]
844    pub const fn mul_code(self) -> u8 {
845        self.0 & 0x07
846    }
847
848    /// Decode magnitude via LUT (0–255).
849    #[inline]
850    pub const fn magnitude(self) -> u8 {
851        LOG_LUT[self.mag_code() as usize]
852    }
853
854    /// Decode multiplier via LUT (0–255).
855    #[inline]
856    pub const fn multiplier(self) -> u8 {
857        LOG_LUT[self.mul_code() as usize]
858    }
859
860    // -- Current computation -------------------------------------------------
861
862    /// Compute the true scalar current via precomputed LUT.
863    ///
864    /// Returns `polarity × magnitude × multiplier` as i32.
865    /// Use this for operations that need the exact product (similarity, TERNARY_MATMUL,
866    /// metabolic cost) — not for membrane integration (use [`shift_value`] instead).
867    #[inline]
868    pub const fn current(self) -> i32 {
869        CURRENT_LUT[self.0 as usize]
870    }
871
872    /// Compute the shift-weighted integration value for membrane accumulation.
873    ///
874    /// Returns a signed i16 value: `polarity × (magnitude << shift)` where shift
875    /// is determined by the multiplier code. Maximum: ±2040. Safe for i16 accumulators.
876    #[inline]
877    pub const fn shift_value(self) -> i16 {
878        let pol = self.polarity();
879        if pol == 0 {
880            return 0;
881        }
882
883        let mag = self.magnitude() as i16;
884        let shift = SHIFT_TABLE[self.mul_code() as usize];
885
886        let adjusted = if shift > 0 {
887            mag << (shift as u16)
888        } else if shift < 0 {
889            mag >> ((-shift) as u16)
890        } else {
891            mag
892        };
893
894        if pol > 0 {
895            adjusted
896        } else {
897            -adjusted
898        }
899    }
900
901    // -- Queries -------------------------------------------------------------
902
903    /// Is this signal active (non-zero polarity and non-zero magnitude)?
904    #[inline]
905    pub const fn is_active(self) -> bool {
906        let pol = self.0 >> 6;
907        let mag_code = (self.0 >> 3) & 0x07;
908        (pol == 1 || pol == 2) && mag_code > 0
909    }
910
911    /// Is this a positive / excitatory signal?
912    #[inline]
913    pub const fn is_positive(self) -> bool {
914        (self.0 >> 6) == 1 && ((self.0 >> 3) & 0x07) > 0
915    }
916
917    /// Is this a negative / inhibitory signal?
918    #[inline]
919    pub const fn is_negative(self) -> bool {
920        (self.0 >> 6) == 2 && ((self.0 >> 3) & 0x07) > 0
921    }
922
923    // -- Conversion to/from Signal -------------------------------------------
924
925    /// Convert to the unpacked 3-byte [`Signal`] representation.
926    ///
927    /// Magnitude and multiplier are decoded through the log-scale LUT.
928    #[inline]
929    pub const fn to_signal(self) -> Signal {
930        Signal {
931            polarity: self.polarity(),
932            magnitude: self.magnitude(),
933            multiplier: self.multiplier(),
934        }
935    }
936
937    /// Pack a [`Signal`] into compact 1-byte format.
938    ///
939    /// Lossy: magnitude and multiplier are quantized to 3-bit log-scale codes.
940    #[inline]
941    pub const fn from_signal(signal: &Signal) -> Self {
942        Self::pack(signal.polarity, signal.magnitude, signal.multiplier)
943    }
944}
945
946impl From<Signal> for PackedSignal {
947    #[inline]
948    fn from(s: Signal) -> Self {
949        Self::from_signal(&s)
950    }
951}
952
953impl From<PackedSignal> for Signal {
954    #[inline]
955    fn from(p: PackedSignal) -> Self {
956        p.to_signal()
957    }
958}
959
960impl core::fmt::Display for PackedSignal {
961    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
962        // Display uses the true current (LUT), same semantics as Signal
963        let cur = self.current();
964        if cur > 0 {
965            write!(f, "+{}", cur)
966        } else if cur < 0 {
967            // current() already negative, display without extra minus
968            write!(f, "{}", cur)
969        } else {
970            write!(f, "0")
971        }
972    }
973}
974
975// ---------------------------------------------------------------------------
976// Display
977// ---------------------------------------------------------------------------
978
979impl core::fmt::Display for Signal {
980    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
981        let eff = self.effective_magnitude();
982        match self.polarity {
983            p if p > 0 => write!(f, "+{}", eff),
984            p if p < 0 => write!(f, "-{}", eff),
985            _ => write!(f, "0"),
986        }
987    }
988}
989
990impl core::fmt::Display for Polarity {
991    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
992        match self {
993            Polarity::Negative => write!(f, "-"),
994            Polarity::Zero => write!(f, "0"),
995            Polarity::Positive => write!(f, "+"),
996        }
997    }
998}
999
1000// ---------------------------------------------------------------------------
1001// no_std f32 clamp helper (core doesn't have f32::clamp in all editions)
1002// ---------------------------------------------------------------------------
1003
1004#[inline]
1005fn clamp_f32(val: f32, min: f32, max: f32) -> f32 {
1006    if val < min {
1007        min
1008    } else if val > max {
1009        max
1010    } else {
1011        val
1012    }
1013}
1014
1015// ---------------------------------------------------------------------------
1016// Tests
1017// ---------------------------------------------------------------------------
1018
1019#[cfg(test)]
1020mod tests {
1021    extern crate std;
1022    use super::*;
1023
1024    #[test]
1025    fn test_signal_size() {
1026        assert_eq!(core::mem::size_of::<Signal>(), 3);
1027    }
1028
1029    #[test]
1030    fn test_signal_creation() {
1031        let zero = Signal::zero();
1032        assert!(!zero.is_active());
1033
1034        let pos = Signal::positive(200);
1035        assert!(pos.is_positive());
1036        assert!(pos.is_active());
1037        assert_eq!(pos.multiplier, 1);
1038        assert_eq!(pos.effective_magnitude(), 200);
1039
1040        let neg = Signal::negative(128);
1041        assert!(neg.is_negative());
1042        assert_eq!(neg.multiplier, 1);
1043    }
1044
1045    #[test]
1046    fn test_amplified_creation() {
1047        let burst = Signal::positive_amplified(200, 100);
1048        assert!(burst.is_positive());
1049        assert_eq!(burst.magnitude, 200);
1050        assert_eq!(burst.multiplier, 100);
1051        assert_eq!(burst.effective_magnitude(), 20_000);
1052
1053        let max = Signal::positive_amplified(255, 255);
1054        assert_eq!(max.effective_magnitude(), 65_025);
1055
1056        let silenced = Signal::positive_amplified(200, 0);
1057        assert_eq!(silenced.effective_magnitude(), 0);
1058    }
1059
1060    #[test]
1061    fn test_current() {
1062        let pos = Signal::positive_amplified(100, 50);
1063        assert_eq!(pos.current(), 5000);
1064
1065        let neg = Signal::negative_amplified(100, 50);
1066        assert_eq!(neg.current(), -5000);
1067
1068        let zero = Signal::zero();
1069        assert_eq!(zero.current(), 0);
1070    }
1071
1072    #[test]
1073    fn test_multiplier_ops() {
1074        let sig = Signal::positive(150);
1075        assert_eq!(sig.multiplier, 1);
1076
1077        let amped = sig.with_multiplier(200);
1078        assert_eq!(amped.magnitude, 150);
1079        assert_eq!(amped.multiplier, 200);
1080        assert_eq!(amped.effective_magnitude(), 30_000);
1081
1082        let more = sig.amplify(50);
1083        assert_eq!(more.multiplier, 51); // 1 + 50
1084
1085        let less = amped.attenuate(50);
1086        assert_eq!(less.multiplier, 150); // 200 - 50
1087
1088        // Saturation
1089        let maxed = sig.amplify(255);
1090        assert_eq!(maxed.multiplier, 255); // saturates at 255
1091
1092        let floored = sig.attenuate(255);
1093        assert_eq!(floored.multiplier, 0); // saturates at 0
1094    }
1095
1096    #[test]
1097    fn test_from_signed() {
1098        let from_pos = Signal::from_signed(0.75);
1099        assert_eq!(from_pos.polarity, 1);
1100        assert_eq!(from_pos.multiplier, 1);
1101
1102        let from_neg = Signal::from_signed(-0.5);
1103        assert_eq!(from_neg.polarity, -1);
1104
1105        let from_zero = Signal::from_signed(0.005);
1106        assert_eq!(from_zero.polarity, 0);
1107    }
1108
1109    #[test]
1110    fn test_decay() {
1111        let mut signal = Signal::positive(200);
1112        signal.decay(0.5);
1113        assert_eq!(signal.magnitude, 100);
1114        for _ in 0..10 {
1115            signal.decay(0.5);
1116        }
1117        assert!(!signal.is_active());
1118        assert_eq!(signal.polarity, 0);
1119    }
1120
1121    #[test]
1122    fn test_decay_preserves_multiplier() {
1123        let mut signal = Signal::positive_amplified(200, 100);
1124        signal.decay(0.5);
1125        assert_eq!(signal.magnitude, 100);
1126        assert_eq!(signal.multiplier, 100); // multiplier unchanged
1127    }
1128
1129    #[test]
1130    fn test_add() {
1131        let a = Signal::positive(100);
1132        let b = Signal::positive(100);
1133        let sum = a.add(&b);
1134        assert!(sum.is_positive());
1135
1136        let c = Signal::negative(100);
1137        let cancel = a.add(&c);
1138        assert!(!cancel.is_active() || cancel.magnitude < 10);
1139    }
1140
1141    #[test]
1142    fn test_polarity_enum() {
1143        assert_eq!(Polarity::Negative.as_i8(), -1);
1144        assert_eq!(Polarity::Zero.as_i8(), 0);
1145        assert_eq!(Polarity::Positive.as_i8(), 1);
1146        assert_eq!(Polarity::from_i8(2), None);
1147    }
1148
1149    #[test]
1150    fn test_step_toward() {
1151        let signal = Signal::positive(100);
1152        let target = Signal::positive(200);
1153        let result = signal.step_toward(&target);
1154        assert_eq!(result.magnitude, 101);
1155    }
1156
1157    #[test]
1158    fn test_step_toward_preserves_multiplier() {
1159        let signal = Signal::positive(100).with_multiplier(50);
1160        let target = Signal::positive(200);
1161        let result = signal.step_toward(&target);
1162        assert_eq!(result.magnitude, 101);
1163        assert_eq!(result.multiplier, 50); // preserved from source
1164    }
1165
1166    #[test]
1167    fn test_new_checked() {
1168        assert!(Signal::new_checked(1, 100).is_some());
1169        assert!(Signal::new_checked(2, 100).is_none());
1170    }
1171
1172    #[test]
1173    fn test_display() {
1174        extern crate alloc;
1175        use alloc::format;
1176        // Display shows effective magnitude (magnitude × multiplier)
1177        assert_eq!(format!("{}", Signal::positive(42)), "+42"); // 42 × 1
1178        assert_eq!(format!("{}", Signal::negative(10)), "-10"); // 10 × 1
1179        assert_eq!(format!("{}", Signal::zero()), "0");
1180        assert_eq!(format!("{}", Signal::positive_amplified(10, 100)), "+1000");
1181        // 10 × 100
1182    }
1183
1184    #[test]
1185    fn test_constants() {
1186        assert_eq!(Signal::ZERO.polarity, 0);
1187        assert_eq!(Signal::ZERO.magnitude, 0);
1188        assert_eq!(Signal::ZERO.multiplier, 0);
1189
1190        assert_eq!(Signal::MAX_POSITIVE.polarity, 1);
1191        assert_eq!(Signal::MAX_POSITIVE.magnitude, 255);
1192        assert_eq!(Signal::MAX_POSITIVE.multiplier, 255);
1193        assert_eq!(Signal::MAX_POSITIVE.effective_magnitude(), 65_025);
1194
1195        assert_eq!(Signal::MAX_NEGATIVE.polarity, -1);
1196        assert_eq!(Signal::MAX_NEGATIVE.magnitude, 255);
1197        assert_eq!(Signal::MAX_NEGATIVE.multiplier, 255);
1198    }
1199
1200    // -- PackedSignal tests --------------------------------------------------
1201
1202    #[test]
1203    fn test_packed_signal_size() {
1204        assert_eq!(core::mem::size_of::<PackedSignal>(), 1);
1205    }
1206
1207    #[test]
1208    fn test_packed_zero() {
1209        let z = PackedSignal::ZERO;
1210        assert_eq!(z.0, 0);
1211        assert_eq!(z.polarity(), 0);
1212        assert_eq!(z.magnitude(), 0);
1213        assert_eq!(z.multiplier(), 0);
1214        assert_eq!(z.current(), 0);
1215        assert_eq!(z.shift_value(), 0);
1216        assert!(!z.is_active());
1217    }
1218
1219    #[test]
1220    fn test_packed_polarity_encoding() {
1221        let pos = PackedSignal::pack(1, 100, 50);
1222        assert_eq!(pos.polarity(), 1);
1223        assert!(pos.is_positive());
1224        assert!(!pos.is_negative());
1225
1226        let neg = PackedSignal::pack(-1, 100, 50);
1227        assert_eq!(neg.polarity(), -1);
1228        assert!(!neg.is_positive());
1229        assert!(neg.is_negative());
1230
1231        let zero = PackedSignal::pack(0, 100, 50);
1232        assert_eq!(zero.polarity(), 0);
1233        assert!(!zero.is_active());
1234    }
1235
1236    #[test]
1237    fn test_packed_magnitude_lut() {
1238        // Verify each code maps correctly
1239        for code in 0..8u8 {
1240            let packed = PackedSignal::from_raw(0b01_000_000 | (code << 3));
1241            assert_eq!(packed.mag_code(), code);
1242            assert_eq!(packed.magnitude(), LOG_LUT[code as usize]);
1243        }
1244    }
1245
1246    #[test]
1247    fn test_packed_multiplier_lut() {
1248        for code in 0..8u8 {
1249            let packed = PackedSignal::from_raw(0b01_000_000 | code);
1250            assert_eq!(packed.mul_code(), code);
1251            assert_eq!(packed.multiplier(), LOG_LUT[code as usize]);
1252        }
1253    }
1254
1255    #[test]
1256    fn test_packed_current_lut() {
1257        // Verify CURRENT_LUT matches manual computation for every byte value
1258        for byte in 0..=255u8 {
1259            let p = PackedSignal::from_raw(byte);
1260            let manual = p.polarity() as i32 * p.magnitude() as i32 * p.multiplier() as i32;
1261            assert_eq!(
1262                p.current(),
1263                manual,
1264                "CURRENT_LUT mismatch at byte 0x{:02X}",
1265                byte
1266            );
1267        }
1268    }
1269
1270    #[test]
1271    fn test_packed_max_values() {
1272        let max_pos = PackedSignal::MAX_POSITIVE;
1273        assert_eq!(max_pos.polarity(), 1);
1274        assert_eq!(max_pos.magnitude(), 255);
1275        assert_eq!(max_pos.multiplier(), 255);
1276        assert_eq!(max_pos.current(), 65_025);
1277
1278        let max_neg = PackedSignal::MAX_NEGATIVE;
1279        assert_eq!(max_neg.polarity(), -1);
1280        assert_eq!(max_neg.current(), -65_025);
1281    }
1282
1283    #[test]
1284    fn test_packed_shift_value_range() {
1285        // Maximum shift_value should fit i16 (max ±2040)
1286        let max = PackedSignal::MAX_POSITIVE;
1287        let sv = max.shift_value();
1288        assert!(sv <= 2040, "shift_value {} exceeds 2040", sv);
1289        assert!(sv > 0);
1290
1291        let min = PackedSignal::MAX_NEGATIVE;
1292        let sv_neg = min.shift_value();
1293        assert!(sv_neg >= -2040, "shift_value {} below -2040", sv_neg);
1294        assert!(sv_neg < 0);
1295
1296        // Zero polarity always yields 0
1297        let zero = PackedSignal::pack(0, 255, 255);
1298        assert_eq!(zero.shift_value(), 0);
1299    }
1300
1301    #[test]
1302    fn test_packed_shift_attenuate_amplify() {
1303        // Low multiplier should attenuate
1304        let quiet = PackedSignal::pack(1, 128, 1); // mul code 1 → >>2
1305        let sv = quiet.shift_value();
1306        let raw_mag = quiet.magnitude() as i16;
1307        assert!(
1308            sv < raw_mag,
1309            "low mul should attenuate: shift={} vs raw={}",
1310            sv,
1311            raw_mag
1312        );
1313
1314        // High multiplier should amplify
1315        let burst = PackedSignal::pack(1, 128, 255); // mul code 7 → <<3
1316        let sv_burst = burst.shift_value();
1317        assert!(
1318            sv_burst > raw_mag,
1319            "high mul should amplify: shift={} vs raw={}",
1320            sv_burst,
1321            raw_mag
1322        );
1323    }
1324
1325    #[test]
1326    fn test_packed_quantization_roundtrip() {
1327        // Quantization is lossy but consistent
1328        let cases: &[(i8, u8, u8)] = &[
1329            (1, 0, 0),
1330            (1, 1, 1),
1331            (-1, 50, 30),
1332            (1, 200, 100),
1333            (1, 255, 255),
1334            (0, 128, 64),
1335        ];
1336        for &(pol, mag, mul) in cases {
1337            let packed = PackedSignal::pack(pol, mag, mul);
1338            // Polarity is always exact
1339            assert_eq!(
1340                packed.polarity(),
1341                pol,
1342                "polarity mismatch for ({}, {}, {})",
1343                pol,
1344                mag,
1345                mul
1346            );
1347            // Magnitude and multiplier are quantized — just check they're in LUT
1348            assert!(LOG_LUT.contains(&packed.magnitude()));
1349            assert!(LOG_LUT.contains(&packed.multiplier()));
1350        }
1351    }
1352
1353    #[test]
1354    fn test_packed_signal_conversion() {
1355        let signal = Signal::positive_amplified(200, 100);
1356        let packed = PackedSignal::from(signal);
1357        let back = Signal::from(packed);
1358
1359        // Polarity survives exactly
1360        assert_eq!(back.polarity, signal.polarity);
1361        // Magnitude and multiplier are quantized (lossy)
1362        assert!(LOG_LUT.contains(&back.magnitude));
1363        assert!(LOG_LUT.contains(&back.multiplier));
1364    }
1365
1366    #[test]
1367    fn test_packed_from_signal_method() {
1368        let s = Signal::negative(50);
1369        let p = PackedSignal::from_signal(&s);
1370        assert_eq!(p.polarity(), -1);
1371        assert!(p.is_negative());
1372
1373        let roundtrip = p.to_signal();
1374        assert_eq!(roundtrip.polarity, -1);
1375    }
1376
1377    #[test]
1378    fn test_packed_typed_polarity() {
1379        let p = PackedSignal::pack_typed(Polarity::Positive, 100, 64);
1380        assert_eq!(p.get_polarity(), Polarity::Positive);
1381
1382        let n = PackedSignal::pack_typed(Polarity::Negative, 100, 64);
1383        assert_eq!(n.get_polarity(), Polarity::Negative);
1384
1385        let z = PackedSignal::pack_typed(Polarity::Zero, 100, 64);
1386        assert_eq!(z.get_polarity(), Polarity::Zero);
1387    }
1388
1389    #[test]
1390    fn test_packed_display() {
1391        extern crate alloc;
1392        use alloc::format;
1393        assert_eq!(format!("{}", PackedSignal::ZERO), "0");
1394        assert_eq!(format!("{}", PackedSignal::MAX_POSITIVE), "+65025");
1395        assert_eq!(format!("{}", PackedSignal::MAX_NEGATIVE), "-65025");
1396    }
1397}