Skip to content

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

Latest

Choose a tag to compare

@jmxpearson jmxpearson released this 11 Sep 19:56
· 3 commits to main since this release

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.