use super::SpatialSynapse;
use ternary_signal::Signal;
use std::collections::HashMap;
#[derive(Clone, Copy, Debug)]
pub struct MasteryConfig {
pub pressure_threshold: i16,
pub participation_threshold: f32,
pub magnitude_cost: u8,
pub flip_penalty: u8,
pub pressure_scale: f32,
pub hub_threshold: u16,
pub hub_decay_rate: f32,
pub flip_cooldown_us: u64,
}
impl Default for MasteryConfig {
fn default() -> Self {
Self {
pressure_threshold: 50,
participation_threshold: 0.25,
magnitude_cost: 5,
flip_penalty: 50,
pressure_scale: 1.0,
hub_threshold: 20, hub_decay_rate: 0.1, flip_cooldown_us: 100_000, }
}
}
#[derive(Clone, Debug, Default)]
pub struct HubTracker {
fan_in: Vec<u16>,
activation: Vec<u32>,
}
impl HubTracker {
pub fn new(neuron_count: usize) -> Self {
Self {
fan_in: vec![0; neuron_count],
activation: vec![0; neuron_count],
}
}
pub fn resize(&mut self, neuron_count: usize) {
self.fan_in.resize(neuron_count, 0);
self.activation.resize(neuron_count, 0);
}
pub fn record_connection(&mut self, target: u32) {
if let Some(count) = self.fan_in.get_mut(target as usize) {
*count = count.saturating_add(1);
}
}
pub fn remove_connection(&mut self, target: u32) {
if let Some(count) = self.fan_in.get_mut(target as usize) {
*count = count.saturating_sub(1);
}
}
pub fn record_activation(&mut self, target: u32, magnitude: u16) {
if let Some(act) = self.activation.get_mut(target as usize) {
*act = act.saturating_add(magnitude as u32);
}
}
pub fn fan_in(&self, neuron: u32) -> u16 {
self.fan_in.get(neuron as usize).copied().unwrap_or(0)
}
pub fn activation(&self, neuron: u32) -> u32 {
self.activation.get(neuron as usize).copied().unwrap_or(0)
}
pub fn is_hub(&self, neuron: u32, threshold: u16) -> bool {
self.fan_in(neuron) > threshold
}
pub fn clear_activation(&mut self) {
for a in &mut self.activation {
*a = 0;
}
}
pub fn hub_synapses_to_weaken(&self, threshold: u16) -> Vec<u32> {
self.fan_in
.iter()
.enumerate()
.filter(|(_, &count)| count > threshold)
.map(|(idx, _)| idx as u32)
.collect()
}
}
#[derive(Clone, Debug, Default)]
pub struct FlipCooldown {
last_flip: HashMap<usize, u64>,
}
impl FlipCooldown {
pub fn new() -> Self {
Self {
last_flip: HashMap::new(),
}
}
pub fn can_flip(&self, synapse_idx: usize, current_time: u64, cooldown_us: u64) -> bool {
match self.last_flip.get(&synapse_idx) {
Some(&last) => current_time >= last + cooldown_us,
None => true, }
}
pub fn record_flip(&mut self, synapse_idx: usize, time: u64) {
self.last_flip.insert(synapse_idx, time);
}
pub fn time_since_flip(&self, synapse_idx: usize, current_time: u64) -> u64 {
match self.last_flip.get(&synapse_idx) {
Some(&last) => current_time.saturating_sub(last),
None => u64::MAX,
}
}
pub fn prune_old(&mut self, older_than: u64) {
self.last_flip.retain(|_, &mut time| time >= older_than);
}
}
#[derive(Clone, Debug)]
pub struct MasteryState {
config: MasteryConfig,
pressure: Vec<i16>,
metabolic_budget: u32,
flip_cooldown: FlipCooldown,
current_time: u64,
}
impl MasteryState {
pub fn new(synapse_count: usize, config: MasteryConfig, initial_budget: u32) -> Self {
Self {
config,
pressure: vec![0; synapse_count],
metabolic_budget: initial_budget,
flip_cooldown: FlipCooldown::new(),
current_time: 0,
}
}
pub fn set_time(&mut self, time_us: u64) {
self.current_time = time_us;
}
pub fn resize(&mut self, synapse_count: usize) {
self.pressure.resize(synapse_count, 0);
}
pub fn add_budget(&mut self, amount: u32) {
self.metabolic_budget = self.metabolic_budget.saturating_add(amount);
}
pub fn budget(&self) -> u32 {
self.metabolic_budget
}
pub fn accumulate_pressure(&mut self, synapse_idx: usize, activity: f32, direction: i8) {
if synapse_idx >= self.pressure.len() {
return;
}
if activity < self.config.participation_threshold {
return;
}
let delta = (direction as f32 * activity * self.config.pressure_scale * 10.0) as i16;
self.pressure[synapse_idx] = self.pressure[synapse_idx].saturating_add(delta);
}
pub fn apply_learning(&mut self, synapse_idx: usize, synapse: &mut SpatialSynapse) -> Option<PolarityChange> {
if synapse_idx >= self.pressure.len() {
return None;
}
let pressure = self.pressure[synapse_idx];
let abs_pressure = pressure.abs();
if abs_pressure < self.config.pressure_threshold {
return None;
}
let desired_positive = pressure > 0;
let current_positive = synapse.signal.polarity > 0;
let current_negative = synapse.signal.polarity < 0;
let change = if synapse.signal.polarity == 0 {
let cost = self.config.magnitude_cost;
if !self.try_spend(cost as u32) {
return None;
}
synapse.signal.polarity = if desired_positive { 1 } else { -1 };
synapse.signal.magnitude = synapse.signal.magnitude.saturating_add(10);
PolarityChange::Awakened
} else if (desired_positive && current_positive) || (!desired_positive && current_negative) {
let cost = self.config.magnitude_cost;
if !self.try_spend(cost as u32) {
return None;
}
synapse.signal.magnitude = synapse.signal.magnitude.saturating_add(5);
PolarityChange::Strengthened
} else {
let cost = self.config.magnitude_cost;
if !self.try_spend(cost as u32) {
return None;
}
if synapse.signal.magnitude > 5 {
synapse.signal.magnitude = synapse.signal.magnitude.saturating_sub(5);
PolarityChange::Weakened
} else {
if !self.flip_cooldown.can_flip(synapse_idx, self.current_time, self.config.flip_cooldown_us) {
synapse.signal.magnitude = 0;
synapse.signal.polarity = 0;
return Some(PolarityChange::GoneDormant);
}
let flip_cost = self.config.flip_penalty;
if !self.try_spend(flip_cost as u32) {
synapse.signal.magnitude = 0;
synapse.signal.polarity = 0;
return Some(PolarityChange::GoneDormant);
}
synapse.signal.polarity = if desired_positive { 1 } else { -1 };
synapse.signal.magnitude = 10;
self.flip_cooldown.record_flip(synapse_idx, self.current_time);
PolarityChange::Flipped
}
};
self.pressure[synapse_idx] = 0;
synapse.mature(1);
Some(change)
}
fn try_spend(&mut self, cost: u32) -> bool {
if self.metabolic_budget >= cost {
self.metabolic_budget -= cost;
true
} else {
false
}
}
pub fn clear_pressure(&mut self) {
for p in &mut self.pressure {
*p = 0;
}
}
pub fn get_pressure(&self, synapse_idx: usize) -> i16 {
self.pressure.get(synapse_idx).copied().unwrap_or(0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PolarityChange {
Strengthened,
Weakened,
GoneDormant,
Awakened,
Flipped,
}
pub fn modification_cost(current: &Signal, proposed: &Signal) -> u8 {
if current.polarity == proposed.polarity {
let mag_diff = (current.magnitude as i16 - proposed.magnitude as i16).abs();
(mag_diff / 10) as u8 } else if proposed.polarity == 0 {
current.magnitude / 2 } else if current.polarity == 0 {
proposed.magnitude / 2
} else {
current
.magnitude
.saturating_add(50)
.saturating_add(proposed.magnitude)
}
}
#[inline]
pub fn learning_direction(target: i16, output: i16) -> i8 {
let diff = target as i32 - output as i32;
if diff > 0 {
1
} else if diff < 0 {
-1
} else {
0
}
}
#[inline]
pub fn is_participant(activity: f32, threshold: f32) -> bool {
activity >= threshold
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_learning_direction() {
assert_eq!(learning_direction(100, 50), 1);
assert_eq!(learning_direction(50, 100), -1);
assert_eq!(learning_direction(50, 50), 0);
}
#[test]
fn test_modification_cost() {
let same_pol = Signal::positive(100);
let stronger = Signal::positive(150);
assert!(modification_cost(&same_pol, &stronger) < 10);
let pos = Signal::positive(100);
let neg = Signal::negative(100);
assert!(modification_cost(&pos, &neg) > 100);
}
#[test]
fn test_weaken_before_flip() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 1000);
let mut syn = SpatialSynapse::excitatory(0, 1, 100, 1000);
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, -1); }
let change = state.apply_learning(0, &mut syn);
assert!(matches!(change, Some(PolarityChange::Weakened)));
assert!(syn.is_excitatory());
syn.signal.magnitude = 3; for _ in 0..10 {
state.accumulate_pressure(0, 1.0, -1);
}
let change = state.apply_learning(0, &mut syn);
assert!(matches!(change, Some(PolarityChange::Flipped)));
assert!(syn.is_inhibitory()); }
#[test]
fn test_metabolic_budget() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 10);
let mut syn = SpatialSynapse::excitatory(0, 1, 100, 1000);
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, 1);
}
let change = state.apply_learning(0, &mut syn);
assert!(change.is_some());
state.metabolic_budget = 0;
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, 1);
}
let change = state.apply_learning(0, &mut syn);
assert!(change.is_none());
}
#[test]
fn test_participation_threshold() {
let config = MasteryConfig {
participation_threshold: 0.5,
..Default::default()
};
let mut state = MasteryState::new(2, config, 1000);
state.accumulate_pressure(0, 0.2, 1);
assert_eq!(state.get_pressure(0), 0);
state.accumulate_pressure(1, 0.8, 1);
assert!(state.get_pressure(1) > 0);
}
#[test]
fn test_pressure_threshold() {
let config = MasteryConfig {
pressure_threshold: 100,
..Default::default()
};
let mut state = MasteryState::new(1, config, 1000);
let mut syn = SpatialSynapse::excitatory(0, 1, 100, 1000);
state.accumulate_pressure(0, 1.0, 1);
state.accumulate_pressure(0, 1.0, 1);
let change = state.apply_learning(0, &mut syn);
assert!(change.is_none());
for _ in 0..20 {
state.accumulate_pressure(0, 1.0, 1);
}
let change = state.apply_learning(0, &mut syn);
assert!(change.is_some());
}
#[test]
fn test_dormant_synapse_awakening() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 1000);
let mut syn = SpatialSynapse::dormant(0, 1, 1000);
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, 1);
}
let change = state.apply_learning(0, &mut syn);
assert!(matches!(change, Some(PolarityChange::Awakened)));
assert!(syn.is_excitatory());
let mut syn2 = SpatialSynapse::dormant(0, 1, 1000);
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, -1);
}
let change = state.apply_learning(0, &mut syn2);
assert!(matches!(change, Some(PolarityChange::Awakened)));
assert!(syn2.is_inhibitory());
}
#[test]
fn test_strengthening_aligned() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 1000);
let mut syn = SpatialSynapse::excitatory(0, 1, 100, 1000);
let original_mag = syn.signal.magnitude;
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, 1);
}
let change = state.apply_learning(0, &mut syn);
assert!(matches!(change, Some(PolarityChange::Strengthened)));
assert!(syn.signal.magnitude > original_mag);
}
#[test]
fn test_maturity_increases_on_change() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 1000);
let mut syn = SpatialSynapse::excitatory(0, 1, 100, 1000);
let original_maturity = syn.maturity;
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, 1);
}
state.apply_learning(0, &mut syn);
assert!(syn.maturity > original_maturity);
}
#[test]
fn test_clear_pressure() {
let mut state = MasteryState::new(3, MasteryConfig::default(), 1000);
state.accumulate_pressure(0, 1.0, 1);
state.accumulate_pressure(1, 1.0, -1);
state.accumulate_pressure(2, 1.0, 1);
assert!(state.get_pressure(0) > 0);
assert!(state.get_pressure(1) < 0);
state.clear_pressure();
assert_eq!(state.get_pressure(0), 0);
assert_eq!(state.get_pressure(1), 0);
assert_eq!(state.get_pressure(2), 0);
}
#[test]
fn test_resize() {
let mut state = MasteryState::new(2, MasteryConfig::default(), 1000);
state.accumulate_pressure(0, 1.0, 1);
state.resize(5);
assert_eq!(state.get_pressure(4), 0);
state.resize(1);
assert!(state.get_pressure(0) > 0); }
#[test]
fn test_add_budget() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 100);
assert_eq!(state.budget(), 100);
state.add_budget(50);
assert_eq!(state.budget(), 150);
}
#[test]
fn test_modification_cost_dormant() {
let dormant = Signal::zero();
let active = Signal::positive(100);
let cost = modification_cost(&dormant, &active);
assert!(cost > 0);
assert!(cost < 100); }
#[test]
fn test_is_participant_boundary() {
assert!(is_participant(0.25, 0.25));
assert!(!is_participant(0.24, 0.25));
assert!(is_participant(0.5, 0.25));
}
#[test]
fn test_flip_cost_insufficient_budget() {
let mut state = MasteryState::new(1, MasteryConfig::default(), 10);
let mut syn = SpatialSynapse::excitatory(0, 1, 3, 1000);
for _ in 0..10 {
state.accumulate_pressure(0, 1.0, -1);
}
let change = state.apply_learning(0, &mut syn);
assert!(matches!(change, Some(PolarityChange::GoneDormant)));
assert!(syn.is_dormant());
}
}