petgraph_live/hebbian/
bcm.rs1use petgraph::EdgeType;
7use petgraph::graph::NodeIndex;
8use petgraph::stable_graph::StableGraph;
9
10#[derive(Debug, Clone, Copy)]
12pub struct BcmConfig {
13 pub learning_rate: f64,
15 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#[derive(Debug, Clone)]
30pub struct BcmState {
31 pub thresholds: Vec<f64>,
33}
34
35impl BcmState {
36 pub fn new(n: usize, initial_threshold: f64) -> Self {
38 Self {
39 thresholds: vec![initial_threshold; n],
40 }
41 }
42}
43
44pub 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 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 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 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 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 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 bcm_update(&mut g, &[(a, 1.0)], &mut state, &config);
160 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 for _ in 0..20 {
195 bcm_update(&mut g, &[(a, 1.0), (b, 1.0)], &mut state, &config);
196 }
197 assert!(state.thresholds[1] > 0.9);
199 }
200}