1use serde::{Deserialize, Serialize};
20
21use crate::error::{MeshError, Result};
22
23pub const MAX_SPARSE_NEURONS: usize = u16::MAX as usize + 1;
25
26#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
28pub struct Synapse {
29 pub target: u16,
31 pub weight: f32,
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct SparseSynapticMap<const N: usize> {
46 pub row_ptr: Vec<usize>,
49 pub col_indices: Vec<u16>,
51 pub values: Vec<f32>,
53}
54
55impl<const N: usize> Default for SparseSynapticMap<N> {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl<const N: usize> SparseSynapticMap<N> {
62 pub fn new() -> Self {
69 Self::try_new().unwrap_or_else(|err| panic!("{err}"))
70 }
71
72 pub fn try_new() -> Result<Self> {
75 validate_neuron_count::<N>()?;
76 Ok(Self {
77 row_ptr: vec![0; N + 1],
78 col_indices: Vec::new(),
79 values: Vec::new(),
80 })
81 }
82
83 pub fn from_dense(matrix: &[[f32; N]; N], sparsity_threshold: f32) -> Self {
89 Self::try_from_dense(matrix, sparsity_threshold).unwrap_or_else(|err| panic!("{err}"))
90 }
91
92 pub fn try_from_dense(matrix: &[[f32; N]; N], sparsity_threshold: f32) -> Result<Self> {
94 validate_neuron_count::<N>()?;
95 let mut row_ptr = Vec::with_capacity(N + 1);
96 let mut col_indices = Vec::new();
97 let mut values = Vec::new();
98
99 row_ptr.push(0);
100 for row in matrix.iter() {
101 for (col, &w) in row.iter().enumerate() {
102 if w.abs() > sparsity_threshold {
103 col_indices.push(index_as_u16::<N>(col, "column")?);
104 values.push(w);
105 }
106 }
107 row_ptr.push(col_indices.len());
108 }
109
110 Ok(Self {
111 row_ptr,
112 col_indices,
113 values,
114 })
115 }
116
117 pub fn from_adjacency(adjacency: &[Vec<Synapse>]) -> Self {
124 Self::try_from_adjacency(adjacency).unwrap_or_else(|err| panic!("{err}"))
125 }
126
127 pub fn try_from_adjacency(adjacency: &[Vec<Synapse>]) -> Result<Self> {
129 validate_neuron_count::<N>()?;
130 if adjacency.len() != N {
131 return Err(MeshError::NeuronCountMismatch {
132 expected: N,
133 got: adjacency.len(),
134 context: "from_adjacency".into(),
135 });
136 }
137
138 let mut row_ptr = Vec::with_capacity(N + 1);
139 let mut col_indices = Vec::new();
140 let mut values = Vec::new();
141
142 row_ptr.push(0);
143 for synapses in adjacency {
144 for syn in synapses {
145 if syn.target as usize >= N {
146 return Err(MeshError::IndexOutOfBounds {
147 index: syn.target as usize,
148 max: N.saturating_sub(1),
149 });
150 }
151 col_indices.push(syn.target);
152 values.push(syn.weight);
153 }
154 row_ptr.push(col_indices.len());
155 }
156
157 Ok(Self {
158 row_ptr,
159 col_indices,
160 values,
161 })
162 }
163
164 pub fn get_row(&self, row: usize) -> impl Iterator<Item = (u16, f32)> + '_ {
166 let start = self.row_ptr[row];
167 let end = self.row_ptr[row + 1];
168 (start..end).map(move |i| (self.col_indices[i], self.values[i]))
169 }
170
171 pub fn get_weight(&self, row: usize, col: usize) -> f32 {
173 let start = self.row_ptr[row];
174 let end = self.row_ptr[row + 1];
175 for i in start..end {
176 if self.col_indices[i] as usize == col {
177 return self.values[i];
178 }
179 }
180 0.0
181 }
182
183 pub fn set_weight(&mut self, row: usize, col: usize, weight: f32, sparsity_threshold: f32) {
191 self.try_set_weight(row, col, weight, sparsity_threshold)
192 .unwrap_or_else(|err| panic!("{err}"))
193 }
194
195 pub fn try_set_weight(
199 &mut self,
200 row: usize,
201 col: usize,
202 weight: f32,
203 sparsity_threshold: f32,
204 ) -> Result<()> {
205 let _ = index_as_u16::<N>(row, "row")?;
206 let col_u16 = index_as_u16::<N>(col, "column")?;
207 if let Some(i) = self.find_connection(row, col) {
208 self.update_or_remove(i, row, weight, sparsity_threshold);
209 return Ok(());
210 }
211 if weight.abs() > sparsity_threshold {
212 self.insert_connection(row, col_u16, weight);
213 }
214 Ok(())
215 }
216
217 fn find_connection(&self, row: usize, col: usize) -> Option<usize> {
218 let start = self.row_ptr[row];
219 let end = self.row_ptr[row + 1];
220 (start..end).find(|&i| self.col_indices[i] as usize == col)
221 }
222
223 fn update_or_remove(&mut self, index: usize, row: usize, weight: f32, sparsity_threshold: f32) {
224 if weight.abs() > sparsity_threshold {
225 self.values[index] = weight;
226 return;
227 }
228 self.col_indices.remove(index);
229 self.values.remove(index);
230 self.shift_row_ptr(row, -1);
231 }
232
233 fn insert_connection(&mut self, row: usize, col: u16, weight: f32) {
234 let end = self.row_ptr[row + 1];
235 self.col_indices.insert(end, col);
236 self.values.insert(end, weight);
237 self.shift_row_ptr(row, 1);
238 }
239
240 fn shift_row_ptr(&mut self, row: usize, delta: isize) {
241 for ptr in &mut self.row_ptr[(row + 1)..=N] {
242 *ptr = ptr
243 .checked_add_signed(delta)
244 .expect("row_ptr stays in range");
245 }
246 }
247
248 pub fn nnz(&self) -> usize {
250 self.values.len()
251 }
252
253 pub fn sparsity(&self) -> f32 {
258 let total = (N as u64).saturating_mul(N as u64);
259 if total == 0 {
260 return 1.0;
261 }
262 1.0 - (self.nnz() as f32 / total as f32)
263 }
264
265 pub fn to_dense(&self) -> [[f32; N]; N] {
267 let mut matrix = [[0.0; N]; N];
268 for (row, row_data) in matrix.iter_mut().enumerate().take(N) {
269 for (col, w) in self.get_row(row) {
270 row_data[col as usize] = w;
271 }
272 }
273 matrix
274 }
275
276 pub fn to_gpu_arrays(&self) -> (Vec<u32>, Vec<u32>, Vec<f32>) {
284 self.try_to_gpu_arrays()
285 .unwrap_or_else(|err| panic!("{err}"))
286 }
287
288 pub fn try_to_gpu_arrays(&self) -> Result<(Vec<u32>, Vec<u32>, Vec<f32>)> {
290 let row_ptr = usizes_to_u32(&self.row_ptr)?;
291 let col_indices: Vec<u32> = self.col_indices.iter().map(|&x| u32::from(x)).collect();
292 Ok((row_ptr, col_indices, self.values.clone()))
293 }
294}
295
296fn validate_neuron_count<const N: usize>() -> Result<()> {
297 if N > MAX_SPARSE_NEURONS {
298 return Err(MeshError::InvalidConfig(format!(
299 "SparseSynapticMap neuron count N={N} exceeds the u16 target address space ({MAX_SPARSE_NEURONS})"
300 )));
301 }
302 Ok(())
303}
304
305fn index_as_u16<const N: usize>(index: usize, what: &str) -> Result<u16> {
306 if index >= N {
307 return Err(MeshError::IndexOutOfBounds {
308 index,
309 max: N.saturating_sub(1),
310 });
311 }
312 u16::try_from(index).map_err(|_| {
313 MeshError::InvalidConfig(format!("{what} {index} is not representable as u16"))
314 })
315}
316
317pub(crate) fn usizes_to_u32(values: &[usize]) -> Result<Vec<u32>> {
322 values
323 .iter()
324 .map(|&offset| {
325 u32::try_from(offset).map_err(|_| {
326 MeshError::InvalidConfig(format!("row pointer offset {offset} exceeds u32::MAX"))
327 })
328 })
329 .collect()
330}
331
332pub struct SparseSynapticMapBuilder<const N: usize> {
334 adjacency: Vec<Vec<Synapse>>,
335 default_weight: f32,
336 sparsity_threshold: f32,
337}
338
339impl<const N: usize> SparseSynapticMapBuilder<N> {
340 pub fn new() -> Self {
344 Self::try_new().unwrap_or_else(|err| panic!("{err}"))
345 }
346
347 pub fn try_new() -> Result<Self> {
349 validate_neuron_count::<N>()?;
350 Ok(Self {
351 adjacency: vec![Vec::new(); N],
352 default_weight: 0.0,
353 sparsity_threshold: 0.01,
354 })
355 }
356
357 pub fn with_self_weight(mut self, weight: f32) -> Self {
359 self.default_weight = weight;
360 self
361 }
362
363 pub fn with_sparsity_threshold(mut self, threshold: f32) -> Self {
365 self.sparsity_threshold = threshold;
366 self
367 }
368
369 pub fn connect(mut self, from: usize, to: usize, weight: f32) -> Self {
376 self.try_connect(from, to, weight)
377 .unwrap_or_else(|err| panic!("{err}"));
378 self
379 }
380
381 pub fn try_connect(&mut self, from: usize, to: usize, weight: f32) -> Result<()> {
384 let _ = index_as_u16::<N>(from, "source")?;
385 let target = index_as_u16::<N>(to, "target")?;
386 if weight.abs() > self.sparsity_threshold {
387 self.adjacency[from].push(Synapse { target, weight });
388 }
389 Ok(())
390 }
391
392 pub fn with_self_connections(self) -> Self {
394 let w = self.default_weight;
395 let mut result = self;
396 for i in 0..N {
397 result = result.connect(i, i, w);
398 }
399 result
400 }
401
402 pub fn with_lateral_inhibition(self, weight: f32) -> Self {
404 let mut result = self;
405 for i in 0..N {
406 for j in 0..N {
407 if i != j {
408 result = result.connect(i, j, weight);
409 }
410 }
411 }
412 result
413 }
414
415 pub fn build(self) -> SparseSynapticMap<N> {
417 SparseSynapticMap::from_adjacency(&self.adjacency)
418 }
419}
420
421impl<const N: usize> Default for SparseSynapticMapBuilder<N> {
422 fn default() -> Self {
423 Self::new()
424 }
425}
426
427#[derive(Clone, Debug, Serialize, Deserialize)]
432pub struct NeuronStateSnapshot {
433 pub adaptation: Vec<f32>,
435 pub spike_counts: Vec<u32>,
437 #[serde(alias = "quant_error")]
439 pub error: Vec<f32>,
440 pub step: u64,
442}
443
444impl NeuronStateSnapshot {
445 pub fn new(num_neurons: usize) -> Self {
446 Self {
447 adaptation: vec![0.0; num_neurons],
448 spike_counts: vec![0; num_neurons],
449 error: vec![0.0; num_neurons],
450 step: 0,
451 }
452 }
453
454 pub fn adaptation_penalty(&self, neuron: usize, alpha: f32) -> f32 {
457 alpha * self.adaptation.get(neuron).copied().unwrap_or(0.0)
458 }
459
460 pub fn error_bonus(&self, neuron: usize, beta: f32) -> f32 {
463 let err = self.error.get(neuron).copied().unwrap_or(0.0);
464 beta * (1.0 - err.min(1.0))
465 }
466}
467
468#[derive(Clone, Debug, Serialize, Deserialize)]
473pub struct RoutingPolicy {
474 pub alpha: f32,
476 pub beta: f32,
478 pub gamma: f32,
480 pub delta: f32,
482 pub threshold: f32,
484 pub description: String,
486}
487
488impl Default for RoutingPolicy {
489 fn default() -> Self {
490 Self {
491 alpha: 1.0,
492 beta: 0.5,
493 gamma: 0.3,
494 delta: 0.8,
495 threshold: 0.1,
496 description: "default policy".to_string(),
497 }
498 }
499}
500
501impl RoutingPolicy {
502 pub fn score(&self, neuron: usize, snapshot: &NeuronStateSnapshot, base_weight: f32) -> f32 {
504 let spikes = snapshot.spike_counts.get(neuron).copied().unwrap_or(0) as f32;
505 let adapt = snapshot.adaptation.get(neuron).copied().unwrap_or(0.0);
506 let err = snapshot.error.get(neuron).copied().unwrap_or(0.0);
507
508 self.alpha * spikes - self.beta * adapt - self.gamma * err + self.delta * base_weight
509 }
510
511 pub fn should_activate(&self, score: f32) -> bool {
513 score >= self.threshold
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn set_weight_rejects_truncated_u16_column() {
523 let mut map = SparseSynapticMap::<2>::new();
524 assert!(map.try_set_weight(0, 65_536, 1.0, 0.0).is_err());
525 assert_eq!(map.nnz(), 0);
526 assert_eq!(map.get_weight(0, 0), 0.0);
527 }
528
529 #[test]
530 #[should_panic(expected = "out of bounds")]
531 fn set_weight_panics_on_column_equal_to_n() {
532 let mut map = SparseSynapticMap::<2>::new();
533 map.set_weight(0, 2, 1.0, 0.0);
534 }
535
536 #[test]
537 fn set_weight_rejects_column_equal_to_n_without_mutation() {
538 let mut map = SparseSynapticMap::<2>::new();
539 map.try_set_weight(0, 1, 0.5, 0.0).unwrap();
540 assert!(map.try_set_weight(0, 2, 1.0, 0.0).is_err());
541 assert_eq!(map.nnz(), 1);
542 assert!((map.get_weight(0, 1) - 0.5).abs() < 1e-6);
543 }
544
545 #[test]
546 fn set_weight_rejects_invalid_source_row() {
547 let mut map = SparseSynapticMap::<2>::new();
548 assert!(map.try_set_weight(2, 0, 1.0, 0.0).is_err());
549 assert_eq!(map.nnz(), 0);
550 }
551
552 #[test]
553 fn set_weight_insert_update_remove_roundtrip() {
554 let mut map = SparseSynapticMap::<2>::new();
555 map.try_set_weight(0, 1, 1.0, 0.1).unwrap();
556 assert!((map.get_weight(0, 1) - 1.0).abs() < 1e-6);
557 map.try_set_weight(0, 1, 0.5, 0.1).unwrap();
558 assert!((map.get_weight(0, 1) - 0.5).abs() < 1e-6);
559 map.try_set_weight(0, 1, 0.01, 0.1).unwrap();
560 assert_eq!(map.get_weight(0, 1), 0.0);
561 assert_eq!(map.nnz(), 0);
562 }
563
564 #[test]
565 fn from_adjacency_rejects_target_equal_to_n() {
566 let adjacency = vec![
567 vec![Synapse {
568 target: 2,
569 weight: 1.0,
570 }],
571 vec![],
572 ];
573 assert!(SparseSynapticMap::<2>::try_from_adjacency(&adjacency).is_err());
574 }
575
576 #[test]
577 fn builder_rejects_invalid_target() {
578 let mut builder = SparseSynapticMapBuilder::<2>::new();
579 assert!(builder.try_connect(0, 2, 1.0).is_err());
580 assert!(builder.try_connect(0, 65_536, 1.0).is_err());
581 let map = builder.build();
582 assert_eq!(map.nnz(), 0);
583 }
584
585 #[test]
586 fn empty_and_single_neuron_maps() {
587 let mut empty = SparseSynapticMap::<0>::try_new().unwrap();
588 assert_eq!(empty.nnz(), 0);
589 assert!(empty.try_set_weight(0, 0, 1.0, 0.0).is_err());
590
591 let mut single = SparseSynapticMap::<1>::new();
592 single.try_set_weight(0, 0, 0.8, 0.0).unwrap();
593 assert!((single.get_weight(0, 0) - 0.8).abs() < 1e-6);
594 assert!(single.try_set_weight(0, 1, 0.8, 0.0).is_err());
595 }
596
597 #[test]
598 fn highest_representable_target_is_accepted() {
599 let mut map = SparseSynapticMap::<65_536>::try_new().unwrap();
601 map.try_set_weight(0, 65_535, 1.0, 0.0).unwrap();
602 assert!((map.get_weight(0, 65_535) - 1.0).abs() < 1e-6);
603 assert!((map.sparsity() - 1.0).abs() < 1e-6);
604 let (_row_ptr, cols, values) = map.try_to_gpu_arrays().unwrap();
605 assert_eq!(cols, vec![65_535]);
606 assert!((values[0] - 1.0).abs() < 1e-6);
607 }
608
609 #[test]
610 fn try_new_rejects_n_above_u16_address_space() {
611 assert!(SparseSynapticMap::<65_537>::try_new().is_err());
613 assert!(SparseSynapticMapBuilder::<65_537>::try_new().is_err());
614 }
615
616 #[test]
617 fn usizes_to_u32_accepts_u32_max() {
618 assert_eq!(usizes_to_u32(&[0, u32::MAX as usize]).unwrap().len(), 2);
619 }
620
621 #[cfg(target_pointer_width = "64")]
622 #[test]
623 fn usizes_to_u32_rejects_unrepresentable_offset() {
624 let too_big = (u32::MAX as usize) + 1;
625 assert!(usizes_to_u32(&[0, too_big]).is_err());
626 }
627}