Skip to main content

axon_encoder/encoders/
phase.rs

1use crate::prelude::*;
2
3/// Encodes analog values as phase-locked spikes within a repeating oscillation cycle
4///
5/// Each input channel produces at most one positive spike per call, with the spike
6/// timestamp positioned relative to the current background phase according to the
7/// normalized input value. Higher values map to later phase bins
8///
9/// Timestamps are computed as `current_phase + phase_offset`, which keeps ordering
10/// stable *within* a single encode call (higher-value channels get later timestamps)
11/// Ordering *across* calls is not globally guaranteed, since `phase_offset` can exceed
12/// the per-call phase advance. Cycle-relative phase is recoverable as
13/// `timestamp % cycle_steps`.
14///
15/// # Examples
16///
17/// ```rust
18/// use axon_encoder::prelude::*;
19/// # fn main() -> Result<(), EncoderError> {
20/// let mut enc = PhaseEncoder::try_new(16, (0.0, 1.0))?;
21/// let out = enc.encode(&[0.0, 1.0]);
22/// assert_eq!(out.spikes.len(), 2);
23/// # Ok(())
24/// # }
25/// ```
26#[derive(Clone, Debug, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize))]
28pub struct PhaseEncoder {
29    cycle_steps: u64,
30    range: (f32, f32),
31    current_phase: u64,
32}
33
34/// Validates `cycle_steps` and `range`, returning an error message if invalid
35///
36/// Shared by both `PhaseEncoder::new` (which panics on failure) and the
37/// `Deserialize` impl (which surfaces the message as a deserialization error)
38fn validate_params(cycle_steps: u64, range: (f32, f32)) -> Result<(), EncoderError> {
39    if cycle_steps == 0 {
40        return Err(EncoderError::WindowMustBePositive {
41            parameter: "cycle_steps",
42        });
43    }
44    crate::error::validate_range("range", range)
45}
46
47impl PhaseEncoder {
48    /// Creates a new `PhaseEncoder`, panicking if configuration is invalid.
49    ///
50    /// Prefer [`try_new`](Self::try_new) for typed validation errors.
51    ///
52    /// # Panics
53    ///
54    /// Panics if `cycle_steps == 0` or if range bounds are non-finite or `range.0 >= range.1`.
55    pub fn new(cycle_steps: u64, range: (f32, f32)) -> Self {
56        Self::try_new(cycle_steps, range).unwrap_or_else(|error| panic!("{error}"))
57    }
58
59    /// Creates a new `PhaseEncoder`, returning an [`EncoderError`] for invalid configuration.
60    pub fn try_new(cycle_steps: u64, range: (f32, f32)) -> Result<Self, EncoderError> {
61        validate_params(cycle_steps, range)?;
62        Ok(Self {
63            cycle_steps,
64            range,
65            current_phase: 0,
66        })
67    }
68
69    fn normalize(&self, value: f32) -> f64 {
70        // Use f64 to prevent overflow for valid f32 ranges (e.g., f32::MIN..f32::MAX).
71        let clamped = value.clamp(self.range.0, self.range.1) as f64;
72        let lo = self.range.0 as f64;
73        let hi = self.range.1 as f64;
74        (clamped - lo) / (hi - lo)
75    }
76
77    fn phase_offset(&self, normalized: f64) -> u64 {
78        ((normalized * self.cycle_steps as f64).floor() as u64).min(self.cycle_steps - 1)
79    }
80
81    fn encode_current_cycle(&self, input: &[f32]) -> EncodedOutput {
82        let mut output = EncodedOutput::new();
83
84        for (channel, &value) in input.iter().enumerate() {
85            // Non-finite inputs are invalid readings — skip rather than emit a
86            // misleading phase-0 spike (NaN as u64 saturates to 0).
87            if !value.is_finite() {
88                continue;
89            }
90
91            let Ok(channel_u16) = u16::try_from(channel) else {
92                // Remaining channels exceed u16::MAX; stop rather than wrap.
93                break;
94            };
95
96            let phase_offset = self.phase_offset(self.normalize(value));
97            // Monotonic timestamps preserve higher-value → later-phase ordering
98            // even when phase_offset would wrap a modular cycle counter.
99            output.spikes.push(SpikeEvent {
100                channel: channel_u16,
101                timestamp: self.current_phase.saturating_add(phase_offset),
102                polarity: true,
103            });
104        }
105
106        output
107    }
108
109    fn advance_phase(&mut self) {
110        self.current_phase = self.current_phase.saturating_add(1);
111    }
112
113    fn encode_current_cycle_with_sensitivity_scale(
114        &self,
115        input: &[f32],
116        sensitivity_scale: f32,
117    ) -> EncodedOutput {
118        let mut output = EncodedOutput::new();
119
120        // Guard: zero or non-finite sensitivity collapses the range, suppressing all output.
121        if !sensitivity_scale.is_finite() || sensitivity_scale <= 0.0 {
122            return output;
123        }
124
125        // Use f64 to prevent overflow for valid f32 ranges and scales.
126        let lo = self.range.0 as f64;
127        let hi = lo + (self.range.1 as f64 - lo) * (sensitivity_scale as f64);
128
129        for (channel, &value) in input.iter().enumerate() {
130            if !value.is_finite() {
131                continue;
132            }
133
134            let Ok(channel_u16) = u16::try_from(channel) else {
135                break;
136            };
137
138            let normalized = ((value as f64 - lo) / (hi - lo)).clamp(0.0, 1.0);
139            let phase_offset = self.phase_offset(normalized);
140            output.spikes.push(SpikeEvent {
141                channel: channel_u16,
142                timestamp: self.current_phase.saturating_add(phase_offset),
143                polarity: true,
144            });
145        }
146
147        output
148    }
149
150    /// Encodes input using neuromodulator-driven gain curves.
151    ///
152    /// Inherent wrapper so callers need not import [`ModulatedEncoder`].
153    pub fn encode_with_modulators(
154        &mut self,
155        input: &[f32],
156        modulators: &NeuroModulators,
157        gain_curves: &NeuromodulatorGainCurves,
158    ) -> EncodedOutput {
159        <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
160    }
161
162    /// Step-wise variant of [`encode_with_modulators`](Self::encode_with_modulators).
163    pub fn encode_step_with_modulators(
164        &mut self,
165        input: &[f32],
166        modulators: &NeuroModulators,
167        gain_curves: &NeuromodulatorGainCurves,
168    ) -> EncodedOutput {
169        <Self as ModulatedEncoder>::encode_step_with_modulators(
170            self,
171            input,
172            modulators,
173            gain_curves,
174        )
175    }
176}
177
178impl Encoder for PhaseEncoder {
179    fn encode(&mut self, input: &[f32]) -> EncodedOutput {
180        let output = self.encode_current_cycle(input);
181        self.advance_phase();
182        output
183    }
184
185    fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
186        // Streaming and batch modes share the same phase-step semantics for
187        // this encoder: each call advances the background oscillation by one.
188        let output = self.encode_current_cycle(input);
189        self.advance_phase();
190        output
191    }
192
193    fn reset(&mut self) {
194        self.current_phase = 0;
195    }
196}
197
198impl ModulatedEncoder for PhaseEncoder {
199    fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
200        let output = self
201            .encode_current_cycle_with_sensitivity_scale(input, gains.sanitize().sensitivity_scale);
202        self.advance_phase();
203        output
204    }
205}
206
207#[cfg(feature = "serde")]
208impl<'de> serde::Deserialize<'de> for PhaseEncoder {
209    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210    where
211        D: serde::Deserializer<'de>,
212    {
213        #[derive(serde::Deserialize)]
214        struct Helper {
215            cycle_steps: u64,
216            range: (f32, f32),
217            #[serde(default)]
218            current_phase: u64,
219        }
220
221        let helper = Helper::deserialize(deserializer)?;
222
223        validate_params(helper.cycle_steps, helper.range).map_err(serde::de::Error::custom)?;
224
225        Ok(Self {
226            cycle_steps: helper.cycle_steps,
227            range: helper.range,
228            current_phase: helper.current_phase,
229        })
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_wide_range_normalizes_without_nan() {
239        let mut encoder = PhaseEncoder::new(8, (f32::MIN, f32::MAX));
240        let output = encoder.encode(&[f32::MAX]);
241        assert_eq!(output.spikes.len(), 1);
242        // f32::MAX maps to the last phase bin, not NaN → phase 0.
243        assert_eq!(output.spikes[0].timestamp, 7);
244    }
245
246    #[test]
247    fn test_phase_mapping_clamps_and_quantizes() {
248        let mut encoder = PhaseEncoder::new(8, (0.0, 10.0));
249
250        let output = encoder.encode(&[-5.0, 0.0, 5.0, 10.0, 15.0]);
251        let timestamps: Vec<u64> = output.spikes.iter().map(|spike| spike.timestamp).collect();
252        let polarities: Vec<bool> = output.spikes.iter().map(|spike| spike.polarity).collect();
253
254        assert_eq!(timestamps, vec![0, 0, 4, 7, 7]);
255        assert_eq!(polarities, vec![true; 5]);
256    }
257
258    #[test]
259    fn test_phase_advances_after_each_call() {
260        let mut encoder = PhaseEncoder::new(4, (0.0, 1.0));
261
262        assert_eq!(encoder.encode(&[0.0]).spikes[0].timestamp, 0);
263        assert_eq!(encoder.encode(&[0.0]).spikes[0].timestamp, 1);
264        assert_eq!(encoder.encode_step(&[0.0]).spikes[0].timestamp, 2);
265        assert_eq!(encoder.encode_step(&[0.0]).spikes[0].timestamp, 3);
266        // Monotonic absolute phase time (cycle phase is timestamp % cycle_steps).
267        assert_eq!(encoder.encode(&[0.0]).spikes[0].timestamp, 4);
268        assert_eq!(encoder.encode(&[0.0]).spikes[0].timestamp % 4, 1);
269    }
270
271    #[test]
272    fn test_within_call_ordering_preserved_after_phase_advance() {
273        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
274        // Advance near the end of a modular cycle so a wrap would reorder.
275        for _ in 0..6 {
276            encoder.encode(&[0.0]);
277        }
278        let output = encoder.encode(&[0.125, 0.375]); // offsets 1 and 3
279        let timestamps: Vec<u64> = output.spikes.iter().map(|s| s.timestamp).collect();
280        // 6+1=7, 6+3=9 — strictly ordered (no modular wrap inversion).
281        assert_eq!(timestamps, vec![7, 9]);
282        assert!(timestamps[0] < timestamps[1]);
283    }
284
285    #[test]
286    fn test_reset_restores_initial_phase() {
287        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
288
289        encoder.encode(&[0.0]);
290        encoder.encode(&[0.0]);
291        encoder.reset();
292
293        let output = encoder.encode(&[1.0]);
294        assert_eq!(output.spikes[0].timestamp, 7);
295    }
296
297    #[test]
298    fn test_empty_input_returns_no_spikes() {
299        let mut encoder = PhaseEncoder::new(4, (0.0, 1.0));
300
301        let output = encoder.encode(&[]);
302        assert!(output.spikes.is_empty());
303
304        let next_output = encoder.encode(&[0.0]);
305        assert_eq!(next_output.spikes[0].timestamp, 1);
306    }
307
308    #[test]
309    fn test_nan_input_skips_channel() {
310        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
311        let output = encoder.encode(&[0.0, f32::NAN, 1.0]);
312        assert_eq!(output.spikes.len(), 2);
313        assert_eq!(output.spikes[0].channel, 0);
314        assert_eq!(output.spikes[1].channel, 2);
315    }
316
317    #[test]
318    #[should_panic(expected = "cycle_steps must be greater than 0")]
319    fn test_zero_cycle_steps_rejected() {
320        let _ = PhaseEncoder::new(0, (0.0, 1.0));
321    }
322
323    #[test]
324    #[should_panic(expected = "range must be finite and min must be less than max")]
325    fn test_invalid_range_rejected() {
326        let _ = PhaseEncoder::new(8, (1.0, 1.0));
327    }
328
329    #[test]
330    fn test_encode_step_matches_encode() {
331        let input = [2.5, 7.5];
332        let mut encode_encoder = PhaseEncoder::new(8, (0.0, 10.0));
333        let mut step_encoder = PhaseEncoder::new(8, (0.0, 10.0));
334
335        assert_eq!(
336            encode_encoder.encode(&input),
337            step_encoder.encode_step(&input)
338        );
339        assert_eq!(
340            encode_encoder.encode(&input),
341            step_encoder.encode_step(&input)
342        );
343    }
344
345    #[cfg(feature = "serde")]
346    #[test]
347    fn test_deserialize_rejects_zero_cycle_steps() {
348        let json = r#"{"cycle_steps":0,"range":[0.0,1.0],"current_phase":0}"#;
349        let err = serde_json::from_str::<PhaseEncoder>(json).unwrap_err();
350        assert!(err.to_string().contains("cycle_steps"));
351    }
352
353    #[test]
354    fn test_encode_with_modulators_identity() {
355        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
356        let curves = NeuromodulatorGainCurves::default();
357        let mods = NeuroModulators::default();
358
359        let plain = encoder.encode(&[0.5]);
360        let mut encoder2 = PhaseEncoder::new(8, (0.0, 1.0));
361        let modulated = encoder2.encode_with_modulators(&[0.5], &mods, &curves);
362
363        assert_eq!(plain.spikes[0].timestamp, modulated.spikes[0].timestamp);
364    }
365
366    #[test]
367    fn test_encode_with_modulators_sensitivity_scale() {
368        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
369        let curves = NeuromodulatorGainCurves {
370            dopamine: ModulatorGainCurves {
371                sensitivity: Some(GainCurve::new((0.0, 1.0), (0.5, 0.5))),
372                ..Default::default()
373            },
374            ..Default::default()
375        };
376        let mods = NeuroModulators {
377            dopamine: 1.0,
378            ..Default::default()
379        };
380
381        let output = encoder.encode_with_modulators(&[0.5], &mods, &curves);
382        // sensitivity_scale = 0.5, range = (0.0, 0.5)
383        // value 0.5 maps to normalized 1.0, phase_offset = 7
384        assert_eq!(output.spikes[0].timestamp, 7);
385    }
386
387    #[test]
388    fn test_encode_step_with_modulators_matches_encode() {
389        let input = [0.5];
390        let curves = NeuromodulatorGainCurves::default();
391        let mods = NeuroModulators::default();
392
393        let mut encoder1 = PhaseEncoder::new(8, (0.0, 1.0));
394        let mut encoder2 = PhaseEncoder::new(8, (0.0, 1.0));
395
396        let batch = encoder1.encode_with_modulators(&input, &mods, &curves);
397        let step = encoder2.encode_step_with_modulators(&input, &mods, &curves);
398
399        assert_eq!(batch, step);
400    }
401
402    #[test]
403    fn test_encode_with_modulators_zero_sensitivity_suppresses() {
404        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
405        let curves = NeuromodulatorGainCurves {
406            dopamine: ModulatorGainCurves {
407                sensitivity: Some(GainCurve::new((0.0, 1.0), (0.0, 0.0))),
408                ..Default::default()
409            },
410            ..Default::default()
411        };
412        let mods = NeuroModulators {
413            dopamine: 1.0,
414            ..Default::default()
415        };
416
417        let output = encoder.encode_with_modulators(&[0.5], &mods, &curves);
418        assert!(output.spikes.is_empty());
419    }
420
421    #[test]
422    fn test_encode_with_modulators_nan_input_skips() {
423        let mut encoder = PhaseEncoder::new(8, (0.0, 1.0));
424        let curves = NeuromodulatorGainCurves {
425            dopamine: ModulatorGainCurves {
426                sensitivity: Some(GainCurve::new((0.0, 1.0), (1.0, 1.0))),
427                ..Default::default()
428            },
429            ..Default::default()
430        };
431        let mods = NeuroModulators {
432            dopamine: 1.0,
433            ..Default::default()
434        };
435
436        let output = encoder.encode_with_modulators(&[0.0, f32::NAN, 1.0], &mods, &curves);
437        assert_eq!(output.spikes.len(), 2);
438        assert_eq!(output.spikes[0].channel, 0);
439        assert_eq!(output.spikes[1].channel, 2);
440    }
441    #[test]
442    fn test_phase_encoder_try_new_validation() {
443        assert_eq!(
444            PhaseEncoder::try_new(0, (0.0, 1.0)).err(),
445            Some(EncoderError::WindowMustBePositive {
446                parameter: "cycle_steps"
447            })
448        );
449        assert_eq!(
450            PhaseEncoder::try_new(1, (1.0, 1.0)).err(),
451            Some(EncoderError::InvalidRange { parameter: "range" })
452        );
453    }
454}