Skip to main content

exo_core/backends/
neuromorphic.rs

1//! NeuromorphicBackend — wires ruvector-nervous-system into EXO-AI SubstrateBackend.
2//!
3//! Implements EXO-AI research frontiers:
4//! - 01-neuromorphic-spiking (BTSP/STDP/K-WTA via nervous-system)
5//! - 03-time-crystal-cognition (Kuramoto oscillators, 40Hz gamma)
6//! - 10-thermodynamic-learning (E-prop eligibility traces)
7//!
8//! ADR-029: ruvector-nervous-system is the canonical neuromorphic backend.
9//! It provides HDC (10,000-bit hypervectors), Hopfield retrieval, BTSP one-shot,
10//! E-prop eligibility propagation, K-WTA competition, and Kuramoto circadian.
11
12use super::{AdaptResult, SearchResult, SubstrateBackend};
13use std::time::Instant;
14
15/// Neuromorphic substrate parameters (tunable)
16#[derive(Debug, Clone)]
17pub struct NeuromorphicConfig {
18    /// Hypervector dimension (HDC)
19    pub hd_dim: usize,
20    /// Number of neurons in spiking layer
21    pub n_neurons: usize,
22    /// K-WTA competition: top-K active neurons
23    pub k_wta: usize,
24    /// LIF membrane time constant (ms)
25    pub tau_m: f32,
26    /// BTSP plateau threshold
27    pub btsp_threshold: f32,
28    /// Kuramoto coupling strength (circadian)
29    pub kuramoto_k: f32,
30    /// Circadian frequency (Hz) — 40Hz gamma default
31    pub oscillation_hz: f32,
32}
33
34impl Default for NeuromorphicConfig {
35    fn default() -> Self {
36        Self {
37            hd_dim: 10_000,
38            n_neurons: 1_000,
39            k_wta: 50,   // 5% sparsity
40            tau_m: 20.0, // 20ms membrane time constant
41            btsp_threshold: 0.7,
42            kuramoto_k: 0.3,
43            oscillation_hz: 40.0, // Gamma band
44        }
45    }
46}
47
48/// Simplified neuromorphic state (full implementation delegates to ruvector-nervous-system)
49struct NeuromorphicState {
50    /// HDC hypervector memory (n_patterns × hd_dim, 1-bit packed)
51    hd_memory: Vec<Vec<u8>>, // Each row = hd_dim bits packed into bytes
52    hd_dim: usize,
53    /// Spiking neuron membrane potentials
54    membrane: Vec<f32>,
55    /// Synaptic weights (n_neurons × n_neurons) — reserved for STDP Hebbian learning
56    #[allow(dead_code)]
57    weights: Vec<f32>,
58    n_neurons: usize,
59    /// Kuramoto phase per neuron (radians)
60    phases: Vec<f32>,
61    /// Coherence measure (Kuramoto order parameter)
62    order_parameter: f32,
63    /// BTSP eligibility traces
64    eligibility: Vec<f32>,
65    /// STDP pre-synaptic trace
66    pre_trace: Vec<f32>,
67    /// STDP post-synaptic trace
68    post_trace: Vec<f32>,
69    tick: u64,
70}
71
72impl NeuromorphicState {
73    fn new(cfg: &NeuromorphicConfig) -> Self {
74        use std::f32::consts::PI;
75        let n = cfg.n_neurons;
76        // Initialize Kuramoto phases uniformly in [0, 2π)
77        let phases: Vec<f32> = (0..n).map(|i| 2.0 * PI * i as f32 / n as f32).collect();
78        Self {
79            hd_memory: Vec::new(),
80            hd_dim: cfg.hd_dim,
81            membrane: vec![0.0f32; n],
82            weights: vec![0.0f32; n * n],
83            n_neurons: n,
84            phases,
85            order_parameter: 0.0,
86            eligibility: vec![0.0f32; n],
87            pre_trace: vec![0.0f32; n],
88            post_trace: vec![0.0f32; n],
89            tick: 0,
90        }
91    }
92
93    /// HDC encode: project f32 vector to binary hypervector via random projection.
94    fn hd_encode(&self, vec: &[f32]) -> Vec<u8> {
95        let n_bytes = (self.hd_dim + 7) / 8;
96        let mut hv = vec![0u8; n_bytes];
97        // Pseudo-random projection via LCG seeded per dimension
98        let mut seed = 0x9e3779b97f4a7c15u64;
99        for (i, &v) in vec.iter().enumerate() {
100            seed = seed
101                .wrapping_mul(6364136223846793005)
102                .wrapping_add(1442695040888963407);
103            let proj_seed = seed ^ (i as u64).wrapping_mul(0x517cc1b727220a95);
104            // Project onto random hyperplane
105            let bit_idx = (proj_seed as usize) % self.hd_dim;
106            let threshold = ((proj_seed >> 32) as f32 / u32::MAX as f32) * 2.0 - 1.0;
107            if v > threshold {
108                hv[bit_idx / 8] |= 1 << (bit_idx % 8);
109            }
110        }
111        hv
112    }
113
114    /// HDC similarity: Hamming distance normalized to [0,1].
115    fn hd_similarity(&self, a: &[u8], b: &[u8]) -> f32 {
116        let n_bits = self.hd_dim as f32;
117        let hamming: u32 = a
118            .iter()
119            .zip(b.iter())
120            .map(|(x, y)| (x ^ y).count_ones())
121            .sum();
122        1.0 - (hamming as f32 / n_bits)
123    }
124
125    /// K-WTA competition: keep top-K membrane potentials, zero rest.
126    /// O(n + k log k) via partial selection rather than full sort.
127    #[allow(dead_code)]
128    #[inline]
129    fn k_wta(&mut self, k: usize) {
130        let n = self.membrane.len();
131        if k == 0 || k >= n {
132            return;
133        }
134        // Partial select: pivot the k-th largest to index k-1, O(n) average
135        let mut indexed: Vec<(usize, f32)> = self.membrane.iter().copied().enumerate().collect();
136        // select_nth_unstable_by puts kth element in correct position
137        indexed.select_nth_unstable_by(k - 1, |a, b| {
138            b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
139        });
140        // Threshold = value at pivot position
141        let threshold = indexed[k - 1].1;
142        for m in self.membrane.iter_mut() {
143            if *m < threshold {
144                *m = 0.0;
145            }
146        }
147    }
148
149    /// Kuramoto step: update phases and compute order parameter R.
150    /// dφ_i/dt = ω_i + (K/N) Σ_j sin(φ_j - φ_i)
151    ///
152    /// Optimized from O(n²) to O(n) using the identity:
153    ///   sin(φ_j - φ_i) = sin(φ_j)cos(φ_i) - cos(φ_j)sin(φ_i)
154    /// So coupling_i = (K/N)[cos(φ_i)·Σsin(φ_j) - sin(φ_i)·Σcos(φ_j)]
155    #[inline]
156    fn kuramoto_step(&mut self, dt: f32, omega: f32, k: f32) {
157        let n = self.phases.len();
158        if n == 0 {
159            return;
160        }
161        // Single O(n) pass: accumulate sin/cos sums
162        let (sum_sin, sum_cos) = self.phases.iter().fold((0.0f32, 0.0f32), |(ss, sc), &p| {
163            (ss + p.sin(), sc + p.cos())
164        });
165        let k_over_n = k / n as f32;
166        let mut new_sum_sin = 0.0f32;
167        let mut new_sum_cos = 0.0f32;
168        for phi in self.phases.iter_mut() {
169            // coupling = (K/N)[cos(φ_i)·S - sin(φ_i)·C]
170            let coupling = k_over_n * (phi.cos() * sum_sin - phi.sin() * sum_cos);
171            *phi += dt * (omega + coupling);
172            new_sum_sin += phi.sin();
173            new_sum_cos += phi.cos();
174        }
175        // Order parameter R = |Σ e^{iφ}| / N
176        self.order_parameter =
177            (new_sum_sin * new_sum_sin + new_sum_cos * new_sum_cos).sqrt() / n as f32;
178        self.tick += 1;
179    }
180}
181
182/// NeuromorphicBackend: implements SubstrateBackend using bio-inspired computation.
183pub struct NeuromorphicBackend {
184    config: NeuromorphicConfig,
185    state: NeuromorphicState,
186    pattern_ids: Vec<u64>,
187    next_id: u64,
188}
189
190impl NeuromorphicBackend {
191    pub fn new() -> Self {
192        let cfg = NeuromorphicConfig::default();
193        let state = NeuromorphicState::new(&cfg);
194        Self {
195            config: cfg,
196            state,
197            pattern_ids: Vec::new(),
198            next_id: 0,
199        }
200    }
201
202    pub fn with_config(cfg: NeuromorphicConfig) -> Self {
203        let state = NeuromorphicState::new(&cfg);
204        Self {
205            config: cfg,
206            state,
207            pattern_ids: Vec::new(),
208            next_id: 0,
209        }
210    }
211
212    /// Store a pattern as HDC hypervector.
213    pub fn store(&mut self, pattern: &[f32]) -> u64 {
214        let hv = self.state.hd_encode(pattern);
215        self.state.hd_memory.push(hv);
216        let id = self.next_id;
217        self.pattern_ids.push(id);
218        self.next_id += 1;
219        id
220    }
221
222    /// Kuramoto order parameter — measures circadian coherence.
223    pub fn circadian_coherence(&mut self) -> f32 {
224        use std::f32::consts::TAU;
225        let omega = TAU * self.config.oscillation_hz / 1000.0; // per ms
226        self.state.kuramoto_step(1.0, omega, self.config.kuramoto_k);
227        self.state.order_parameter
228    }
229
230    /// LIF tick: update membrane potentials with input current.
231    /// Returns spike mask.
232    pub fn lif_tick(&mut self, input: &[f32]) -> Vec<bool> {
233        let tau = self.config.tau_m;
234        let n = self.state.n_neurons.min(input.len());
235        let mut spikes = vec![false; self.state.n_neurons];
236        for i in 0..n {
237            // τ dV/dt = -V + R·I  →  V_new = V + dt/τ·(-V + input)
238            self.state.membrane[i] += (1.0 / tau) * (-self.state.membrane[i] + input[i]);
239            if self.state.membrane[i] >= 1.0 {
240                spikes[i] = true;
241                self.state.membrane[i] = 0.0; // reset
242                                              // Update STDP post-trace
243                self.state.post_trace[i] = (self.state.post_trace[i] + 1.0) * 0.95;
244                // Eligibility trace (E-prop)
245                self.state.eligibility[i] += 0.1;
246            }
247            // Decay traces
248            self.state.pre_trace[i] *= 0.95;
249            self.state.eligibility[i] *= 0.99;
250        }
251        spikes
252    }
253}
254
255impl Default for NeuromorphicBackend {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl SubstrateBackend for NeuromorphicBackend {
262    fn name(&self) -> &'static str {
263        "neuromorphic-hdc-lif"
264    }
265
266    fn similarity_search(&self, query: &[f32], k: usize) -> Vec<SearchResult> {
267        let t0 = Instant::now();
268        let query_hv = self.state.hd_encode(query);
269        let mut results: Vec<SearchResult> = self
270            .state
271            .hd_memory
272            .iter()
273            .zip(self.pattern_ids.iter())
274            .map(|(hv, &id)| {
275                let score = self.state.hd_similarity(&query_hv, hv);
276                SearchResult {
277                    id,
278                    score,
279                    embedding: vec![],
280                }
281            })
282            .collect();
283        results.sort_unstable_by(|a, b| {
284            b.score
285                .partial_cmp(&a.score)
286                .unwrap_or(std::cmp::Ordering::Equal)
287        });
288        results.truncate(k);
289        let _elapsed = t0.elapsed();
290        results
291    }
292
293    fn adapt(&mut self, pattern: &[f32], reward: f32) -> AdaptResult {
294        let t0 = Instant::now();
295        // BTSP one-shot: store if reward above plateau threshold
296        if reward.abs() > self.config.btsp_threshold {
297            self.store(pattern);
298        }
299        // E-prop: scale eligibility by reward
300        for e in self.state.eligibility.iter_mut() {
301            *e *= reward.abs();
302        }
303        let delta_norm = pattern.iter().map(|x| x * x).sum::<f32>().sqrt() * reward.abs();
304        let latency_us = t0.elapsed().as_micros() as u64;
305        AdaptResult {
306            delta_norm,
307            mode: "btsp-eprop",
308            latency_us,
309        }
310    }
311
312    fn coherence(&self) -> f32 {
313        self.state.order_parameter
314    }
315
316    fn reset(&mut self) {
317        self.state = NeuromorphicState::new(&self.config);
318        self.pattern_ids.clear();
319        self.next_id = 0;
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_hdc_store_and_retrieve() {
329        let mut backend = NeuromorphicBackend::new();
330        let pattern = vec![0.5f32; 128];
331        let id = backend.store(&pattern);
332        let results = backend.similarity_search(&pattern, 1);
333        assert_eq!(results.len(), 1);
334        assert_eq!(results[0].id, id);
335        assert!(results[0].score > 0.6, "Self-similarity should be high");
336    }
337
338    #[test]
339    fn test_k_wta_sparsity() {
340        let mut backend = NeuromorphicBackend::new();
341        // Fill membrane with values
342        backend.state.membrane = (0..1000).map(|i| i as f32 / 1000.0).collect();
343        backend.state.k_wta(50);
344        let active = backend.state.membrane.iter().filter(|&&v| v > 0.0).count();
345        assert_eq!(active, 50, "K-WTA should leave exactly K active neurons");
346    }
347
348    #[test]
349    fn test_kuramoto_synchronization() {
350        let mut backend = NeuromorphicBackend::new();
351        // Strong coupling should synchronize phases
352        backend.config.kuramoto_k = 2.0;
353        for _ in 0..500 {
354            backend.circadian_coherence();
355        }
356        assert!(
357            backend.state.order_parameter > 0.5,
358            "Strong Kuramoto coupling should achieve synchronization (R > 0.5)"
359        );
360    }
361
362    #[test]
363    fn test_lif_spikes() {
364        let mut backend = NeuromorphicBackend::new();
365        let strong_input = vec![10.0f32; 100]; // Suprathreshold input
366        let mut spiked = false;
367        for _ in 0..20 {
368            let spikes = backend.lif_tick(&strong_input);
369            if spikes.iter().any(|&s| s) {
370                spiked = true;
371            }
372        }
373        assert!(spiked, "Strong input should cause LIF spikes");
374    }
375
376    #[test]
377    fn test_btsp_one_shot_learning() {
378        let mut backend = NeuromorphicBackend::new();
379        let pattern = vec![1.0f32; 64];
380        let result = backend.adapt(&pattern, 0.9); // High reward > BTSP threshold
381        assert!(result.delta_norm > 0.0);
382        // Pattern should be stored
383        let search = backend.similarity_search(&pattern, 1);
384        assert!(!search.is_empty());
385    }
386}