use petgraph::EdgeType;
use petgraph::graph::NodeIndex;
use petgraph::stable_graph::StableGraph;
#[derive(Debug, Clone, Copy)]
pub struct SokmConfig {
pub decay_factor: f64,
pub delta: f64,
pub min_weight: f64,
pub formula: StrengthFormula,
}
impl Default for SokmConfig {
fn default() -> Self {
Self {
decay_factor: 0.95,
delta: 0.02,
min_weight: 0.001,
formula: StrengthFormula::default(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum StrengthFormula {
#[default]
Product,
Min,
Average,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HebbianReport {
pub decayed: usize,
pub strengthened: usize,
pub pruned: usize,
}
pub fn decay<N, Ty: EdgeType>(graph: &mut StableGraph<N, f64, Ty>, factor: f64) -> usize {
let indices: Vec<_> = graph.edge_indices().collect();
for &idx in &indices {
if let Some(w) = graph.edge_weight_mut(idx) {
*w *= factor;
}
}
indices.len()
}
pub fn strengthen<N, Ty: EdgeType>(
graph: &mut StableGraph<N, f64, Ty>,
activated: &[(NodeIndex, f64)],
config: &SokmConfig,
) -> usize {
let mut count = 0;
for i in 0..activated.len() {
let j_start = if Ty::is_directed() { 0 } else { i + 1 };
for j in j_start..activated.len() {
if i == j {
continue;
}
let (na, sa) = activated[i];
let (nb, sb) = activated[j];
let increment = match config.formula {
StrengthFormula::Product => config.delta * sa * sb,
StrengthFormula::Min => config.delta * sa.min(sb),
StrengthFormula::Average => config.delta * (sa + sb) / 2.0,
};
if let Some(idx) = graph.find_edge(na, nb) {
if let Some(w) = graph.edge_weight_mut(idx) {
*w += increment;
}
} else {
graph.add_edge(na, nb, increment);
}
count += 1;
}
}
count
}
pub fn prune<N, Ty: EdgeType>(graph: &mut StableGraph<N, f64, Ty>, threshold: f64) -> usize {
let to_remove: Vec<_> = graph
.edge_indices()
.filter(|&idx| graph.edge_weight(idx).is_some_and(|&w| w < threshold))
.collect();
let count = to_remove.len();
for idx in to_remove {
graph.remove_edge(idx);
}
count
}
pub fn sokm_tick<N, Ty: EdgeType>(
graph: &mut StableGraph<N, f64, Ty>,
activated: &[(NodeIndex, f64)],
config: &SokmConfig,
) -> HebbianReport {
let decayed = decay(graph, config.decay_factor);
let strengthened = strengthen(graph, activated, config);
let pruned = prune(graph, config.min_weight);
HebbianReport {
decayed,
strengthened,
pruned,
}
}
#[cfg(test)]
mod tests {
use super::*;
use petgraph::stable_graph::{StableDiGraph, StableUnGraph};
fn make_graph() -> StableDiGraph<(), f64> {
let mut g = StableDiGraph::new();
let a = g.add_node(());
let b = g.add_node(());
let c = g.add_node(());
g.add_edge(a, b, 0.5);
g.add_edge(b, c, 0.3);
g.add_edge(a, c, 0.01);
g
}
#[test]
fn decay_multiplies_all_weights() {
let mut g = make_graph();
assert_eq!(decay(&mut g, 0.5), 3);
assert_eq!(*g.edge_weight(0.into()).unwrap(), 0.25);
assert_eq!(*g.edge_weight(1.into()).unwrap(), 0.15);
}
#[test]
fn decay_repeated_converges() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
g.add_edge(a, b, 1.0);
for _ in 0..10 {
decay(&mut g, 0.95);
}
let expected = 0.95f64.powi(10);
assert!((g.edge_weight(0.into()).unwrap() - expected).abs() < 1e-10);
}
#[test]
fn prune_removes_below_threshold() {
let mut g = make_graph();
assert_eq!(prune(&mut g, 0.1), 1);
assert_eq!(g.edge_count(), 2);
}
#[test]
fn prune_retains_above_threshold() {
let mut g = make_graph();
assert_eq!(prune(&mut g, 0.001), 0);
assert_eq!(g.edge_count(), 3);
}
#[test]
fn strengthen_creates_missing_edges() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
let config = SokmConfig::default();
strengthen(&mut g, &[(a, 1.0), (b, 1.0)], &config);
assert_eq!(g.edge_count(), 2); }
#[test]
fn strengthen_higher_scores_get_more() {
let mut g1 = StableDiGraph::<(), f64>::new();
let a1 = g1.add_node(());
let b1 = g1.add_node(());
let mut g2 = StableDiGraph::<(), f64>::new();
let a2 = g2.add_node(());
let b2 = g2.add_node(());
let config = SokmConfig::default();
strengthen(&mut g1, &[(a1, 0.5), (b1, 0.5)], &config);
strengthen(&mut g2, &[(a2, 1.0), (b2, 1.0)], &config);
let w1 = *g1.edge_weight(g1.find_edge(a1, b1).unwrap()).unwrap();
let w2 = *g2.edge_weight(g2.find_edge(a2, b2).unwrap()).unwrap();
assert!(w2 > w1);
}
#[test]
fn strengthen_non_activated_pairs_unchanged() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let _b = g.add_node(());
let c = g.add_node(());
g.add_edge(a, c, 0.5);
let config = SokmConfig::default();
strengthen(&mut g, &[(a, 1.0)], &config);
assert_eq!(*g.edge_weight(0.into()).unwrap(), 0.5);
}
#[test]
fn strengthen_increments_existing_edge() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
g.add_edge(a, b, 0.5);
let config = SokmConfig::default();
strengthen(&mut g, &[(a, 1.0), (b, 1.0)], &config);
assert!((g.edge_weight(0.into()).unwrap() - 0.52).abs() < 1e-10);
}
#[test]
fn sokm_tick_full_cycle() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
g.add_edge(a, b, 0.0005);
let config = SokmConfig::default();
let report = sokm_tick(&mut g, &[(a, 1.0), (b, 1.0)], &config);
assert_eq!(report.decayed, 1);
assert_eq!(report.strengthened, 2);
assert_eq!(report.pruned, 0);
}
#[test]
fn sokm_tick_prunes_weak_edge() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
let c = g.add_node(());
g.add_edge(a, c, 0.0005);
let config = SokmConfig::default();
let report = sokm_tick(&mut g, &[(a, 1.0), (b, 1.0)], &config);
assert_eq!(report.pruned, 1);
}
#[test]
fn strength_formula_min() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
let config = SokmConfig {
formula: StrengthFormula::Min,
..SokmConfig::default()
};
strengthen(&mut g, &[(a, 0.8), (b, 0.3)], &config);
assert!((g.edge_weight(0.into()).unwrap() - 0.006).abs() < 1e-10);
}
#[test]
fn strength_formula_average() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
let config = SokmConfig {
formula: StrengthFormula::Average,
..SokmConfig::default()
};
strengthen(&mut g, &[(a, 0.8), (b, 0.4)], &config);
assert!((g.edge_weight(0.into()).unwrap() - 0.012).abs() < 1e-10);
}
#[test]
fn undirected_strengthen_single_edge_per_pair() {
let mut g = StableUnGraph::<(), f64>::with_capacity(0, 0);
let a = g.add_node(());
let b = g.add_node(());
let config = SokmConfig::default();
let count = strengthen(&mut g, &[(a, 1.0), (b, 1.0)], &config);
assert_eq!(count, 1);
assert_eq!(g.edge_count(), 1);
assert!((g.edge_weight(0.into()).unwrap() - 0.02).abs() < 1e-10);
}
#[test]
fn undirected_sokm_tick() {
let mut g = StableUnGraph::<(), f64>::with_capacity(0, 0);
let a = g.add_node(());
let b = g.add_node(());
let c = g.add_node(());
g.add_edge(a, b, 0.5);
g.add_edge(b, c, 0.0005);
let config = SokmConfig::default();
let report = sokm_tick(&mut g, &[(a, 1.0), (b, 0.9)], &config);
assert_eq!(report.decayed, 2);
assert_eq!(report.strengthened, 1);
assert_eq!(report.pruned, 1);
}
}