pynapple draft · MaxInterval and logISI
Burst detection, R beside Python
Each section puts a piece of the R reference on the left and the Python that answers to it on the right. Coloured bands mark the lines that do the same job. Hover or tap a band, or one of the labels above the code, to light up its partner on the other side.
The Python was written from the published description of each method, with the R read for constants and edge cases, so it is a counterpart and not a line-by-line translation. Where the two are structured differently, the notes under each pair say why the result is the same.
| R | Python | Section |
|---|---|---|
mi.find.bursts, phase 1 | _find_runs | Find runs |
mi.find.bursts, phase 2 | _merge_close_runs | Merge close bursts |
mi.find.bursts, phase 3 | _drop_small_runs, _runs_to_interval_set | Drop small bursts |
logisi.break.calc | _log_isi_histogram | Histogram |
get.peaks | _find_peaks | Peaks |
find.thresh | _log_isi_valley | Threshold |
logisi.pasq.method | detect_bursts_log_isi | Which threshold |
add.brs | _extend_to_related_spikes | Burst-related spikes |
logisi.find.burst | _find_runs and the two phases after it, reused | Where they disagree |
MaxInterval, phase 1: find runs
Both versions walk the spike train once with the same two-state machine. R keeps an in.burst flag and writes rows into a preallocated matrix; Python keeps start (None means not in a burst) and appends (first, last) index pairs. Indices go from 1-based to 0-based.
| Figure 3 | R, mi.par | Python | Default |
|---|---|---|---|
| max ISI at start of burst | beg.isi | max_isi_start | 0.17 s |
| max ISI in burst | end.isi | max_isi_burst | 0.3 s |
| min inter-burst interval | min.ibi | min_inter_burst_interval | 0.2 s |
| min burst duration | min.durn | min_burst_duration | 0.01 s |
| min number of spikes in burst | min.spikes | min_spikes_in_burst | 3 |
R reads these from a global list, mi.par. Python takes them as arguments; the defaults are Table 1 of Cotterill et al. 2016.
- The comparisons are the same: a burst starts on an interval strictly below the start threshold and ends on one strictly above the in-burst threshold.
- R stores each burst's gap to the previous one (
IBI) here for phase 2. Python does not; phase 2 reads the gap from the spike times. - The
max.burstspreallocation and itsbrowser()guard have no counterpart; a Python list grows.
MaxInterval, phase 2: merge close bursts
R marks every burst whose stored gap is below min.ibi, walks those in reverse to copy each end onto the burst before it, then deletes the marked rows. Python builds the merged list front to back: a run that begins too soon after the previous run's last spike extends that run.
- The gap is first spike of this run minus last spike of the previous original run in both versions, so a chain of three or more bursts merges the same way. R needs the reverse walk to get that; the forward build gets it for free.
MaxInterval, phase 3: drop small bursts
R computes a length and a duration column and deletes the rows that fail either test. Python keeps the runs that pass both.
- R returns a matrix with
beg,end,IBI,len,durn,mean.isisandSI. Python returns anIntervalSetfrom first to last spike with one metadata column,n_spikes(R'slen). Duration and inter-burst intervals follow from the intervals themselves. - The same three Python functions serve logISI and the instantaneous-rate detector. In R,
logisi.find.burstis a second copy of this whole function.
logISI: the histogram
The threshold is read off a histogram of the interspike intervals on a logarithmic axis. Both versions work in milliseconds, leave out intervals under 1 ms, and normalise the counts.
CIBP is the intra-burst peak, Cmin the minimum before each later peak.- R's
histcloses bins on the right,(a, b]; NumPy'shistogramcloses them on the left. Python therefore bins withsearchsortedto get the same edges, so an interval sitting exactly on an edge lands in the same bin. - The smoothing line has no Python counterpart. With ten bins per decade the histogram has at most 60 bins for any train whose longest interval is under 1000 s. At
f = 0.05R'slowessthen uses a two-point neighbourhood and returns its input unchanged. Checked in R for 20 to 60 bins: zero difference. - The conversion to milliseconds (
diff(st) * 1000, R line 114) sits one function up in Python, in_log_isi_valley.
logISI: finding the peaks
This is where the two differ most line by line. R's get.peaks is a general peak finder with a minimum peak distance Pd, a height margin Th, a peak limit Np and a skip-ahead after each peak. It is only ever called with the defaults. With Pd = 2 and Th = 0 it reduces to one rule, which Python states directly.
- The skip-ahead (R lines 50–53) changes nothing: a bin within two bins of a peak cannot itself be strictly higher than that peak.
- R tests the edge condition inside the comparison (
j != 1 && j != L); Python leaves the edge bins out of the loop. - The
Pd <= 1branch (lines 67–71) andNpare never reached with the defaults and are not carried over.
logISI: from peaks to threshold
Take the largest peak below the cutoff as the intra-burst peak. For each later peak, find the lowest bin between the two and score the separation with the void parameter. The threshold is the interval at the first minimum that scores 0.7 or more.
- R signals its three outcomes with three kinds of value:
-1000(no intra-burst peak),NULL(no clear valley) and a number. Python returns a pair,(has_intraburst_peak, threshold or None). - R computes the void parameter for every later peak and then takes the first that passes; Python stops at the first that passes. Same result.
- The reference uses
>=0.7 where the chapter writes>; Python follows the reference so the two agree.
logISI: which threshold to detect with
Four cases, in both versions. R builds a parameter list for each and calls logisi.find.burst; Python calls the three MaxInterval phases with the start and in-burst thresholds set to the same value.
fallback=Falseis Python only and not in the published method: a train without a clear valley then gets no bursts instead of the fixed cutoff.- In the third case R passes
min.ibi = isi.low, so burst cores closer than the threshold merge before the three-spike minimum is applied. Python does the same with_merge_close_runs(times, cores, threshold). Two two-spike cores close together therefore survive as one burst in both. - R requires more than three spikes before doing anything (line 6). Python has no such guard; it needs only enough spikes to make a burst.
logISI: burst-related spikes
When the threshold lies above the cutoff, each core is widened to the spikes within the threshold of its ends. R detects a second set of bursts at the wider threshold (brs), searches it for the one overlapping each core with a nested loop and two early exits, then removes duplicates by comparing neighbours. Python looks up the run that contains the core and skips it if already taken.
- Every core lies inside exactly one wider run, because its intervals are below the cutoff and the cutoff is below the threshold. So R's overlap test and Python's containment test pick the same run.
- R lines 169–177 rebuild the output columns (duration, mean ISI, IBI); in Python that is
_runs_to_interval_set, shared with every detector.
The one place they disagree
logisi.find.burst is a copy of the MaxInterval loop with two changes. One is a tolerance of 10-10 on the comparisons. The other is the loop bound: n < nspikes where MaxInterval has n <= nspikes. The last interval is never examined, and a burst still open at that point is closed on the last spike whatever that interval was. Python uses the one _find_runs for both methods and examines every interval.
- The comparison script reproduces this bound by wrapping
_find_runs, and counts a mismatch as explained only when the wrapped Python equals the R output exactly.
Agreement on 500 synthetic trains
Burst boundaries compared as spike indices: 125 trains each of bursty with Poisson background, slow bursts around the 100 ms cutoff, Poisson, and gamma renewal; 300 s each, 1,052,872 spikes in total. Run with python tests/_test_burst_r_reference.py.
| Exact | Explained by the loop bound | Unexplained | |
|---|---|---|---|
| MaxInterval bursts | 500 / 500 | – | 0 |
| logISI threshold | 500 / 500 | – | 0 |
| logISI bursts | 448 / 500 | 52 | 0 |
logISI cases covered: 192 trains with the threshold at or below the cutoff, 65 above it (burst-related spikes), 167 falling back to the cutoff, 76 with no intra-burst peak.