synapse_models/
network.rs

1//! Network-level synaptic dynamics.
2//!
3//! This module provides structures for managing synaptic networks including:
4//! - Chemical synapses
5//! - Gap junctions (electrical synapses)
6//! - Ephaptic coupling
7//! - Neuromodulation
8//! - Spike propagation with delays
9
10use crate::error::{Result, SynapseError};
11use crate::synapse::Synapse;
12use std::collections::{HashMap, VecDeque};
13use std::sync::{Arc, Mutex};
14
15/// Synaptic connection in a network.
16#[derive(Debug, Clone)]
17pub struct Connection {
18    /// Presynaptic neuron ID.
19    pub pre_neuron: usize,
20
21    /// Postsynaptic neuron ID.
22    pub post_neuron: usize,
23
24    /// Synapse model.
25    pub synapse: Synapse,
26}
27
28/// Gap junction (electrical synapse).
29///
30/// Provides bidirectional, instantaneous electrical coupling between neurons.
31#[derive(Debug, Clone)]
32pub struct GapJunction {
33    /// First neuron ID.
34    pub neuron1: usize,
35
36    /// Second neuron ID.
37    pub neuron2: usize,
38
39    /// Coupling conductance (nS).
40    pub conductance: f64,
41
42    /// Whether the junction is rectifying (one-way).
43    pub rectifying: bool,
44}
45
46impl GapJunction {
47    /// Create a new gap junction.
48    pub fn new(neuron1: usize, neuron2: usize, conductance: f64) -> Result<Self> {
49        if conductance < 0.0 {
50            return Err(SynapseError::InvalidWeight(conductance, 0.0, f64::INFINITY));
51        }
52
53        Ok(Self {
54            neuron1,
55            neuron2,
56            conductance,
57            rectifying: false,
58        })
59    }
60
61    /// Calculate current from neuron1 to neuron2.
62    pub fn current(&self, v1: f64, v2: f64) -> f64 {
63        let i12 = self.conductance * (v2 - v1);
64
65        if self.rectifying && i12 < 0.0 {
66            0.0
67        } else {
68            i12
69        }
70    }
71}
72
73/// Ephaptic coupling - electric field effects between nearby neurons.
74///
75/// Models extracellular potential effects on neighboring neurons.
76#[derive(Debug, Clone)]
77pub struct EphapticCoupling {
78    /// Source neuron ID.
79    pub source_neuron: usize,
80
81    /// Target neuron ID.
82    pub target_neuron: usize,
83
84    /// Coupling strength (mV/Hz).
85    pub strength: f64,
86
87    /// Distance between neurons (μm).
88    pub distance: f64,
89
90    /// Time constant for field effects (ms).
91    pub tau: f64,
92}
93
94impl EphapticCoupling {
95    /// Create new ephaptic coupling.
96    pub fn new(source: usize, target: usize, distance: f64) -> Self {
97        // Strength decays with distance
98        let strength = 0.01 / (1.0 + distance / 100.0);
99
100        Self {
101            source_neuron: source,
102            target_neuron: target,
103            strength,
104            distance,
105            tau: 10.0, // 10 ms time constant
106        }
107    }
108
109    /// Calculate ephaptic effect on target neuron.
110    ///
111    /// # Arguments
112    /// * `source_rate` - Firing rate of source neuron (Hz)
113    pub fn effect(&self, source_rate: f64) -> f64 {
114        self.strength * source_rate
115    }
116}
117
118/// Neuromodulator state.
119#[derive(Debug, Clone)]
120pub struct NeuromodulatorState {
121    /// Dopamine concentration (μM).
122    pub dopamine: f64,
123
124    /// Serotonin concentration (μM).
125    pub serotonin: f64,
126
127    /// Acetylcholine concentration (μM).
128    pub acetylcholine: f64,
129
130    /// Norepinephrine concentration (μM).
131    pub norepinephrine: f64,
132
133    /// Time constants for decay (ms).
134    pub tau_decay: f64,
135}
136
137impl Default for NeuromodulatorState {
138    fn default() -> Self {
139        Self {
140            dopamine: 0.0,
141            serotonin: 0.0,
142            acetylcholine: 0.0,
143            norepinephrine: 0.0,
144            tau_decay: 1000.0, // Slow decay
145        }
146    }
147}
148
149impl NeuromodulatorState {
150    /// Create new neuromodulator state.
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Update neuromodulator concentrations.
156    ///
157    /// # Arguments
158    /// * `dt` - Time step (ms)
159    pub fn update(&mut self, dt: f64) {
160        // Exponential decay
161        let decay = (-dt / self.tau_decay).exp();
162        self.dopamine *= decay;
163        self.serotonin *= decay;
164        self.acetylcholine *= decay;
165        self.norepinephrine *= decay;
166    }
167
168    /// Release dopamine.
169    pub fn release_dopamine(&mut self, amount: f64) {
170        self.dopamine += amount;
171    }
172
173    /// Release serotonin.
174    pub fn release_serotonin(&mut self, amount: f64) {
175        self.serotonin += amount;
176    }
177
178    /// Release acetylcholine.
179    pub fn release_acetylcholine(&mut self, amount: f64) {
180        self.acetylcholine += amount;
181    }
182
183    /// Release norepinephrine.
184    pub fn release_norepinephrine(&mut self, amount: f64) {
185        self.norepinephrine += amount;
186    }
187
188    /// Get total neuromodulatory effect on synaptic weight.
189    pub fn weight_modulation(&self) -> f64 {
190        // Dopamine enhances learning
191        let da_effect = 1.0 + 0.5 * self.dopamine.min(1.0);
192
193        // Serotonin slightly suppresses
194        let sero_effect = 1.0 - 0.2 * self.serotonin.min(1.0);
195
196        // ACh enhances
197        let ach_effect = 1.0 + 0.3 * self.acetylcholine.min(1.0);
198
199        // NE enhances
200        let ne_effect = 1.0 + 0.4 * self.norepinephrine.min(1.0);
201
202        da_effect * sero_effect * ach_effect * ne_effect
203    }
204}
205
206/// Delayed spike event.
207#[derive(Debug, Clone, Copy)]
208struct DelayedSpike {
209    /// Target neuron ID.
210    #[allow(dead_code)]
211    target_neuron: usize,
212
213    /// Connection index.
214    #[allow(dead_code)]
215    connection_idx: usize,
216
217    /// Arrival time (ms).
218    arrival_time: f64,
219}
220
221/// Synaptic network.
222///
223/// Manages a network of neurons connected by synapses.
224pub struct SynapticNetwork {
225    /// Number of neurons in the network.
226    pub n_neurons: usize,
227
228    /// Chemical synaptic connections.
229    pub connections: Vec<Connection>,
230
231    /// Gap junctions.
232    pub gap_junctions: Vec<GapJunction>,
233
234    /// Ephaptic couplings.
235    pub ephaptic_couplings: Vec<EphapticCoupling>,
236
237    /// Neuromodulator state.
238    pub neuromodulators: NeuromodulatorState,
239
240    /// Current time (ms).
241    current_time: f64,
242
243    /// Queue of delayed spikes.
244    spike_queue: VecDeque<DelayedSpike>,
245
246    /// Adjacency list: neuron -> connections.
247    adjacency: HashMap<usize, Vec<usize>>,
248
249    /// Neuron voltages (mV).
250    neuron_voltages: Vec<f64>,
251}
252
253impl SynapticNetwork {
254    /// Create a new synaptic network.
255    ///
256    /// # Arguments
257    /// * `n_neurons` - Number of neurons in the network
258    pub fn new(n_neurons: usize) -> Self {
259        Self {
260            n_neurons,
261            connections: Vec::new(),
262            gap_junctions: Vec::new(),
263            ephaptic_couplings: Vec::new(),
264            neuromodulators: NeuromodulatorState::new(),
265            current_time: 0.0,
266            spike_queue: VecDeque::new(),
267            adjacency: HashMap::new(),
268            neuron_voltages: vec![-65.0; n_neurons],
269        }
270    }
271
272    /// Add a chemical synapse connection.
273    pub fn add_connection(&mut self, pre: usize, post: usize, synapse: Synapse) -> Result<()> {
274        if pre >= self.n_neurons {
275            return Err(SynapseError::NeuronNotFound(pre));
276        }
277        if post >= self.n_neurons {
278            return Err(SynapseError::NeuronNotFound(post));
279        }
280
281        let conn_idx = self.connections.len();
282        self.connections.push(Connection {
283            pre_neuron: pre,
284            post_neuron: post,
285            synapse,
286        });
287
288        // Update adjacency list
289        self.adjacency.entry(pre).or_insert_with(Vec::new).push(conn_idx);
290
291        Ok(())
292    }
293
294    /// Add a gap junction.
295    pub fn add_gap_junction(&mut self, neuron1: usize, neuron2: usize, conductance: f64) -> Result<()> {
296        if neuron1 >= self.n_neurons || neuron2 >= self.n_neurons {
297            return Err(SynapseError::NeuronNotFound(neuron1.max(neuron2)));
298        }
299
300        self.gap_junctions.push(GapJunction::new(neuron1, neuron2, conductance)?);
301        Ok(())
302    }
303
304    /// Add ephaptic coupling.
305    pub fn add_ephaptic_coupling(&mut self, source: usize, target: usize, distance: f64) -> Result<()> {
306        if source >= self.n_neurons || target >= self.n_neurons {
307            return Err(SynapseError::NeuronNotFound(source.max(target)));
308        }
309
310        self.ephaptic_couplings.push(EphapticCoupling::new(source, target, distance));
311        Ok(())
312    }
313
314    /// Process spike from a neuron.
315    ///
316    /// # Arguments
317    /// * `neuron_id` - ID of spiking neuron
318    pub fn spike(&mut self, neuron_id: usize) -> Result<()> {
319        if neuron_id >= self.n_neurons {
320            return Err(SynapseError::NeuronNotFound(neuron_id));
321        }
322
323        // Find all connections from this neuron
324        if let Some(conn_indices) = self.adjacency.get(&neuron_id) {
325            for &conn_idx in conn_indices {
326                let conn = &mut self.connections[conn_idx];
327                let delay = conn.synapse.delay;
328
329                // Trigger presynaptic spike
330                conn.synapse.presynaptic_spike(self.current_time)?;
331
332                // Schedule delayed spike arrival
333                self.spike_queue.push_back(DelayedSpike {
334                    target_neuron: conn.post_neuron,
335                    connection_idx: conn_idx,
336                    arrival_time: self.current_time + delay,
337                });
338            }
339        }
340
341        Ok(())
342    }
343
344    /// Update network dynamics.
345    ///
346    /// # Arguments
347    /// * `neuron_voltages` - Current voltage of each neuron (mV)
348    /// * `dt` - Time step (ms)
349    pub fn update(&mut self, neuron_voltages: &[f64], dt: f64) -> Result<()> {
350        if neuron_voltages.len() != self.n_neurons {
351            return Err(SynapseError::InvalidNetwork(
352                format!("Expected {} voltages, got {}", self.n_neurons, neuron_voltages.len())
353            ));
354        }
355
356        self.neuron_voltages.copy_from_slice(neuron_voltages);
357        self.current_time += dt;
358
359        // Process delayed spikes that have arrived
360        while let Some(spike) = self.spike_queue.front() {
361            if spike.arrival_time <= self.current_time {
362                let _spike = self.spike_queue.pop_front().unwrap();
363                // Spike has arrived - already processed in synapse
364            } else {
365                break;
366            }
367        }
368
369        // Update all synapses
370        for conn in &mut self.connections {
371            let post_voltage = self.neuron_voltages[conn.post_neuron];
372            conn.synapse.update(self.current_time, post_voltage, dt)?;
373        }
374
375        // Update neuromodulators
376        self.neuromodulators.update(dt);
377
378        Ok(())
379    }
380
381    /// Get synaptic current to a neuron.
382    ///
383    /// # Arguments
384    /// * `neuron_id` - Target neuron ID
385    ///
386    /// # Returns
387    /// Total synaptic current (pA)
388    pub fn get_synaptic_current(&self, neuron_id: usize) -> Result<f64> {
389        if neuron_id >= self.n_neurons {
390            return Err(SynapseError::NeuronNotFound(neuron_id));
391        }
392
393        let voltage = self.neuron_voltages[neuron_id];
394        let mut total_current = 0.0;
395
396        // Chemical synapses
397        for conn in &self.connections {
398            if conn.post_neuron == neuron_id {
399                let modulation = self.neuromodulators.weight_modulation();
400                total_current += conn.synapse.current(voltage) * modulation;
401            }
402        }
403
404        // Gap junctions
405        for gap in &self.gap_junctions {
406            if gap.neuron1 == neuron_id {
407                let v_other = self.neuron_voltages[gap.neuron2];
408                total_current += gap.current(voltage, v_other);
409            } else if gap.neuron2 == neuron_id {
410                let v_other = self.neuron_voltages[gap.neuron1];
411                total_current -= gap.current(v_other, voltage);
412            }
413        }
414
415        Ok(total_current)
416    }
417
418    /// Get all synaptic connections to a neuron.
419    pub fn get_inputs(&self, neuron_id: usize) -> Vec<&Connection> {
420        self.connections
421            .iter()
422            .filter(|c| c.post_neuron == neuron_id)
423            .collect()
424    }
425
426    /// Get all synaptic connections from a neuron.
427    pub fn get_outputs(&self, neuron_id: usize) -> Vec<&Connection> {
428        self.connections
429            .iter()
430            .filter(|c| c.pre_neuron == neuron_id)
431            .collect()
432    }
433
434    /// Get connectivity statistics.
435    pub fn connectivity_stats(&self) -> NetworkStats {
436        let mut in_degrees = vec![0; self.n_neurons];
437        let mut out_degrees = vec![0; self.n_neurons];
438
439        for conn in &self.connections {
440            out_degrees[conn.pre_neuron] += 1;
441            in_degrees[conn.post_neuron] += 1;
442        }
443
444        NetworkStats {
445            n_neurons: self.n_neurons,
446            n_connections: self.connections.len(),
447            n_gap_junctions: self.gap_junctions.len(),
448            n_ephaptic: self.ephaptic_couplings.len(),
449            avg_in_degree: in_degrees.iter().sum::<usize>() as f64 / self.n_neurons as f64,
450            avg_out_degree: out_degrees.iter().sum::<usize>() as f64 / self.n_neurons as f64,
451        }
452    }
453
454    /// Reset all synapses in the network.
455    pub fn reset(&mut self) {
456        for conn in &mut self.connections {
457            conn.synapse.reset();
458        }
459        self.spike_queue.clear();
460        self.current_time = 0.0;
461        self.neuron_voltages.fill(-65.0);
462    }
463}
464
465/// Network connectivity statistics.
466#[derive(Debug, Clone)]
467pub struct NetworkStats {
468    /// Number of neurons.
469    pub n_neurons: usize,
470
471    /// Number of chemical synapses.
472    pub n_connections: usize,
473
474    /// Number of gap junctions.
475    pub n_gap_junctions: usize,
476
477    /// Number of ephaptic couplings.
478    pub n_ephaptic: usize,
479
480    /// Average in-degree.
481    pub avg_in_degree: f64,
482
483    /// Average out-degree.
484    pub avg_out_degree: f64,
485}
486
487/// Thread-safe wrapper for synaptic network.
488pub type SharedNetwork = Arc<Mutex<SynapticNetwork>>;
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_network_creation() {
496        let net = SynapticNetwork::new(10);
497        assert_eq!(net.n_neurons, 10);
498        assert_eq!(net.connections.len(), 0);
499    }
500
501    #[test]
502    fn test_add_connection() {
503        let mut net = SynapticNetwork::new(3);
504        let syn = Synapse::excitatory(1.0, 1.0).unwrap();
505
506        net.add_connection(0, 1, syn).unwrap();
507        assert_eq!(net.connections.len(), 1);
508    }
509
510    #[test]
511    fn test_add_gap_junction() {
512        let mut net = SynapticNetwork::new(3);
513        net.add_gap_junction(0, 1, 0.5).unwrap();
514
515        assert_eq!(net.gap_junctions.len(), 1);
516        assert_eq!(net.gap_junctions[0].conductance, 0.5);
517    }
518
519    #[test]
520    fn test_spike_propagation() {
521        let mut net = SynapticNetwork::new(2);
522        let syn = Synapse::excitatory(1.0, 1.0).unwrap();
523        net.add_connection(0, 1, syn).unwrap();
524
525        // Spike from neuron 0
526        net.spike(0).unwrap();
527
528        // Should have scheduled spike
529        assert_eq!(net.spike_queue.len(), 1);
530    }
531
532    #[test]
533    fn test_network_update() {
534        let mut net = SynapticNetwork::new(2);
535        let syn = Synapse::excitatory(1.0, 1.0).unwrap();
536        net.add_connection(0, 1, syn).unwrap();
537
538        let voltages = vec![-65.0, -65.0];
539        net.update(&voltages, 0.1).unwrap();
540
541        assert_eq!(net.current_time, 0.1);
542    }
543
544    #[test]
545    fn test_neuromodulator_state() {
546        let mut nm = NeuromodulatorState::new();
547
548        nm.release_dopamine(0.5);
549        assert_eq!(nm.dopamine, 0.5);
550
551        // Should decay
552        nm.update(100.0);
553        assert!(nm.dopamine < 0.5);
554    }
555
556    #[test]
557    fn test_gap_junction_current() {
558        let gap = GapJunction::new(0, 1, 1.0).unwrap();
559
560        let i = gap.current(-70.0, -60.0);
561        // Current from lower to higher voltage
562        assert!(i > 0.0);
563    }
564
565    #[test]
566    fn test_connectivity_stats() {
567        let mut net = SynapticNetwork::new(3);
568
569        let syn1 = Synapse::excitatory(1.0, 1.0).unwrap();
570        let syn2 = Synapse::excitatory(1.0, 1.0).unwrap();
571
572        net.add_connection(0, 1, syn1).unwrap();
573        net.add_connection(1, 2, syn2).unwrap();
574
575        let stats = net.connectivity_stats();
576        assert_eq!(stats.n_connections, 2);
577        assert_eq!(stats.n_neurons, 3);
578    }
579
580    #[test]
581    fn test_ephaptic_coupling() {
582        let eph = EphapticCoupling::new(0, 1, 50.0);
583
584        let effect = eph.effect(10.0); // 10 Hz
585        assert!(effect > 0.0);
586    }
587}