1use crate::error::{MeshError, Result};
18use serde::de::{Deserializer, Error as DeError};
19use serde::{Deserialize, Serialize};
20
21const ROUTING_TIMESTEPS: usize = 16;
23
24const MIN_FIRE_RATE: f32 = 0.1875;
26
27pub const MAX_ROUTER_CHANNELS: usize = 1024;
33
34pub const MAX_ROUTING_TIMESTEPS: usize = 4096;
39
40#[derive(Clone, Serialize, Deserialize, Debug)]
51#[serde(default)]
52pub struct NeuromodNeuron {
53 pub v: f32,
55 pub v_rest: f32,
57 pub v_reset: f32,
59 pub leak: f32,
61 pub threshold: f32,
63
64 pub gain: f32,
66
67 pub weights: Vec<f32>,
69 pub last_spike: bool,
71}
72
73impl Default for NeuromodNeuron {
74 fn default() -> Self {
75 Self {
76 v: 0.0,
77 v_rest: 0.0,
78 v_reset: 0.0,
79 leak: 0.12,
80 threshold: 0.25,
81 gain: 1.0,
82 weights: Vec::new(),
83 last_spike: false,
84 }
85 }
86}
87
88impl NeuromodNeuron {
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn integrate(&mut self, stimulus: f32) {
97 self.v += stimulus * self.gain;
99 self.v -= (self.v - self.v_rest) * self.leak;
101 }
102
103 pub fn check_fire(&mut self) -> Option<f32> {
105 if self.v >= self.threshold {
106 let peak = self.v;
107 self.v = self.v_reset;
108 self.last_spike = true;
109 return Some(peak);
110 }
111 self.last_spike = false;
112 None
113 }
114
115 pub fn set_gain(&mut self, new_gain: f32) {
117 self.gain = new_gain;
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Serialize)]
141pub struct RouterConfig {
142 pub channel_count: usize,
144 pub self_weight: f32,
148 pub cross_weight: f32,
153 pub threshold: f32,
159 pub leak: f32,
164 pub routing_timesteps: usize,
169 pub min_fire_rate: f32,
173 pub plasticity_decay: f32,
178 pub plasticity_potentiate: f32,
183 pub plasticity_speed: f32,
188 pub fatigue_accumulation: f32,
193 pub fatigue_recovery: f32,
197}
198
199#[derive(Deserialize)]
200struct RawRouterConfig {
201 channel_count: usize,
202 self_weight: f32,
203 cross_weight: f32,
204 threshold: f32,
205 leak: f32,
206 routing_timesteps: usize,
207 min_fire_rate: f32,
208 #[serde(default = "default_plasticity_decay")]
209 plasticity_decay: f32,
210 #[serde(default = "default_plasticity_potentiate")]
211 plasticity_potentiate: f32,
212 #[serde(default = "default_plasticity_speed")]
213 plasticity_speed: f32,
214 #[serde(default = "default_fatigue_accumulation")]
215 fatigue_accumulation: f32,
216 #[serde(default = "default_fatigue_recovery")]
217 fatigue_recovery: f32,
218}
219
220impl RawRouterConfig {
221 fn into_config(self) -> Result<RouterConfig> {
222 let config = RouterConfig {
223 channel_count: self.channel_count,
224 self_weight: self.self_weight,
225 cross_weight: self.cross_weight,
226 threshold: self.threshold,
227 leak: self.leak,
228 routing_timesteps: self.routing_timesteps,
229 min_fire_rate: self.min_fire_rate,
230 plasticity_decay: self.plasticity_decay,
231 plasticity_potentiate: self.plasticity_potentiate,
232 plasticity_speed: self.plasticity_speed,
233 fatigue_accumulation: self.fatigue_accumulation,
234 fatigue_recovery: self.fatigue_recovery,
235 };
236 config.validate()?;
237 Ok(config)
238 }
239}
240
241impl<'de> Deserialize<'de> for RouterConfig {
242 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
243 where
244 D: Deserializer<'de>,
245 {
246 RawRouterConfig::deserialize(deserializer)?
247 .into_config()
248 .map_err(DeError::custom)
249 }
250}
251
252fn default_plasticity_decay() -> f32 {
253 0.02
254}
255fn default_plasticity_potentiate() -> f32 {
256 0.05
257}
258fn default_plasticity_speed() -> f32 {
259 0.1
260}
261fn default_fatigue_accumulation() -> f32 {
262 0.15
263}
264fn default_fatigue_recovery() -> f32 {
265 0.05
266}
267
268impl Default for RouterConfig {
269 fn default() -> Self {
270 Self {
271 channel_count: 3,
272 self_weight: 0.9,
273 cross_weight: -0.15,
274 threshold: 0.22,
275 leak: 0.12,
276 routing_timesteps: ROUTING_TIMESTEPS,
277 min_fire_rate: MIN_FIRE_RATE,
278 plasticity_decay: 0.02,
279 plasticity_potentiate: 0.05,
280 plasticity_speed: 0.1,
281 fatigue_accumulation: 0.15,
282 fatigue_recovery: 0.05,
283 }
284 }
285}
286
287impl RouterConfig {
288 pub fn validate(&self) -> Result<()> {
295 if self.channel_count == 0 || self.channel_count > MAX_ROUTER_CHANNELS {
296 return Err(MeshError::invalid_router_config(
297 "channel_count",
298 format!(
299 "must be in 1..={MAX_ROUTER_CHANNELS}, got {}",
300 self.channel_count
301 ),
302 ));
303 }
304 if self.routing_timesteps == 0 || self.routing_timesteps > MAX_ROUTING_TIMESTEPS {
305 return Err(MeshError::invalid_router_config(
306 "routing_timesteps",
307 format!(
308 "must be in 1..={MAX_ROUTING_TIMESTEPS}, got {}",
309 self.routing_timesteps
310 ),
311 ));
312 }
313 require_finite("self_weight", self.self_weight)?;
314 require_finite("cross_weight", self.cross_weight)?;
315 require_finite("threshold", self.threshold)?;
316 require_unit_interval("leak", self.leak)?;
317 require_unit_interval("min_fire_rate", self.min_fire_rate)?;
318 require_unit_interval("plasticity_decay", self.plasticity_decay)?;
319 require_non_negative("plasticity_potentiate", self.plasticity_potentiate)?;
320 require_unit_interval("plasticity_speed", self.plasticity_speed)?;
321 require_unit_interval("fatigue_accumulation", self.fatigue_accumulation)?;
322 require_unit_interval("fatigue_recovery", self.fatigue_recovery)?;
323 Ok(())
324 }
325}
326
327fn require_finite(field: &'static str, value: f32) -> Result<()> {
328 if value.is_finite() {
329 Ok(())
330 } else {
331 Err(MeshError::invalid_router_config(
332 field,
333 format!("must be finite, got {value}"),
334 ))
335 }
336}
337
338fn require_unit_interval(field: &'static str, value: f32) -> Result<()> {
339 require_finite(field, value)?;
340 if (0.0..=1.0).contains(&value) {
341 Ok(())
342 } else {
343 Err(MeshError::invalid_router_config(
344 field,
345 format!("must be finite and in 0.0..=1.0, got {value}"),
346 ))
347 }
348}
349
350fn require_non_negative(field: &'static str, value: f32) -> Result<()> {
351 require_finite(field, value)?;
352 if value >= 0.0 {
353 Ok(())
354 } else {
355 Err(MeshError::invalid_router_config(
356 field,
357 format!("must be finite and >= 0, got {value}"),
358 ))
359 }
360}
361
362const NEUROMOD_MIN: f32 = 0.0;
364const NEUROMOD_MAX: f32 = 1.0;
365
366#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
379pub struct NeuromodState {
380 pub cortisol: f32,
384 pub dopamine: f32,
387 pub serotonin: f32,
390}
391
392impl NeuromodState {
393 pub fn balanced() -> Self {
395 Self {
396 cortisol: 0.0,
397 dopamine: 0.0,
398 serotonin: 0.0,
399 }
400 }
401
402 pub fn stressed() -> Self {
404 Self {
405 cortisol: 0.8,
406 dopamine: 0.0,
407 serotonin: 0.0,
408 }
409 }
410
411 pub fn rewarded() -> Self {
413 Self {
414 cortisol: 0.0,
415 dopamine: 0.8,
416 serotonin: 0.0,
417 }
418 }
419
420 pub fn validate(&self) -> Result<()> {
426 for (field, value) in [
427 ("cortisol", self.cortisol),
428 ("dopamine", self.dopamine),
429 ("serotonin", self.serotonin),
430 ] {
431 if !value.is_finite() {
432 return Err(MeshError::NonFiniteNeuromodulator { field });
433 }
434 if !(NEUROMOD_MIN..=NEUROMOD_MAX).contains(&value) {
435 return Err(MeshError::OutOfRangeNeuromodulator { field, value });
436 }
437 }
438 Ok(())
439 }
440}
441
442#[derive(Debug, Clone, Default)]
444pub struct RoutingDecision {
445 pub active_channels: Vec<usize>,
447 pub firing_rates: Vec<f32>,
449 pub input_signals: Vec<f32>,
451}
452
453impl RoutingDecision {
454 pub fn is_active(&self, channel: usize) -> bool {
455 self.active_channels.contains(&channel)
456 }
457
458 pub fn is_empty(&self) -> bool {
460 self.active_channels.is_empty()
461 }
462}
463
464fn validate_route_ingress(
468 signals: &[f32],
469 mods: &NeuromodState,
470 expected_len: usize,
471 error_context: &str,
472) -> Result<()> {
473 if signals.len() != expected_len {
474 return Err(MeshError::NeuronCountMismatch {
475 expected: expected_len,
476 got: signals.len(),
477 context: error_context.into(),
478 });
479 }
480 for (index, &value) in signals.iter().enumerate() {
481 if !value.is_finite() {
482 return Err(MeshError::NonFiniteSignal {
483 index,
484 context: error_context.into(),
485 });
486 }
487 }
488 mods.validate()
489}
490
491#[derive(Clone, Debug, Serialize)]
512pub struct ChannelRouter {
513 neurons: Vec<NeuromodNeuron>,
514 config: RouterConfig,
515 pub total_routes: u64,
517 pub channel_fatigue: Vec<f32>,
519 baseline_weights: Vec<Vec<f32>>,
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Default)]
524enum LegacyField<T> {
525 #[default]
526 Omitted,
527 Null,
528 Value(T),
529}
530
531impl<'de, T: Deserialize<'de>> Deserialize<'de> for LegacyField<T> {
532 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
533 where
534 D: serde::Deserializer<'de>,
535 {
536 struct LegacyFieldVisitor<T>(std::marker::PhantomData<T>);
537
538 impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for LegacyFieldVisitor<T> {
539 type Value = LegacyField<T>;
540
541 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
542 formatter.write_str("a value or null")
543 }
544
545 fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
546 where
547 E: serde::de::Error,
548 {
549 Ok(LegacyField::Null)
550 }
551
552 fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
553 where
554 E: serde::de::Error,
555 {
556 Ok(LegacyField::Null)
557 }
558
559 fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
560 where
561 D: serde::Deserializer<'de>,
562 {
563 T::deserialize(deserializer).map(LegacyField::Value)
564 }
565 }
566
567 deserializer.deserialize_option(LegacyFieldVisitor(std::marker::PhantomData))
568 }
569}
570
571#[derive(Deserialize)]
572struct RawChannelRouter {
573 neurons: Vec<NeuromodNeuron>,
574 #[serde(default)]
575 config: LegacyField<RouterConfig>,
576 #[serde(default)]
577 total_routes: u64,
578 #[serde(default)]
579 channel_fatigue: LegacyField<Vec<f32>>,
580 #[serde(default)]
581 baseline_weights: LegacyField<Vec<Vec<f32>>>,
582}
583
584impl RawChannelRouter {
585 fn into_router(self) -> Result<ChannelRouter> {
586 let n_neurons = self.neurons.len();
587 let config = match self.config {
588 LegacyField::Omitted => RouterConfig {
589 channel_count: n_neurons,
590 ..RouterConfig::default()
591 },
592 LegacyField::Null => {
593 return Err(MeshError::invalid_router_config(
594 "config",
595 "null value is not allowed; omit field for legacy format",
596 ));
597 }
598 LegacyField::Value(config) => config,
599 };
600 config.validate()?;
604
605 let n = config.channel_count;
606 validate_neuron_bank(n, &self.neurons)?;
607 let channel_fatigue = match self.channel_fatigue {
608 LegacyField::Omitted => vec![0.0; n],
609 LegacyField::Null => {
610 return Err(MeshError::invalid_router_config(
611 "channel_fatigue",
612 "null value is not allowed; omit field for legacy format",
613 ));
614 }
615 LegacyField::Value(fatigue) => fatigue,
616 };
617 let baseline_weights = match self.baseline_weights {
618 LegacyField::Omitted => self.neurons.iter().map(|neu| neu.weights.clone()).collect(),
619 LegacyField::Null => {
620 return Err(MeshError::invalid_router_config(
621 "baseline_weights",
622 "null value is not allowed; omit field for legacy format",
623 ));
624 }
625 LegacyField::Value(weights) => weights,
626 };
627 validate_fatigue_and_baseline(n, &channel_fatigue, &baseline_weights)?;
628 Ok(ChannelRouter {
629 neurons: self.neurons,
630 config,
631 total_routes: self.total_routes,
632 channel_fatigue,
633 baseline_weights,
634 })
635 }
636}
637
638fn validate_neuron_bank(n: usize, neurons: &[NeuromodNeuron]) -> Result<()> {
642 if neurons.len() != n {
643 return Err(MeshError::invalid_router_config(
644 "neurons",
645 format!("length {} does not match channel_count {n}", neurons.len()),
646 ));
647 }
648 for (i, neu) in neurons.iter().enumerate() {
649 require_finite_named("v", neu.v, i)?;
650 require_finite_named("v_rest", neu.v_rest, i)?;
651 require_finite_named("v_reset", neu.v_reset, i)?;
652 require_finite_named("leak", neu.leak, i)?;
653 require_finite_named("threshold", neu.threshold, i)?;
654 require_finite_named("gain", neu.gain, i)?;
655 if neu.weights.len() != n {
656 return Err(MeshError::invalid_router_config(
657 "weights",
658 format!(
659 "neuron {i} weights length {} does not match channel_count {n}",
660 neu.weights.len()
661 ),
662 ));
663 }
664 if neu.weights.iter().any(|w| !w.is_finite()) {
665 return Err(MeshError::invalid_router_config(
666 "weights",
667 format!("neuron {i} weights contain a non-finite value"),
668 ));
669 }
670 }
671 Ok(())
672}
673
674fn require_finite_named(field: &'static str, value: f32, neuron: usize) -> Result<()> {
675 if value.is_finite() {
676 Ok(())
677 } else {
678 Err(MeshError::invalid_router_config(
679 field,
680 format!("neuron {neuron} {field} must be finite, got {value}"),
681 ))
682 }
683}
684
685fn validate_fatigue_and_baseline(
686 n: usize,
687 channel_fatigue: &[f32],
688 baseline_weights: &[Vec<f32>],
689) -> Result<()> {
690 if channel_fatigue.len() != n {
691 return Err(MeshError::invalid_router_config(
692 "channel_fatigue",
693 format!(
694 "length {} does not match channel_count {n}",
695 channel_fatigue.len()
696 ),
697 ));
698 }
699 for (i, &fatigue) in channel_fatigue.iter().enumerate() {
700 if !fatigue.is_finite() || !(0.0..=1.0).contains(&fatigue) {
701 return Err(MeshError::invalid_router_config(
702 "channel_fatigue",
703 format!("index {i} must be finite and in 0.0..=1.0, got {fatigue}"),
704 ));
705 }
706 }
707 if baseline_weights.len() != n {
708 return Err(MeshError::invalid_router_config(
709 "baseline_weights",
710 format!(
711 "length {} does not match channel_count {n}",
712 baseline_weights.len()
713 ),
714 ));
715 }
716 for (i, row) in baseline_weights.iter().enumerate() {
717 if row.len() != n {
718 return Err(MeshError::invalid_router_config(
719 "baseline_weights",
720 format!(
721 "row {i} length {} does not match channel_count {n}",
722 row.len()
723 ),
724 ));
725 }
726 if row.iter().any(|w| !w.is_finite()) {
727 return Err(MeshError::invalid_router_config(
728 "baseline_weights",
729 format!("row {i} contains a non-finite value"),
730 ));
731 }
732 }
733 Ok(())
734}
735
736impl<'de> Deserialize<'de> for ChannelRouter {
737 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
738 where
739 D: Deserializer<'de>,
740 {
741 RawChannelRouter::deserialize(deserializer)?
742 .into_router()
743 .map_err(DeError::custom)
744 }
745}
746
747impl Default for ChannelRouter {
748 fn default() -> Self {
749 Self::new()
750 }
751}
752
753impl ChannelRouter {
754 pub fn new() -> Self {
756 Self::try_with_config(RouterConfig::default()).expect("default RouterConfig is valid")
757 }
758
759 pub fn with_config(config: RouterConfig) -> Self {
770 Self::try_with_config(config).unwrap_or_else(|err| panic!("{err}"))
771 }
772
773 pub fn try_with_config(config: RouterConfig) -> Result<Self> {
778 config.validate()?;
779 let n = config.channel_count;
780 let neurons: Vec<NeuromodNeuron> = (0..n)
781 .map(|i| {
782 let mut neu = NeuromodNeuron::new();
783 neu.weights = vec![config.cross_weight; n];
785 neu.weights[i] = config.self_weight;
786 neu.threshold = config.threshold;
787 neu.leak = config.leak;
788 neu
789 })
790 .collect();
791
792 let baseline_weights = neurons.iter().map(|neu| neu.weights.clone()).collect();
793
794 Ok(Self {
795 neurons,
796 config,
797 total_routes: 0,
798 channel_fatigue: vec![0.0; n],
799 baseline_weights,
800 })
801 }
802
803 pub fn route<S: AsRef<[f32]>>(&mut self, signals: S) -> Result<RoutingDecision> {
820 self.route_modulated_with_context(signals, &NeuromodState::balanced(), "route signals")
821 }
822
823 pub fn route_modulated<S: AsRef<[f32]>>(
840 &mut self,
841 signals: S,
842 mods: &NeuromodState,
843 ) -> Result<RoutingDecision> {
844 self.route_modulated_with_context(signals, mods, "route_modulated signals")
845 }
846
847 fn route_modulated_with_context<S: AsRef<[f32]>>(
851 &mut self,
852 signals: S,
853 mods: &NeuromodState,
854 error_context: &str,
855 ) -> Result<RoutingDecision> {
856 let signals = signals.as_ref();
857 let n = self.config.channel_count;
858 validate_route_ingress(signals, mods, n, error_context)?;
859
860 self.ensure_neuromod_state_synced();
864 let (effective_thresholds, effective_leaks) = self.compute_effective_params(mods);
865
866 for neu in &mut self.neurons {
868 neu.v = 0.0;
869 }
870
871 let timesteps = self.config.routing_timesteps;
872 let min_rate = self.config.min_fire_rate;
873 let spike_counts =
874 self.integrate_signals(signals, &effective_thresholds, &effective_leaks, timesteps);
875
876 let mut firing_rates = vec![0.0f32; n];
877 let mut active_channels = Vec::new();
878 for i in 0..n {
879 firing_rates[i] = spike_counts[i] as f32 / timesteps as f32;
880 if firing_rates[i] >= min_rate {
881 active_channels.push(i);
882 }
883 }
884
885 self.apply_plasticity(&active_channels, mods);
887
888 self.total_routes += 1;
889 Ok(RoutingDecision {
890 active_channels,
891 firing_rates,
892 input_signals: signals.to_vec(),
893 })
894 }
895
896 fn integrate_signals(
904 &mut self,
905 signals: &[f32],
906 effective_thresholds: &[f32],
907 effective_leaks: &[f32],
908 timesteps: usize,
909 ) -> Vec<u32> {
910 let n = self.config.channel_count;
911 let mut spike_counts = vec![0u32; n];
912 for _ in 0..timesteps {
913 for (i, neu) in self.neurons.iter_mut().enumerate().take(n) {
918 debug_assert_eq!(
919 neu.weights.len(),
920 signals.len(),
921 "Neuron weights length mismatch"
922 );
923 let stimulus: f32 = signals
924 .iter()
925 .zip(neu.weights.iter())
926 .map(|(sig, w)| sig * w)
927 .sum();
928 neu.leak = effective_leaks[i];
929 neu.integrate(stimulus);
930 neu.threshold = effective_thresholds[i];
931 if neu.check_fire().is_some() {
932 spike_counts[i] += 1;
933 }
934 }
935 }
936 spike_counts
937 }
938
939 fn ensure_neuromod_state_synced(&mut self) {
954 let n = self.config.channel_count;
955
956 if self.channel_fatigue.len() != n {
958 self.channel_fatigue.resize(n, 0.0);
959 }
960
961 let mut weights_repaired = false;
974 for neu in &mut self.neurons {
975 if neu.weights.len() != n {
976 weights_repaired = true;
977 neu.weights.resize(n, 0.0);
978 }
979 }
980
981 let baseline_ok = !weights_repaired
985 && self.baseline_weights.len() == n
986 && self.baseline_weights.iter().all(|row| row.len() == n);
987 if !baseline_ok {
988 self.baseline_weights = self.neurons.iter().map(|neu| neu.weights.clone()).collect();
989 }
990 }
991
992 fn compute_effective_params(&self, mods: &NeuromodState) -> (Vec<f32>, Vec<f32>) {
1000 let n = self.config.channel_count;
1001 let mut thresholds = vec![0.0f32; n];
1002 let mut leaks = vec![0.0f32; n];
1003 for i in 0..n {
1004 let baseline_stress = 1.0 + mods.cortisol * 0.5;
1005 let fatigue_amplification = 1.0 + mods.cortisol * self.channel_fatigue[i];
1006 let fatigue_factor = baseline_stress * fatigue_amplification;
1007 let dopamine_factor = 1.0 - mods.dopamine * 0.5;
1008 thresholds[i] =
1009 (self.config.threshold * fatigue_factor * dopamine_factor).clamp(0.05, 2.0);
1010 leaks[i] = (self.config.leak * (1.0 + mods.serotonin)).clamp(0.0, 1.0);
1011 }
1012 (thresholds, leaks)
1013 }
1014
1015 fn apply_plasticity(&mut self, active_channels: &[usize], mods: &NeuromodState) {
1026 let n = self.config.channel_count;
1027 debug_assert!(
1031 self.neurons.len() >= n
1032 && self.neurons.iter().all(|neu| neu.weights.len() >= n)
1033 && self.baseline_weights.len() >= n
1034 && self.baseline_weights.iter().all(|row| row.len() >= n),
1035 "apply_plasticity invariant violation — ensure_neuromod_state_synced should have rebuilt"
1036 );
1037 if n > self.neurons.len()
1038 || self.neurons.iter().any(|neu| neu.weights.len() < n)
1039 || self.baseline_weights.len() < n
1040 || self.baseline_weights.iter().any(|row| row.len() < n)
1041 {
1042 return;
1043 }
1044 let decay = self.config.plasticity_decay;
1045 let potentiate = self.config.plasticity_potentiate;
1046 let plasticity_speed = self.config.plasticity_speed;
1047 let fatigue_acc = self.config.fatigue_accumulation;
1048 let fatigue_rec = self.config.fatigue_recovery;
1049
1050 for i in 0..n {
1051 if active_channels.contains(&i) {
1052 let strengthen = potentiate * (1.0 + mods.dopamine);
1054 for j in 0..n {
1055 let baseline = self.baseline_weights[i][j];
1056 let current = self.neurons[i].weights[j];
1057 let next_weight = if plasticity_speed == 0.0 {
1059 current
1060 } else {
1061 let target = baseline * (1.0 + strengthen);
1062 let update = if !target.is_finite() {
1063 let extreme = if (baseline > 0.0 && strengthen >= -1.0)
1064 || (baseline < 0.0 && strengthen < -1.0)
1065 {
1066 2.0
1067 } else {
1068 -1.5
1069 };
1070 current + (extreme - current) * plasticity_speed
1071 } else {
1072 current + (target - current) * plasticity_speed
1073 };
1074 if update.is_finite() {
1075 update.clamp(-1.5, 2.0)
1076 } else if update.is_sign_positive() {
1077 2.0
1078 } else {
1079 -1.5
1080 }
1081 };
1082 self.neurons[i].weights[j] = next_weight;
1083 }
1084 self.channel_fatigue[i] = (self.channel_fatigue[i] + fatigue_acc).min(1.0);
1085 } else {
1086 for j in 0..n {
1088 let baseline = self.baseline_weights[i][j];
1089 let current = self.neurons[i].weights[j];
1090 let update = if decay == 0.0 || current == baseline {
1091 current
1092 } else {
1093 let diff = baseline - current;
1094 if !diff.is_finite() {
1095 if baseline > current { 2.0 } else { -1.5 }
1096 } else {
1097 current + diff * decay
1098 }
1099 };
1100 self.neurons[i].weights[j] = if update.is_finite() {
1101 update.clamp(-1.5, 2.0)
1102 } else if update.is_sign_positive() {
1103 2.0
1104 } else {
1105 -1.5
1106 };
1107 }
1108 self.channel_fatigue[i] = (self.channel_fatigue[i] - fatigue_rec).max(0.0);
1109 }
1110 }
1111 }
1112
1113 pub fn apply_feedback(&mut self, channel_idx: usize, reward: f32) {
1120 self.ensure_neuromod_state_synced();
1121 let n = self.config.channel_count;
1122 debug_assert!(
1127 channel_idx < n
1128 && self.neurons.len() >= n
1129 && self.neurons.iter().all(|neu| neu.weights.len() >= n),
1130 "apply_feedback invariant violation — ensure_neuromod_state_synced should have rebuilt"
1131 );
1132 if channel_idx >= n
1133 || n > self.neurons.len()
1134 || self.neurons.iter().any(|neu| neu.weights.len() < n)
1135 {
1136 return;
1137 }
1138
1139 let delta = reward * 0.01;
1140
1141 self.neurons[channel_idx].weights[channel_idx] =
1142 (self.neurons[channel_idx].weights[channel_idx] + delta).clamp(0.1, 2.0);
1143
1144 if reward > 0.0 {
1145 for j in 0..n {
1146 if j != channel_idx {
1147 self.neurons[j].weights[channel_idx] =
1148 (self.neurons[j].weights[channel_idx] - delta * 0.3).clamp(-1.0, 1.5);
1149 }
1150 }
1151 }
1152
1153 self.sync_baseline_after_feedback(channel_idx, reward);
1155 }
1156
1157 fn sync_baseline_after_feedback(&mut self, channel_idx: usize, reward: f32) {
1162 let n = self.config.channel_count;
1163 debug_assert!(
1168 self.baseline_weights.len() == n
1169 && self.baseline_weights.iter().all(|row| row.len() == n),
1170 "baseline_weights shape mismatch — ensure_neuromod_state_synced should have rebuilt"
1171 );
1172 if self.baseline_weights.len() != n
1173 || self.baseline_weights.iter().any(|row| row.len() != n)
1174 {
1175 return;
1176 }
1177 self.baseline_weights[channel_idx][channel_idx] =
1178 self.neurons[channel_idx].weights[channel_idx];
1179 if reward > 0.0 {
1180 for j in 0..n {
1181 if j != channel_idx {
1182 self.baseline_weights[j][channel_idx] = self.neurons[j].weights[channel_idx];
1183 }
1184 }
1185 }
1186 }
1187
1188 pub fn set_global_gain(&mut self, gain: f32) {
1190 for neu in &mut self.neurons {
1191 neu.set_gain(gain);
1192 }
1193 }
1194
1195 pub fn weight_matrix(&self) -> Vec<Vec<f32>> {
1197 self.neurons.iter().map(|neu| neu.weights.clone()).collect()
1198 }
1199
1200 pub fn config(&self) -> &RouterConfig {
1202 &self.config
1203 }
1204
1205 pub fn fatigue(&self) -> &[f32] {
1207 &self.channel_fatigue
1208 }
1209}
1210
1211#[cfg(test)]
1212mod validate_tests {
1213 use super::*;
1214 use serde_json::json;
1215
1216 fn router_field(err: &MeshError) -> &'static str {
1217 match err {
1218 MeshError::InvalidRouterConfig { field, .. } => field,
1219 other => panic!("expected InvalidRouterConfig, got {other:?}"),
1220 }
1221 }
1222
1223 fn serde_field_from_config_json(value: serde_json::Value) -> String {
1224 let err = serde_json::from_value::<RouterConfig>(value).expect_err("config should fail");
1225 err.to_string()
1226 }
1227
1228 fn serde_field_from_router_json(value: serde_json::Value) -> String {
1229 let err = serde_json::from_value::<ChannelRouter>(value).expect_err("router should fail");
1230 err.to_string()
1231 }
1232
1233 enum BadValue {
1234 Count(usize),
1235 Float(f32),
1236 }
1237
1238 struct InvalidCase {
1239 name: &'static str,
1240 field: &'static str,
1241 value: BadValue,
1242 }
1243
1244 fn apply_bad(config: &mut RouterConfig, field: &str, value: &BadValue) {
1245 match (field, value) {
1246 ("channel_count", BadValue::Count(v)) => config.channel_count = *v,
1247 ("routing_timesteps", BadValue::Count(v)) => config.routing_timesteps = *v,
1248 ("self_weight", BadValue::Float(v)) => config.self_weight = *v,
1249 ("cross_weight", BadValue::Float(v)) => config.cross_weight = *v,
1250 ("threshold", BadValue::Float(v)) => config.threshold = *v,
1251 ("leak", BadValue::Float(v)) => config.leak = *v,
1252 ("min_fire_rate", BadValue::Float(v)) => config.min_fire_rate = *v,
1253 ("plasticity_decay", BadValue::Float(v)) => config.plasticity_decay = *v,
1254 ("plasticity_potentiate", BadValue::Float(v)) => config.plasticity_potentiate = *v,
1255 ("plasticity_speed", BadValue::Float(v)) => config.plasticity_speed = *v,
1256 ("fatigue_accumulation", BadValue::Float(v)) => config.fatigue_accumulation = *v,
1257 ("fatigue_recovery", BadValue::Float(v)) => config.fatigue_recovery = *v,
1258 _ => panic!("unhandled invalid-case field {field}"),
1259 }
1260 }
1261
1262 fn json_representable(value: &BadValue) -> bool {
1263 match *value {
1264 BadValue::Count(_) => true,
1265 BadValue::Float(v) => v.is_finite(),
1266 }
1267 }
1268
1269 fn invalid_cases() -> Vec<InvalidCase> {
1270 let counts = [
1271 ("channel_count_zero", "channel_count", 0usize),
1272 (
1273 "channel_count_over_max",
1274 "channel_count",
1275 MAX_ROUTER_CHANNELS + 1,
1276 ),
1277 ("channel_count_usize_max", "channel_count", usize::MAX),
1278 ("routing_timesteps_zero", "routing_timesteps", 0),
1279 (
1280 "routing_timesteps_over_max",
1281 "routing_timesteps",
1282 MAX_ROUTING_TIMESTEPS + 1,
1283 ),
1284 ];
1285 let floats = [
1286 ("self_weight_nan", "self_weight", f32::NAN),
1287 ("self_weight_inf", "self_weight", f32::INFINITY),
1288 ("cross_weight_neg_inf", "cross_weight", f32::NEG_INFINITY),
1289 ("threshold_nan", "threshold", f32::NAN),
1290 ("leak_below", "leak", -0.01),
1291 ("leak_above", "leak", 1.01),
1292 ("leak_nan", "leak", f32::NAN),
1293 ("leak_inf", "leak", f32::INFINITY),
1294 ("min_fire_rate_below", "min_fire_rate", -0.01),
1295 ("min_fire_rate_above", "min_fire_rate", 1.01),
1296 ("min_fire_rate_nan", "min_fire_rate", f32::NAN),
1297 ("plasticity_decay_below", "plasticity_decay", -0.01),
1298 ("plasticity_decay_above", "plasticity_decay", 1.01),
1299 ("plasticity_decay_nan", "plasticity_decay", f32::NAN),
1300 (
1301 "plasticity_potentiate_negative",
1302 "plasticity_potentiate",
1303 -0.01,
1304 ),
1305 (
1306 "plasticity_potentiate_nan",
1307 "plasticity_potentiate",
1308 f32::NAN,
1309 ),
1310 (
1311 "plasticity_potentiate_inf",
1312 "plasticity_potentiate",
1313 f32::INFINITY,
1314 ),
1315 ("plasticity_speed_below", "plasticity_speed", -0.01),
1316 ("plasticity_speed_above", "plasticity_speed", 1.01),
1317 ("plasticity_speed_nan", "plasticity_speed", f32::NAN),
1318 ("fatigue_accumulation_below", "fatigue_accumulation", -0.01),
1319 ("fatigue_accumulation_above", "fatigue_accumulation", 1.01),
1320 ("fatigue_accumulation_nan", "fatigue_accumulation", f32::NAN),
1321 ("fatigue_recovery_below", "fatigue_recovery", -0.01),
1322 ("fatigue_recovery_above", "fatigue_recovery", 1.01),
1323 ("fatigue_recovery_inf", "fatigue_recovery", f32::INFINITY),
1324 ];
1325 counts
1326 .into_iter()
1327 .map(|(name, field, value)| InvalidCase {
1328 name,
1329 field,
1330 value: BadValue::Count(value),
1331 })
1332 .chain(floats.into_iter().map(|(name, field, value)| InvalidCase {
1333 name,
1334 field,
1335 value: BadValue::Float(value),
1336 }))
1337 .collect()
1338 }
1339 #[test]
1340 fn default_config_is_valid() {
1341 RouterConfig::default().validate().unwrap();
1342 }
1343
1344 #[test]
1345 fn table_rejects_every_invalid_config_field() {
1346 for case in invalid_cases() {
1347 let mut config = RouterConfig::default();
1348 apply_bad(&mut config, case.field, &case.value);
1349 let err = match config.validate() {
1350 Err(err) => err,
1351 Ok(()) => panic!("{}: validate should fail", case.name),
1352 };
1353 assert_eq!(
1354 router_field(&err),
1355 case.field,
1356 "{}: unexpected field in {err}",
1357 case.name
1358 );
1359
1360 let ctor_err = ChannelRouter::try_with_config(config.clone()).expect_err(case.name);
1361 assert_eq!(
1362 router_field(&ctor_err),
1363 case.field,
1364 "{}: constructor field mismatch",
1365 case.name
1366 );
1367 assert_eq!(
1368 ctor_err, err,
1369 "{}: validate and try_with_config must return the same error",
1370 case.name
1371 );
1372
1373 if !json_representable(&case.value) {
1374 continue;
1375 }
1376 let mut mutated = RouterConfig::default();
1377 apply_bad(&mut mutated, case.field, &case.value);
1378 let config_msg = serde_field_from_config_json(serde_json::to_value(&mutated).unwrap());
1379 assert!(
1380 config_msg.contains(case.field),
1381 "{}: RouterConfig serde missing field name: {config_msg}",
1382 case.name
1383 );
1384 assert!(
1385 config_msg.contains(&err.to_string()),
1386 "{}: RouterConfig serde should carry the same MeshError Display, got {config_msg}, expected {}",
1387 case.name,
1388 err
1389 );
1390
1391 let mut router_json = serde_json::to_value(ChannelRouter::default()).unwrap();
1392 router_json["config"] = serde_json::to_value(&mutated).unwrap();
1393 let router_msg = serde_field_from_router_json(router_json);
1394 assert!(
1395 router_msg.contains(case.field),
1396 "{}: ChannelRouter serde missing field name: {router_msg}",
1397 case.name
1398 );
1399 assert!(
1400 router_msg.contains(&err.to_string()),
1401 "{}: ChannelRouter serde should carry the same MeshError Display, got {router_msg}, expected {}",
1402 case.name,
1403 err
1404 );
1405 }
1406 }
1407
1408 type BoundaryMutator = fn(&mut RouterConfig);
1409
1410 fn valid_boundary_mutators() -> Vec<(&'static str, BoundaryMutator)> {
1411 vec![
1412 ("min_channels", |c| c.channel_count = 1),
1413 ("max_channels_validate_only", |c| {
1414 c.channel_count = MAX_ROUTER_CHANNELS
1415 }),
1416 ("leak_0", |c| c.leak = 0.0),
1417 ("leak_1", |c| c.leak = 1.0),
1418 ("min_fire_0", |c| c.min_fire_rate = 0.0),
1419 ("min_fire_1", |c| c.min_fire_rate = 1.0),
1420 ("decay_0", |c| c.plasticity_decay = 0.0),
1421 ("decay_1", |c| c.plasticity_decay = 1.0),
1422 ("potentiate_0", |c| c.plasticity_potentiate = 0.0),
1423 ("speed_0", |c| c.plasticity_speed = 0.0),
1424 ("speed_1", |c| c.plasticity_speed = 1.0),
1425 ("fatigue_acc_0", |c| c.fatigue_accumulation = 0.0),
1426 ("fatigue_acc_1", |c| c.fatigue_accumulation = 1.0),
1427 ("fatigue_rec_0", |c| c.fatigue_recovery = 0.0),
1428 ("fatigue_rec_1", |c| c.fatigue_recovery = 1.0),
1429 ("signed_cross_inhibition", |c| c.cross_weight = -1.0),
1430 ("positive_cross_weight", |c| c.cross_weight = 0.25),
1431 ("signed_self_weight", |c| c.self_weight = -0.3),
1432 ("zero_threshold", |c| c.threshold = 0.0),
1433 ("one_timestep", |c| c.routing_timesteps = 1),
1434 ("max_timesteps", |c| {
1435 c.routing_timesteps = MAX_ROUTING_TIMESTEPS
1436 }),
1437 ]
1438 }
1439
1440 #[test]
1441 fn valid_boundaries_round_trip() {
1442 for (name, mutate) in valid_boundary_mutators() {
1443 let mut config = RouterConfig::default();
1444 mutate(&mut config);
1445 config
1446 .validate()
1447 .unwrap_or_else(|err| panic!("{name}: valid boundary rejected: {err}"));
1448 let json = serde_json::to_value(&config).unwrap();
1449 let restored: RouterConfig = serde_json::from_value(json)
1450 .unwrap_or_else(|err| panic!("{name}: valid config failed to deserialize: {err}"));
1451 assert_eq!(restored, config, "{name}: config round-trip");
1452
1453 if config.channel_count == MAX_ROUTER_CHANNELS {
1454 continue;
1455 }
1456 let router = ChannelRouter::try_with_config(config.clone())
1457 .unwrap_or_else(|err| panic!("{name}: try_with_config: {err}"));
1458 let router_json = serde_json::to_value(&router).unwrap();
1459 let restored_router: ChannelRouter = serde_json::from_value(router_json)
1460 .unwrap_or_else(|err| panic!("{name}: router deserialize: {err}"));
1461 assert_eq!(restored_router.config(), router.config(), "{name}");
1462 assert_eq!(
1463 restored_router.weight_matrix(),
1464 router.weight_matrix(),
1465 "{name}"
1466 );
1467 assert_eq!(restored_router.fatigue(), router.fatigue(), "{name}");
1468 assert_eq!(restored_router.total_routes, router.total_routes, "{name}");
1469 }
1470 }
1471
1472 #[test]
1473 fn custom_and_default_routers_round_trip() {
1474 let default_router = ChannelRouter::default();
1475 let json = serde_json::to_value(&default_router).unwrap();
1476 let restored: ChannelRouter = serde_json::from_value(json).unwrap();
1477 assert_eq!(restored.config(), default_router.config());
1478 assert_eq!(restored.weight_matrix(), default_router.weight_matrix());
1479
1480 let custom = RouterConfig {
1481 channel_count: 4,
1482 self_weight: 1.1,
1483 cross_weight: -0.2,
1484 threshold: 0.3,
1485 leak: 0.05,
1486 routing_timesteps: 8,
1487 min_fire_rate: 0.25,
1488 plasticity_decay: 0.03,
1489 plasticity_potentiate: 0.2,
1490 plasticity_speed: 0.4,
1491 fatigue_accumulation: 0.2,
1492 fatigue_recovery: 0.1,
1493 };
1494 let router = ChannelRouter::try_with_config(custom.clone()).unwrap();
1495 let json = serde_json::to_value(&router).unwrap();
1496 let restored: ChannelRouter = serde_json::from_value(json).unwrap();
1497 assert_eq!(restored.config(), &custom);
1498 assert_eq!(restored.weight_matrix(), router.weight_matrix());
1499 }
1500
1501 #[test]
1502 fn try_with_config_rejects_oversized_channel_count_without_allocating() {
1503 let config = RouterConfig {
1506 channel_count: usize::MAX,
1507 ..RouterConfig::default()
1508 };
1509 let err = ChannelRouter::try_with_config(config).unwrap_err();
1510 assert_eq!(router_field(&err), "channel_count");
1511 }
1512
1513 #[test]
1514 fn max_channel_count_is_accepted_by_validate_and_constructor() {
1515 let config = RouterConfig {
1516 channel_count: MAX_ROUTER_CHANNELS,
1517 ..RouterConfig::default()
1518 };
1519 config.validate().unwrap();
1520 let router = ChannelRouter::try_with_config(config).unwrap();
1521 assert_eq!(router.config().channel_count, MAX_ROUTER_CHANNELS);
1522 assert_eq!(router.weight_matrix().len(), MAX_ROUTER_CHANNELS);
1523 assert_eq!(router.weight_matrix()[0].len(), MAX_ROUTER_CHANNELS);
1524 assert_eq!(router.fatigue().len(), MAX_ROUTER_CHANNELS);
1525 }
1526
1527 #[test]
1528 fn malformed_shapes_are_rejected_with_matching_fields() {
1529 let router = ChannelRouter::default();
1530 let base = serde_json::to_value(&router).unwrap();
1531
1532 let mut empty_inner = base.clone();
1533 empty_inner["baseline_weights"] = json!([[], [], []]);
1534 let msg = serde_field_from_router_json(empty_inner);
1535 assert!(msg.contains("baseline_weights"), "empty inner rows: {msg}");
1536
1537 let mut short_fatigue = base.clone();
1538 short_fatigue["channel_fatigue"] = json!([0.0, 0.0]);
1539 let msg = serde_field_from_router_json(short_fatigue);
1540 assert!(msg.contains("channel_fatigue"), "short fatigue: {msg}");
1541
1542 let mut extra_neurons = base.clone();
1543 extra_neurons["neurons"] = json!([]);
1544 let msg = serde_field_from_router_json(extra_neurons);
1545 assert!(msg.contains("neurons"), "empty neurons: {msg}");
1546
1547 let mut missing_weights = base.clone();
1548 missing_weights["neurons"][0]
1549 .as_object_mut()
1550 .unwrap()
1551 .remove("weights");
1552 let msg = serde_field_from_router_json(missing_weights);
1553 assert!(msg.contains("weights"), "missing neuron weights: {msg}");
1554
1555 let mut ragged_baseline = base.clone();
1556 ragged_baseline["baseline_weights"] = json!([[0.9, -0.15, -0.15], [0.0], [0.0, 0.0, 0.0]]);
1557 let msg = serde_field_from_router_json(ragged_baseline);
1558 assert!(msg.contains("baseline_weights"), "ragged baseline: {msg}");
1559
1560 let mut out_of_range_fatigue = base;
1561 out_of_range_fatigue["channel_fatigue"] = json!([0.0, 1.5, 0.0]);
1562 let msg = serde_field_from_router_json(out_of_range_fatigue);
1563 assert!(
1564 msg.contains("channel_fatigue"),
1565 "fatigue out of range: {msg}"
1566 );
1567 }
1568
1569 #[test]
1570 fn legacy_snapshot_without_optional_fields_deserializes() {
1571 let router = ChannelRouter::default();
1572 let mut json = serde_json::to_value(&router).unwrap();
1573 let obj = json.as_object_mut().unwrap();
1574 obj.remove("config");
1575 obj.remove("channel_fatigue");
1576 obj.remove("baseline_weights");
1577
1578 let restored: ChannelRouter =
1579 serde_json::from_value(json).expect("legacy snapshot must deserialize");
1580 assert_eq!(restored.config().channel_count, 3);
1581 assert_eq!(restored.weight_matrix().len(), 3);
1582 assert_eq!(restored.fatigue(), &[0.0, 0.0, 0.0]);
1583 let mut restored = restored;
1584 restored
1585 .route_modulated([0.5, 0.0, 0.0], &NeuromodState::balanced())
1586 .unwrap();
1587 }
1588
1589 #[test]
1590 fn signed_zero_rates_are_accepted() {
1591 let config = RouterConfig {
1592 leak: -0.0,
1593 min_fire_rate: -0.0,
1594 plasticity_decay: -0.0,
1595 plasticity_potentiate: -0.0,
1596 plasticity_speed: -0.0,
1597 fatigue_accumulation: -0.0,
1598 fatigue_recovery: -0.0,
1599 ..RouterConfig::default()
1600 };
1601 config.validate().unwrap();
1602 ChannelRouter::try_with_config(config).unwrap();
1603 }
1604
1605 #[test]
1606 fn explicit_null_fields_are_rejected() {
1607 for field in ["config", "channel_fatigue", "baseline_weights"] {
1608 let router = ChannelRouter::default();
1609 let mut json = serde_json::to_value(&router).unwrap();
1610 json[field] = serde_json::Value::Null;
1611 let err = serde_json::from_value::<ChannelRouter>(json)
1612 .expect_err("explicit null field must be rejected");
1613 let msg = err.to_string();
1614 assert!(
1615 msg.contains(field),
1616 "error should mention field {field}, got: {msg}"
1617 );
1618 }
1619 }
1620
1621 #[test]
1622 fn extreme_weights_and_potentiate_with_zero_speed_do_not_produce_nan() {
1623 let config = RouterConfig {
1624 self_weight: 1.0,
1625 cross_weight: -0.5,
1626 plasticity_potentiate: f32::MAX,
1627 plasticity_speed: 0.0,
1628 ..RouterConfig::default()
1629 };
1630 config.validate().unwrap();
1631 let mut router = ChannelRouter::try_with_config(config).unwrap();
1632 let initial_weights = router.weight_matrix();
1633 let res = router.route_modulated([1.0, 0.0, 0.0], &NeuromodState::balanced());
1634 assert!(res.is_ok());
1635 for (row_idx, row) in router.weight_matrix().iter().enumerate() {
1636 for (col_idx, &w) in row.iter().enumerate() {
1637 assert!(
1638 w.is_finite(),
1639 "weight at [{row_idx}][{col_idx}] must remain finite, got {w}"
1640 );
1641 assert_eq!(
1642 w, initial_weights[row_idx][col_idx],
1643 "weight with plasticity_speed 0.0 must remain unchanged"
1644 );
1645 }
1646 }
1647
1648 let config_speed = RouterConfig {
1650 self_weight: 1.0,
1651 cross_weight: -0.5,
1652 plasticity_potentiate: f32::MAX,
1653 plasticity_speed: 0.5,
1654 ..RouterConfig::default()
1655 };
1656 let mut router_speed = ChannelRouter::try_with_config(config_speed).unwrap();
1657 let res_speed = router_speed.route_modulated([1.0, 0.0, 0.0], &NeuromodState::balanced());
1658 assert!(res_speed.is_ok());
1659 for (row_idx, row) in router_speed.weight_matrix().iter().enumerate() {
1660 for (col_idx, &w) in row.iter().enumerate() {
1661 assert!(
1662 w.is_finite(),
1663 "weight at [{row_idx}][{col_idx}] must remain finite, got {w}"
1664 );
1665 assert!(
1666 (-1.5..=2.0).contains(&w),
1667 "weight {w} at [{row_idx}][{col_idx}] must stay within [-1.5, 2.0]"
1668 );
1669 }
1670 }
1671 }
1672}