1use crate::prelude::*;
2
3#[derive(Clone, Debug, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47pub struct PopulationEncoder {
48 num_neurons: usize,
49 input_range: (f32, f32),
50 tuning_width: f32, }
52
53impl PopulationEncoder {
54 pub fn new(num_neurons: usize, input_range: (f32, f32), tuning_width: f32) -> Self {
58 Self::try_new(num_neurons, input_range, tuning_width)
59 .expect("invalid PopulationEncoder configuration")
60 }
61
62 pub fn try_new(
64 num_neurons: usize,
65 input_range: (f32, f32),
66 tuning_width: f32,
67 ) -> Result<Self, EncoderError> {
68 if num_neurons == 0 {
69 return Err(EncoderError::CountMustBePositive {
70 parameter: "num_neurons",
71 });
72 }
73 crate::error::validate_channel_count(num_neurons)?;
74 crate::error::validate_range_f32_span("input_range", input_range)?;
75 if !tuning_width.is_finite() || tuning_width <= 0.0 {
76 return Err(EncoderError::NonPositiveOrNonFinite {
77 parameter: "tuning_width",
78 });
79 }
80 Ok(Self {
81 num_neurons,
82 input_range,
83 tuning_width,
84 })
85 }
86
87 pub fn num_neurons(&self) -> usize {
89 self.num_neurons
90 }
91
92 fn get_rate_with_tuning_width(
93 &self,
94 input: f32,
95 neuron_index: usize,
96 tuning_width: f32,
97 ) -> f32 {
98 let range_span = self.input_range.1 - self.input_range.0;
99 let preferred_value =
100 self.input_range.0 + (neuron_index as f32 / self.num_neurons as f32) * range_span;
101
102 let distance = (input - preferred_value).abs();
103 (-(distance * distance) / (2.0 * tuning_width * tuning_width)).exp()
105 }
106
107 fn effective_tuning_width(&self, sensitivity_scale: f32) -> f32 {
114 if !sensitivity_scale.is_finite() || sensitivity_scale <= 0.0 {
115 return self.tuning_width.max(f32::EPSILON);
116 }
117 if sensitivity_scale >= 1.0 {
118 return (self.tuning_width / sensitivity_scale).max(f32::EPSILON);
119 }
120 self.tuning_width.max(f32::EPSILON)
122 }
123
124 fn encode_with_sensitivity_scale(
125 &mut self,
126 input: &[f32],
127 sensitivity_scale: f32,
128 ) -> EncodedOutput {
129 let mut output = EncodedOutput::new();
130 if !sensitivity_scale.is_finite() || sensitivity_scale <= 0.0 {
132 return output;
133 }
134 let tuning_width = self.effective_tuning_width(sensitivity_scale);
135 let rate_gain = sensitivity_scale.min(1.0);
138
139 if let Some(&value) = input.first() {
141 let mut rng = rand::rng();
142 for i in 0..self.num_neurons {
143 let Ok(channel) = u16::try_from(i) else {
144 break;
146 };
147 let rate = self.get_rate_with_tuning_width(value, i, tuning_width) * rate_gain;
148 if crate::rng::gen_unit_f32_with_rng(&mut rng) < rate {
149 output.spikes.push(SpikeEvent {
150 channel,
151 timestamp: 0, polarity: true,
153 });
154 }
155 }
156 }
157 output
158 }
159
160 pub fn encode_with_modulators(
164 &mut self,
165 input: &[f32],
166 modulators: &NeuroModulators,
167 gain_curves: &NeuromodulatorGainCurves,
168 ) -> EncodedOutput {
169 <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
170 }
171
172 pub fn encode_step_with_modulators(
174 &mut self,
175 input: &[f32],
176 modulators: &NeuroModulators,
177 gain_curves: &NeuromodulatorGainCurves,
178 ) -> EncodedOutput {
179 <Self as ModulatedEncoder>::encode_step_with_modulators(
180 self,
181 input,
182 modulators,
183 gain_curves,
184 )
185 }
186}
187
188#[cfg(feature = "serde")]
189impl<'de> serde::Deserialize<'de> for PopulationEncoder {
190 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191 where
192 D: serde::Deserializer<'de>,
193 {
194 #[derive(serde::Deserialize)]
195 struct Helper {
196 num_neurons: usize,
197 input_range: (f32, f32),
198 tuning_width: f32,
199 }
200 let helper = Helper::deserialize(deserializer)?;
201 Self::try_new(helper.num_neurons, helper.input_range, helper.tuning_width)
202 .map_err(serde::de::Error::custom)
203 }
204}
205
206impl Encoder for PopulationEncoder {
207 fn encode(&mut self, input: &[f32]) -> EncodedOutput {
208 self.encode_with_sensitivity_scale(input, 1.0)
209 }
210
211 fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
212 self.encode(input)
213 }
214
215 fn reset(&mut self) {
216 }
218}
219
220impl ModulatedEncoder for PopulationEncoder {
221 fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
222 self.encode_with_sensitivity_scale(input, gains.sanitize().sensitivity_scale)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_population_encoder() {
232 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
233 let input = [50.0];
235 let output = encoder.encode(&input);
236
237 let rates: Vec<f32> = (0..10)
240 .map(|i| encoder.get_rate_with_tuning_width(50.0, i, encoder.tuning_width))
241 .collect();
242 let max_rate_index = rates
243 .iter()
244 .enumerate()
245 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
246 .unwrap()
247 .0;
248
249 assert!(
251 max_rate_index == 4 || max_rate_index == 5,
252 "Peak activity should be near the middle neuron for an input of 50."
253 );
254 assert!(output.spikes.len() <= 10);
255 }
256
257 #[test]
258 fn test_population_encoder_empty_input() {
259 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
260 let empty: [f32; 0] = [];
261 let via_encode = encoder.encode(&empty);
262 assert!(
263 via_encode.spikes.is_empty(),
264 "empty input must yield no spikes through encode"
265 );
266 let via_scale = encoder.encode_with_sensitivity_scale(&empty, 1.0);
267 assert!(
268 via_scale.spikes.is_empty(),
269 "empty input must yield no spikes through encode_with_sensitivity_scale"
270 );
271 let via_step = encoder.encode_step(&empty);
272 assert!(via_step.spikes.is_empty());
273 }
274
275 #[test]
276 fn test_effective_tuning_width_sub_unity() {
277 let encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
278 let width = encoder.effective_tuning_width(0.5);
280 assert_eq!(width, encoder.tuning_width.max(f32::EPSILON));
281 }
282
283 #[test]
284 fn test_effective_tuning_width_zero_and_negative() {
285 let encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
286 assert_eq!(
287 encoder.effective_tuning_width(0.0),
288 encoder.tuning_width.max(f32::EPSILON)
289 );
290 assert_eq!(
291 encoder.effective_tuning_width(-1.0),
292 encoder.tuning_width.max(f32::EPSILON)
293 );
294 assert_eq!(
295 encoder.effective_tuning_width(f32::NAN),
296 encoder.tuning_width.max(f32::EPSILON)
297 );
298 }
299
300 #[test]
301 fn test_encode_with_zero_sensitivity_returns_empty() {
302 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
303 let output = encoder.encode_with_sensitivity_scale(&[50.0], 0.0);
304 assert!(output.spikes.is_empty());
305 }
306
307 #[test]
308 fn test_encode_with_negative_sensitivity_returns_empty() {
309 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
310 let output = encoder.encode_with_sensitivity_scale(&[50.0], -1.0);
311 assert!(output.spikes.is_empty());
312 }
313
314 #[test]
315 fn test_encode_with_nan_sensitivity_returns_empty() {
316 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
317 let output = encoder.encode_with_sensitivity_scale(&[50.0], f32::NAN);
318 assert!(output.spikes.is_empty());
319 }
320
321 #[test]
322 fn test_sub_unity_sensitivity_suppresses_firing() {
323 let encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
324 let baseline_width = encoder.effective_tuning_width(1.0);
327 let suppressed_width = encoder.effective_tuning_width(0.1);
328 assert_eq!(baseline_width, suppressed_width);
330
331 let baseline_rate = encoder.get_rate_with_tuning_width(50.0, 5, baseline_width);
333 let suppressed_rate = encoder.get_rate_with_tuning_width(50.0, 5, suppressed_width) * 0.1;
334 assert!(
336 suppressed_rate < baseline_rate * 0.15,
337 "suppressed_rate {} should be < 15% of baseline_rate {}",
338 suppressed_rate,
339 baseline_rate
340 );
341 }
342
343 #[test]
344 fn test_encode_with_modulators_uses_gain_curves() {
345 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
346 let mods = NeuroModulators::default();
347 let curves = NeuromodulatorGainCurves::default();
348 let output = encoder.encode_with_modulators(&[50.0], &mods, &curves);
350 assert!(output.spikes.len() <= 10);
351 }
352
353 #[test]
354 fn test_encode_step_with_modulators() {
355 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
356 let mods = NeuroModulators::default();
357 let curves = NeuromodulatorGainCurves::default();
358 let output = encoder.encode_step_with_modulators(&[50.0], &mods, &curves);
359 assert!(output.spikes.len() <= 10);
360 }
361
362 #[test]
363 fn test_population_encoder_modulators_adjust_sensitivity() {
364 let encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
365 let modulators = NeuroModulators {
366 tempo: 1.0,
367 ..Default::default()
368 };
369 let gain_curves = NeuromodulatorGainCurves {
370 tempo: ModulatorGainCurves {
371 sensitivity: Some(GainCurve::new((0.0, 1.0), (1.0, 2.0))),
372 ..Default::default()
373 },
374 ..Default::default()
375 };
376
377 let baseline_width = encoder.effective_tuning_width(1.0);
378 let modulated_width =
379 encoder.effective_tuning_width(gain_curves.evaluate(&modulators).sensitivity_scale);
380 let baseline_rate = encoder.get_rate_with_tuning_width(50.0, 0, baseline_width);
381 let modulated_rate = encoder.get_rate_with_tuning_width(50.0, 0, modulated_width);
382
383 assert!(modulated_width < baseline_width);
384 assert!(modulated_rate < baseline_rate);
385 }
386
387 #[test]
388 fn test_population_encoder_step_and_accessors() {
389 let mut encoder = PopulationEncoder::new(10, (0.0, 100.0), 10.0);
390 assert_eq!(encoder.num_neurons(), 10);
391
392 let step_output = encoder.encode_step(&[50.0]);
393 assert!(step_output.spikes.len() <= 10);
394
395 encoder.reset();
396 assert_eq!(encoder.num_neurons(), 10);
397 }
398 #[test]
399 fn test_population_encoder_try_new_validation() {
400 assert_eq!(
401 PopulationEncoder::try_new(0, (0.0, 1.0), 0.1).err(),
402 Some(EncoderError::CountMustBePositive {
403 parameter: "num_neurons"
404 })
405 );
406 assert_eq!(
407 PopulationEncoder::try_new(u16::MAX as usize + 2, (0.0, 1.0), 0.1).err(),
408 Some(EncoderError::NumChannelsTooLarge)
409 );
410 assert_eq!(
411 PopulationEncoder::try_new(1, (1.0, 1.0), 0.1).err(),
412 Some(EncoderError::InvalidRange {
413 parameter: "input_range"
414 })
415 );
416 assert_eq!(
417 PopulationEncoder::try_new(1, (0.0, 1.0), 0.0).err(),
418 Some(EncoderError::NonPositiveOrNonFinite {
419 parameter: "tuning_width"
420 })
421 );
422 }
423}
424
425#[cfg(test)]
432mod property_tests {
433 use super::*;
434 use crate::encoders::property_support::{
435 TRIALS, assert_unique_channel_spikes, sample_gain_scale, sample_input_value,
436 sample_positive_finite, scale_is_inactive,
437 };
438 use rand::rngs::StdRng;
439 use rand::{RngExt, SeedableRng};
440
441 const SEED: u64 = 0xAE69_0002;
442
443 fn sample_valid_encoder(rng: &mut StdRng) -> PopulationEncoder {
444 loop {
445 let num_neurons = rng.random_range(1usize..=32);
446 let lo = rng.random_range(-100.0_f32..100.0);
447 let hi = lo + sample_positive_finite(rng);
448 let width = sample_positive_finite(rng);
449 if let Ok(enc) = PopulationEncoder::try_new(num_neurons, (lo, hi), width) {
450 return enc;
451 }
452 }
453 }
454
455 fn assert_active_population_spikes(trial: usize, out: &EncodedOutput, n_neurons: usize) {
456 assert!(
457 out.spikes.len() <= n_neurons,
458 "trial {trial}: spikes {} > num_neurons {n_neurons}",
459 out.spikes.len()
460 );
461 assert_unique_channel_spikes(&out.spikes, n_neurons);
462 }
463
464 #[test]
465 fn prop_population_silence_and_spike_bounds() {
466 let mut rng = StdRng::seed_from_u64(SEED);
467 for trial in 0..TRIALS {
468 let mut encoder = sample_valid_encoder(&mut rng);
469 let n_neurons = encoder.num_neurons();
470 let sensitivity = sample_gain_scale(&mut rng);
471
472 let empty = encoder.encode_with_sensitivity_scale(&[], sensitivity);
473 assert!(
474 empty.spikes.is_empty(),
475 "trial {trial}: empty input must silence"
476 );
477
478 let value = sample_input_value(&mut rng, (0.0, 100.0));
479 let out = encoder.encode_with_sensitivity_scale(&[value], sensitivity);
480
481 if scale_is_inactive(sensitivity) {
482 assert!(
483 out.spikes.is_empty(),
484 "trial {trial}: inactive sensitivity={sensitivity:?} must silence"
485 );
486 continue;
487 }
488 assert_active_population_spikes(trial, &out, n_neurons);
489 }
490 }
491
492 #[test]
493 fn prop_population_tuning_rates_in_unit_interval() {
494 let mut rng = StdRng::seed_from_u64(SEED ^ 0x51A7);
495 for trial in 0..TRIALS {
496 let encoder = sample_valid_encoder(&mut rng);
497 let value = sample_input_value(&mut rng, (0.0, 100.0));
498 if !value.is_finite() {
499 continue;
500 }
501 let sens = sample_gain_scale(&mut rng);
502 let width =
503 encoder.effective_tuning_width(if scale_is_inactive(sens) { 1.0 } else { sens });
504 assert!(
505 width.is_finite() && width > 0.0,
506 "trial {trial}: effective width {width}"
507 );
508 for i in 0..encoder.num_neurons() {
509 let rate = encoder.get_rate_with_tuning_width(value, i, width);
510 assert!(
511 rate.is_finite() && (0.0..=1.0).contains(&rate),
512 "trial {trial}: neuron {i} rate {rate} outside [0,1]"
513 );
514 }
515 }
516 }
517
518 #[test]
519 fn prop_population_encode_never_panics_on_sampled_inputs() {
520 let mut rng = StdRng::seed_from_u64(SEED ^ 0xBAD5);
521 for _ in 0..TRIALS {
522 let mut encoder = sample_valid_encoder(&mut rng);
523 let value = sample_input_value(&mut rng, (-50.0, 50.0));
524 let sens = sample_gain_scale(&mut rng);
525 let _ = encoder.encode_with_sensitivity_scale(&[value], sens);
526 let _ = encoder.encode(&[value]);
527 let _ = encoder.encode_step(&[value]);
528 encoder.reset();
529 }
530 }
531}