Skip to main content

sc_neurocore_engine/neurons/biophysical/
huber_braun.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 — Huber-Braun Neuron Model
8
9//! Huber-Braun temperature-sensitive cold-receptor dynamics.
10
11/// Huber-Braun — temperature-sensitive cold receptor. Braun et al. 1998.
12#[derive(Clone, Debug)]
13pub struct HuberBraunNeuron {
14    pub v: f64,
15    pub a_sd: f64,
16    pub a_sr: f64,
17    pub g_sd: f64,
18    pub g_sr: f64,
19    pub g_l: f64,
20    pub e_sd: f64,
21    pub e_sr: f64,
22    pub e_l: f64,
23    pub tau_sd: f64,
24    pub tau_sr: f64,
25    pub eta: f64,
26    pub dt: f64,
27    pub v_threshold: f64,
28}
29
30impl HuberBraunNeuron {
31    pub fn new() -> Self {
32        Self {
33            v: -50.0,
34            a_sd: 0.0,
35            a_sr: 0.0,
36            g_sd: 1.5,
37            g_sr: 0.4,
38            g_l: 0.1,
39            e_sd: 50.0,
40            e_sr: -90.0,
41            e_l: -60.0,
42            tau_sd: 10.0,
43            tau_sr: 20.0,
44            eta: 0.012,
45            dt: 0.1,
46            v_threshold: -20.0,
47        }
48    }
49    pub fn step(&mut self, current: f64) -> i32 {
50        let v_prev = self.v;
51        // Braun, Huber et al. 1998: SD V1/2 = -40 mV, slope = 6
52        let sd_inf = 1.0 / (1.0 + (-(self.v + 40.0) / 6.0).exp());
53        let sr_inf = 1.0 / (1.0 + ((self.v + 40.0) / 6.0).exp());
54        self.a_sd += (sd_inf - self.a_sd) / self.tau_sd * self.dt;
55        self.a_sr += (sr_inf - self.a_sr) / self.tau_sr * self.dt;
56        let i_sd = self.g_sd * self.a_sd * (self.v - self.e_sd);
57        let i_sr = self.g_sr * self.a_sr * (self.v - self.e_sr);
58        let i_l = self.g_l * (self.v - self.e_l);
59        self.v += (-i_sd - i_sr - i_l + current) * self.dt;
60        if self.v >= self.v_threshold && v_prev < self.v_threshold {
61            1
62        } else {
63            0
64        }
65    }
66    pub fn reset(&mut self) {
67        self.v = -50.0;
68        self.a_sd = 0.0;
69        self.a_sr = 0.0;
70    }
71}
72impl Default for HuberBraunNeuron {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn default_matches_constructor_state() {
84        let default = HuberBraunNeuron::default();
85        let constructed = HuberBraunNeuron::new();
86        assert_eq!(default.v, constructed.v);
87    }
88
89    #[test]
90    fn hb_fires() {
91        let mut n = HuberBraunNeuron::new();
92        let t: i32 = (0..5000).map(|_| n.step(10.0)).sum();
93        assert!(t > 0);
94    }
95
96    // -- HuberBraun --
97    #[test]
98    fn hb_silent_without_input() {
99        let mut n = HuberBraunNeuron::new();
100        let _t: i32 = (0..500).map(|_| n.step(0.0)).sum();
101        // HuberBraun may have spontaneous activity, so just check finite
102        assert!(n.v.is_finite());
103    }
104    #[test]
105    fn hb_reset_clears_state() {
106        let mut n = HuberBraunNeuron::new();
107        for _ in 0..200 {
108            n.step(10.0);
109        }
110        n.reset();
111        assert!((n.v - (-50.0)).abs() < 1e-10);
112    }
113    #[test]
114    fn hb_extreme_bounded() {
115        let mut n = HuberBraunNeuron::new();
116        for _ in 0..200 {
117            n.step(1e4);
118        }
119        assert!(n.v.is_finite());
120    }
121    #[test]
122    fn hb_negative_no_crash() {
123        let mut n = HuberBraunNeuron::new();
124        for _ in 0..500 {
125            n.step(-10.0);
126        }
127        assert!(n.v.is_finite());
128    }
129    #[test]
130    fn hb_nan_no_panic() {
131        let mut n = HuberBraunNeuron::new();
132        n.step(f64::NAN);
133    }
134}