Repository navigation
Fix disabled refractoriness gate in swarmsplitter.check_CCG - #1043
Conversation
The guard added in v4.1.5 reads len(st2 == 0), which is the length of a boolean array and therefore truthy for every non-empty spike train. As a result check_CCG returned (False, False) on all real inputs and the refractory-CCG veto in swarmsplitter.split (via refractoriness()) has been dead code in v4.1.5 through v4.1.7: splits were decided purely by modularity and bimodality, allowing oversplits of units whose halves remain mutually refractory. Fixes MouseLand#1042. Also move the empty-train check above the compute_CCG call: compute_CCG raises ValueError on .max() of an empty array, so the previous guard could never reach its empty-array case anyway. Verified against kilosort 4.1.7: a simulated refractory unit (3 ms refractory period, ~10 Hz, 20 min) split in half yields R12=0.0, Q12=0.0 (cross_refractory=True under the pre-4.1.5 logic) yet check_CCG returned (False, False) before this fix; after the fix the veto fires again, and check_CCG on an empty second train returns (False, False) instead of raising. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012d2sJAD5EUp4GqAStqoBX7
|
@cindykrafft thanks for tracking this down — I took a pass at independently exercising the fix and the downstream "refractoriness()" behavior. I tested the current behavior, this PR's fix, and a few deliberately incomplete/broken variants across empty inputs, refractory spike trains, and non-refractory controls. The PR version passed all 12 cases; importantly, a variant where "check_CCG()" was fixed but its result was effectively ignored downstream failed the end-to-end regression. One thing that might be useful before landing this is a small regression test covering both levels: def test_check_ccg_empty_inputs(): def test_refractoriness_blocks_refractory_split(): The second assertion seems especially valuable because it protects the actual behavioral contract — a refractory pair should veto the split — rather than only guarding the typo itself. My verification was CPU-only and isolated to these Kilosort modules, not a full GPU sorting run. Thanks again for finding this. |
check_CCG on empty or zero-duration trains returns (False, False) without raising, two halves of one simulated refractory unit are reported as cross-refractory and refractoriness() vetoes their split, and two independent units are not vetoed. With the swarmsplitter.py of v4.1.5 through v4.1.7 the empty-input test raises ValueError inside compute_CCG and the veto test fails (refractoriness returns 0).
|
Thanks for checking it independently, and for the test sketch. I pushed eabed49 with
With this branch all four pass (CPU torch); with the |
|
@cindykrafft thanks for adding these, I independently re-ran your unchanged All four tests pass on CPU. Replacing only The independent-unit control passes too, which protects against accidentally vetoing every split. This addresses the regression coverage I was suggesting; thanks for keeping it focused. Verification boundary: AI-assisted testing of complete source modules and your test file, checked against GitHub's blob hashes. I used a minimal package initializer rather than the full Kilosort package setup, so this does not validate package initialization, the full test suite, or a GPU sorting run. |
grahamfindlay/Kilosort perf/clustering_chunk_size was rebased onto upstream main (4.1.7 plus the swarmsplitter check_CCG fix from MouseLand/Kilosort#1043) and gained validation of clustering_chunk_size in initialize_ops, as requested in the review of PR #1037. Pin moves from d0123d6d9 to c5e98f3e1.
Fixes #1042.
What
swarmsplitter.check_CCG(since v4.1.5) containsst2 == 0is a boolean array, solen(st2 == 0)equalslen(st2)and is truthy for every non-empty spike train. The guard therefore fires on all real inputs andcheck_CCGalways returns(False, False). Downstream,refractoriness()can never return 1, so the refractory-CCG veto inswarmsplitter.split("never split a pair whose cross-correlogram is refractory") has been dead code in v4.1.5–v4.1.7. Splits are currently decided by modularity and bimodality alone, which allows oversplitting of units whose halves remain mutually refractory (bursty, amplitude-varying, or drifting units) — exactly the case the veto was designed to catch. v4.0–v4.1.4 applied the veto.There is a second problem in the same line: the guard sits after the
compute_CCGcall, butcompute_CCGraisesValueErroron.max()of an empty array, so the empty-train case the guard tests for crashes before the guard is reached. Only theT == 0clause could ever usefully trigger (presumably the motivation from the ZeroDivisionError reports, e.g. #1002).Fix
len(st2 == 0)tolen(st2) == 0;compute_CCGcall, keeping theT == 0check after it.Verification (kilosort 4.1.7, CPU)
A single simulated refractory neuron (3 ms refractory period, ~10 Hz, 20 min) split randomly in half — the canonical oversplit the veto should block. Its cross-CCG metrics are
R12 = 0.0000,Q12 = 0.0000(cross_refractory = Trueunder the v4.1.4 logic), yet:Edge cases after the fix:
Standalone reproduction script: https://github.com/cindykrafft/research-software-audit/blob/main/audits/kilosort/verify/kl1_swarmsplitter_gate.py
🤖 Generated with Claude Code
https://claude.ai/code/session_012d2sJAD5EUp4GqAStqoBX7