Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3dc5729
Test IBL extractors tests failing for PI update
alejoe91 Dec 29, 2025
d1a0532
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 6, 2026
33c6769
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 16, 2026
2c94bac
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 20, 2026
a412bd8
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Feb 2, 2026
504e19d
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Feb 12, 2026
cd09c19
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Feb 19, 2026
a40d073
Merge branch 'main' of github.com:alejoe91/spikeinterface
alejoe91 Feb 24, 2026
a1da327
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 2, 2026
ef19a8e
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 3, 2026
a098b51
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 6, 2026
61c317a
Fix OpenEphys tests
alejoe91 Mar 6, 2026
c9ff247
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 9, 2026
3520138
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 16, 2026
f61329d
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 16, 2026
d64ae6a
Merge branch 'main' of github.com:alejoe91/spikeinterface
alejoe91 Mar 16, 2026
aef197d
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 17, 2026
e82331b
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 20, 2026
710cb6f
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 23, 2026
c2f8db1
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 23, 2026
161d25b
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 27, 2026
1d09ec6
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 30, 2026
afb7d33
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 30, 2026
fa556ba
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 30, 2026
8e68f16
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Apr 14, 2026
32cb1e7
Enable slicing of time vector
alejoe91 Apr 14, 2026
9e11cbd
fix tests
alejoe91 Apr 14, 2026
5eff14e
refactor plot_traces to not load times
alejoe91 Apr 14, 2026
d4ecd3a
Merge branch 'main' into improve-get-times
chrishalcrow Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/spikeinterface/core/baserecording.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,9 @@ def get_time_info(self, segment_index=None) -> dict:

return time_kwargs

def get_times(self, segment_index=None) -> np.ndarray:
def get_times(
self, segment_index: int | None = None, start_frame: int | None = None, end_frame: int | None = None
) -> np.ndarray:
"""Get time vector for a recording segment.

If the segment has a time_vector, then it is returned. Otherwise
Expand All @@ -407,6 +409,10 @@ def get_times(self, segment_index=None) -> np.ndarray:
----------
segment_index : int or None, default: None
The segment index (required for multi-segment)
start_frame : int or None, default: None
The start frame index. If None, it starts from the beginning of the segment.
end_frame : int or None, default: None
The end frame index. If None, it goes until the end of the segment.

Returns
-------
Expand All @@ -415,7 +421,7 @@ def get_times(self, segment_index=None) -> np.ndarray:
"""
segment_index = self._check_segment_index(segment_index)
rs = self.segments[segment_index]
times = rs.get_times()
times = rs.get_times(start_frame=start_frame, end_frame=end_frame)
return times

def get_start_time(self, segment_index=None) -> float:
Expand Down Expand Up @@ -913,12 +919,22 @@ def __init__(self, sampling_frequency=None, t_start=None, time_vector=None):

BaseSegment.__init__(self)

def get_times(self) -> np.ndarray:
def get_times(self, start_frame: int | None = None, end_frame: int | None = None) -> np.ndarray:
if self.time_vector is not None:
self.time_vector = np.asarray(self.time_vector)
return self.time_vector
# Cache full times as numpy if start_frame and end_frame are None. If the user passes start_frame and
# end_frame, we slice the time vector and return the sliced version as numpy array.
# This is useful for very long recordings, where the full time vector might be too large to fit in memory.
if start_frame is None and end_frame is None:
self.time_vector = np.asarray(self.time_vector)
return self.time_vector
else:
start_frame = int(start_frame) if start_frame is not None else 0
end_frame = int(end_frame) if end_frame is not None else self.get_num_samples()
return np.asarray(self.time_vector[start_frame:end_frame])
else:
time_vector = np.arange(self.get_num_samples(), dtype="float64")
start_frame = int(start_frame) if start_frame is not None else 0
end_frame = int(end_frame) if end_frame is not None else self.get_num_samples()
time_vector = np.arange(start_frame, end_frame, dtype="float64")
time_vector /= self.sampling_frequency
if self.t_start is not None:
time_vector += self.t_start
Expand Down
9 changes: 7 additions & 2 deletions src/spikeinterface/core/basesorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,12 @@ def has_time_vector(self, segment_index: int | None = None) -> bool:
else:
return False

def get_times(self, segment_index=None):
def get_times(
self,
segment_index: int | None = None,
start_frame: int | None = None,
end_frame: int | None = None,
):
"""
Get time vector for a registered recording segment.

Expand All @@ -359,7 +364,7 @@ def get_times(self, segment_index=None):
"""
segment_index = self._check_segment_index(segment_index)
if self.has_recording():
return self._recording.get_times(segment_index=segment_index)
return self._recording.get_times(segment_index=segment_index, start_frame=start_frame, end_frame=end_frame)
else:
return None

Expand Down
2 changes: 2 additions & 0 deletions src/spikeinterface/core/tests/test_baserecording.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def test_BaseRecording(create_cache_folder):
assert values.dtype.kind == "i"

times0 = rec.get_times(segment_index=0)
times0_slice = rec.get_times(segment_index=0, start_frame=10, end_frame=20)
assert np.allclose(times0_slice, times0[10:20])

# dump/load dict
d = rec.to_dict(include_annotations=True, include_properties=True)
Expand Down
62 changes: 20 additions & 42 deletions src/spikeinterface/widgets/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,6 @@ def __init__(
raise ValueError('You must provide "segment_index" for multisegment recordings.')
segment_index = 0

if not rec0.has_time_vector(segment_index=segment_index):
times = None
else:
times = rec0.get_times(segment_index=segment_index)
t_start = rec0.get_start_time(segment_index=segment_index)
t_end = rec0.get_end_time(segment_index=segment_index)

Expand All @@ -172,7 +168,7 @@ def __init__(
cmap = cmap

times_in_range, list_traces, frame_range, channel_ids = _get_trace_list(
recordings, channel_ids, time_range, segment_index, return_in_uV=return_in_uV, times=times
recordings, channel_ids, segment_index, time_range=time_range, return_in_uV=return_in_uV
)

list_traces = [traces * scale for traces in list_traces]
Expand Down Expand Up @@ -405,25 +401,12 @@ def plot_ipywidgets(self, data_plot, **backend_kwargs):
self.figure.canvas.header_visible = False
plt.show()

if not self.rec0.has_time_vector(segment_index=data_plot["segment_index"]):
times = None
t_starts = [
rec0.get_start_time(segment_index=segment_index) for segment_index in range(rec0.get_num_segments())
]
else:
times = [
np.array(self.rec0.get_times(segment_index=segment_index))
for segment_index in range(self.rec0.get_num_segments())
]
t_starts = None

# some widgets
self.time_slider = TimeSlider(
durations=[rec0.get_duration(s) for s in range(rec0.get_num_segments())],
sampling_frequency=rec0.sampling_frequency,
time_range=data_plot["time_range"],
times=times,
t_starts=t_starts,
frame_range=data_plot["frame_range"],
rec0=rec0,
)
# handle times
if data_plot["events"] is not None:
Expand Down Expand Up @@ -559,24 +542,17 @@ def _retrieve_traces(self, change=None):

start_frame, end_frame, segment_index = self.time_slider.value

if not self.rec0.has_time_vector(segment_index=segment_index):
times = None
time_range = np.array([start_frame, end_frame]) / self.rec0.sampling_frequency + self.rec0.get_start_time(
segment_index=segment_index
)
else:
times = self.rec0.get_times(segment_index=segment_index)
time_range = np.array([times[start_frame], times[end_frame]])
frame_range = np.array([start_frame, end_frame])

self._selected_recordings = {k: self.recordings[k] for k in self._get_layers()}
times_in_range, list_traces, frame_range, channel_ids = _get_trace_list(
self._selected_recordings,
channel_ids,
time_range,
segment_index,
return_in_uV=self.return_in_uV,
times=times,
frame_range=frame_range,
)
time_range = np.array([times_in_range[0], times_in_range[-1]])

self._channel_ids = channel_ids
self._list_traces = list_traces
Expand Down Expand Up @@ -640,12 +616,11 @@ def plot_figpack(self, data_plot, **backend_kwargs):
handle_display_and_url,
import_figpack_or_sortingview,
)
import importlib.util

use_sortingview = backend_kwargs.get("use_sortingview", False)
vv_base, vv_views = import_figpack_or_sortingview(use_sortingview)

import importlib.util

spec = importlib.util.find_spec("pyvips")
if spec is None:
raise ImportError("To use `plot_traces()` in sortingview you need the pyvips package.")
Expand Down Expand Up @@ -705,25 +680,28 @@ def plot_ephyviewer(self, data_plot, **backend_kwargs):
app.exec()


def _get_trace_list(recordings, channel_ids, time_range, segment_index, return_in_uV=False, times=None):
def _get_trace_list(recordings, channel_ids, segment_index, time_range=None, return_in_uV=False, frame_range=None):
# function also used in ipywidgets plotter
k0 = list(recordings.keys())[0]
rec0 = recordings[k0]

fs = rec0.get_sampling_frequency()

if return_in_uV:
assert all(
rec.has_scaleable_traces() for rec in recordings.values()
), "Some recording layers do not have scaled traces. Use `return_in_uV=False`"
if times is not None:
frame_range = np.searchsorted(times, time_range)
times = times[frame_range[0] : frame_range[1]]
else:
frame_range = rec0.time_to_sample_index(time_range, segment_index=segment_index)

assert time_range is not None or frame_range is not None, "You must provide either time_range or frame_range"

if frame_range is None:
# use the sampling-frequency approximation to avoid loading the full time vector
t_start = rec0.get_start_time(segment_index=segment_index)
fs = rec0.get_sampling_frequency()
frame_range = np.round((np.asarray(time_range) - t_start) * fs).astype(np.int64)
a_max = rec0.get_num_frames(segment_index=segment_index)
frame_range = np.clip(frame_range, 0, a_max)
times = np.arange(frame_range[0], frame_range[1]) / fs + rec0.get_start_time(segment_index=segment_index)

# lazily load only the needed time slice
times_in_range = rec0.get_times(segment_index=segment_index, start_frame=frame_range[0], end_frame=frame_range[1])

list_traces = []
for rec_name, rec in recordings.items():
Expand All @@ -737,4 +715,4 @@ def _get_trace_list(recordings, channel_ids, time_range, segment_index, return_i

list_traces.append(traces)

return times, list_traces, frame_range, channel_ids
return times_in_range, list_traces, frame_range, channel_ids
42 changes: 26 additions & 16 deletions src/spikeinterface/widgets/utils_ipywidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,25 @@ def check_ipywidget_backend():
class TimeSlider(W.HBox):
value = traitlets.Tuple(traitlets.Int(), traitlets.Int(), traitlets.Int())

def __init__(self, durations, sampling_frequency, time_range, times=None, t_starts=None, **kwargs):
def __init__(self, durations, sampling_frequency, frame_range, rec0=None, t_starts=None, **kwargs):
self.num_segments = len(durations)
self.frame_limits = [int(sampling_frequency * d) for d in durations]
self.sampling_frequency = sampling_frequency
self.segment_index = 0

if times is not None:
assert len(times) == len(durations), "times should be a list of arrays with one array per segment"
times_segment = times[self.segment_index]
start_frame, end_frame = np.searchsorted(times_segment, time_range)
self.times = times
start_frame, end_frame = int(frame_range[0]), int(frame_range[1])

if rec0 is not None:
self.rec0 = rec0
self.t_starts = None
else:
assert t_starts is not None
t_start_segment = t_starts[self.segment_index]
start_frame = int((time_range[0] - t_start_segment) * sampling_frequency)
end_frame = int((time_range[1] - t_start_segment) * sampling_frequency)
self.times = None
self.rec0 = None
self.t_starts = t_starts

self.frame_range = (start_frame, end_frame)

self.value = (int(start_frame), int(end_frame), self.segment_index)
self.value = (start_frame, end_frame, self.segment_index)

layout = W.Layout(align_items="center", width="2.5cm", height="1.cm")
but_left = W.Button(description="", disabled=False, button_style="", icon="arrow-left", layout=layout)
Expand All @@ -63,8 +59,16 @@ def __init__(self, durations, sampling_frequency, time_range, times=None, t_star
)

# DatetimePicker is only for ipywidget v8 (which is not working in vscode 2023-03)
if self.rec0 is not None:
initial_time = float(
self.rec0.get_times(
segment_index=self.segment_index, start_frame=start_frame, end_frame=start_frame + 1
)[0]
)
else:
initial_time = start_frame / sampling_frequency + self.t_starts[self.segment_index]
self.time_label = W.Text(
value=f"{time_range[0]}", description="", disabled=False, layout=W.Layout(width="2.5cm")
value=f"{initial_time}", description="", disabled=False, layout=W.Layout(width="2.5cm")
)
self.time_label.observe(self.time_label_changed, names="value", type="change")

Expand Down Expand Up @@ -137,8 +141,10 @@ def update_time(self, new_frame=None, new_time=None, update_slider=False, update
if new_frame is None and new_time is None:
start_frame = self.slider.value
elif new_frame is None:
if self.times is not None:
start_frame = int(np.searchsorted(self.times[self.segment_index], [new_time])[0])
if self.rec0 is not None:
# approximate via sampling frequency to avoid loading the full time vector
t_start = float(self.rec0.get_start_time(segment_index=self.segment_index))
start_frame = int((new_time - t_start) * self.sampling_frequency)
else:
start_frame = int((new_time - self.t_starts[self.segment_index]) * self.sampling_frequency)
else:
Expand All @@ -153,8 +159,12 @@ def update_time(self, new_frame=None, new_time=None, update_slider=False, update

end_frame = min(self.frame_limits[self.segment_index], end_frame)

if self.times is not None:
start_time = self.times[self.segment_index][start_frame]
if self.rec0 is not None:
start_time = float(
self.rec0.get_times(
segment_index=self.segment_index, start_frame=start_frame, end_frame=start_frame + 1
)[0]
)
else:
start_time = start_frame / self.sampling_frequency + self.t_starts[self.segment_index]

Expand Down
Loading