diff --git a/spikeinterface_gui/backend_panel.py b/spikeinterface_gui/backend_panel.py index 39e3fa2..bf28cb8 100644 --- a/spikeinterface_gui/backend_panel.py +++ b/spikeinterface_gui/backend_panel.py @@ -2,12 +2,11 @@ import param import panel as pn -import numpy as np -from copy import copy from .viewlist import get_all_possible_views -from .layout_presets import get_layout_description from .utils_global import fill_unnecessary_space, get_present_zones_in_half_of_layout + + # Used by views to emit/trigger signals class SignalNotifier(param.Parameterized): spike_selection_changed = param.Event() diff --git a/spikeinterface_gui/basescatterview.py b/spikeinterface_gui/basescatterview.py index 9f9c5d4..d38d7e7 100644 --- a/spikeinterface_gui/basescatterview.py +++ b/spikeinterface_gui/basescatterview.py @@ -18,6 +18,7 @@ class BaseScatterView(ViewBase): {'name': 'range_type', 'type': 'list', 'limits': ['percentiles', 'absolute']}, {'name': 'range_min', 'type': 'float', 'value' : 1.0}, {'name': 'range_max', 'type': 'float', 'value' : 99.0}, + {'name': 'cache_data', 'type': 'bool', 'value' : False} ] _need_compute = False @@ -39,17 +40,62 @@ def __init__(self, spike_data, y_label, controller=None, parent=None, backend="q self._current_selected = 0 self._block_auto_refresh_and_notify = False self._first_refresh_done = False + self._cache_data = {} ViewBase.__init__(self, controller=controller, parent=parent, backend=backend) self.valid_period_regions = [] + # settings are only available after ViewBase.__init__() + self.on_decimate_or_cache_change() + + def on_decimate_or_cache_change(self, *args): + if self.settings["cache_data"]: + self.cache_data() + else: + self._cache_data = {} + + def cache_data(self): + if not self.settings['cache_data']: + return + + # load once: per-unit fancy indexing on lazy (zarr) arrays re-fetches every chunk for each unit + all_sample_indices = np.asarray(self.controller.spikes["sample_index"]) + all_spike_data = np.asarray(self.spike_data) + + if self.controller.verbose: + print(f"Caching data scatter data in: {self.__class__.__name__}") + for unit_id in self.controller.unit_ids: + self._cache_data[unit_id] = [] + for segment_index in range(self.controller.num_segments): + inds = self.controller.get_spike_indices(unit_id, segment_index=segment_index) + spike_indices = all_sample_indices[inds] + spike_times = self.controller.sample_index_to_time(spike_indices) + if self.settings["auto_decimate"]: + decimate_factor = max(1, len(inds) // self.settings['max_spikes_per_unit']) + spike_times = spike_times[::decimate_factor] + inds = inds[::decimate_factor] + spike_data = all_spike_data[inds] + self._cache_data[unit_id].append(dict(times=spike_times, spike_data=spike_data, inds=inds)) + def get_unit_data(self, unit_id, segment_index=0): - inds = self.controller.get_spike_indices(unit_id, segment_index=segment_index) - spike_indices = self.controller.spikes["sample_index"][inds] - spike_times = self.controller.sample_index_to_time(spike_indices) - spike_data = self.spike_data[inds] + if unit_id in self._cache_data: + cached = self._cache_data[unit_id][segment_index] + spike_times = cached['times'] + spike_data = cached['spike_data'] + inds = cached['inds'] + else: + inds = self.controller.get_spike_indices(unit_id, segment_index=segment_index) + spike_indices = self.controller.spikes["sample_index"][inds] + spike_times = self.controller.sample_index_to_time(spike_indices) + spike_data = self.spike_data[inds] + + if self.settings["auto_decimate"] and spike_times.size > self.settings['max_spikes_per_unit']: + step = spike_times.size // self.settings['max_spikes_per_unit'] + spike_times = spike_times[::step] + spike_data = spike_data[::step] + inds = inds[::step] if len(spike_data) == 1: spike_data_value = spike_data[0] @@ -85,13 +131,6 @@ def get_unit_data(self, unit_id, segment_index=0): hist_count, hist_bins = np.histogram(spike_data, bins=bins) - if self.settings["auto_decimate"] and spike_times.size > self.settings['max_spikes_per_unit']: - step = spike_times.size // self.settings['max_spikes_per_unit'] - spike_times = spike_times[::step] - spike_data = spike_data[::step] - inds = inds[::step] - - return spike_times, spike_data, hist_count, hist_bins, ymin, ymax, inds def get_selected_spikes_data(self, segment_index=0, visible_inds=None): @@ -104,7 +143,6 @@ def get_selected_spikes_data(self, segment_index=0, visible_inds=None): spike_data = self.spike_data[in_seg] return (spike_times, spike_data) - def select_all_spikes_from_lasso(self, keep_already_selected=False): """ Select all spikes within the lasso vertices. @@ -193,11 +231,6 @@ def _perform_split(self, visible_unit_id): self._lasso_vertices = {segment_index: None for segment_index in range(self.controller.num_segments)} self.refresh() self.notify_manual_curation_updated() - - - def _on_settings_changed(self): - self.refresh(set_scatter_range=True) - def on_unit_visibility_changed(self): self._lasso_vertices = {segment_index: None for segment_index in range(self.controller.num_segments)} @@ -215,6 +248,9 @@ def on_spike_selection_changed(self): def on_use_times_updated(self): self.refresh(set_scatter_range=True) + def _on_settings_changed(self): + self.refresh(set_scatter_range=True) + ## QT zone ## def _qt_make_layout(self): from .myqt import QT @@ -262,6 +298,8 @@ def _qt_make_layout(self): self.plot.addItem(self.scatter_select) self.scatter_select.setZValue(1000) + for setting_name in ('cache_data', 'max_spikes_per_unit', 'auto_decimate'): + self.settings.param(setting_name).sigValueChanged.connect(self.on_decimate_or_cache_change) def _qt_initialize_plot(self): import pyqtgraph as pg @@ -546,6 +584,9 @@ def _panel_make_layout(self): ) self.plotted_inds = [] + for setting_name in ('cache_data', 'max_spikes_per_unit', 'auto_decimate'): + self.settings._parameterized.param.watch(self.on_decimate_or_cache_change, setting_name) + def _panel_refresh(self, set_scatter_range=False): import panel as pn from bokeh.models import FixedTicker @@ -642,7 +683,6 @@ def _panel_refresh(self, set_scatter_range=False): self.hist_fig.x_range.end = max_count self.hist_fig.xaxis.ticker = FixedTicker(ticks=[0, max_count // 2, max_count]) - def _panel_on_select_button(self, event): import panel as pn diff --git a/spikeinterface_gui/controller.py b/spikeinterface_gui/controller.py index 24cf3a5..c765f60 100644 --- a/spikeinterface_gui/controller.py +++ b/spikeinterface_gui/controller.py @@ -8,7 +8,7 @@ from copy import deepcopy from spikeinterface import compute_sparsity -from spikeinterface.core.sorting_tools import spike_vector_to_indices +from spikeinterface.core.base import minimum_spike_dtype from spikeinterface.curation import validate_curation_dict, apply_curation from spikeinterface.curation.curation_model import Curation from spikeinterface.widgets.utils import make_units_table_from_analyzer @@ -19,9 +19,6 @@ from .curation_tools import add_merge, default_label_definitions, empty_curation_data from .event_tools import parse_events -spike_dtype =[('sample_index', 'int64'), ('unit_index', 'int64'), - ('channel_index', 'int64'), ('segment_index', 'int64'), - ('visible', 'bool'), ('selected', 'bool'), ('rand_selected', 'bool')] _default_main_settings = dict( @@ -140,10 +137,10 @@ def set_analyzer_info(self, analyzer): temp_ext = self.analyzer.compute_one_extension("templates") self.nbefore, self.nafter = temp_ext.nbefore, temp_ext.nafter - self.templates_average = temp_ext.get_templates(operator='average') + self.templates_average = np.asarray(temp_ext.get_templates(operator='average')) if 'std' in temp_ext.params['operators']: - self.templates_std = temp_ext.get_templates(operator='std') + self.templates_std = np.asarray(temp_ext.get_templates(operator='std')) else: self.templates_std = None @@ -163,7 +160,7 @@ def set_analyzer_info(self, analyzer): if ext is None and self.has_extension('recording'): print('Force compute "noise_levels" is needed') ext = analyzer.compute_one_extension('noise_levels') - self.noise_levels = ext.get_data() if ext is not None else None + self.noise_levels = np.asarray(ext.get_data()) if ext is not None else None if "quality_metrics" in self.skip_extensions: if self.verbose: @@ -227,7 +224,8 @@ def set_analyzer_info(self, analyzer): print('\tLoading correlograms') ccg_ext = analyzer.get_extension('correlograms') if ccg_ext is not None: - self.correlograms, self.correlograms_bins = ccg_ext.get_data() + # materialize: views index these per selection, which is slow on lazy (remote) arrays + self.correlograms, self.correlograms_bins = (np.asarray(d) for d in ccg_ext.get_data()) else: self.correlograms, self.correlograms_bins = None, None @@ -241,7 +239,7 @@ def set_analyzer_info(self, analyzer): print('\tLoading isi_histograms') isi_ext = analyzer.get_extension('isi_histograms') if isi_ext is not None: - self.isi_histograms, self.isi_bins = isi_ext.get_data() + self.isi_histograms, self.isi_bins = (np.asarray(d) for d in isi_ext.get_data()) else: self.isi_histograms, self.isi_bins = None, None @@ -255,7 +253,7 @@ def set_analyzer_info(self, analyzer): ts_ext = analyzer.get_extension('template_similarity') if ts_ext is not None: method = ts_ext.params["method"] - self._similarity_by_method[method] = ts_ext.get_data() + self._similarity_by_method[method] = np.asarray(ts_ext.get_data()) else: if len(self.unit_ids) <= 64 and len(self.channel_ids) <= 64: # precompute similarity when low channel/units count @@ -286,11 +284,15 @@ def set_analyzer_info(self, analyzer): pc_ext = analyzer.get_extension('principal_components') self.pc_ext = pc_ext - if analyzer.has_extension("valid_unit_periods"): + self.valid_periods = None + if "valid_unit_periods" in self.skip_extensions: + if self.verbose: + print('\tSkipping valid_unit_periods') + elif analyzer.has_extension("valid_unit_periods"): + if self.verbose: + print('\tLoading valid unit periods') valid_periods_ext = analyzer.get_extension("valid_unit_periods") self.valid_periods = valid_periods_ext.get_data(outputs="by_unit") - else: - self.valid_periods = None self._potential_merges = None # some direct attribute @@ -321,44 +323,57 @@ def set_analyzer_info(self, analyzer): unit_ids = self.analyzer.unit_ids num_seg = self.analyzer.get_num_segments() self.num_spikes = self.analyzer.sorting.count_num_spikes_per_unit(outputs="dict") - # print("self.num_spikes", self.num_spikes) - spike_vector = self.analyzer.sorting.to_spike_vector( - concatenated=True, main_channel_indices=self.analyzer.get_main_channels(outputs="index", with_dict=False) - ) - - self.random_spikes_indices = self.analyzer.get_extension("random_spikes").get_data() - - # align=True is required for np.searchsorted (and therefore trace - # views) to be fast. - self.spikes = np.zeros(spike_vector.size, dtype=np.dtype(spike_dtype, align=True)) - self.spikes['sample_index'] = spike_vector['sample_index'] - self.spikes['unit_index'] = spike_vector['unit_index'] - self.spikes['segment_index'] = spike_vector['segment_index'] - self.spikes['channel_index'] = spike_vector['channel_index'] - self.spikes['rand_selected'][:] = False - self.spikes['rand_selected'][self.random_spikes_indices] = True - - # self.num_spikes = self.analyzer.sorting.count_num_spikes_per_unit(outputs="dict") - seg_limits = np.searchsorted(self.spikes["segment_index"], np.arange(num_seg + 1)) - self.segment_slices = {segment_index: slice(seg_limits[segment_index], seg_limits[segment_index + 1]) for segment_index in range(num_seg)} - - spike_vector2 = self.analyzer.sorting.to_spike_vector(concatenated=False) - self.final_spike_samples = [segment_spike_vector[-1][0] for segment_spike_vector in spike_vector2] - # this is dict of list because per segment spike_indices[segment_index][unit_id] - spike_indices_abs = spike_vector_to_indices(spike_vector2, unit_ids, absolute_index=True) - spike_indices = spike_vector_to_indices(spike_vector2, unit_ids) - # this is flatten - spike_per_seg = [s.size for s in spike_vector2] - # dict[unit_id] -> all indices for this unit across segments - self._spike_index_by_units = {} - # dict[segment_index][unit_id] -> all indices for this unit for one segment - self._spike_index_by_segment_and_units = spike_indices_abs - for unit_id in unit_ids: - inds = [] - for seg_ind in range(num_seg): - inds.append(spike_indices[seg_ind][unit_id] + int(np.sum(spike_per_seg[:seg_ind]))) - self._spike_index_by_units[unit_id] = np.concatenate(inds) + if self.analyzer._lazy: + # If the analyzer is lazy, avoid materializing the full spike vector, which can be very large. + self.spikes = self.analyzer.sorting.to_spike_vector() + else: + # In this case we make a copy with align=True, which is required for np.searchsorted + # (and therefore trace views) to be fast. + spike_vector = self.analyzer.sorting.to_spike_vector() + self.spikes = np.zeros(spike_vector.size, dtype=np.dtype(minimum_spike_dtype, align=True)) + self.spikes['sample_index'] = spike_vector['sample_index'] + self.spikes['unit_index'] = spike_vector['unit_index'] + self.spikes['segment_index'] = spike_vector['segment_index'] + + # bounded by max_spikes_per_unit * num_units, so safe to materialize even in lazy mode + self.random_spikes_indices = np.asarray(self.analyzer.get_extension("random_spikes").get_data()) + self._random_spikes_set = set(int(i) for i in self.random_spikes_indices) + + self._ext_channel_inds = np.array([self._main_channels[unit_id] for unit_id in self.unit_ids]) + + cached = self.analyzer.sorting._cached_spike_vector_segment_slices + if cached is not None: + # shape (num_seg, 2): columns are [start, stop] + self.segment_slices = {seg: slice(int(cached[seg, 0]), int(cached[seg, 1])) for seg in range(num_seg)} + else: + bounds = np.searchsorted(np.asarray(self.spikes["segment_index"]), np.arange(num_seg + 1)) + self.segment_slices = {seg: slice(int(bounds[seg]), int(bounds[seg + 1])) for seg in range(num_seg)} + + # Load unit_index once to build per-unit lookup structures, avoiding a + # second to_spike_vector() call that would materialise full structured + # arrays from zarr for every segment. + unit_index_all = np.asarray(self.spikes["unit_index"]) + + # last sample per segment: one cheap element read instead of full materialisation + sample_index_arr = self.spikes["sample_index"] + self.final_spike_samples = [int(sample_index_arr[self.segment_slices[seg].stop - 1]) for seg in range(num_seg)] + + # dict[segment_index][unit_id] -> absolute spike indices for that unit in that segment + num_units = len(unit_ids) + self._spike_index_by_segment_and_units = {} + for seg_ind in range(num_seg): + sl = self.segment_slices[seg_ind] + seg_unit_idx = unit_index_all[sl] + abs_offset = sl.start + order = np.argsort(seg_unit_idx, stable=True) + sorted_unit = seg_unit_idx[order] + sorted_abs = (order + abs_offset).astype(np.int64) + boundaries = np.searchsorted(sorted_unit, np.arange(num_units + 1, dtype=np.int64)) + self._spike_index_by_segment_and_units[seg_ind] = { + uid: sorted_abs[boundaries[u]:boundaries[u + 1]].copy() + for u, uid in enumerate(unit_ids) + } t1 = time.perf_counter() if self.verbose: @@ -710,9 +725,10 @@ def get_dict_unit_visible(self): def update_visible_spikes(self): inds = [] - for unit_index, unit_id in self.iter_visible_units(): - inds.append(self._spike_index_by_units[unit_id]) - + for _, unit_id in self.iter_visible_units(): + for seg_ind in self._spike_index_by_segment_and_units: + inds.append(self._spike_index_by_segment_and_units[seg_ind][unit_id]) + if len(inds) > 0: inds = np.concatenate(inds) inds = np.sort(inds) @@ -739,10 +755,11 @@ def set_indices_spike_selected(self, inds): def get_spike_indices(self, unit_id, segment_index=None): if segment_index is None: - # dict[unit_id] -> all indices for this unit across segments - return self._spike_index_by_units[unit_id] + return np.concatenate([ + self._spike_index_by_segment_and_units[seg][unit_id] + for seg in sorted(self._spike_index_by_segment_and_units) + ]) else: - # dict[segment_index][unit_id] -> all indices for this unit for one segment return self._spike_index_by_segment_and_units[segment_index][unit_id] def get_num_samples(self, segment_index): diff --git a/spikeinterface_gui/spikelistview.py b/spikeinterface_gui/spikelistview.py index c8cd73c..1149d8a 100644 --- a/spikeinterface_gui/spikelistview.py +++ b/spikeinterface_gui/spikelistview.py @@ -53,6 +53,8 @@ def data(self, index, role): abs_ind = self.visible_ind[row] spike = self.controller.spikes[abs_ind] + channel_index = self.controller._ext_channel_inds[spike['unit_index']] + rand_selected = int(abs_ind) in self.controller._random_spikes_set unit_id = self.controller.unit_ids[spike['unit_index']] if role ==QT.Qt.DisplayRole : @@ -65,9 +67,9 @@ def data(self, index, role): elif col == 3: return '{}'.format(spike['sample_index']) elif col == 4: - return '{}'.format(spike['channel_index']) + return '{}'.format(channel_index) elif col == 5: - return '{}'.format(spike['rand_selected']) + return '{}'.format(rand_selected) else: return None elif role == QT.Qt.DecorationRole : @@ -316,14 +318,17 @@ def _panel_refresh_table(self): color = mcolors.to_hex(self.controller.get_unit_color(unit_id)) spike_unit_ids.append({"id": unit_id, "color": color}) + channel_inds = self.controller._ext_channel_inds[spikes['unit_index']] + rand_selected = np.isin(visible_inds, self.controller.random_spikes_indices) + # Prepare data for tabulator data = { '#': visible_inds, 'unit_id': spike_unit_ids, 'segment_index': spikes['segment_index'], 'sample_index': spikes['sample_index'], - 'channel_index': spikes['channel_index'], - 'rand_selected': spikes['rand_selected'] + 'channel_index': channel_inds, + 'rand_selected': rand_selected } # Update table data without replacing entire dataframe diff --git a/spikeinterface_gui/spikerateview.py b/spikeinterface_gui/spikerateview.py index d76ddf7..dbf9201 100644 --- a/spikeinterface_gui/spikerateview.py +++ b/spikeinterface_gui/spikerateview.py @@ -77,7 +77,7 @@ def _qt_refresh(self): for r, unit_id in enumerate(visible_unit_ids): spike_inds = self.controller.get_spike_indices(unit_id, segment_index=segment_index) - spikes = self.controller.spikes[spike_inds]['sample_index'] + spikes = self.controller.spikes['sample_index'][spike_inds] count, _ = np.histogram(spikes, bins=bin_edges) @@ -161,7 +161,7 @@ def _panel_refresh(self): for unit_id in visible_unit_ids: spike_inds = self.controller.get_spike_indices(unit_id, segment_index=segment_index) - spikes = self.controller.spikes[spike_inds]['sample_index'] + spikes = self.controller.spikes['sample_index'][spike_inds] count, _ = np.histogram(spikes, bins=bin_edges) diff --git a/spikeinterface_gui/tests/test_mainwindow_panel.py b/spikeinterface_gui/tests/test_mainwindow_panel.py index 3fa381b..a304f7f 100644 --- a/spikeinterface_gui/tests/test_mainwindow_panel.py +++ b/spikeinterface_gui/tests/test_mainwindow_panel.py @@ -20,9 +20,9 @@ def teardown_module(): clean_all(test_folder) -def test_mainwindow(start_app=False, verbose=True, curation=False, only_some_extensions=False, events=False, iterative_curation=False, layout="default", port=0): +def test_mainwindow(start_app=False, verbose=True, curation=False, only_some_extensions=False, events=False, port=0, iterative_curation=False, layout="default", lazy_load=False): - analyzer = load_sorting_analyzer(test_folder / "sorting_analyzer") + analyzer = load_sorting_analyzer(test_folder / "sorting_analyzer", load_extensions=False, lazy=lazy_load) # analyzer = load_analyzer(test_folder / "sorting_analyzer.zarr") print(analyzer) @@ -114,6 +114,7 @@ def test_launcher(verbose=True): parser.add_argument('--events', action="store_true", help='Simulate and add events') parser.add_argument('--iterative', action="store_true", help='Run iterative curation') parser.add_argument('--layout', default="default", help='Layout of the GUI, default is "default"') +parser.add_argument('--lazy', action="store_true", help='Lazy load') if __name__ == '__main__': args = parser.parse_args() @@ -125,9 +126,16 @@ def test_launcher(verbose=True): if not test_folder.is_dir(): setup_module() - iterative_curation = args.iterative - - win = test_mainwindow(start_app=True, verbose=True, curation=True, events=args.events, iterative_curation=iterative_curation, layout=args.layout, port=0) + win = test_mainwindow( + start_app=True, + verbose=True, + curation=True, + events=args.events, + port=0, + layout=args.layout, + iterative_curation=args.iterative, + lazy_load=args.lazy + ) # test_launcher(verbose=True) diff --git a/spikeinterface_gui/tests/test_mainwindow_qt.py b/spikeinterface_gui/tests/test_mainwindow_qt.py index 919cd32..efb18ed 100644 --- a/spikeinterface_gui/tests/test_mainwindow_qt.py +++ b/spikeinterface_gui/tests/test_mainwindow_qt.py @@ -21,10 +21,9 @@ def teardown_module(): clean_all(test_folder) -def test_mainwindow(start_app=False, verbose=True, curation=False, only_some_extensions=False, events=False, iterative_curation=False, layout="default"): +def test_mainwindow(start_app=False, verbose=True, curation=False, only_some_extensions=False, events=False, iterative_curation=False, layout="default", lazy_load=False): - - analyzer = load_sorting_analyzer(test_folder / "sorting_analyzer") + analyzer = load_sorting_analyzer(test_folder / "sorting_analyzer", load_extensions=False, lazy=lazy_load) # analyzer = load_analyzer(test_folder / "sorting_analyzer.zarr") @@ -94,7 +93,7 @@ def test_mainwindow(start_app=False, verbose=True, curation=False, only_some_ext displayed_unit_properties=None, extra_unit_properties=extra_unit_properties, layout_preset=layout, - events=events_dict + events=events_dict, # user_settings={"mainsettings": {"color_mode": "color_by_visibility", "max_visible_units": 5}} ) @@ -131,6 +130,7 @@ def test_launcher(verbose=True): parser.add_argument('--events', action="store_true", help='Simulate and add events') parser.add_argument('--iterative', action="store_true", help='Run iterative curation') parser.add_argument('--layout', default="default", help='Enable layout preset') +parser.add_argument('--lazy', action="store_true", help='Lazy load') if __name__ == '__main__': args = parser.parse_args() @@ -142,9 +142,15 @@ def test_launcher(verbose=True): if not test_folder.is_dir(): setup_module() - iterative_curation = args.iterative - - win = test_mainwindow(start_app=True, verbose=True, curation=True, events=args.events, iterative_curation=iterative_curation, layout=args.layout) + win = test_mainwindow( + start_app=True, + verbose=True, + curation=True, + events=args.events, + layout=args.layout, + iterative_curation=args.iterative, + lazy_load=args.lazy + ) # win = test_mainwindow(start_app=True, verbose=True, curation=False) # test_launcher(verbose=True) diff --git a/spikeinterface_gui/traceview.py b/spikeinterface_gui/traceview.py index abe3926..ced41d0 100644 --- a/spikeinterface_gui/traceview.py +++ b/spikeinterface_gui/traceview.py @@ -38,6 +38,7 @@ def get_data_in_chunk(self, t1, t2, segment_index): spikes_seg = self.controller.spikes[sl] i1, i2 = np.searchsorted(spikes_seg["sample_index"], [ind1, ind2]) spikes_chunk = spikes_seg[i1:i2].copy() + spikes_channel_chunk = self.controller._ext_channel_inds[spikes_chunk["unit_index"]] spikes_chunk["sample_index"] -= ind1 # for trace map view, this returns the channels ordered by depth @@ -73,7 +74,7 @@ def get_data_in_chunk(self, t1, t2, segment_index): # Get spikes for this unit unit_spikes = spikes_chunk[inds] - channel_inds = unit_spikes["channel_index"] + channel_inds = spikes_channel_chunk[inds] sample_inds = unit_spikes["sample_index"] chan_mask = np.isin(channel_inds, visible_channel_inds) @@ -937,14 +938,15 @@ def find_nearest_spike(controller, x, segment_index, max_distance_samples=None): max_distance_samples = controller.sampling_frequency // 30 ind_click = controller.time_to_sample_index(x) - (in_seg,) = np.nonzero(controller.spikes["segment_index"] == segment_index) + sl = controller.segment_slices[segment_index] - if len(in_seg) == 0: + if sl.start == sl.stop: return None - nearest = np.argmin(np.abs(controller.spikes[in_seg]["sample_index"] - ind_click)) - ind_spike_nearest = in_seg[nearest] - sample_index = controller.spikes[ind_spike_nearest]["sample_index"] + sample_index_seg = np.asarray(controller.spikes["sample_index"][sl]) + nearest_in_seg = np.argmin(np.abs(sample_index_seg - ind_click)) + ind_spike_nearest = sl.start + nearest_in_seg + sample_index = sample_index_seg[nearest_in_seg] if np.abs(ind_click - sample_index) > max_distance_samples: return None diff --git a/spikeinterface_gui/view_base.py b/spikeinterface_gui/view_base.py index 6303e36..b9f4547 100644 --- a/spikeinterface_gui/view_base.py +++ b/spikeinterface_gui/view_base.py @@ -108,14 +108,14 @@ def is_view_active(self): return self._panel_view_is_active def refresh(self, **kwargs): - if self.controller.verbose: - t0 = time.perf_counter() if not self.is_view_visible(): return - self._refresh(**kwargs) - if self.controller.verbose: - t1 = time.perf_counter() - print(f"Refresh {self.__class__.__name__} took {t1 - t0:.3f} seconds", flush=True) + if self.controller.verbose and self.backend == "qt": + t0 = time.perf_counter() + self._refresh(**kwargs) + print(f"Refresh {self.__class__.__name__} took {time.perf_counter() - t0:.3f} seconds", flush=True) + else: + self._refresh(**kwargs) def reinitialize(self, **kwargs): if self.controller.verbose: @@ -138,7 +138,15 @@ def _refresh(self, **kwargs): self._qt_refresh(**kwargs) elif self.backend == "panel": import panel as pn - pn.state.execute(lambda: self._panel_refresh(**kwargs), schedule=True) + pn.state.execute(lambda: self._timed_panel_refresh(**kwargs), schedule=True) + + def _timed_panel_refresh(self, **kwargs): + # panel refresh is scheduled, so it must be timed where it actually runs + if not self.controller.verbose: + return self._panel_refresh(**kwargs) + t0 = time.perf_counter() + self._panel_refresh(**kwargs) + print(f"Refresh {self.__class__.__name__} took {time.perf_counter() - t0:.3f} seconds", flush=True) def _reinitialize(self, **kwargs): if self.backend == "qt":