Repository navigation
Description
NeoSortingSegment.get_unit_spike_train turns Neo's spike times back into frames with
spike_frames = ((spike_times_seconds - t_start) * self._sampling_frequency).astype("int64")(src/spikeinterface/extractors/neoextractors/neobaseextractor.py, line 647 on main). For Blackrock, Neo converts the integer .nev ticks to seconds, and this line converts them back. Float round-off leaves some products just under an integer, for example 30936 / 30000 * 30000 == 30935.999999999996, and astype truncates, so those spikes move one sample earlier.
Reproduce with blackrock/blackrock_2_1/l101210-001.nev from ephy_testing_data. Its timestamp resolution is 30 kHz, the same as sampling_frequency, so frames should equal ticks exactly:
import numpy as np
from spikeinterface.extractors import read_blackrock_sorting
sorting = read_blackrock_sorting(file_path="blackrock/blackrock_2_1/l101210-001.nev", sampling_frequency=30000.0)
neo = sorting.neo_reader
diffs, units_off = [], 0
for i, unit_id in enumerate(sorting.unit_ids):
ticks = neo.get_spike_timestamps(block_index=0, seg_index=0, spike_channel_index=i).astype(np.int64)
d = sorting.get_unit_spike_train(unit_id) - ticks
diffs.append(d)
units_off += bool(np.any(d))
d = np.concatenate(diffs)
values, counts = np.unique(d, return_counts=True)
print(f"spikes: {d.size}, frame - tick: { {int(v): int(c) for v, c in zip(values, counts)} }")
print(f"units with at least one shifted spike: {units_off} of {len(sorting.unit_ids)}")spikes: 14210, frame - tick: {-1: 272, 0: 13938}
units with at least one shifted spike: 107 of 218
So 272 of 14,210 spikes (1.9%) come out one sample early, and none late. Over tick values 0 to 109,225, 2.3% truncate this way.
Affected readers: those with neo_returns_frames = False, which in 0.105.4 are Blackrock and MEArec. NeuroConv's BlackrockSortingInterface goes through this path, so NWB units tables written from .nev files carry the shift.
Suggested fix:
spike_frames = np.round((spike_times_seconds - t_start) * self._sampling_frequency).astype("int64")With that change all 14,210 spikes match the .nev ticks exactly.
Environment: Windows 11, Python 3.12.13, spikeinterface 0.105.4 (the line is unchanged on main), neo 0.14.6, numpy 2.5.4.
I found this while checking a NeuroConv conversion against the raw files: https://github.com/JamesBelanger/blackrock-nwb-roundtrip (script: repro/si_spike_truncation.py).
Activity
JamesBelanger commentedon Nov 5, 2026
Correction to the scope in the issue: MEArec should not change. Its spike times are continuous, and MEArec places spikes in its recordings with
(st.times * fs).astype("int")(recordinggenerator.py#L1140), so truncation is the right conversion there. Thenp.roundchange I suggested would move 376 of the 747 spikes inmearec_test_10s.h5one frame late.The bug is Blackrock-only, where Neo returns integer clock ticks. I have a branch that adds a
round_spike_framesclass attribute (default False), set only onBlackrockSortingExtractor, with a test against the raw .nev timestamps. Happy to open it as a PR if that approach works for you.h-mayorquin commentedon Nov 5, 2026
Thanks for reporting this, I am on the middle of fixing a lot of Blarock bugs and I will address this one.