sc_neurocore_engine/neurons/
cazelles_map.rs1#[derive(Clone, Debug)]
13pub struct CazellesMapNeuron {
14 pub x: f64,
15 pub alpha: f64,
16 pub exponent: u8,
17 pub x0: f64,
18 pub x1: f64,
19 pub x2: f64,
20 pub x3: f64,
21 pub x4: f64,
22 pub a1: f64,
23 pub a2: f64,
24 pub a3: f64,
25 pub a4: f64,
26 pub b1: f64,
27 pub b2: f64,
28 pub b3: f64,
29 pub b4: f64,
30}
31
32impl CazellesMapNeuron {
33 pub fn new() -> Self {
34 Self {
35 x: 0.1,
36 alpha: 0.0,
37 exponent: 2,
38 x0: 0.0,
39 x1: 0.4,
40 x2: 0.6,
41 x3: 0.7,
42 x4: 1.0,
43 a1: 0.0,
44 a2: 1.5,
45 a3: -0.9,
46 a4: 1.4,
47 b1: 1.05,
48 b2: -1.25,
49 b3: 1.5,
50 b4: -1.0,
51 }
52 }
53
54 fn parameters_are_valid(&self) -> bool {
55 [
56 self.x, self.alpha, self.x0, self.x1, self.x2, self.x3, self.x4, self.a1, self.a2,
57 self.a3, self.a4, self.b1, self.b2, self.b3, self.b4,
58 ]
59 .iter()
60 .all(|value| value.is_finite())
61 && (0.0..1.0).contains(&self.alpha)
62 && matches!(self.exponent, 1 | 2)
63 && self.x0 < self.x1
64 && self.x1 < self.x2
65 && self.x2 < self.x3
66 && self.x3 < self.x4
67 && (self.x0..=self.x4).contains(&self.x)
68 }
69
70 fn candidate(&self, current: f64) -> Result<f64, &'static str> {
71 let base = if self.x < self.x1 {
72 self.a1 + self.b1 * self.x
73 } else if self.x < self.x2 {
74 self.a2 + self.b2 * self.x
75 } else if self.x < self.x3 {
76 self.a3 + self.b3 * self.x
77 } else {
78 self.a4 + self.b4 * self.x
79 };
80 let power = if self.exponent == 1 {
81 self.x
82 } else {
83 self.x * self.x
84 };
85 let mut candidate = base + self.alpha * power + current;
86 if !candidate.is_finite() {
87 return Err("Cazelles candidate became non-finite");
88 }
89 let tolerance = 8.0 * f64::EPSILON * self.x0.abs().max(self.x4.abs()).max(1.0);
90 if candidate < self.x0 && candidate >= self.x0 - tolerance {
91 candidate = self.x0;
92 } else if candidate > self.x4 && candidate <= self.x4 + tolerance {
93 candidate = self.x4;
94 }
95 if !(self.x0..=self.x4).contains(&candidate) {
96 return Err("Cazelles candidate left its configured domain");
97 }
98 Ok(candidate)
99 }
100
101 pub fn try_step(&mut self, current: f64) -> Result<i32, &'static str> {
103 if !self.parameters_are_valid() {
104 return Err("invalid Cazelles runtime state");
105 }
106 if !current.is_finite() {
107 return Err("invalid Cazelles current");
108 }
109 let candidate = self.candidate(current)?;
110 let event = i32::from(self.x >= self.x1 && candidate < self.x1);
111 self.x = candidate;
112 Ok(event)
113 }
114
115 pub fn step(&mut self, current: f64) -> i32 {
117 self.try_step(current).unwrap_or(0)
118 }
119
120 pub fn simulate(
121 &mut self,
122 n_steps: usize,
123 current: f64,
124 ) -> Result<(Vec<f64>, i64), &'static str> {
125 let mut trace = Vec::with_capacity(n_steps);
126 let mut events = 0_i64;
127 for _ in 0..n_steps {
128 events += i64::from(self.try_step(current)?);
129 trace.push(self.x);
130 }
131 Ok((trace, events))
132 }
133
134 pub fn reset(&mut self) {
135 if self.parameters_are_valid() || self.x0 < self.x4 {
136 self.x = if (self.x0..=self.x4).contains(&0.1) {
137 0.1
138 } else {
139 self.x0
140 };
141 }
142 }
143}
144
145impl Default for CazellesMapNeuron {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn matches_figure_one_orbit() {
157 let mut neuron = CazellesMapNeuron::new();
158 let (trace, events) = neuron.simulate(200, 0.0).unwrap();
159 assert_eq!(events, 2);
160 assert_eq!(trace[0], 0.105_000_000_000_000_01);
161 assert!(trace.iter().all(|x| (0.0..=1.0).contains(x)));
162 }
163
164 #[test]
165 fn breakpoint_convention_is_right_continuous() {
166 let mut neuron = CazellesMapNeuron {
167 x: 0.4,
168 ..CazellesMapNeuron::new()
169 };
170 neuron.try_step(0.0).unwrap();
171 assert_eq!(neuron.x, 1.0);
172 }
173
174 #[test]
175 fn invalid_candidate_is_atomic() {
176 let mut neuron = CazellesMapNeuron::new();
177 let before = neuron.x;
178 assert!(neuron.try_step(2.0).is_err());
179 assert_eq!(neuron.x, before);
180 }
181}