Skip to main content

axon_encoder/encoders/
rate.rs

1use crate::prelude::*;
2
3/// Encodes analog values as spike rates based on input intensity.
4///
5/// Each input channel is mapped to a firing rate between `base_rate` and `max_rate`.
6/// In batch mode (`encode`), each call generates independent probabilistic spikes.
7/// In streaming mode (`encode_step`), accumulates expected spikes and fires deterministically
8/// when the accumulated value exceeds a threshold per channel.
9///
10/// # Mathematical Model
11///
12/// For batch encoding:
13/// ```text
14/// rate_hz = base_rate + normalized * (max_rate - base_rate)
15/// probability = 1 - exp(-rate_hz * dt_seconds)
16/// spike if random() < probability
17/// ```
18///
19/// For streaming (`encode_step`):
20/// ```text
21/// rate_hz = base_rate + normalized[i] * (max_rate - base_rate)
22/// accumulator[i] += rate_hz * dt_seconds
23/// spike if accumulator[i] >= 1.0 (then accumulator -= 1.0)
24/// ```
25///
26/// # When to Use
27///
28/// - Converting continuous sensor values to spike rates
29/// - Poisson-like spike generation with controllable average rates
30/// - Real-time encoding where spike timing follows input intensity
31///
32/// # Parameters
33///
34/// - `base_rate`: Minimum firing rate in hertz (Hz) when input is at range minimum
35/// - `max_rate`: Maximum firing rate in hertz (Hz) when input is at range maximum
36/// - `range`: Tuple of (min, max) input values
37/// - `dt_seconds`: Duration, in seconds, represented by each encode step
38///
39/// # Migration
40///
41/// [`RateEncoder::new`] keeps the previous constructor shape and uses
42/// `dt_seconds = 0.1`, which preserves the old deterministic `/ 10.0`
43/// increment for unit rates. Prefer [`RateEncoder::try_new`] for new code that
44/// wants explicit time-step configuration and validation.
45///
46/// # Examples
47///
48/// ```rust
49/// use axon_encoder::prelude::*;
50/// # fn main() -> Result<(), EncoderError> {
51/// let mut enc = RateEncoder::try_new(5.0, 100.0, (0.0, 1.0), 0.010)?;
52/// let out = enc.encode(&[0.0, 0.5, 1.0]);
53/// // Stochastic batch mode: at most one spike per channel.
54/// assert!(out.spikes.len() <= 3);
55/// # Ok(())
56/// # }
57/// ```
58#[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    /// Fractional phase per channel, kept in `[0, 1)`.
66    ///
67    /// Serialized as `accumulators` for backward compatibility with earlier
68    /// checkpoints that stored a single combined float per channel.
69    #[cfg_attr(feature = "serde", serde(rename = "accumulators"))]
70    phases: Vec<f64>,
71    /// Exact whole-spike backlog per channel (drainable past f64's `2^53` cliff).
72    #[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    /// Compatibility time step used by [`RateEncoder::new`].
81    ///
82    /// A 100 ms step makes `rate_hz * dt_seconds` equal to the previous
83    /// deterministic `/ 10.0` increment for the same rate value.
84    pub const DEFAULT_DT_SECONDS: f32 = 0.1;
85
86    /// Creates a rate encoder with the compatibility `dt_seconds = 0.1`.
87    ///
88    /// Prefer [`RateEncoder::try_new`] when selecting an explicit sampling
89    /// interval for new code.
90    ///
91    /// # Panics
92    ///
93    /// Panics if rates or range are invalid (`dt_seconds` is always the valid
94    /// default `0.1`, so callers cannot panic via the time step).
95    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    /// Creates a rate encoder with an explicit time step in seconds.
101    ///
102    /// Rates must be finite and non-negative with `base_rate <= max_rate`.
103    /// `dt_seconds` must be finite and strictly positive. Range must be a
104    /// non-degenerate finite f32 span (bounds finite, ordered, and
105    /// `max - min` finite in f32).
106    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    /// Returns the configured time step in seconds.
130    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    /// Effective firing rate in Hz after range mapping and gain scale.
153    ///
154    /// Always returns a finite non-negative `f32`: non-finite inputs silence,
155    /// and positive overflow saturates to `f32::MAX` (so batch encoding does not
156    /// treat strong modulated rates as silent via non-finite rejection).
157    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        // NaN comparisons are false → 0.0; +∞ > 0 → MAX; finite values clamp.
166        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    /// Split a finite non-negative value into whole spikes + fractional phase.
180    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    /// Apply a finite non-negative expected-spike increment to channel state.
194    fn apply_streaming_increment(&mut self, channel_idx: usize, increment: f64) {
195        if increment <= 0.0 {
196            return;
197        }
198        // Cap so phase + increment stays finite and within the u64 backlog range.
199        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        // Match PopulationEncoder: non-finite or non-positive scales fully silence.
211        // Avoids NaN probabilities that would silently never spike.
212        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                // Remaining channels exceed u16::MAX; stop rather than wrap.
220                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    /// Cap on spikes emitted per channel per streaming step.
238    ///
239    /// Bounds allocation when `rate_hz * dt_seconds` is huge. Remaining whole
240    /// spikes stay in the exact `u64` pending queue and drain on later
241    /// `encode_step` calls (no permanent loss of expected spike count, and no
242    /// stall past f64's `2^53` integer cliff where `acc -= 1.0` would no-op).
243    /// Non-finite increments are skipped so the emission loop always terminates.
244    const MAX_SPIKES_PER_CHANNEL_PER_STEP: usize = 1024;
245
246    /// Expected spikes for one streaming step (`rate_hz * dt_seconds` in f64).
247    ///
248    /// Always finite and non-negative; saturates at `u64::MAX`. Using f64 avoids
249    /// silent drops when the f32 product would overflow to `+inf`.
250    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        // rate_hz and dt_seconds are finite → product is finite in f64.
256        (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        // Any remaining whole spikes stay queued for subsequent steps.
279    }
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                // Documented silence (`firing_rate_scale = 0`): no spikes and
300                // drop backlog so a later non-zero gain cannot burst old debt.
301                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    /// Encodes input using neuromodulator-driven gain curves.
314    ///
315    /// Inherent wrapper so callers need not import [`ModulatedEncoder`].
316    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    /// Step-wise variant of [`encode_with_modulators`](Self::encode_with_modulators).
326    ///
327    /// Uses the internal accumulator-based rate scale path for streaming.
328    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            /// Legacy combined float (phase + whole spikes) and/or fractional phase.
357            #[serde(default)]
358            accumulators: Vec<f64>,
359            /// Exact whole-spike backlog (new format). Folded with any whole part
360            /// still present in `accumulators` for forward/backward compatibility.
361            #[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        // Allow values >= 1.0 in `accumulators` (legacy combined representation).
374        // Reject only non-finite or negative state.
375        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        // max_rate 10.0 -> increment = (0.0 + 1.0 * 10.0) / 10.0 = 1.0
439        let output = encoder.encode_step(&[1.0]);
440        assert_eq!(output.spikes.len(), 1);
441
442        let output2 = encoder.encode_step(&[0.5]);
443        // 0.5 * 10.0 / 10.0 = 0.5 increment
444        assert_eq!(output2.spikes.len(), 0);
445        let output3 = encoder.encode_step(&[0.5]);
446        // another 0.5 -> 1.0 -> spike
447        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        // Use the deterministic streaming path: dopamine doubles the 10 Hz max
550        // rate to 20 Hz, so at dt=0.1s the accumulator advances by 2.0 and emits
551        // two spikes. Batch `encode_with_modulators` is stochastic (p ≈ 0.865)
552        // and flaky under CI, so it is not used here.
553        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        // Baseline (identity gains) advances by 1.0 and emits a single spike.
558        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        // Grow accumulators to two channels, then step with a shorter slice so only
571        // channel 0 is updated; channel 1 state is left untouched.
572        let _ = encoder.encode_step(&[0.0, 0.0]);
573        let output = encoder.encode_step(&[1.0]);
574        assert_eq!(output.spikes.len(), 1);
575        // Channel 1 still at zero accumulation: another zero-only step on both
576        // channels must not invent a ch1 spike.
577        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        // Accumulators must not be poisoned: a normal step after NaN still works.
609        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        // Backlog whole spikes (>= 1.0) must round-trip after a capped step.
670        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        // JSON null is a type error
682        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        // Above ~2^24, f32 cannot subtract 1.0; u64 pending must still drain.
693        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        // Quiet step must continue draining the same cap amount.
700        let second = encoder.encode_step(&[0.0]);
701        assert_eq!(
702            second.spikes.len(),
703            RateEncoder::MAX_SPIKES_PER_CHANNEL_PER_STEP
704        );
705        // After many drain steps, total emitted should exceed a single cap.
706        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        // Above 2^53, f64 `acc -= 1.0` is a no-op. Exact u64 pending must still
719        // decrease so quiet steps eventually exhaust the queue instead of
720        // emitting the 1024-spike cap forever.
721        //
722        // Seed a modest backlog via serde (above one cap, well below 2^53 so the
723        // test finishes quickly) and a huge runtime increment past 2^53.
724        #[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            // 2500 - 2*1024 = 452 remaining
742            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        // Runtime path: 1e16 Hz × 1 s is past f64's exact integer range.
752        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        // Pending must decrease exactly by the cap each quiet step (not stall).
759        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        // f32::MAX is a valid finite dt; f32 rate*dt would be +inf and was
779        // previously dropped. f64 product stays finite and enqueues under the
780        // per-step cap (must terminate, no OOM / hang).
781        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        // Huge but finite expected count is capped per step; remainder is queued.
789        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        // Undispatched whole spikes remain and drain on later quiet steps.
796        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        // Finite input after NaN still works (state not poisoned).
819        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        // max_rate ≈ 1e38, dt=10: f32 product overflows to +inf; f64 product
826        // remains finite and must enqueue under the cap.
827        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        // Zero gain = documented silence: no spikes, backlog discarded.
850        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        // After silence, a quiet identity step must not emit frozen backlog.
855        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        // max_rate * MAX_GAIN_SCALE (1e4) overflows f32 multiplication to +inf
869        // when max_rate is ~1e35. Saturated effective rate must yield p ≈ 1, not
870        // the non-finite-rate silence path in probability_from_rate_hz.
871        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/// Property-style suites for rate / silence / bound contracts (#69 / LIM-1016).
924///
925/// Uses a seeded [`StdRng`] to sample configurations and inputs so failures are
926/// reproducible. Encoder-internal spike draws still use the process RNG; the
927/// invariants asserted here are independent of those draws.
928#[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    /// Fixed seed so CI and local failures replay identically.
939    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}