Skip to content

Commit 6ba1551

Browse files
authored
Merge pull request #3649 from chrishalcrow/add-spike-location-widget
Add LocationsWidget and plot_locations
2 parents 4f73302 + 8ba7223 commit 6ba1551

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
from warnings import warn
5+
6+
from .base import BaseWidget, to_attr
7+
from .utils import get_some_colors
8+
9+
from ..core.sortinganalyzer import SortingAnalyzer
10+
11+
12+
class LocationsWidget(BaseWidget):
13+
"""
14+
Plots spike locations as a function of time
15+
16+
Parameters
17+
----------
18+
sorting_analyzer : SortingAnalyzer
19+
The input sorting analyzer
20+
unit_ids : list or None, default: None
21+
List of unit ids
22+
segment_index : int or None, default: None
23+
The segment index (or None if mono-segment)
24+
max_spikes_per_unit : int or None, default: None
25+
Number of max spikes per unit to display. Use None for all spikes
26+
plot_histogram : bool, default: False
27+
If True, an histogram of the locations is plotted on the right axis
28+
(matplotlib backend)
29+
bins : int or None, default: None
30+
If plot_histogram is True, the number of bins for the location histogram.
31+
If None this is automatically adjusted
32+
plot_legend : bool, default: True
33+
True includes legend in plot
34+
locations_axis : str, default: 'y'
35+
Which location axis to use when plotting locations.
36+
"""
37+
38+
def __init__(
39+
self,
40+
sorting_analyzer: SortingAnalyzer,
41+
unit_ids=None,
42+
unit_colors=None,
43+
segment_index=None,
44+
max_spikes_per_unit=None,
45+
plot_histograms=False,
46+
bins=None,
47+
plot_legend=True,
48+
locations_axis="y",
49+
backend=None,
50+
**backend_kwargs,
51+
):
52+
53+
sorting_analyzer = self.ensure_sorting_analyzer(sorting_analyzer)
54+
55+
sorting = sorting_analyzer.sorting
56+
self.check_extensions(sorting_analyzer, "spike_locations")
57+
58+
locations = sorting_analyzer.get_extension("spike_locations").get_data(outputs="by_unit")
59+
60+
if unit_ids is None:
61+
unit_ids = sorting.unit_ids
62+
63+
if unit_colors is None:
64+
unit_colors = get_some_colors(sorting.unit_ids)
65+
66+
if sorting.get_num_segments() > 1:
67+
if segment_index is None:
68+
warn("More than one segment available! Using segment_index 0")
69+
segment_index = 0
70+
else:
71+
segment_index = 0
72+
locations_segment = locations[segment_index]
73+
total_duration = sorting_analyzer.get_num_samples(segment_index) / sorting_analyzer.sampling_frequency
74+
75+
spiketrains_segment = {}
76+
for i, unit_id in enumerate(sorting.unit_ids):
77+
times = sorting.get_unit_spike_train(unit_id, segment_index=segment_index)
78+
times = times / sorting.get_sampling_frequency()
79+
spiketrains_segment[unit_id] = times
80+
81+
all_spiketrains = spiketrains_segment
82+
all_locations = locations_segment
83+
if max_spikes_per_unit is not None:
84+
spiketrains_to_plot = dict()
85+
locations_to_plot = dict()
86+
for unit, st in all_spiketrains.items():
87+
locs = all_locations[unit][locations_axis]
88+
if len(st) > max_spikes_per_unit:
89+
random_idxs = np.random.choice(len(st), size=max_spikes_per_unit, replace=False)
90+
spiketrains_to_plot[unit] = st[random_idxs]
91+
locations_to_plot[unit] = locs[random_idxs]
92+
else:
93+
spiketrains_to_plot[unit] = st
94+
locations_to_plot[unit] = locs
95+
else:
96+
spiketrains_to_plot = all_spiketrains
97+
locations_to_plot = {
98+
unit_id: all_locations[unit_id][locations_axis] for unit_id in sorting_analyzer.unit_ids
99+
}
100+
101+
if plot_histograms and bins is None:
102+
bins = 100
103+
104+
plot_data = dict(
105+
sorting_analyzer=sorting_analyzer,
106+
locations=locations_to_plot,
107+
unit_ids=unit_ids,
108+
unit_colors=unit_colors,
109+
spiketrains=spiketrains_to_plot,
110+
total_duration=total_duration,
111+
plot_histograms=plot_histograms,
112+
bins=bins,
113+
plot_legend=plot_legend,
114+
)
115+
116+
BaseWidget.__init__(self, plot_data, backend=backend, **backend_kwargs)
117+
118+
def plot_matplotlib(self, data_plot, **backend_kwargs):
119+
import matplotlib.pyplot as plt
120+
from .utils_matplotlib import make_mpl_figure
121+
122+
dp = to_attr(data_plot)
123+
124+
if backend_kwargs["axes"] is not None:
125+
axes = backend_kwargs["axes"]
126+
if dp.plot_histograms:
127+
assert np.asarray(axes).size == 2
128+
else:
129+
assert np.asarray(axes).size == 1
130+
elif backend_kwargs["ax"] is not None:
131+
assert not dp.plot_histograms
132+
else:
133+
if dp.plot_histograms:
134+
backend_kwargs["num_axes"] = 2
135+
backend_kwargs["ncols"] = 2
136+
else:
137+
backend_kwargs["num_axes"] = None
138+
139+
self.figure, self.axes, self.ax = make_mpl_figure(**backend_kwargs)
140+
141+
scatter_ax = self.axes.flatten()[0]
142+
143+
for unit_id in dp.unit_ids:
144+
spiketrains = dp.spiketrains[unit_id]
145+
locs = dp.locations[unit_id]
146+
scatter_ax.scatter(spiketrains, locs, color=dp.unit_colors[unit_id], s=3, alpha=1, label=unit_id)
147+
148+
if dp.plot_histograms:
149+
if dp.bins is None:
150+
bins = int(len(spiketrains) / 30)
151+
else:
152+
bins = dp.bins
153+
ax_hist = self.axes.flatten()[1]
154+
count, bins = np.histogram(locs, bins=bins)
155+
ax_hist.plot(count, bins[:-1], color=dp.unit_colors[unit_id], alpha=0.8)
156+
157+
if dp.plot_histograms:
158+
ax_hist = self.axes.flatten()[1]
159+
ax_hist.set_ylim(scatter_ax.get_ylim())
160+
ax_hist.axis("off")
161+
# self.figure.tight_layout()
162+
163+
if dp.plot_legend:
164+
if hasattr(self, "legend") and self.legend is not None:
165+
self.legend.remove()
166+
self.legend = self.figure.legend(
167+
loc="upper center", bbox_to_anchor=(0.5, 1.0), ncol=5, fancybox=True, shadow=True
168+
)
169+
170+
scatter_ax.set_xlim(0, dp.total_duration)
171+
scatter_ax.set_xlabel("Times [s]")
172+
scatter_ax.set_ylabel(f"Location [um]")
173+
scatter_ax.spines["top"].set_visible(False)
174+
scatter_ax.spines["right"].set_visible(False)
175+
self.figure.subplots_adjust(bottom=0.1, top=0.9, left=0.1)
176+
177+
def plot_ipywidgets(self, data_plot, **backend_kwargs):
178+
import matplotlib.pyplot as plt
179+
180+
# import ipywidgets.widgets as widgets
181+
import ipywidgets.widgets as W
182+
from IPython.display import display
183+
from .utils_ipywidgets import check_ipywidget_backend, UnitSelector
184+
185+
check_ipywidget_backend()
186+
187+
self.next_data_plot = data_plot.copy()
188+
189+
cm = 1 / 2.54
190+
analyzer = data_plot["sorting_analyzer"]
191+
192+
width_cm = backend_kwargs["width_cm"]
193+
height_cm = backend_kwargs["height_cm"]
194+
195+
ratios = [0.15, 0.85]
196+
197+
with plt.ioff():
198+
output = W.Output()
199+
with output:
200+
self.figure = plt.figure(figsize=((ratios[1] * width_cm) * cm, height_cm * cm))
201+
plt.show()
202+
203+
self.unit_selector = UnitSelector(analyzer.unit_ids)
204+
self.unit_selector.value = list(analyzer.unit_ids)[:1]
205+
206+
self.checkbox_histograms = W.Checkbox(
207+
value=data_plot["plot_histograms"],
208+
description="hist",
209+
)
210+
211+
left_sidebar = W.VBox(
212+
children=[
213+
self.unit_selector,
214+
self.checkbox_histograms,
215+
],
216+
layout=W.Layout(align_items="center", width="100%", height="100%"),
217+
)
218+
219+
self.widget = W.AppLayout(
220+
center=self.figure.canvas,
221+
left_sidebar=left_sidebar,
222+
pane_widths=ratios + [0],
223+
)
224+
225+
# a first update
226+
self._full_update_plot()
227+
228+
self.unit_selector.observe(self._update_plot, names="value", type="change")
229+
self.checkbox_histograms.observe(self._full_update_plot, names="value", type="change")
230+
231+
if backend_kwargs["display"]:
232+
display(self.widget)
233+
234+
def _full_update_plot(self, change=None):
235+
self.figure.clear()
236+
data_plot = self.next_data_plot
237+
data_plot["unit_ids"] = self.unit_selector.value
238+
data_plot["plot_histograms"] = self.checkbox_histograms.value
239+
data_plot["plot_legend"] = False
240+
241+
backend_kwargs = dict(figure=self.figure, axes=None, ax=None)
242+
self.plot_matplotlib(data_plot, **backend_kwargs)
243+
self._update_plot()
244+
245+
def _update_plot(self, change=None):
246+
for ax in self.axes.flatten():
247+
ax.clear()
248+
249+
data_plot = self.next_data_plot
250+
data_plot["unit_ids"] = self.unit_selector.value
251+
data_plot["plot_histograms"] = self.checkbox_histograms.value
252+
data_plot["plot_legend"] = False
253+
254+
backend_kwargs = dict(figure=None, axes=self.axes, ax=None)
255+
self.plot_matplotlib(data_plot, **backend_kwargs)
256+
257+
self.figure.canvas.draw()
258+
self.figure.canvas.flush_events()

src/spikeinterface/widgets/tests/test_widgets.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,30 @@ def test_plot_spike_locations(self):
480480
self.sorting_analyzer_sparse, with_channel_ids=True, backend=backend, **self.backend_kwargs[backend]
481481
)
482482

483+
def test_plot_locations(self):
484+
possible_backends = list(sw.LocationsWidget.get_possible_backends())
485+
for backend in possible_backends:
486+
if backend not in self.skip_backends:
487+
sw.plot_locations(self.sorting_analyzer_dense, backend=backend, **self.backend_kwargs[backend])
488+
unit_ids = self.sorting_analyzer_dense.unit_ids[:4]
489+
sw.plot_locations(
490+
self.sorting_analyzer_dense, unit_ids=unit_ids, backend=backend, **self.backend_kwargs[backend]
491+
)
492+
sw.plot_locations(
493+
self.sorting_analyzer_dense,
494+
unit_ids=unit_ids,
495+
plot_histograms=True,
496+
backend=backend,
497+
**self.backend_kwargs[backend],
498+
)
499+
sw.plot_locations(
500+
self.sorting_analyzer_sparse,
501+
unit_ids=unit_ids,
502+
plot_histograms=True,
503+
backend=backend,
504+
**self.backend_kwargs[backend],
505+
)
506+
483507
def test_plot_similarity(self):
484508
possible_backends = list(sw.TemplateSimilarityWidget.get_possible_backends())
485509
for backend in possible_backends:

src/spikeinterface/widgets/widget_list.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .rasters import RasterWidget
2121
from .sorting_summary import SortingSummaryWidget
2222
from .spike_locations import SpikeLocationsWidget
23+
from .spike_locations_by_time import LocationsWidget
2324
from .spikes_on_traces import SpikesOnTracesWidget
2425
from .template_metrics import TemplateMetricsWidget
2526
from .template_similarity import TemplateSimilarityWidget
@@ -46,6 +47,7 @@
4647
CrossCorrelogramsWidget,
4748
DriftRasterMapWidget,
4849
ISIDistributionWidget,
50+
LocationsWidget,
4951
MotionWidget,
5052
MotionInfoWidget,
5153
MultiCompGlobalAgreementWidget,
@@ -121,6 +123,7 @@
121123
plot_crosscorrelograms = CrossCorrelogramsWidget
122124
plot_drift_raster_map = DriftRasterMapWidget
123125
plot_isi_distribution = ISIDistributionWidget
126+
plot_locations = LocationsWidget
124127
plot_motion = MotionWidget
125128
plot_motion_info = MotionInfoWidget
126129
plot_multicomparison_agreement = MultiCompGlobalAgreementWidget

0 commit comments

Comments
 (0)