Skip to content

Commit 8f250c6

Browse files
authored
Merge pull request #1925 from cboulay/nev-timestamp-slice-float-seconds
Blackrock: stop dropping NEV spikes and events at segment edges (1 ns timestamps)
2 parents c5d0cfb + 352d4e0 commit 8f250c6

2 files changed

Lines changed: 50 additions & 42 deletions

File tree

‎neo/rawio/blackrockrawio.py‎

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
import os
6666
import re
6767
import warnings
68-
import math
6968

7069
import numpy as np
7170
import quantities as pq
@@ -131,6 +130,10 @@ class BlackrockRawIO(BaseRawIO):
131130
2.2 and 2.3 are automatically extracted and the data set is split into different
132131
segments.
133132
133+
* The times of spikes and events are compared with `t_start` and `t_stop` as float64 seconds, and
134+
`t_stop` is exclusive. To include a spike at `t_stop`, such as the last of a segment, pass
135+
`t_stop + 1e-5`.
136+
134137
* The Blackrock data format consists not of a single file, but a set of
135138
different files. This constructor associates itself with a set of files
136139
that constitute a common data set. By default, all files belonging to
@@ -826,18 +829,15 @@ def _get_blackrock_timestamps(self, block_index, seg_index, i_start, i_stop, str
826829
def _spike_count(self, block_index, seg_index, unit_index):
827830
channel_id, unit_id = self.internal_unit_ids[unit_index]
828831

829-
all_spikes = self.nev_data["Spikes"][0]
830-
mask = (all_spikes["packet_id"] == channel_id) & (all_spikes["unit_class_nb"] == unit_id)
831-
if self._nb_segment == 1:
832-
# very fast
833-
nb = int(np.sum(mask))
834-
else:
835-
# must clip in time time range
836-
timestamp = all_spikes[mask]["timestamp"]
837-
sl = self._get_timestamp_slice(timestamp, seg_index, None, None)
838-
timestamp = timestamp[sl]
839-
nb = timestamp.size
840-
return nb
832+
all_spikes, event_segment_ids = self.nev_data["Spikes"]
833+
# Selected by segment id, as _get_spike_timestamps does, so the count always equals
834+
# the number of timestamps it returns.
835+
mask = (
836+
(all_spikes["packet_id"] == channel_id)
837+
& (all_spikes["unit_class_nb"] == unit_id)
838+
& (event_segment_ids == seg_index)
839+
)
840+
return int(np.sum(mask))
841841

842842
def _get_spike_timestamps(self, block_index, seg_index, unit_index, t_start, t_stop):
843843
channel_id, unit_id = self.internal_unit_ids[unit_index]
@@ -859,27 +859,27 @@ def _get_spike_timestamps(self, block_index, seg_index, unit_index, t_start, t_s
859859
return timestamp
860860

861861
def _get_timestamp_slice(self, timestamp, seg_index, t_start, t_stop):
862-
if self._nb_segment > 1:
863-
# we must clip event in seg time limits
864-
if t_start is None:
865-
t_start = self._seg_t_starts[seg_index]
866-
if t_stop is None:
867-
t_stop = self._seg_t_stops[seg_index] + 1 / float(self._nev_basic_header["timestamp_resolution"])
868-
869-
if t_start is None:
870-
ind_start = None
871-
else:
872-
ts = math.ceil(t_start * self._nev_basic_header["timestamp_resolution"])
873-
ind_start = np.searchsorted(timestamp, ts)
862+
"""
863+
Slice of `timestamp` (one segment's, in file order) within [t_start, t_stop), in seconds.
874864
875-
if t_stop is None:
876-
ind_stop = None
877-
else:
878-
ts = int(t_stop * self._nev_basic_header["timestamp_resolution"])
879-
ind_stop = np.searchsorted(timestamp, ts) # +1
865+
The segment itself is already selected by segment id, so None means no limit.
880866
881-
sl = slice(ind_start, ind_stop)
882-
return sl
867+
The bounds are compared with the times neo reports (the timestamps in float64 seconds), so a
868+
time obtained from neo, such as `segment_t_start`, selects the item it came from. Converting the
869+
bounds to ticks instead does not work at the 1 ns resolution of PTP files: a float64 number of
870+
seconds (about 1.75e9) is only precise to about 240 ns, so the converted bound can fall a few
871+
ticks past the item and drop it, and np.searchsorted compares uint64 with a Python int as float64.
872+
873+
`t_stop` is exclusive. To include an item at `t_stop`, such as the last of a segment,
874+
pass `t_stop + 1e-5` (10 us, shorter than the interval between samples at 30 kHz and longer
875+
than the precision of float64 seconds).
876+
"""
877+
if t_start is None and t_stop is None:
878+
return slice(None)
879+
times = timestamp.astype("float64") / self._nev_basic_header["timestamp_resolution"]
880+
ind_start = None if t_start is None else np.searchsorted(times, t_start, side="left")
881+
ind_stop = None if t_stop is None else np.searchsorted(times, t_stop, side="left")
882+
return slice(ind_start, ind_stop)
883883

884884
def _rescale_spike_timestamp(self, spike_timestamps, dtype):
885885
spike_times = spike_timestamps.astype(dtype)
@@ -924,17 +924,9 @@ def _event_count(self, block_index, seg_index, event_channel_index):
924924
else:
925925
events_data, event_segment_ids = self.nev_data["NonNeural"]
926926
ev_dict = self._nonneural_evdicts[self._nev_spec](events_data)[name]
927+
# Selected by segment id, as _get_event_timestamps does.
927928
mask = ev_dict["mask"] & (event_segment_ids == seg_index)
928-
if self._nb_segment == 1:
929-
# very fast
930-
nb = int(np.sum(mask))
931-
else:
932-
# must clip in time time range
933-
timestamp = events_data[ev_dict["mask"]]["timestamp"]
934-
sl = self._get_timestamp_slice(timestamp, seg_index, None, None)
935-
timestamp = timestamp[sl]
936-
nb = timestamp.size
937-
return nb
929+
return int(np.sum(mask))
938930

939931
def _get_event_timestamps(self, block_index, seg_index, event_channel_index, t_start, t_stop):
940932
name = self.header["event_channels"]["name"][event_channel_index]

‎neo/test/rawiotest/test_blackrockrawio.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class TestBlackrockRawIO(
2929
"blackrock/blackrock_2_1/l101210-001",
3030
"blackrock/blackrock_3_0/file_spec_3_0",
3131
"blackrock/blackrock_3_0_ptp/20231027-125608-001",
32+
"blackrock/segment/ResetCorrect/reset",
3233
]
3334

3435
@unittest.skipUnless(HAVE_SCIPY, "requires scipy")
@@ -374,6 +375,21 @@ def test_gap_tolerance_ms_parameter(self):
374375
segments_strict = reader_strict.segment_count(0)
375376
self.assertEqual(segments_strict, 3) #
376377

378+
def test_nev_spikes_at_segment_edges_ptp(self):
379+
reader = BlackrockRawIO(
380+
filename=self.get_local_path(
381+
"blackrock/blackrock_ptp_with_missing_samples/Hub1-NWBtestfile_neural_wspikes"
382+
),
383+
nsx_to_load=[],
384+
)
385+
reader.parse_header()
386+
returned = 0
387+
for block_index in range(reader.block_count()):
388+
for seg_index in range(reader.segment_count(block_index)):
389+
for unit_index in range(reader.spike_channels_count()):
390+
returned += reader.get_spike_timestamps(block_index, seg_index, unit_index, None, None).size
391+
self.assertEqual(returned, 999)
392+
377393

378394
if __name__ == "__main__":
379395
unittest.main()

0 commit comments

Comments
 (0)