1use crate::prelude::*;
2
3#[derive(Clone, Debug, PartialEq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize))]
60pub struct RateEncoder {
61 base_rate: f32,
62 max_rate: f32,
63 range: (f32, f32),
64 dt_seconds: f32,
65 #[cfg_attr(feature = "serde", serde(rename = "accumulators"))]
70 phases: Vec<f64>,
71 #[cfg_attr(
73 feature = "serde",
74 serde(default, skip_serializing_if = "Vec::is_empty")
75 )]
76 pending_spikes: Vec<u64>,
77}
78
79impl RateEncoder {
80 pub const DEFAULT_DT_SECONDS: f32 = 0.1;
85
86 pub fn new(base_rate: f32, max_rate: f32, range: (f32, f32)) -> Self {
96 Self::try_new(base_rate, max_rate, range, Self::DEFAULT_DT_SECONDS)
97 .expect("invalid RateEncoder configuration")
98 }
99
100 pub fn try_new(
107 base_rate: f32,
108 max_rate: f32,
109 range: (f32, f32),
110 dt_seconds: f32,
111 ) -> Result<Self, EncoderError> {
112 crate::error::validate_non_negative_finite("base_rate", base_rate)?;
113 crate::error::validate_non_negative_finite("max_rate", max_rate)?;
114 if base_rate > max_rate {
115 return Err(EncoderError::RateOrder);
116 }
117 crate::error::validate_range_f32_span("range", range)?;
118 Self::validate_dt_seconds(dt_seconds)?;
119 Ok(Self {
120 base_rate,
121 max_rate,
122 range,
123 dt_seconds,
124 phases: Vec::new(),
125 pending_spikes: Vec::new(),
126 })
127 }
128
129 pub fn dt_seconds(&self) -> f32 {
131 self.dt_seconds
132 }
133
134 pub fn default_dt_seconds() -> f32 {
135 Self::DEFAULT_DT_SECONDS
136 }
137
138 fn validate_dt_seconds(dt_seconds: f32) -> Result<(), EncoderError> {
139 if dt_seconds.is_finite() && dt_seconds > 0.0 {
140 Ok(())
141 } else {
142 Err(EncoderError::NonPositiveOrNonFinite {
143 parameter: "dt_seconds",
144 })
145 }
146 }
147
148 fn normalize(&self, value: f32) -> f32 {
149 ((value - self.range.0) / (self.range.1 - self.range.0)).clamp(0.0, 1.0)
150 }
151
152 fn effective_rate_hz(&self, value: f32, rate_scale: f32) -> f32 {
158 if !value.is_finite() {
159 return 0.0;
160 }
161 let normalized = f64::from(self.normalize(value));
162 let base = f64::from(self.base_rate)
163 + normalized * (f64::from(self.max_rate) - f64::from(self.base_rate));
164 let rate = base * f64::from(rate_scale);
165 if !rate.is_finite() {
167 return if rate > 0.0 { f32::MAX } else { 0.0 };
168 }
169 rate.clamp(0.0, f64::from(f32::MAX)) as f32
170 }
171
172 fn ensure_accumulators(&mut self, num_channels: usize) {
173 if self.phases.len() < num_channels {
174 self.phases.resize(num_channels, 0.0);
175 self.pending_spikes.resize(num_channels, 0);
176 }
177 }
178
179 fn split_whole_and_frac(value: f64) -> (u64, f64) {
181 debug_assert!(value.is_finite() && value >= 0.0);
182 if value < 1.0 {
183 return (0, value);
184 }
185 if value >= u64::MAX as f64 {
186 return (u64::MAX, 0.0);
187 }
188 let whole = value.trunc() as u64;
189 let frac = (value - whole as f64).clamp(0.0, 1.0 - f64::EPSILON);
190 (whole, frac)
191 }
192
193 fn apply_streaming_increment(&mut self, channel_idx: usize, increment: f64) {
195 if increment <= 0.0 {
196 return;
197 }
198 let sum = (self.phases[channel_idx] + increment).min(u64::MAX as f64);
200 let (whole, frac) = Self::split_whole_and_frac(sum);
201 self.pending_spikes[channel_idx] = self.pending_spikes[channel_idx].saturating_add(whole);
202 self.phases[channel_idx] = frac;
203 }
204
205 fn encode_with_rate_scale(&mut self, input: &[f32], rate_scale: f32) -> EncodedOutput {
206 let mut output = EncodedOutput::new();
207 if input.is_empty() {
208 return output;
209 }
210 if !rate_scale.is_finite() || rate_scale <= 0.0 {
213 return output;
214 }
215
216 let mut rng = rand::rng();
217 for (i, &value) in input.iter().enumerate() {
218 let Ok(channel) = u16::try_from(i) else {
219 break;
221 };
222 let rate = self.effective_rate_hz(value, rate_scale);
223 let probability = crate::poisson::probability_from_rate_hz(rate, self.dt_seconds);
224
225 if crate::rng::gen_unit_f32_with_rng(&mut rng) < probability {
226 output.spikes.push(SpikeEvent {
227 channel,
228 timestamp: 0,
229 polarity: true,
230 });
231 }
232 }
233
234 output
235 }
236
237 const MAX_SPIKES_PER_CHANNEL_PER_STEP: usize = 1024;
245
246 fn streaming_increment(&self, value: f32, rate_scale: f32) -> f64 {
251 let rate_hz = self.effective_rate_hz(value, rate_scale);
252 if rate_hz <= 0.0 {
253 return 0.0;
254 }
255 (f64::from(rate_hz) * f64::from(self.dt_seconds)).clamp(0.0, u64::MAX as f64)
257 }
258
259 fn emit_capped_channel_spikes(
260 &mut self,
261 channel: u16,
262 channel_idx: usize,
263 output: &mut EncodedOutput,
264 ) {
265 let pending = self.pending_spikes[channel_idx];
266 if pending == 0 {
267 return;
268 }
269 let emit = pending.min(Self::MAX_SPIKES_PER_CHANNEL_PER_STEP as u64) as usize;
270 for _ in 0..emit {
271 output.spikes.push(SpikeEvent {
272 channel,
273 timestamp: 0,
274 polarity: true,
275 });
276 }
277 self.pending_spikes[channel_idx] = pending - emit as u64;
278 }
280
281 fn rate_scale_is_active(rate_scale: f32) -> bool {
282 rate_scale.is_finite() && rate_scale > 0.0
283 }
284
285 fn encode_step_with_rate_scale(&mut self, input: &[f32], rate_scale: f32) -> EncodedOutput {
286 let mut output = EncodedOutput::new();
287 if input.is_empty() {
288 return output;
289 }
290
291 self.ensure_accumulators(input.len());
292 let active = Self::rate_scale_is_active(rate_scale);
293
294 for (i, &value) in input.iter().enumerate() {
295 let Ok(channel) = u16::try_from(i) else {
296 break;
297 };
298 if !active {
299 self.pending_spikes[i] = 0;
302 self.phases[i] = 0.0;
303 continue;
304 }
305 let increment = self.streaming_increment(value, rate_scale);
306 self.apply_streaming_increment(i, increment);
307 self.emit_capped_channel_spikes(channel, i, &mut output);
308 }
309
310 output
311 }
312
313 pub fn encode_with_modulators(
317 &mut self,
318 input: &[f32],
319 modulators: &NeuroModulators,
320 gain_curves: &NeuromodulatorGainCurves,
321 ) -> EncodedOutput {
322 <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
323 }
324
325 pub fn encode_step_with_modulators(
329 &mut self,
330 input: &[f32],
331 modulators: &NeuroModulators,
332 gain_curves: &NeuromodulatorGainCurves,
333 ) -> EncodedOutput {
334 <Self as ModulatedEncoder>::encode_step_with_modulators(
335 self,
336 input,
337 modulators,
338 gain_curves,
339 )
340 }
341}
342
343#[cfg(feature = "serde")]
344impl<'de> serde::Deserialize<'de> for RateEncoder {
345 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
346 where
347 D: serde::Deserializer<'de>,
348 {
349 #[derive(serde::Deserialize)]
350 struct Helper {
351 base_rate: f32,
352 max_rate: f32,
353 range: (f32, f32),
354 #[serde(default = "RateEncoder::default_dt_seconds")]
355 dt_seconds: f32,
356 #[serde(default)]
358 accumulators: Vec<f64>,
359 #[serde(default)]
362 pending_spikes: Vec<u64>,
363 }
364
365 let helper = Helper::deserialize(deserializer)?;
366 let mut encoder = Self::try_new(
367 helper.base_rate,
368 helper.max_rate,
369 helper.range,
370 helper.dt_seconds,
371 )
372 .map_err(serde::de::Error::custom)?;
373 if helper
376 .accumulators
377 .iter()
378 .any(|value| !value.is_finite() || *value < 0.0)
379 {
380 return Err(serde::de::Error::custom(
381 "accumulators must be finite and non-negative",
382 ));
383 }
384 let n = helper.accumulators.len().max(helper.pending_spikes.len());
385 encoder.phases = vec![0.0; n];
386 encoder.pending_spikes = vec![0; n];
387 for i in 0..n {
388 let combined = helper.accumulators.get(i).copied().unwrap_or(0.0);
389 let (whole_from_acc, phase) = Self::split_whole_and_frac(combined);
390 let pending = helper.pending_spikes.get(i).copied().unwrap_or(0);
391 encoder.phases[i] = phase;
392 encoder.pending_spikes[i] = pending.saturating_add(whole_from_acc);
393 }
394 Ok(encoder)
395 }
396}
397
398impl Encoder for RateEncoder {
399 fn encode(&mut self, input: &[f32]) -> EncodedOutput {
400 self.encode_with_rate_scale(input, 1.0)
401 }
402
403 fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
404 self.encode_step_with_rate_scale(input, 1.0)
405 }
406
407 fn reset(&mut self) {
408 self.phases.fill(0.0);
409 self.pending_spikes.fill(0);
410 }
411}
412
413impl ModulatedEncoder for RateEncoder {
414 fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
415 self.encode_with_rate_scale(input, gains.sanitize().firing_rate_scale)
416 }
417
418 fn encode_step_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
419 self.encode_step_with_rate_scale(input, gains.sanitize().firing_rate_scale)
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_rate_encoder_basic() {
429 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 100.0));
430 let input = [0.0, 50.0, 100.0];
431 let output = encoder.encode(&input);
432 assert!(output.spikes.len() <= 3);
433 }
434
435 #[test]
436 fn test_rate_encoder_encode_step() {
437 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
438 let output = encoder.encode_step(&[1.0]);
440 assert_eq!(output.spikes.len(), 1);
441
442 let output2 = encoder.encode_step(&[0.5]);
443 assert_eq!(output2.spikes.len(), 0);
445 let output3 = encoder.encode_step(&[0.5]);
446 assert_eq!(output3.spikes.len(), 1);
448 }
449
450 #[test]
451 fn test_rate_encoder_empty_input() {
452 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 100.0));
453 let input: [f32; 0] = [];
454 let output = encoder.encode(&input);
455 assert_eq!(output.spikes.len(), 0);
456 let output_step = encoder.encode_step(&input);
457 assert_eq!(output_step.spikes.len(), 0);
458 }
459
460 #[test]
461 fn test_rate_encoder_single_channel() {
462 let mut encoder = RateEncoder::new(5.0, 10.0, (0.0, 1.0));
463 let input = [0.5];
464 let output = encoder.encode(&input);
465 assert!(output.spikes.len() <= 1);
466 }
467
468 #[test]
469 fn test_rate_encoder_below_min() {
470 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 100.0));
471 let input = [-50.0, -100.0, -1.0];
472 let output = encoder.encode(&input);
473 assert!(
474 output.spikes.is_empty(),
475 "Below-min inputs should produce no spikes"
476 );
477 }
478
479 #[test]
480 fn test_rate_encoder_above_max() {
481 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 100.0));
482 let input = [150.0, 200.0, 101.0];
483 let output = encoder.encode(&input);
484 assert!(output.spikes.len() <= 3);
485 for spike in &output.spikes {
486 assert!(u32::from(spike.channel) < 3);
487 }
488 }
489
490 #[test]
491 fn test_rate_encoder_reset_does_not_panic() {
492 let mut encoder = RateEncoder::new(5.0, 10.0, (0.0, 1.0));
493 let input = [0.5; 10];
494 encoder.encode(&input);
495 encoder.reset();
496 encoder.encode(&input);
497 }
498
499 #[test]
500 fn test_rate_encoder_never_panics() {
501 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 100.0));
502 let inputs: [&[f32]; 4] = [&[], &[0.0], &[50.0, 100.0], &[f32::MIN, f32::MAX]];
503 for input in inputs {
504 let result =
505 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| encoder.encode(input)));
506 assert!(result.is_ok());
507 }
508 }
509
510 #[test]
511 fn test_rate_encoder_modulated_step_scales_firing_rate() {
512 let mut encoder = RateEncoder::new(0.0, 5.0, (0.0, 1.0));
513 let modulators = NeuroModulators {
514 dopamine: 1.0,
515 ..Default::default()
516 };
517 let gain_curves = NeuromodulatorGainCurves {
518 dopamine: ModulatorGainCurves {
519 firing_rate: Some(GainCurve::new((0.0, 1.0), (1.0, 2.0))),
520 ..Default::default()
521 },
522 ..Default::default()
523 };
524
525 let baseline = encoder.encode_step(&[1.0]);
526 assert!(baseline.spikes.is_empty());
527
528 encoder.reset();
529
530 let boosted = encoder.encode_step_with_modulators(&[1.0], &modulators, &gain_curves);
531 assert_eq!(boosted.spikes.len(), 1);
532 }
533
534 #[test]
535 fn test_rate_encoder_encode_with_modulators() {
536 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
537 let modulators = NeuroModulators {
538 dopamine: 1.0,
539 ..Default::default()
540 };
541 let gain_curves = NeuromodulatorGainCurves {
542 dopamine: ModulatorGainCurves {
543 firing_rate: Some(GainCurve::new((0.0, 1.0), (1.0, 2.0))),
544 ..Default::default()
545 },
546 ..Default::default()
547 };
548
549 let boosted = encoder.encode_step_with_modulators(&[1.0], &modulators, &gain_curves);
554 assert_eq!(boosted.spikes.len(), 2);
555 assert!(boosted.spikes.iter().all(|s| s.channel == 0));
556
557 let mut baseline = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
559 let identity = baseline.encode_step_with_modulators(
560 &[1.0],
561 &NeuroModulators::default(),
562 &NeuromodulatorGainCurves::default(),
563 );
564 assert_eq!(identity.spikes.len(), 1);
565 }
566
567 #[test]
568 fn test_rate_encoder_step_shorter_input() {
569 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
570 let _ = encoder.encode_step(&[0.0, 0.0]);
573 let output = encoder.encode_step(&[1.0]);
574 assert_eq!(output.spikes.len(), 1);
575 let quiet = encoder.encode_step(&[0.0, 0.0]);
578 assert!(quiet.spikes.is_empty());
579 }
580
581 #[test]
582 fn test_rate_encoder_zero_rate_scale_never_accumulates() {
583 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
584 for _ in 0..10_000 {
585 let output = encoder.encode_step_with_rate_scale(&[1.0], 0.0);
586 assert!(
587 output.spikes.is_empty(),
588 "zero firing-rate scale must fully silence streaming output"
589 );
590 }
591 }
592
593 #[test]
594 fn test_rate_encoder_non_finite_rate_scale_silences() {
595 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
596 for scale in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
597 let batch = encoder.encode_with_rate_scale(&[1.0], scale);
598 assert!(
599 batch.spikes.is_empty(),
600 "non-finite/negative rate_scale ({scale}) must silence batch encode"
601 );
602 let step = encoder.encode_step_with_rate_scale(&[1.0], scale);
603 assert!(
604 step.spikes.is_empty(),
605 "non-finite/negative rate_scale ({scale}) must silence streaming encode"
606 );
607 }
608 encoder.reset();
610 let recovered = encoder.encode_step_with_rate_scale(&[1.0], 1.0);
611 assert_eq!(recovered.spikes.len(), 1);
612 }
613
614 #[test]
615 fn test_rate_encoder_try_new_validation() {
616 let dt = RateEncoder::DEFAULT_DT_SECONDS;
617 assert_eq!(
618 RateEncoder::try_new(f32::NAN, 1.0, (0.0, 1.0), dt).err(),
619 Some(EncoderError::NonNegativeFinite {
620 parameter: "base_rate"
621 })
622 );
623 assert_eq!(
624 RateEncoder::try_new(0.0, f32::INFINITY, (0.0, 1.0), dt).err(),
625 Some(EncoderError::NonNegativeFinite {
626 parameter: "max_rate"
627 })
628 );
629 assert_eq!(
630 RateEncoder::try_new(-5.0, 10.0, (0.0, 1.0), dt).err(),
631 Some(EncoderError::NonNegativeFinite {
632 parameter: "base_rate"
633 })
634 );
635 assert_eq!(
636 RateEncoder::try_new(2.0, 1.0, (0.0, 1.0), dt).err(),
637 Some(EncoderError::RateOrder)
638 );
639 assert_eq!(
640 RateEncoder::try_new(0.0, 1.0, (1.0, 1.0), dt).err(),
641 Some(EncoderError::InvalidRange { parameter: "range" })
642 );
643 assert_eq!(
644 RateEncoder::try_new(0.0, 1.0, (f32::MIN, f32::MAX), dt).err(),
645 Some(EncoderError::InvalidRange { parameter: "range" })
646 );
647 assert_eq!(
648 RateEncoder::try_new(0.0, 10.0, (0.0, 1.0), 0.0).err(),
649 Some(EncoderError::NonPositiveOrNonFinite {
650 parameter: "dt_seconds"
651 })
652 );
653 }
654
655 #[test]
656 fn test_rate_encoder_try_new_validates_dt_seconds() {
657 assert!(RateEncoder::try_new(0.0, 10.0, (0.0, 1.0), 0.001).is_ok());
658 for dt in [0.0, -0.001, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
659 assert!(
660 RateEncoder::try_new(0.0, 10.0, (0.0, 1.0), dt).is_err(),
661 "dt_seconds={dt:?} should be rejected"
662 );
663 }
664 }
665
666 #[cfg(feature = "serde")]
667 #[test]
668 fn test_rate_encoder_serde_rejects_out_of_range_accumulators() {
669 let backlog = r#"{"base_rate":0.0,"max_rate":10.0,"range":[0.0,1.0],"accumulators":[5.0]}"#;
671 let res: Result<RateEncoder, _> = serde_json::from_str(backlog);
672 assert!(res.is_ok());
673
674 let negative =
675 r#"{"base_rate":0.0,"max_rate":10.0,"range":[0.0,1.0],"accumulators":[-0.1]}"#;
676 let res: Result<RateEncoder, _> = serde_json::from_str(negative);
677 assert!(res.is_err());
678
679 let non_finite =
680 r#"{"base_rate":0.0,"max_rate":10.0,"range":[0.0,1.0],"accumulators":[null]}"#;
681 let res: Result<RateEncoder, _> = serde_json::from_str(non_finite);
683 assert!(res.is_err());
684
685 let ok = r#"{"base_rate":0.0,"max_rate":10.0,"range":[0.0,1.0],"dt_seconds":0.1,"accumulators":[0.5]}"#;
686 let res: Result<RateEncoder, _> = serde_json::from_str(ok);
687 assert!(res.is_ok());
688 }
689
690 #[test]
691 fn test_rate_encoder_large_backlog_drains_exactly() {
692 let mut encoder = RateEncoder::try_new(0.0, 20_000_000.0, (0.0, 1.0), 1.0).unwrap();
694 let first = encoder.encode_step(&[1.0]);
695 assert_eq!(
696 first.spikes.len(),
697 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
698 );
699 let second = encoder.encode_step(&[0.0]);
701 assert_eq!(
702 second.spikes.len(),
703 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
704 );
705 let mut total = first.spikes.len() + second.spikes.len();
707 for _ in 0..10 {
708 total += encoder.encode_step(&[0.0]).spikes.len();
709 }
710 assert!(
711 total > RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP * 2,
712 "backlog should keep draining across steps, total={total}"
713 );
714 }
715
716 #[test]
717 fn test_rate_encoder_backlog_drains_above_f64_precision() {
718 #[cfg(feature = "serde")]
725 {
726 let seeded: RateEncoder = serde_json::from_str(
727 r#"{"base_rate":0.0,"max_rate":1.0,"range":[0.0,1.0],"dt_seconds":1.0,"accumulators":[2500.0]}"#,
728 )
729 .unwrap();
730 let mut encoder = seeded;
731 let first = encoder.encode_step(&[0.0]);
732 assert_eq!(
733 first.spikes.len(),
734 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
735 );
736 let second = encoder.encode_step(&[0.0]);
737 assert_eq!(
738 second.spikes.len(),
739 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
740 );
741 let third = encoder.encode_step(&[0.0]);
743 assert_eq!(third.spikes.len(), 452);
744 let fourth = encoder.encode_step(&[0.0]);
745 assert!(
746 fourth.spikes.is_empty(),
747 "backlog must fully drain rather than emit forever"
748 );
749 }
750
751 let mut encoder = RateEncoder::try_new(0.0, 1.0e16, (0.0, 1.0), 1.0).unwrap();
753 let first = encoder.encode_step(&[1.0]);
754 assert_eq!(
755 first.spikes.len(),
756 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
757 );
758 let before = encoder.pending_spikes[0];
760 assert!(
761 before > RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP as u64,
762 "expected a large exact backlog, got {before}"
763 );
764 let quiet = encoder.encode_step(&[0.0]);
765 assert_eq!(
766 quiet.spikes.len(),
767 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
768 );
769 assert_eq!(
770 encoder.pending_spikes[0],
771 before - RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP as u64,
772 "u64 pending must decrement exactly past the f64 precision cliff"
773 );
774 }
775
776 #[test]
777 fn test_rate_encoder_streaming_bounds_extreme_dt() {
778 let mut encoder = RateEncoder::try_new(0.0, 10.0, (0.0, 1.0), f32::MAX).unwrap();
782 let output = encoder.encode_step(&[1.0]);
783 assert_eq!(
784 output.spikes.len(),
785 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
786 );
787
788 let mut encoder = RateEncoder::try_new(0.0, 1.0e6, (0.0, 1.0), 1.0).unwrap();
790 let output = encoder.encode_step(&[1.0]);
791 assert_eq!(
792 output.spikes.len(),
793 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
794 );
795 let next = encoder.encode_step(&[0.0]);
797 assert_eq!(
798 next.spikes.len(),
799 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
800 );
801 }
802
803 #[test]
804 fn test_rate_encoder_nan_input_is_silent() {
805 let mut encoder = RateEncoder::try_new(0.0, 10.0, (0.0, 1.0), 0.1).unwrap();
806 let batch = encoder.encode(&[f32::NAN]);
807 assert!(
808 batch.spikes.is_empty(),
809 "NaN sensor values must not map to max-rate / p≈1"
810 );
811 let step = encoder.encode_step(&[f32::NAN]);
812 assert!(step.spikes.is_empty());
813 assert_eq!(
814 encoder.pending_spikes.first().copied().unwrap_or(0),
815 0,
816 "NaN must not seed a huge pending backlog"
817 );
818 let ok = encoder.encode_step(&[1.0]);
820 assert_eq!(ok.spikes.len(), 1);
821 }
822
823 #[test]
824 fn test_rate_encoder_rate_dt_product_saturates_not_silent() {
825 let mut encoder = RateEncoder::try_new(0.0, 1.0e38, (0.0, 1.0), 10.0).unwrap();
828 let first = encoder.encode_step(&[1.0]);
829 assert_eq!(
830 first.spikes.len(),
831 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP,
832 "rate×dt f32 overflow must not silence streaming"
833 );
834 assert!(
835 encoder.pending_spikes[0] > 0,
836 "expected a queued backlog after the per-step cap"
837 );
838 }
839
840 #[test]
841 fn test_rate_encoder_inactive_gain_clears_pending() {
842 let mut encoder = RateEncoder::try_new(0.0, 1.0e6, (0.0, 1.0), 1.0).unwrap();
843 let first = encoder.encode_step(&[1.0]);
844 assert_eq!(
845 first.spikes.len(),
846 RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
847 );
848 assert!(encoder.pending_spikes[0] > 0);
849 let silenced = encoder.encode_step_with_rate_scale(&[0.0], 0.0);
851 assert!(silenced.spikes.is_empty());
852 assert_eq!(encoder.pending_spikes[0], 0);
853 assert_eq!(encoder.phases[0], 0.0);
854 let quiet = encoder.encode_step(&[0.0]);
856 assert!(quiet.spikes.is_empty());
857 }
858
859 #[test]
860 fn test_rate_encoder_default_dt_preserves_streaming_compatibility() {
861 let mut encoder = RateEncoder::new(0.0, 10.0, (0.0, 1.0));
862 assert_eq!(encoder.dt_seconds(), RateEncoder::DEFAULT_DT_SECONDS);
863 assert_eq!(encoder.encode_step(&[1.0]).spikes.len(), 1);
864 }
865
866 #[test]
867 fn test_rate_encoder_overflowed_gain_rate_saturates_not_silent() {
868 let mut encoder = RateEncoder::try_new(0.0, 1.0e35, (0.0, 1.0), 0.01).unwrap();
872 let gains = EncodingGains {
873 firing_rate_scale: 1.0e4,
874 ..Default::default()
875 };
876 let mut spikes = 0usize;
877 for _ in 0..32 {
878 spikes += encoder.encode_with_gains(&[1.0], gains).spikes.len();
879 }
880 assert!(
881 spikes >= 28,
882 "overflowed modulated rate should saturate near p=1, got {spikes}/32 spikes"
883 );
884 }
885
886 #[test]
887 fn test_rate_encoder_streaming_uses_hz_times_dt() {
888 let cases = [(5.0, 0.2, 10), (20.0, 0.05, 20), (7.5, 0.1, 40)];
889 for (rate_hz, dt_seconds, steps) in cases {
890 let mut encoder = RateEncoder::try_new(0.0, rate_hz, (0.0, 1.0), dt_seconds).unwrap();
891 let spikes: usize = (0..steps)
892 .map(|_| encoder.encode_step(&[1.0]).spikes.len())
893 .sum();
894 let elapsed_seconds = dt_seconds * steps as f32;
895 let observed_hz = spikes as f32 / elapsed_seconds;
896 assert!(
897 (observed_hz - rate_hz).abs() <= 1.0 / elapsed_seconds,
898 "rate_hz={rate_hz}, dt={dt_seconds}, observed={observed_hz}"
899 );
900 }
901 }
902
903 #[test]
904 fn test_rate_encoder_stochastic_mean_matches_poisson_probability() {
905 let cases = [(2.0, 0.01), (10.0, 0.005), (25.0, 0.002)];
906 let trials = 50_000;
907 for (rate_hz, dt_seconds) in cases {
908 let mut encoder = RateEncoder::try_new(0.0, rate_hz, (0.0, 1.0), dt_seconds).unwrap();
909 let spikes: usize = (0..trials)
910 .map(|_| encoder.encode(&[1.0]).spikes.len())
911 .sum();
912 let observed_probability = spikes as f32 / trials as f32;
913 let expected_probability =
914 crate::poisson::probability_from_rate_hz(rate_hz, dt_seconds);
915 assert!(
916 (observed_probability - expected_probability).abs() < 0.01,
917 "rate_hz={rate_hz}, dt={dt_seconds}, observed_p={observed_probability}, expected_p={expected_probability}"
918 );
919 }
920 }
921}
922
923#[cfg(test)]
929mod property_tests {
930 use super::*;
931 use crate::encoders::property_support::{
932 TRIALS, assert_unique_channel_spikes, sample_gain_scale, sample_input_value,
933 sample_positive_finite, scale_is_inactive,
934 };
935 use rand::rngs::StdRng;
936 use rand::{RngExt, SeedableRng};
937
938 const SEED: u64 = 0xAE69_0001;
940
941 fn sample_valid_encoder(rng: &mut StdRng) -> RateEncoder {
942 loop {
943 let base = sample_positive_finite(rng) * rng.random::<f32>();
944 let max = base + sample_positive_finite(rng);
945 let lo = rng.random_range(-50.0_f32..50.0);
946 let hi = lo + sample_positive_finite(rng);
947 let dt = sample_positive_finite(rng).clamp(1e-4, 1.0);
948 if let Ok(enc) = RateEncoder::try_new(base, max, (lo, hi), dt) {
949 return enc;
950 }
951 }
952 }
953
954 fn sample_input_vec(rng: &mut StdRng, n: usize, range: (f32, f32)) -> Vec<f32> {
955 (0..n).map(|_| sample_input_value(rng, range)).collect()
956 }
957
958 fn assert_both_silent(trial: usize, batch: &EncodedOutput, step: &EncodedOutput, why: &str) {
959 assert!(
960 batch.spikes.is_empty() && step.spikes.is_empty(),
961 "trial {trial}: {why}"
962 );
963 }
964
965 fn assert_active_batch_bounds(trial: usize, batch: &EncodedOutput, n_channels: usize) {
966 assert!(
967 batch.spikes.len() <= n_channels,
968 "trial {trial}: batch spikes {} > channels {n_channels}",
969 batch.spikes.len()
970 );
971 assert_unique_channel_spikes(&batch.spikes, n_channels);
972 }
973
974 fn assert_active_step_bounds(trial: usize, step: &EncodedOutput, n_channels: usize) {
975 let max_step = RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP.saturating_mul(n_channels);
976 assert!(
977 step.spikes.len() <= max_step,
978 "trial {trial}: step spikes {} exceed bound {max_step}",
979 step.spikes.len()
980 );
981 for spike in &step.spikes {
982 assert!((spike.channel as usize) < n_channels);
983 assert!(spike.polarity);
984 }
985 }
986
987 #[test]
988 fn prop_rate_silence_and_channel_bounds() {
989 let mut rng = StdRng::seed_from_u64(SEED);
990 for trial in 0..TRIALS {
991 let mut encoder = sample_valid_encoder(&mut rng);
992 let n = rng.random_range(0usize..=8);
993 let input = sample_input_vec(&mut rng, n, (0.0, 1.0));
994 let scale = sample_gain_scale(&mut rng);
995
996 let batch = encoder.encode_with_rate_scale(&input, scale);
997 let step = encoder.encode_step_with_rate_scale(&input, scale);
998
999 if input.is_empty() {
1000 assert_both_silent(
1001 trial,
1002 &batch,
1003 &step,
1004 "empty input must silence batch and step",
1005 );
1006 continue;
1007 }
1008 if scale_is_inactive(scale) {
1009 assert_both_silent(
1010 trial,
1011 &batch,
1012 &step,
1013 &format!("inactive rate_scale={scale:?} must silence"),
1014 );
1015 continue;
1016 }
1017
1018 assert_active_batch_bounds(trial, &batch, input.len());
1019 assert_active_step_bounds(trial, &step, input.len());
1020 if input.iter().all(|v| !v.is_finite()) {
1021 assert!(
1022 batch.spikes.is_empty(),
1023 "trial {trial}: all non-finite inputs must silence batch"
1024 );
1025 }
1026 }
1027 }
1028
1029 #[test]
1030 fn prop_rate_probability_stays_in_unit_interval() {
1031 let mut rng = StdRng::seed_from_u64(SEED ^ 0xB0B5);
1032 for _ in 0..TRIALS {
1033 let rate = match rng.random_range(0u8..8) {
1034 0 => 0.0,
1035 1 => -1.0,
1036 2 => f32::NAN,
1037 3 => f32::INFINITY,
1038 4 => f32::MAX,
1039 _ => sample_positive_finite(&mut rng) * rng.random_range(0.0_f32..100.0),
1040 };
1041 let dt = match rng.random_range(0u8..6) {
1042 0 => 0.0,
1043 1 => -0.1,
1044 2 => f32::NAN,
1045 3 => f32::INFINITY,
1046 _ => sample_positive_finite(&mut rng).clamp(1e-6, 2.0),
1047 };
1048 let p = crate::poisson::probability_from_rate_hz(rate, dt);
1049 assert!(
1050 p.is_finite() && (0.0..=1.0).contains(&p),
1051 "probability_from_rate_hz({rate}, {dt}) = {p}"
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn prop_rate_encode_never_panics_on_sampled_inputs() {
1058 let mut rng = StdRng::seed_from_u64(SEED ^ 0xBAD5);
1059 for _ in 0..TRIALS {
1060 let mut encoder = sample_valid_encoder(&mut rng);
1061 let n = rng.random_range(0usize..=16);
1062 let input = sample_input_vec(&mut rng, n, (-10.0, 10.0));
1063 let scale = sample_gain_scale(&mut rng);
1064 let _ = encoder.encode_with_rate_scale(&input, scale);
1065 let _ = encoder.encode_step_with_rate_scale(&input, scale);
1066 encoder.reset();
1067 let _ = encoder.encode(&input);
1068 let _ = encoder.encode_step(&input);
1069 }
1070 }
1071}