1#![no_std]
34
35#[cfg(feature = "serde")]
36use serde::{Deserialize, Serialize};
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47#[repr(i8)]
48pub enum Polarity {
49 Negative = -1,
51 #[default]
53 Zero = 0,
54 Positive = 1,
56}
57
58impl Polarity {
59 #[inline]
61 pub const fn as_i8(self) -> i8 {
62 self as i8
63 }
64
65 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124#[repr(C)]
125pub struct Signal {
126 pub polarity: i8,
128 pub magnitude: u8,
130 pub multiplier: u8,
135}
136
137impl Signal {
138 pub const ZERO: Self = Self {
142 polarity: 0,
143 magnitude: 0,
144 multiplier: 0,
145 };
146 pub const MAX_POSITIVE: Self = Self {
148 polarity: 1,
149 magnitude: 255,
150 multiplier: 255,
151 };
152 pub const MAX_NEGATIVE: Self = Self {
154 polarity: -1,
155 magnitude: 255,
156 multiplier: 255,
157 };
158
159 #[inline]
163 pub const fn effective_magnitude(&self) -> u16 {
164 self.magnitude as u16 * self.multiplier as u16
165 }
166
167 #[inline]
169 pub const fn current(&self) -> i32 {
170 self.polarity as i32 * self.effective_magnitude() as i32
171 }
172
173 pub const BASELINE_MULTIPLIER: u8 = 1;
175
176 #[inline]
180 pub const fn zero() -> Self {
181 Self::ZERO
182 }
183
184 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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 #[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 #[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 #[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 #[inline]
471 pub fn as_signed_i32(&self) -> i32 {
472 self.current()
473 }
474
475 #[inline]
477 pub fn magnitude_f32(&self) -> f32 {
478 self.magnitude as f32 / 255.0
479 }
480
481 #[inline]
483 pub fn effective_magnitude_f32(&self) -> f32 {
484 self.effective_magnitude() as f32 / 65025.0
485 }
486
487 #[inline]
489 pub fn as_signed_f32(&self) -> f32 {
490 self.polarity as f32 * self.effective_magnitude_f32()
491 }
492
493 #[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 #[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 #[inline]
519 pub fn get_polarity(&self) -> Polarity {
520 Polarity::from_i8(self.polarity).unwrap_or(Polarity::Zero)
521 }
522
523 #[inline]
527 pub fn is_active(&self) -> bool {
528 self.polarity != 0 && self.magnitude > 0
529 }
530
531 #[inline]
533 pub fn is_positive(&self) -> bool {
534 self.polarity > 0 && self.magnitude > 0
535 }
536
537 #[inline]
539 pub fn is_negative(&self) -> bool {
540 self.polarity < 0 && self.magnitude > 0
541 }
542
543 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
608 pub fn decayed(&self, retention: f32) -> Self {
609 let mut copy = *self;
610 copy.decay(retention);
611 copy
612 }
613
614 #[inline]
618 pub fn step_toward(&self, target: &Self) -> Self {
619 self.step_toward_by(target, 1)
620 }
621
622 #[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 #[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; if raw < 1 {
653 1
654 } else {
655 raw
656 }
657 };
658 self.step_toward_by(target, delta as u8)
659 }
660
661 #[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
672pub const LOG_LUT: [u8; 8] = [0, 1, 4, 16, 32, 64, 128, 255];
683
684const SHIFT_TABLE: [i8; 8] = [
689 0, -2, -1, 0, 0, 1, 2, 3, ];
698
699const 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#[inline]
724const fn quantize(v: u8) -> u8 {
725 if v == 0 {
726 return 0;
727 }
728 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#[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 pub const ZERO: Self = Self(0);
774
775 pub const MAX_POSITIVE: Self = Self(0b01_111_111);
777
778 pub const MAX_NEGATIVE: Self = Self(0b10_111_111);
780
781 #[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 #[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 #[inline]
804 pub const fn as_u8(self) -> u8 {
805 self.0
806 }
807
808 #[inline]
810 pub const fn from_raw(byte: u8) -> Self {
811 Self(byte)
812 }
813
814 #[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 #[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 #[inline]
838 pub const fn mag_code(self) -> u8 {
839 (self.0 >> 3) & 0x07
840 }
841
842 #[inline]
844 pub const fn mul_code(self) -> u8 {
845 self.0 & 0x07
846 }
847
848 #[inline]
850 pub const fn magnitude(self) -> u8 {
851 LOG_LUT[self.mag_code() as usize]
852 }
853
854 #[inline]
856 pub const fn multiplier(self) -> u8 {
857 LOG_LUT[self.mul_code() as usize]
858 }
859
860 #[inline]
868 pub const fn current(self) -> i32 {
869 CURRENT_LUT[self.0 as usize]
870 }
871
872 #[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 #[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 #[inline]
913 pub const fn is_positive(self) -> bool {
914 (self.0 >> 6) == 1 && ((self.0 >> 3) & 0x07) > 0
915 }
916
917 #[inline]
919 pub const fn is_negative(self) -> bool {
920 (self.0 >> 6) == 2 && ((self.0 >> 3) & 0x07) > 0
921 }
922
923 #[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 #[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 let cur = self.current();
964 if cur > 0 {
965 write!(f, "+{}", cur)
966 } else if cur < 0 {
967 write!(f, "{}", cur)
969 } else {
970 write!(f, "0")
971 }
972 }
973}
974
975impl 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#[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#[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); let less = amped.attenuate(50);
1086 assert_eq!(less.multiplier, 150); let maxed = sig.amplify(255);
1090 assert_eq!(maxed.multiplier, 255); let floored = sig.attenuate(255);
1093 assert_eq!(floored.multiplier, 0); }
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); }
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); }
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 assert_eq!(format!("{}", Signal::positive(42)), "+42"); assert_eq!(format!("{}", Signal::negative(10)), "-10"); assert_eq!(format!("{}", Signal::zero()), "0");
1180 assert_eq!(format!("{}", Signal::positive_amplified(10, 100)), "+1000");
1181 }
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 #[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 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 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 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 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 let quiet = PackedSignal::pack(1, 128, 1); 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 let burst = PackedSignal::pack(1, 128, 255); 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 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 assert_eq!(
1340 packed.polarity(),
1341 pol,
1342 "polarity mismatch for ({}, {}, {})",
1343 pol,
1344 mag,
1345 mul
1346 );
1347 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 assert_eq!(back.polarity, signal.polarity);
1361 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}