1use serde::de::{Deserializer, Error as DeError};
10use serde::{Deserialize, Serialize};
11
12use crate::error::{MeshError, Result};
13use crate::types::{
14 DelayTicks, NeuronId, Polarity, SynapseDescriptor, invalid_signed_weight_polarity_msg,
15 invalid_weight_magnitude_msg, signed_weight_agrees_with_polarity, weight_magnitude_is_valid,
16};
17
18#[derive(Clone, Debug, PartialEq, Serialize)]
33pub struct SynapticGraph {
34 neuron_count: usize,
36 row_ptr: Vec<usize>,
39 targets: Vec<NeuronId>,
41 weights: Vec<f32>,
43 delays: Vec<DelayTicks>,
45 polarities: Vec<Polarity>,
47}
48
49#[derive(Deserialize)]
50struct RawSynapticGraph {
51 neuron_count: usize,
52 row_ptr: Vec<usize>,
53 targets: Vec<NeuronId>,
54 weights: Vec<f32>,
55 delays: Vec<DelayTicks>,
56 polarities: Vec<Polarity>,
57}
58
59impl RawSynapticGraph {
60 fn into_graph(self) -> std::result::Result<SynapticGraph, String> {
66 let nnz = validate_row_ptr(&self.row_ptr, self.neuron_count)?;
67 validate_edge_array_lengths(
68 nnz,
69 [
70 ("targets", self.targets.len()),
71 ("weights", self.weights.len()),
72 ("delays", self.delays.len()),
73 ("polarities", self.polarities.len()),
74 ],
75 )?;
76 validate_targets_in_bounds(&self.targets, self.neuron_count)?;
77 validate_weights_finite(&self.weights)?;
78 validate_weights_agree_with_polarities(&self.weights, &self.polarities)?;
79
80 Ok(SynapticGraph {
81 neuron_count: self.neuron_count,
82 row_ptr: self.row_ptr,
83 targets: self.targets,
84 weights: self.weights,
85 delays: self.delays,
86 polarities: self.polarities,
87 })
88 }
89}
90
91fn validate_row_ptr(row_ptr: &[usize], neuron_count: usize) -> std::result::Result<usize, String> {
94 let expected_len = neuron_count
95 .checked_add(1)
96 .ok_or_else(|| format!("neuron_count {neuron_count} is too large"))?;
97 if row_ptr.len() != expected_len {
98 return Err(format!(
99 "row_ptr length {} does not match neuron_count + 1 ({})",
100 row_ptr.len(),
101 expected_len
102 ));
103 }
104 if row_ptr.first().copied() != Some(0) {
105 return Err("row_ptr must start at 0".to_string());
106 }
107 if !row_ptr.windows(2).all(|w| w[0] <= w[1]) {
108 return Err("row_ptr must be non-decreasing".to_string());
109 }
110 Ok(*row_ptr.last().expect("row_ptr is non-empty"))
111}
112
113fn validate_edge_array_lengths(
115 nnz: usize,
116 arrays: [(&str, usize); 4],
117) -> std::result::Result<(), String> {
118 for (name, len) in arrays {
119 if len != nnz {
120 return Err(format!(
121 "{name} length {len} does not match row_ptr's final offset {nnz}"
122 ));
123 }
124 }
125 Ok(())
126}
127
128fn validate_targets_in_bounds(
130 targets: &[NeuronId],
131 neuron_count: usize,
132) -> std::result::Result<(), String> {
133 if targets.iter().any(|&t| t as usize >= neuron_count) {
134 return Err("target neuron id out of bounds".to_string());
135 }
136 Ok(())
137}
138
139fn validate_weights_finite(weights: &[f32]) -> std::result::Result<(), String> {
143 if weights.iter().any(|w| !w.is_finite()) {
144 return Err("synapse weight must be finite".to_string());
145 }
146 Ok(())
147}
148
149fn validate_weights_agree_with_polarities(
152 weights: &[f32],
153 polarities: &[Polarity],
154) -> std::result::Result<(), String> {
155 for (&weight, &polarity) in weights.iter().zip(polarities.iter()) {
156 if !signed_weight_agrees_with_polarity(weight, polarity) {
157 return Err(invalid_signed_weight_polarity_msg(weight, polarity));
158 }
159 }
160 Ok(())
161}
162
163impl<'de> Deserialize<'de> for SynapticGraph {
164 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
165 where
166 D: Deserializer<'de>,
167 {
168 RawSynapticGraph::deserialize(deserializer)?
169 .into_graph()
170 .map_err(DeError::custom)
171 }
172}
173
174impl SynapticGraph {
175 pub fn new(n: usize) -> Self {
177 Self {
178 neuron_count: n,
179 row_ptr: vec![0; n + 1],
180 targets: Vec::new(),
181 weights: Vec::new(),
182 delays: Vec::new(),
183 polarities: Vec::new(),
184 }
185 }
186
187 pub fn from_descriptors(
194 neuron_count: usize,
195 descriptors: &[SynapseDescriptor],
196 ) -> Result<Self> {
197 Self::validate_descriptor_indices(neuron_count, descriptors)?;
198 Self::validate_descriptor_weights(descriptors)?;
199
200 let mut counts = vec![0usize; neuron_count];
202 for desc in descriptors {
203 counts[desc.source as usize] += 1;
204 }
205
206 let mut row_ptr = Vec::with_capacity(neuron_count + 1);
208 row_ptr.push(0);
209 for &c in &counts {
210 row_ptr.push(row_ptr.last().unwrap() + c);
211 }
212
213 let nnz = descriptors.len();
214 let mut targets = vec![0u32; nnz];
215 let mut weights = vec![0.0f32; nnz];
216 let mut delays = vec![0u16; nnz];
217 let mut polarities = vec![Polarity::Excitatory; nnz];
218
219 let mut cursor = counts.clone();
221 cursor.fill(0);
222
223 for desc in descriptors {
224 let src = desc.source as usize;
225 let pos = row_ptr[src] + cursor[src];
226 targets[pos] = desc.target;
227 weights[pos] = desc.effective_weight();
228 delays[pos] = desc.delay;
229 polarities[pos] = desc.polarity;
230 cursor[src] += 1;
231 }
232
233 Ok(Self {
234 neuron_count,
235 row_ptr,
236 targets,
237 weights,
238 delays,
239 polarities,
240 })
241 }
242
243 fn validate_descriptor_indices(
245 neuron_count: usize,
246 descriptors: &[SynapseDescriptor],
247 ) -> Result<()> {
248 if neuron_count == 0 && !descriptors.is_empty() {
249 return Err(MeshError::InvalidConfig(
250 "descriptors provided for a graph with 0 neurons".into(),
251 ));
252 }
253 for desc in descriptors {
254 if desc.source as usize >= neuron_count {
255 return Err(MeshError::IndexOutOfBounds {
256 index: desc.source as usize,
257 max: neuron_count.saturating_sub(1),
258 });
259 }
260 if desc.target as usize >= neuron_count {
261 return Err(MeshError::IndexOutOfBounds {
262 index: desc.target as usize,
263 max: neuron_count.saturating_sub(1),
264 });
265 }
266 }
267 Ok(())
268 }
269
270 fn validate_descriptor_weights(descriptors: &[SynapseDescriptor]) -> Result<()> {
273 for desc in descriptors {
274 if !weight_magnitude_is_valid(desc.weight) {
275 return Err(MeshError::InvalidConfig(invalid_weight_magnitude_msg(
276 desc.weight,
277 )));
278 }
279 }
280 Ok(())
281 }
282
283 pub fn neuron_count(&self) -> usize {
285 self.neuron_count
286 }
287
288 pub fn synapse_count(&self) -> usize {
290 self.targets.len()
291 }
292
293 pub fn sparsity(&self) -> f32 {
295 let total = self.neuron_count * self.neuron_count;
296 if total == 0 {
297 return 1.0;
298 }
299 1.0 - (self.synapse_count() as f32 / total as f32)
300 }
301
302 pub fn outgoing(
305 &self,
306 source: usize,
307 ) -> impl Iterator<Item = (NeuronId, f32, DelayTicks, Polarity)> + '_ {
308 let start = self.row_ptr[source];
309 let end = self.row_ptr[source + 1];
310 (start..end).map(move |i| {
311 (
312 self.targets[i],
313 self.weights[i],
314 self.delays[i],
315 self.polarities[i],
316 )
317 })
318 }
319
320 pub fn out_degree(&self, neuron: usize) -> usize {
322 self.row_ptr[neuron + 1] - self.row_ptr[neuron]
323 }
324
325 pub fn max_delay(&self) -> DelayTicks {
327 self.delays.iter().copied().max().unwrap_or(0)
328 }
329
330 pub fn mean_degree(&self) -> f32 {
332 if self.neuron_count == 0 {
333 return 0.0;
334 }
335 self.synapse_count() as f32 / self.neuron_count as f32
336 }
337
338 pub fn to_gpu_arrays(&self) -> (Vec<u32>, Vec<u32>, Vec<f32>, Vec<u16>) {
340 let row_ptr: Vec<u32> = self.row_ptr.iter().map(|&x| x as u32).collect();
341 let targets: Vec<u32> = self.targets.to_vec();
342 (row_ptr, targets, self.weights.clone(), self.delays.clone())
343 }
344
345 pub fn row_ptr(&self) -> &[usize] {
347 &self.row_ptr
348 }
349
350 pub fn targets(&self) -> &[NeuronId] {
352 &self.targets
353 }
354
355 pub fn weights(&self) -> &[f32] {
357 &self.weights
358 }
359
360 pub fn delays_slice(&self) -> &[DelayTicks] {
362 &self.delays
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn empty_graph_has_no_synapses() {
372 let g = SynapticGraph::new(100);
373 assert_eq!(g.neuron_count(), 100);
374 assert_eq!(g.synapse_count(), 0);
375 assert!((g.sparsity() - 1.0).abs() < 1e-6);
376 }
377
378 #[test]
379 fn from_descriptors_roundtrip() {
380 let descs = vec![
381 SynapseDescriptor {
382 source: 0,
383 target: 1,
384 weight: 0.9,
385 delay: 3,
386 polarity: Polarity::Excitatory,
387 },
388 SynapseDescriptor {
389 source: 0,
390 target: 2,
391 weight: 0.15,
392 delay: 1,
393 polarity: Polarity::Inhibitory,
394 },
395 SynapseDescriptor {
396 source: 1,
397 target: 0,
398 weight: 0.5,
399 delay: 2,
400 polarity: Polarity::Excitatory,
401 },
402 ];
403
404 let graph = SynapticGraph::from_descriptors(3, &descs).unwrap();
405 assert_eq!(graph.neuron_count(), 3);
406 assert_eq!(graph.synapse_count(), 3);
407
408 assert_eq!(graph.out_degree(0), 2);
410 assert_eq!(graph.out_degree(1), 1);
412 assert_eq!(graph.out_degree(2), 0);
414
415 let edges: Vec<_> = graph.outgoing(0).collect();
417 let inh_edge = edges
418 .iter()
419 .find(|(_, _, _, p)| *p == Polarity::Inhibitory)
420 .unwrap();
421 assert!(inh_edge.1 < 0.0);
422 }
423
424 #[test]
425 fn synaptic_graph_json_roundtrip() {
426 let descs = vec![
427 SynapseDescriptor {
428 source: 0,
429 target: 1,
430 weight: 0.9,
431 delay: 3,
432 polarity: Polarity::Excitatory,
433 },
434 SynapseDescriptor {
435 source: 0,
436 target: 2,
437 weight: 0.15,
438 delay: 1,
439 polarity: Polarity::Inhibitory,
440 },
441 SynapseDescriptor {
442 source: 2,
443 target: 1,
444 weight: 0.4,
445 delay: 5,
446 polarity: Polarity::Excitatory,
447 },
448 ];
449 let graph = SynapticGraph::from_descriptors(3, &descs)
450 .expect("hand-written descriptors must build a graph");
451 let json = serde_json::to_string(&graph).expect("serialize SynapticGraph to JSON");
452 let restored: SynapticGraph =
453 serde_json::from_str(&json).expect("deserialize SynapticGraph from JSON");
454 assert_eq!(restored, graph);
455 }
456
457 #[test]
458 fn max_delay_reports_correctly() {
459 let descs = vec![
460 SynapseDescriptor {
461 source: 0,
462 target: 1,
463 weight: 0.5,
464 delay: 7,
465 polarity: Polarity::Excitatory,
466 },
467 SynapseDescriptor {
468 source: 1,
469 target: 0,
470 weight: 0.5,
471 delay: 3,
472 polarity: Polarity::Excitatory,
473 },
474 ];
475 let graph = SynapticGraph::from_descriptors(2, &descs).unwrap();
476 assert_eq!(graph.max_delay(), 7);
477 }
478
479 #[test]
480 fn deserialize_rejects_row_ptr_length_mismatch() {
481 let json = r#"{"neuron_count":2,"row_ptr":[0,1],"targets":[],"weights":[],"delays":[],"polarities":[]}"#;
482 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
483 }
484
485 #[test]
486 fn deserialize_rejects_non_monotonic_row_ptr() {
487 let json = r#"{"neuron_count":2,"row_ptr":[0,3,1],"targets":[0,0,0],"weights":[0.1,0.1,0.1],"delays":[1,1,1],"polarities":["Excitatory","Excitatory","Excitatory"]}"#;
488 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
489 }
490
491 #[test]
492 fn deserialize_rejects_edge_array_length_mismatch() {
493 let json = r#"{"neuron_count":1,"row_ptr":[0,2],"targets":[0],"weights":[0.1,0.1],"delays":[1,1],"polarities":["Excitatory","Excitatory"]}"#;
494 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
495 }
496
497 #[test]
498 fn validate_weights_finite_rejects_nan_and_infinite() {
499 assert!(validate_weights_finite(&[0.1, f32::NAN]).is_err());
504 assert!(validate_weights_finite(&[0.1, f32::INFINITY]).is_err());
505 assert!(validate_weights_finite(&[0.1, f32::NEG_INFINITY]).is_err());
506 assert!(validate_weights_finite(&[0.1, -0.4, 2.0]).is_ok());
507 }
508
509 #[test]
510 fn deserialize_rejects_usize_max_neuron_count_without_overflow_panic() {
511 let json = r#"{"neuron_count":18446744073709551615,"row_ptr":[0,1],"targets":[0],"weights":[0.1],"delays":[1],"polarities":["Excitatory"]}"#;
512 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
513 }
514
515 #[test]
516 fn deserialize_rejects_out_of_range_target() {
517 let json = r#"{"neuron_count":1,"row_ptr":[0,1],"targets":[5],"weights":[0.1],"delays":[1],"polarities":["Excitatory"]}"#;
518 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
519 }
520
521 #[test]
522 fn out_of_bounds_source_rejected() {
523 let descs = vec![SynapseDescriptor {
524 source: 5,
525 target: 0,
526 weight: 0.5,
527 delay: 1,
528 polarity: Polarity::Excitatory,
529 }];
530 assert!(SynapticGraph::from_descriptors(3, &descs).is_err());
531 }
532
533 fn descriptor(weight: f32, polarity: Polarity) -> SynapseDescriptor {
534 SynapseDescriptor {
535 source: 0,
536 target: 1,
537 weight,
538 delay: 0,
539 polarity,
540 }
541 }
542
543 #[test]
544 fn from_descriptors_rejects_negative_inhibitory_magnitude() {
545 let invalid = descriptor(-0.5, Polarity::Inhibitory);
548 assert!(SynapticGraph::from_descriptors(2, &[invalid]).is_err());
549 }
550
551 #[test]
552 fn from_descriptors_rejects_nan_and_infinite_magnitudes() {
553 assert!(
554 SynapticGraph::from_descriptors(2, &[descriptor(f32::NAN, Polarity::Excitatory)])
555 .is_err()
556 );
557 assert!(
558 SynapticGraph::from_descriptors(2, &[descriptor(f32::INFINITY, Polarity::Excitatory)])
559 .is_err()
560 );
561 assert!(
562 SynapticGraph::from_descriptors(
563 2,
564 &[descriptor(f32::NEG_INFINITY, Polarity::Inhibitory)]
565 )
566 .is_err()
567 );
568 }
569
570 #[test]
571 fn from_descriptors_accepts_zero_and_positive_magnitudes() {
572 let zero = SynapticGraph::from_descriptors(2, &[descriptor(0.0, Polarity::Inhibitory)]);
573 assert!(zero.is_ok());
574 let signed_zero =
575 SynapticGraph::from_descriptors(2, &[descriptor(-0.0, Polarity::Excitatory)]);
576 assert!(signed_zero.is_ok());
577 let positive = SynapticGraph::from_descriptors(2, &[descriptor(0.5, Polarity::Excitatory)]);
578 assert!(positive.is_ok());
579 let edges: Vec<_> = positive.unwrap().outgoing(0).collect();
580 assert!((edges[0].1 - 0.5).abs() < 1e-6);
581 }
582
583 #[test]
584 fn deserialize_rejects_positive_inhibitory_weight() {
585 let json = r#"{"neuron_count":2,"row_ptr":[0,1,1],"targets":[1],"weights":[0.5],"delays":[0],"polarities":["Inhibitory"]}"#;
586 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
587 }
588
589 #[test]
590 fn deserialize_rejects_negative_excitatory_weight() {
591 let json = r#"{"neuron_count":2,"row_ptr":[0,1,1],"targets":[1],"weights":[-0.5],"delays":[0],"polarities":["Excitatory"]}"#;
592 assert!(serde_json::from_str::<SynapticGraph>(json).is_err());
593 }
594
595 #[test]
596 fn deserialize_accepts_zero_weight_for_both_polarities() {
597 let inh = r#"{"neuron_count":2,"row_ptr":[0,1,1],"targets":[1],"weights":[0.0],"delays":[0],"polarities":["Inhibitory"]}"#;
598 let exc = r#"{"neuron_count":2,"row_ptr":[0,1,1],"targets":[1],"weights":[0.0],"delays":[0],"polarities":["Excitatory"]}"#;
599 assert!(serde_json::from_str::<SynapticGraph>(inh).is_ok());
600 assert!(serde_json::from_str::<SynapticGraph>(exc).is_ok());
601 }
602
603 #[test]
604 fn validate_weights_agree_with_polarities_rejects_nan_without_json() {
605 assert!(
607 validate_weights_agree_with_polarities(&[f32::NAN], &[Polarity::Excitatory]).is_err()
608 );
609 assert!(
610 validate_weights_agree_with_polarities(&[f32::INFINITY], &[Polarity::Inhibitory])
611 .is_err()
612 );
613 assert!(
614 validate_weights_agree_with_polarities(&[f32::NEG_INFINITY], &[Polarity::Excitatory])
615 .is_err()
616 );
617 assert!(validate_weights_agree_with_polarities(&[-0.0], &[Polarity::Inhibitory]).is_ok());
618 assert!(validate_weights_agree_with_polarities(&[0.0], &[Polarity::Excitatory]).is_ok());
619 }
620}