1use super::{AdaptResult, SearchResult, SubstrateBackend};
13use std::time::Instant;
14
15#[derive(Debug, Clone)]
17pub struct NeuromorphicConfig {
18 pub hd_dim: usize,
20 pub n_neurons: usize,
22 pub k_wta: usize,
24 pub tau_m: f32,
26 pub btsp_threshold: f32,
28 pub kuramoto_k: f32,
30 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, tau_m: 20.0, btsp_threshold: 0.7,
42 kuramoto_k: 0.3,
43 oscillation_hz: 40.0, }
45 }
46}
47
48struct NeuromorphicState {
50 hd_memory: Vec<Vec<u8>>, hd_dim: usize,
53 membrane: Vec<f32>,
55 #[allow(dead_code)]
57 weights: Vec<f32>,
58 n_neurons: usize,
59 phases: Vec<f32>,
61 order_parameter: f32,
63 eligibility: Vec<f32>,
65 pre_trace: Vec<f32>,
67 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 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 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 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 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 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 #[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 let mut indexed: Vec<(usize, f32)> = self.membrane.iter().copied().enumerate().collect();
136 indexed.select_nth_unstable_by(k - 1, |a, b| {
138 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
139 });
140 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 #[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 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 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 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
182pub 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 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 pub fn circadian_coherence(&mut self) -> f32 {
224 use std::f32::consts::TAU;
225 let omega = TAU * self.config.oscillation_hz / 1000.0; self.state.kuramoto_step(1.0, omega, self.config.kuramoto_k);
227 self.state.order_parameter
228 }
229
230 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 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; self.state.post_trace[i] = (self.state.post_trace[i] + 1.0) * 0.95;
244 self.state.eligibility[i] += 0.1;
246 }
247 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 if reward.abs() > self.config.btsp_threshold {
297 self.store(pattern);
298 }
299 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 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 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]; 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); assert!(result.delta_norm > 0.0);
382 let search = backend.similarity_search(&pattern, 1);
384 assert!(!search.is_empty());
385 }
386}