1use super::{
7 to_generic_or, MembraneDynamicsConfig, NeuromorphicMetrics, PlasticityModel, STDPConfig, Spike,
8 SpikeTrain,
9};
10
11use crate::error::Result;
12use scirs2_core::ndarray::{Array1, Array2};
13use scirs2_core::numeric::Float;
14use scirs2_core::random::thread_rng;
15use std::collections::{HashMap, VecDeque};
16use std::fmt::Debug;
17
18#[derive(Debug, Clone)]
20pub struct SpikingConfig<T: Float + Debug + Send + Sync + 'static> {
21 pub time_step: T,
23
24 pub simulation_time: T,
26
27 pub encoding_method: SpikeEncodingMethod,
29
30 pub decoding_method: SpikeDecodingMethod,
32
33 pub spike_learning_rate: T,
35
36 pub temporal_window: T,
38
39 pub lateral_inhibition: bool,
41
42 pub homeostatic_config: HomeostaticConfig<T>,
44
45 pub noise_config: SpikeNoiseConfig<T>,
47}
48
49#[derive(Debug, Clone, Copy)]
51pub enum SpikeEncodingMethod {
52 RateCoding,
54
55 TemporalCoding,
57
58 PopulationVectorCoding,
60
61 SparseCoding,
63
64 PhaseCoding,
66
67 BurstCoding,
69
70 RankOrderCoding,
72}
73
74#[derive(Debug, Clone, Copy)]
76pub enum SpikeDecodingMethod {
77 RateDecoding,
79
80 TemporalDecoding,
82
83 PopulationVectorDecoding,
85
86 WeightedSpikeCount,
88
89 MovingAverageFilter,
91
92 ExponentialDecayFilter,
94}
95
96#[derive(Debug, Clone)]
98pub struct HomeostaticConfig<T: Float + Debug + Send + Sync + 'static> {
99 pub enable_homeostatic_scaling: bool,
101
102 pub target_firing_rate: T,
104
105 pub scaling_time_constant: T,
107
108 pub scaling_factor: T,
110
111 pub enable_intrinsic_plasticity: bool,
113
114 pub threshold_adaptation_rate: T,
116}
117
118#[derive(Debug, Clone)]
120pub struct SpikeNoiseConfig<T: Float + Debug + Send + Sync + 'static> {
121 pub background_rate: T,
123
124 pub jitter_std: T,
126
127 pub poisson_noise: bool,
129
130 pub noise_amplitude: T,
132
133 pub correlation_noise: T,
135}
136
137impl<T: Float + Debug + Send + Sync + 'static> Default for SpikingConfig<T> {
138 fn default() -> Self {
139 Self {
140 time_step: T::from(0.1).unwrap_or_else(|| T::zero()),
141 simulation_time: T::from(1000.0).unwrap_or_else(|| T::zero()),
142 encoding_method: SpikeEncodingMethod::RateCoding,
143 decoding_method: SpikeDecodingMethod::RateDecoding,
144 spike_learning_rate: T::from(0.01).unwrap_or_else(|| T::zero()),
145 temporal_window: T::from(20.0).unwrap_or_else(|| T::zero()),
146 lateral_inhibition: false,
147 homeostatic_config: HomeostaticConfig::default(),
148 noise_config: SpikeNoiseConfig::default(),
149 }
150 }
151}
152
153impl<T: Float + Debug + Send + Sync + 'static> Default for HomeostaticConfig<T> {
154 fn default() -> Self {
155 Self {
156 enable_homeostatic_scaling: false,
157 target_firing_rate: T::from(10.0).unwrap_or_else(|| T::zero()),
158 scaling_time_constant: T::from(1000.0).unwrap_or_else(|| T::zero()),
159 scaling_factor: T::from(0.01).unwrap_or_else(|| T::zero()),
160 enable_intrinsic_plasticity: false,
161 threshold_adaptation_rate: T::from(0.001).unwrap_or_else(|| T::zero()),
162 }
163 }
164}
165
166const RATE_CODING_MAX_RATE_HZ: f64 = 100.0;
172
173impl<T: Float + Debug + Send + Sync + 'static> Default for SpikeNoiseConfig<T> {
174 fn default() -> Self {
175 Self {
176 background_rate: T::from(1.0).unwrap_or_else(|| T::zero()),
177 jitter_std: T::from(0.5).unwrap_or_else(|| T::zero()),
178 poisson_noise: false,
179 noise_amplitude: T::from(0.1).unwrap_or_else(|| T::zero()),
180 correlation_noise: T::zero(),
181 }
182 }
183}
184
185pub struct SpikingOptimizer<
187 T: Float + Debug + Send + Sync + scirs2_core::ndarray::ScalarOperand + 'static,
188> {
189 config: SpikingConfig<T>,
191
192 stdp_config: STDPConfig<T>,
194
195 membrane_config: MembraneDynamicsConfig<T>,
197
198 current_time: T,
200
201 spike_trains: HashMap<usize, SpikeTrain<T>>,
203
204 membrane_potentials: Array1<T>,
206
207 synaptic_weights: Array2<T>,
209
210 last_spike_times: Array1<T>,
212
213 refractory_until: Array1<T>,
215
216 synaptic_current: Array1<T>,
221
222 homeostatic_scales: Array1<T>,
224
225 spike_buffer: VecDeque<Spike<T>>,
227
228 metrics: NeuromorphicMetrics<T>,
230
231 plasticity_model: PlasticityModel,
233}
234
235impl<
236 T: Float
237 + Debug
238 + Send
239 + Sync
240 + scirs2_core::ndarray::ScalarOperand
241 + 'static
242 + std::iter::Sum,
243 > SpikingOptimizer<T>
244{
245 pub fn new(
247 config: SpikingConfig<T>,
248 stdp_config: STDPConfig<T>,
249 membrane_config: MembraneDynamicsConfig<T>,
250 num_neurons: usize,
251 ) -> Self {
252 let resting_potential = membrane_config.resting_potential;
253 Self {
254 config,
255 stdp_config,
256 membrane_config,
257 current_time: T::zero(),
258 spike_trains: HashMap::new(),
259 membrane_potentials: Array1::from_elem(num_neurons, resting_potential),
260 synaptic_weights: Array2::ones((num_neurons, num_neurons))
261 * T::from(0.1).unwrap_or_else(|| T::zero()),
262 last_spike_times: Array1::from_elem(
263 num_neurons,
264 T::from(-1000.0).unwrap_or_else(|| T::zero()),
265 ),
266 refractory_until: Array1::zeros(num_neurons),
267 synaptic_current: Array1::zeros(num_neurons),
268 homeostatic_scales: Array1::ones(num_neurons),
269 spike_buffer: VecDeque::new(),
270 metrics: NeuromorphicMetrics::default(),
271 plasticity_model: PlasticityModel::STDP,
272 }
273 }
274
275 pub fn encode_input(&self, input: &Array1<T>) -> Result<Vec<SpikeTrain<T>>> {
277 let mut spike_trains = Vec::new();
278
279 for (neuron_id, &value) in input.iter().enumerate() {
280 let spike_train = match self.config.encoding_method {
281 SpikeEncodingMethod::RateCoding => self.rate_encode(neuron_id, value)?,
282 SpikeEncodingMethod::TemporalCoding => self.temporal_encode(neuron_id, value)?,
283 SpikeEncodingMethod::PopulationVectorCoding => {
284 self.population_vector_encode(neuron_id, value)?
285 }
286 SpikeEncodingMethod::SparseCoding => self.sparse_encode(neuron_id, value)?,
287 _ => {
288 self.rate_encode(neuron_id, value)?
290 }
291 };
292
293 spike_trains.push(spike_train);
294 }
295
296 Ok(spike_trains)
297 }
298
299 fn rate_coding_window(&self) -> T {
305 self.config.simulation_time
306 }
307
308 fn rate_encode(&self, neuron_id: usize, value: T) -> Result<SpikeTrain<T>> {
310 let max_rate = to_generic_or(RATE_CODING_MAX_RATE_HZ, T::one()); let firing_rate = value.abs() * max_rate;
312
313 let mut spike_times = Vec::new();
314 let dt = self.config.time_step;
315 let total_time = self.rate_coding_window();
316
317 let mut time = T::zero();
318 while time < total_time {
319 let spike_prob = firing_rate * dt / to_generic_or(1000.0, T::one());
321
322 if thread_rng().random::<f64>() < spike_prob.to_f64().unwrap_or(0.0) {
323 spike_times.push(time);
324 }
325
326 time = time + dt;
327 }
328
329 Ok(SpikeTrain::new(neuron_id, spike_times))
330 }
331
332 fn temporal_encode(&self, neuron_id: usize, value: T) -> Result<SpikeTrain<T>> {
334 let max_delay = T::from(20.0).unwrap_or_else(|| T::zero()); let spike_time = if value > T::zero() {
336 max_delay * (T::one() - value.min(T::one()))
337 } else {
338 max_delay };
340
341 let spike_times = if spike_time < max_delay {
342 vec![spike_time]
343 } else {
344 Vec::new()
345 };
346
347 Ok(SpikeTrain::new(neuron_id, spike_times))
348 }
349
350 fn population_vector_encode(&self, neuron_id: usize, value: T) -> Result<SpikeTrain<T>> {
352 self.rate_encode(neuron_id, value)
354 }
355
356 fn sparse_encode(&self, neuron_id: usize, value: T) -> Result<SpikeTrain<T>> {
358 let threshold = T::from(0.5).unwrap_or_else(|| T::zero());
359
360 if value.abs() > threshold {
361 self.rate_encode(neuron_id, value)
362 } else {
363 Ok(SpikeTrain::new(neuron_id, Vec::new()))
364 }
365 }
366
367 pub fn decode_output(&self, spike_trains: &[SpikeTrain<T>]) -> Result<Array1<T>> {
369 let mut output = Array1::zeros(spike_trains.len());
370
371 for (i, spike_train) in spike_trains.iter().enumerate() {
372 output[i] = match self.config.decoding_method {
373 SpikeDecodingMethod::RateDecoding => self.rate_decode(spike_train)?,
374 SpikeDecodingMethod::TemporalDecoding => self.temporal_decode(spike_train)?,
375 SpikeDecodingMethod::WeightedSpikeCount => {
376 self.weighted_spike_count_decode(spike_train)?
377 }
378 _ => {
379 self.rate_decode(spike_train)?
381 }
382 };
383 }
384
385 Ok(output)
386 }
387
388 fn rate_decode(&self, spike_train: &SpikeTrain<T>) -> Result<T> {
393 let window_duration = self.rate_coding_window();
394 let spike_count = to_generic_or(spike_train.spike_count as f64, T::zero());
395 let window_seconds = window_duration / to_generic_or(1000.0, T::one());
396 if window_seconds <= T::zero() {
397 return Ok(T::zero());
398 }
399 let rate = spike_count / window_seconds;
400 let max_rate = to_generic_or(RATE_CODING_MAX_RATE_HZ, T::one());
401 Ok(rate / max_rate) }
403
404 fn temporal_decode(&self, spike_train: &SpikeTrain<T>) -> Result<T> {
406 if spike_train.spike_times.is_empty() {
407 Ok(T::zero())
408 } else {
409 let first_spike = spike_train.spike_times[0];
410 let max_delay = T::from(20.0).unwrap_or_else(|| T::zero());
411 Ok(T::one() - (first_spike / max_delay).min(T::one()))
412 }
413 }
414
415 fn weighted_spike_count_decode(&self, spike_train: &SpikeTrain<T>) -> Result<T> {
417 if spike_train.spike_times.is_empty() {
418 return Ok(T::zero());
419 }
420
421 let mut weighted_sum = T::zero();
422 let current_time = self.current_time;
423
424 for &spike_time in &spike_train.spike_times {
425 let time_diff = current_time - spike_time;
426 let weight = (-time_diff / T::from(10.0).unwrap_or_else(|| T::zero())).exp(); weighted_sum = weighted_sum + weight;
428 }
429
430 Ok(weighted_sum)
431 }
432
433 pub fn simulate_step(&mut self, input_spikes: &[Spike<T>]) -> Result<Vec<Spike<T>>> {
435 let mut output_spikes = Vec::new();
436 let dt = self.config.time_step;
437
438 for spike in input_spikes {
440 self.process_input_spike(spike)?;
441 }
442
443 for neuron_id in 0..self.membrane_potentials.len() {
445 if self.current_time >= self.refractory_until[neuron_id] {
446 self.update_membrane_potential(neuron_id, dt)?;
447
448 if self.membrane_potentials[neuron_id] >= self.membrane_config.threshold_potential {
450 let spike = self.generate_spike(neuron_id)?;
451 output_spikes.push(spike);
452 }
453 }
454 }
455
456 self.update_plasticity(&output_spikes)?;
458
459 if self.config.homeostatic_config.enable_homeostatic_scaling {
461 self.update_homeostatic_scaling()?;
462 }
463
464 self.current_time = self.current_time + dt;
465
466 Ok(output_spikes)
467 }
468
469 fn process_input_spike(&mut self, spike: &Spike<T>) -> Result<()> {
474 let target_neuron = spike.postsynaptic_id.unwrap_or(spike.neuron_id);
475
476 if target_neuron < self.synaptic_current.len() {
477 let synaptic_current = spike.weight * spike.amplitude;
478 self.synaptic_current[target_neuron] =
479 self.synaptic_current[target_neuron] + synaptic_current;
480 }
481
482 Ok(())
483 }
484
485 fn update_membrane_potential(&mut self, neuron_id: usize, dt: T) -> Result<()> {
492 let v = self.membrane_potentials[neuron_id];
493 let v_rest = self.membrane_config.resting_potential;
494 let tau = self.membrane_config.tau_membrane;
495 let leak_conductance = self.membrane_config.leak_conductance;
496 let membrane_resistance = if leak_conductance > T::zero() {
497 T::one() / leak_conductance
498 } else {
499 T::zero()
500 };
501 let i_syn = self.synaptic_current[neuron_id];
502
503 let dv_dt = if tau > T::zero() {
504 ((v_rest - v) + membrane_resistance * i_syn) / tau
505 } else {
506 T::zero()
507 };
508 let new_v = v + dv_dt * dt;
509
510 self.membrane_potentials[neuron_id] = new_v;
511
512 self.synaptic_current[neuron_id] = T::zero();
515
516 Ok(())
517 }
518
519 fn generate_spike(&mut self, neuron_id: usize) -> Result<Spike<T>> {
521 self.membrane_potentials[neuron_id] = self.membrane_config.reset_potential;
523
524 self.refractory_until[neuron_id] =
526 self.current_time + self.membrane_config.refractory_period;
527
528 self.last_spike_times[neuron_id] = self.current_time;
530
531 let spike = Spike {
533 neuron_id,
534 time: self.current_time,
535 amplitude: to_generic_or(1.0, T::one()),
536 width: Some(to_generic_or(1.0, T::one())),
537 weight: T::one(),
538 presynaptic_id: None,
539 postsynaptic_id: None,
540 };
541
542 for target_id in 0..self.synaptic_weights.ncols() {
547 if target_id != neuron_id {
548 let w = self.synaptic_weights[[neuron_id, target_id]];
549 self.synaptic_current[target_id] = self.synaptic_current[target_id] + w;
550 }
551 }
552
553 self.spike_trains
557 .entry(neuron_id)
558 .or_insert_with(|| SpikeTrain::new(neuron_id, Vec::new()))
559 .record_spike(self.current_time);
560
561 self.metrics.total_spikes += 1;
563
564 Ok(spike)
565 }
566
567 fn update_plasticity(&mut self, output_spikes: &[Spike<T>]) -> Result<()> {
569 match self.plasticity_model {
570 PlasticityModel::STDP => {
571 self.update_stdp(output_spikes)?;
572 }
573 PlasticityModel::Hebbian => {
574 self.update_hebbian(output_spikes)?;
575 }
576 _ => {
577 self.update_stdp(output_spikes)?;
579 }
580 }
581
582 Ok(())
583 }
584
585 fn update_stdp(&mut self, output_spikes: &[Spike<T>]) -> Result<()> {
587 let long_ago = to_generic_or(-1000.0, T::zero());
588
589 for spike in output_spikes {
590 let fired_id = spike.neuron_id;
591 let fired_time = spike.time;
592
593 for other_id in 0..self.last_spike_times.len() {
594 if other_id == fired_id {
595 continue;
596 }
597 let other_time = self.last_spike_times[other_id];
598 if other_time <= long_ago {
599 continue; }
601
602 let dt_ltp = fired_time - other_time;
606 let ltp = self.compute_stdp_update(dt_ltp);
607 self.synaptic_weights[[other_id, fired_id]] =
608 (self.synaptic_weights[[other_id, fired_id]] + ltp)
609 .max(self.stdp_config.weight_min)
610 .min(self.stdp_config.weight_max);
611
612 let dt_ltd = other_time - fired_time;
622 let ltd = self.compute_stdp_update(dt_ltd);
623 self.synaptic_weights[[fired_id, other_id]] =
624 (self.synaptic_weights[[fired_id, other_id]] + ltd)
625 .max(self.stdp_config.weight_min)
626 .min(self.stdp_config.weight_max);
627 }
628 }
629
630 Ok(())
631 }
632
633 fn compute_stdp_update(&self, dt: T) -> T {
635 if dt > T::zero() {
636 let exp_arg = -dt / self.stdp_config.tau_pot;
638 self.stdp_config.learning_rate_pot * exp_arg.exp()
639 } else {
640 let exp_arg = dt / self.stdp_config.tau_dep;
642 -self.stdp_config.learning_rate_dep * exp_arg.exp()
643 }
644 }
645
646 fn update_hebbian(&mut self, output_spikes: &[Spike<T>]) -> Result<()> {
653 let v_rest = self.membrane_config.resting_potential;
654 let v_thresh = self.membrane_config.threshold_potential;
655 let range = v_thresh - v_rest;
656
657 for spike in output_spikes {
658 let post_id = spike.neuron_id;
659
660 for pre_id in 0..self.membrane_potentials.len() {
661 if pre_id != post_id {
662 let pre_activity = if range != T::zero() {
663 ((self.membrane_potentials[pre_id] - v_rest) / range).max(T::zero())
664 } else {
665 T::zero()
666 };
667
668 let weight_change = self.stdp_config.learning_rate_pot * pre_activity;
669
670 self.synaptic_weights[[pre_id, post_id]] =
671 (self.synaptic_weights[[pre_id, post_id]] + weight_change)
672 .max(self.stdp_config.weight_min)
673 .min(self.stdp_config.weight_max);
674 }
675 }
676 }
677
678 Ok(())
679 }
680
681 fn update_homeostatic_scaling(&mut self) -> Result<()> {
693 let target_rate = self.config.homeostatic_config.target_firing_rate;
694 let time_constant = self.config.homeostatic_config.scaling_time_constant;
695 let dt = self.config.time_step;
696 if time_constant <= T::zero() {
697 return Ok(());
698 }
699
700 let min_step = to_generic_or(0.9, T::one());
701 let max_step = to_generic_or(1.1, T::one());
702 let min_cumulative = to_generic_or(0.1, T::zero());
703 let max_cumulative = to_generic_or(10.0, T::one());
704
705 for neuron_id in 0..self.homeostatic_scales.len() {
706 if let Some(spike_train) = self.spike_trains.get(&neuron_id) {
707 let current_rate = spike_train.firing_rate;
708 let rate_error = target_rate - current_rate;
709
710 let raw_step_scale = T::one() + rate_error * dt / time_constant;
713 let step_multiplier = raw_step_scale.max(min_step).min(max_step);
714
715 self.homeostatic_scales[neuron_id] = (self.homeostatic_scales[neuron_id]
718 * step_multiplier)
719 .max(min_cumulative)
720 .min(max_cumulative);
721
722 for pre_id in 0..self.synaptic_weights.nrows() {
725 self.synaptic_weights[[pre_id, neuron_id]] =
726 (self.synaptic_weights[[pre_id, neuron_id]] * step_multiplier)
727 .max(self.stdp_config.weight_min)
728 .min(self.stdp_config.weight_max);
729 }
730 }
731 }
732
733 Ok(())
734 }
735
736 pub fn get_metrics(&self) -> &NeuromorphicMetrics<T> {
738 &self.metrics
739 }
740
741 pub fn reset(&mut self) {
743 self.current_time = T::zero();
744 self.membrane_potentials
745 .fill(self.membrane_config.resting_potential);
746 self.last_spike_times
747 .fill(T::from(-1000.0).unwrap_or_else(|| T::zero()));
748 self.refractory_until.fill(T::zero());
749 self.synaptic_current.fill(T::zero());
750 self.spike_trains.clear();
751 self.spike_buffer.clear();
752 self.metrics = NeuromorphicMetrics::default();
753 }
754}
755
756pub struct SpikeTrainOptimizer<
758 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + std::fmt::Debug + Send + Sync,
759> {
760 config: SpikingConfig<T>,
762
763 pattern_templates: Vec<SpikePattern<T>>,
765
766 matching_threshold: T,
768
769 pattern_learning_rate: T,
771
772 temporal_kernel: TemporalKernel<T>,
774}
775
776#[derive(Debug, Clone)]
778pub struct SpikePattern<T: Float + Debug + Send + Sync + 'static> {
779 pub pattern_id: usize,
781
782 pub relative_spike_times: Vec<T>,
784
785 pub duration: T,
787
788 pub weight: T,
790
791 pub observation_count: usize,
793}
794
795#[derive(Debug, Clone)]
797pub struct TemporalKernel<T: Float + Debug + Send + Sync + 'static> {
798 pub kernel_type: TemporalKernelType,
800
801 pub width: T,
803
804 pub parameters: Vec<T>,
806}
807
808#[derive(Debug, Clone, Copy)]
810pub enum TemporalKernelType {
811 Gaussian,
813
814 Exponential,
816
817 Alpha,
819
820 Rectangular,
822}
823
824impl<T: Float + Debug + Send + Sync + scirs2_core::ndarray::ScalarOperand + std::fmt::Debug>
825 SpikeTrainOptimizer<T>
826{
827 pub fn new(config: SpikingConfig<T>) -> Self {
829 let kernel_width = config.temporal_window;
834 let pattern_learning_rate = config.spike_learning_rate;
835 Self {
836 config,
837 pattern_templates: Vec::new(),
838 matching_threshold: to_generic_or(0.8, T::zero()),
839 pattern_learning_rate,
840 temporal_kernel: TemporalKernel {
841 kernel_type: TemporalKernelType::Gaussian,
842 width: kernel_width,
843 parameters: vec![T::one()],
844 },
845 }
846 }
847
848 pub fn learn_patterns(&mut self, spike_trains: &[SpikeTrain<T>]) -> Result<()> {
850 for spike_train in spike_trains {
851 self.extract_and_learn_patterns(spike_train)?;
852 }
853
854 Ok(())
855 }
856
857 fn extract_and_learn_patterns(&mut self, spike_train: &SpikeTrain<T>) -> Result<()> {
859 let step_size = self.config.time_step.max(to_generic_or(1e-6, T::one()));
865 let window_size = self.config.temporal_window.max(step_size);
866
867 let mut window_start = T::zero();
868
869 while window_start < spike_train.duration {
870 let window_end = window_start + window_size;
871
872 let window_spikes: Vec<T> = spike_train
874 .spike_times
875 .iter()
876 .filter(|&&t| t >= window_start && t < window_end)
877 .map(|&t| t - window_start) .collect();
879
880 if !window_spikes.is_empty() {
881 let pattern = SpikePattern {
882 pattern_id: self.pattern_templates.len(),
883 relative_spike_times: window_spikes,
884 duration: window_size,
885 weight: T::one(),
886 observation_count: 1,
887 };
888
889 if let Some(similar_pattern_id) = self.find_similar_pattern(&pattern) {
891 self.update_pattern(similar_pattern_id, &pattern)?;
892 } else {
893 self.pattern_templates.push(pattern);
894 }
895 }
896
897 window_start = window_start + step_size;
898 }
899
900 Ok(())
901 }
902
903 fn find_similar_pattern(&self, new_pattern: &SpikePattern<T>) -> Option<usize> {
905 for (i, existing_pattern) in self.pattern_templates.iter().enumerate() {
906 let similarity = self.compute_pattern_similarity(new_pattern, existing_pattern);
907 if similarity > self.matching_threshold {
908 return Some(i);
909 }
910 }
911
912 None
913 }
914
915 fn compute_pattern_similarity(
917 &self,
918 pattern1: &SpikePattern<T>,
919 pattern2: &SpikePattern<T>,
920 ) -> T {
921 let max_spikes = pattern1
923 .relative_spike_times
924 .len()
925 .max(pattern2.relative_spike_times.len());
926 if max_spikes == 0 {
927 return T::one();
928 }
929
930 let count_diff = (pattern1.relative_spike_times.len() as i32
932 - pattern2.relative_spike_times.len() as i32)
933 .abs() as f64;
934 let count_similarity =
935 T::one() - T::from(count_diff / max_spikes as f64).unwrap_or_else(|| T::zero());
936
937 if !pattern1.relative_spike_times.is_empty() && !pattern2.relative_spike_times.is_empty() {
939 let temporal_similarity = self.compute_temporal_similarity(
940 &pattern1.relative_spike_times,
941 &pattern2.relative_spike_times,
942 );
943 (count_similarity + temporal_similarity) / T::from(2.0).unwrap_or_else(|| T::zero())
944 } else {
945 count_similarity
946 }
947 }
948
949 fn compute_temporal_similarity(&self, spikes1: &[T], spikes2: &[T]) -> T {
951 let mut max_correlation = T::zero();
953 let max_shift = T::from(10.0).unwrap_or_else(|| T::zero()); let shift_step = T::from(1.0).unwrap_or_else(|| T::zero());
955
956 let mut shift = -max_shift;
957 while shift <= max_shift {
958 let correlation = self.compute_spike_correlation(spikes1, spikes2, shift);
959 max_correlation = max_correlation.max(correlation);
960 shift = shift + shift_step;
961 }
962
963 max_correlation
964 }
965
966 fn compute_spike_correlation(&self, spikes1: &[T], spikes2: &[T], shift: T) -> T {
968 let mut correlation = T::zero();
969 let kernel_width = self.temporal_kernel.width;
970
971 for &t1 in spikes1 {
972 for &t2 in spikes2 {
973 let dt = (t1 - (t2 + shift)).abs();
974 let kernel_value = (-dt * dt
975 / (T::from(2.0).unwrap_or_else(|| T::zero()) * kernel_width * kernel_width))
976 .exp();
977 correlation = correlation + kernel_value;
978 }
979 }
980
981 if !spikes1.is_empty() && !spikes2.is_empty() {
983 correlation / to_generic_or((spikes1.len() * spikes2.len()) as f64, T::one())
984 } else {
985 T::zero()
986 }
987 }
988
989 fn update_pattern(&mut self, pattern_id: usize, new_pattern: &SpikePattern<T>) -> Result<()> {
991 if let Some(existing_pattern) = self.pattern_templates.get_mut(pattern_id) {
992 let alpha = self.pattern_learning_rate;
994
995 if existing_pattern.relative_spike_times.len() == new_pattern.relative_spike_times.len()
997 {
998 for (existing_time, &new_time) in existing_pattern
999 .relative_spike_times
1000 .iter_mut()
1001 .zip(new_pattern.relative_spike_times.iter())
1002 {
1003 *existing_time = *existing_time * (T::one() - alpha) + new_time * alpha;
1004 }
1005 }
1006
1007 existing_pattern.observation_count += 1;
1008 existing_pattern.weight =
1009 existing_pattern.weight * (T::one() - alpha) + new_pattern.weight * alpha;
1010 }
1011
1012 Ok(())
1013 }
1014
1015 pub fn recognize_patterns(&self, spike_train: &SpikeTrain<T>) -> Result<Vec<(usize, T, T)>> {
1017 let mut recognized_patterns = Vec::new();
1018 let window_size = T::from(50.0).unwrap_or_else(|| T::zero());
1019 let step_size = T::from(5.0).unwrap_or_else(|| T::zero());
1020
1021 let mut window_start = T::zero();
1022
1023 while window_start < spike_train.duration {
1024 let window_end = window_start + window_size;
1025
1026 let window_spikes: Vec<T> = spike_train
1027 .spike_times
1028 .iter()
1029 .filter(|&&t| t >= window_start && t < window_end)
1030 .map(|&t| t - window_start)
1031 .collect();
1032
1033 if !window_spikes.is_empty() {
1034 let test_pattern = SpikePattern {
1035 pattern_id: 0,
1036 relative_spike_times: window_spikes,
1037 duration: window_size,
1038 weight: T::one(),
1039 observation_count: 1,
1040 };
1041
1042 let mut best_match = (0, T::zero());
1044 for (i, template) in self.pattern_templates.iter().enumerate() {
1045 let similarity = self.compute_pattern_similarity(&test_pattern, template);
1046 if similarity > best_match.1 {
1047 best_match = (i, similarity);
1048 }
1049 }
1050
1051 if best_match.1 > self.matching_threshold {
1052 recognized_patterns.push((best_match.0, window_start, best_match.1));
1053 }
1054 }
1055
1056 window_start = window_start + step_size;
1057 }
1058
1059 Ok(recognized_patterns)
1060 }
1061
1062 pub fn get_patterns(&self) -> &[SpikePattern<T>] {
1064 &self.pattern_templates
1065 }
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070 use super::*;
1071
1072 fn make_optimizer(num_neurons: usize) -> SpikingOptimizer<f64> {
1073 SpikingOptimizer::new(
1074 SpikingConfig::default(),
1075 STDPConfig::default(),
1076 MembraneDynamicsConfig::default(),
1077 num_neurons,
1078 )
1079 }
1080
1081 fn dummy_spike(neuron_id: usize, time: f64) -> Spike<f64> {
1082 Spike {
1083 neuron_id,
1084 time,
1085 amplitude: 1.0,
1086 width: None,
1087 weight: 1.0,
1088 presynaptic_id: None,
1089 postsynaptic_id: None,
1090 }
1091 }
1092
1093 #[test]
1096 fn stdp_produces_both_potentiation_and_depression() {
1097 let mut optimizer = make_optimizer(2);
1098 optimizer.last_spike_times[0] = 5.0;
1099
1100 optimizer
1101 .update_stdp(&[dummy_spike(1, 15.0)])
1102 .expect("update_stdp failed");
1103
1104 let initial = 0.1;
1105 assert!(
1106 optimizer.synaptic_weights[[0, 1]] > initial,
1107 "LTP (0->1) did not fire: {}",
1108 optimizer.synaptic_weights[[0, 1]]
1109 );
1110 assert!(
1111 optimizer.synaptic_weights[[1, 0]] < initial,
1112 "LTD (1->0) did not fire (F50 regression): {}",
1113 optimizer.synaptic_weights[[1, 0]]
1114 );
1115 }
1116
1117 #[test]
1120 fn homeostatic_scaling_does_not_blow_up() {
1121 let mut optimizer = make_optimizer(3);
1122 optimizer
1123 .config
1124 .homeostatic_config
1125 .enable_homeostatic_scaling = true;
1126
1127 for step in 0..500 {
1128 optimizer.current_time = step as f64 * 0.1;
1129 let train = optimizer
1130 .spike_trains
1131 .entry(0)
1132 .or_insert_with(|| SpikeTrain::new(0, Vec::new()));
1133 if step % 5 == 0 {
1134 let t = optimizer.current_time;
1135 train.record_spike(t);
1136 }
1137 optimizer
1138 .update_homeostatic_scaling()
1139 .expect("update_homeostatic_scaling failed");
1140 }
1141
1142 for &w in optimizer.synaptic_weights.iter() {
1143 assert!(w.is_finite(), "weight diverged: {w}");
1144 assert!(
1145 (0.0..=1.0).contains(&w),
1146 "weight left [weight_min, weight_max]: {w}"
1147 );
1148 }
1149 for &s in optimizer.homeostatic_scales.iter() {
1150 assert!(
1151 s.is_finite() && (0.1..=10.0).contains(&s),
1152 "homeostatic scale diverged (F51 regression): {s}"
1153 );
1154 }
1155 }
1156
1157 #[test]
1160 fn synaptic_weights_propagate_into_membrane_dynamics() {
1161 let mut optimizer = make_optimizer(2);
1162 optimizer.synaptic_weights[[0, 1]] = 50.0;
1163 optimizer.membrane_potentials[1] = optimizer.membrane_config.resting_potential;
1164 optimizer.membrane_potentials[0] = optimizer.membrane_config.threshold_potential;
1165
1166 optimizer.generate_spike(0).expect("generate_spike failed");
1167 let dt = optimizer.config.time_step;
1168 optimizer
1169 .update_membrane_potential(1, dt)
1170 .expect("update_membrane_potential failed");
1171
1172 assert!(
1173 optimizer.membrane_potentials[1] > optimizer.membrane_config.resting_potential,
1174 "postsynaptic potential did not respond to the propagated synaptic weight (F52 regression)"
1175 );
1176 }
1177
1178 #[test]
1182 fn hebbian_activity_increases_with_depolarization() {
1183 let run = |pre_potential: f64| -> f64 {
1184 let mut optimizer = make_optimizer(2);
1185 optimizer.plasticity_model = PlasticityModel::Hebbian;
1186 optimizer.membrane_potentials[0] = pre_potential;
1187 optimizer
1188 .update_hebbian(&[dummy_spike(1, 1.0)])
1189 .expect("update_hebbian failed");
1190 optimizer.synaptic_weights[[0, 1]]
1191 };
1192
1193 let membrane_config = MembraneDynamicsConfig::<f64>::default();
1194 let weight_at_rest = run(membrane_config.resting_potential);
1195 let weight_near_threshold = run(membrane_config.threshold_potential);
1196
1197 assert!(
1198 (weight_at_rest - 0.1).abs() < 1e-9,
1199 "resting potential should contribute zero Hebbian activity: {weight_at_rest}"
1200 );
1201 assert!(
1202 weight_near_threshold > weight_at_rest,
1203 "activity did not increase with depolarization (F53 regression): \
1204 rest={weight_at_rest}, near_threshold={weight_near_threshold}"
1205 );
1206 }
1207
1208 #[test]
1212 fn rate_encode_decode_round_trip_within_noise_tolerance() {
1213 let optimizer = make_optimizer(1);
1214 let true_value = 0.5_f64;
1215 let trials = 20;
1216
1217 let mut sum = 0.0;
1218 for _ in 0..trials {
1219 let train = optimizer
1220 .rate_encode(0, true_value)
1221 .expect("rate_encode failed");
1222 sum += optimizer.rate_decode(&train).expect("rate_decode failed");
1223 }
1224 let avg_decoded = sum / trials as f64;
1225
1226 assert!(
1227 (avg_decoded - true_value).abs() < 0.08,
1228 "decode(encode(v)) did not recover v within noise tolerance (F54 regression): \
1229 v={true_value}, avg_decoded={avg_decoded}"
1230 );
1231 }
1232}