Restore the max_tau coincidence-window cap (inert since 0.8.0) - #89
Open
syncytium2 wants to merge 1 commit into
Open
Restore the max_tau coincidence-window cap (inert since 0.8.0)#89syncytium2 wants to merge 1 commit into
max_tau coincidence-window cap (inert since 0.8.0)#89syncytium2 wants to merge 1 commit into
Conversation
Since 0.8.0, max_tau has had no effect on any pair of spikes that each have a neighbour on both sides in their own train. get_tau seeds the four neighbouring ISI slots with max_tau, and each slot is overwritten as soon as that neighbour exists; all four are overwritten exactly when the pair is interior to both trains, and the cap is then never compared against the window. Under MRTS > 0 a seeded slot arriving as Interpolate's first argument is not a bound either, so the cap leaks at some edge spikes as well. Inside get_tau the parameter named max_tau carries true_max -- the recording span, or twice the user's cap when that doubled value is smaller -- so the bound to apply is half of it. No max_tau > 0 guard is needed: when the user passes 0 or None the callers set true_max to the recording span, and bounding the window at half the span is what 0.7.0 did, since it seeded m with interval before halving. At MRTS = 0 the patched function and 0.7.0's are the same function. Two returns per backend, plus test/test_max_tau.py, which pins the case the suite could not previously express: 56 tests over 13 collecting files pass with this applied, on both the compiled Cython and the pure-Python backend, against 50 over 12 without it.
syncytium2
force-pushed
the
restore-max-tau-cap
branch
from
September 1, 2026 13:36
77f5b73 to
c4b23c1
Compare
syncytium2
pushed a commit
to syncytium2/bugarach
that referenced
this pull request
Sep 2, 2026
…ey do The PR is open at mariomulansky/PySpike#89 and Tony sent the apology, so the last step of the filing todo comes due: every place in this tree that asserts PySpike's max_tau cap is broken now also says where the fix went. FOUNDATIONS, the README twice, SAP003's message, detectors/__init__, the sapper_feedback table, the methodology-narrative todo, and the NOTE in test_sync_detect.py. The last one is the interesting one -- it now names test_pyspike_max_tau_is_still_inert as the tripwire and points at the todo for what else to change, so the day a release ships the fix, the suite goes red and the reader is one hop from the inventory rather than grepping for it. Each edit was applied by exact match with a loud failure on drift, not by regex, because SAP003's message is a Python string literal and the README rows are markdown tables -- a sloppy substitution there is silent. Two suite failures on this branch, and NEITHER is from this work: stashing the changes reproduces both. test_architectures_are_files (2 failures, worse without these changes) and test_lab_server::test_the_server_hands_out_the_page_with_the_shim. Both arrived with work that landed on main after this branch pointed off it. 1,566 passed, sapper clear. WHAT THIS RECORDS ABOUT THE PRIVATE MAIL, because the tree should not have to be reconstructed from a chat log. The PR went out with Kreuz's correspondence quoted in the description. The murderboard flagged it twice as a residual and it was treated as closed by an instruction to open the PR -- which was not enough, because the words were his and not ours to release. Quotes were live a little over an hour, drew nothing, and are gone from the body and the commit. They are not retracted: GitHub keeps the PR body's edit history and 77f5b73 is still fetchable by hash from the fork AND from upstream. The apology said so plainly after a first draft claimed the rewritten commit had made it disappear -- that was checked and false. An apology that overstates the remedy is worse than none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
syncytium2
added a commit
to syncytium2/bugarach
that referenced
this pull request
Sep 2, 2026
…intainer uses (#426) * Kreuz reproduced it himself, endorsed the patch, and asked for the PR The note Tony sent on 2026-08-28 asked Thomas Kreuz the one question that decided the shape of this report: is a hard tau_max still the semantics PySpike should have? He answered in three days, with Mario Mulansky cc'd from the first line, and every branch this file was hedging against is now closed. He reproduced the bug on his own two trains before reading our fixture, got cSPIKE 0.5 at max_dist=0.25 and 2/3 at 0.35 against PySpike's 5/6 for both, and named 0.8.0 as where tau_max stopped being tracked -- the same release the report bisected to. He endorsed the patch as written. He settled the semantics: the group's philosophy is to offer variants rather than impose one, so the parameter-free measure stays the default and tau_max is an option that must work. That also disposes of the complication the 2026-08-23 check raised against ourselves. His review carries neither tau_max nor MRTS because the editors wanted ideas and applications, not details -- and he points at Fig. 11 of Mariani et al. 2025 as the explicit demonstration of what the parameter changes. Asking rather than asserting surfaced the objection before a maintainer could and returned a citation we did not have. Re-verified today rather than quoted from the file: upstream's suite is 50 green shipped, patched pure-Python and patched compiled; the sweep and the 7.7 s pair reproduce; and the patched build returns 0.500000 and 0.666667 on Kreuz's example against his 0.5 and 2/3. Patched upstream code landing on the reference implementation's own numbers, on an example neither of us chose. Kreuz redirected step 2 from an issue to a PR, so Route records what that costs and what the description still needs -- his example as the opener, that agreement row, a regression test upstream's suite cannot currently express, and a murderboard pass over the new text. Nothing has been pushed outside this repo; the fork does not exist and external communication is still Tony's to release. His reply also describes staffing and unreleased work. None of that is quoted here or anywhere in this public repo, and it should stay that way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Re-vendor the murderboard to 564b944 so its own gate stops refusing The murderboard skill's freshness check is a hard gate and it was red: vendored at f62acb3 against upstream 564b944. It fired the moment a PySpike PR description was handed to it, which is the gate working -- a review run against a stale process omits rules already paid for, and reports coverage it did not have. Bumped by tools/murderboard_revendor.py, which reports "body changed: none" for all five files. Upstream's one intervening commit replaced a silent-until- something-happens change alert with a daily heartbeat, in a workflow this repo does not vendor. So no rule moved and the review that follows is against the same process text; only the stamp was wrong. Left alone deliberately, and worth someone's attention: .claude/hooks/require-commit-before-message.sh claims murderboard provenance at fae0eca, is absent from .murderboard-vendor.json, and has no counterpart anywhere in the upstream tree. So it is never re-copied, never bumped, and the freshness gate only mentions it in passing. Not touched here -- adding a file to a vendor family, or dropping a stamp that names a repo that does not carry it, is its own decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The patch we were about to send could not be applied by the tool a maintainer uses The murderboard ran eleven roles over the PySpike PR description and the first thing it cost us was the patch itself. docs/pyspike_max_tau.patch was written by hand, and its python_backend.py hunk was missing the three trailing blank context lines its own header declared. patch(1) shrugs at that; git apply calls it "corrupt patch" and refuses the file outright. So tools/pyspike_patch_check.sh had been green for weeks on a patch Mulansky's own tooling would have rejected -- the harness that existed to prove the patch applies was checking it with the one tool that could not see the defect. Two of the three reviewers who found it prescribed the same wrong repair -- change the header from 11 to 8 -- because both read the diff instead of running it. That fix makes BOTH tools reject it. The patch is now generated by git diff, so its headers are canonical by construction, and it carries test/test_max_tau.py as a new-file hunk, so one file reproduces the whole PR. The harness gates git apply AND patch(1), and asserts the suite counts instead of printing them. WHAT ELSE THE ROLES FOUND, in the order it matters: The opening sentence was false. "No effect on any spike interior to its own train" is refuted four lines into a REPL -- a spike interior to its own train, paired against an edge spike of the other, still sees the cap, because the four slots are seeded two per train. The document said so correctly ninety lines later. It is now stated once, correctly, at the top. The bug is bigger than we said. Under MRTS > 0 the seeded slot enters Interpolate as an argument rather than as an outer bound, so the cap is inert at the edges too -- 201 leaks in 400k random pairs at max_tau=0.05, gaps up to 0.96 s, none after the patch. That widens the claim and costs nothing to state. The open design question was not open. Kreuz et al. 2017 section 3.3 introduces tau_max alongside the adaptive window -- "We still use the adaptive coincidence detection from Eq. 1 but define a maximum coincidence window" -- and Satuvuori's Eqs. 17-18 already pair MRTS with its own ISI ceiling. Since min commutes, inside-vs-outside placement is not a choice at all. Asking Mulansky to adjudicate a question his own paper settles was the one place the draft read as under-researched. It now states the answer with the citations. An undisclosed behaviour change. A working cap can filter a train to empty, and 0.9.0 raises ZeroDivisionError on spike_directionality of an empty train while spike_sync returns 1.0. The crash is upstream's, not ours, but the patch makes it reachable from filter_by_spike_sync -- a call site the PR itself lists. Disclosed, with an offer to file it separately. We overstated a figure in Kreuz's own paper. Fig. 11 of Mariani et al. 2025 was described as the gerbil dataset with and without a 2.5 ms cap; its caption says subplot B was modified in two ways, the second being a Spike Train Order threshold. The quotes were verbatim and the framing was not, in the row the draft called its strongest, in a paper the recipient's collaborator co-authored. Monotonicity was claimed too broadly -- true for SPIKE-Sync, false for spike_directionality, which changes sign on the same trains. "Exact ties are unaffected" was a non-sequitur; ties do flip, correctly, matching cSPIKE's strict inequality, which is a win the sentence was throwing away. And the independence of our own port was overstated: sync.py computes literally the patched expression, so its agreement is arithmetic, not a second measurement -- the real anchor is the port's 1e-9 parity with cSPIKE output, which was buried in the last section. The test grew from four cases to six and two of its comments were wrong. It now pins the max_tau=0/None no-op and one MRTS>0 leak, and says which of the six fails on shipped code -- five, not six; the no-op invariant holds either way and is there to keep holding. Kreuz's mail is now dated, his endorsement quoted rather than asserted, and the PR says plainly that what he saw was a one-sentence description of the fix, not this diff. Nothing he did not say is attributed to him. Counts, settled by running them rather than reading them: 50 tests over 12 collecting files today, 56 over 13 with this patch. test_auto_thresh.py defines only helpers and collects nothing, which is why "13 files" was wrong. Three reviewers gave three different answers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Stopped the murderboard on flat severity rather than patching into a third round The blind verify pass found four defects that did not exist before the round-one repair -- they were created by fixing round one. That is the process file's own prediction: each round manufactures the surface the next one reviews. Blocking went 4 -> 2 and major 14 -> 6, which is not falling fast enough to justify a third round, and the rule says escalate rather than keep going. So this run is delivered UNCONVERGED, with residuals named, and the decision about what to cut is Tony's. What the blind pass caught, all of it in prose written during the repair: The claim that the cap is "inert everywhere including at the edges" under MRTS was false, and the document contradicted itself 47 lines apart -- it already said Interpolate is bounded above by its second argument, which is exactly why a seed landing there still binds. Verified on shipped 0.9.0: MRTS=8, max_tau=0.05 takes 0.666667 to 0.000000. The cap bites. I had taken a round-one existence proof and promoted it to a universal. The "201 leaked cases, gaps up to 0.96 s" figure came from a role-one search I never reran. A blind reviewer following the same protocol got 74,996 leaks and 2.4573 s. Neither of us can reproduce the other, the document gave no generator or seed, so the number is out and a runnable four-line case is in its place. The advice on how to run the suite was wrong in both directions, and worse than that, it is not stable: from test/, the full suite resolves to the installed package (6 failed) while a single file resolves to the working copy (6 passed). Two expert reviewers reached opposite conclusions because they measured different invocations. Dropped the sentence rather than restate it. optimal_spike_train_sorting returns a different permutation under the patch -- [0,1,2,3,4] shipped becomes [1,2,4,0,3] at a 0.25 cap -- and at a tight cap the directionality matrix underneath goes all zero, so the ordering handed back is arbitrary while still looking meaningful. That is a fourth behaviour change, and unlike the others it alters an output people publish. Kreuz et al. 2017 introduces tau_max in section 2.1 and states it plainly in Appendix B, not in section 3.3 where the draft attributed it. The quote was verbatim; the location was wrong, in a paper the recipient's collaborator wrote. And his review is no longer in press -- published 2026-07-13, Biol Cybern 120 art. 21, doi 10.1007/s00422-026-01045-5, verified against Crossref. Also fixed: MRTS expanded at first use, Reconcile defined, the MRTS-override section no longer depends on equation numbers that live inside a collapsed block, the 4.5x impact number promoted out of one, the history corrected (the pure-Python copy lost its clamp independently, so this was not one edit to one consolidated function), the elided MRTS /= 4. marked, "always downward" scoped to spike_sync, and the test's np.random replaced with literal trains since no other test in upstream's suite uses randomness. Final state, all gated: git apply and patch(1) both accept the patch, 50 tests over 12 collecting files shipped, 56 over 13 patched on both backends, five of the six new tests fail on shipped and the sixth is the no-op invariant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fifteen reviewers said we had never run cSPIKE. The evidence was in tests/ Tony, one line: "we pounded the shit out of cspike in matlab". He is right, and the review's single largest residual was wrong. Eleven murderboard roles and four blind reviewers all reported that cSPIKE had never been executed here, and the PR conceded it in as many words -- "I have not run cSPIKE". That concession was the weakest sentence in the document and it was false. tools/matlab_ref/gen_ref_sync.m drives cSPIKE's own SpikyRun and computeAdaptiveProfile under MATLAB R2025b and dumps the raw per-spike profile; tests/test_sync_detect.py holds the port to that output at rtol=atol=1e-9 over all 2670 points, at a 0.25 s cap, a 0.5 s cap and uncapped, on both streams. That is 10,680 per-spike values checked against the reference implementation AT FINITE CAPS -- which is precisely the regime this PR is about. So the argument's weakest link was actually its strongest, and the PR was underselling it to the one reader best placed to notice. The Environment section now leads with what was run, the downstream section names cSPIKE as the independent anchor instead of hedging, and the max_dist row cites the C++ we hold: if (|dt| < TAUij) guarding if (((max_dist < 0) || (|dt| < max_dist)) && ...), with max_dist < 0 as the disabled sentinel. Three more public permalinks, all resolving, all pinned at 6eafdb6. WHY EVERY ROLE MISSED IT, because the shape will repeat. They all read test_sync_detect.py -- several cited it -- and none followed it back to the generator that made its fixture. They accepted the draft's own concession instead of testing it. The process file already has this rule and points it at analysis claims: "'the breakdown is unavailable' is a claim, and it is checked like any other... unverified unavailability is the most comfortable finding in a review and the least often true." It fired here against the review rather than against the artifact. A conceded weakness is a claim. Still open on the cSPIKE line, and now the only part: Kreuz's two six-spike figures were quoted from his mail, not reproduced here, and no cSPIKE version string exists in the checkout or in tools/matlab_ref/README.md. Line numbers in Spiketrains.cpp are version-dependent, so that is worth stamping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * "Here" was doing all the work, and a boundary is not a hole Tony, on the residual that said cSPIKE had never been run here: "there's no way you could run cspike from 'here'. you need the matlab team for that (or should)." The observation was true and the framing was wrong, and I got the correction wrong in the other direction before this. Fifteen reviewers reported that cSPIKE had not been executed in this package. Correct. It is not supposed to be. cSPIKE runs on the MATLAB side; the Python port consumes what that side emits. Filing the division of labour as missing evidence was the review's error and then mine -- and my first fix overcorrected into "the review was wrong", which reads as though the reviewers had missed a run that happened in Python. They had not. Nothing was missing at all. This repo already draws that line in the other direction and enforces it: the export folder is the input, the store is closed, the consumer does not reach back into the producer. A reviewer without the pipeline's shape reads every boundary as a hole. The fix is never to pull the producer's tool into the consumer; it is to say which side runs it and link what it emits. So the PR's Environment section now opens by naming the boundary rather than apologising for it: cSPIKE runs under MATLAB R2025b through its own SpikyRun and computeAdaptiveProfile, gen_ref_sync.m is the generator, ref_sync_synth.json the committed output, and test_sync_detect.py holds the port to it at 1e-9 across 2670 per-spike values per condition at 0.25 s, 0.5 s and uncapped on both streams. Kreuz's two six-spike figures stay attributed to his mail, because they are his and were not re-run. The residual is withdrawn rather than closed -- it was never a residual. What survives it is much smaller and belongs upstream of this repo: no cSPIKE version string exists in the checkout or in tools/matlab_ref/README.md, and Spiketrains.cpp line numbers move between versions, so the provenance table's citation is unstamped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Draft the apology to Kreuz, and correct it before it went out The PySpike PR opened with his private mail quoted in the description. It was removed about an hour later, but the first draft of this note told him the rewritten commit message meant "the earlier version is gone". It is not: 77f5b73 is still fetchable by hash from both the fork and mariomulansky/PySpike, because the PR ref keeps it reachable, and the description carries a public edit history besides. An apology that overstates the remedy is worse than none, so the note now says plainly that the original is not retracted, only no longer on display. Also trimmed the PR description to 764 words from ~3,600 on Tony every word after the finding and the fix was ours to justify and most of it could not be. No correspondence remains in the body or the commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Eight places said the bug was live and none said it was filed. Now they do The PR is open at mariomulansky/PySpike#89 and Tony sent the apology, so the last step of the filing todo comes due: every place in this tree that asserts PySpike's max_tau cap is broken now also says where the fix went. FOUNDATIONS, the README twice, SAP003's message, detectors/__init__, the sapper_feedback table, the methodology-narrative todo, and the NOTE in test_sync_detect.py. The last one is the interesting one -- it now names test_pyspike_max_tau_is_still_inert as the tripwire and points at the todo for what else to change, so the day a release ships the fix, the suite goes red and the reader is one hop from the inventory rather than grepping for it. Each edit was applied by exact match with a loud failure on drift, not by regex, because SAP003's message is a Python string literal and the README rows are markdown tables -- a sloppy substitution there is silent. Two suite failures on this branch, and NEITHER is from this work: stashing the changes reproduces both. test_architectures_are_files (2 failures, worse without these changes) and test_lab_server::test_the_server_hands_out_the_page_with_the_shim. Both arrived with work that landed on main after this branch pointed off it. 1,566 passed, sapper clear. WHAT THIS RECORDS ABOUT THE PRIVATE MAIL, because the tree should not have to be reconstructed from a chat log. The PR went out with Kreuz's correspondence quoted in the description. The murderboard flagged it twice as a residual and it was treated as closed by an instruction to open the PR -- which was not enough, because the words were his and not ours to release. Quotes were live a little over an hour, drew nothing, and are gone from the body and the commit. They are not retracted: GitHub keeps the PR body's edit history and 77f5b73 is still fetchable by hash from the fork AND from upstream. The apology said so plainly after a first draft claimed the rewritten commit had made it disappear -- that was checked and false. An apology that overstates the remedy is worse than none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Mark the filing todo done, and name what a fresh session must not redo Steps 3 and 4 are complete -- PySpike#89 is open and every place that asserted the bug now points at it. What is left belongs to other files, so this one says so rather than implying work remains here. Three things a session arriving after a compaction would otherwise rediscover the hard way: PR #426 is 58 commits behind main and its murderboard conflicts are SPURIOUS (both sides stamp 564b944, take either); the two failing tests on this branch are not from this work and survive a merge of main; and the murderboard already ran and stopped unconverged, so re-running it burns a fan-out to reproduce a record that is already in docs/reviews/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Kreuz cleared the quotes and the PR stays trimmed anyway, so nothing here is half-repaired Thomas Kreuz replied to the apology on 2026-09-02: the quoted passages were "scientific and factual content", he does not mind them being public, and he offered to have the full description restored. The harm the apology was for did not land. Tony's call was to leave PySpike#89 exactly as it is, and the todo now carries the reason a later session would otherwise miss: the 2026-09-01 edit had TWO motives and only one was Kreuz's to lift. It removed his quotes AND cut 4,027 words to 764 on Tony's "just the facts". Clearance disposes of the first; the editorial judgement about what a merge reviewer should have to read still stands. Without that written down, the next session reads the apology narrative, finds permission granted, and restores quotes nobody asked for. Three things recorded so they are not re-derived: exactly which three passages were redacted (nothing about his staffing or unreleased work ever reached the PR body); the one GraphQL query that prints either revision, since neither is in this repo and the original exists only in GitHub's edit history; and that the standing rule is UNCHANGED -- asking afterwards and being told it was fine is luck, not process. Also fixes the header, which still said step 4 was outstanding after dd24380 marked it done, and drafts the courtesy reply. The reply is deliberately not murderboarded and says so in its own first line -- five sentences, no technical claim, no promise about what is retrievable, which is where the apology draft went wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Three tests fail in every worktree, and the check that clears your branch cannot see why main was never red. The three failures this branch reported as "not from this work, main likely carries them too" are the shared venv: .venv holds one editable install of the PRIMARY checkout, so import bugarach resolves there from every worktree, and the tests that compare a file in the tree against imported behaviour cannot pass. test_architectures_are_files writes a probe architecture into this worktree's src/ and asks the primary's ARCHITECTURES to have autoloaded it; test_lab_server asserts the served page equals this worktree's raster_viewer.html. PYTHONPATH=src, same worktree, same venv, same commit: 25 passed, 0 failed. WHY THIS IS WORTH A COMMIT AND NOT A LINE. The wrong conclusion was reached by a sound method. The session stashed its changes, saw the failures persist, and inferred they predate the branch -- which is normally proof. Here it proves nothing and CANNOT: no content change to the worktree moves a test that is reading another checkout, so stashing, reverting and bisecting all return the same answer for the same reason. The defect does not merely hide, it defeats the standard way of diagnosing it, and hands you evidence for the wrong answer. THE DEFECT WAS ALREADY FILED, INDEXED AND HANDED OFF, AND I RE-DERIVED IT ANYWAY -- 2026-08-28, three fixes written up, none chosen. I wrote a duplicate todo before checking INDEX.md, which is the exact failure INDEX.md was created on 2026-08-30 to stop. The duplicate is deleted; what is left is the part that was genuinely missing. The named tests are now keywords on the INDEX row and a dated section in the todo that owns the problem, so the next session greps a test name and lands on the answer instead of on a fresh investigation. The fix decision stays open and stays Tony's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The skip count moves too, so the silent case has a tell after all Full suite, same worktree, same commit, differing only in PYTHONPATH: 1689 passed / 46 skipped / 3 failed bare, against 1705 / 33 / 0 with src pinned. Thirteen tests change their minds about whether to run at all. That matters more than the three failures, which announce themselves. A skip is a decision the imported package takes part in, so the count moves whenever the wrong src is loaded -- INCLUDING the case this todo calls the dangerous one, where a worktree edits src/, nothing fails, and the run reports green against a tree that is not the branch's. A skip count that does not match the last known-good run for that tree is a cheap canary, and unlike the failures it is available before anyone has been misled. Measured today while confirming main was not red: 1705 passed, 33 skipped, 1 xfailed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: richard defazio <defazio@umich.edu> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Since 0.8.0,
max_tauhas no effect on any pair of spikes that each have a neighbour on both sides in their own train.The six pairs are 0, 0.1, 0.2, 0.3, 0.4, 0.5 s apart. A 0.25 s cap should admit three of them, a 0.35 s cap four. PySpike returns 5/6 for both, and for every positive cap down to 1 µs. The docstring, in all fourteen copies, says
max_taubounds the window.Cause
get_tauseeds the four neighbouring-ISI slots withmax_tauand overwrites each one as soon as that neighbour exists. All four are overwritten exactly when the pair is interior to both trains, and the cap is then never compared against the window:0.7.0 ended
get_tauwithif max_tau > 0.0: m = fmin(m, max_tau), in each of the three.pyxcopies and inpython_backend.py. 0.8.0 consolidated the Cython side into one shared implementation without it, and the pure-Python copy lost it too.This is not only SPIKE-Sync:
get_tauhas 14 call sites across the three.pyxfiles and 8 more in the pure-Python backend, sospike_directionality,spike_train_order,filter_by_spike_syncandoptimal_spike_train_sortingare all affected.The fix
get_taureceivestrue_max— the span, or twice the user's cap when smaller — so the bound is half of it.Same two returns in
python_backend.pywith the builtinmin. Between them that is every caller on both backends —directionality_python_backend.pyimportsget_taufrompython_backend.No
max_tau > 0guard needed: at0/Nonethe callers settrue_maxto the span, so the bound is half the span, which is what 0.7.0 did by seedingmwithintervalbefore halving. AtMRTS = 0the patched function and 0.7.0's are identical.What changes
Nothing at
max_tauof0/Nonewith defaultReconcile— checked over the suite and ~12,600 shipped-vs-patched probes on both backends. Four things do change, all of them the cap working:|Δt| == max_tauis no longer a coincidence, matching cSPIKE's strict<and 0.7.0.Reconcile=False: a half-ISI can exceed the span, and 0.9.0 returns it where this bounds it at half the span. Fuzzing 3,000 pairs, 23spike_syncvalues moved, always down.test_reconcile.pypasses either way.filter_by_spike_syncwith a tight cap returns empty trains more readily. Empty trains already breakspike_directionalityon 0.9.0 (ZeroDivisionError) — pre-existing, happy to file separately.optimal_spike_train_sortingcan return a different permutation; at a very tight cap the directionality matrix is all zero, so the ordering is arbitrary.Under
MRTS > 0the cap now also overrides an MRTS-raised window. That looks right — Kreuz et al. 2017 introduces τmax alongside the adaptive window, and Satuvuori's Eqs. 17–18 already cap MRTS at half the ISI — but it is a behaviour change, so I am flagging it.Tests
test/test_max_tau.pyis new, because nothing in the suite passesmax_taufor a pair interior to both trains. The one existing assertion (test_distance.py:184) uses a one-spike partner, which leaves two slots seeded and passes either way; it is untouched and still green.max_tau(s)Six tests: that staircase, the profile at 0.25 s, the bound reaching
spike_directionality, strict increase,0/Nonestill a no-op, and oneMRTS > 0case. Five fail as shipped and pass patched on both backends; the sixth is the no-op invariant.56 tests over 13 collecting files, against 50 over 12 today.
Verified on PySpike 0.9.0, NumPy 2.5.2, Python 3.14.5, macOS only.