feagi_structures/genomic/
modulator.rs1use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum ModulatorKind {
16 FiringThreshold,
18 Leak,
20 FiringProbability,
22 TransmissionGain,
24 Reward,
26 LearningRate,
28 Gate,
31 MemoryFormation,
34}
35
36impl ModulatorKind {
37 pub fn as_str(self) -> &'static str {
39 match self {
40 Self::FiringThreshold => "neuro.firing_threshold",
41 Self::Leak => "neuro.leak",
42 Self::FiringProbability => "neuro.firing_probability",
43 Self::TransmissionGain => "synaptic.transmission_gain",
44 Self::Reward => "synaptic.reward",
45 Self::LearningRate => "synaptic.learning_rate",
46 Self::Gate => "synaptic.gate",
47 Self::MemoryFormation => "memory.formation",
48 }
49 }
50
51 pub fn parse(value: &str) -> Result<Self, String> {
53 match value {
54 "neuro.firing_threshold" => Ok(Self::FiringThreshold),
55 "neuro.leak" => Ok(Self::Leak),
56 "neuro.firing_probability" => Ok(Self::FiringProbability),
57 "synaptic.transmission_gain" => Ok(Self::TransmissionGain),
58 "synaptic.reward" => Ok(Self::Reward),
59 "synaptic.learning_rate" => Ok(Self::LearningRate),
60 "synaptic.gate" => Ok(Self::Gate),
61 "memory.formation" => Ok(Self::MemoryFormation),
62 other => Err(format!("unknown modulator type '{other}'")),
63 }
64 }
65
66 pub fn all() -> &'static [Self] {
68 &[
69 Self::FiringThreshold,
70 Self::Leak,
71 Self::FiringProbability,
72 Self::TransmissionGain,
73 Self::Reward,
74 Self::LearningRate,
75 Self::Gate,
76 Self::MemoryFormation,
77 ]
78 }
79
80 pub fn is_neuromodulator(self) -> bool {
82 matches!(
83 self,
84 Self::FiringThreshold | Self::Leak | Self::FiringProbability
85 )
86 }
87
88 pub fn is_area_level(self) -> bool {
90 self.is_neuromodulator() || self == Self::MemoryFormation
91 }
92
93 pub fn is_synaptic(self) -> bool {
95 !self.is_area_level()
96 }
97
98 pub fn combines_by_addition(self) -> bool {
100 matches!(self, Self::Reward)
101 }
102}
103
104impl fmt::Display for ModulatorKind {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.write_str(self.as_str())
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct ModulatorValidationError(pub String);
113
114impl fmt::Display for ModulatorValidationError {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 f.write_str(&self.0)
117 }
118}
119
120pub fn validate_instance_fields(
126 magnitude_percent: f32,
127 effect_duration_bursts: u16,
128 graded: bool,
129 full_scale_potential: Option<f32>,
130) -> Result<(), ModulatorValidationError> {
131 if !magnitude_percent.is_finite() || magnitude_percent < -100.0 {
132 return Err(ModulatorValidationError(
133 "magnitude_percent must be finite and >= -100".to_string(),
134 ));
135 }
136 if effect_duration_bursts < 1 {
137 return Err(ModulatorValidationError(
138 "effect_duration_bursts must be >= 1".to_string(),
139 ));
140 }
141 if graded {
142 match full_scale_potential {
143 Some(scale) if scale.is_finite() && scale > 0.0 => {}
144 _ => {
145 return Err(ModulatorValidationError(
146 "graded modulators require full_scale_potential > 0".to_string(),
147 ));
148 }
149 }
150 }
151 Ok(())
152}
153
154pub fn validate_spike_train(
156 enabled: bool,
157 consecutive_fire_limit: u16,
158) -> Result<(), ModulatorValidationError> {
159 if enabled && consecutive_fire_limit < 1 {
160 return Err(ModulatorValidationError(
161 "spike_train requires consecutive_fire_limit >= 1".to_string(),
162 ));
163 }
164 Ok(())
165}
166
167pub fn modulator_signal(
173 magnitude_percent: f32,
174 fired: bool,
175 graded: bool,
176 membrane_potential: f32,
177 full_scale_potential: f32,
178) -> f32 {
179 if !fired {
180 return 0.0;
181 }
182 let strength = if graded {
183 (membrane_potential / full_scale_potential).min(1.0)
184 } else {
185 1.0
186 };
187 (magnitude_percent / 100.0) * strength
188}
189
190pub fn multiplicative_factor(signals: &[f32]) -> f32 {
192 signals
193 .iter()
194 .fold(1.0_f32, |acc, signal| acc * (1.0 + signal))
195}
196
197pub fn gate_factor(signals: &[f32]) -> f32 {
200 signals.iter().copied().fold(0.0_f32, f32::max)
201}
202
203pub fn memory_formation_factor(signals: &[f32]) -> f32 {
206 multiplicative_factor(signals).clamp(0.0, 1.0)
207}
208
209pub fn summed_reward(signals: &[f32]) -> f32 {
211 signals.iter().sum()
212}
213
214pub fn scale_baseline(kind: ModulatorKind, baseline: f32, factor: f32) -> f32 {
219 let value = baseline * factor;
220 match kind {
221 ModulatorKind::Leak | ModulatorKind::FiringProbability => value.clamp(0.0, 1.0),
222 ModulatorKind::FiringThreshold
223 | ModulatorKind::TransmissionGain
224 | ModulatorKind::LearningRate
225 | ModulatorKind::Gate
226 | ModulatorKind::MemoryFormation => value.max(0.0),
227 ModulatorKind::Reward => value,
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn silent_driver_contributes_nothing() {
237 assert_eq!(modulator_signal(30.0, false, false, 10.0, 10.0), 0.0);
238 }
239
240 #[test]
241 fn ungraded_firing_uses_full_magnitude() {
242 assert!((modulator_signal(30.0, true, false, 1.0, 10.0) - 0.3).abs() < 1e-6);
243 assert!((modulator_signal(-80.0, true, false, 1.0, 10.0) + 0.8).abs() < 1e-6);
244 }
245
246 #[test]
247 fn graded_firing_scales_with_membrane_potential_and_caps_at_one() {
248 let half = modulator_signal(30.0, true, true, 5.0, 10.0);
249 assert!((half - 0.15).abs() < 1e-6);
250 let capped = modulator_signal(30.0, true, true, 40.0, 10.0);
251 assert!((capped - 0.3).abs() < 1e-6);
252 }
253
254 #[test]
255 fn factors_multiply_and_reward_adds() {
256 let factor = multiplicative_factor(&[0.3, -0.5]);
257 assert!((factor - (1.3 * 0.5)).abs() < 1e-6);
258 assert!((summed_reward(&[0.3, -0.5]) + 0.2).abs() < 1e-6);
259 }
260
261 #[test]
262 fn leak_and_excitability_clamp_to_unit_interval() {
263 assert!((scale_baseline(ModulatorKind::Leak, 0.8, 2.0) - 1.0).abs() < 1e-6);
264 assert!((scale_baseline(ModulatorKind::FiringProbability, 0.2, 0.0) - 0.0).abs() < 1e-6);
265 assert!(scale_baseline(ModulatorKind::FiringThreshold, 2.0, 3.0) > 0.0);
266 }
267
268 #[test]
269 fn magnitude_below_minus_100_is_rejected() {
270 assert!(validate_instance_fields(-100.1, 1, false, None).is_err());
271 assert!(validate_instance_fields(-100.0, 1, false, None).is_ok());
272 }
273
274 #[test]
275 fn graded_requires_positive_full_scale() {
276 assert!(validate_instance_fields(10.0, 1, true, None).is_err());
277 assert!(validate_instance_fields(10.0, 1, true, Some(0.0)).is_err());
278 assert!(validate_instance_fields(10.0, 1, true, Some(4.0)).is_ok());
279 }
280
281 #[test]
282 fn spike_train_rejects_unlimited_consecutive_fire() {
283 assert!(validate_spike_train(true, 0).is_err());
284 assert!(validate_spike_train(true, 1).is_ok());
285 assert!(validate_spike_train(false, 0).is_ok());
286 }
287
288 #[test]
289 fn type_strings_round_trip() {
290 for kind in ModulatorKind::all() {
291 assert_eq!(ModulatorKind::parse(kind.as_str()).unwrap(), *kind);
292 }
293 }
294
295 #[test]
296 fn gate_kind_is_synaptic_and_parses() {
297 assert_eq!(
298 ModulatorKind::parse("synaptic.gate").unwrap(),
299 ModulatorKind::Gate
300 );
301 assert_eq!(ModulatorKind::Gate.as_str(), "synaptic.gate");
302 assert!(ModulatorKind::Gate.is_synaptic());
303 assert!(!ModulatorKind::Gate.combines_by_addition());
304 }
305
306 #[test]
307 fn gate_factor_is_closed_when_silent_and_open_if_any_open() {
308 assert_eq!(gate_factor(&[0.0]), 0.0);
309 assert_eq!(gate_factor(&[0.0, 1.0]), 1.0);
310 assert!((gate_factor(&[0.5, 0.25]) - 0.5).abs() < 1e-6);
311 }
312
313 #[test]
314 fn memory_formation_is_area_level_and_clamped() {
315 assert_eq!(
316 ModulatorKind::parse("memory.formation").unwrap(),
317 ModulatorKind::MemoryFormation
318 );
319 assert!(ModulatorKind::MemoryFormation.is_area_level());
320 assert!(!ModulatorKind::MemoryFormation.is_synaptic());
321 assert!(!ModulatorKind::MemoryFormation.is_neuromodulator());
322 assert_eq!(memory_formation_factor(&[-1.0]), 0.0);
323 assert_eq!(memory_formation_factor(&[0.0]), 1.0);
324 assert_eq!(memory_formation_factor(&[0.5]), 1.0);
325 assert!((memory_formation_factor(&[-0.25]) - 0.75).abs() < 1e-6);
326 assert_eq!(memory_formation_factor(&[]), 1.0);
327 }
328}