sc_neurocore_engine/neurons/misc/
smooth_muscle.rs1#[derive(Clone, Debug)]
32pub struct SmoothMuscleCell {
33 pub v: f64,
34 pub d: f64, pub f: f64, pub ca: f64, pub ca_store: f64, pub c_m: f64,
39 pub g_cal: f64, pub g_bk: f64, pub g_l: f64, pub e_ca: f64,
43 pub e_k: f64,
44 pub e_l: f64,
45 pub tau_ca: f64, pub v_serca: f64, pub k_serca: f64, pub ip3: f64, pub v_ip3r: f64, pub k_ip3: f64, pub k_ca_ip3: f64, pub kd_bk: f64, pub dt: f64,
54 pub sub_steps: usize,
55 pub gain: f64,
56}
57
58impl Default for SmoothMuscleCell {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl SmoothMuscleCell {
65 pub fn new() -> Self {
66 Self {
67 v: -60.0,
68 d: 0.01,
69 f: 0.95,
70 ca: 0.1,
71 ca_store: 100.0, c_m: 1.0,
73 g_cal: 2.0, g_bk: 1.0, g_l: 0.1,
76 e_ca: 60.0,
77 e_k: -80.0,
78 e_l: -50.0,
79 tau_ca: 50.0,
80 v_serca: 0.5, k_serca: 0.3, ip3: 0.5, v_ip3r: 2.0, k_ip3: 0.3, k_ca_ip3: 0.3, kd_bk: 0.5, dt: 1.0, sub_steps: 4,
89 gain: 1.0,
90 }
91 }
92
93 #[inline]
94 fn boltz(v: f64, vh: f64, k: f64) -> f64 {
95 1.0 / (1.0 + (-(v - vh) / k).exp())
96 }
97
98 pub fn step(&mut self, current: f64) -> i32 {
99 let input = self.gain * current;
100 let dt_sub = self.dt / self.sub_steps as f64;
101 let v_prev = self.v;
102
103 for _ in 0..self.sub_steps {
104 let v = self.v;
105
106 let d_inf = Self::boltz(v, -20.0, 6.0);
108 let tau_d = 5.0 + 20.0 / (1.0 + ((v + 20.0) / 10.0).powi(2)).max(0.01);
109 self.d += dt_sub * (d_inf - self.d) / tau_d;
110
111 let f_inf = Self::boltz(v, -35.0, -8.0);
113 let tau_f = 50.0 + 200.0 / (1.0 + ((v + 35.0) / 10.0).powi(2)).max(0.01);
114 self.f += dt_sub * (f_inf - self.f) / tau_f;
115
116 self.d = self.d.clamp(0.0, 1.0);
117 self.f = self.f.clamp(0.0, 1.0);
118
119 let bk_ca = self.ca * self.ca / (self.ca * self.ca + self.kd_bk * self.kd_bk);
121 let bk_v = Self::boltz(v, -10.0, 15.0);
122 let bk_inf = bk_ca * bk_v;
123
124 let i_cal = self.g_cal * self.d * self.f * (v - self.e_ca);
126 let i_bk = self.g_bk * bk_inf * (v - self.e_k);
127 let i_l = self.g_l * (v - self.e_l);
128
129 let dv = (-(i_cal + i_bk + i_l) + input) / self.c_m;
130 self.v += dt_sub * dv;
131
132 let ca_entry = if i_cal < 0.0 { -i_cal * 0.01 } else { 0.0 };
135
136 let ip3_act = self.ip3 / (self.ip3 + self.k_ip3);
138 let ca_act = self.ca / (self.ca + self.k_ca_ip3);
139 let ip3_release = self.v_ip3r * ip3_act * ca_act * self.ca_store;
140
141 let serca = self.v_serca * self.ca * self.ca
143 / (self.ca * self.ca + self.k_serca * self.k_serca);
144
145 self.ca += dt_sub * (ca_entry + ip3_release - serca - self.ca / self.tau_ca);
147 self.ca_store += dt_sub * (serca - ip3_release);
148
149 self.ca = self.ca.max(0.0);
150 self.ca_store = self.ca_store.max(0.0);
151 }
152
153 self.v = self.v.clamp(-100.0, 40.0);
155 if !self.v.is_finite() {
156 self.v = -60.0;
157 }
158 if !self.ca.is_finite() {
159 self.ca = 0.1;
160 }
161 if !self.ca_store.is_finite() {
162 self.ca_store = 100.0;
163 }
164
165 if self.v >= -30.0 && v_prev < -30.0 {
167 1
168 } else {
169 0
170 }
171 }
172
173 pub fn reset(&mut self) {
174 *self = Self::new();
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
185 fn smooth_fires_with_input() {
186 let mut n = SmoothMuscleCell::new();
187 let mut spikes = 0;
188 for _ in 0..10_000 {
189 spikes += n.step(5.0);
190 }
191 assert!(
192 spikes > 0,
193 "Smooth muscle must produce slow waves, got {spikes}"
194 );
195 }
196
197 #[test]
198 fn smooth_silent_without_input() {
199 let mut n = SmoothMuscleCell::new();
200 n.ip3 = 0.0; let mut spikes = 0;
202 for _ in 0..5_000 {
203 spikes += n.step(0.0);
204 }
205 assert!(
206 spikes <= 1,
207 "Should be essentially silent without drive, got {spikes}"
208 );
209 }
210
211 #[test]
212 fn smooth_has_ip3_pathway() {
213 let n = SmoothMuscleCell::new();
214 assert!(n.v_ip3r > 0.0, "Must have IP3R release pathway");
215 assert!(n.ip3 > 0.0, "Must have tonic IP3");
216 }
217
218 #[test]
219 fn smooth_has_serca() {
220 let n = SmoothMuscleCell::new();
221 assert!(n.v_serca > 0.0, "Must have SERCA pump");
222 }
223
224 #[test]
225 fn smooth_ca_store_exists() {
226 let n = SmoothMuscleCell::new();
227 assert!(n.ca_store > 0.0, "Must have ER/SR Ca²⁺ store");
228 }
229
230 #[test]
231 fn smooth_has_bk() {
232 let n = SmoothMuscleCell::new();
233 assert!(n.g_bk > 0.0, "Must have BK (Ca²⁺-activated K) channel");
234 }
235
236 #[test]
237 fn smooth_no_fast_na() {
238 let n = SmoothMuscleCell::new();
240 assert!(n.g_cal > 0.0, "Depolarisation must be Ca²⁺-dependent");
242 }
243
244 #[test]
245 fn smooth_nan_stays_finite() {
246 let mut n = SmoothMuscleCell::new();
247 n.step(f64::NAN);
248 assert!(n.v.is_finite());
249 assert!(n.ca.is_finite());
250 }
251
252 #[test]
253 fn smooth_reset_clears() {
254 let mut n = SmoothMuscleCell::new();
255 for _ in 0..1000 {
256 n.step(3.0);
257 }
258 n.reset();
259 assert_eq!(n.v, -60.0);
260 assert_eq!(n.ca, 0.1);
261 assert_eq!(n.ca_store, 100.0);
262 }
263
264 #[test]
265 fn smooth_performance_1k_steps() {
266 let start = std::time::Instant::now();
267 let mut n = SmoothMuscleCell::new();
268 for _ in 0..1_000 {
269 std::hint::black_box(n.step(2.0));
270 }
271 let elapsed = start.elapsed();
272 assert!(elapsed.as_millis() < 50, "1k steps must complete in <50ms");
273 }
274
275 #[test]
276 fn smooth_default_matches_constructor() {
277 let default = SmoothMuscleCell::default();
278 let constructed = SmoothMuscleCell::new();
279 assert_eq!(default.v, constructed.v);
280 assert_eq!(default.ca_store, constructed.ca_store);
281 assert_eq!(default.sub_steps, constructed.sub_steps);
282 }
283
284 #[test]
285 fn smooth_nonfinite_calcium_state_recovers_to_baseline() {
286 let mut n = SmoothMuscleCell::new();
287 n.ca = f64::INFINITY;
288 n.ca_store = f64::INFINITY;
289 n.sub_steps = 0;
290 n.step(0.0);
291 assert_eq!(n.ca, 0.1);
292 assert_eq!(n.ca_store, 100.0);
293 assert!(n.v.is_finite());
294 }
295}