1use serde::de::{Deserializer, Error as DeError};
9use serde::{Deserialize, Serialize};
10
11pub type NeuronId = u32;
15
16pub type DelayTicks = u16;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
30pub enum Polarity {
31 #[default]
33 Excitatory,
34 Inhibitory,
36}
37
38impl Polarity {
39 pub fn sign(self) -> f32 {
41 match self {
42 Self::Excitatory => 1.0,
43 Self::Inhibitory => -1.0,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
52pub struct SynapseDescriptor {
53 pub source: NeuronId,
55 pub target: NeuronId,
57 #[serde(deserialize_with = "deserialize_nonnegative_weight")]
59 pub weight: f32,
60 pub delay: DelayTicks,
62 pub polarity: Polarity,
64}
65
66impl SynapseDescriptor {
67 pub fn effective_weight(&self) -> f32 {
80 assert!(
81 weight_magnitude_is_valid(self.weight),
82 "synapse weight must be finite and non-negative, got {}",
83 self.weight
84 );
85 self.weight * self.polarity.sign()
86 }
87}
88
89pub(crate) fn weight_magnitude_is_valid(weight: f32) -> bool {
94 weight.is_finite() && weight >= 0.0
95}
96
97pub(crate) fn invalid_weight_magnitude_msg(weight: f32) -> String {
99 format!("synapse weight must be finite and non-negative, got {weight}")
100}
101
102pub(crate) fn signed_weight_agrees_with_polarity(weight: f32, polarity: Polarity) -> bool {
106 if !weight.is_finite() {
107 return false;
108 }
109 match polarity {
110 Polarity::Excitatory => weight >= 0.0,
111 Polarity::Inhibitory => weight <= 0.0,
112 }
113}
114
115pub(crate) fn invalid_signed_weight_polarity_msg(weight: f32, polarity: Polarity) -> String {
117 if !weight.is_finite() {
118 return format!("synapse weight must be finite, got {weight}");
119 }
120 match polarity {
121 Polarity::Excitatory => {
122 format!("excitatory synapse weight must be >= 0 (signed zero allowed), got {weight}")
123 }
124 Polarity::Inhibitory => {
125 format!("inhibitory synapse weight must be <= 0 (signed zero allowed), got {weight}")
126 }
127 }
128}
129
130fn deserialize_nonnegative_weight<'de, D>(deserializer: D) -> std::result::Result<f32, D::Error>
134where
135 D: Deserializer<'de>,
136{
137 let weight = f32::deserialize(deserializer)?;
138 if !weight_magnitude_is_valid(weight) {
139 return Err(DeError::custom(invalid_weight_magnitude_msg(weight)));
140 }
141 Ok(weight)
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct TopologyConfig {
149 pub neuron_count: usize,
151 pub inhibitory_fraction: f32,
154 pub max_delay: DelayTicks,
156 pub sparsity_threshold: f32,
158}
159
160impl Default for TopologyConfig {
161 fn default() -> Self {
162 Self {
163 neuron_count: 2048,
164 inhibitory_fraction: 0.20,
165 max_delay: 20,
166 sparsity_threshold: 0.01,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
175pub enum ConnectionModel {
176 Uniform { p: f32 },
178 DistanceDependent { p_max: f32, lambda: f32 },
181 SmallWorld { k: usize, beta: f32 },
187 ScaleFree { m0: usize, m: usize },
192 Layered {
195 layer_sizes: Vec<usize>,
196 inter_layer_p: f32,
197 },
198}
199
200impl Default for ConnectionModel {
201 fn default() -> Self {
202 Self::Uniform { p: 0.05 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
210pub enum DelayModel {
211 Fixed { delay: DelayTicks },
213 DistanceProportional {
216 speed: f32,
217 min_delay: DelayTicks,
218 max_delay: DelayTicks,
219 },
220 UniformRandom {
222 min_delay: DelayTicks,
223 max_delay: DelayTicks,
224 },
225}
226
227impl Default for DelayModel {
228 fn default() -> Self {
229 Self::Fixed { delay: 1 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn deserialize_rejects_negative_weight() {
239 let json = r#"{"source":0,"target":1,"weight":-0.5,"delay":1,"polarity":"Excitatory"}"#;
240 assert!(serde_json::from_str::<SynapseDescriptor>(json).is_err());
241 }
242
243 #[test]
244 fn deserialize_accepts_valid_weight() {
245 let json = r#"{"source":0,"target":1,"weight":0.5,"delay":1,"polarity":"Inhibitory"}"#;
246 let desc: SynapseDescriptor = serde_json::from_str(json).unwrap();
247 assert_eq!(desc.weight, 0.5);
248 assert!((desc.effective_weight() + 0.5).abs() < 1e-6);
249 }
250
251 #[test]
252 fn deserialize_accepts_zero_weight() {
253 let json = r#"{"source":0,"target":1,"weight":0.0,"delay":1,"polarity":"Excitatory"}"#;
254 let desc: SynapseDescriptor = serde_json::from_str(json).unwrap();
255 assert_eq!(desc.weight, 0.0);
256 assert_eq!(desc.effective_weight(), 0.0);
257 }
258
259 #[test]
260 #[should_panic(expected = "synapse weight must be finite and non-negative")]
261 fn effective_weight_panics_on_negative_inhibitory_magnitude() {
262 let desc = SynapseDescriptor {
263 source: 0,
264 target: 1,
265 weight: -0.5,
266 delay: 1,
267 polarity: Polarity::Inhibitory,
268 };
269 let _ = desc.effective_weight();
270 }
271
272 #[test]
273 fn weight_magnitude_accepts_signed_zero_and_rejects_non_finite() {
274 assert!(weight_magnitude_is_valid(0.0));
275 assert!(weight_magnitude_is_valid(-0.0));
276 assert!(weight_magnitude_is_valid(0.5));
277 assert!(!weight_magnitude_is_valid(-0.5));
278 assert!(!weight_magnitude_is_valid(f32::NAN));
279 assert!(!weight_magnitude_is_valid(f32::INFINITY));
280 assert!(!weight_magnitude_is_valid(f32::NEG_INFINITY));
281 }
282
283 #[test]
284 fn signed_weight_polarity_accepts_signed_zero_for_both() {
285 assert!(signed_weight_agrees_with_polarity(
286 0.0,
287 Polarity::Excitatory
288 ));
289 assert!(signed_weight_agrees_with_polarity(
290 -0.0,
291 Polarity::Excitatory
292 ));
293 assert!(signed_weight_agrees_with_polarity(
294 0.0,
295 Polarity::Inhibitory
296 ));
297 assert!(signed_weight_agrees_with_polarity(
298 -0.0,
299 Polarity::Inhibitory
300 ));
301 assert!(signed_weight_agrees_with_polarity(
302 0.5,
303 Polarity::Excitatory
304 ));
305 assert!(signed_weight_agrees_with_polarity(
306 -0.5,
307 Polarity::Inhibitory
308 ));
309 assert!(!signed_weight_agrees_with_polarity(
310 -0.5,
311 Polarity::Excitatory
312 ));
313 assert!(!signed_weight_agrees_with_polarity(
314 0.5,
315 Polarity::Inhibitory
316 ));
317 assert!(!signed_weight_agrees_with_polarity(
318 f32::NAN,
319 Polarity::Excitatory
320 ));
321 assert!(!signed_weight_agrees_with_polarity(
322 f32::INFINITY,
323 Polarity::Inhibitory
324 ));
325 assert!(!signed_weight_agrees_with_polarity(
326 f32::NEG_INFINITY,
327 Polarity::Excitatory
328 ));
329 }
330}