sc_neurocore_engine/neurons/rate/
sigmoid_rate.rs1#[derive(Clone, Debug)]
11pub struct SigmoidRateNeuron {
12 pub r: f64,
13 pub tau: f64,
14 pub beta: f64,
15 pub theta: f64,
16 pub dt: f64,
17}
18
19impl SigmoidRateNeuron {
20 pub fn new() -> Self {
22 Self::with_parameters(0.0, 10.0, 1.0, 0.0, 0.1)
23 .expect("the factory-default sigmoid-rate contract is valid")
24 }
25
26 pub fn with_parameters(
28 r: f64,
29 tau: f64,
30 beta: f64,
31 theta: f64,
32 dt: f64,
33 ) -> Result<Self, String> {
34 let neuron = Self {
35 r,
36 tau,
37 beta,
38 theta,
39 dt,
40 };
41 neuron.validate()?;
42 Ok(neuron)
43 }
44
45 pub fn validate(&self) -> Result<(), String> {
47 if !self.r.is_finite()
48 || !(0.0..=1.0).contains(&self.r)
49 || !self.tau.is_finite()
50 || self.tau <= 0.0
51 || !self.beta.is_finite()
52 || !self.theta.is_finite()
53 || !self.dt.is_finite()
54 || self.dt <= 0.0
55 {
56 return Err(
57 "sigmoid-rate state and parameters must be finite, with r in [0,1] and positive tau/dt"
58 .into(),
59 );
60 }
61 Ok(())
62 }
63
64 pub fn try_step(&mut self, current: f64) -> Result<f64, String> {
66 self.validate()?;
67 if !current.is_finite() {
68 return Err("sigmoid-rate current must be finite".into());
69 }
70 let target = stable_sigmoid(self.beta, current, self.theta)?;
71 let decay = (-self.dt / self.tau).exp();
72 let candidate = decay * self.r + (1.0 - decay) * target;
73 if !candidate.is_finite() || !(0.0..=1.0).contains(&candidate) {
74 return Err("sigmoid-rate exact relaxation left the finite unit interval".into());
75 }
76 self.r = candidate;
77 Ok(candidate)
78 }
79
80 pub fn step(&mut self, current: f64) -> f64 {
82 self.try_step(current).unwrap_or(self.r)
83 }
84
85 pub fn reset(&mut self) {
87 self.r = 0.0;
88 }
89}
90
91fn stable_sigmoid(beta: f64, current: f64, theta: f64) -> Result<f64, String> {
92 let argument = beta * (current - theta);
93 if argument.is_infinite() {
94 return Ok(if argument.is_sign_positive() {
95 1.0
96 } else {
97 0.0
98 });
99 }
100 if !argument.is_finite() {
101 return Err("sigmoid-rate transfer argument must be finite or saturating".into());
102 }
103 if argument >= 0.0 {
104 Ok(1.0 / (1.0 + (-argument).exp()))
105 } else {
106 let exponential = argument.exp();
107 Ok(exponential / (1.0 + exponential))
108 }
109}
110impl Default for SigmoidRateNeuron {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn sigmoid_rate() {
122 let mut n = SigmoidRateNeuron::new();
123 for _ in 0..100 {
124 n.step(5.0);
125 }
126 assert!(n.r > 0.5);
127 }
128
129 #[test]
130 fn sigmoid_rate_matches_python_exact_relaxation_golden() {
131 let mut neuron = SigmoidRateNeuron::with_parameters(0.25, 10.0, 2.0, 1.0, 0.5).unwrap();
132 let expected = [
133 0.2857007338135623,
134 0.3196603222932904,
135 0.3519636820991432,
136 0.38269158845670403,
137 0.41192087713731845,
138 0.43972463658754457,
139 ];
140 for target in expected {
141 let rate = neuron.try_step(3.0).unwrap();
142 assert!((rate - target).abs() <= 2.0e-15, "{rate} != {target}");
143 }
144 }
145
146 #[test]
147 fn sigmoid_rate_exact_relaxation_is_bounded_for_large_timestep() {
148 let mut neuron = SigmoidRateNeuron::with_parameters(1.0, 0.1, 1.0, 0.0, 5.0).unwrap();
149 let rate = neuron.try_step(-100.0).unwrap();
150 assert!((rate - 1.9287498479639178e-22).abs() <= 1.0e-36);
151 assert!((0.0..=1.0).contains(&rate));
152 }
153
154 #[test]
155 fn sigmoid_rate_rejects_invalid_contract_without_mutation() {
156 let invalid_contracts = [
157 (-0.1, 10.0, 1.0, 0.0, 0.1),
158 (1.1, 10.0, 1.0, 0.0, 0.1),
159 (0.0, 0.0, 1.0, 0.0, 0.1),
160 (0.0, 10.0, f64::NAN, 0.0, 0.1),
161 (0.0, 10.0, 1.0, f64::INFINITY, 0.1),
162 (0.0, 10.0, 1.0, 0.0, -0.1),
163 ];
164 for (r, tau, beta, theta, dt) in invalid_contracts {
165 assert!(SigmoidRateNeuron::with_parameters(r, tau, beta, theta, dt).is_err());
166 }
167
168 let mut neuron = SigmoidRateNeuron::with_parameters(0.25, 10.0, 2.0, 1.0, 0.5).unwrap();
169 let before = neuron.r;
170 assert!(neuron.try_step(f64::NAN).is_err());
171 assert_eq!(neuron.r, before);
172 neuron.tau = 0.0;
173 assert!(neuron.try_step(3.0).is_err());
174 assert_eq!(neuron.r, before);
175 }
176
177 #[test]
178 fn sigmoid_rate_saturates_extreme_finite_drive() {
179 let mut high = SigmoidRateNeuron::with_parameters(0.0, 10.0, 1.0e308, 0.0, 0.1).unwrap();
180 let mut low = high.clone();
181 assert!(high.try_step(1.0e308).unwrap() > 0.0);
182 assert_eq!(low.try_step(-1.0e308).unwrap(), 0.0);
183 }
184
185 #[test]
186 fn sigmoid_rate_reset_preserves_configuration() {
187 let mut neuron = SigmoidRateNeuron::with_parameters(0.25, 7.0, 2.5, -0.4, 0.2).unwrap();
188 neuron.try_step(3.0).unwrap();
189 neuron.reset();
190 assert_eq!(neuron.r, 0.0);
191 assert_eq!(
192 (neuron.tau, neuron.beta, neuron.theta, neuron.dt),
193 (7.0, 2.5, -0.4, 0.2)
194 );
195 }
196
197 #[test]
198 fn sigmoid_rate_legacy_step_fails_closed() {
199 let mut neuron = SigmoidRateNeuron::with_parameters(0.25, 10.0, 2.0, 1.0, 0.5).unwrap();
200 assert_eq!(neuron.step(f64::NAN), 0.25);
201 assert_eq!(neuron.r, 0.25);
202 }
203}