Skip to main content

corpus_ipc/models/
traces.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use serde::{Deserialize, Serialize};
4
5use crate::validation::{
6    ProtocolLimits, Validate, ValidationError, add_to_total, check_count, check_finite,
7    check_string, check_unique_by,
8};
9
10use super::de::{de_session_id, de_trace_value, de_traces, prefix_path};
11
12/// IPC transport batch of eligibility traces for credit assignment.
13///
14/// This is a **wire-level** payload for [`IpcMessage::EligibilityTraces`](crate::IpcMessage::EligibilityTraces).
15/// Field names and layout are part of the serialized protocol.
16///
17/// **Not** an in-memory training batch (a single unstructured `traces`
18/// field). This type carries `session_id` / `batch_id` plus typed [`TraceData`] rows. Use
19/// [`IpcTraceBatch`] when the name collision matters.
20///
21/// Names stay `TraceBatch` so existing Rust imports and serde identifiers
22/// remain compatible (RM-324 / #7).
23#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
24#[serde(try_from = "TraceBatchWire")]
25pub struct TraceBatch {
26    /// Session ID for routing.
27    pub session_id: String,
28    /// Batch ID correlation.
29    pub batch_id: u64,
30    /// Eligibility trace data.
31    pub traces: Vec<TraceData>,
32}
33
34/// Explicit IPC-domain name for [`TraceBatch`].
35///
36/// Same type and same wire format. Prefer this alias in new code that sits
37/// next to training-side batch types.
38pub type IpcTraceBatch = TraceBatch;
39
40#[derive(Deserialize)]
41struct TraceBatchWire {
42    #[serde(deserialize_with = "de_session_id")]
43    session_id: String,
44    batch_id: u64,
45    #[serde(deserialize_with = "de_traces")]
46    traces: Vec<TraceData>,
47}
48
49impl TryFrom<TraceBatchWire> for TraceBatch {
50    type Error = ValidationError;
51
52    fn try_from(wire: TraceBatchWire) -> Result<Self, Self::Error> {
53        let batch = TraceBatch {
54            session_id: wire.session_id,
55            batch_id: wire.batch_id,
56            traces: wire.traces,
57        };
58        batch.validate()?;
59        Ok(batch)
60    }
61}
62
63impl Validate for TraceBatch {
64    fn validate_with(&self, limits: ProtocolLimits) -> Result<(), ValidationError> {
65        check_string("session_id", &self.session_id, limits)?;
66        check_count("traces", self.traces.len(), limits.max_traces)?;
67        let mut total = 0;
68        add_to_total(&mut total, self.traces.len(), limits, "aggregate")?;
69        for (index, trace) in self.traces.iter().enumerate() {
70            trace
71                .validate_with(limits)
72                .map_err(|err| prefix_path(err, &format!("traces[{index}]")))?;
73        }
74        check_unique_by("traces", &self.traces, |row| row.channel_id)?;
75        Ok(())
76    }
77}
78
79/// Individual eligibility trace data on the IPC wire.
80///
81/// An element of an IPC [`TraceBatch`], not an in-memory training-side trace type.
82#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
83#[serde(try_from = "TraceDataWire")]
84pub struct TraceData {
85    /// Channel or synapse identifier.
86    #[serde(alias = "neuron_id")]
87    pub channel_id: u16,
88    /// Trace value (decay-modulated spike history).
89    pub trace_value: f32,
90    /// Timestamp of last contributing spike.
91    pub last_spike_time: u32,
92}
93
94#[derive(Deserialize)]
95struct TraceDataWire {
96    #[serde(alias = "neuron_id")]
97    channel_id: u16,
98    #[serde(deserialize_with = "de_trace_value")]
99    trace_value: f32,
100    last_spike_time: u32,
101}
102
103impl TryFrom<TraceDataWire> for TraceData {
104    type Error = ValidationError;
105
106    fn try_from(wire: TraceDataWire) -> Result<Self, Self::Error> {
107        let data = TraceData {
108            channel_id: wire.channel_id,
109            trace_value: wire.trace_value,
110            last_spike_time: wire.last_spike_time,
111        };
112        data.validate()?;
113        Ok(data)
114    }
115}
116
117impl Validate for TraceData {
118    fn validate_with(&self, _limits: ProtocolLimits) -> Result<(), ValidationError> {
119        check_finite("trace_value", self.trace_value)
120    }
121}