Skip to main content

sc_neurocore_engine/
rall_dendrite.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later | Commercial license available
2// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
3// © Code 2020–2026 Miroslav Šotek. All rights reserved.
4// ORCID: 0009-0009-3560-0851
5// Contact: www.anulum.li | protoscience@anulum.li
6// SC-NeuroCore — Rall branching dendrite model in Rust
7
8//! Compartmental dendritic tree with Rall's 3/2 power rule.
9//! Distal → proximal propagation with inter-compartment coupling.
10
11/// Rall branching dendrite with configurable topology.
12pub struct RallDendriteRust {
13    pub n_branches: usize,
14    pub branch_length: usize,
15    #[allow(dead_code)]
16    tau: f64,
17    coupling: f64,
18    #[allow(dead_code)]
19    dt: f64,
20    decay: f64,
21    dt_over_tau: f64,
22    attenuation: Vec<f64>,
23    /// Compartment voltages: [n_branches][branch_length]
24    v: Vec<Vec<f64>>,
25    pub soma_v: f64,
26}
27
28impl RallDendriteRust {
29    pub fn new(n_branches: usize, branch_length: usize, tau: f64, coupling: f64, dt: f64) -> Self {
30        let decay = (-dt / tau).exp();
31        let dt_over_tau = dt / tau;
32        // Rall 3/2: d_parent^1.5 = n * d_daughter^1.5, d_daughter = 1
33        let parent_d = (n_branches as f64).powf(2.0 / 3.0);
34        let attenuation: Vec<f64> = (0..n_branches)
35            .map(|_| (1.0 / parent_d).powf(1.5))
36            .collect();
37
38        Self {
39            n_branches,
40            branch_length,
41            tau,
42            coupling,
43            dt,
44            decay,
45            dt_over_tau,
46            attenuation,
47            v: vec![vec![0.0; branch_length]; n_branches],
48            soma_v: 0.0,
49        }
50    }
51
52    /// Advance one timestep. branch_inputs: [n_branches] injected at distal tip.
53    pub fn step(&mut self, branch_inputs: &[f64]) -> f64 {
54        let nb = self.n_branches;
55        let bl = self.branch_length;
56
57        // Decay all compartments
58        for b in 0..nb {
59            for k in 0..bl {
60                self.v[b][k] *= self.decay;
61            }
62        }
63
64        // Inject at distal tip (last compartment)
65        for b in 0..nb.min(branch_inputs.len()) {
66            self.v[b][bl - 1] += branch_inputs[b] * self.dt_over_tau;
67        }
68
69        // Propagate distal → proximal
70        for k in (1..bl).rev() {
71            for b in 0..nb {
72                let flow = self.coupling * (self.v[b][k] - self.v[b][k - 1]);
73                self.v[b][k] -= flow;
74                self.v[b][k - 1] += flow;
75            }
76        }
77
78        // Sum proximal with Rall attenuation
79        let mut soma_input = 0.0;
80        for b in 0..nb {
81            soma_input += self.v[b][0] * self.attenuation[b];
82        }
83        self.soma_v = self.decay * self.soma_v + soma_input * self.dt_over_tau;
84        self.soma_v
85    }
86
87    pub fn reset(&mut self) {
88        for b in &mut self.v {
89            b.fill(0.0);
90        }
91        self.soma_v = 0.0;
92    }
93
94    pub fn branch_voltages(&self) -> Vec<Vec<f64>> {
95        self.v.clone()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_initial_zero() {
105        let d = RallDendriteRust::new(4, 3, 10.0, 0.5, 1.0);
106        assert_eq!(d.soma_v, 0.0);
107        assert!(d.v.iter().all(|b| b.iter().all(|&v| v == 0.0)));
108    }
109
110    #[test]
111    fn test_input_reaches_soma() {
112        let mut d = RallDendriteRust::new(2, 3, 10.0, 0.5, 1.0);
113        for _ in 0..20 {
114            d.step(&[1.0, 0.0]);
115        }
116        assert!(d.soma_v > 0.0);
117    }
118
119    #[test]
120    fn test_more_branches_more_input() {
121        let mut d1 = RallDendriteRust::new(4, 2, 10.0, 0.5, 1.0);
122        let mut d2 = RallDendriteRust::new(4, 2, 10.0, 0.5, 1.0);
123        for _ in 0..20 {
124            d1.step(&[1.0, 0.0, 0.0, 0.0]);
125            d2.step(&[1.0, 1.0, 1.0, 1.0]);
126        }
127        assert!(d2.soma_v > d1.soma_v);
128    }
129
130    #[test]
131    fn test_reset() {
132        let mut d = RallDendriteRust::new(2, 2, 10.0, 0.5, 1.0);
133        d.step(&[5.0, 5.0]);
134        d.reset();
135        assert_eq!(d.soma_v, 0.0);
136        assert!(d.v.iter().all(|b| b.iter().all(|&v| v == 0.0)));
137    }
138
139    #[test]
140    fn test_distal_higher_than_proximal() {
141        let mut d = RallDendriteRust::new(1, 5, 20.0, 0.3, 1.0);
142        for _ in 0..10 {
143            d.step(&[2.0]);
144        }
145        let bv = &d.v[0];
146        assert!(bv[4] > bv[0], "Distal should be higher than proximal");
147    }
148}