Skip to main content

corpus_ipc/models/
spikes.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use serde::{Deserialize, Serialize};
4
5use crate::validation::{
6    ProtocolLimits, Validate, ValidationError, check_count, check_finite, check_opt_string,
7};
8
9use super::BatchMetadata;
10use super::de::{
11    de_opt_session_id, de_spikes, de_strength, prefix_path, validate_optional_metadata,
12};
13
14/// IPC transport batch of spike events from compute processing.
15///
16/// This is a **wire-level** payload for [`IpcMessage::Spikes`](crate::IpcMessage::Spikes). Field names
17/// and layout are part of the serialized protocol and must not be changed
18/// casually.
19///
20/// **Not** an in-memory training batch of spike trains (`spikes` / `times` /
21/// `targets`); this type is a session-correlated list of [`SpikeEvent`]s.
22/// Use [`IpcSpikeBatch`] when the name collision matters.
23///
24/// Names stay `SpikeBatch` so existing Rust imports and serde identifiers
25/// remain compatible (RM-324 / #7).
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
27#[serde(try_from = "SpikeBatchWire")]
28pub struct SpikeBatch {
29    /// Optional session ID for concurrent experiment isolation.
30    pub session_id: Option<String>,
31    /// Unique batch identifier for correlation.
32    pub batch_id: u64,
33    /// Timestamp in nanoseconds (UTC or relative).
34    pub timestamp: u64,
35    /// Individual spike events.
36    pub spikes: Vec<SpikeEvent>,
37    /// Optional batch-level metadata.
38    pub metadata: Option<BatchMetadata>,
39}
40
41#[derive(Deserialize)]
42struct SpikeBatchWire {
43    #[serde(default, deserialize_with = "de_opt_session_id")]
44    session_id: Option<String>,
45    batch_id: u64,
46    timestamp: u64,
47    #[serde(deserialize_with = "de_spikes")]
48    spikes: Vec<SpikeEvent>,
49    metadata: Option<BatchMetadata>,
50}
51
52impl TryFrom<SpikeBatchWire> for SpikeBatch {
53    type Error = ValidationError;
54
55    fn try_from(wire: SpikeBatchWire) -> Result<Self, Self::Error> {
56        let batch = SpikeBatch {
57            session_id: wire.session_id,
58            batch_id: wire.batch_id,
59            timestamp: wire.timestamp,
60            spikes: wire.spikes,
61            metadata: wire.metadata,
62        };
63        batch.validate()?;
64        Ok(batch)
65    }
66}
67
68impl Validate for SpikeBatch {
69    fn validate_with(&self, limits: ProtocolLimits) -> Result<(), ValidationError> {
70        check_opt_string("session_id", self.session_id.as_deref(), limits)?;
71        check_count("spikes", self.spikes.len(), limits.max_spike_events)?;
72        for (index, spike) in self.spikes.iter().enumerate() {
73            spike
74                .validate_with(limits)
75                .map_err(|err| prefix_path(err, &format!("spikes[{index}]")))?;
76        }
77        validate_optional_metadata(&self.metadata, limits, self.spikes.len())
78    }
79}
80
81/// Explicit IPC-domain name for [`SpikeBatch`].
82///
83/// Same type and same wire format. Prefer this alias in new code that sits
84/// next to training-side batch types.
85pub type IpcSpikeBatch = SpikeBatch;
86
87/// Individual spike event with channel, timing, and strength.
88///
89/// An element of an IPC [`SpikeBatch`], not a training row.
90#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
91#[serde(try_from = "SpikeEventWire")]
92pub struct SpikeEvent {
93    /// Compute channel or channel identifier.
94    pub channel: u16,
95    /// Spike timestamp (relative or absolute).
96    pub time: u32,
97    /// Spike strength or amplitude.
98    pub strength: f32,
99}
100
101#[derive(Deserialize)]
102struct SpikeEventWire {
103    channel: u16,
104    time: u32,
105    #[serde(deserialize_with = "de_strength")]
106    strength: f32,
107}
108
109impl TryFrom<SpikeEventWire> for SpikeEvent {
110    type Error = ValidationError;
111
112    fn try_from(wire: SpikeEventWire) -> Result<Self, Self::Error> {
113        let event = SpikeEvent {
114            channel: wire.channel,
115            time: wire.time,
116            strength: wire.strength,
117        };
118        event.validate()?;
119        Ok(event)
120    }
121}
122
123impl Validate for SpikeEvent {
124    fn validate_with(&self, _limits: ProtocolLimits) -> Result<(), ValidationError> {
125        check_finite("strength", self.strength)
126    }
127}