From f1a33659159293be043cf296fd2ffb3540e72776 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Fri, 28 Aug 2026 14:03:12 +0100 Subject: [PATCH 01/23] wip --- playing.py | 54 +++++++++++++++++++++++ src/spikeinterface/extractors/cbin_ibl.py | 10 ++--- src/spikeinterface/widgets/rasters.py | 33 ++++++++------ 3 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 playing.py diff --git a/playing.py b/playing.py new file mode 100644 index 0000000000..6aab527ea1 --- /dev/null +++ b/playing.py @@ -0,0 +1,54 @@ +""" +Generate a multi-segment recording whose segments start at non-zero times, +then plot a raster of the segments. +""" + +import matplotlib.pyplot as plt + +from spikeinterface.core import append_recordings, append_sortings, generate_ground_truth_recording +import spikeinterface.widgets as sw + + +def make_multi_segment_recording(num_segments=3, seed=2205): + """Build a multi-segment recording and sorting with non-zero segment start times.""" + + common_kwargs = dict( + durations=[10.0], + sampling_frequency=30000.0, + num_channels=64, + num_units=60, + ) + + recordings = [] + sortings = [] + # Offsets so segments do NOT start at time zero. + start_time_offsets = [5.0, 120.0, 260.0] + + for seg in range(num_segments): + recording, sorting = generate_ground_truth_recording(seed=seed + seg, **common_kwargs) + # Give this segment a non-zero start time. + recording.shift_times(shift=start_time_offsets[seg % len(start_time_offsets)]) + recordings.append(recording) + sortings.append(sorting) + + multi_segment_recording = append_recordings(recordings) + multi_segment_sorting = append_sortings(sortings) + return multi_segment_recording, multi_segment_sorting + + +def main(): + recording, sorting = make_multi_segment_recording() + + print(f"Number of segments: {recording.get_num_segments()}") + for seg in range(recording.get_num_segments()): + print(f" segment {seg}: start={recording.get_start_time(seg):.1f}s end={recording.get_end_time(seg):.1f}s") + + # Plot a raster of all segments. + segment_indices = list(range(sorting.get_num_segments())) + sw.plot_rasters(sorting, segment_indices=segment_indices) + + plt.show() + + +if __name__ == "__main__": + main() diff --git a/src/spikeinterface/extractors/cbin_ibl.py b/src/spikeinterface/extractors/cbin_ibl.py index 4b222d7e59..4dae0f765c 100644 --- a/src/spikeinterface/extractors/cbin_ibl.py +++ b/src/spikeinterface/extractors/cbin_ibl.py @@ -107,12 +107,12 @@ def __init__(self, folder_path=None, load_sync_channel=False, stream_name="ap", self.set_probe(probe, in_place=True) # load num_channels_per_adc depending on probe type - ptype = probe.annotations["probe_type"] +# ptype = probe.annotations["probe_type"] - if ptype in [21, 24]: # NP2.0 - num_channels_per_adc = 16 - else: # NP1.0 - num_channels_per_adc = 12 + # if ptype in [21, 24]: # NP2.0 + num_channels_per_adc = 16 + # else: # NP1.0 + # num_channels_per_adc = 12 sample_shifts = get_neuropixels_sample_shifts_from_probe(probe, num_channels_per_adc) self.set_property("inter_sample_shift", sample_shifts) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 1ddde77bf9..232b884551 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -65,6 +65,7 @@ def __init__( y_axis_data: dict, unit_ids: list | None = None, segment_indices: list | None = None, + segment_start_stop_times: np.ndarray | None = None, durations: list | None = None, plot_histograms: bool = False, bins: int | None = None, @@ -82,6 +83,11 @@ def __init__( backend: str | None = None, **backend_kwargs, ): + if durations is not None and segment_start_stop_times is not None: + raise ValueError("`durations` should not be passed with `segment_start_stop_times`. Use `segment_start_stop_times` only.") + + if durations is not None and segment_start_stop_times is None: + segment_start_stop_times = np.r_[np.cumsum(durations)] # Set default segment boundary kwargs if not provided if segment_boundary_kwargs is None: @@ -112,15 +118,14 @@ def __init__( unit_ids = list(all_units) # Calculate cumulative durations for segment boundaries - segment_boundaries = np.cumsum(durations) - cumulative_durations = np.concatenate([[0], segment_boundaries]) + # segment_boundaries = np.array(np.r_[seg[0], seg[1]] for seg in segment_start_stop_times) # np.cumsum(durations) + # cumulative_durations = np.concatenate([[0], segment_boundaries]) # Concatenate data across segments with proper time offsets concatenated_spike_trains = {unit_id: np.array([]) for unit_id in unit_ids} concatenated_y_axis = {unit_id: np.array([]) for unit_id in unit_ids} - for offset, spike_train_segment, y_axis_segment in zip( - cumulative_durations, + for spike_train_segment, y_axis_segment in zip( [spike_train_data[idx] for idx in segments_to_use], [y_axis_data[idx] for idx in segments_to_use], ): @@ -133,11 +138,11 @@ def __init__( y_values = y_axis_segment[unit_id] # Apply offset to spike times - adjusted_times = spike_times + offset + # adjusted_times = spike_times + offset # Add to concatenated data concatenated_spike_trains[unit_id] = np.concatenate( - [concatenated_spike_trains[unit_id], adjusted_times] + [concatenated_spike_trains[unit_id], spike_times] ) concatenated_y_axis[unit_id] = np.concatenate([concatenated_y_axis[unit_id], y_values]) @@ -153,12 +158,13 @@ def __init__( unit_colors=unit_colors, y_label=y_label, title=title, - durations=durations, + segment_start_stop_times=segment_start_stop_times, + # durations=durations, plot_legend=plot_legend, bins=bins, y_ticks=y_ticks, hide_unit_selector=hide_unit_selector, - segment_boundaries=segment_boundaries, + # segment_boundaries=segment_boundaries, segment_boundary_kwargs=segment_boundary_kwargs, ) @@ -226,8 +232,8 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): ax_hist.plot(count, bins[:-1], color=unit_colors[unit_id], alpha=0.8) # Add segment boundary lines if provided - if getattr(dp, "segment_boundaries", None) is not None: - for boundary in dp.segment_boundaries: + if dp.segment_start_stop_times is not None: + for boundary in dp.segment_start_stop_times: scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) if dp.plot_histograms: @@ -245,9 +251,10 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): if dp.y_lim is not None: scatter_ax.set_ylim(*dp.y_lim) x_lim = dp.x_lim - if x_lim is None: - x_lim = [0, np.sum(dp.durations)] - scatter_ax.set_xlim(x_lim) + + if x_lim is None and dp.segment_start_stop_times is not None: + x_lim = (dp.segment_start_stop_times[0], dp.segment_start_stop_times[-1]) + scatter_ax.set_xlim(x_lim) if dp.y_ticks: scatter_ax.set_yticks(**dp.y_ticks) From e3c603e68765b6a4599e0ef8b6f238680e792d99 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Fri, 28 Aug 2026 16:56:02 +0100 Subject: [PATCH 02/23] update. --- playing.py | 2 +- src/spikeinterface/widgets/rasters.py | 45 ++++++++++++++++++++------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/playing.py b/playing.py index 6aab527ea1..0d4ac816cc 100644 --- a/playing.py +++ b/playing.py @@ -38,8 +38,8 @@ def make_multi_segment_recording(num_segments=3, seed=2205): def main(): recording, sorting = make_multi_segment_recording() + sorting.register_recording(recording) - print(f"Number of segments: {recording.get_num_segments()}") for seg in range(recording.get_num_segments()): print(f" segment {seg}: start={recording.get_start_time(seg):.1f}s end={recording.get_end_time(seg):.1f}s") diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 232b884551..678499ab6d 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -65,7 +65,7 @@ def __init__( y_axis_data: dict, unit_ids: list | None = None, segment_indices: list | None = None, - segment_start_stop_times: np.ndarray | None = None, + segment_start_stop_times: list[tuple] | None = None, durations: list | None = None, plot_histograms: bool = False, bins: int | None = None, @@ -87,6 +87,8 @@ def __init__( raise ValueError("`durations` should not be passed with `segment_start_stop_times`. Use `segment_start_stop_times` only.") if durations is not None and segment_start_stop_times is None: + # this is not correct! + raise NotImplementedError() segment_start_stop_times = np.r_[np.cumsum(durations)] # Set default segment boundary kwargs if not provided @@ -110,6 +112,9 @@ def __init__( else: raise ValueError("segment_index must be `list` or `None`") + # have as list of tuples up to here so can drop from segments to use. + # now convert to np.array + # Get all unit IDs present in any segment if not specified if unit_ids is None: all_units = set() @@ -117,6 +122,12 @@ def __init__( all_units.update(spike_train_data[seg_idx].keys()) unit_ids = list(all_units) + segment_start_stop_times_array = [] + for seg in segments_to_use: + segment_start_stop_times_array.append( + segment_start_stop_times.index(seg) + ) + # Calculate cumulative durations for segment boundaries # segment_boundaries = np.array(np.r_[seg[0], seg[1]] for seg in segment_start_stop_times) # np.cumsum(durations) # cumulative_durations = np.concatenate([[0], segment_boundaries]) @@ -158,7 +169,7 @@ def __init__( unit_colors=unit_colors, y_label=y_label, title=title, - segment_start_stop_times=segment_start_stop_times, + segment_start_stop_times_array=segment_start_stop_times_array, # durations=durations, plot_legend=plot_legend, bins=bins, @@ -232,9 +243,13 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): ax_hist.plot(count, bins[:-1], color=unit_colors[unit_id], alpha=0.8) # Add segment boundary lines if provided - if dp.segment_start_stop_times is not None: - for boundary in dp.segment_start_stop_times: - scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) + if dp.segment_start_stop_times_array is not None: + # When segments do not have times, the start/stop + # times are all about the same (estimated from spike times). + # Ignore this case so we only plot boundaries for sequential segments. + if np.all(np.diff(dp.segment_start_stop_times_array) > 0): + for boundary in dp.segment_start_stop_times_array: + scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) if dp.plot_histograms: ax_hist = self.axes.flatten()[1] @@ -252,8 +267,8 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): scatter_ax.set_ylim(*dp.y_lim) x_lim = dp.x_lim - if x_lim is None and dp.segment_start_stop_times is not None: - x_lim = (dp.segment_start_stop_times[0], dp.segment_start_stop_times[-1]) + if x_lim is None and dp.segment_start_stop_times_array is not None: + x_lim = (dp.segment_start_stop_times_array[0], dp.segment_start_stop_times_array[-1]) scatter_ax.set_xlim(x_lim) if dp.y_ticks: @@ -409,23 +424,27 @@ def __init__( unit_indices_map = {unit_id: i for i, unit_id in enumerate(unit_ids)} # Estimate segment duration from max spike time in each segment - durations = get_segment_durations(sorting, segment_indices) + # durations = get_segment_durations(sorting, segment_indices) # Extract spike data for all segments and units at once spike_train_data = {seg_idx: {} for seg_idx in segment_indices} y_axis_data = {seg_idx: {} for seg_idx in segment_indices} + segment_start_stop_times = [] for seg_idx in segment_indices: for unit_id in unit_ids: # Get spikes for this segment and unit spike_times = ( - sorting.get_unit_spike_train(unit_id=unit_id, segment_index=seg_idx) / sorting.sampling_frequency + sorting.get_unit_spike_train_in_seconds(unit_id=unit_id, segment_index=seg_idx) ) - # Store data spike_train_data[seg_idx][unit_id] = spike_times y_axis_data[seg_idx][unit_id] = unit_indices_map[unit_id] * np.ones(len(spike_times)) + segment_start_stop_times.append( + (np.min(spike_times), np.max(spike_times)) + ) + # Apply time range filtering if specified if time_range is not None: assert len(time_range) == 2, "'time_range' should be a list with start and end time in seconds" @@ -449,7 +468,11 @@ def __init__( unit_colors=unit_colors, plot_histograms=None, y_ticks=y_ticks, - durations=durations, + segment_start_stop_times=segment_start_stop_times, + # durations=durations, ) BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs) + +# segments do not have times +# no segment start_stop times passed to base raster From b2b61129fe37b14c3f5631de5c8db345e173d775 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:41:08 +0000 Subject: [PATCH 03/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spikeinterface/widgets/rasters.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index d2c53a137b..8984f7cf25 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -86,7 +86,9 @@ def __init__( **backend_kwargs, ): if durations is not None and segment_start_stop_times is not None: - raise ValueError("`durations` should not be passed with `segment_start_stop_times`. Use `segment_start_stop_times` only.") + raise ValueError( + "`durations` should not be passed with `segment_start_stop_times`. Use `segment_start_stop_times` only." + ) if durations is not None and segment_start_stop_times is None: # this is not correct! @@ -126,9 +128,7 @@ def __init__( segment_start_stop_times_array = [] for seg in segments_to_use: - segment_start_stop_times_array.append( - segment_start_stop_times.index(seg) - ) + segment_start_stop_times_array.append(segment_start_stop_times.index(seg)) # Calculate cumulative durations for segment boundaries # segment_boundaries = np.array(np.r_[seg[0], seg[1]] for seg in segment_start_stop_times) # np.cumsum(durations) @@ -154,9 +154,7 @@ def __init__( # adjusted_times = spike_times + offset # Add to concatenated data - concatenated_spike_trains[unit_id] = np.concatenate( - [concatenated_spike_trains[unit_id], spike_times] - ) + concatenated_spike_trains[unit_id] = np.concatenate([concatenated_spike_trains[unit_id], spike_times]) concatenated_y_axis[unit_id] = np.concatenate([concatenated_y_axis[unit_id], y_values]) plot_data = dict( @@ -274,7 +272,7 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): if dp.y_lim is not None: scatter_ax.set_ylim(*dp.y_lim) x_lim = dp.x_lim - + if x_lim is None and dp.segment_start_stop_times_array is not None: x_lim = (dp.segment_start_stop_times_array[0], dp.segment_start_stop_times_array[-1]) scatter_ax.set_xlim(x_lim) @@ -488,16 +486,12 @@ def __init__( for seg_idx in segment_indices: for unit_id in unit_ids: # Get spikes for this segment and unit - spike_times = ( - sorting.get_unit_spike_train_in_seconds(unit_id=unit_id, segment_index=seg_idx) - ) + spike_times = sorting.get_unit_spike_train_in_seconds(unit_id=unit_id, segment_index=seg_idx) # Store data spike_train_data[seg_idx][unit_id] = spike_times y_axis_data[seg_idx][unit_id] = unit_indices_map[unit_id] * np.ones(len(spike_times)) - segment_start_stop_times.append( - (np.min(spike_times), np.max(spike_times)) - ) + segment_start_stop_times.append((np.min(spike_times), np.max(spike_times))) # Apply time range filtering if specified if time_range is not None: @@ -530,5 +524,6 @@ def __init__( BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs) + # segments do not have times # no segment start_stop times passed to base raster From 22c6e160e5fd2a4217cad2d278e7acd5eadd4154 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Wed, 16 Sep 2026 20:44:30 +0100 Subject: [PATCH 04/23] Tidy up,tests. --- src/spikeinterface/widgets/motion.py | 14 ++++++++++++-- src/spikeinterface/widgets/rasters.py | 2 +- src/spikeinterface/widgets/tests/test_widgets.py | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index 030f11b276..bb8cbdc149 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -229,7 +229,12 @@ def __init__( segment_locations = filtered_locations[segment_mask] # Convert peak times to seconds - spike_times = segment_peaks["sample_index"] / sampling_frequency + if recording is None: + spike_times = segment_peaks["sample_index"] / sampling_frequency + else: + spike_times = recording.sample_index_to_time( + segment_peaks["sample_index"], segment_index=seg_idx + ) # Store in dict of dicts format (using 0 as the "unit" id) spike_train_data[seg_idx] = {0: spike_times} @@ -259,7 +264,10 @@ def __init__( # Calculate segment durations for x-axis limits if recording is not None: - durations = [recording.get_duration(seg_idx) for seg_idx in segment_indices] + durations = None + segment_start_stop_times = [ + (recording.get_start_time(seg_idx), recording.get_end_time(seg_idx)) for seg_idx in segment_indices + ] else: # Find boundaries between segments using searchsorted segment_boundaries = [ @@ -270,6 +278,7 @@ def __init__( durations = [ (filtered_peaks["sample_index"][end - 1] + 1) / sampling_frequency for (_, end) in segment_boundaries ] + segment_start_stop_times = None plot_data = dict( spike_train_data=spike_train_data, @@ -281,6 +290,7 @@ def __init__( title="Peak depth", y_label="Depth [um]", durations=durations, + segment_start_stop_times=segment_start_stop_times, ) BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 8984f7cf25..2cf0dd297f 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -128,7 +128,7 @@ def __init__( segment_start_stop_times_array = [] for seg in segments_to_use: - segment_start_stop_times_array.append(segment_start_stop_times.index(seg)) + segment_start_stop_times_array.extend(segment_start_stop_times[seg]) # Calculate cumulative durations for segment boundaries # segment_boundaries = np.array(np.r_[seg[0], seg[1]] for seg in segment_start_stop_times) # np.cumsum(durations) diff --git a/src/spikeinterface/widgets/tests/test_widgets.py b/src/spikeinterface/widgets/tests/test_widgets.py index c811c386e2..8ece826de9 100644 --- a/src/spikeinterface/widgets/tests/test_widgets.py +++ b/src/spikeinterface/widgets/tests/test_widgets.py @@ -632,6 +632,11 @@ def test_plot_rasters(self): if backend not in self.skip_backends: sw.plot_rasters(self.sorting) + shifted_sorting = self.sorting.clone() + shifted_sorting.shift_times(5.0) + widget = sw.plot_rasters(shifted_sorting, backend="matplotlib") + assert widget.ax.get_xlim()[0] >= 5.0 + def test_plot_unit_probe_map(self): possible_backends = list(sw.UnitProbeMapWidget.get_possible_backends()) for backend in possible_backends: @@ -691,6 +696,16 @@ def test_drift_raster_map(self): sw.plot_drift_raster_map( peaks=peaks, peak_locations=peak_locations, recording=recording, color_amplitude=True ) + if backend == "matplotlib": + shifted_recording = recording.clone() + shifted_recording.shift_times(5.0) + widget = sw.plot_drift_raster_map( + peaks=peaks, peak_locations=peak_locations, recording=shifted_recording + ) + np.testing.assert_allclose( + widget.ax.get_xlim(), + [shifted_recording.get_start_time(), shifted_recording.get_end_time()], + ) # without recording sw.plot_drift_raster_map( peaks=peaks, From e572373c888d33c0320b5671ef1d44d7ce3ac874 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Wed, 16 Sep 2026 20:54:25 +0100 Subject: [PATCH 05/23] Tidy --- src/spikeinterface/widgets/motion.py | 4 +++- src/spikeinterface/widgets/rasters.py | 25 ++++--------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index bb8cbdc149..cad52f3590 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -278,7 +278,9 @@ def __init__( durations = [ (filtered_peaks["sample_index"][end - 1] + 1) / sampling_frequency for (_, end) in segment_boundaries ] - segment_start_stop_times = None + segment_edges = np.concatenate([[0], np.cumsum(durations)]) + segment_start_stop_times = list(zip(segment_edges[:-1], segment_edges[1:])) + durations = None plot_data = dict( spike_train_data=spike_train_data, diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 2cf0dd297f..892722d240 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -91,9 +91,9 @@ def __init__( ) if durations is not None and segment_start_stop_times is None: - # this is not correct! - raise NotImplementedError() - segment_start_stop_times = np.r_[np.cumsum(durations)] + # This path assumes t_start are zero, which is not a good assumption. This is not really use facing, maybe we remove durations here? + edges = np.r_[0, np.cumsum(durations)] + segment_start_stop_times = list(zip(edges[:-1], edges[1:])) # Set default segment boundary kwargs if not provided if segment_boundary_kwargs is None: @@ -116,9 +116,6 @@ def __init__( else: raise ValueError("segment_index must be `list` or `None`") - # have as list of tuples up to here so can drop from segments to use. - # now convert to np.array - # Get all unit IDs present in any segment if not specified if unit_ids is None: all_units = set() @@ -130,10 +127,6 @@ def __init__( for seg in segments_to_use: segment_start_stop_times_array.extend(segment_start_stop_times[seg]) - # Calculate cumulative durations for segment boundaries - # segment_boundaries = np.array(np.r_[seg[0], seg[1]] for seg in segment_start_stop_times) # np.cumsum(durations) - # cumulative_durations = np.concatenate([[0], segment_boundaries]) - # Concatenate data across segments with proper time offsets concatenated_spike_trains = {unit_id: np.array([]) for unit_id in unit_ids} concatenated_y_axis = {unit_id: np.array([]) for unit_id in unit_ids} @@ -150,8 +143,6 @@ def __init__( # Get y-axis values for this unit y_values = y_axis_segment[unit_id] - # Apply offset to spike times - # adjusted_times = spike_times + offset # Add to concatenated data concatenated_spike_trains[unit_id] = np.concatenate([concatenated_spike_trains[unit_id], spike_times]) @@ -172,12 +163,10 @@ def __init__( y_label=y_label, title=title, segment_start_stop_times_array=segment_start_stop_times_array, - # durations=durations, plot_legend=plot_legend, bins=bins, y_ticks=y_ticks, hide_unit_selector=hide_unit_selector, - # segment_boundaries=segment_boundaries, segment_boundary_kwargs=segment_boundary_kwargs, ) @@ -250,9 +239,7 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): # Add segment boundary lines if provided if dp.segment_start_stop_times_array is not None: - # When segments do not have times, the start/stop - # times are all about the same (estimated from spike times). - # Ignore this case so we only plot boundaries for sequential segments. + # We only plot boundaries for sequential segments. if np.all(np.diff(dp.segment_start_stop_times_array) > 0): for boundary in dp.segment_start_stop_times_array: scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) @@ -475,9 +462,6 @@ def __init__( # Create a lookup dictionary for unit indices unit_indices_map = {unit_id: i for i, unit_id in enumerate(unit_ids)} - # Estimate segment duration from max spike time in each segment - # durations = get_segment_durations(sorting, segment_indices) - # Extract spike data for all segments and units at once spike_train_data = {seg_idx: {} for seg_idx in segment_indices} y_axis_data = {seg_idx: {} for seg_idx in segment_indices} @@ -519,7 +503,6 @@ def __init__( plot_histograms=None, y_ticks=y_ticks, segment_start_stop_times=segment_start_stop_times, - # durations=durations, ) BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs) From 0d1745a20e971788af9eb74856368203e2306ba0 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Wed, 16 Sep 2026 21:01:31 +0100 Subject: [PATCH 06/23] Fix linting. --- src/spikeinterface/widgets/motion.py | 4 +--- src/spikeinterface/widgets/rasters.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index cad52f3590..5a062a2a6b 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -232,9 +232,7 @@ def __init__( if recording is None: spike_times = segment_peaks["sample_index"] / sampling_frequency else: - spike_times = recording.sample_index_to_time( - segment_peaks["sample_index"], segment_index=seg_idx - ) + spike_times = recording.sample_index_to_time(segment_peaks["sample_index"], segment_index=seg_idx) # Store in dict of dicts format (using 0 as the "unit" id) spike_train_data[seg_idx] = {0: spike_times} diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 892722d240..cf5e4d4452 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -143,7 +143,6 @@ def __init__( # Get y-axis values for this unit y_values = y_axis_segment[unit_id] - # Add to concatenated data concatenated_spike_trains[unit_id] = np.concatenate([concatenated_spike_trains[unit_id], spike_times]) concatenated_y_axis[unit_id] = np.concatenate([concatenated_y_axis[unit_id], y_values]) From acf27b019c921c60b7b04c948d848e8b2a94b422 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Wed, 16 Sep 2026 21:04:59 +0100 Subject: [PATCH 07/23] Small tidy up. --- src/spikeinterface/widgets/motion.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index 5a062a2a6b..13e85b3069 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -260,9 +260,8 @@ def __init__( else: color_kwargs = dict(color=color, c=None, alpha=alpha) - # Calculate segment durations for x-axis limits + # Calculate segment start/stop times for x-axis limits if recording is not None: - durations = None segment_start_stop_times = [ (recording.get_start_time(seg_idx), recording.get_end_time(seg_idx)) for seg_idx in segment_indices ] @@ -272,13 +271,12 @@ def __init__( np.searchsorted(filtered_peaks["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices ] - # Calculate durations from max sample in each segment - durations = [ + # Calculate cumulative segment end times from the last sample in each segment + segment_end_times = [ (filtered_peaks["sample_index"][end - 1] + 1) / sampling_frequency for (_, end) in segment_boundaries ] - segment_edges = np.concatenate([[0], np.cumsum(durations)]) - segment_start_stop_times = list(zip(segment_edges[:-1], segment_edges[1:])) - durations = None + segment_start_times = np.concatenate([[0], segment_end_times[:-1]]) + segment_start_stop_times = list(zip(segment_start_times, segment_end_times)) plot_data = dict( spike_train_data=spike_train_data, @@ -289,7 +287,6 @@ def __init__( scatter_decimate=scatter_decimate, title="Peak depth", y_label="Depth [um]", - durations=durations, segment_start_stop_times=segment_start_stop_times, ) From 1f31ce78412c778077f3daae58dd388add1022ad Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 12:44:16 +0100 Subject: [PATCH 08/23] Fix sorting bug, roll out across all plots, remove durations. --- playing.py | 44 +++++++++++++++++-- src/spikeinterface/core/numpyextractors.py | 3 ++ src/spikeinterface/core/sorting_tools.py | 2 +- .../core/tests/test_numpy_extractors.py | 15 +++++++ src/spikeinterface/widgets/amplitudes.py | 4 +- src/spikeinterface/widgets/motion.py | 15 ++----- src/spikeinterface/widgets/rasters.py | 11 ----- .../widgets/tests/test_widgets_utils.py | 4 +- src/spikeinterface/widgets/utils.py | 28 ++++++++++-- 9 files changed, 93 insertions(+), 33 deletions(-) diff --git a/playing.py b/playing.py index 0d4ac816cc..9e2a4d3a02 100644 --- a/playing.py +++ b/playing.py @@ -3,9 +3,14 @@ then plot a raster of the segments. """ +import matplotlib + +matplotlib.use("QtAgg") import matplotlib.pyplot as plt -from spikeinterface.core import append_recordings, append_sortings, generate_ground_truth_recording +from spikeinterface.core import append_recordings, append_sortings, create_sorting_analyzer, generate_ground_truth_recording +from spikeinterface.generation import generate_drifting_recording +from spikeinterface.preprocessing import compute_motion import spikeinterface.widgets as sw @@ -36,6 +41,22 @@ def make_multi_segment_recording(num_segments=3, seed=2205): return multi_segment_recording, multi_segment_sorting +def plot_amplitudes_output(recording, sorting): + """Plot spike amplitudes for a few units across all recording segments.""" + + analyzer = create_sorting_analyzer(sorting=sorting, recording=recording, format="memory") + analyzer.compute(["random_spikes", "waveforms", "templates", "spike_amplitudes"]) + + amplitude_widget = sw.plot_amplitudes( + analyzer, + unit_ids=sorting.unit_ids[:6], + segment_indices=list(range(sorting.get_num_segments())), + plot_histograms=True, + ) + amplitude_widget.figure.canvas.manager.set_window_title("Amplitude output") + amplitude_widget.axes.flatten()[0].set_title("Spike amplitudes") + + def main(): recording, sorting = make_multi_segment_recording() sorting.register_recording(recording) @@ -45,9 +66,26 @@ def main(): # Plot a raster of all segments. segment_indices = list(range(sorting.get_num_segments())) - sw.plot_rasters(sorting, segment_indices=segment_indices) + # sw.plot_rasters(sorting, segment_indices=segment_indices) + # plot_amplitudes_output(recording, sorting) + + + motion_recording = recording.select_segments([0]) + motion, motion_info = compute_motion( + motion_recording, + preset="rigid_fast", + estimate_motion_kwargs=dict(method="decentralized", conv_engine="numpy"), + n_jobs=-1, + progress_bar=True, + output_motion_info=True, + ) + print(motion) + motion_widget = sw.plot_motion_info(motion_info, recording=motion_recording) + motion_widget.figure.canvas.manager.set_window_title("Motion output") + motion_widget.figure.canvas.draw() + motion_widget.figure.show() - plt.show() + plt.show(block=True) if __name__ == "__main__": diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index cc04d51b61..f04bf68366 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -283,6 +283,9 @@ def from_sorting(source_sorting: BaseSorting, with_metadata=False, copy_spike_ve sorting = NumpySorting(spike_vector, source_sorting.get_sampling_frequency(), source_sorting.unit_ids.copy()) if source_sorting.has_recording(): sorting._recording = source_sorting._recording + for source_segment, target_segment in zip(source_sorting._sorting_segments, sorting._sorting_segments): + target_segment._t_start = source_segment._t_start + target_segment._native_t_start = source_segment._native_t_start if with_metadata: source_sorting.copy_metadata(sorting) return sorting diff --git a/src/spikeinterface/core/sorting_tools.py b/src/spikeinterface/core/sorting_tools.py index f37b24046d..2de9dc22d2 100644 --- a/src/spikeinterface/core/sorting_tools.py +++ b/src/spikeinterface/core/sorting_tools.py @@ -447,7 +447,7 @@ def random_spikes_selection( if maximum_rate is None: raise ValueError(f"maximum_rate must be defined") - t_duration = np.sum(get_segment_durations(sorting)) + t_duration = np.sum(get_segment_durations(sorting)[0]) rng_size = min(int(t_duration * maximum_rate), max_spikes_per_unit, all_unit_indices.size) selected_unit_indices = rng.choice(all_unit_indices, size=rng_size, replace=False, shuffle=False) diff --git a/src/spikeinterface/core/tests/test_numpy_extractors.py b/src/spikeinterface/core/tests/test_numpy_extractors.py index 6eb9918b66..81f8c2300a 100644 --- a/src/spikeinterface/core/tests/test_numpy_extractors.py +++ b/src/spikeinterface/core/tests/test_numpy_extractors.py @@ -90,6 +90,21 @@ def test_NumpySorting(setup_NumpyRecording): sorting = NumpySorting.from_sorting(other_sorting) # print(sorting) + recording = generate_recording(num_channels=2, durations=[1.0, 1.0]) + recording.shift_times(shift=5.0, segment_index=0) + recording.shift_times(shift=120.0, segment_index=1) + other_sorting.register_recording(recording) + other_sorting.shift_times(2.0) + sorting_with_times = NumpySorting.from_sorting(other_sorting) + + for segment_index in range(other_sorting.get_num_segments()): + assert sorting_with_times.get_start_time(segment_index) == other_sorting.get_start_time(segment_index) + for unit_id in other_sorting.unit_ids: + assert np.array_equal( + sorting_with_times.get_unit_spike_train(unit_id, segment_index=segment_index, return_times=True), + other_sorting.get_unit_spike_train(unit_id, segment_index=segment_index, return_times=True), + ) + # construct back from kwargs keep the same array sorting2 = load(sorting.to_dict()) assert np.shares_memory(sorting2._cached_spike_vector, sorting._cached_spike_vector) diff --git a/src/spikeinterface/widgets/amplitudes.py b/src/spikeinterface/widgets/amplitudes.py index 374135aeff..91b22c03a6 100644 --- a/src/spikeinterface/widgets/amplitudes.py +++ b/src/spikeinterface/widgets/amplitudes.py @@ -122,14 +122,14 @@ def __init__( bins = 100 # Calculate durations for all segments for x-axis limits - durations = get_segment_durations(sorting, segment_indices) + _, segment_start_stop_times = get_segment_durations(sorting, segment_indices) # Build the plot data with the full dict of dicts structure plot_data = dict( unit_colors=unit_colors, plot_histograms=plot_histograms, bins=bins, - durations=durations, + segment_start_stop_times=segment_start_stop_times, unit_ids=unit_ids, hide_unit_selector=hide_unit_selector, plot_legend=plot_legend, diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index 13e85b3069..c0aec4a449 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -155,7 +155,7 @@ def __init__( color: str = "Gray", clim: tuple[float, float] | None = None, alpha: float = 1, - segment_index: int | list[int] | None = None, + segment_index: int | list[int] | None = None, # TODO: this is no longer used, need to re-insert backend: str | None = None, **backend_kwargs, ): @@ -267,16 +267,9 @@ def __init__( ] else: # Find boundaries between segments using searchsorted - segment_boundaries = [ - np.searchsorted(filtered_peaks["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices - ] - - # Calculate cumulative segment end times from the last sample in each segment - segment_end_times = [ - (filtered_peaks["sample_index"][end - 1] + 1) / sampling_frequency for (_, end) in segment_boundaries - ] - segment_start_times = np.concatenate([[0], segment_end_times[:-1]]) - segment_start_stop_times = list(zip(segment_start_times, segment_end_times)) + _, segment_start_stop_times = compute_segment_durations_from_spike_vector( + filtered_peaks, segment_indices, sampling_frequency + ) plot_data = dict( spike_train_data=spike_train_data, diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index cf5e4d4452..b2e921aa80 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -68,7 +68,6 @@ def __init__( unit_ids: list | None = None, segment_indices: list | None = None, segment_start_stop_times: list[tuple] | None = None, - durations: list | None = None, plot_histograms: bool = False, bins: int | None = None, scatter_decimate: int = 1, @@ -85,16 +84,6 @@ def __init__( backend: str | None = None, **backend_kwargs, ): - if durations is not None and segment_start_stop_times is not None: - raise ValueError( - "`durations` should not be passed with `segment_start_stop_times`. Use `segment_start_stop_times` only." - ) - - if durations is not None and segment_start_stop_times is None: - # This path assumes t_start are zero, which is not a good assumption. This is not really use facing, maybe we remove durations here? - edges = np.r_[0, np.cumsum(durations)] - segment_start_stop_times = list(zip(edges[:-1], edges[1:])) - # Set default segment boundary kwargs if not provided if segment_boundary_kwargs is None: segment_boundary_kwargs = {"color": "gray", "linestyle": "--", "alpha": 0.7} diff --git a/src/spikeinterface/widgets/tests/test_widgets_utils.py b/src/spikeinterface/widgets/tests/test_widgets_utils.py index 3adf31c189..8e8ce0ebe5 100644 --- a/src/spikeinterface/widgets/tests/test_widgets_utils.py +++ b/src/spikeinterface/widgets/tests/test_widgets_utils.py @@ -66,7 +66,7 @@ def test_get_segment_durations(): segment_indices = list(range(sorting.get_num_segments())) # Calculate durations - calculated_durations = get_segment_durations(sorting, segment_indices) + calculated_durations, _ = get_segment_durations(sorting, segment_indices) # Check results assert len(calculated_durations) == len(durations) @@ -84,7 +84,7 @@ def test_get_segment_durations(): firing_rates=15.0, ) - single_duration = get_segment_durations(sorting_single, [0])[0] + single_duration, _ = get_segment_durations(sorting_single, [0])[0] # Test that the calculated duration is reasonable assert single_duration <= 7.0 diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 40da8fd0d0..256a737f17 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -421,10 +421,32 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non spikes = sorting.to_spike_vector() + segment_t_starts = [sorting.get_start_time(seg_idx) for seg_idx in segment_indices] + + return compute_segment_durations_from_spike_vector(spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts) + + +def compute_segment_durations_from_spike_vector( + spike_vector: np.ndarray, segment_indices: list[int], sampling_frequency: float, segment_t_starts: list[float] | None +): + """ + If segment_t_starts is `None` then assume 0 + """ + if segment_t_starts is None: + segment_t_starts = [0] * len(segment_indices) + segment_boundaries = [ - np.searchsorted(spikes["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices + np.searchsorted(spike_vector["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices ] + # TODO: fix this horrible loop + segment_start_stop_times = [ + ( + (spike_vector["sample_index"][start]) / sampling_frequency + segment_t_starts[idx], + (spike_vector["sample_index"][end - 1] + 1) / sampling_frequency + segment_t_starts[idx] + ) for idx, (start, end) in enumerate(segment_boundaries) + ] + + durations = np.array([end-start for (end, start) in segment_start_stop_times]) - durations = [(spikes["sample_index"][end - 1] + 1) / sorting.sampling_frequency for (_, end) in segment_boundaries] + return durations, segment_start_stop_times - return durations From c9e8e778e92eaf2106e0bd3ce68e8887a2f39846 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:44:50 +0000 Subject: [PATCH 09/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- playing.py | 2 +- src/spikeinterface/widgets/utils.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/playing.py b/playing.py index 9e2a4d3a02..2894b60265 100644 --- a/playing.py +++ b/playing.py @@ -77,7 +77,7 @@ def main(): estimate_motion_kwargs=dict(method="decentralized", conv_engine="numpy"), n_jobs=-1, progress_bar=True, - output_motion_info=True, + output_motion_info=True, ) print(motion) motion_widget = sw.plot_motion_info(motion_info, recording=motion_recording) diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 256a737f17..def7cc434b 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -423,11 +423,16 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non segment_t_starts = [sorting.get_start_time(seg_idx) for seg_idx in segment_indices] - return compute_segment_durations_from_spike_vector(spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts) + return compute_segment_durations_from_spike_vector( + spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts + ) def compute_segment_durations_from_spike_vector( - spike_vector: np.ndarray, segment_indices: list[int], sampling_frequency: float, segment_t_starts: list[float] | None + spike_vector: np.ndarray, + segment_indices: list[int], + sampling_frequency: float, + segment_t_starts: list[float] | None, ): """ If segment_t_starts is `None` then assume 0 @@ -442,11 +447,11 @@ def compute_segment_durations_from_spike_vector( segment_start_stop_times = [ ( (spike_vector["sample_index"][start]) / sampling_frequency + segment_t_starts[idx], - (spike_vector["sample_index"][end - 1] + 1) / sampling_frequency + segment_t_starts[idx] - ) for idx, (start, end) in enumerate(segment_boundaries) + (spike_vector["sample_index"][end - 1] + 1) / sampling_frequency + segment_t_starts[idx], + ) + for idx, (start, end) in enumerate(segment_boundaries) ] - durations = np.array([end-start for (end, start) in segment_start_stop_times]) + durations = np.array([end - start for (end, start) in segment_start_stop_times]) return durations, segment_start_stop_times - From 3851db2a66169f4ef5e31b2c223558a256e27d69 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 14:40:36 +0100 Subject: [PATCH 10/23] Update tests. --- .../widgets/tests/test_widgets_utils.py | 18 +++++++++++++++--- src/spikeinterface/widgets/utils.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/spikeinterface/widgets/tests/test_widgets_utils.py b/src/spikeinterface/widgets/tests/test_widgets_utils.py index 8e8ce0ebe5..44da5c210b 100644 --- a/src/spikeinterface/widgets/tests/test_widgets_utils.py +++ b/src/spikeinterface/widgets/tests/test_widgets_utils.py @@ -2,6 +2,7 @@ from spikeinterface import generate_sorting from spikeinterface.widgets.utils import get_some_colors, validate_segment_indices, get_segment_durations +import numpy as np def test_get_some_colors(): @@ -50,23 +51,29 @@ def test_validate_segment_indices(): with pytest.raises(ValueError): validate_segment_indices([5], sorting_multiple) - def test_get_segment_durations(): from spikeinterface import generate_sorting # Test with a normal multi-segment sorting durations = [5.0, 10.0, 15.0] + t_starts = [10, 20, 30] # Create sorting with high fr to ensure spikes near the end segments sorting = generate_sorting( durations=durations, firing_rates=15.0, + t_starts=t_starts, ) segment_indices = list(range(sorting.get_num_segments())) # Calculate durations - calculated_durations, _ = get_segment_durations(sorting, segment_indices) + calculated_durations, segment_start_stop_times = get_segment_durations(sorting, segment_indices) + + # Check results + expected_start_stop_times = np.array([(10, 15), (20, 30), (30, 45)], dtype=float) + start_stop_times = np.array(segment_start_stop_times, dtype=float) + assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Check results assert len(calculated_durations) == len(durations) @@ -82,9 +89,14 @@ def test_get_segment_durations(): sorting_single = generate_sorting( durations=[7.0], firing_rates=15.0, + t_starts=[4], ) - single_duration, _ = get_segment_durations(sorting_single, [0])[0] + single_duration, segment_start_stop_times = get_segment_durations(sorting_single, [0]) + + expected_start_stop_times = [(4, 11)] + start_stop_times = np.array(segment_start_stop_times, dtype=float) + assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Test that the calculated duration is reasonable assert single_duration <= 7.0 diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index def7cc434b..79bb9c24de 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -452,6 +452,6 @@ def compute_segment_durations_from_spike_vector( for idx, (start, end) in enumerate(segment_boundaries) ] - durations = np.array([end - start for (end, start) in segment_start_stop_times]) + durations = np.array([end - start for (start, end) in segment_start_stop_times]) return durations, segment_start_stop_times From 9ead8844e704354316dfc41a2991e11cf60d62d7 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 14:46:32 +0100 Subject: [PATCH 11/23] Add comment. --- src/spikeinterface/core/tests/test_numpy_extractors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spikeinterface/core/tests/test_numpy_extractors.py b/src/spikeinterface/core/tests/test_numpy_extractors.py index 81f8c2300a..73029bb670 100644 --- a/src/spikeinterface/core/tests/test_numpy_extractors.py +++ b/src/spikeinterface/core/tests/test_numpy_extractors.py @@ -90,6 +90,7 @@ def test_NumpySorting(setup_NumpyRecording): sorting = NumpySorting.from_sorting(other_sorting) # print(sorting) + # Verify recording segment offsets and shifted sorting times survive conversion to NumpySorting. recording = generate_recording(num_channels=2, durations=[1.0, 1.0]) recording.shift_times(shift=5.0, segment_index=0) recording.shift_times(shift=120.0, segment_index=1) From 98143d1c267700835cc2c5b66999e3a56ebbcd95 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 14:48:56 +0100 Subject: [PATCH 12/23] Remove unused var. --- src/spikeinterface/widgets/motion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index c0aec4a449..11f5e95cdd 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -155,7 +155,6 @@ def __init__( color: str = "Gray", clim: tuple[float, float] | None = None, alpha: float = 1, - segment_index: int | list[int] | None = None, # TODO: this is no longer used, need to re-insert backend: str | None = None, **backend_kwargs, ): From 539b7ed16f8f92aeff5bf89f0bfda9fae2406233 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 15:15:38 +0100 Subject: [PATCH 13/23] Fix raster widgets. --- src/spikeinterface/widgets/rasters.py | 22 +++++++++++++++++++++- src/spikeinterface/widgets/utils.py | 17 +++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index b2e921aa80..5fb6d354eb 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -455,7 +455,15 @@ def __init__( y_axis_data = {seg_idx: {} for seg_idx in segment_indices} segment_start_stop_times = [] + + # Get the spikes for this segment. Also, if the sorting does not have a recording, + # build the segment start / stop times from the spike times. This is not done + # with get_segment_durations to avoid the conversion to spike_vector. for seg_idx in segment_indices: + + min_spiketime = np.inf + max_spiketime = -np.inf + for unit_id in unit_ids: # Get spikes for this segment and unit spike_times = sorting.get_unit_spike_train_in_seconds(unit_id=unit_id, segment_index=seg_idx) @@ -463,7 +471,19 @@ def __init__( spike_train_data[seg_idx][unit_id] = spike_times y_axis_data[seg_idx][unit_id] = unit_indices_map[unit_id] * np.ones(len(spike_times)) - segment_start_stop_times.append((np.min(spike_times), np.max(spike_times))) + if not sorting.has_recording(): + min_spiketime = np.min(min_spiketime, spike_times.min()) + max_spiketime = np.max(max_spiketime, spike_times.max()) + + if sorting.has_recording(): + segment_start_stop_times.append( + sorting.sorting.recording.get_start_time(seg_idx), + sorting.recording.get_end_time(seg_idx), + ) + else: + segment_start_stop_times.append( + (min_spiketime, max(spike_times)), + ) # Apply time range filtering if specified if time_range is not None: diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 79bb9c24de..25995e0ebb 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -423,6 +423,8 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non segment_t_starts = [sorting.get_start_time(seg_idx) for seg_idx in segment_indices] + # TODO: if the sorting has a recording, shouldn't we use sorting.recording.get_start/end_times() + return compute_segment_durations_from_spike_vector( spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts ) @@ -443,14 +445,13 @@ def compute_segment_durations_from_spike_vector( segment_boundaries = [ np.searchsorted(spike_vector["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices ] - # TODO: fix this horrible loop - segment_start_stop_times = [ - ( - (spike_vector["sample_index"][start]) / sampling_frequency + segment_t_starts[idx], - (spike_vector["sample_index"][end - 1] + 1) / sampling_frequency + segment_t_starts[idx], - ) - for idx, (start, end) in enumerate(segment_boundaries) - ] + + for segment_t_start, (start, end) in zip(segment_t_starts, segment_boundaries): + + segment_start = spike_vector["sample_index"][start] / sampling_frequency + segment_t_start + segment_end = (spike_vector["sample_index"][end - 1] + 1) / sampling_frequency + segment_t_start + + segment_start_stop_times.append((segment_start, segment_end)) durations = np.array([end - start for (start, end) in segment_start_stop_times]) From 48ca1c93c5f5668157ef4451df2d24cb63f679e4 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 15:19:44 +0100 Subject: [PATCH 14/23] Fix rasters. --- src/spikeinterface/widgets/rasters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 5fb6d354eb..251ab07c20 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -477,8 +477,8 @@ def __init__( if sorting.has_recording(): segment_start_stop_times.append( - sorting.sorting.recording.get_start_time(seg_idx), - sorting.recording.get_end_time(seg_idx), + (sorting.get_start_time(seg_idx), + sorting.get_end_time(seg_idx)), ) else: segment_start_stop_times.append( From 25de7b48a8152e554e16b27c88a52469e4a99fb6 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 15:46:09 +0100 Subject: [PATCH 15/23] Convert to dict. --- src/spikeinterface/widgets/motion.py | 8 +-- src/spikeinterface/widgets/rasters.py | 50 ++++++++++--------- .../widgets/tests/test_widgets_utils.py | 6 ++- src/spikeinterface/widgets/utils.py | 21 ++++---- 4 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/spikeinterface/widgets/motion.py b/src/spikeinterface/widgets/motion.py index 11f5e95cdd..36f91a0a0c 100644 --- a/src/spikeinterface/widgets/motion.py +++ b/src/spikeinterface/widgets/motion.py @@ -4,6 +4,7 @@ from spikeinterface.core import BaseRecording, SortingAnalyzer from .rasters import BaseRasterWidget +from .utils import compute_segment_durations_from_spike_vector from spikeinterface.core.motion import Motion @@ -261,9 +262,10 @@ def __init__( # Calculate segment start/stop times for x-axis limits if recording is not None: - segment_start_stop_times = [ - (recording.get_start_time(seg_idx), recording.get_end_time(seg_idx)) for seg_idx in segment_indices - ] + segment_start_stop_times = { + seg_idx: (recording.get_start_time(seg_idx), recording.get_end_time(seg_idx)) + for seg_idx in segment_indices + } else: # Find boundaries between segments using searchsorted _, segment_start_stop_times = compute_segment_durations_from_spike_vector( diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 251ab07c20..82e3a66b31 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -67,7 +67,7 @@ def __init__( sort_by_depth: bool = False, unit_ids: list | None = None, segment_indices: list | None = None, - segment_start_stop_times: list[tuple] | None = None, + segment_start_stop_times: dict[int, tuple[float, float]] | None = None, plot_histograms: bool = False, bins: int | None = None, scatter_decimate: int = 1, @@ -112,9 +112,11 @@ def __init__( all_units.update(spike_train_data[seg_idx].keys()) unit_ids = list(all_units) - segment_start_stop_times_array = [] - for seg in segments_to_use: - segment_start_stop_times_array.extend(segment_start_stop_times[seg]) + # Drop segment times we don't want to use + if segment_start_stop_times is not None: + segment_start_stop_times = { + seg: segment_start_stop_times[seg] for seg in segments_to_use + } # Concatenate data across segments with proper time offsets concatenated_spike_trains = {unit_id: np.array([]) for unit_id in unit_ids} @@ -150,7 +152,7 @@ def __init__( unit_colors=unit_colors, y_label=y_label, title=title, - segment_start_stop_times_array=segment_start_stop_times_array, + segment_start_stop_times=segment_start_stop_times, plot_legend=plot_legend, bins=bins, y_ticks=y_ticks, @@ -226,10 +228,15 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): ax_hist.plot(count, bins[:-1], color=unit_colors[unit_id], alpha=0.8) # Add segment boundary lines if provided - if dp.segment_start_stop_times_array is not None: + if dp.segment_start_stop_times is not None: # We only plot boundaries for sequential segments. - if np.all(np.diff(dp.segment_start_stop_times_array) > 0): - for boundary in dp.segment_start_stop_times_array: + segment_boundaries = np.asarray( + list(dp.segment_start_stop_times.values()), + dtype=float + ).ravel() + + if np.all(np.diff(segment_boundaries) > 0): + for boundary in segment_boundaries: scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) if dp.plot_histograms: @@ -248,8 +255,9 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): scatter_ax.set_ylim(*dp.y_lim) x_lim = dp.x_lim - if x_lim is None and dp.segment_start_stop_times_array is not None: - x_lim = (dp.segment_start_stop_times_array[0], dp.segment_start_stop_times_array[-1]) + if x_lim is None and dp.segment_start_stop_times is not None: + selected_segment_times = list(dp.segment_start_stop_times.values()) + x_lim = (selected_segment_times[0][0], selected_segment_times[-1][1]) scatter_ax.set_xlim(x_lim) if dp.sort_by_depth and dp.depth_dict is not None: @@ -454,7 +462,7 @@ def __init__( spike_train_data = {seg_idx: {} for seg_idx in segment_indices} y_axis_data = {seg_idx: {} for seg_idx in segment_indices} - segment_start_stop_times = [] + segment_start_stop_times = {} # Get the spikes for this segment. Also, if the sorting does not have a recording, # build the segment start / stop times from the spike times. This is not done @@ -471,19 +479,17 @@ def __init__( spike_train_data[seg_idx][unit_id] = spike_times y_axis_data[seg_idx][unit_id] = unit_indices_map[unit_id] * np.ones(len(spike_times)) - if not sorting.has_recording(): - min_spiketime = np.min(min_spiketime, spike_times.min()) - max_spiketime = np.max(max_spiketime, spike_times.max()) + if not sorting.has_recording() and len(spike_times) > 0: + min_spiketime = min(min_spiketime, spike_times[0]) + max_spiketime = max(max_spiketime, spike_times[-1]) if sorting.has_recording(): - segment_start_stop_times.append( - (sorting.get_start_time(seg_idx), - sorting.get_end_time(seg_idx)), + segment_start_stop_times[seg_idx] = ( + sorting.get_start_time(seg_idx), + sorting.get_end_time(seg_idx), ) else: - segment_start_stop_times.append( - (min_spiketime, max(spike_times)), - ) + segment_start_stop_times[seg_idx] = (min_spiketime, max_spiketime) # Apply time range filtering if specified if time_range is not None: @@ -514,7 +520,3 @@ def __init__( ) BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs) - - -# segments do not have times -# no segment start_stop times passed to base raster diff --git a/src/spikeinterface/widgets/tests/test_widgets_utils.py b/src/spikeinterface/widgets/tests/test_widgets_utils.py index 44da5c210b..af2d6a9721 100644 --- a/src/spikeinterface/widgets/tests/test_widgets_utils.py +++ b/src/spikeinterface/widgets/tests/test_widgets_utils.py @@ -72,7 +72,8 @@ def test_get_segment_durations(): # Check results expected_start_stop_times = np.array([(10, 15), (20, 30), (30, 45)], dtype=float) - start_stop_times = np.array(segment_start_stop_times, dtype=float) + assert list(segment_start_stop_times) == segment_indices + start_stop_times = np.array(list(segment_start_stop_times.values()), dtype=float) assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Check results @@ -95,7 +96,8 @@ def test_get_segment_durations(): single_duration, segment_start_stop_times = get_segment_durations(sorting_single, [0]) expected_start_stop_times = [(4, 11)] - start_stop_times = np.array(segment_start_stop_times, dtype=float) + assert list(segment_start_stop_times) == [0] + start_stop_times = np.array(list(segment_start_stop_times.values()), dtype=float) assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Test that the calculated duration is reasonable diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 25995e0ebb..3602f9f07c 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -400,7 +400,7 @@ def validate_segment_indices(segment_indices: list[int] | None, sorting: BaseSor return segment_indices -def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = None) -> list[float]: +def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = None): """ Calculate the duration of each segment in a sorting object. @@ -413,8 +413,10 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non Returns ------- - list[float] - List of segment durations in seconds + durations : np.ndarray + Segment durations in seconds, ordered like ``segment_indices``. + segment_start_stop_times : dict[int, tuple[float, float]] + Start and stop times keyed by segment index. """ if segment_indices is None: segment_indices = range(sorting.get_num_segments()) @@ -423,8 +425,6 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non segment_t_starts = [sorting.get_start_time(seg_idx) for seg_idx in segment_indices] - # TODO: if the sorting has a recording, shouldn't we use sorting.recording.get_start/end_times() - return compute_segment_durations_from_spike_vector( spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts ) @@ -434,7 +434,7 @@ def compute_segment_durations_from_spike_vector( spike_vector: np.ndarray, segment_indices: list[int], sampling_frequency: float, - segment_t_starts: list[float] | None, + segment_t_starts: list[float] | None = None, ): """ If segment_t_starts is `None` then assume 0 @@ -446,13 +446,16 @@ def compute_segment_durations_from_spike_vector( np.searchsorted(spike_vector["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices ] - for segment_t_start, (start, end) in zip(segment_t_starts, segment_boundaries): + segment_start_stop_times = {} + for seg_idx, segment_t_start, (start, end) in zip(segment_indices, segment_t_starts, segment_boundaries): segment_start = spike_vector["sample_index"][start] / sampling_frequency + segment_t_start segment_end = (spike_vector["sample_index"][end - 1] + 1) / sampling_frequency + segment_t_start - segment_start_stop_times.append((segment_start, segment_end)) + segment_start_stop_times[seg_idx] = (segment_start, segment_end) - durations = np.array([end - start for (start, end) in segment_start_stop_times]) + durations = np.array( + [segment_start_stop_times[seg_idx][1] - segment_start_stop_times[seg_idx][0] for seg_idx in segment_indices] + ) return durations, segment_start_stop_times From 5ae61c4941c564adf5fa74ade3b574f75ada41f5 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 15:57:13 +0100 Subject: [PATCH 16/23] Util for conversion to boundaries. --- src/spikeinterface/widgets/rasters.py | 10 +++------- .../widgets/tests/test_widgets_utils.py | 11 ++++++++--- src/spikeinterface/widgets/utils.py | 7 +++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 82e3a66b31..eb40c1ccf9 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -3,7 +3,7 @@ from spikeinterface.core import SortingAnalyzer, BaseSorting from .base import BaseWidget, to_attr, default_backend_kwargs -from .utils import get_some_colors, validate_segment_indices, get_segment_durations +from .utils import get_some_colors, validate_segment_indices, get_segment_durations, segment_start_stop_times_to_boundaries class BaseRasterWidget(BaseWidget): @@ -230,10 +230,7 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): # Add segment boundary lines if provided if dp.segment_start_stop_times is not None: # We only plot boundaries for sequential segments. - segment_boundaries = np.asarray( - list(dp.segment_start_stop_times.values()), - dtype=float - ).ravel() + segment_boundaries = segment_start_stop_times_to_boundaries(dp.segment_start_stop_times) if np.all(np.diff(segment_boundaries) > 0): for boundary in segment_boundaries: @@ -256,8 +253,7 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): x_lim = dp.x_lim if x_lim is None and dp.segment_start_stop_times is not None: - selected_segment_times = list(dp.segment_start_stop_times.values()) - x_lim = (selected_segment_times[0][0], selected_segment_times[-1][1]) + x_lim = (segment_boundaries.min(), segment_boundaries.max()) scatter_ax.set_xlim(x_lim) if dp.sort_by_depth and dp.depth_dict is not None: diff --git a/src/spikeinterface/widgets/tests/test_widgets_utils.py b/src/spikeinterface/widgets/tests/test_widgets_utils.py index af2d6a9721..03c9d89e65 100644 --- a/src/spikeinterface/widgets/tests/test_widgets_utils.py +++ b/src/spikeinterface/widgets/tests/test_widgets_utils.py @@ -1,7 +1,12 @@ import pytest from spikeinterface import generate_sorting -from spikeinterface.widgets.utils import get_some_colors, validate_segment_indices, get_segment_durations +from spikeinterface.widgets.utils import ( + get_some_colors, + validate_segment_indices, + get_segment_durations, + segment_start_stop_times_to_boundaries, +) import numpy as np @@ -73,7 +78,7 @@ def test_get_segment_durations(): # Check results expected_start_stop_times = np.array([(10, 15), (20, 30), (30, 45)], dtype=float) assert list(segment_start_stop_times) == segment_indices - start_stop_times = np.array(list(segment_start_stop_times.values()), dtype=float) + start_stop_times = segment_start_stop_times_to_boundaries(segment_start_stop_times).reshape(-1, 2) assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Check results @@ -97,7 +102,7 @@ def test_get_segment_durations(): expected_start_stop_times = [(4, 11)] assert list(segment_start_stop_times) == [0] - start_stop_times = np.array(list(segment_start_stop_times.values()), dtype=float) + start_stop_times = segment_start_stop_times_to_boundaries(segment_start_stop_times).reshape(-1, 2) assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Test that the calculated duration is reasonable diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 3602f9f07c..7c0764dd6d 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -430,6 +430,13 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non ) +def segment_start_stop_times_to_boundaries( + segment_start_stop_times: dict[int, tuple[float, float]], +) -> np.ndarray: + # The dictionary can follow an out-of-order segment_indices selection, so sort the boundaries chronologically. + return np.sort(np.asarray(list(segment_start_stop_times.values()), dtype=float).ravel()) + + def compute_segment_durations_from_spike_vector( spike_vector: np.ndarray, segment_indices: list[int], From da4456c20f2bef81b9cced1095341944f3104d81 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 17:28:41 +0100 Subject: [PATCH 17/23] Tidy. --- src/spikeinterface/widgets/rasters.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index eb40c1ccf9..e13744eae3 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -227,15 +227,6 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): count, bins = np.histogram(unit_y_data, bins=bins) ax_hist.plot(count, bins[:-1], color=unit_colors[unit_id], alpha=0.8) - # Add segment boundary lines if provided - if dp.segment_start_stop_times is not None: - # We only plot boundaries for sequential segments. - segment_boundaries = segment_start_stop_times_to_boundaries(dp.segment_start_stop_times) - - if np.all(np.diff(segment_boundaries) > 0): - for boundary in segment_boundaries: - scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) - if dp.plot_histograms: ax_hist = self.axes.flatten()[1] ax_hist.set_ylim(scatter_ax.get_ylim()) @@ -252,9 +243,19 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): scatter_ax.set_ylim(*dp.y_lim) x_lim = dp.x_lim - if x_lim is None and dp.segment_start_stop_times is not None: - x_lim = (segment_boundaries.min(), segment_boundaries.max()) - scatter_ax.set_xlim(x_lim) + # Add segment boundary lines if provided and handle x limits + if dp.segment_start_stop_times is not None: + + # We only plot boundaries for sequential segments. + segment_boundaries = segment_start_stop_times_to_boundaries(dp.segment_start_stop_times) + + if np.all(np.diff(segment_boundaries) > 0): + for boundary in segment_boundaries: + scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs) + + if x_lim is None: + x_lim = (segment_boundaries.min(), segment_boundaries.max()) + scatter_ax.set_xlim(x_lim) if dp.sort_by_depth and dp.depth_dict is not None: scatter_ax.set_yticks(ticks=list(range(len(dp.depth_dict))), labels=list(dp.depth_dict.keys())) From c000d5bcd6036e279e14b9434d03971eea2349a9 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 17:31:51 +0100 Subject: [PATCH 18/23] Linting. --- src/spikeinterface/widgets/rasters.py | 13 ++++++++----- .../widgets/tests/test_widgets_utils.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index e13744eae3..89fabeda04 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -3,7 +3,12 @@ from spikeinterface.core import SortingAnalyzer, BaseSorting from .base import BaseWidget, to_attr, default_backend_kwargs -from .utils import get_some_colors, validate_segment_indices, get_segment_durations, segment_start_stop_times_to_boundaries +from .utils import ( + get_some_colors, + validate_segment_indices, + get_segment_durations, + segment_start_stop_times_to_boundaries, +) class BaseRasterWidget(BaseWidget): @@ -114,9 +119,7 @@ def __init__( # Drop segment times we don't want to use if segment_start_stop_times is not None: - segment_start_stop_times = { - seg: segment_start_stop_times[seg] for seg in segments_to_use - } + segment_start_stop_times = {seg: segment_start_stop_times[seg] for seg in segments_to_use} # Concatenate data across segments with proper time offsets concatenated_spike_trains = {unit_id: np.array([]) for unit_id in unit_ids} @@ -244,7 +247,7 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): x_lim = dp.x_lim # Add segment boundary lines if provided and handle x limits - if dp.segment_start_stop_times is not None: + if dp.segment_start_stop_times is not None: # We only plot boundaries for sequential segments. segment_boundaries = segment_start_stop_times_to_boundaries(dp.segment_start_stop_times) diff --git a/src/spikeinterface/widgets/tests/test_widgets_utils.py b/src/spikeinterface/widgets/tests/test_widgets_utils.py index 03c9d89e65..586617b164 100644 --- a/src/spikeinterface/widgets/tests/test_widgets_utils.py +++ b/src/spikeinterface/widgets/tests/test_widgets_utils.py @@ -56,6 +56,7 @@ def test_validate_segment_indices(): with pytest.raises(ValueError): validate_segment_indices([5], sorting_multiple) + def test_get_segment_durations(): from spikeinterface import generate_sorting @@ -98,7 +99,7 @@ def test_get_segment_durations(): t_starts=[4], ) - single_duration, segment_start_stop_times = get_segment_durations(sorting_single, [0]) + single_duration, segment_start_stop_times = get_segment_durations(sorting_single, [0]) expected_start_stop_times = [(4, 11)] assert list(segment_start_stop_times) == [0] From edf00d42efe85a019e89bcc20653eb22ee3b8905 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 17:42:09 +0100 Subject: [PATCH 19/23] Remove stuff now in PR 4796. --- src/spikeinterface/core/numpyextractors.py | 3 --- .../core/tests/test_numpy_extractors.py | 16 ---------------- 2 files changed, 19 deletions(-) diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index f04bf68366..cc04d51b61 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -283,9 +283,6 @@ def from_sorting(source_sorting: BaseSorting, with_metadata=False, copy_spike_ve sorting = NumpySorting(spike_vector, source_sorting.get_sampling_frequency(), source_sorting.unit_ids.copy()) if source_sorting.has_recording(): sorting._recording = source_sorting._recording - for source_segment, target_segment in zip(source_sorting._sorting_segments, sorting._sorting_segments): - target_segment._t_start = source_segment._t_start - target_segment._native_t_start = source_segment._native_t_start if with_metadata: source_sorting.copy_metadata(sorting) return sorting diff --git a/src/spikeinterface/core/tests/test_numpy_extractors.py b/src/spikeinterface/core/tests/test_numpy_extractors.py index 73029bb670..6eb9918b66 100644 --- a/src/spikeinterface/core/tests/test_numpy_extractors.py +++ b/src/spikeinterface/core/tests/test_numpy_extractors.py @@ -90,22 +90,6 @@ def test_NumpySorting(setup_NumpyRecording): sorting = NumpySorting.from_sorting(other_sorting) # print(sorting) - # Verify recording segment offsets and shifted sorting times survive conversion to NumpySorting. - recording = generate_recording(num_channels=2, durations=[1.0, 1.0]) - recording.shift_times(shift=5.0, segment_index=0) - recording.shift_times(shift=120.0, segment_index=1) - other_sorting.register_recording(recording) - other_sorting.shift_times(2.0) - sorting_with_times = NumpySorting.from_sorting(other_sorting) - - for segment_index in range(other_sorting.get_num_segments()): - assert sorting_with_times.get_start_time(segment_index) == other_sorting.get_start_time(segment_index) - for unit_id in other_sorting.unit_ids: - assert np.array_equal( - sorting_with_times.get_unit_spike_train(unit_id, segment_index=segment_index, return_times=True), - other_sorting.get_unit_spike_train(unit_id, segment_index=segment_index, return_times=True), - ) - # construct back from kwargs keep the same array sorting2 = load(sorting.to_dict()) assert np.shares_memory(sorting2._cached_spike_vector, sorting._cached_spike_vector) From 3e9d80555a66bcd9af5d928f5a66836fb2202836 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 18:25:52 +0100 Subject: [PATCH 20/23] Fix test. --- src/spikeinterface/widgets/amplitudes.py | 4 +++- src/spikeinterface/widgets/rasters.py | 3 +-- src/spikeinterface/widgets/utils.py | 8 -------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/spikeinterface/widgets/amplitudes.py b/src/spikeinterface/widgets/amplitudes.py index 91b22c03a6..d8aeedb3a5 100644 --- a/src/spikeinterface/widgets/amplitudes.py +++ b/src/spikeinterface/widgets/amplitudes.py @@ -180,9 +180,11 @@ def plot_figpack(self, data_plot, **backend_kwargs): for u in unit_ids ] + end_time_sec = np.max(list(dp.segment_start_stop_times.values())) + self.view = vv_views.SpikeAmplitudes( start_time_sec=0, - end_time_sec=np.sum(dp.durations), + end_time_sec=end_time_sec, plots=sa_items, # hide_unit_selector=dp.hide_unit_selector, ) diff --git a/src/spikeinterface/widgets/rasters.py b/src/spikeinterface/widgets/rasters.py index 89fabeda04..5e70777e06 100644 --- a/src/spikeinterface/widgets/rasters.py +++ b/src/spikeinterface/widgets/rasters.py @@ -7,7 +7,6 @@ get_some_colors, validate_segment_indices, get_segment_durations, - segment_start_stop_times_to_boundaries, ) @@ -250,7 +249,7 @@ def plot_matplotlib(self, data_plot, **backend_kwargs): if dp.segment_start_stop_times is not None: # We only plot boundaries for sequential segments. - segment_boundaries = segment_start_stop_times_to_boundaries(dp.segment_start_stop_times) + segment_boundaries = np.array(list(dp.segment_start_stop_times.values())).ravel() if np.all(np.diff(segment_boundaries) > 0): for boundary in segment_boundaries: diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 7c0764dd6d..2183bdfa03 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -429,14 +429,6 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts ) - -def segment_start_stop_times_to_boundaries( - segment_start_stop_times: dict[int, tuple[float, float]], -) -> np.ndarray: - # The dictionary can follow an out-of-order segment_indices selection, so sort the boundaries chronologically. - return np.sort(np.asarray(list(segment_start_stop_times.values()), dtype=float).ravel()) - - def compute_segment_durations_from_spike_vector( spike_vector: np.ndarray, segment_indices: list[int], From 033d3a30fad38a37b08de4597cf5869801d2f45d Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 18:29:13 +0100 Subject: [PATCH 21/23] Fix tests. --- src/spikeinterface/widgets/amplitudes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/widgets/amplitudes.py b/src/spikeinterface/widgets/amplitudes.py index d8aeedb3a5..5a129821e8 100644 --- a/src/spikeinterface/widgets/amplitudes.py +++ b/src/spikeinterface/widgets/amplitudes.py @@ -180,10 +180,13 @@ def plot_figpack(self, data_plot, **backend_kwargs): for u in unit_ids ] - end_time_sec = np.max(list(dp.segment_start_stop_times.values())) + segment_times = dp.segment_start_stop_times.values() + + start_time_sec = min(start for start, _ in segment_times) + end_time_sec = max(stop for _, stop in segment_times) self.view = vv_views.SpikeAmplitudes( - start_time_sec=0, + start_time_sec=start_time_sec, end_time_sec=end_time_sec, plots=sa_items, # hide_unit_selector=dp.hide_unit_selector, From 244d985fbc3bc384c61a8b58e87151d665d68b66 Mon Sep 17 00:00:00 2001 From: JoeZiminski Date: Thu, 17 Sep 2026 18:56:01 +0100 Subject: [PATCH 22/23] Fix tests....again. --- src/spikeinterface/widgets/tests/test_widgets_utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/spikeinterface/widgets/tests/test_widgets_utils.py b/src/spikeinterface/widgets/tests/test_widgets_utils.py index 586617b164..1a3868acea 100644 --- a/src/spikeinterface/widgets/tests/test_widgets_utils.py +++ b/src/spikeinterface/widgets/tests/test_widgets_utils.py @@ -5,7 +5,6 @@ get_some_colors, validate_segment_indices, get_segment_durations, - segment_start_stop_times_to_boundaries, ) import numpy as np @@ -79,7 +78,7 @@ def test_get_segment_durations(): # Check results expected_start_stop_times = np.array([(10, 15), (20, 30), (30, 45)], dtype=float) assert list(segment_start_stop_times) == segment_indices - start_stop_times = segment_start_stop_times_to_boundaries(segment_start_stop_times).reshape(-1, 2) + start_stop_times = np.array(list(segment_start_stop_times.values())) assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Check results @@ -103,7 +102,7 @@ def test_get_segment_durations(): expected_start_stop_times = [(4, 11)] assert list(segment_start_stop_times) == [0] - start_stop_times = segment_start_stop_times_to_boundaries(segment_start_stop_times).reshape(-1, 2) + start_stop_times = np.array(list(segment_start_stop_times.values())) assert np.allclose(expected_start_stop_times, start_stop_times, rtol=0, atol=0.1) # Test that the calculated duration is reasonable From 6db46debccca74c604732923bf6890c4abb51a16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:56:36 +0000 Subject: [PATCH 23/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spikeinterface/widgets/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spikeinterface/widgets/utils.py b/src/spikeinterface/widgets/utils.py index 2183bdfa03..3602f9f07c 100644 --- a/src/spikeinterface/widgets/utils.py +++ b/src/spikeinterface/widgets/utils.py @@ -429,6 +429,7 @@ def get_segment_durations(sorting: BaseSorting, segment_indices: list[int] = Non spikes, segment_indices, sorting.get_sampling_frequency(), segment_t_starts ) + def compute_segment_durations_from_spike_vector( spike_vector: np.ndarray, segment_indices: list[int],