Skip to content

Releases: pearsonlab/spiking-ven

v0.3.0 - Brian2 parity fixes and dead-knob removal

Choose a tag to compare

@jmxpearson jmxpearson released this 11 Sep 19:56

A code review of the package found seven confirmed bugs, three blocks of surface that read as live but was not, and several instances of the drift the package already had machinery to prevent. Minor rather than patch because the fixes remove public surface and change two Brian2 defaults.

The numpy model and the published metrics are unchanged: K1 7.78 / K2 22.19 / K3 2.85x / K4 2.13x, and a retrained model is bit-identical across all 41 shared npz keys.

Fixed: brian2_runtime.py

The module exists so that values tuned against VocalErrorNetV2 "transfer here without translation". Nothing checked that — the tests built the groups and counted synapse objects without ever running the network, and five defects lived in that gap.

  • The STDP presynaptic trace counted synapses, not spikes. x_i_pre += 1 sat in the JIE synapse's on_pre, but x_i is a neuron-level variable, so it advanced once per outgoing synapse. Measured peak x_i after a single I spike: 61 / 306 / 1225 for N_e = 10 / 50 / 200, against 1.0 in the numpy model — the effective learning rate scaled with the E population, ~60x at the tuned operating point. Now incremented in the I group's own reset.
  • The analytic threshold fallbacks used a superseded formula. theta_e came out at 0.184 against numpy's 0.114 (+61%), because target_rate * tau_s stood in for the tonic drive; theta_i drifted whenever tau_s != 10 ms. Both now match VocalErrorNetV2 exactly.
  • An all-zero weight matrix crashed inside Brian2's index handling with a bare ValueError: zero-size array to reduction operation maximum. Only aud→E was guarded; all four matrices now wire through one helper.
  • noise_i was accepted and silently dropped — the I equation had no noise term at all. The term is now built in only when the amplitude is non-zero, so a noiseless population stays exactly noiseless instead of consuming a random draw per timestep and shifting every other group's stream.
  • tau_s was accepted by build_ven_synapses and unreadable there. It can only take effect via build_ven_groups, so a mismatch now raises instead of being ignored.
  • Conduction delays defaulted to 18/18 ms while the docstring cited 23 ms and the numpy model trains at 23/5. Defaults now come from constants.

tests/test_brian2_parity.py pins every one of these, including a numpy-vs-Brian2 E-rate check.

Fixed: figures

  • The white-noise column and the DAF column's added noise were the same realisation — both seeded seed + 1. Independent now, which moves the DAF column's E rate from 16.7 to 18.7 Hz and leaves the other three columns pixel-identical. The attached figure is re-rendered.
  • make_figure accepted aud_delay_ms/hvc_delay_ms and never read them, and sven-figure wired two flags straight into them. The delays belong to the trained model; the entry point now prints the ones in effect.
  • The output-tag regex used re.sub, which returns its input unchanged on no match: any model not named *ven_model*.npz produced a tag containing the whole path, and a figure path with a directory inside the basename.
  • Both figure modules ran os.makedirs("outputs") at import, in the process working directory.
  • A DAF window past the end of the motif silently produced a DAF column identical to the training column; it now raises.

Removed

xe_th was computed, passed into the simulation loop, and never read. So r_e_th did nothing despite having a CLI flag, and alpha_bcm maintained a theta_bcm that never influenced plasticity — while the docstring said it replaced xe_th. Those are gone, along with --r-e-th and the out-of-scope Huber weight regulariser.

The knobs that are implemented but unexercised by the shipped pipeline (heterosynaptic competition, E→I iSTDP, JIE decay, song-target homeostasis, use_mask) are kept, and the class docstring now says they are untested.

Breaking

  • VocalErrorNetV2(...) no longer accepts r_e_th, alpha_bcm, lambda_huber, huber_delta; .theta_bcm is gone. Models saved by earlier versions still load — the extra keys are ignored.
  • sven-train-ven --r-e-th and sven-figure --aud-delay-ms/--hvc-delay-ms are gone (all three were no-ops).
  • build_ven_groups() threshold fallbacks and build_ven_synapses() delay defaults changed values; build_ven_synapses now raises on a tau_s mismatch.
  • compute_encoding_columns() and make_rate_figure() no longer take n_kernels (unused).
  • generate_hvc_spikes() raises when T is too short to hold its bursts, instead of silently dropping them.
  • OlshausenFieldEncoder.fit() raises on a patch matrix passed positionally, which used to run the audio path on patch rows and appear to work.

Also

  • K2/K3 are matched-input-rate comparisons. The encoder path RMS-normalises and then rate-calibrates every stimulus, so DAF_WN_AMPLITUDE divides straight back out: the metrics measure a spectrotemporal mismatch at equal input drive, not a louder stimulus. That is the stronger result, but not what the "DAF" name implies, so it is now stated in evaluate.py, constants.py and the README. The figure's DAF column is a genuine amplitude manipulation and is unaffected.
  • constants.SR / AUD_DELAY_MS / HVC_DELAY_MS were read only by sven-figure; train_ven hardcoded 23/5 and the sample rate. One source now.
  • data/motifs.py re-derived the template rendition instead of calling corpus.highest_rms_index, which exists for that call site; train_encoder opened motifs.npz by hand and re-derived an unused T_song.
  • spike_to_rate runs through lfilter instead of a Python loop over timesteps — bit-identical for bool, float32 and float64 input.
  • Corrected paper reference: the first author is Gong, not Duarte Ortiz, and the paper is now an eLife reviewed preprint (doi:10.7554/eLife.111771.1, 2026).

39 fast tests (was 27), 7 reproduction assertions, lint clean.

v0.2.0 - remove the Smith-Lewicki encoder

Choose a tag to compare

@jmxpearson jmxpearson released this 11 Sep 18:33

Removed

smith_lewicki.py (259 lines) and its three exports — SmithLewickiDictionary, sl_gram, sl_gram_to_spikes.

The Lewicki encoder was ruled out as a VEN front end: its forward/reversed activation correlation is ~0.99 because it is amplitude-invariant, so K4 caps at 1.22× against the Olshausen-Field encoder's 2.11×. It survived here only because finchsim's circuit path still used it, and that path is moving to OF. The README note explaining why it was kept goes with it.

The OF path is unchanged — the figure renders byte-identical to v0.1.1 and all 7 reproduction assertions pass.

Added: of_to_spikes(calibrate_on=...)

The rate scale is mean_rate_hz / act.mean() over the whole array, so handing it a sparse timeline — one song tiled into motif windows with silence between — divides by a mean diluted by the silence and inflates the rate during song. Measured on a 25% duty cycle: 61 Hz against a 15 Hz target. Passing the song-window activations as calibrate_on gives 14.2 Hz. Inert when unused (output identical with calibrate_on=None).

This is needed by the finchsim circuit migration, and is the kind of error that would otherwise have been adopted silently as 'the new AIV drive' rather than noticed.

Also

n_channels's docstring no longer points at a sibling class that no longer exists, and its test is parametrised over 5/7/64 widths instead of comparing two encoder classes.

27 fast tests (was 25), 7 reproduction assertions, lint clean.

v0.1.1 - review cleanups

Choose a tag to compare

@jmxpearson jmxpearson released this 11 Sep 15:07

Quality pass over the whole package. No behaviour change — verified by the figure rendering byte-identical to v0.1.0 (sha256 9ec64bb3…) and sven-evaluate returning bit-identical metrics (K1 7.83237886428833, K4 2.0749239803578408).

API changes

Three public symbols removed, all dead in this package and in finchsim:
of_encode (superseded by coch_encode), build_sl_filterbank and build_sl_spike_group (no callers, and the only pieces here that could not compile to Brian2 standalone).

sven-figure --encoder lewicki is gone. It read outputs/lewicki_motif_filters.npz, which nothing in this package produces — that training code stayed in finchsim — so the option was unusable from a clean clone. The Smith-Lewicki classes remain exported and supported, since finchsim's circuit path uses them; passing one to the figure now raises a clear TypeError.

New

  • constants.py — SR, KERNEL_WIDTH_MS, PEAK_RATE_HZ, DAF_WN_AMPLITUDE, DAF_WINDOW_S, and the two conduction delays. PEAK_RATE was derived in one file and hardcoded as 300.0 in three figure signatures, so changing the kernel width would silently have left the figures behind.
  • corpus.py — Corpus and highest_rms_index(). The template rule ('the loudest rendition is the template') was re-derived in four places; if they had diverged, the figure and the metrics would have described a different motif than training used.
  • n_channels on both encoder classes, replacing a hasattr(enc, 'n_bases') else 64 fallback that would have mis-sized arrays for any dictionary not 64 wide.

Also removed a vestigial np.random.seed() before inference — a leftover from before the network owned a seeded generator. The byte-identical render is the proof it no longer did anything.

Tests

25 fast (was 18) + 7 reproduction assertions, lint clean, make verify clean.

v0.1.0 - spiking auditory encoder and vocal error network

Choose a tag to compare

@jmxpearson jmxpearson released this 11 Sep 13:42

First release of the spiking sparse auditory encoder and vocal error network (VEN) for the zebra finch song system — the spiking counterpart to the rate-based Wilson–Cowan model in pearsonlab/vocal-error-network (Duarte Ortiz et al. 2025, bioRxiv 2025.07.18.665446).

Extracted from the finchsim circuit model so it can be cited and reused on its own. finchsim now consumes it as a dependency.

What it does

Song audio → gammatone cochleagram → sparse-coded spike train → an excitatory/inhibitory network that learns to cancel the predictable auditory response to the bird's own song, leaving an error signal on novel or perturbed sound.

The result, from outputs/encoding_comparison_k4max.pdf: excitatory-population rates of ~7 Hz on the trained song versus ~18, ~21 and ~17 Hz on the time-reversed motif, white noise, and distorted auditory feedback.

Metrics (Mandelblat-Cerf et al. 2014, Fig. 7/8)

Metric This release Biological target
encoder forward/reversed correlation 0.0036 near zero
K1 — correct song + HVC 7.78 Hz mean 7.7 Hz, SD 8.7
K2 — white noise (DAF) + HVC 22.19 Hz pop. ~16 Hz; responders ~28 Hz
K3 — K2/K1 2.85× pop. ~2.1×; responders ~3.6×
K4 — reversed / correct 2.13× > 1×

The model is a responder-only population, so the responder-only targets are the relevant ones. make test-repro asserts all of these against the attached model.

Reproducing

uv sync
make figure   # corpus -> encoder -> network -> figure, ~10 min on CPU
make verify   # check artifacts against MANIFEST.sha256

Fully seeded end to end: re-running reproduces the numbers rather than landing nearby. The encoder's dictionary learning and the network's simulation noise are both driven by explicit generators.

Dependencies

Core install is numpy and scipy only — a fresh uv sync brings in three packages. Brian2, soundfile and matplotlib sit behind the brian2, data and plots extras. import spiking_ven never pulls in Brian2. No PyTorch, numba, seaborn or scikit-learn.

The ERB/gammatone filterbank is vendored (from PyPI Gammatone 1.0.3, BSD-3-Clause, upstream archived 2024) rather than depended on, copied byte-for-byte and verified bit-identical to the released package. See THIRD_PARTY_NOTICES.md.

Attached artifacts

File What
motifs.npz 38 DTW-aligned R469 motifs @ 16 kHz
of_encoder.npz trained Olshausen-Field sparse encoder (64 bases)
of_ven_model_k4max.npz trained vocal error network
encoding_comparison_k4max.pdf the cancellation figure

The corpus is derived from "Labeled Zebra Finch Songs" (Koch, Therese), DOI 10.18738/T8/SAWMUN, released under CC0 1.0. Please cite it.

Known limitations

  • The corpus download fails from datacenter IPs (CloudFront returns 403), so CI runs a synthetic-corpus smoke test instead. Use --skip-download with a hand-placed corpus if you are blocked.
  • Checksums in MANIFEST.sha256 record what this build produced; a different platform or BLAS can give different last-bit results and so a different digest without anything being wrong. make test-repro is the authoritative reproduction check.