Skip to main content

petgraph_live/hebbian/
bcm.rs

1//! BCM (Bienenstock-Cooper-Munro) rule — homeostatic plasticity.
2//!
3//! Activity-dependent threshold: highly active nodes become harder to strengthen.
4//! Prevents runaway strengthening and stabilizes the network.
5
6use petgraph::EdgeType;
7use petgraph::graph::NodeIndex;
8use petgraph::stable_graph::StableGraph;
9
10/// BCM configuration.
11#[derive(Debug, Clone, Copy)]
12pub struct BcmConfig {
13    /// Learning rate (default: 0.01).
14    pub learning_rate: f64,
15    /// Threshold sliding rate (default: 0.001).
16    pub threshold_rate: f64,
17}
18
19impl Default for BcmConfig {
20    fn default() -> Self {
21        Self {
22            learning_rate: 0.01,
23            threshold_rate: 0.001,
24        }
25    }
26}
27
28/// BCM state — tracks per-node sliding threshold.
29#[derive(Debug, Clone)]
30pub struct BcmState {
31    /// Per-node activity threshold (indexed by `NodeIndexable::to_index`).
32    pub thresholds: Vec<f64>,
33}
34
35impl BcmState {
36    /// Create state for a graph with `n` nodes, initialized to `initial_threshold`.
37    pub fn new(n: usize, initial_threshold: f64) -> Self {
38        Self {
39            thresholds: vec![initial_threshold; n],
40        }
41    }
42}
43
44/// Apply BCM rule: strengthen if activation > threshold, weaken if below.
45///
46/// For each edge (pre → post) where both are in `activated`:
47///   Δw = η × y_post × (y_post − θ_post) × x_pre
48///
49/// Thresholds slide toward recent activity:
50///   Δθ = threshold_rate × (y² − θ)
51///
52/// Returns number of edges modified.
53///
54/// # Examples
55///
56/// ```
57/// use petgraph::stable_graph::StableDiGraph;
58/// use petgraph_live::hebbian::{bcm_update, BcmConfig, BcmState};
59///
60/// let mut g = StableDiGraph::<(), f64>::new();
61/// let a = g.add_node(());
62/// let b = g.add_node(());
63/// g.add_edge(a, b, 0.5);
64///
65/// let mut state = BcmState::new(2, 0.5);
66/// let modified = bcm_update(&mut g, &[(a, 0.8), (b, 0.9)], &mut state, &BcmConfig::default());
67/// assert_eq!(modified, 1);
68/// ```
69pub fn bcm_update<N, Ty: EdgeType>(
70    graph: &mut StableGraph<N, f64, Ty>,
71    activated: &[(NodeIndex, f64)],
72    state: &mut BcmState,
73    config: &BcmConfig,
74) -> usize {
75    // Update thresholds for all activated nodes
76    for &(node, y) in activated {
77        let idx = node.index();
78        if idx < state.thresholds.len() {
79            let theta = &mut state.thresholds[idx];
80            *theta += config.threshold_rate * (y * y - *theta);
81        }
82    }
83
84    // Modify edges between activated pairs
85    let mut modified = 0;
86    for i in 0..activated.len() {
87        let j_start = if Ty::is_directed() { 0 } else { i + 1 };
88        for j in j_start..activated.len() {
89            if i == j {
90                continue;
91            }
92            let (pre_node, x) = activated[i];
93            let (post_node, y) = activated[j];
94            let post_idx = post_node.index();
95            if post_idx >= state.thresholds.len() {
96                continue;
97            }
98            let theta = state.thresholds[post_idx];
99            // Δw = η × y × (y − θ) × x
100            let delta_w = config.learning_rate * y * (y - theta) * x;
101
102            if let Some(edge_idx) = graph.find_edge(pre_node, post_node)
103                && let Some(w) = graph.edge_weight_mut(edge_idx)
104            {
105                *w += delta_w;
106                modified += 1;
107            }
108        }
109    }
110    modified
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use petgraph::stable_graph::StableDiGraph;
117
118    #[test]
119    fn above_threshold_strengthens() {
120        let mut g = StableDiGraph::<(), f64>::new();
121        let a = g.add_node(());
122        let b = g.add_node(());
123        g.add_edge(a, b, 0.5);
124
125        // threshold=0.3, activation=0.9 → above threshold → strengthen
126        let mut state = BcmState::new(2, 0.3);
127        let config = BcmConfig::default();
128        bcm_update(&mut g, &[(a, 0.9), (b, 0.9)], &mut state, &config);
129        assert!(*g.edge_weight(0.into()).unwrap() > 0.5);
130    }
131
132    #[test]
133    fn below_threshold_weakens() {
134        let mut g = StableDiGraph::<(), f64>::new();
135        let a = g.add_node(());
136        let b = g.add_node(());
137        g.add_edge(a, b, 0.5);
138
139        // threshold=0.9, activation=0.3 → below threshold → weaken
140        let mut state = BcmState::new(2, 0.9);
141        let config = BcmConfig::default();
142        bcm_update(&mut g, &[(a, 0.3), (b, 0.3)], &mut state, &config);
143        assert!(*g.edge_weight(0.into()).unwrap() < 0.5);
144    }
145
146    #[test]
147    fn threshold_slides_toward_activity() {
148        let mut g = StableDiGraph::<(), f64>::new();
149        let a = g.add_node(());
150        let _b = g.add_node(());
151
152        let mut state = BcmState::new(2, 0.5);
153        let config = BcmConfig {
154            threshold_rate: 0.1,
155            ..BcmConfig::default()
156        };
157
158        // Activate a with y=1.0 → threshold moves toward y²=1.0
159        bcm_update(&mut g, &[(a, 1.0)], &mut state, &config);
160        // θ += 0.1 * (1.0 - 0.5) = 0.55
161        assert!((state.thresholds[0] - 0.55).abs() < 1e-10);
162    }
163
164    #[test]
165    fn no_edge_no_modification() {
166        let mut g = StableDiGraph::<(), f64>::new();
167        let a = g.add_node(());
168        let b = g.add_node(());
169
170        let mut state = BcmState::new(2, 0.5);
171        let modified = bcm_update(
172            &mut g,
173            &[(a, 0.8), (b, 0.8)],
174            &mut state,
175            &BcmConfig::default(),
176        );
177        assert_eq!(modified, 0);
178    }
179
180    #[test]
181    fn repeated_high_activity_raises_threshold() {
182        let mut g = StableDiGraph::<(), f64>::new();
183        let a = g.add_node(());
184        let b = g.add_node(());
185        g.add_edge(a, b, 0.5);
186
187        let mut state = BcmState::new(2, 0.1);
188        let config = BcmConfig {
189            threshold_rate: 0.5,
190            learning_rate: 0.01,
191        };
192
193        // Repeated high activation raises threshold → eventually weakens
194        for _ in 0..20 {
195            bcm_update(&mut g, &[(a, 1.0), (b, 1.0)], &mut state, &config);
196        }
197        // Threshold for b should be near 1.0 (y²=1.0)
198        assert!(state.thresholds[1] > 0.9);
199    }
200}