Skip to main content

sc_neurocore_engine/
rall_dendrite.rs

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