use petgraph::EdgeType;
use petgraph::graph::NodeIndex;
use petgraph::stable_graph::StableGraph;
#[derive(Debug, Clone, Copy)]
pub struct BcmConfig {
pub learning_rate: f64,
pub threshold_rate: f64,
}
impl Default for BcmConfig {
fn default() -> Self {
Self {
learning_rate: 0.01,
threshold_rate: 0.001,
}
}
}
#[derive(Debug, Clone)]
pub struct BcmState {
pub thresholds: Vec<f64>,
}
impl BcmState {
pub fn new(n: usize, initial_threshold: f64) -> Self {
Self {
thresholds: vec![initial_threshold; n],
}
}
}
pub fn bcm_update<N, Ty: EdgeType>(
graph: &mut StableGraph<N, f64, Ty>,
activated: &[(NodeIndex, f64)],
state: &mut BcmState,
config: &BcmConfig,
) -> usize {
for &(node, y) in activated {
let idx = node.index();
if idx < state.thresholds.len() {
let theta = &mut state.thresholds[idx];
*theta += config.threshold_rate * (y * y - *theta);
}
}
let mut modified = 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 (pre_node, x) = activated[i];
let (post_node, y) = activated[j];
let post_idx = post_node.index();
if post_idx >= state.thresholds.len() {
continue;
}
let theta = state.thresholds[post_idx];
let delta_w = config.learning_rate * y * (y - theta) * x;
if let Some(edge_idx) = graph.find_edge(pre_node, post_node)
&& let Some(w) = graph.edge_weight_mut(edge_idx)
{
*w += delta_w;
modified += 1;
}
}
}
modified
}
#[cfg(test)]
mod tests {
use super::*;
use petgraph::stable_graph::StableDiGraph;
#[test]
fn above_threshold_strengthens() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
g.add_edge(a, b, 0.5);
let mut state = BcmState::new(2, 0.3);
let config = BcmConfig::default();
bcm_update(&mut g, &[(a, 0.9), (b, 0.9)], &mut state, &config);
assert!(*g.edge_weight(0.into()).unwrap() > 0.5);
}
#[test]
fn below_threshold_weakens() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
g.add_edge(a, b, 0.5);
let mut state = BcmState::new(2, 0.9);
let config = BcmConfig::default();
bcm_update(&mut g, &[(a, 0.3), (b, 0.3)], &mut state, &config);
assert!(*g.edge_weight(0.into()).unwrap() < 0.5);
}
#[test]
fn threshold_slides_toward_activity() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let _b = g.add_node(());
let mut state = BcmState::new(2, 0.5);
let config = BcmConfig {
threshold_rate: 0.1,
..BcmConfig::default()
};
bcm_update(&mut g, &[(a, 1.0)], &mut state, &config);
assert!((state.thresholds[0] - 0.55).abs() < 1e-10);
}
#[test]
fn no_edge_no_modification() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
let mut state = BcmState::new(2, 0.5);
let modified = bcm_update(
&mut g,
&[(a, 0.8), (b, 0.8)],
&mut state,
&BcmConfig::default(),
);
assert_eq!(modified, 0);
}
#[test]
fn repeated_high_activity_raises_threshold() {
let mut g = StableDiGraph::<(), f64>::new();
let a = g.add_node(());
let b = g.add_node(());
g.add_edge(a, b, 0.5);
let mut state = BcmState::new(2, 0.1);
let config = BcmConfig {
threshold_rate: 0.5,
learning_rate: 0.01,
};
for _ in 0..20 {
bcm_update(&mut g, &[(a, 1.0), (b, 1.0)], &mut state, &config);
}
assert!(state.thresholds[1] > 0.9);
}
}