use scirs2_core::ndarray::{s, Array1, Array2, Array3, ArrayView2};
use scirs2_core::numeric::{Float, FromPrimitive};
use scirs2_core::random::{Rng, RngExt};
use std::collections::VecDeque;
use crate::error::{NdimageError, NdimageResult};
#[derive(Debug, Clone)]
pub struct NeuromorphicConfig {
pub tau_membrane: f64,
pub tau_synaptic: f64,
pub spike_threshold: f64,
pub refractory_period: usize,
pub learning_rate: f64,
pub tau_homeostatic: f64,
pub max_weight: f64,
pub stdp_window: usize,
pub event_threshold: f64,
}
impl Default for NeuromorphicConfig {
fn default() -> Self {
Self {
tau_membrane: 20.0,
tau_synaptic: 5.0,
spike_threshold: 1.0,
refractory_period: 2,
learning_rate: 0.01,
tau_homeostatic: 1000.0,
max_weight: 2.0,
stdp_window: 20,
event_threshold: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub struct SpikingNeuron {
pub membrane_potential: f64,
pub synaptic_current: f64,
pub time_since_spike: usize,
pub spike_times: VecDeque<usize>,
pub target_rate: f64,
pub current_rate: f64,
pub adaptation_current: f64,
pub pre_spike_trace: f64,
pub post_spike_trace: f64,
}
impl Default for SpikingNeuron {
fn default() -> Self {
Self {
membrane_potential: 0.0,
synaptic_current: 0.0,
time_since_spike: 0,
spike_times: VecDeque::new(),
target_rate: 0.1, current_rate: 0.0,
adaptation_current: 0.0,
pre_spike_trace: 0.0,
post_spike_trace: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct Event {
pub x: usize,
pub y: usize,
pub timestamp: usize,
pub polarity: bool, pub value: f64,
}
#[derive(Debug, Clone)]
pub struct PlasticSynapse {
pub weight: f64,
pub delay: usize,
pub pre_spike_trace: f64,
pub post_spike_trace: f64,
pub eligibility_trace: f64,
}
impl Default for PlasticSynapse {
fn default() -> Self {
Self {
weight: 0.5,
delay: 1,
pre_spike_trace: 0.0,
post_spike_trace: 0.0,
eligibility_trace: 0.0,
}
}
}
#[allow(dead_code)]
pub fn spiking_neural_network_filter<T>(
image: ArrayView2<T>,
network_layers: &[usize],
config: &NeuromorphicConfig,
time_steps: usize,
) -> NdimageResult<Array2<T>>
where
T: Float + FromPrimitive + Copy + Send + Sync,
{
let (height, width) = image.dim();
let mut network = initialize_snn(network_layers, height, width)?;
let spike_trains = image_to_spike_trains(&image, time_steps, config)?;
let mut output_spikes = Array3::zeros((time_steps, height, width));
for t in 0..time_steps {
let input_spikes = spike_trains.slice(s![t, .., ..]);
let layer_output = forward_propagate_snn(&mut network, &input_spikes, config, t)?;
output_spikes.slice_mut(s![t, .., ..]).assign(&layer_output);
apply_stdp_learning(&mut network, config, t)?;
}
let result = spike_trains_toimage(output_spikes.view(), config)?;
Ok(result)
}
#[allow(dead_code)]
pub fn event_driven_processing<T>(
current_frame: ArrayView2<T>,
previous_frame: Option<ArrayView2<T>>,
config: &NeuromorphicConfig,
) -> NdimageResult<(Array2<T>, Vec<Event>)>
where
T: Float + FromPrimitive + Copy + Send + Sync,
{
let (height, width) = current_frame.dim();
let events = if let Some(prev) = previous_frame {
generate_events(¤t_frame, &prev, config)?
} else {
generate_initial_events(¤t_frame, config)?
};
let mut processedimage = Array2::zeros((height, width));
let mut event_accumulator = Array2::zeros((height, width));
for event in &events {
if event.x < width && event.y < height {
apply_event_kernel(&mut event_accumulator, event, config)?;
}
}
for y in 0..height {
for x in 0..width {
let accumulated = event_accumulator[(y, x)];
let normalized = T::from_f64(accumulated.tanh()).ok_or_else(|| {
NdimageError::ComputationError("Type conversion failed".to_string())
})?;
processedimage[(y, x)] = normalized;
}
}
Ok((processedimage, events))
}
#[allow(dead_code)]
pub fn liquidstate_machine<T>(
image_sequence: &[ArrayView2<T>],
reservoir_size: usize,
config: &NeuromorphicConfig,
) -> NdimageResult<Array2<T>>
where
T: Float + FromPrimitive + Copy + Send + Sync,
{
if image_sequence.is_empty() {
return Err(NdimageError::InvalidInput(
"Empty image _sequence".to_string(),
));
}
let (height, width) = image_sequence[0].dim();
let mut reservoir = initialize_reservoir(reservoir_size, height, width, config)?;
let mut liquidstates = Vec::new();
for (t, image) in image_sequence.iter().enumerate() {
let input_currents = image_to_currents(image)?;
update_reservoir_dynamics(&mut reservoir, &input_currents, config)?;
let state = capture_reservoirstate(&reservoir)?;
liquidstates.push(state);
}
let processedimage_f64 = readout_from_liquidstates(&liquidstates, (height, width), config)?;
let processedimage = processedimage_f64.mapv(|v| T::from_f64(v).unwrap_or(T::zero()));
Ok(processedimage)
}
#[allow(dead_code)]
pub fn homeostatic_adaptive_filter<T>(
image: ArrayView2<T>,
config: &NeuromorphicConfig,
adaptation_steps: usize,
) -> NdimageResult<Array2<T>>
where
T: Float + FromPrimitive + Copy + Send + Sync,
{
let (height, width) = image.dim();
let mut neurons = Array2::from_elem((height, width), SpikingNeuron::default());
let mut synaptic_weights = Array3::from_elem((height, width, 9), 0.5);
let mut processedimage = Array2::zeros((height, width));
for step in 0..adaptation_steps {
for y in 1..height - 1 {
for x in 1..width - 1 {
let mut weighted_sum = 0.0;
let mut weight_index = 0;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let ny = (y as i32 + dy) as usize;
let nx = (x as i32 + dx) as usize;
let pixel_val = image[(ny, nx)].to_f64().unwrap_or(0.0);
let weight = synaptic_weights[(y, x, weight_index)];
weighted_sum += pixel_val * weight;
weight_index += 1;
}
}
let neuron = &mut neurons[(y, x)];
neuron.synaptic_current = weighted_sum;
let decay = (-1.0 / config.tau_membrane).exp();
neuron.membrane_potential =
neuron.membrane_potential * decay + neuron.synaptic_current;
let output_value = if neuron.membrane_potential > config.spike_threshold {
neuron.membrane_potential = 0.0; neuron.time_since_spike = 0;
neuron.spike_times.push_back(step);
1.0
} else {
neuron.time_since_spike += 1;
0.0
};
processedimage[(y, x)] = T::from_f64(output_value).ok_or_else(|| {
NdimageError::ComputationError("Type conversion failed".to_string())
})?;
update_homeostatic_weights(&mut synaptic_weights, (y, x), neuron, config, step)?;
}
}
}
Ok(processedimage)
}
#[allow(dead_code)]
pub fn temporal_coding_feature_extraction<T>(
image: ArrayView2<T>,
feature_detectors: &[Array2<f64>],
config: &NeuromorphicConfig,
time_window: usize,
) -> NdimageResult<Array3<T>>
where
T: Float + FromPrimitive + Copy + Send + Sync,
{
let (height, width) = image.dim();
let num_features = feature_detectors.len();
let mut feature_maps = Array3::zeros((num_features, height, width));
let temporalimage = create_temporal_patterns(&image, time_window, config)?;
for (feature_idx, detector) in feature_detectors.iter().enumerate() {
let (det_h, det_w) = detector.dim();
for y in 0..height.saturating_sub(det_h) {
for x in 0..width.saturating_sub(det_w) {
let mut temporal_correlation = 0.0;
for t in 0..time_window {
let mut spatial_correlation = 0.0;
for dy in 0..det_h {
for dx in 0..det_w {
let img_val = temporalimage[(t, y + dy, x + dx)];
let det_val = detector[(dy, dx)];
spatial_correlation += img_val * det_val;
}
}
let temporal_weight = (-(t as f64) / config.tau_synaptic).exp();
temporal_correlation += spatial_correlation * temporal_weight;
}
let feature_strength =
T::from_f64(temporal_correlation.tanh()).ok_or_else(|| {
NdimageError::ComputationError("Type conversion failed".to_string())
})?;
feature_maps[(feature_idx, y, x)] = feature_strength;
}
}
}
Ok(feature_maps)
}
#[allow(dead_code)]
pub fn stdp_unsupervised_learning<T>(
trainingimages: &[ArrayView2<T>],
filter_size: (usize, usize),
config: &NeuromorphicConfig,
epochs: usize,
) -> NdimageResult<Array2<f64>>
where
T: Float + FromPrimitive + Copy + Send + Sync,
{
let (filter_h, filter_w) = filter_size;
let mut rng = scirs2_core::random::rng();
let mut learned_filter = Array2::from_shape_fn(filter_size, |_| (rng.random_range(-0.1..0.1)));
let pre_synaptic_traces = Array2::<f64>::zeros(filter_size);
let post_synaptic_trace = 0.0;
for epoch in 0..epochs {
for (img_idx, image) in trainingimages.iter().enumerate() {
let (height, width) = image.dim();
let y_start = rng.random_range(0..height.saturating_sub(filter_h));
let x_start = rng.random_range(0..width.saturating_sub(filter_w));
let patch = image.slice(s![y_start..y_start + filter_h, x_start..x_start + filter_w]);
let mut pre_spike_times = Array2::zeros(filter_size);
let mut post_spike_time = 0;
for y in 0..filter_h {
for x in 0..filter_w {
let intensity = patch[(y, x)].to_f64().unwrap_or(0.0);
let spike_time =
(config.stdp_window as f64 * (1.0 - intensity)).max(1.0) as usize;
pre_spike_times[(y, x)] = spike_time as f64;
}
}
let response: f64 = patch
.iter()
.zip(learned_filter.iter())
.map(|(&p, &w)| p.to_f64().unwrap_or(0.0) * w)
.sum();
if response > config.spike_threshold {
post_spike_time = config.stdp_window / 2; }
for y in 0..filter_h {
for x in 0..filter_w {
let pre_time = pre_spike_times[(y, x)] as usize;
let dt = post_spike_time as i32 - pre_time as i32;
let stdp_update = if dt > 0 {
config.learning_rate * (-dt.abs() as f64 / config.stdp_window as f64).exp()
} else if dt < 0 {
-config.learning_rate * (-dt.abs() as f64 / config.stdp_window as f64).exp()
} else {
0.0
};
learned_filter[(y, x)] += stdp_update;
learned_filter[(y, x)] =
learned_filter[(y, x)].clamp(-config.max_weight, config.max_weight);
}
}
let weight_sum: f64 = learned_filter.iter().map(|&w| w.abs()).sum();
if weight_sum > 0.0 {
learned_filter
.mapv_inplace(|w| w / weight_sum * filter_size.0 as f64 * filter_size.1 as f64);
}
}
}
Ok(learned_filter)
}
#[allow(dead_code)]
fn initialize_snn(
layers: &[usize],
height: usize,
width: usize,
) -> NdimageResult<Vec<Array2<SpikingNeuron>>> {
let mut network = Vec::new();
for &_layer_size in layers {
let neurons = Array2::from_elem((height, width), SpikingNeuron::default());
network.push(neurons);
}
Ok(network)
}
#[allow(dead_code)]
fn image_to_spike_trains<T>(
image: &ArrayView2<T>,
time_steps: usize,
config: &NeuromorphicConfig,
) -> NdimageResult<Array3<f64>>
where
T: Float + FromPrimitive + Copy,
{
let (height, width) = image.dim();
let mut spike_trains = Array3::zeros((time_steps, height, width));
let mut rng = scirs2_core::random::rng();
for y in 0..height {
for x in 0..width {
let intensity = image[(y, x)].to_f64().unwrap_or(0.0);
let spike_rate = intensity.max(0.0).min(1.0);
for t in 0..time_steps {
if rng.random_range(0.0..1.0) < spike_rate * config.learning_rate {
spike_trains[(t, y, x)] = 1.0;
}
}
}
}
Ok(spike_trains)
}
#[allow(dead_code)]
fn forward_propagate_snn(
network: &mut [Array2<SpikingNeuron>],
input_spikes: &scirs2_core::ndarray::ArrayView2<f64>,
config: &NeuromorphicConfig,
current_time: usize,
) -> NdimageResult<Array2<f64>> {
let (height, width) = input_spikes.dim();
let mut output_spikes = Array2::zeros((height, width));
if !network.is_empty() {
let layer = &mut network[0];
for y in 0..height {
for x in 0..width {
let neuron = &mut layer[(y, x)];
let input_current = input_spikes[(y, x)];
let decay = (-1.0 / config.tau_synaptic).exp();
neuron.synaptic_current = neuron.synaptic_current * decay + input_current;
let membrane_decay = (-1.0 / config.tau_membrane).exp();
neuron.membrane_potential =
neuron.membrane_potential * membrane_decay + neuron.synaptic_current;
if neuron.membrane_potential > config.spike_threshold
&& neuron.time_since_spike > config.refractory_period
{
neuron.membrane_potential = 0.0; neuron.time_since_spike = 0;
neuron.spike_times.push_back(current_time);
if neuron.spike_times.len() > config.stdp_window {
neuron.spike_times.pop_front();
}
output_spikes[(y, x)] = 1.0;
} else {
neuron.time_since_spike += 1;
output_spikes[(y, x)] = 0.0;
}
}
}
}
Ok(output_spikes)
}
#[allow(dead_code)]
fn apply_stdp_learning(
network: &mut [Array2<SpikingNeuron>],
config: &NeuromorphicConfig,
current_time: usize,
) -> NdimageResult<()> {
for layer in network {
for neuron in layer.iter_mut() {
let trace_decay = (-1.0 / config.tau_synaptic).exp();
neuron.pre_spike_trace *= trace_decay;
neuron.post_spike_trace *= trace_decay;
if let Some(&last_spike_time) = neuron.spike_times.back() {
if current_time == last_spike_time {
neuron.post_spike_trace += 1.0;
}
}
}
}
Ok(())
}
#[allow(dead_code)]
fn spike_trains_toimage<T>(
spike_trains: scirs2_core::ndarray::ArrayView3<f64>,
_config: &NeuromorphicConfig,
) -> NdimageResult<Array2<T>>
where
T: Float + FromPrimitive + Copy,
{
let (time_steps, height, width) = spike_trains.dim();
let mut image = Array2::zeros((height, width));
for y in 0..height {
for x in 0..width {
let mut _spike_count = 0.0;
let mut weighted_sum = 0.0;
for t in 0..time_steps {
let spike = spike_trains[(t, y, x)];
if spike > 0.0 {
_spike_count += 1.0;
let temporal_weight = 1.0 - (t as f64 / time_steps as f64);
weighted_sum += spike * temporal_weight;
}
}
let intensity = weighted_sum / time_steps as f64;
image[(y, x)] = T::from_f64(intensity).ok_or_else(|| {
NdimageError::ComputationError("Type conversion failed".to_string())
})?;
}
}
Ok(image)
}
#[allow(dead_code)]
fn generate_events<T>(
current: &ArrayView2<T>,
previous: &ArrayView2<T>,
config: &NeuromorphicConfig,
) -> NdimageResult<Vec<Event>>
where
T: Float + FromPrimitive + Copy,
{
let (height, width) = current.dim();
let mut events = Vec::new();
for y in 0..height {
for x in 0..width {
let curr_val = current[(y, x)].to_f64().unwrap_or(0.0);
let prev_val = previous[(y, x)].to_f64().unwrap_or(0.0);
let diff = curr_val - prev_val;
if diff.abs() > config.event_threshold {
events.push(Event {
x,
y,
timestamp: 0, polarity: diff > 0.0,
value: diff.abs(),
});
}
}
}
Ok(events)
}
#[allow(dead_code)]
fn generate_initial_events<T>(
image: &ArrayView2<T>,
config: &NeuromorphicConfig,
) -> NdimageResult<Vec<Event>>
where
T: Float + FromPrimitive + Copy,
{
let (height, width) = image.dim();
let mut events = Vec::new();
for y in 0..height {
for x in 0..width {
let intensity = image[(y, x)].to_f64().unwrap_or(0.0);
if intensity > config.event_threshold {
events.push(Event {
x,
y,
timestamp: 0,
polarity: true,
value: intensity,
});
}
}
}
Ok(events)
}
#[allow(dead_code)]
fn apply_event_kernel(
accumulator: &mut Array2<f64>,
event: &Event,
config: &NeuromorphicConfig,
) -> NdimageResult<()> {
let (height, width) = accumulator.dim();
let kernel_size = 3;
let half_kernel = kernel_size / 2;
for dy in -(half_kernel as i32)..=(half_kernel as i32) {
for dx in -(half_kernel as i32)..=(half_kernel as i32) {
let ny = event.y as i32 + dy;
let nx = event.x as i32 + dx;
if ny >= 0 && ny < height as i32 && nx >= 0 && nx < width as i32 {
let uy = ny as usize;
let ux = nx as usize;
let spatial_weight =
(-((dy * dy + dx * dx) as f64) / (2.0 * config.tau_synaptic)).exp();
let temporal_weight = (-(event.timestamp as f64) / config.tau_membrane).exp();
let contribution = event.value * spatial_weight * temporal_weight;
accumulator[(uy, ux)] += if event.polarity {
contribution
} else {
-contribution
};
}
}
}
Ok(())
}
#[allow(dead_code)]
fn initialize_reservoir(
reservoir_size: usize,
_height: usize,
width: usize,
config: &NeuromorphicConfig,
) -> NdimageResult<Array1<SpikingNeuron>> {
let mut reservoir = Array1::from_elem(reservoir_size, SpikingNeuron::default());
let mut rng = scirs2_core::random::rng();
for (i, neuron) in reservoir.iter_mut().enumerate() {
neuron.target_rate = 0.05 + 0.1 * (i as f64 / reservoir_size as f64);
neuron.membrane_potential = rng.random_range(-0.05..0.05);
}
Ok(reservoir)
}
#[allow(dead_code)]
fn image_to_currents<T>(image: &ArrayView2<T>) -> NdimageResult<Array2<f64>>
where
T: Float + FromPrimitive + Copy,
{
let (height, width) = image.dim();
let mut currents = Array2::zeros((height, width));
for y in 0..height {
for x in 0..width {
currents[(y, x)] = image[(y, x)].to_f64().unwrap_or(0.0);
}
}
Ok(currents)
}
#[allow(dead_code)]
fn update_reservoir_dynamics(
reservoir: &mut Array1<SpikingNeuron>,
input_currents: &Array2<f64>,
config: &NeuromorphicConfig,
) -> NdimageResult<()> {
let (height, width) = input_currents.dim();
for (i, neuron) in reservoir.iter_mut().enumerate() {
let input_y = i % height;
let input_x = (i / height) % width;
let input_current = input_currents.get((input_y, input_x)).unwrap_or(&0.0);
let decay = (-1.0 / config.tau_membrane).exp();
neuron.membrane_potential = neuron.membrane_potential * decay + input_current;
if neuron.membrane_potential > config.spike_threshold {
neuron.membrane_potential = 0.0;
neuron.time_since_spike = 0;
} else {
neuron.time_since_spike += 1;
}
}
Ok(())
}
#[allow(dead_code)]
fn capture_reservoirstate(reservoir: &Array1<SpikingNeuron>) -> NdimageResult<Array1<f64>> {
let mut state = Array1::zeros(reservoir.len());
for (i, neuron) in reservoir.iter().enumerate() {
state[i] = neuron.membrane_potential;
}
Ok(state)
}
#[allow(dead_code)]
fn readout_from_liquidstates(
liquidstates: &[Array1<f64>],
outputshape: (usize, usize),
_config: &NeuromorphicConfig,
) -> NdimageResult<Array2<f64>> {
let (height, width) = outputshape;
let mut output = Array2::zeros((height, width));
if liquidstates.is_empty() {
return Ok(output);
}
let reservoir_size = liquidstates[0].len();
for y in 0..height {
for x in 0..width {
let mut sum = 0.0;
for state in liquidstates {
for i in 0..reservoir_size {
let weight = ((y * width + x + i) as f64
/ (height * width * reservoir_size) as f64)
.sin();
sum += state[i] * weight;
}
}
output[(y, x)] = sum.tanh(); }
}
Ok(output)
}
#[allow(dead_code)]
fn update_homeostatic_weights(
weights: &mut Array3<f64>,
pos: (usize, usize),
neuron: &SpikingNeuron,
config: &NeuromorphicConfig,
step: usize,
) -> NdimageResult<()> {
let (y, x) = pos;
let recent_spikes = neuron
.spike_times
.iter()
.filter(|&&spike_time| step.saturating_sub(spike_time) < 100)
.count() as f64;
let current_rate = recent_spikes / 100.0;
let rate_error = neuron.target_rate - current_rate;
let scaling_factor = 1.0 + config.learning_rate * rate_error / config.tau_homeostatic;
for i in 0..9 {
weights[(y, x, i)] *= scaling_factor;
weights[(y, x, i)] = weights[(y, x, i)].clamp(0.0, config.max_weight);
}
Ok(())
}
#[allow(dead_code)]
fn create_temporal_patterns<T>(
image: &ArrayView2<T>,
time_window: usize,
config: &NeuromorphicConfig,
) -> NdimageResult<Array3<f64>>
where
T: Float + FromPrimitive + Copy,
{
let (height, width) = image.dim();
let mut temporal_patterns = Array3::zeros((time_window, height, width));
for t in 0..time_window {
let temporal_weight = (-(t as f64) / config.tau_synaptic).exp();
for y in 0..height {
for x in 0..width {
let intensity = image[(y, x)].to_f64().unwrap_or(0.0);
temporal_patterns[(t, y, x)] = intensity * temporal_weight;
}
}
}
Ok(temporal_patterns)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_neuromorphic_config_default() {
let config = NeuromorphicConfig::default();
assert_eq!(config.tau_membrane, 20.0);
assert_eq!(config.tau_synaptic, 5.0);
assert_eq!(config.spike_threshold, 1.0);
assert_eq!(config.refractory_period, 2);
assert_eq!(config.learning_rate, 0.01);
}
#[test]
fn test_spiking_neuron_default() {
let neuron = SpikingNeuron::default();
assert_eq!(neuron.membrane_potential, 0.0);
assert_eq!(neuron.synaptic_current, 0.0);
assert_eq!(neuron.time_since_spike, 0);
assert_eq!(neuron.target_rate, 0.1);
}
#[test]
fn test_event_driven_processing() {
let current =
Array2::from_shape_vec((3, 3), vec![0.0, 1.0, 0.0, 1.0, 0.5, 1.0, 0.0, 1.0, 0.0])
.expect("Test: operation failed");
let previous =
Array2::from_shape_vec((3, 3), vec![0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 0.0, 0.5, 0.0])
.expect("Test: operation failed");
let config = NeuromorphicConfig::default();
let result = event_driven_processing(current.view(), Some(previous.view()), &config)
.expect("Operation failed");
assert_eq!(result.0.dim(), (3, 3));
assert!(!result.1.is_empty()); }
#[test]
fn test_homeostatic_adaptive_filter() {
let image = Array2::from_shape_vec((5, 5), (0..25).map(|x| x as f64 / 25.0).collect())
.expect("Operation failed");
let config = NeuromorphicConfig::default();
let result =
homeostatic_adaptive_filter(image.view(), &config, 5).expect("Operation failed");
assert_eq!(result.dim(), (5, 5));
assert!(result.iter().all(|&x| x.is_finite()));
}
#[test]
fn test_temporal_coding_feature_extraction() {
let image = Array2::from_shape_vec((4, 4), (0..16).map(|x| x as f64 / 16.0).collect())
.expect("Operation failed");
let edge_detector = Array2::from_shape_vec(
(3, 3),
vec![-1.0, -1.0, -1.0, -1.0, 8.0, -1.0, -1.0, -1.0, -1.0],
)
.expect("Test: operation failed");
let detectors = vec![edge_detector];
let config = NeuromorphicConfig::default();
let result = temporal_coding_feature_extraction(image.view(), &detectors, &config, 10)
.expect("Operation failed");
assert_eq!(result.dim(), (1, 4, 4));
assert!(result.iter().all(|&x| x.is_finite()));
}
#[test]
fn test_stdp_unsupervised_learning() {
let trainingimage =
Array2::from_shape_vec((6, 6), (0..36).map(|x| x as f64 / 36.0).collect())
.expect("Operation failed");
let trainingimages = vec![trainingimage.view()];
let config = NeuromorphicConfig::default();
let learned_filter = stdp_unsupervised_learning(&trainingimages, (3, 3), &config, 2)
.expect("Operation failed");
assert_eq!(learned_filter.dim(), (3, 3));
assert!(learned_filter.iter().all(|&x| x.is_finite()));
}
}