corpus-ipc 0.2.1

Spike and neuromodulator wire protocol and IPC transports for Limen-Neural SNN runtimes.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Pure-Rust native backend — no external dependencies.

use crate::{BackendError, IpcBackend};

/// Rust-native compute backend.
///
/// Implements a simple push-pull encoding: each input channel is split
/// into a positive/negative pair. Channel `i` → `output[i*2]` (positive),
/// channel `i` → `output[i*2+1]` (negative magnitude).
///
/// Useful as a smoke-test stub and software fallback when external IPC is
/// unavailable.
pub struct RustBackend {
    initialized: bool,
}

impl RustBackend {
    /// Construct and return a fresh `RustBackend` in the uninitialized state.
    pub fn new() -> Self {
        Self { initialized: false }
    }
}

impl Default for RustBackend {
    fn default() -> Self {
        Self::new()
    }
}

impl IpcBackend for RustBackend {
    fn process_batch(&mut self, inputs: &[f32]) -> Result<Vec<f32>, BackendError> {
        if !self.initialized {
            return Err(BackendError::InitializationError(
                "RustBackend not initialized — call initialize() first".to_string(),
            ));
        }

        // Push-pull encoding: positive inputs map to even channels; negative magnitudes map to odd channels.
        let mut output = vec![0.0f32; inputs.len() * 2];
        for i in 0..inputs.len() {
            let val = inputs[i];
            if val > 0.0 {
                output[i * 2] = val;
                output[i * 2 + 1] = 0.0;
            } else {
                output[i * 2] = 0.0;
                output[i * 2 + 1] = val.abs();
            }
        }
        Ok(output)
    }

    fn initialize(&mut self, _model_path: Option<&str>) -> Result<(), BackendError> {
        self.initialized = true;
        Ok(())
    }

    fn save_state(&self, _model_path: &str) -> Result<(), BackendError> {
        Ok(()) // no state to persist
    }

    fn get_spike_states(&self) -> Vec<bool> {
        // This backend is stateless, so it always returns an empty Vec.
        Vec::new()
    }

    fn reset(&mut self) -> Result<(), BackendError> {
        // Clear the initialized flag for consistency with other backends
        // (e.g. ZMQ). After reset(), callers must invoke initialize()
        // again before process_batch().
        self.initialized = false;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn process_before_init_returns_error() {
        let mut b = RustBackend::new();
        assert!(b.process_batch(&[0.0; 4]).is_err());
    }

    #[test]
    fn positive_input_goes_to_even_channel() {
        let mut b = RustBackend::new();
        b.initialize(None).unwrap();
        let inputs = vec![0.8, 0.0, 0.0];
        let out = b.process_batch(&inputs).unwrap();
        assert!((out[0] - 0.8).abs() < 1e-5);
        assert_eq!(out[1], 0.0);
    }

    #[test]
    fn negative_input_goes_to_odd_channel() {
        let mut b = RustBackend::new();
        b.initialize(None).unwrap();
        let inputs = vec![0.0, 0.0, -0.5];
        let out = b.process_batch(&inputs).unwrap();
        assert_eq!(out[4], 0.0); // positive channel for ch2
        assert!((out[5] - 0.5).abs() < 1e-5); // negative-magnitude channel
    }

    #[test]
    fn output_has_double_the_elements() {
        let mut b = RustBackend::new();
        b.initialize(None).unwrap();
        let out = b.process_batch(&[0.1; 8]).unwrap();
        assert_eq!(out.len(), 16);
        let out_5 = b.process_batch(&[0.1; 5]).unwrap();
        assert_eq!(out_5.len(), 10);
    }

    #[test]
    fn spike_states_is_empty() {
        let b = RustBackend::new();
        assert!(b.get_spike_states().is_empty());
    }

    #[test]
    fn reset_clears_initialized_and_next_process_fails() {
        let mut b = RustBackend::new();
        b.initialize(None).unwrap();
        let _ = b.process_batch(&[0.1; 2]).unwrap();
        b.reset().unwrap();
        // After reset(), process_batch must fail until initialize() is called again.
        assert!(b.process_batch(&[0.1; 2]).is_err());
    }
}