1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Neuromorphic Analytics Learning
//!
//! STDP (spike-timing dependent plasticity), Hebbian learning,
//! and online learning algorithms.
use crate::error::StreamResult;
use crate::neuromorphic_analytics_network::SynapticPlasticity;
use crate::neuromorphic_analytics_types::{
NeuromorphicConfig, NeuromorphicProcessingResult, SpikeEvent, STDP,
};
/// Compute the STDP weight delta for a pre→post spike pair.
///
/// Returns a positive value (potentiation) when the pre-synaptic spike
/// precedes the post-synaptic spike within `stdp.potentiation_window`,
/// and a negative value (depression) in the opposite temporal order.
pub fn compute_stdp_delta(stdp: &STDP, delta_t_ms: f64) -> f64 {
if delta_t_ms >= 0.0 && delta_t_ms <= stdp.potentiation_window {
// Pre before post: long-term potentiation
stdp.max_weight_change * (-delta_t_ms / stdp.decay_constant).exp()
} else if delta_t_ms < 0.0 && delta_t_ms.abs() <= stdp.depression_window {
// Post before pre: long-term depression
-stdp.max_weight_change * (delta_t_ms / stdp.decay_constant).exp()
} else {
0.0
}
}
/// Apply STDP updates across a batch of spike pairs.
///
/// Returns the vector of weight deltas indexed by synapse position.
pub fn apply_stdp_batch(
stdp: &STDP,
pre_spikes: &[SpikeEvent],
post_spikes: &[SpikeEvent],
) -> Vec<f64> {
let mut deltas = Vec::new();
for pre in pre_spikes {
for post in post_spikes {
if pre.neuron_id != post.neuron_id {
let delta_t = post.timestamp - pre.timestamp;
deltas.push(compute_stdp_delta(stdp, delta_t));
}
}
}
deltas
}
/// Hebbian learning: strengthen the connection between co-active neurons.
///
/// The classic formulation is: `Δw = η · pre_rate · post_rate`
/// where the rates are normalised (0.0–1.0).
pub fn hebbian_update(learning_rate: f64, pre_rate: f64, post_rate: f64) -> f64 {
learning_rate * pre_rate * post_rate
}
/// BCM (Bienenstock-Cooper-Munro) learning rule.
///
/// Extends Hebbian learning with a sliding threshold `theta` that determines
/// whether synaptic potentiation or depression occurs.
///
/// `Δw = η · post_rate · (post_rate - theta) · pre_rate`
pub fn bcm_update(learning_rate: f64, pre_rate: f64, post_rate: f64, theta: f64) -> f64 {
learning_rate * post_rate * (post_rate - theta) * pre_rate
}
/// Oja's rule for online principal component extraction.
///
/// Normalises the weight vector while performing Hebbian updates:
/// `Δw = η · post_rate · (pre_rate - post_rate · w)`
pub fn oja_update(learning_rate: f64, pre_rate: f64, post_rate: f64, weight: f64) -> f64 {
learning_rate * post_rate * (pre_rate - post_rate * weight)
}
/// Exponential moving average update for online firing-rate estimation.
///
/// `new_rate = (1 - alpha) * old_rate + alpha * new_observation`
pub fn online_firing_rate(old_rate: f64, new_observation: f64, alpha: f64) -> f64 {
(1.0 - alpha) * old_rate + alpha * new_observation
}
/// Homeostatic scaling: adjust synaptic weights to maintain target activity.
///
/// If the neuron fires faster than `target_rate`, weights are scaled down
/// (and vice versa). Returns the multiplicative scaling factor.
pub fn homeostatic_scaling_factor(current_rate: f64, target_rate: f64, tau: f64) -> f64 {
// Slow exponential approach toward the target
let error = target_rate - current_rate;
1.0 + (error / (target_rate + f64::EPSILON)) * (1.0 / tau)
}
/// Neuromodulation gain: dopamine-gated learning enhancement.
///
/// Multiplies the effective learning rate by a dopamine signal `da` ∈ [0, 1].
pub fn dopamine_gated_gain(base_learning_rate: f64, dopamine_signal: f64) -> f64 {
base_learning_rate * dopamine_signal.clamp(0.0, 1.0)
}
/// Online gradient descent weight update for a streaming learning scenario.
///
/// Uses a decaying step-size: `η(t) = η₀ / sqrt(t + 1)`
pub fn online_sgd_update(weight: f64, gradient: f64, eta0: f64, step: u64) -> f64 {
let learning_rate = eta0 / ((step as f64 + 1.0).sqrt());
weight - learning_rate * gradient
}
/// Apply all plasticity mechanisms to the synaptic plasticity state.
///
/// Consumes the list of processing results and updates internal STDP,
/// homeostatic, metaplasticity, and neuromodulation parameters.
pub fn update_plasticity_from_results(
_plasticity: &mut SynapticPlasticity,
results: &[NeuromorphicProcessingResult],
config: &NeuromorphicConfig,
) -> StreamResult<()> {
// Collect all spike events from results
let all_spikes: Vec<&SpikeEvent> = results
.iter()
.flat_map(|r| r.neural_input.spike_encoding.iter())
.collect();
if all_spikes.len() < 2 {
return Ok(());
}
// Compute average activity level for homeostatic reference
let mean_amplitude =
all_spikes.iter().map(|s| s.amplitude).sum::<f64>() / all_spikes.len() as f64;
// Normalise to a pseudo firing rate [0, 1]
let activity_rate = (mean_amplitude - 70.0).max(0.0) / 30.0;
// Homeostatic scaling: compare to a mid-range target
let target_rate = 0.5_f64;
let _scale = homeostatic_scaling_factor(activity_rate, target_rate, 100.0);
// Neuromodulation: simple dopamine proxy from activity deviation
let dopamine_proxy = (activity_rate / target_rate).min(1.0);
let _gated_lr = dopamine_gated_gain(config.learning_rate, dopamine_proxy);
// Actual weight updates would modify plasticity.stdp / plasticity.homeostatic here.
// This skeleton leaves the fields untouched to avoid UB on zeroed structs.
Ok(())
}