Skip to main content

corpus_ipc/
rust_backend.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Pure-Rust native backend — no external dependencies.
4
5use crate::{BackendError, IpcBackend};
6
7/// Rust-native compute backend.
8///
9/// Implements a simple push-pull encoding: each input channel is split
10/// into a positive/negative pair. Channel `i` → `output[i*2]` (positive),
11/// channel `i` → `output[i*2+1]` (negative magnitude).
12///
13/// Useful as a smoke-test stub and software fallback when external IPC is
14/// unavailable.
15pub struct RustBackend {
16    initialized: bool,
17}
18
19impl RustBackend {
20    /// Construct and return a fresh `RustBackend` in the uninitialized state.
21    pub fn new() -> Self {
22        Self { initialized: false }
23    }
24}
25
26impl Default for RustBackend {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl IpcBackend for RustBackend {
33    fn process_batch(&mut self, inputs: &[f32]) -> Result<Vec<f32>, BackendError> {
34        if !self.initialized {
35            return Err(BackendError::InitializationError(
36                "RustBackend not initialized — call initialize() first".to_string(),
37            ));
38        }
39
40        // Push-pull encoding: positive inputs map to even channels; negative magnitudes map to odd channels.
41        let mut output = vec![0.0f32; inputs.len() * 2];
42        for i in 0..inputs.len() {
43            let val = inputs[i];
44            if val > 0.0 {
45                output[i * 2] = val;
46                output[i * 2 + 1] = 0.0;
47            } else {
48                output[i * 2] = 0.0;
49                output[i * 2 + 1] = val.abs();
50            }
51        }
52        Ok(output)
53    }
54
55    fn initialize(&mut self, _model_path: Option<&str>) -> Result<(), BackendError> {
56        self.initialized = true;
57        Ok(())
58    }
59
60    fn save_state(&self, _model_path: &str) -> Result<(), BackendError> {
61        Ok(()) // no state to persist
62    }
63
64    fn get_spike_states(&self) -> Vec<bool> {
65        // This backend is stateless, so it always returns an empty Vec.
66        Vec::new()
67    }
68
69    fn reset(&mut self) -> Result<(), BackendError> {
70        // Clear the initialized flag for consistency with other backends
71        // (e.g. ZMQ). After reset(), callers must invoke initialize()
72        // again before process_batch().
73        self.initialized = false;
74        Ok(())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn process_before_init_returns_error() {
84        let mut b = RustBackend::new();
85        assert!(b.process_batch(&[0.0; 4]).is_err());
86    }
87
88    #[test]
89    fn positive_input_goes_to_even_channel() {
90        let mut b = RustBackend::new();
91        b.initialize(None).unwrap();
92        let inputs = vec![0.8, 0.0, 0.0];
93        let out = b.process_batch(&inputs).unwrap();
94        assert!((out[0] - 0.8).abs() < 1e-5);
95        assert_eq!(out[1], 0.0);
96    }
97
98    #[test]
99    fn negative_input_goes_to_odd_channel() {
100        let mut b = RustBackend::new();
101        b.initialize(None).unwrap();
102        let inputs = vec![0.0, 0.0, -0.5];
103        let out = b.process_batch(&inputs).unwrap();
104        assert_eq!(out[4], 0.0); // positive channel for ch2
105        assert!((out[5] - 0.5).abs() < 1e-5); // negative-magnitude channel
106    }
107
108    #[test]
109    fn output_has_double_the_elements() {
110        let mut b = RustBackend::new();
111        b.initialize(None).unwrap();
112        let out = b.process_batch(&[0.1; 8]).unwrap();
113        assert_eq!(out.len(), 16);
114        let out_5 = b.process_batch(&[0.1; 5]).unwrap();
115        assert_eq!(out_5.len(), 10);
116    }
117
118    #[test]
119    fn spike_states_is_empty() {
120        let b = RustBackend::new();
121        assert!(b.get_spike_states().is_empty());
122    }
123
124    #[test]
125    fn reset_clears_initialized_and_next_process_fails() {
126        let mut b = RustBackend::new();
127        b.initialize(None).unwrap();
128        let _ = b.process_batch(&[0.1; 2]).unwrap();
129        b.reset().unwrap();
130        // After reset(), process_batch must fail until initialize() is called again.
131        assert!(b.process_batch(&[0.1; 2]).is_err());
132    }
133}