Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 92 additions & 0 deletions playing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Generate a multi-segment recording whose segments start at non-zero times,
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, 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


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 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)

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)
# 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(block=True)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion src/spikeinterface/core/sorting_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 9 additions & 4 deletions src/spikeinterface/widgets/amplitudes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -180,9 +180,14 @@ def plot_figpack(self, data_plot, **backend_kwargs):
for u in unit_ids
]

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,
end_time_sec=np.sum(dp.durations),
start_time_sec=start_time_sec,
end_time_sec=end_time_sec,
plots=sa_items,
# hide_unit_selector=dp.hide_unit_selector,
)
Expand Down
27 changes: 14 additions & 13 deletions src/spikeinterface/widgets/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -155,7 +156,6 @@ def __init__(
color: str = "Gray",
clim: tuple[float, float] | None = None,
alpha: float = 1,
segment_index: int | list[int] | None = None,

@JoeZiminski JoeZiminski Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused in main, but I guess this is public facing and will need a deprecation warning? (although maybe not if its never used anyway)

backend: str | None = None,
**backend_kwargs,
):
Expand Down Expand Up @@ -229,7 +229,10 @@ 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}
Expand Down Expand Up @@ -257,19 +260,17 @@ 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 = [recording.get_duration(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_boundaries = [
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 = [
(filtered_peaks["sample_index"][end - 1] + 1) / sampling_frequency for (_, end) in segment_boundaries
]
_, 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,
Expand All @@ -280,7 +281,7 @@ def __init__(
scatter_decimate=scatter_decimate,
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)
Expand Down
81 changes: 49 additions & 32 deletions src/spikeinterface/widgets/rasters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

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,
)


class BaseRasterWidget(BaseWidget):
Expand Down Expand Up @@ -67,7 +71,7 @@ def __init__(
sort_by_depth: bool = False,
unit_ids: list | None = None,
segment_indices: list | None = None,
durations: list | 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,
Expand All @@ -84,7 +88,6 @@ def __init__(
backend: str | None = None,
**backend_kwargs,
):

# Set default segment boundary kwargs if not provided
if segment_boundary_kwargs is None:
segment_boundary_kwargs = {"color": "gray", "linestyle": "--", "alpha": 0.7}
Expand Down Expand Up @@ -113,16 +116,15 @@ def __init__(
all_units.update(spike_train_data[seg_idx].keys())
unit_ids = list(all_units)

# Calculate cumulative durations for segment boundaries
segment_boundaries = np.cumsum(durations)
cumulative_durations = np.concatenate([[0], segment_boundaries])
# 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}
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],
):
Expand All @@ -134,13 +136,8 @@ 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], adjusted_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(
Expand All @@ -157,12 +154,11 @@ def __init__(
unit_colors=unit_colors,
y_label=y_label,
title=title,
durations=durations,
segment_start_stop_times=segment_start_stop_times,
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,
)

Expand Down Expand Up @@ -233,11 +229,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 getattr(dp, "segment_boundaries", None) is not None:
for boundary in dp.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())
Expand All @@ -253,9 +244,20 @@ 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)

# 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 = np.array(list(dp.segment_start_stop_times.values())).ravel()

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()))
Expand Down Expand Up @@ -455,24 +457,39 @@ 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}

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(unit_id=unit_id, segment_index=seg_idx) / sorting.sampling_frequency
)

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))

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[seg_idx] = (
sorting.get_start_time(seg_idx),
sorting.get_end_time(seg_idx),
)
else:
segment_start_stop_times[seg_idx] = (min_spiketime, max_spiketime)

# 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"
Expand All @@ -498,7 +515,7 @@ def __init__(
unit_colors=unit_colors,
plot_histograms=None,
y_ticks=y_ticks,
durations=durations,
segment_start_stop_times=segment_start_stop_times,
)

BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs)
Loading
Loading