use crate::error::{MeshError, Result};
use serde::de::{Deserializer, Error as DeError};
use serde::{Deserialize, Serialize};
const ROUTING_TIMESTEPS: usize = 16;
const MIN_FIRE_RATE: f32 = 0.1875;
pub const MAX_ROUTER_CHANNELS: usize = 1024;
pub const MAX_ROUTING_TIMESTEPS: usize = 4096;
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct NeuromodNeuron {
pub v: f32,
pub v_rest: f32,
pub v_reset: f32,
pub leak: f32,
pub threshold: f32,
pub gain: f32,
pub weights: Vec<f32>,
pub last_spike: bool,
}
impl Default for NeuromodNeuron {
fn default() -> Self {
Self {
v: 0.0,
v_rest: 0.0,
v_reset: 0.0,
leak: 0.12,
threshold: 0.25,
gain: 1.0,
weights: Vec::new(),
last_spike: false,
}
}
}
impl NeuromodNeuron {
pub fn new() -> Self {
Self::default()
}
pub fn integrate(&mut self, stimulus: f32) {
self.v += stimulus * self.gain;
self.v -= (self.v - self.v_rest) * self.leak;
}
pub fn check_fire(&mut self) -> Option<f32> {
if self.v >= self.threshold {
let peak = self.v;
self.v = self.v_reset;
self.last_spike = true;
return Some(peak);
}
self.last_spike = false;
None
}
pub fn set_gain(&mut self, new_gain: f32) {
self.gain = new_gain;
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RouterConfig {
pub channel_count: usize,
pub self_weight: f32,
pub cross_weight: f32,
pub threshold: f32,
pub leak: f32,
pub routing_timesteps: usize,
pub min_fire_rate: f32,
pub plasticity_decay: f32,
pub plasticity_potentiate: f32,
pub plasticity_speed: f32,
pub fatigue_accumulation: f32,
pub fatigue_recovery: f32,
}
#[derive(Deserialize)]
struct RawRouterConfig {
channel_count: usize,
self_weight: f32,
cross_weight: f32,
threshold: f32,
leak: f32,
routing_timesteps: usize,
min_fire_rate: f32,
#[serde(default = "default_plasticity_decay")]
plasticity_decay: f32,
#[serde(default = "default_plasticity_potentiate")]
plasticity_potentiate: f32,
#[serde(default = "default_plasticity_speed")]
plasticity_speed: f32,
#[serde(default = "default_fatigue_accumulation")]
fatigue_accumulation: f32,
#[serde(default = "default_fatigue_recovery")]
fatigue_recovery: f32,
}
impl RawRouterConfig {
fn into_config(self) -> Result<RouterConfig> {
let config = RouterConfig {
channel_count: self.channel_count,
self_weight: self.self_weight,
cross_weight: self.cross_weight,
threshold: self.threshold,
leak: self.leak,
routing_timesteps: self.routing_timesteps,
min_fire_rate: self.min_fire_rate,
plasticity_decay: self.plasticity_decay,
plasticity_potentiate: self.plasticity_potentiate,
plasticity_speed: self.plasticity_speed,
fatigue_accumulation: self.fatigue_accumulation,
fatigue_recovery: self.fatigue_recovery,
};
config.validate()?;
Ok(config)
}
}
impl<'de> Deserialize<'de> for RouterConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
RawRouterConfig::deserialize(deserializer)?
.into_config()
.map_err(DeError::custom)
}
}
fn default_plasticity_decay() -> f32 {
0.02
}
fn default_plasticity_potentiate() -> f32 {
0.05
}
fn default_plasticity_speed() -> f32 {
0.1
}
fn default_fatigue_accumulation() -> f32 {
0.15
}
fn default_fatigue_recovery() -> f32 {
0.05
}
impl Default for RouterConfig {
fn default() -> Self {
Self {
channel_count: 3,
self_weight: 0.9,
cross_weight: -0.15,
threshold: 0.22,
leak: 0.12,
routing_timesteps: ROUTING_TIMESTEPS,
min_fire_rate: MIN_FIRE_RATE,
plasticity_decay: 0.02,
plasticity_potentiate: 0.05,
plasticity_speed: 0.1,
fatigue_accumulation: 0.15,
fatigue_recovery: 0.05,
}
}
}
impl RouterConfig {
pub fn validate(&self) -> Result<()> {
if self.channel_count == 0 || self.channel_count > MAX_ROUTER_CHANNELS {
return Err(MeshError::invalid_router_config(
"channel_count",
format!(
"must be in 1..={MAX_ROUTER_CHANNELS}, got {}",
self.channel_count
),
));
}
if self.routing_timesteps == 0 || self.routing_timesteps > MAX_ROUTING_TIMESTEPS {
return Err(MeshError::invalid_router_config(
"routing_timesteps",
format!(
"must be in 1..={MAX_ROUTING_TIMESTEPS}, got {}",
self.routing_timesteps
),
));
}
require_finite("self_weight", self.self_weight)?;
require_finite("cross_weight", self.cross_weight)?;
require_finite("threshold", self.threshold)?;
require_unit_interval("leak", self.leak)?;
require_unit_interval("min_fire_rate", self.min_fire_rate)?;
require_unit_interval("plasticity_decay", self.plasticity_decay)?;
require_non_negative("plasticity_potentiate", self.plasticity_potentiate)?;
require_unit_interval("plasticity_speed", self.plasticity_speed)?;
require_unit_interval("fatigue_accumulation", self.fatigue_accumulation)?;
require_unit_interval("fatigue_recovery", self.fatigue_recovery)?;
Ok(())
}
}
fn require_finite(field: &'static str, value: f32) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
Err(MeshError::invalid_router_config(
field,
format!("must be finite, got {value}"),
))
}
}
fn require_unit_interval(field: &'static str, value: f32) -> Result<()> {
require_finite(field, value)?;
if (0.0..=1.0).contains(&value) {
Ok(())
} else {
Err(MeshError::invalid_router_config(
field,
format!("must be finite and in 0.0..=1.0, got {value}"),
))
}
}
fn require_non_negative(field: &'static str, value: f32) -> Result<()> {
require_finite(field, value)?;
if value >= 0.0 {
Ok(())
} else {
Err(MeshError::invalid_router_config(
field,
format!("must be finite and >= 0, got {value}"),
))
}
}
const NEUROMOD_MIN: f32 = 0.0;
const NEUROMOD_MAX: f32 = 1.0;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct NeuromodState {
pub cortisol: f32,
pub dopamine: f32,
pub serotonin: f32,
}
impl NeuromodState {
pub fn balanced() -> Self {
Self {
cortisol: 0.0,
dopamine: 0.0,
serotonin: 0.0,
}
}
pub fn stressed() -> Self {
Self {
cortisol: 0.8,
dopamine: 0.0,
serotonin: 0.0,
}
}
pub fn rewarded() -> Self {
Self {
cortisol: 0.0,
dopamine: 0.8,
serotonin: 0.0,
}
}
pub fn validate(&self) -> Result<()> {
for (field, value) in [
("cortisol", self.cortisol),
("dopamine", self.dopamine),
("serotonin", self.serotonin),
] {
if !value.is_finite() {
return Err(MeshError::NonFiniteNeuromodulator { field });
}
if !(NEUROMOD_MIN..=NEUROMOD_MAX).contains(&value) {
return Err(MeshError::OutOfRangeNeuromodulator { field, value });
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct RoutingDecision {
pub active_channels: Vec<usize>,
pub firing_rates: Vec<f32>,
pub input_signals: Vec<f32>,
}
impl RoutingDecision {
pub fn is_active(&self, channel: usize) -> bool {
self.active_channels.contains(&channel)
}
pub fn is_empty(&self) -> bool {
self.active_channels.is_empty()
}
}
fn validate_route_ingress(
signals: &[f32],
mods: &NeuromodState,
expected_len: usize,
error_context: &str,
) -> Result<()> {
if signals.len() != expected_len {
return Err(MeshError::NeuronCountMismatch {
expected: expected_len,
got: signals.len(),
context: error_context.into(),
});
}
for (index, &value) in signals.iter().enumerate() {
if !value.is_finite() {
return Err(MeshError::NonFiniteSignal {
index,
context: error_context.into(),
});
}
}
mods.validate()
}
#[derive(Clone, Debug, Serialize)]
pub struct ChannelRouter {
neurons: Vec<NeuromodNeuron>,
config: RouterConfig,
pub total_routes: u64,
pub channel_fatigue: Vec<f32>,
baseline_weights: Vec<Vec<f32>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
enum LegacyField<T> {
#[default]
Omitted,
Null,
Value(T),
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for LegacyField<T> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LegacyFieldVisitor<T>(std::marker::PhantomData<T>);
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for LegacyFieldVisitor<T> {
type Value = LegacyField<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a value or null")
}
fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(LegacyField::Null)
}
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(LegacyField::Null)
}
fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(LegacyField::Value)
}
}
deserializer.deserialize_option(LegacyFieldVisitor(std::marker::PhantomData))
}
}
#[derive(Deserialize)]
struct RawChannelRouter {
neurons: Vec<NeuromodNeuron>,
#[serde(default)]
config: LegacyField<RouterConfig>,
#[serde(default)]
total_routes: u64,
#[serde(default)]
channel_fatigue: LegacyField<Vec<f32>>,
#[serde(default)]
baseline_weights: LegacyField<Vec<Vec<f32>>>,
}
impl RawChannelRouter {
fn into_router(self) -> Result<ChannelRouter> {
let n_neurons = self.neurons.len();
let config = match self.config {
LegacyField::Omitted => RouterConfig {
channel_count: n_neurons,
..RouterConfig::default()
},
LegacyField::Null => {
return Err(MeshError::invalid_router_config(
"config",
"null value is not allowed; omit field for legacy format",
));
}
LegacyField::Value(config) => config,
};
config.validate()?;
let n = config.channel_count;
validate_neuron_bank(n, &self.neurons)?;
let channel_fatigue = match self.channel_fatigue {
LegacyField::Omitted => vec![0.0; n],
LegacyField::Null => {
return Err(MeshError::invalid_router_config(
"channel_fatigue",
"null value is not allowed; omit field for legacy format",
));
}
LegacyField::Value(fatigue) => fatigue,
};
let baseline_weights = match self.baseline_weights {
LegacyField::Omitted => self.neurons.iter().map(|neu| neu.weights.clone()).collect(),
LegacyField::Null => {
return Err(MeshError::invalid_router_config(
"baseline_weights",
"null value is not allowed; omit field for legacy format",
));
}
LegacyField::Value(weights) => weights,
};
validate_fatigue_and_baseline(n, &channel_fatigue, &baseline_weights)?;
Ok(ChannelRouter {
neurons: self.neurons,
config,
total_routes: self.total_routes,
channel_fatigue,
baseline_weights,
})
}
}
fn validate_neuron_bank(n: usize, neurons: &[NeuromodNeuron]) -> Result<()> {
if neurons.len() != n {
return Err(MeshError::invalid_router_config(
"neurons",
format!("length {} does not match channel_count {n}", neurons.len()),
));
}
for (i, neu) in neurons.iter().enumerate() {
require_finite_named("v", neu.v, i)?;
require_finite_named("v_rest", neu.v_rest, i)?;
require_finite_named("v_reset", neu.v_reset, i)?;
require_finite_named("leak", neu.leak, i)?;
require_finite_named("threshold", neu.threshold, i)?;
require_finite_named("gain", neu.gain, i)?;
if neu.weights.len() != n {
return Err(MeshError::invalid_router_config(
"weights",
format!(
"neuron {i} weights length {} does not match channel_count {n}",
neu.weights.len()
),
));
}
if neu.weights.iter().any(|w| !w.is_finite()) {
return Err(MeshError::invalid_router_config(
"weights",
format!("neuron {i} weights contain a non-finite value"),
));
}
}
Ok(())
}
fn require_finite_named(field: &'static str, value: f32, neuron: usize) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
Err(MeshError::invalid_router_config(
field,
format!("neuron {neuron} {field} must be finite, got {value}"),
))
}
}
fn validate_fatigue_and_baseline(
n: usize,
channel_fatigue: &[f32],
baseline_weights: &[Vec<f32>],
) -> Result<()> {
if channel_fatigue.len() != n {
return Err(MeshError::invalid_router_config(
"channel_fatigue",
format!(
"length {} does not match channel_count {n}",
channel_fatigue.len()
),
));
}
for (i, &fatigue) in channel_fatigue.iter().enumerate() {
if !fatigue.is_finite() || !(0.0..=1.0).contains(&fatigue) {
return Err(MeshError::invalid_router_config(
"channel_fatigue",
format!("index {i} must be finite and in 0.0..=1.0, got {fatigue}"),
));
}
}
if baseline_weights.len() != n {
return Err(MeshError::invalid_router_config(
"baseline_weights",
format!(
"length {} does not match channel_count {n}",
baseline_weights.len()
),
));
}
for (i, row) in baseline_weights.iter().enumerate() {
if row.len() != n {
return Err(MeshError::invalid_router_config(
"baseline_weights",
format!(
"row {i} length {} does not match channel_count {n}",
row.len()
),
));
}
if row.iter().any(|w| !w.is_finite()) {
return Err(MeshError::invalid_router_config(
"baseline_weights",
format!("row {i} contains a non-finite value"),
));
}
}
Ok(())
}
impl<'de> Deserialize<'de> for ChannelRouter {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
RawChannelRouter::deserialize(deserializer)?
.into_router()
.map_err(DeError::custom)
}
}
impl Default for ChannelRouter {
fn default() -> Self {
Self::new()
}
}
impl ChannelRouter {
pub fn new() -> Self {
Self::try_with_config(RouterConfig::default()).expect("default RouterConfig is valid")
}
pub fn with_config(config: RouterConfig) -> Self {
Self::try_with_config(config).unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_with_config(config: RouterConfig) -> Result<Self> {
config.validate()?;
let n = config.channel_count;
let neurons: Vec<NeuromodNeuron> = (0..n)
.map(|i| {
let mut neu = NeuromodNeuron::new();
neu.weights = vec![config.cross_weight; n];
neu.weights[i] = config.self_weight;
neu.threshold = config.threshold;
neu.leak = config.leak;
neu
})
.collect();
let baseline_weights = neurons.iter().map(|neu| neu.weights.clone()).collect();
Ok(Self {
neurons,
config,
total_routes: 0,
channel_fatigue: vec![0.0; n],
baseline_weights,
})
}
pub fn route<S: AsRef<[f32]>>(&mut self, signals: S) -> Result<RoutingDecision> {
self.route_modulated_with_context(signals, &NeuromodState::balanced(), "route signals")
}
pub fn route_modulated<S: AsRef<[f32]>>(
&mut self,
signals: S,
mods: &NeuromodState,
) -> Result<RoutingDecision> {
self.route_modulated_with_context(signals, mods, "route_modulated signals")
}
fn route_modulated_with_context<S: AsRef<[f32]>>(
&mut self,
signals: S,
mods: &NeuromodState,
error_context: &str,
) -> Result<RoutingDecision> {
let signals = signals.as_ref();
let n = self.config.channel_count;
validate_route_ingress(signals, mods, n, error_context)?;
self.ensure_neuromod_state_synced();
let (effective_thresholds, effective_leaks) = self.compute_effective_params(mods);
for neu in &mut self.neurons {
neu.v = 0.0;
}
let timesteps = self.config.routing_timesteps;
let min_rate = self.config.min_fire_rate;
let spike_counts =
self.integrate_signals(signals, &effective_thresholds, &effective_leaks, timesteps);
let mut firing_rates = vec![0.0f32; n];
let mut active_channels = Vec::new();
for i in 0..n {
firing_rates[i] = spike_counts[i] as f32 / timesteps as f32;
if firing_rates[i] >= min_rate {
active_channels.push(i);
}
}
self.apply_plasticity(&active_channels, mods);
self.total_routes += 1;
Ok(RoutingDecision {
active_channels,
firing_rates,
input_signals: signals.to_vec(),
})
}
fn integrate_signals(
&mut self,
signals: &[f32],
effective_thresholds: &[f32],
effective_leaks: &[f32],
timesteps: usize,
) -> Vec<u32> {
let n = self.config.channel_count;
let mut spike_counts = vec![0u32; n];
for _ in 0..timesteps {
for (i, neu) in self.neurons.iter_mut().enumerate().take(n) {
debug_assert_eq!(
neu.weights.len(),
signals.len(),
"Neuron weights length mismatch"
);
let stimulus: f32 = signals
.iter()
.zip(neu.weights.iter())
.map(|(sig, w)| sig * w)
.sum();
neu.leak = effective_leaks[i];
neu.integrate(stimulus);
neu.threshold = effective_thresholds[i];
if neu.check_fire().is_some() {
spike_counts[i] += 1;
}
}
}
spike_counts
}
fn ensure_neuromod_state_synced(&mut self) {
let n = self.config.channel_count;
if self.channel_fatigue.len() != n {
self.channel_fatigue.resize(n, 0.0);
}
let mut weights_repaired = false;
for neu in &mut self.neurons {
if neu.weights.len() != n {
weights_repaired = true;
neu.weights.resize(n, 0.0);
}
}
let baseline_ok = !weights_repaired
&& self.baseline_weights.len() == n
&& self.baseline_weights.iter().all(|row| row.len() == n);
if !baseline_ok {
self.baseline_weights = self.neurons.iter().map(|neu| neu.weights.clone()).collect();
}
}
fn compute_effective_params(&self, mods: &NeuromodState) -> (Vec<f32>, Vec<f32>) {
let n = self.config.channel_count;
let mut thresholds = vec![0.0f32; n];
let mut leaks = vec![0.0f32; n];
for i in 0..n {
let baseline_stress = 1.0 + mods.cortisol * 0.5;
let fatigue_amplification = 1.0 + mods.cortisol * self.channel_fatigue[i];
let fatigue_factor = baseline_stress * fatigue_amplification;
let dopamine_factor = 1.0 - mods.dopamine * 0.5;
thresholds[i] =
(self.config.threshold * fatigue_factor * dopamine_factor).clamp(0.05, 2.0);
leaks[i] = (self.config.leak * (1.0 + mods.serotonin)).clamp(0.0, 1.0);
}
(thresholds, leaks)
}
fn apply_plasticity(&mut self, active_channels: &[usize], mods: &NeuromodState) {
let n = self.config.channel_count;
debug_assert!(
self.neurons.len() >= n
&& self.neurons.iter().all(|neu| neu.weights.len() >= n)
&& self.baseline_weights.len() >= n
&& self.baseline_weights.iter().all(|row| row.len() >= n),
"apply_plasticity invariant violation — ensure_neuromod_state_synced should have rebuilt"
);
if n > self.neurons.len()
|| self.neurons.iter().any(|neu| neu.weights.len() < n)
|| self.baseline_weights.len() < n
|| self.baseline_weights.iter().any(|row| row.len() < n)
{
return;
}
let decay = self.config.plasticity_decay;
let potentiate = self.config.plasticity_potentiate;
let plasticity_speed = self.config.plasticity_speed;
let fatigue_acc = self.config.fatigue_accumulation;
let fatigue_rec = self.config.fatigue_recovery;
for i in 0..n {
if active_channels.contains(&i) {
let strengthen = potentiate * (1.0 + mods.dopamine);
for j in 0..n {
let baseline = self.baseline_weights[i][j];
let current = self.neurons[i].weights[j];
let next_weight = if plasticity_speed == 0.0 {
current
} else {
let target = baseline * (1.0 + strengthen);
let update = if !target.is_finite() {
let extreme = if (baseline > 0.0 && strengthen >= -1.0)
|| (baseline < 0.0 && strengthen < -1.0)
{
2.0
} else {
-1.5
};
current + (extreme - current) * plasticity_speed
} else {
current + (target - current) * plasticity_speed
};
if update.is_finite() {
update.clamp(-1.5, 2.0)
} else if update.is_sign_positive() {
2.0
} else {
-1.5
}
};
self.neurons[i].weights[j] = next_weight;
}
self.channel_fatigue[i] = (self.channel_fatigue[i] + fatigue_acc).min(1.0);
} else {
for j in 0..n {
let baseline = self.baseline_weights[i][j];
let current = self.neurons[i].weights[j];
let update = if decay == 0.0 || current == baseline {
current
} else {
let diff = baseline - current;
if !diff.is_finite() {
if baseline > current { 2.0 } else { -1.5 }
} else {
current + diff * decay
}
};
self.neurons[i].weights[j] = if update.is_finite() {
update.clamp(-1.5, 2.0)
} else if update.is_sign_positive() {
2.0
} else {
-1.5
};
}
self.channel_fatigue[i] = (self.channel_fatigue[i] - fatigue_rec).max(0.0);
}
}
}
pub fn apply_feedback(&mut self, channel_idx: usize, reward: f32) {
self.ensure_neuromod_state_synced();
let n = self.config.channel_count;
debug_assert!(
channel_idx < n
&& self.neurons.len() >= n
&& self.neurons.iter().all(|neu| neu.weights.len() >= n),
"apply_feedback invariant violation — ensure_neuromod_state_synced should have rebuilt"
);
if channel_idx >= n
|| n > self.neurons.len()
|| self.neurons.iter().any(|neu| neu.weights.len() < n)
{
return;
}
let delta = reward * 0.01;
self.neurons[channel_idx].weights[channel_idx] =
(self.neurons[channel_idx].weights[channel_idx] + delta).clamp(0.1, 2.0);
if reward > 0.0 {
for j in 0..n {
if j != channel_idx {
self.neurons[j].weights[channel_idx] =
(self.neurons[j].weights[channel_idx] - delta * 0.3).clamp(-1.0, 1.5);
}
}
}
self.sync_baseline_after_feedback(channel_idx, reward);
}
fn sync_baseline_after_feedback(&mut self, channel_idx: usize, reward: f32) {
let n = self.config.channel_count;
debug_assert!(
self.baseline_weights.len() == n
&& self.baseline_weights.iter().all(|row| row.len() == n),
"baseline_weights shape mismatch — ensure_neuromod_state_synced should have rebuilt"
);
if self.baseline_weights.len() != n
|| self.baseline_weights.iter().any(|row| row.len() != n)
{
return;
}
self.baseline_weights[channel_idx][channel_idx] =
self.neurons[channel_idx].weights[channel_idx];
if reward > 0.0 {
for j in 0..n {
if j != channel_idx {
self.baseline_weights[j][channel_idx] = self.neurons[j].weights[channel_idx];
}
}
}
}
pub fn set_global_gain(&mut self, gain: f32) {
for neu in &mut self.neurons {
neu.set_gain(gain);
}
}
pub fn weight_matrix(&self) -> Vec<Vec<f32>> {
self.neurons.iter().map(|neu| neu.weights.clone()).collect()
}
pub fn config(&self) -> &RouterConfig {
&self.config
}
pub fn fatigue(&self) -> &[f32] {
&self.channel_fatigue
}
}
#[cfg(test)]
mod validate_tests {
use super::*;
use serde_json::json;
fn router_field(err: &MeshError) -> &'static str {
match err {
MeshError::InvalidRouterConfig { field, .. } => field,
other => panic!("expected InvalidRouterConfig, got {other:?}"),
}
}
fn serde_field_from_config_json(value: serde_json::Value) -> String {
let err = serde_json::from_value::<RouterConfig>(value).expect_err("config should fail");
err.to_string()
}
fn serde_field_from_router_json(value: serde_json::Value) -> String {
let err = serde_json::from_value::<ChannelRouter>(value).expect_err("router should fail");
err.to_string()
}
enum BadValue {
Count(usize),
Float(f32),
}
struct InvalidCase {
name: &'static str,
field: &'static str,
value: BadValue,
}
fn apply_bad(config: &mut RouterConfig, field: &str, value: &BadValue) {
match (field, value) {
("channel_count", BadValue::Count(v)) => config.channel_count = *v,
("routing_timesteps", BadValue::Count(v)) => config.routing_timesteps = *v,
("self_weight", BadValue::Float(v)) => config.self_weight = *v,
("cross_weight", BadValue::Float(v)) => config.cross_weight = *v,
("threshold", BadValue::Float(v)) => config.threshold = *v,
("leak", BadValue::Float(v)) => config.leak = *v,
("min_fire_rate", BadValue::Float(v)) => config.min_fire_rate = *v,
("plasticity_decay", BadValue::Float(v)) => config.plasticity_decay = *v,
("plasticity_potentiate", BadValue::Float(v)) => config.plasticity_potentiate = *v,
("plasticity_speed", BadValue::Float(v)) => config.plasticity_speed = *v,
("fatigue_accumulation", BadValue::Float(v)) => config.fatigue_accumulation = *v,
("fatigue_recovery", BadValue::Float(v)) => config.fatigue_recovery = *v,
_ => panic!("unhandled invalid-case field {field}"),
}
}
fn json_representable(value: &BadValue) -> bool {
match *value {
BadValue::Count(_) => true,
BadValue::Float(v) => v.is_finite(),
}
}
fn invalid_cases() -> Vec<InvalidCase> {
let counts = [
("channel_count_zero", "channel_count", 0usize),
(
"channel_count_over_max",
"channel_count",
MAX_ROUTER_CHANNELS + 1,
),
("channel_count_usize_max", "channel_count", usize::MAX),
("routing_timesteps_zero", "routing_timesteps", 0),
(
"routing_timesteps_over_max",
"routing_timesteps",
MAX_ROUTING_TIMESTEPS + 1,
),
];
let floats = [
("self_weight_nan", "self_weight", f32::NAN),
("self_weight_inf", "self_weight", f32::INFINITY),
("cross_weight_neg_inf", "cross_weight", f32::NEG_INFINITY),
("threshold_nan", "threshold", f32::NAN),
("leak_below", "leak", -0.01),
("leak_above", "leak", 1.01),
("leak_nan", "leak", f32::NAN),
("leak_inf", "leak", f32::INFINITY),
("min_fire_rate_below", "min_fire_rate", -0.01),
("min_fire_rate_above", "min_fire_rate", 1.01),
("min_fire_rate_nan", "min_fire_rate", f32::NAN),
("plasticity_decay_below", "plasticity_decay", -0.01),
("plasticity_decay_above", "plasticity_decay", 1.01),
("plasticity_decay_nan", "plasticity_decay", f32::NAN),
(
"plasticity_potentiate_negative",
"plasticity_potentiate",
-0.01,
),
(
"plasticity_potentiate_nan",
"plasticity_potentiate",
f32::NAN,
),
(
"plasticity_potentiate_inf",
"plasticity_potentiate",
f32::INFINITY,
),
("plasticity_speed_below", "plasticity_speed", -0.01),
("plasticity_speed_above", "plasticity_speed", 1.01),
("plasticity_speed_nan", "plasticity_speed", f32::NAN),
("fatigue_accumulation_below", "fatigue_accumulation", -0.01),
("fatigue_accumulation_above", "fatigue_accumulation", 1.01),
("fatigue_accumulation_nan", "fatigue_accumulation", f32::NAN),
("fatigue_recovery_below", "fatigue_recovery", -0.01),
("fatigue_recovery_above", "fatigue_recovery", 1.01),
("fatigue_recovery_inf", "fatigue_recovery", f32::INFINITY),
];
counts
.into_iter()
.map(|(name, field, value)| InvalidCase {
name,
field,
value: BadValue::Count(value),
})
.chain(floats.into_iter().map(|(name, field, value)| InvalidCase {
name,
field,
value: BadValue::Float(value),
}))
.collect()
}
#[test]
fn default_config_is_valid() {
RouterConfig::default().validate().unwrap();
}
#[test]
fn table_rejects_every_invalid_config_field() {
for case in invalid_cases() {
let mut config = RouterConfig::default();
apply_bad(&mut config, case.field, &case.value);
let err = match config.validate() {
Err(err) => err,
Ok(()) => panic!("{}: validate should fail", case.name),
};
assert_eq!(
router_field(&err),
case.field,
"{}: unexpected field in {err}",
case.name
);
let ctor_err = ChannelRouter::try_with_config(config.clone()).expect_err(case.name);
assert_eq!(
router_field(&ctor_err),
case.field,
"{}: constructor field mismatch",
case.name
);
assert_eq!(
ctor_err, err,
"{}: validate and try_with_config must return the same error",
case.name
);
if !json_representable(&case.value) {
continue;
}
let mut mutated = RouterConfig::default();
apply_bad(&mut mutated, case.field, &case.value);
let config_msg = serde_field_from_config_json(serde_json::to_value(&mutated).unwrap());
assert!(
config_msg.contains(case.field),
"{}: RouterConfig serde missing field name: {config_msg}",
case.name
);
assert!(
config_msg.contains(&err.to_string()),
"{}: RouterConfig serde should carry the same MeshError Display, got {config_msg}, expected {}",
case.name,
err
);
let mut router_json = serde_json::to_value(ChannelRouter::default()).unwrap();
router_json["config"] = serde_json::to_value(&mutated).unwrap();
let router_msg = serde_field_from_router_json(router_json);
assert!(
router_msg.contains(case.field),
"{}: ChannelRouter serde missing field name: {router_msg}",
case.name
);
assert!(
router_msg.contains(&err.to_string()),
"{}: ChannelRouter serde should carry the same MeshError Display, got {router_msg}, expected {}",
case.name,
err
);
}
}
type BoundaryMutator = fn(&mut RouterConfig);
fn valid_boundary_mutators() -> Vec<(&'static str, BoundaryMutator)> {
vec![
("min_channels", |c| c.channel_count = 1),
("max_channels_validate_only", |c| {
c.channel_count = MAX_ROUTER_CHANNELS
}),
("leak_0", |c| c.leak = 0.0),
("leak_1", |c| c.leak = 1.0),
("min_fire_0", |c| c.min_fire_rate = 0.0),
("min_fire_1", |c| c.min_fire_rate = 1.0),
("decay_0", |c| c.plasticity_decay = 0.0),
("decay_1", |c| c.plasticity_decay = 1.0),
("potentiate_0", |c| c.plasticity_potentiate = 0.0),
("speed_0", |c| c.plasticity_speed = 0.0),
("speed_1", |c| c.plasticity_speed = 1.0),
("fatigue_acc_0", |c| c.fatigue_accumulation = 0.0),
("fatigue_acc_1", |c| c.fatigue_accumulation = 1.0),
("fatigue_rec_0", |c| c.fatigue_recovery = 0.0),
("fatigue_rec_1", |c| c.fatigue_recovery = 1.0),
("signed_cross_inhibition", |c| c.cross_weight = -1.0),
("positive_cross_weight", |c| c.cross_weight = 0.25),
("signed_self_weight", |c| c.self_weight = -0.3),
("zero_threshold", |c| c.threshold = 0.0),
("one_timestep", |c| c.routing_timesteps = 1),
("max_timesteps", |c| {
c.routing_timesteps = MAX_ROUTING_TIMESTEPS
}),
]
}
#[test]
fn valid_boundaries_round_trip() {
for (name, mutate) in valid_boundary_mutators() {
let mut config = RouterConfig::default();
mutate(&mut config);
config
.validate()
.unwrap_or_else(|err| panic!("{name}: valid boundary rejected: {err}"));
let json = serde_json::to_value(&config).unwrap();
let restored: RouterConfig = serde_json::from_value(json)
.unwrap_or_else(|err| panic!("{name}: valid config failed to deserialize: {err}"));
assert_eq!(restored, config, "{name}: config round-trip");
if config.channel_count == MAX_ROUTER_CHANNELS {
continue;
}
let router = ChannelRouter::try_with_config(config.clone())
.unwrap_or_else(|err| panic!("{name}: try_with_config: {err}"));
let router_json = serde_json::to_value(&router).unwrap();
let restored_router: ChannelRouter = serde_json::from_value(router_json)
.unwrap_or_else(|err| panic!("{name}: router deserialize: {err}"));
assert_eq!(restored_router.config(), router.config(), "{name}");
assert_eq!(
restored_router.weight_matrix(),
router.weight_matrix(),
"{name}"
);
assert_eq!(restored_router.fatigue(), router.fatigue(), "{name}");
assert_eq!(restored_router.total_routes, router.total_routes, "{name}");
}
}
#[test]
fn custom_and_default_routers_round_trip() {
let default_router = ChannelRouter::default();
let json = serde_json::to_value(&default_router).unwrap();
let restored: ChannelRouter = serde_json::from_value(json).unwrap();
assert_eq!(restored.config(), default_router.config());
assert_eq!(restored.weight_matrix(), default_router.weight_matrix());
let custom = RouterConfig {
channel_count: 4,
self_weight: 1.1,
cross_weight: -0.2,
threshold: 0.3,
leak: 0.05,
routing_timesteps: 8,
min_fire_rate: 0.25,
plasticity_decay: 0.03,
plasticity_potentiate: 0.2,
plasticity_speed: 0.4,
fatigue_accumulation: 0.2,
fatigue_recovery: 0.1,
};
let router = ChannelRouter::try_with_config(custom.clone()).unwrap();
let json = serde_json::to_value(&router).unwrap();
let restored: ChannelRouter = serde_json::from_value(json).unwrap();
assert_eq!(restored.config(), &custom);
assert_eq!(restored.weight_matrix(), router.weight_matrix());
}
#[test]
fn try_with_config_rejects_oversized_channel_count_without_allocating() {
let config = RouterConfig {
channel_count: usize::MAX,
..RouterConfig::default()
};
let err = ChannelRouter::try_with_config(config).unwrap_err();
assert_eq!(router_field(&err), "channel_count");
}
#[test]
fn max_channel_count_is_accepted_by_validate_and_constructor() {
let config = RouterConfig {
channel_count: MAX_ROUTER_CHANNELS,
..RouterConfig::default()
};
config.validate().unwrap();
let router = ChannelRouter::try_with_config(config).unwrap();
assert_eq!(router.config().channel_count, MAX_ROUTER_CHANNELS);
assert_eq!(router.weight_matrix().len(), MAX_ROUTER_CHANNELS);
assert_eq!(router.weight_matrix()[0].len(), MAX_ROUTER_CHANNELS);
assert_eq!(router.fatigue().len(), MAX_ROUTER_CHANNELS);
}
#[test]
fn malformed_shapes_are_rejected_with_matching_fields() {
let router = ChannelRouter::default();
let base = serde_json::to_value(&router).unwrap();
let mut empty_inner = base.clone();
empty_inner["baseline_weights"] = json!([[], [], []]);
let msg = serde_field_from_router_json(empty_inner);
assert!(msg.contains("baseline_weights"), "empty inner rows: {msg}");
let mut short_fatigue = base.clone();
short_fatigue["channel_fatigue"] = json!([0.0, 0.0]);
let msg = serde_field_from_router_json(short_fatigue);
assert!(msg.contains("channel_fatigue"), "short fatigue: {msg}");
let mut extra_neurons = base.clone();
extra_neurons["neurons"] = json!([]);
let msg = serde_field_from_router_json(extra_neurons);
assert!(msg.contains("neurons"), "empty neurons: {msg}");
let mut missing_weights = base.clone();
missing_weights["neurons"][0]
.as_object_mut()
.unwrap()
.remove("weights");
let msg = serde_field_from_router_json(missing_weights);
assert!(msg.contains("weights"), "missing neuron weights: {msg}");
let mut ragged_baseline = base.clone();
ragged_baseline["baseline_weights"] = json!([[0.9, -0.15, -0.15], [0.0], [0.0, 0.0, 0.0]]);
let msg = serde_field_from_router_json(ragged_baseline);
assert!(msg.contains("baseline_weights"), "ragged baseline: {msg}");
let mut out_of_range_fatigue = base;
out_of_range_fatigue["channel_fatigue"] = json!([0.0, 1.5, 0.0]);
let msg = serde_field_from_router_json(out_of_range_fatigue);
assert!(
msg.contains("channel_fatigue"),
"fatigue out of range: {msg}"
);
}
#[test]
fn legacy_snapshot_without_optional_fields_deserializes() {
let router = ChannelRouter::default();
let mut json = serde_json::to_value(&router).unwrap();
let obj = json.as_object_mut().unwrap();
obj.remove("config");
obj.remove("channel_fatigue");
obj.remove("baseline_weights");
let restored: ChannelRouter =
serde_json::from_value(json).expect("legacy snapshot must deserialize");
assert_eq!(restored.config().channel_count, 3);
assert_eq!(restored.weight_matrix().len(), 3);
assert_eq!(restored.fatigue(), &[0.0, 0.0, 0.0]);
let mut restored = restored;
restored
.route_modulated([0.5, 0.0, 0.0], &NeuromodState::balanced())
.unwrap();
}
#[test]
fn signed_zero_rates_are_accepted() {
let config = RouterConfig {
leak: -0.0,
min_fire_rate: -0.0,
plasticity_decay: -0.0,
plasticity_potentiate: -0.0,
plasticity_speed: -0.0,
fatigue_accumulation: -0.0,
fatigue_recovery: -0.0,
..RouterConfig::default()
};
config.validate().unwrap();
ChannelRouter::try_with_config(config).unwrap();
}
#[test]
fn explicit_null_fields_are_rejected() {
for field in ["config", "channel_fatigue", "baseline_weights"] {
let router = ChannelRouter::default();
let mut json = serde_json::to_value(&router).unwrap();
json[field] = serde_json::Value::Null;
let err = serde_json::from_value::<ChannelRouter>(json)
.expect_err("explicit null field must be rejected");
let msg = err.to_string();
assert!(
msg.contains(field),
"error should mention field {field}, got: {msg}"
);
}
}
#[test]
fn extreme_weights_and_potentiate_with_zero_speed_do_not_produce_nan() {
let config = RouterConfig {
self_weight: 1.0,
cross_weight: -0.5,
plasticity_potentiate: f32::MAX,
plasticity_speed: 0.0,
..RouterConfig::default()
};
config.validate().unwrap();
let mut router = ChannelRouter::try_with_config(config).unwrap();
let initial_weights = router.weight_matrix();
let res = router.route_modulated([1.0, 0.0, 0.0], &NeuromodState::balanced());
assert!(res.is_ok());
for (row_idx, row) in router.weight_matrix().iter().enumerate() {
for (col_idx, &w) in row.iter().enumerate() {
assert!(
w.is_finite(),
"weight at [{row_idx}][{col_idx}] must remain finite, got {w}"
);
assert_eq!(
w, initial_weights[row_idx][col_idx],
"weight with plasticity_speed 0.0 must remain unchanged"
);
}
}
let config_speed = RouterConfig {
self_weight: 1.0,
cross_weight: -0.5,
plasticity_potentiate: f32::MAX,
plasticity_speed: 0.5,
..RouterConfig::default()
};
let mut router_speed = ChannelRouter::try_with_config(config_speed).unwrap();
let res_speed = router_speed.route_modulated([1.0, 0.0, 0.0], &NeuromodState::balanced());
assert!(res_speed.is_ok());
for (row_idx, row) in router_speed.weight_matrix().iter().enumerate() {
for (col_idx, &w) in row.iter().enumerate() {
assert!(
w.is_finite(),
"weight at [{row_idx}][{col_idx}] must remain finite, got {w}"
);
assert!(
(-1.5..=2.0).contains(&w),
"weight {w} at [{row_idx}][{col_idx}] must stay within [-1.5, 2.0]"
);
}
}
}
}