Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0308b27
SparseConnection support
n-shevko Jan 23, 2025
d1d3e42
Test for SparseConnection
n-shevko Jan 25, 2025
471d455
Sparsity for MulticompartmentConnection
n-shevko Feb 15, 2025
8b1cb4e
Sparse batch_eth_mnist
n-shevko Feb 26, 2025
12d8e03
Sparsity for s monitor
n-shevko Feb 27, 2025
2c0760d
Add batch dimension in case if sparse == True
n-shevko Mar 6, 2025
9678254
Add doc for sparse=True
n-shevko Mar 6, 2025
fd0ef9b
Make --sparse influence Monitors
n-shevko Mar 6, 2025
ec63566
Note about performance
n-shevko Mar 6, 2025
e0f9bf2
Lowering precision for old connections
n-shevko Mar 8, 2025
e9dfcbf
w_dtype for example
n-shevko Mar 8, 2025
bed0623
Make device and batch_size optional
n-shevko Mar 8, 2025
326d270
Add sparse support for other learning rules
n-shevko Mar 8, 2025
95626a9
Sparse tensors for monitors explanation
n-shevko Mar 19, 2025
9ff6f01
Add runtime experiment
n-shevko Apr 14, 2025
3c9528a
Use cuda
n-shevko Apr 15, 2025
717ee17
Make tensor smaller to fit in 7gb of gpu memory
n-shevko Apr 15, 2025
988cb91
Documentation update
n-shevko Apr 15, 2025
88f01a8
Merge pull request #711 from BindsNET/hananel
Hananel-Hazan Jun 20, 2025
85c8cb1
docs: Update README badges
neural-loop Jul 31, 2025
6e938f4
docs: Update neuromorphic badge in README
neural-loop Jul 31, 2025
5833f9d
Merge pull request #712 from neural-loop/collaboration-network
Hananel-Hazan Aug 1, 2025
80ba5e5
Update badges README.md
Hananel-Hazan Aug 1, 2025
a7dc702
Bump jupyterlab from 4.4.3 to 4.4.8
dependabot[bot] Sep 26, 2025
1c8cb82
Merge pull request #720 from BindsNET/dependabot/pip/jupyterlab-4.4.8
Hananel-Hazan Sep 26, 2025
be29c59
Bump pillow from 11.2.1 to 11.3.0
dependabot[bot] Sep 26, 2025
731206e
Merge pull request #721 from BindsNET/dependabot/pip/pillow-11.3.0
Hananel-Hazan Sep 26, 2025
6b6e4b5
Refactor code for improved readability and consistency in tensor hand…
Hananel-Hazan Oct 31, 2025
08f4bd0
Refactor code for improved readability and consistency in weight hand…
Hananel-Hazan Oct 31, 2025
5aceb2e
Docs
n-shevko Nov 6, 2025
2adfcba
Merge branch 'lowering_precision' of https://github.com/BindsNET/bind…
n-shevko Nov 6, 2025
b5c608b
Refactor string formatting for improved readability in lowering_preci…
Hananel-Hazan Nov 7, 2025
4c894b7
Merge pull request #703 from BindsNET/sparse_connection
Hananel-Hazan Nov 7, 2025
f5a716d
Merge branch 'master' into lowering_precision
n-shevko Nov 7, 2025
f19692c
Refactor code for improved readability by formatting arguments in con…
Hananel-Hazan Nov 7, 2025
d072219
Merge pull request #707 from BindsNET/lowering_precision
Hananel-Hazan Nov 7, 2025
fd3b904
Merge branch 'hananel' into master
Hananel-Hazan Nov 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ test/models/__pycache__/*
test/network/__pycache__/*
test/analysis/__pycache__/*
*.pyc
**/*.pyc
dist/*
logs/*
.pytest_cache/*
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ This package is used as part of ongoing research on applying SNNs, machine learn

Check out the [BindsNET examples](https://github.com/BindsNET/bindsnet/tree/master/examples) for a collection of experiments, functions for the analysis of results, plots of experiment outcomes, and more. Documentation for the package can be found [here](https://bindsnet-docs.readthedocs.io).

![Build Status](https://github.com/BindsNET/bindsnet/actions/workflows/python-app.yml/badge.svg?branch=master)
[![CodeQL](https://github.com/BindsNET/bindsnet/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/BindsNET/bindsnet/actions/workflows/github-code-scanning/codeql)
[![Documentation Status](https://readthedocs.org/projects/bindsnet-docs/badge/?version=latest)](https://bindsnet-docs.readthedocs.io/?badge=latest)
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/bindsnet_/community)
[![Neuromorphic Computing](https://img.shields.io/badge/Collaboration_Network-Open_Neuromorphic-blue)](https://open-neuromorphic.org/neuromorphic-computing/)

## Requirements

Expand Down
Binary file removed bindsnet/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file removed bindsnet/__pycache__/utils.cpython-310.pyc
Binary file not shown.
Binary file removed bindsnet/analysis/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed bindsnet/datasets/__pycache__/davis.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed bindsnet/encoding/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 8 additions & 1 deletion bindsnet/evaluation/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ def assign_labels(
indices = torch.nonzero(labels == i).view(-1)

# Compute average firing rates for this label.
selected_spikes = torch.index_select(
spikes, dim=0, index=torch.tensor(indices)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The torch.tensor(indices) call is unnecessary and may cause performance issues or device mismatches. The indices variable is already a tensor on the same device as spikes. It should be used directly: torch.index_select(spikes, dim=0, index=indices).

Suggested change
spikes, dim=0, index=torch.tensor(indices)
spikes, dim=0, index=indices

Copilot uses AI. Check for mistakes.
)
rates[:, i] = alpha * rates[:, i] + (
torch.sum(spikes[indices], 0) / n_labeled
torch.sum(selected_spikes, 0) / n_labeled
)

# Compute proportions of spike activity per class.
Expand Down Expand Up @@ -111,6 +114,8 @@ def all_activity(

# Sum over time dimension (spike ordering doesn't matter).
spikes = spikes.sum(1)
if spikes.is_sparse:
spikes = spikes.to_dense()

rates = torch.zeros((n_samples, n_labels), device=spikes.device)
for i in range(n_labels):
Expand Down Expand Up @@ -152,6 +157,8 @@ def proportion_weighting(

# Sum over time dimension (spike ordering doesn't matter).
spikes = spikes.sum(1)
if spikes.is_sparse:
spikes = spikes.to_dense()

rates = torch.zeros((n_samples, n_labels), device=spikes.device)
for i in range(n_labels):
Expand Down
67 changes: 42 additions & 25 deletions bindsnet/learning/MCC_learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ def update(self, **kwargs) -> None:
if ((self.min is not None) or (self.max is not None)) and not isinstance(
self, NoOp
):
self.feature_value.clamp_(self.min, self.max)
if self.feature_value.is_sparse:
self.feature_value = (
self.feature_value.to_dense().clamp_(self.min, self.max).to_sparse()
)
else:
self.feature_value.clamp_(self.min, self.max)

@abstractmethod
def reset_state_variables(self) -> None:
Expand Down Expand Up @@ -247,10 +252,15 @@ def _connection_update(self, **kwargs) -> None:
torch.mean(self.average_buffer_pre, dim=0) * self.connection.dt
)
else:
self.feature_value -= (
self.reduction(torch.bmm(source_s, target_x), dim=0)
* self.connection.dt
)
if self.feature_value.is_sparse:
self.feature_value -= (
torch.bmm(source_s, target_x) * self.connection.dt
).to_sparse()
Comment on lines +255 to +258
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When self.feature_value.is_sparse is True, the reduction function is skipped but the batch dimension is not reduced. This may result in incorrect tensor shapes. The update should reduce the batch dimension using self.reduction before converting to sparse: self.feature_value -= self.reduction(torch.bmm(source_s, target_x), dim=0).to_sparse() * self.connection.dt.

Copilot uses AI. Check for mistakes.
else:
self.feature_value -= (
self.reduction(torch.bmm(source_s, target_x), dim=0)
* self.connection.dt
)
del source_s, target_x

# Post-synaptic update.
Expand Down Expand Up @@ -278,10 +288,15 @@ def _connection_update(self, **kwargs) -> None:
torch.mean(self.average_buffer_post, dim=0) * self.connection.dt
)
else:
self.feature_value += (
self.reduction(torch.bmm(source_x, target_s), dim=0)
* self.connection.dt
)
if self.feature_value.is_sparse:
self.feature_value += (
torch.bmm(source_x, target_s) * self.connection.dt
).to_sparse()
Comment on lines +293 to +294
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When self.feature_value.is_sparse is True, the reduction function is skipped but the batch dimension is not reduced. This may result in incorrect tensor shapes. The update should reduce the batch dimension using self.reduction before converting to sparse: self.feature_value += self.reduction(torch.bmm(source_x, target_s), dim=0).to_sparse() * self.connection.dt.

Suggested change
torch.bmm(source_x, target_s) * self.connection.dt
).to_sparse()
self.reduction(torch.bmm(source_x, target_s), dim=0).to_sparse() * self.connection.dt
)

Copilot uses AI. Check for mistakes.
else:
self.feature_value += (
self.reduction(torch.bmm(source_x, target_s), dim=0)
* self.connection.dt
)
del source_x, target_s

super().update()
Expand Down Expand Up @@ -508,16 +523,16 @@ def _connection_update(self, **kwargs) -> None:
self.average_buffer_index + 1
) % self.average_update

if self.continues_update:
self.feature_value += self.nu[0] * torch.mean(
self.average_buffer, dim=0
)
elif self.average_buffer_index == 0:
self.feature_value += self.nu[0] * torch.mean(
self.average_buffer, dim=0
)
if self.continues_update or self.average_buffer_index == 0:
update = self.nu[0] * torch.mean(self.average_buffer, dim=0)
if self.feature_value.is_sparse:
update = update.to_sparse()
self.feature_value += update
else:
self.feature_value += self.nu[0] * self.reduction(update, dim=0)
update = self.nu[0] * self.reduction(update, dim=0)
if self.feature_value.is_sparse:
update = update.to_sparse()
self.feature_value += update

# Update P^+ and P^- values.
self.p_plus *= torch.exp(-self.connection.dt / self.tc_plus)
Expand Down Expand Up @@ -686,14 +701,16 @@ def _connection_update(self, **kwargs) -> None:
self.average_buffer_index + 1
) % self.average_update

if self.continues_update:
self.feature_value += torch.mean(self.average_buffer, dim=0)
elif self.average_buffer_index == 0:
self.feature_value += torch.mean(self.average_buffer, dim=0)
if self.continues_update or self.average_buffer_index == 0:
update = torch.mean(self.average_buffer, dim=0)
if self.feature_value.is_sparse:
update = update.to_sparse()
self.feature_value += update
else:
self.feature_value += (
self.nu[0] * self.connection.dt * reward * self.eligibility_trace
)
update = self.nu[0] * self.connection.dt * reward * self.eligibility_trace
if self.feature_value.is_sparse:
update = update.to_sparse()
self.feature_value += update

# Update P^+ and P^- values.
self.p_plus *= torch.exp(-self.connection.dt / self.tc_plus) # Decay
Expand Down
Binary file removed bindsnet/learning/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file removed bindsnet/learning/__pycache__/reward.cpython-310.pyc
Binary file not shown.
37 changes: 28 additions & 9 deletions bindsnet/learning/learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ def update(self) -> None:
(self.connection.wmin != -np.inf).any()
or (self.connection.wmax != np.inf).any()
) and not isinstance(self, NoOp):
self.connection.w.clamp_(self.connection.wmin, self.connection.wmax)
if self.connection.w.is_sparse:
raise Exception("SparseConnection isn't supported for wmin\\wmax")
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception message contains an incorrect escape sequence. The backslash should be properly escaped or the message should use a forward slash. It should be "wmin/wmax" or "wmin\\wmax" instead of "wmin\wmax".

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

else:
self.connection.w.clamp_(self.connection.wmin, self.connection.wmax)


class NoOp(LearningRule):
Expand Down Expand Up @@ -396,7 +399,10 @@ def _connection_update(self, **kwargs) -> None:
if self.nu[0].any():
source_s = self.source.s.view(batch_size, -1).unsqueeze(2).float()
target_x = self.target.x.view(batch_size, -1).unsqueeze(1) * self.nu[0]
self.connection.w -= self.reduction(torch.bmm(source_s, target_x), dim=0)
update = self.reduction(torch.bmm(source_s, target_x), dim=0)
if self.connection.w.is_sparse:
update = update.to_sparse()
self.connection.w -= update
del source_s, target_x

# Post-synaptic update.
Expand All @@ -405,7 +411,10 @@ def _connection_update(self, **kwargs) -> None:
self.target.s.view(batch_size, -1).unsqueeze(1).float() * self.nu[1]
)
source_x = self.source.x.view(batch_size, -1).unsqueeze(2)
self.connection.w += self.reduction(torch.bmm(source_x, target_s), dim=0)
update = self.reduction(torch.bmm(source_x, target_s), dim=0)
if self.connection.w.is_sparse:
update = update.to_sparse()
self.connection.w += update
del source_x, target_s

super().update()
Expand Down Expand Up @@ -1113,10 +1122,14 @@ def _connection_update(self, **kwargs) -> None:

# Pre-synaptic update.
update = self.reduction(torch.bmm(source_s, target_x), dim=0)
if self.connection.w.is_sparse:
update = update.to_sparse()
self.connection.w += self.nu[0] * update

# Post-synaptic update.
update = self.reduction(torch.bmm(source_x, target_s), dim=0)
if self.connection.w.is_sparse:
update = update.to_sparse()
self.connection.w += self.nu[1] * update

super().update()
Expand Down Expand Up @@ -1542,8 +1555,10 @@ def _connection_update(self, **kwargs) -> None:
a_minus = torch.tensor(a_minus, device=self.connection.w.device)

# Compute weight update based on the eligibility value of the past timestep.
update = reward * self.eligibility
self.connection.w += self.nu[0] * self.reduction(update, dim=0)
update = self.reduction(reward * self.eligibility, dim=0)
if self.connection.w.is_sparse:
update = update.to_sparse()
self.connection.w += self.nu[0] * update

# Update P^+ and P^- values.
self.p_plus *= torch.exp(-self.connection.dt / self.tc_plus)
Expand Down Expand Up @@ -2214,10 +2229,11 @@ def _connection_update(self, **kwargs) -> None:
self.eligibility_trace *= torch.exp(-self.connection.dt / self.tc_e_trace)
self.eligibility_trace += self.eligibility / self.tc_e_trace

update = self.nu[0] * self.connection.dt * reward * self.eligibility_trace
if self.connection.w.is_sparse:
update = update.to_sparse()
# Compute weight update.
self.connection.w += (
self.nu[0] * self.connection.dt * reward * self.eligibility_trace
)
self.connection.w += update

# Update P^+ and P^- values.
self.p_plus *= torch.exp(-self.connection.dt / self.tc_plus)
Expand Down Expand Up @@ -2936,6 +2952,9 @@ def _connection_update(self, **kwargs) -> None:
) * source_x[:, None]

# Compute weight update.
self.connection.w += self.nu[0] * reward * self.eligibility_trace
update = self.nu[0] * reward * self.eligibility_trace
if self.connection.w.is_sparse:
update = update.to_sparse()
self.connection.w += update

super().update()
Binary file removed bindsnet/models/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file removed bindsnet/models/__pycache__/models.cpython-310.pyc
Binary file not shown.
68 changes: 55 additions & 13 deletions bindsnet/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
import torch
from scipy.spatial.distance import euclidean
from torch.nn.modules.utils import _pair
from torch import device
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'device' is not used.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


from bindsnet.learning import PostPre
from bindsnet.learning.MCC_learning import PostPre as MMCPostPre
from bindsnet.network import Network
from bindsnet.network.nodes import DiehlAndCookNodes, Input, LIFNodes
from bindsnet.network.topology import Connection, LocalConnection
from bindsnet.network.topology import (
Connection,
LocalConnection,
MulticompartmentConnection,
)
from bindsnet.network.topology_features import Weight


class TwoLayerNetwork(Network):
Expand Down Expand Up @@ -94,6 +101,9 @@ class DiehlAndCook2015(Network):
def __init__(
self,
n_inpt: int,
device: str = "cpu",
batch_size: int = None,
sparse: bool = False,
n_neurons: int = 100,
exc: float = 22.5,
inh: float = 17.5,
Expand All @@ -102,6 +112,7 @@ def __init__(
reduction: Optional[callable] = None,
wmin: float = 0.0,
wmax: float = 1.0,
w_dtype: torch.dtype = torch.float32,
norm: float = 78.4,
theta_plus: float = 0.05,
tc_theta_decay: float = 1e7,
Expand All @@ -124,6 +135,7 @@ def __init__(
dimension.
:param wmin: Minimum allowed weight on input to excitatory synapses.
:param wmax: Maximum allowed weight on input to excitatory synapses.
:param w_dtype: Data type for :code:`w` tensor
:param norm: Input to excitatory layer connection weights normalization
constant.
:param theta_plus: On-spike increment of ``DiehlAndCookNodes`` membrane
Expand Down Expand Up @@ -170,27 +182,57 @@ def __init__(

# Connections
w = 0.3 * torch.rand(self.n_inpt, self.n_neurons)
input_exc_conn = Connection(
input_exc_conn = MulticompartmentConnection(
source=input_layer,
target=exc_layer,
w=w,
update_rule=PostPre,
nu=nu,
reduction=reduction,
wmin=wmin,
wmax=wmax,
norm=norm,
device=device,
pipeline=[
Weight(
"weight",
w,
value_dtype=w_dtype,
range=[wmin, wmax],
norm=norm,
reduction=reduction,
nu=nu,
learning_rule=MMCPostPre,
sparse=sparse,
batch_size=batch_size,
)
],
)
w = self.exc * torch.diag(torch.ones(self.n_neurons))
exc_inh_conn = Connection(
source=exc_layer, target=inh_layer, w=w, wmin=0, wmax=self.exc
if sparse:
w = w.unsqueeze(0).expand(batch_size, -1, -1)
exc_inh_conn = MulticompartmentConnection(
source=exc_layer,
target=inh_layer,
device=device,
pipeline=[
Weight(
"weight", w, value_dtype=w_dtype, range=[0, self.exc], sparse=sparse
)
],
)
w = -self.inh * (
torch.ones(self.n_neurons, self.n_neurons)
- torch.diag(torch.ones(self.n_neurons))
)
inh_exc_conn = Connection(
source=inh_layer, target=exc_layer, w=w, wmin=-self.inh, wmax=0
if sparse:
w = w.unsqueeze(0).expand(batch_size, -1, -1)
inh_exc_conn = MulticompartmentConnection(
source=inh_layer,
target=exc_layer,
device=device,
pipeline=[
Weight(
"weight",
w,
value_dtype=w_dtype,
range=[-self.inh, 0],
sparse=sparse,
)
],
)

# Add to network
Expand Down
Binary file removed bindsnet/network/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file removed bindsnet/network/__pycache__/network.cpython-310.pyc
Binary file not shown.
Binary file removed bindsnet/network/__pycache__/nodes.cpython-310.pyc
Binary file not shown.
Binary file not shown.
13 changes: 8 additions & 5 deletions bindsnet/network/monitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(
time: Optional[int] = None,
batch_size: int = 1,
device: str = "cpu",
sparse: Optional[bool] = False,
):
# language=rst
"""
Expand All @@ -62,6 +63,7 @@ def __init__(
self.time = time
self.batch_size = batch_size
self.device = device
self.sparse = sparse

# if time is not specified the monitor variable accumulate the logs
if self.time is None:
Expand Down Expand Up @@ -98,11 +100,12 @@ def record(self) -> None:
for v in self.state_vars:
data = getattr(self.obj, v).unsqueeze(0)
# self.recording[v].append(data.detach().clone().to(self.device))
self.recording[v].append(
torch.empty_like(data, device=self.device, requires_grad=False).copy_(
data, non_blocking=True
)
)
record = torch.empty_like(
data, device=self.device, requires_grad=False
).copy_(data, non_blocking=True)
if self.sparse:
record = record.to_sparse()
self.recording[v].append(record)
# remove the oldest element (first in the list)
if self.time is not None:
self.recording[v].pop(0)
Expand Down
Loading
Loading