1use crate::prelude::*;
2
3#[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
34fn 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 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 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 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 if !value.is_finite() {
88 continue;
89 }
90
91 let Ok(channel_u16) = u16::try_from(channel) else {
92 break;
94 };
95
96 let phase_offset = self.phase_offset(self.normalize(value));
97 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 if !sensitivity_scale.is_finite() || sensitivity_scale <= 0.0 {
122 return output;
123 }
124
125 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 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 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 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 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 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 for _ in 0..6 {
276 encoder.encode(&[0.0]);
277 }
278 let output = encoder.encode(&[0.125, 0.375]); let timestamps: Vec<u64> = output.spikes.iter().map(|s| s.timestamp).collect();
280 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 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}