1use serde::de::{Deserializer, Error as DeError};
22use serde::{Deserialize, Serialize};
23
24use crate::error::{MeshError, Result};
25
26#[derive(Clone, Debug, Serialize)]
47pub struct SpikeDelayBuffer {
48 slots: Vec<Vec<f32>>,
50 neuron_count: usize,
52 max_delay: usize,
54 current_tick: u64,
56}
57
58#[derive(Deserialize)]
59struct RawSpikeDelayBuffer {
60 slots: Vec<Vec<f32>>,
61 neuron_count: usize,
62 max_delay: usize,
63 current_tick: u64,
64}
65
66impl RawSpikeDelayBuffer {
67 fn into_buffer(self) -> std::result::Result<SpikeDelayBuffer, String> {
68 validate_delay_buffer_shape(&self.slots, self.neuron_count, self.max_delay)?;
69 validate_current_tick(self.current_tick, self.max_delay)?;
70 Ok(SpikeDelayBuffer {
71 slots: self.slots,
72 neuron_count: self.neuron_count,
73 max_delay: self.max_delay,
74 current_tick: self.current_tick,
75 })
76 }
77}
78
79fn validate_delay_buffer_shape(
82 slots: &[Vec<f32>],
83 neuron_count: usize,
84 max_delay: usize,
85) -> std::result::Result<(), String> {
86 let expected_depth = max_delay.checked_add(1).ok_or_else(|| {
87 format!("max_delay {max_delay} + 1 overflows usize; delay buffer depth is invalid")
88 })?;
89 if slots.is_empty() {
90 return Err("delay buffer slots must be non-empty".into());
91 }
92 if slots.len() != expected_depth {
93 return Err(format!(
94 "delay buffer depth {} does not match max_delay + 1 ({expected_depth})",
95 slots.len()
96 ));
97 }
98 for (i, slot) in slots.iter().enumerate() {
99 if slot.len() != neuron_count {
100 return Err(format!(
101 "delay buffer slot {i} width {} does not match neuron_count {neuron_count}",
102 slot.len()
103 ));
104 }
105 if slot.iter().any(|v| !v.is_finite()) {
106 return Err(format!(
107 "delay buffer slot {i} contains a non-finite current; refusing to poison later drain_current_tick sums"
108 ));
109 }
110 }
111 Ok(())
112}
113
114pub(crate) fn validate_current_tick(
118 current_tick: u64,
119 max_delay: usize,
120) -> std::result::Result<(), String> {
121 if current_tick >= usize::MAX as u64 {
122 return Err("current_tick is too large for safe advancement and indexing".into());
123 }
124 let max_safe_tick = (usize::MAX as u64).saturating_sub(max_delay as u64);
125 if current_tick > max_safe_tick {
126 return Err(format!(
127 "current_tick {current_tick} is too large to add max_delay {max_delay} without overflowing usize indexing"
128 ));
129 }
130 if current_tick >= (usize::MAX as u64).saturating_sub(1) {
134 return Err("current_tick is too large for safe advancement and indexing".into());
135 }
136 Ok(())
137}
138
139impl<'de> Deserialize<'de> for SpikeDelayBuffer {
140 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
141 where
142 D: Deserializer<'de>,
143 {
144 RawSpikeDelayBuffer::deserialize(deserializer)?
145 .into_buffer()
146 .map_err(DeError::custom)
147 }
148}
149
150impl SpikeDelayBuffer {
151 pub fn new(neuron_count: usize, max_delay: usize) -> Self {
168 Self::try_new(neuron_count, max_delay).unwrap_or_else(|err| panic!("{err}"))
169 }
170
171 pub fn try_new(neuron_count: usize, max_delay: usize) -> Result<Self> {
174 let depth = max_delay.checked_add(1).ok_or_else(|| {
175 MeshError::DelayError(format!(
176 "max_delay {max_delay} + 1 overflows usize; ring depth cannot be represented"
177 ))
178 })?;
179 Ok(Self {
180 slots: vec![vec![0.0; neuron_count]; depth],
181 neuron_count,
182 max_delay,
183 current_tick: 0,
184 })
185 }
186
187 #[inline]
202 pub fn inject(&mut self, target: usize, weight: f32, delay: usize) {
203 self.try_inject(target, weight, delay)
204 .unwrap_or_else(|err| panic!("{err}"))
205 }
206
207 #[inline]
210 pub fn try_inject(&mut self, target: usize, weight: f32, delay: usize) -> Result<()> {
211 if delay > self.max_delay {
212 return Err(MeshError::DelayError(format!(
213 "delay {delay} exceeds max_delay {}",
214 self.max_delay
215 )));
216 }
217 if target >= self.neuron_count {
218 return Err(MeshError::IndexOutOfBounds {
219 index: target,
220 max: self.neuron_count.saturating_sub(1),
221 });
222 }
223 let depth = self.slots.len();
227 if depth == 0 || delay >= depth {
228 return Err(MeshError::DelayError(format!(
229 "delay {delay} exceeds buffer depth {}",
230 depth.saturating_sub(1)
231 )));
232 }
233 let slot_idx = self.slot_index(delay);
234 self.slots[slot_idx][target] += weight;
235 Ok(())
236 }
237
238 #[inline]
240 pub(crate) fn scheduled_current(&self, target: usize, delay: usize) -> f32 {
241 debug_assert!(
242 delay <= self.max_delay,
243 "delay {delay} > max_delay {}",
244 self.max_delay
245 );
246 debug_assert!(
247 target < self.neuron_count,
248 "target {target} >= neuron_count {}",
249 self.neuron_count
250 );
251 self.slots[self.slot_index(delay)][target]
252 }
253
254 #[inline]
257 fn slot_index(&self, delay: usize) -> usize {
258 let depth = self.slots.len();
259 debug_assert!(depth > 0);
260 let tick_mod = (self.current_tick % depth as u64) as usize;
261 tick_mod.wrapping_add(delay) % depth
262 }
263
264 pub fn drain_current_tick(&mut self) -> Vec<f32> {
273 let mut currents = vec![0.0; self.neuron_count];
274 self.drain_current_tick_into(&mut currents)
275 .expect("output length matches neuron_count");
276 currents
277 }
278
279 pub fn drain_current_tick_into(&mut self, output: &mut [f32]) -> Result<()> {
286 if output.len() != self.neuron_count {
287 return Err(MeshError::NeuronCountMismatch {
288 expected: self.neuron_count,
289 got: output.len(),
290 context: "drain_current_tick_into output".into(),
291 });
292 }
293 let slot_idx = self.slot_index(0);
294 output.copy_from_slice(&self.slots[slot_idx]);
295 self.slots[slot_idx].fill(0.0);
296 Ok(())
297 }
298
299 pub fn advance(&mut self) {
301 self.current_tick += 1;
302 }
303
304 pub fn current_tick(&self) -> u64 {
306 self.current_tick
307 }
308
309 pub fn max_delay(&self) -> usize {
311 self.max_delay
312 }
313
314 pub fn neuron_count(&self) -> usize {
316 self.neuron_count
317 }
318
319 pub fn reset(&mut self) {
321 for slot in &mut self.slots {
322 slot.fill(0.0);
323 }
324 self.current_tick = 0;
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn zero_delay_delivers_same_tick() {
334 let mut buf = SpikeDelayBuffer::new(4, 0);
335 buf.inject(2, 0.75, 0);
336 let currents = buf.drain_current_tick();
337 assert!((currents[2] - 0.75).abs() < 1e-6);
338 assert_eq!(currents[0], 0.0);
339 }
340
341 #[test]
342 fn delayed_delivery() {
343 let mut buf = SpikeDelayBuffer::new(4, 5);
344
345 buf.inject(1, 0.5, 3);
347
348 let c0 = buf.drain_current_tick();
350 assert_eq!(c0[1], 0.0);
351 buf.advance();
352
353 let c1 = buf.drain_current_tick();
355 assert_eq!(c1[1], 0.0);
356 buf.advance();
357
358 let c2 = buf.drain_current_tick();
360 assert_eq!(c2[1], 0.0);
361 buf.advance();
362
363 let c3 = buf.drain_current_tick();
365 assert!((c3[1] - 0.5).abs() < 1e-6);
366 }
367
368 #[test]
369 fn multiple_spikes_accumulate() {
370 let mut buf = SpikeDelayBuffer::new(4, 5);
371 buf.inject(0, 0.3, 2);
372 buf.inject(0, 0.7, 2);
373
374 buf.advance();
375 buf.advance();
376
377 let currents = buf.drain_current_tick();
378 assert!((currents[0] - 1.0).abs() < 1e-6);
379 }
380
381 #[test]
382 fn ring_buffer_wraps_correctly() {
383 let mut buf = SpikeDelayBuffer::new(2, 3);
384
385 for tick in 0..10 {
387 buf.inject(0, 1.0, 2);
388 let currents = buf.drain_current_tick();
389 if tick >= 2 {
390 assert!(
392 (currents[0] - 1.0).abs() < 1e-6,
393 "tick {tick}: expected 1.0, got {}",
394 currents[0]
395 );
396 }
397 buf.advance();
398 }
399 }
400
401 #[test]
402 fn reset_clears_everything() {
403 let mut buf = SpikeDelayBuffer::new(4, 5);
404 buf.inject(0, 0.5, 3);
405 buf.advance();
406 buf.advance();
407 buf.reset();
408
409 assert_eq!(buf.current_tick(), 0);
410 let currents = buf.drain_current_tick();
411 assert!(currents.iter().all(|&v| v == 0.0));
412 }
413
414 #[test]
415 fn inject_delay_zero_is_accepted() {
416 let mut buffer = SpikeDelayBuffer::new(2, 1);
417 buffer.inject(1, 1.0, 0);
418 let currents = buffer.drain_current_tick();
419 assert!((currents[1] - 1.0).abs() < 1e-6);
420 }
421
422 #[test]
423 fn inject_delay_equal_to_capacity_is_accepted() {
424 let mut buffer = SpikeDelayBuffer::new(2, 1);
425 buffer.inject(0, 1.0, 1);
426 let c0 = buffer.drain_current_tick();
427 assert_eq!(c0[0], 0.0);
428 buffer.advance();
429 let c1 = buffer.drain_current_tick();
430 assert!((c1[0] - 1.0).abs() < 1e-6);
431 }
432
433 #[test]
434 #[should_panic(expected = "delay 2 exceeds max_delay 1")]
435 fn inject_excessive_delay_is_rejected() {
436 let mut buffer = SpikeDelayBuffer::new(2, 1);
437 buffer.inject(1, 1.0, 2);
438 }
439
440 #[test]
441 fn try_inject_excessive_delay_leaves_buffer_unchanged() {
442 let mut buffer = SpikeDelayBuffer::new(2, 1);
443 assert!(buffer.try_inject(1, 1.0, 2).is_err());
444 let currents = buffer.drain_current_tick();
445 assert!(currents.iter().all(|&c| c == 0.0));
446 }
447
448 #[test]
449 #[should_panic(expected = "out of bounds")]
450 fn inject_out_of_range_target_is_rejected() {
451 let mut buffer = SpikeDelayBuffer::new(2, 1);
452 buffer.inject(2, 1.0, 0);
453 }
454
455 #[test]
456 fn try_inject_out_of_range_target_leaves_buffer_unchanged() {
457 let mut buffer = SpikeDelayBuffer::new(2, 1);
458 assert!(buffer.try_inject(2, 1.0, 0).is_err());
459 let currents = buffer.drain_current_tick();
460 assert!(currents.iter().all(|&c| c == 0.0));
461 }
462
463 #[test]
464 fn try_new_rejects_max_delay_plus_one_overflow() {
465 let err = SpikeDelayBuffer::try_new(1, usize::MAX).unwrap_err();
466 let msg = err.to_string();
467 assert!(
468 msg.contains("overflows usize") && msg.contains("ring depth cannot be represented"),
469 "expected overflow error, got {msg}"
470 );
471 }
472
473 #[test]
474 #[should_panic(expected = "overflow")]
475 fn new_rejects_max_delay_plus_one_overflow() {
476 let _ = SpikeDelayBuffer::new(1, usize::MAX);
477 }
478
479 #[test]
480 fn deserialize_rejects_empty_slots() {
481 let json = r#"{"slots":[],"neuron_count":2,"max_delay":1,"current_tick":0}"#;
482 assert!(serde_json::from_str::<SpikeDelayBuffer>(json).is_err());
483 }
484
485 #[test]
486 fn deserialize_rejects_wrong_depth() {
487 let json = r#"{"slots":[[0.0,0.0]],"neuron_count":2,"max_delay":1,"current_tick":0}"#;
488 assert!(serde_json::from_str::<SpikeDelayBuffer>(json).is_err());
489 }
490
491 #[test]
492 fn deserialize_rejects_ragged_slot_widths() {
493 let json = r#"{"slots":[[0.0,0.0],[0.0]],"neuron_count":2,"max_delay":1,"current_tick":0}"#;
494 assert!(serde_json::from_str::<SpikeDelayBuffer>(json).is_err());
495 }
496
497 #[test]
498 fn deserialize_rejects_max_delay_plus_one_overflow() {
499 let json =
500 r#"{"slots":[],"neuron_count":1,"max_delay":18446744073709551615,"current_tick":0}"#;
501 assert!(serde_json::from_str::<SpikeDelayBuffer>(json).is_err());
502 assert!(validate_delay_buffer_shape(&[], 1, usize::MAX).is_err());
503 }
504
505 #[test]
506 fn deserialize_rejects_current_tick_at_u64_max() {
507 let json = r#"{"slots":[[0.0],[0.0]],"neuron_count":1,"max_delay":1,"current_tick":18446744073709551615}"#;
508 assert!(serde_json::from_str::<SpikeDelayBuffer>(json).is_err());
509 assert!(validate_current_tick(u64::MAX, 1).is_err());
510 assert!(validate_current_tick(usize::MAX as u64, 1).is_err());
511 assert!(validate_current_tick(0, 1).is_ok());
512 }
513
514 #[test]
515 fn deserialize_rejects_current_tick_that_advances_to_usize_max() {
516 let tick = usize::MAX as u64 - 1;
517 let json_delay0 =
518 format!(r#"{{"slots":[[0.0]],"neuron_count":1,"max_delay":0,"current_tick":{tick}}}"#);
519 let json_delay1 = format!(
520 r#"{{"slots":[[0.0],[0.0]],"neuron_count":1,"max_delay":1,"current_tick":{tick}}}"#
521 );
522 assert!(serde_json::from_str::<SpikeDelayBuffer>(&json_delay0).is_err());
523 assert!(serde_json::from_str::<SpikeDelayBuffer>(&json_delay1).is_err());
524 assert!(validate_current_tick(tick, 0).is_err());
525 assert!(validate_current_tick(tick, 1).is_err());
526 assert!(validate_current_tick(usize::MAX as u64 - 2, 0).is_ok());
527 assert!(validate_current_tick(usize::MAX as u64 - 2, 1).is_ok());
528 }
529
530 #[test]
531 fn deserialize_rejects_current_tick_that_overflows_inject_arithmetic() {
532 let tick = usize::MAX as u64 - 1;
533 let json = format!(
534 r#"{{"slots":[[0.0],[0.0],[0.0]],"neuron_count":1,"max_delay":2,"current_tick":{tick}}}"#
535 );
536 let err = serde_json::from_str::<SpikeDelayBuffer>(&json).unwrap_err();
537 assert!(
538 err.to_string().contains("overflowing usize indexing"),
539 "unexpected error: {err}"
540 );
541 assert!(validate_current_tick(tick, 2).is_err());
542 assert!(validate_current_tick(usize::MAX as u64 - 2, 2).is_ok());
544 }
545
546 #[test]
547 fn deserialize_accepts_current_tick_at_inject_arithmetic_limit() {
548 let tick = usize::MAX as u64 - 2;
549 let json = format!(
550 r#"{{"slots":[[0.0],[0.0],[0.0]],"neuron_count":1,"max_delay":2,"current_tick":{tick}}}"#
551 );
552 let mut buf: SpikeDelayBuffer = serde_json::from_str(&json).unwrap();
553 buf.inject(0, 1.0, 2);
554 assert_eq!(buf.current_tick(), tick);
555 }
556
557 #[cfg(target_pointer_width = "32")]
558 #[test]
559 fn deserialize_rejects_current_tick_that_wraps_usize() {
560 let json =
561 r#"{"slots":[[0.0],[0.0]],"neuron_count":1,"max_delay":1,"current_tick":4294967296}"#;
562 assert!(serde_json::from_str::<SpikeDelayBuffer>(json).is_err());
563 assert!(validate_current_tick((usize::MAX as u64) + 1, 1).is_err());
564 }
565
566 #[test]
567 fn validate_delay_buffer_shape_rejects_non_finite_currents() {
568 assert!(validate_delay_buffer_shape(&[vec![0.1], vec![f32::NAN]], 1, 1).is_err());
571 assert!(validate_delay_buffer_shape(&[vec![f32::INFINITY], vec![0.0]], 1, 1).is_err());
572 assert!(validate_delay_buffer_shape(&[vec![0.0], vec![f32::NEG_INFINITY]], 1, 1).is_err());
573 assert!(validate_delay_buffer_shape(&[vec![0.1, -0.0], vec![2.0, 0.0]], 2, 1).is_ok());
574 }
575
576 #[test]
577 fn deserialize_accepts_zero_neuron_and_zero_delay() {
578 let json = r#"{"slots":[[]],"neuron_count":0,"max_delay":0,"current_tick":0}"#;
579 let buf: SpikeDelayBuffer = serde_json::from_str(json).unwrap();
580 assert_eq!(buf.neuron_count(), 0);
581 assert_eq!(buf.max_delay(), 0);
582 }
583
584 #[test]
585 fn serialize_roundtrip_preserves_in_flight_current() {
586 let mut buf = SpikeDelayBuffer::new(2, 1);
587 buf.inject(1, 0.5, 1);
588 let json = serde_json::to_string(&buf).unwrap();
589 let mut restored: SpikeDelayBuffer = serde_json::from_str(&json).unwrap();
590 assert_eq!(restored.current_tick(), 0);
591 assert_eq!(restored.drain_current_tick()[1], 0.0);
592 restored.advance();
593 assert!((restored.drain_current_tick()[1] - 0.5).abs() < 1e-6);
594 }
595
596 #[test]
597 fn drain_into_matches_allocating_drain() {
598 let mut a = SpikeDelayBuffer::new(4, 2);
599 let mut b = SpikeDelayBuffer::new(4, 2);
600 a.inject(1, 0.5, 0);
601 a.inject(3, 1.25, 0);
602 b.inject(1, 0.5, 0);
603 b.inject(3, 1.25, 0);
604
605 let allocated = a.drain_current_tick();
606 let mut reused = vec![42.0; 4];
607 b.drain_current_tick_into(&mut reused).unwrap();
608 assert_eq!(allocated, reused);
609 }
610
611 #[test]
612 fn drain_into_length_mismatch_leaves_buffer_and_output_unchanged() {
613 let mut buf = SpikeDelayBuffer::new(2, 1);
614 buf.inject(0, 1.0, 0);
615 let mut output = [42.0_f32, 42.0, 42.0];
616 let err = buf.drain_current_tick_into(&mut output).unwrap_err();
617 assert!(err.to_string().contains("drain_current_tick_into output"));
618 assert_eq!(output, [42.0, 42.0, 42.0]);
619 let currents = buf.drain_current_tick();
620 assert!((currents[0] - 1.0).abs() < 1e-6);
621 }
622
623 #[test]
624 fn different_delays_different_arrival() {
625 let mut buf = SpikeDelayBuffer::new(4, 5);
626 buf.inject(0, 1.0, 1); buf.inject(1, 2.0, 3); buf.advance(); let c1 = buf.drain_current_tick();
631 assert!((c1[0] - 1.0).abs() < 1e-6);
632 assert_eq!(c1[1], 0.0);
633
634 buf.advance(); let c2 = buf.drain_current_tick();
636 assert_eq!(c2[0], 0.0);
637 assert_eq!(c2[1], 0.0);
638
639 buf.advance(); let c3 = buf.drain_current_tick();
641 assert_eq!(c3[0], 0.0);
642 assert!((c3[1] - 2.0).abs() < 1e-6);
643 }
644}