Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
**/__pycache__
*.pyc
data
logs
dist
build
.venv
3 changes: 3 additions & 0 deletions .github/workflows/black.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Black Formater

on: [push, pull_request]

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Publish to PyPI

on:
release:
types: [published]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Build sdist and wheel
run: |
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
- name: Check release tag matches pyproject version
if: github.event_name == 'release'
run: |
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['tool']['poetry']['version'])")
tag="${GITHUB_REF_NAME#v}"
if [ "$version" != "$tag" ]; then
echo "Release tag $GITHUB_REF_NAME does not match pyproject version $version"
exit 1
fi
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/bindsnet/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
5 changes: 4 additions & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
pull_request:
branches: [ master ]

permissions:
contents: read

jobs:
build:

Expand All @@ -22,7 +25,7 @@ jobs:
python-version: 3.13
- name: Install Poetry
env:
POETRY_VERSION: 2.1.2
POETRY_VERSION: 2.4.3
run: |
curl -sSL https://install.python-poetry.org | python - -y &&\
poetry config virtualenvs.create false
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Python package

on: [push]

permissions:
contents: read

jobs:
build:

Expand All @@ -19,7 +22,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
env:
POETRY_VERSION: 2.1.2
POETRY_VERSION: 2.4.3
run: |
curl -sSL https://install.python-poetry.org | python - -y
- name: Install dependencies
Expand Down
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"type": "python",
"request": "launch",
"program": "${file}",
"python": "/home/hananel/miniconda3/envs/bindsNET/bin/python",
"console": "integratedTerminal",
"justMyCode": false
}
Expand Down
128 changes: 128 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,42 @@ see the [GitHub releases / tags](https://github.com/BindsNET/bindsnet/releases).

## [Unreleased]

## [0.3.4 (PyPI)] - 2026-09-16

First PyPI upload since 0.2.7. It is built from the `master` branch on this date, not
from the GitHub tag `0.3.4`, so `pip install bindsnet==0.3.4` contains everything in
this section **in addition to** the tag. The Zenodo archive
[10.5281/zenodo.20695116](https://doi.org/10.5281/zenodo.20695116) is the tag only.
Results can differ between the two: see the `MCC_learning.PostPre` entry under Changed.

### Packaging
- Published to PyPI by `.github/workflows/publish.yml` (PyPI trusted publishing; runs
when a GitHub Release is published, or by hand).
- Removed install requirements that no module in `bindsnet/` or `examples/` imports:
`Cython`, `scikit-build`, `foolbox`, `numba`.
- `torch` is now `>=2.14,<3` and `torchvision` `>=0.29,<1` instead of exact pins;
`poetry.lock` still pins the tested versions (torch 2.14.0, torchvision 0.29.0).
- README: `pip install bindsnet`, PyPI badge, and absolute links and logo URL so the
PyPI project page renders.
- `pyproject.toml` package metadata moved from `[tool.poetry]` to the standard
`[project]` table (Poetry 2 deprecates the old one); `[tool.poetry]` now only routes
torch/torchvision to the CUDA 13.0 wheel index. `poetry.lock` resolves to the same
packages and versions. Build backend `poetry-core>=2.0`; the unused `setup.py` is removed.
- Poetry 2.4.3 in CI (was 2.1.2) and in `CONTRIBUTING.md` (said 1.1.8; `poetry shell`
replaced by `poetry env activate`, which Poetry 2 uses).
- `Dockerfile` rewritten. The old one could not build: its CUDA 11.1 base image, the
`get-poetry.py` installer and the `.python-version` file it copied no longer exist.
The new one uses `python:3.13-slim`, Poetry 2.4.3 and `poetry.lock`. The README no
longer links the Docker Hub image `hqkhan/bindsnet` (last updated 2019-01-28).
- Remaining links to the old `Hananel-Hazan/bindsnet` repository point to `BindsNET/bindsnet`.

### Tests
- `test_perf_equivalence.py`: the batch-1 checks of the fused `addmm_` STDP update
required bit-for-bit equality with the un-fused formula. That holds on some CPUs
and not on GitHub's CI runners, where 5 tests failed. They now accept float32
rounding (the tolerance already used for batch>1) and warn with the size of any
difference.

### Added
- Reproducibility/transparency docs: `DATA.md` (dataset & stimulus declaration),
`REPRODUCING.md` (model→script→command→seed map), and a
Expand All @@ -14,9 +50,101 @@ see the [GitHub releases / tags](https://github.com/BindsNET/bindsnet/releases).
- `CHANGELOG.md`.
- `examples/breakout/README.md` documenting the `trained_shallow_ANN.pt` provenance.

### Added
- `bindsnet.learning.DiehlAndCook` and `bindsnet.learning.MCC_learning.DiehlAndCook`:
the post-spike-only STDP of Diehl & Cook (2015), Sect. 2.3,
`dw = eta (x_pre - x_tar)(w_max - w)^mu` (keyword arguments `x_tar`, `mu`).
`DiehlAndCook2015(learning_rule=..., learning_rule_kwargs=...)` selects it; the
model's default stays `PostPre` so published results are unchanged.
- Multicompartment `Weight` features forward extra keyword arguments to their
learning rule.
- `bindsnet/learning/README.md`: rules, source papers, equation numbers, tests and
pitfalls (moved from the top-level README).

### Changed
- `MCC_learning.PostPre` no longer multiplies its update by the simulation step
`dt`; like the classic `PostPre` and Morrison et al. (2008) eqs. 13-14 it is a
per-spike increment. Identical at `dt = 1`; at other steps the effective learning
rate is now `nu` instead of `nu * dt`.
- README Python requirement aligned to `>=3.11,<3.14`; added a reproducible-install note.
- `pyproject.toml` version bumped to 0.3.4 to match the released tag.
- Performance pass on the per-timestep hot paths (numerics unchanged; every item
is pinned by `test/network/test_perf_equivalence.py`, and a seeded old-vs-new
comparison of 59 networks was bit-identical except three batch>1 weight
matrices that differ by one float32 rounding step):
- `PostPre` / `Hebbian` on dense `Connection` and `MulticompartmentConnection`
apply the STDP update with one fused `addmm_` instead of materialising the
`[batch, source.n, target.n]` outer product (dense 784->1000 STDP,
batch 16, 250 steps on CPU: 9.6 s -> 0.42 s; Diehl & Cook 784->400,
batch 1: 1.6 s -> 0.26 s).
- `LearningRule.update` no longer multiplies the whole weight matrix by `1.0`
every step when no weight decay is configured.
- `LocalConnection1D/2D/3D` learning rules scale rows directly instead of
building an `[n, n]` identity matrix per step (64-filter local connection on
GPU: 61 MiB -> 2.8 MiB of per-step temporaries). `MSTDP`/`MSTDPET` keep
their post-synaptic trace as a `[batch, n, 1]` vector instead of a diagonal
matrix.
- `MSTDP` / `MSTDPET` cache `exp(-dt / tc)` and the default learning-rate
tensors instead of recomputing / re-copying them to the device each step.
- Neuron models update `v`, `refrac_count`, `theta`, `x`, ... in place with
the same operations in the same order, avoiding a `Module.__setattr__`
round-trip per assignment per step.
- `rank_order` encoding is vectorised.
- Benchmark script for the above: `examples/benchmark/hot_path_bench.py`.
- Learning rules validated against their source papers, with the equations cited in
`docs/source/models_spec.rst` and pinned by `test/network/test_learning_rule_specs.py`:
`PostPre` / `WeightDependentPostPre` / `Hebbian` against Morrison, Diesmann &
Gerstner (2008) eqs. (11)-(14); `MSTDP` / `MSTDPET` against Florian (2007)
eqs. (3.9)-(3.12) and (2.7)-(2.8) (equation numbers added to
`test_mstdp_florian.py`); `Rmax` against Vasilaki et al. (2009) eqs. (7), (8), (13).
The `MCC_learning` `PostPre` / `Hebbian` are checked to match the classic rules.
- Docstrings corrected: `Rmax` `tc_c` limits were stated backwards (`0` is the strict
policy-gradient rule, `inf` the naive Hebbian rule, Vasilaki et al. eq. 8); the
`MSTDP` / `MSTDPET` `zero_lag` comments called the un-lagged variant "exact Florian",
whereas the default one-step lag is Florian's discrete-time eq. (3.9).

### Fixed
- `network.run(clamp=...)` / `unclamp` are now applied inside `Nodes.forward` before
the spike trace is updated, so a forced spike leaves a trace (and a suppressed one
does not). Previously the clamp was applied after the trace update, so clamped
spikes entered the same-step potentiation term of STDP rules but never the trace
used by later depression terms (affected `examples/mnist/supervised_mnist.py`).
Pinned by `TestClampEntersTraces` and the clamp-driven STDP window test.
- `network.to(device)` crashed on any `MulticompartmentConnection` (used by
`DiehlAndCook2015`) with `_apply() takes 2 positional arguments but 3 were
given`; `AbstractMulticompartmentConnection._apply` now accepts `recurse`.
- `Network.clone()` was broken outright: it called `torch.load` without
`weights_only=False`, so it raised `UnpicklingError` under PyTorch 2.6+, which
changed that default to `True`. It had no test and no caller in the tree, so the
breakage went unnoticed. Pinned by `TestNetwork.test_clone`.
- `Network.save()` called `torch.serialization.add_safe_globals([self])` with a
network instance where PyTorch expects a class. It did nothing useful and
corrupted PyTorch's safe-globals registry, so any later load in the same process
failed with `'Network' object has no attribute '__qualname__'`. Removed. Pinned by
`TestNetwork.test_clone_after_save`.
- `bindsnet.conversion.ann_to_snn` and `data_based_normalization` were broken when
given a path instead of a `torch.nn.Module`, for the same PyTorch 2.6 reason as
`Network.clone()`. Only the in-memory form was tested. Pinned by
`test_conversion_from_path` and `test_data_based_normalization_from_path`.

### Security
- Documented that loading a saved network runs code. `bindsnet.network.load`,
`bindsnet.conversion.ann_to_snn` and `bindsnet.conversion.data_based_normalization`
read Python pickle files via `torch.load`, so a file from an untrusted source can
execute arbitrary code on load. This is the standard behaviour of `torch.load`
across the PyTorch ecosystem and is not a defect specific to BindsNET, but it was
undocumented. Added warnings to each function's docstring and a "Loading saved
networks and models" section to `SECURITY.md`.
- `bindsnet.network.load` gained a `weights_only` parameter, passed through to
`torch.load`. It defaults to `False`, which is required to read files written by
`Network.save` (those store the whole network object, not a tensor state dict), so
behaviour is unchanged. `weights_only=True` refuses code execution and is usable
only for files holding plain tensors.
- `SpokenMNIST` now reads its processed-data cache with `weights_only=True`. That
cache holds only tensors, so refusing code execution there costs nothing. Pinned by
`test/datasets/test_cache_serialization.py`.
- Reported by Gavin Branaa <gbranaa4@gmail.com>, who also prompted the three
`torch.load` fixes listed under Fixed above. Thank you.

## [0.3.4] - 2026-06-15

Expand Down
67 changes: 67 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# BindsNET project notes

## Python environment

This project runs in the conda environment named `bindsNET`.

- Interpreter: `/home/hananel/miniconda3/envs/bindsNET/bin/python`
- Activate: `conda activate bindsNET`
- The package is installed in editable mode from this directory, so edits to
`bindsnet/` are picked up without reinstalling.
- Torch 2.14 with CUDA 13.0 is installed there. Do not use the `base` env.

Run tests with:

```shell
conda run -n bindsNET python -m pytest -q
```

Run a single file with:

```shell
conda run -n bindsNET python -m pytest -q test/network/test_learning.py
```

## Committing

Use `sc "message"` instead of `git commit` (see the user's global instructions).

## Learning rules: sources and how to validate

Each rule is pinned to its paper's equations by a from-scratch reference test.
Read these before touching any rule:

| Rule | Paper and equations | Test |
|---|---|---|
| `PostPre`, `WeightDependentPostPre`, `Hebbian` | Morrison, Diesmann & Gerstner 2008, *Biol. Cybern.* 98:459, eqs. 11-14 (traces Sect. 2.3) | `test/network/test_learning_rule_specs.py` |
| `DiehlAndCook` (classic and MCC) | Diehl & Cook 2015, *Front. Comput. Neurosci.* 9:99, Sect. 2.3: post-spike-only, dw = eta (x_pre - x_tar)(w_max - w)^mu | `test/network/test_learning_rule_specs.py` |
| `MSTDP`, `MSTDPET` | Florian 2007, *Neural Comput.* 19:1468, eqs. 3.9-3.12, 2.7-2.8 | `test/network/test_mstdp_florian.py` |
| `Rmax` | Vasilaki et al. 2009, *PLoS Comput. Biol.* 5:e1000586, eqs. 7, 8, 13 | `test/network/test_learning_rule_specs.py` |

Facts that were wrong in docstrings once and are now fixed (do not reintroduce):
- `MSTDP` default `zero_lag=False` **is** Florian's discrete eq. 3.9 (reward at a
step multiplies the previous step's eligibility). `zero_lag=True` is the
un-lagged variant, not "exact Florian".
- `Rmax` `tc_c = 0` is strict policy gradient; `inf` is naive Hebbian.
- `clamp` spikes enter the trace (applied in `Nodes.forward` before the trace update).

- Pair STDP rules carry no `dt` factor (the MCC `PostPre` used to; removed
2026-09-06). `MSTDPET` keeps the paper's `dt` (Florian eq. 2.7).
- `PostPre` is not Diehl & Cook 2015's rule; the paper's rule is `DiehlAndCook`.
`DiehlAndCook2015` keeps `PostPre` as default (published replication); opt in
with `learning_rule=MCC_learning.DiehlAndCook`.

User-facing summary of all this: `bindsnet/learning/README.md` (keep it in sync
with `docs/source/models_spec.rst`).

## Performance changes: the rule

Any change to a per-timestep path must ship with a test that pins it to the
formula it replaced (`test/network/test_perf_equivalence.py`). Before claiming
"no change in results", run the same seeded networks on the old code (a git
worktree of the previous commit) and the new code and compare with
`torch.equal`; only batch>1 summation-order differences (about 1e-7) are
acceptable, and must be stated. Exception: the batch-1 fused `addmm_` STDP update
matches the un-fused formula bit for bit on some CPUs but not on GitHub's CI runners
(max abs diff 1.5e-8 to 3.0e-8, CI run 35124573539, 2026-09-16), so those tests use
the same tolerance and warn with the size. Benchmark: `examples/benchmark/hot_path_bench.py`.
19 changes: 15 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
To clone this project locally, issue

```shell
git clone https://github.com/Hananel-Hazan/bindsnet.git # clones bindsnet repository
git clone https://github.com/BindsNET/bindsnet.git # clones bindsnet repository
```

in the directory of your choice. This will place the repository's code in a directory titled `bindsnet`.

Install the project with [Poetry](https://python-poetry.org/) (current supported version - 1.1.8)
Install the project with [Poetry](https://python-poetry.org/) (version 2.4.3 is used in CI)

```shell
poetry install
poetry run pre-commit install
```


Now you can access the project environment with `poetry shell` or run commands with `poetry run <command>`. For example, `poetry run python examples/mnist/conv_mnist.py`.
Now you can activate the project environment with `eval $(poetry env activate)` or run commands with `poetry run <command>`. For example, `poetry run python examples/mnist/conv_mnist.py`.

Please make sure the `Poetry` environment is activated when you commit your files! The `git commit` command will invoke `pre-commit`, which is installed with Poetry too. IDEs like PyCharm have plugins for `Poetry` and will activate the environment automatically.

Expand All @@ -29,6 +29,17 @@ poetry run pytest
Notable changes are recorded in [`CHANGELOG.md`](CHANGELOG.md); please add an entry to the
`Unreleased` section in your pull request.

## Releasing to PyPI

1. Set `version` in `pyproject.toml` and `CITATION.cff`, and move the `Unreleased`
entries in `CHANGELOG.md` under the new version.
2. Merge to `master`, then publish a GitHub Release whose tag equals that version.
3. `.github/workflows/publish.yml` builds the package, checks that the tag matches the
`pyproject.toml` version, and uploads to PyPI through trusted publishing (no token).
It can also be started by hand from the Actions tab.

A version number can be uploaded to PyPI only once, even after deletion.

All development should take place on a branch separate from master. To create a branch, issue

```shell
Expand Down Expand Up @@ -102,6 +113,6 @@ where `[origin]` is the name of the remote repository, and `[branch-name]` is th

__Note__: See [push.default](https://git-scm.com/docs/git-config#git-config-pushdefault) for more information.

To merge your changes into the `master` branch (the definitive version of the project's code), open a pull request on the [webpage](https://github.com/Hananel-Hazan/bindsnet) of the project. You can select the `base` branch (typically `master`, to merge changes _into_ the definitive version of the code) and the `compare` branch (say, `dan`, if I added a new feature locally and want to add it to the project code). You may add an optional extended description of your pull request changes. If there are merge conflicts at this stage, you may fix these using GitHub's pull request review interface.
To merge your changes into the `master` branch (the definitive version of the project's code), open a pull request on the [webpage](https://github.com/BindsNET/bindsnet) of the project. You can select the `base` branch (typically `master`, to merge changes _into_ the definitive version of the code) and the `compare` branch (say, `dan`, if I added a new feature locally and want to add it to the project code). You may add an optional extended description of your pull request changes. If there are merge conflicts at this stage, you may fix these using GitHub's pull request review interface.

Assign reviewer(s) from the group of project contributors to perform a code review of your pull request. If the reviewer(s) are happy with your changes, you may then merge it in to the `master` branch. _Code review is crucial for the development of this project_, as the whole team should be held accountable for all changes.
Loading
Loading