use serde::de::{Deserializer, Error as DeError};
use serde::{Deserialize, Serialize};
pub type NeuronId = u32;
pub type DelayTicks = u16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum Polarity {
#[default]
Excitatory,
Inhibitory,
}
impl Polarity {
pub fn sign(self) -> f32 {
match self {
Self::Excitatory => 1.0,
Self::Inhibitory => -1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SynapseDescriptor {
pub source: NeuronId,
pub target: NeuronId,
#[serde(deserialize_with = "deserialize_nonnegative_weight")]
pub weight: f32,
pub delay: DelayTicks,
pub polarity: Polarity,
}
impl SynapseDescriptor {
pub fn effective_weight(&self) -> f32 {
assert!(
weight_magnitude_is_valid(self.weight),
"synapse weight must be finite and non-negative, got {}",
self.weight
);
self.weight * self.polarity.sign()
}
}
pub(crate) fn weight_magnitude_is_valid(weight: f32) -> bool {
weight.is_finite() && weight >= 0.0
}
pub(crate) fn invalid_weight_magnitude_msg(weight: f32) -> String {
format!("synapse weight must be finite and non-negative, got {weight}")
}
pub(crate) fn signed_weight_agrees_with_polarity(weight: f32, polarity: Polarity) -> bool {
if !weight.is_finite() {
return false;
}
match polarity {
Polarity::Excitatory => weight >= 0.0,
Polarity::Inhibitory => weight <= 0.0,
}
}
pub(crate) fn invalid_signed_weight_polarity_msg(weight: f32, polarity: Polarity) -> String {
if !weight.is_finite() {
return format!("synapse weight must be finite, got {weight}");
}
match polarity {
Polarity::Excitatory => {
format!("excitatory synapse weight must be >= 0 (signed zero allowed), got {weight}")
}
Polarity::Inhibitory => {
format!("inhibitory synapse weight must be <= 0 (signed zero allowed), got {weight}")
}
}
}
fn deserialize_nonnegative_weight<'de, D>(deserializer: D) -> std::result::Result<f32, D::Error>
where
D: Deserializer<'de>,
{
let weight = f32::deserialize(deserializer)?;
if !weight_magnitude_is_valid(weight) {
return Err(DeError::custom(invalid_weight_magnitude_msg(weight)));
}
Ok(weight)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopologyConfig {
pub neuron_count: usize,
pub inhibitory_fraction: f32,
pub max_delay: DelayTicks,
pub sparsity_threshold: f32,
}
impl Default for TopologyConfig {
fn default() -> Self {
Self {
neuron_count: 2048,
inhibitory_fraction: 0.20,
max_delay: 20,
sparsity_threshold: 0.01,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConnectionModel {
Uniform { p: f32 },
DistanceDependent { p_max: f32, lambda: f32 },
SmallWorld { k: usize, beta: f32 },
ScaleFree { m0: usize, m: usize },
Layered {
layer_sizes: Vec<usize>,
inter_layer_p: f32,
},
}
impl Default for ConnectionModel {
fn default() -> Self {
Self::Uniform { p: 0.05 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DelayModel {
Fixed { delay: DelayTicks },
DistanceProportional {
speed: f32,
min_delay: DelayTicks,
max_delay: DelayTicks,
},
UniformRandom {
min_delay: DelayTicks,
max_delay: DelayTicks,
},
}
impl Default for DelayModel {
fn default() -> Self {
Self::Fixed { delay: 1 }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_rejects_negative_weight() {
let json = r#"{"source":0,"target":1,"weight":-0.5,"delay":1,"polarity":"Excitatory"}"#;
assert!(serde_json::from_str::<SynapseDescriptor>(json).is_err());
}
#[test]
fn deserialize_accepts_valid_weight() {
let json = r#"{"source":0,"target":1,"weight":0.5,"delay":1,"polarity":"Inhibitory"}"#;
let desc: SynapseDescriptor = serde_json::from_str(json).unwrap();
assert_eq!(desc.weight, 0.5);
assert!((desc.effective_weight() + 0.5).abs() < 1e-6);
}
#[test]
fn deserialize_accepts_zero_weight() {
let json = r#"{"source":0,"target":1,"weight":0.0,"delay":1,"polarity":"Excitatory"}"#;
let desc: SynapseDescriptor = serde_json::from_str(json).unwrap();
assert_eq!(desc.weight, 0.0);
assert_eq!(desc.effective_weight(), 0.0);
}
#[test]
#[should_panic(expected = "synapse weight must be finite and non-negative")]
fn effective_weight_panics_on_negative_inhibitory_magnitude() {
let desc = SynapseDescriptor {
source: 0,
target: 1,
weight: -0.5,
delay: 1,
polarity: Polarity::Inhibitory,
};
let _ = desc.effective_weight();
}
#[test]
fn weight_magnitude_accepts_signed_zero_and_rejects_non_finite() {
assert!(weight_magnitude_is_valid(0.0));
assert!(weight_magnitude_is_valid(-0.0));
assert!(weight_magnitude_is_valid(0.5));
assert!(!weight_magnitude_is_valid(-0.5));
assert!(!weight_magnitude_is_valid(f32::NAN));
assert!(!weight_magnitude_is_valid(f32::INFINITY));
assert!(!weight_magnitude_is_valid(f32::NEG_INFINITY));
}
#[test]
fn signed_weight_polarity_accepts_signed_zero_for_both() {
assert!(signed_weight_agrees_with_polarity(
0.0,
Polarity::Excitatory
));
assert!(signed_weight_agrees_with_polarity(
-0.0,
Polarity::Excitatory
));
assert!(signed_weight_agrees_with_polarity(
0.0,
Polarity::Inhibitory
));
assert!(signed_weight_agrees_with_polarity(
-0.0,
Polarity::Inhibitory
));
assert!(signed_weight_agrees_with_polarity(
0.5,
Polarity::Excitatory
));
assert!(signed_weight_agrees_with_polarity(
-0.5,
Polarity::Inhibitory
));
assert!(!signed_weight_agrees_with_polarity(
-0.5,
Polarity::Excitatory
));
assert!(!signed_weight_agrees_with_polarity(
0.5,
Polarity::Inhibitory
));
assert!(!signed_weight_agrees_with_polarity(
f32::NAN,
Polarity::Excitatory
));
assert!(!signed_weight_agrees_with_polarity(
f32::INFINITY,
Polarity::Inhibitory
));
assert!(!signed_weight_agrees_with_polarity(
f32::NEG_INFINITY,
Polarity::Excitatory
));
}
}