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 MemoryFreeze,
39}
40
41impl ModulatorKind {
42 pub fn as_str(self) -> &'static str {
44 match self {
45 Self::FiringThreshold => "neuro.firing_threshold",
46 Self::Leak => "neuro.leak",
47 Self::FiringProbability => "neuro.firing_probability",
48 Self::TransmissionGain => "synaptic.transmission_gain",
49 Self::Reward => "synaptic.reward",
50 Self::LearningRate => "synaptic.learning_rate",
51 Self::Gate => "synaptic.gate",
52 Self::MemoryFormation => "memory.formation",
53 Self::MemoryFreeze => "memory.freeze",
54 }
55 }
56
57 pub fn parse(value: &str) -> Result<Self, String> {
59 match value {
60 "neuro.firing_threshold" => Ok(Self::FiringThreshold),
61 "neuro.leak" => Ok(Self::Leak),
62 "neuro.firing_probability" => Ok(Self::FiringProbability),
63 "synaptic.transmission_gain" => Ok(Self::TransmissionGain),
64 "synaptic.reward" => Ok(Self::Reward),
65 "synaptic.learning_rate" => Ok(Self::LearningRate),
66 "synaptic.gate" => Ok(Self::Gate),
67 "memory.formation" => Ok(Self::MemoryFormation),
68 "memory.freeze" => Ok(Self::MemoryFreeze),
69 other => Err(format!("unknown modulator type '{other}'")),
70 }
71 }
72
73 pub fn all() -> &'static [Self] {
75 &[
76 Self::FiringThreshold,
77 Self::Leak,
78 Self::FiringProbability,
79 Self::TransmissionGain,
80 Self::Reward,
81 Self::LearningRate,
82 Self::Gate,
83 Self::MemoryFormation,
84 Self::MemoryFreeze,
85 ]
86 }
87
88 pub fn is_neuromodulator(self) -> bool {
90 matches!(
91 self,
92 Self::FiringThreshold | Self::Leak | Self::FiringProbability
93 )
94 }
95
96 pub fn is_area_level(self) -> bool {
98 self.is_neuromodulator() || matches!(self, Self::MemoryFormation | Self::MemoryFreeze)
99 }
100
101 pub fn is_synaptic(self) -> bool {
103 !self.is_area_level()
104 }
105
106 pub fn combines_by_addition(self) -> bool {
108 matches!(self, Self::Reward)
109 }
110}
111
112impl fmt::Display for ModulatorKind {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.write_str(self.as_str())
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ModulatorValidationError(pub String);
121
122impl fmt::Display for ModulatorValidationError {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str(&self.0)
125 }
126}
127
128pub fn validate_instance_fields(
134 magnitude_percent: f32,
135 effect_duration_bursts: u16,
136 graded: bool,
137 full_scale_potential: Option<f32>,
138) -> Result<(), ModulatorValidationError> {
139 if !magnitude_percent.is_finite() || magnitude_percent < -100.0 {
140 return Err(ModulatorValidationError(
141 "magnitude_percent must be finite and >= -100".to_string(),
142 ));
143 }
144 if effect_duration_bursts < 1 {
145 return Err(ModulatorValidationError(
146 "effect_duration_bursts must be >= 1".to_string(),
147 ));
148 }
149 if graded {
150 match full_scale_potential {
151 Some(scale) if scale.is_finite() && scale > 0.0 => {}
152 _ => {
153 return Err(ModulatorValidationError(
154 "graded modulators require full_scale_potential > 0".to_string(),
155 ));
156 }
157 }
158 }
159 Ok(())
160}
161
162pub fn validate_spike_train(
164 enabled: bool,
165 consecutive_fire_limit: u16,
166) -> Result<(), ModulatorValidationError> {
167 if enabled && consecutive_fire_limit < 1 {
168 return Err(ModulatorValidationError(
169 "spike_train requires consecutive_fire_limit >= 1".to_string(),
170 ));
171 }
172 Ok(())
173}
174
175pub fn modulator_signal(
181 magnitude_percent: f32,
182 fired: bool,
183 graded: bool,
184 membrane_potential: f32,
185 full_scale_potential: f32,
186) -> f32 {
187 if !fired {
188 return 0.0;
189 }
190 let strength = if graded {
191 (membrane_potential / full_scale_potential).min(1.0)
192 } else {
193 1.0
194 };
195 (magnitude_percent / 100.0) * strength
196}
197
198pub fn multiplicative_factor(signals: &[f32]) -> f32 {
200 signals
201 .iter()
202 .fold(1.0_f32, |acc, signal| acc * (1.0 + signal))
203}
204
205pub fn gate_factor(signals: &[f32]) -> f32 {
208 signals.iter().copied().fold(0.0_f32, f32::max)
209}
210
211pub fn memory_formation_factor(signals: &[f32]) -> f32 {
214 multiplicative_factor(signals).clamp(0.0, 1.0)
215}
216
217pub fn memory_freeze_active(signals: &[f32]) -> bool {
219 signals.iter().sum::<f32>() > 0.0
220}
221
222pub fn summed_reward(signals: &[f32]) -> f32 {
224 signals.iter().sum()
225}
226
227pub fn scale_baseline(kind: ModulatorKind, baseline: f32, factor: f32) -> f32 {
232 let value = baseline * factor;
233 match kind {
234 ModulatorKind::Leak | ModulatorKind::FiringProbability => value.clamp(0.0, 1.0),
235 ModulatorKind::FiringThreshold
236 | ModulatorKind::TransmissionGain
237 | ModulatorKind::LearningRate
238 | ModulatorKind::Gate
239 | ModulatorKind::MemoryFormation
240 | ModulatorKind::MemoryFreeze => value.max(0.0),
241 ModulatorKind::Reward => value,
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn silent_driver_contributes_nothing() {
251 assert_eq!(modulator_signal(30.0, false, false, 10.0, 10.0), 0.0);
252 }
253
254 #[test]
255 fn ungraded_firing_uses_full_magnitude() {
256 assert!((modulator_signal(30.0, true, false, 1.0, 10.0) - 0.3).abs() < 1e-6);
257 assert!((modulator_signal(-80.0, true, false, 1.0, 10.0) + 0.8).abs() < 1e-6);
258 }
259
260 #[test]
261 fn graded_firing_scales_with_membrane_potential_and_caps_at_one() {
262 let half = modulator_signal(30.0, true, true, 5.0, 10.0);
263 assert!((half - 0.15).abs() < 1e-6);
264 let capped = modulator_signal(30.0, true, true, 40.0, 10.0);
265 assert!((capped - 0.3).abs() < 1e-6);
266 }
267
268 #[test]
269 fn factors_multiply_and_reward_adds() {
270 let factor = multiplicative_factor(&[0.3, -0.5]);
271 assert!((factor - (1.3 * 0.5)).abs() < 1e-6);
272 assert!((summed_reward(&[0.3, -0.5]) + 0.2).abs() < 1e-6);
273 }
274
275 #[test]
276 fn leak_and_excitability_clamp_to_unit_interval() {
277 assert!((scale_baseline(ModulatorKind::Leak, 0.8, 2.0) - 1.0).abs() < 1e-6);
278 assert!((scale_baseline(ModulatorKind::FiringProbability, 0.2, 0.0) - 0.0).abs() < 1e-6);
279 assert!(scale_baseline(ModulatorKind::FiringThreshold, 2.0, 3.0) > 0.0);
280 }
281
282 #[test]
283 fn magnitude_below_minus_100_is_rejected() {
284 assert!(validate_instance_fields(-100.1, 1, false, None).is_err());
285 assert!(validate_instance_fields(-100.0, 1, false, None).is_ok());
286 }
287
288 #[test]
289 fn graded_requires_positive_full_scale() {
290 assert!(validate_instance_fields(10.0, 1, true, None).is_err());
291 assert!(validate_instance_fields(10.0, 1, true, Some(0.0)).is_err());
292 assert!(validate_instance_fields(10.0, 1, true, Some(4.0)).is_ok());
293 }
294
295 #[test]
296 fn spike_train_rejects_unlimited_consecutive_fire() {
297 assert!(validate_spike_train(true, 0).is_err());
298 assert!(validate_spike_train(true, 1).is_ok());
299 assert!(validate_spike_train(false, 0).is_ok());
300 }
301
302 #[test]
303 fn memory_freeze_is_area_level_and_active_above_zero() {
304 assert_eq!(
305 ModulatorKind::parse("memory.freeze").unwrap(),
306 ModulatorKind::MemoryFreeze
307 );
308 assert!(ModulatorKind::MemoryFreeze.is_area_level());
309 assert!(!ModulatorKind::MemoryFreeze.is_synaptic());
310 assert!(!memory_freeze_active(&[]));
311 assert!(!memory_freeze_active(&[0.0]));
312 assert!(memory_freeze_active(&[0.5]));
313 }
314
315 #[test]
316 fn type_strings_round_trip() {
317 for kind in ModulatorKind::all() {
318 assert_eq!(ModulatorKind::parse(kind.as_str()).unwrap(), *kind);
319 }
320 }
321
322 #[test]
323 fn gate_kind_is_synaptic_and_parses() {
324 assert_eq!(
325 ModulatorKind::parse("synaptic.gate").unwrap(),
326 ModulatorKind::Gate
327 );
328 assert_eq!(ModulatorKind::Gate.as_str(), "synaptic.gate");
329 assert!(ModulatorKind::Gate.is_synaptic());
330 assert!(!ModulatorKind::Gate.combines_by_addition());
331 }
332
333 #[test]
334 fn gate_factor_is_closed_when_silent_and_open_if_any_open() {
335 assert_eq!(gate_factor(&[0.0]), 0.0);
336 assert_eq!(gate_factor(&[0.0, 1.0]), 1.0);
337 assert!((gate_factor(&[0.5, 0.25]) - 0.5).abs() < 1e-6);
338 }
339
340 #[test]
341 fn memory_formation_is_area_level_and_clamped() {
342 assert_eq!(
343 ModulatorKind::parse("memory.formation").unwrap(),
344 ModulatorKind::MemoryFormation
345 );
346 assert!(ModulatorKind::MemoryFormation.is_area_level());
347 assert!(!ModulatorKind::MemoryFormation.is_synaptic());
348 assert!(!ModulatorKind::MemoryFormation.is_neuromodulator());
349 assert_eq!(memory_formation_factor(&[-1.0]), 0.0);
350 assert_eq!(memory_formation_factor(&[0.0]), 1.0);
351 assert_eq!(memory_formation_factor(&[0.5]), 1.0);
352 assert!((memory_formation_factor(&[-0.25]) - 0.75).abs() < 1e-6);
353 assert_eq!(memory_formation_factor(&[]), 1.0);
354 }
355}