Skip to content

Commit 679a692

Browse files
committed
Merge with main
2 parents c9c76fe + 4d64476 commit 679a692

File tree

15 files changed

+510
-223
lines changed

15 files changed

+510
-223
lines changed

README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ sigui --mode=web --root-folder path-to-my-analyzers
222222
![launcher_web](docs/source/images/launcher_web.png)
223223

224224

225-
## Customizing the layout
225+
## Customizing layout and settings
226226

227227
You can create your own custom layout by specifying which views you'd like
228228
to see, and where they go. The basic window layout supports eight "zones",
@@ -248,7 +248,7 @@ E.g. suppose your layout is only non-empty in zones 1, 4, 5, 6 and 7:
248248
+---------------+--------------+
249249
```
250250

251-
Then zone1 will stretch right-wards to make a three-zone view. Zone4 will stretch
251+
Then zone1 will stretch right-wards to make a two-zone view. Zone4 will stretch
252252
downwards to make a long two-zone view.
253253

254254
To specify your own layout, put the specification in a `.json` file. This should
@@ -278,6 +278,41 @@ sigui --layout_file=path/to/my_layout.json path/to/sorting_analyzer
278278

279279
Find a list of available views [in this file](https://github.com/SpikeInterface/spikeinterface-gui/blob/main/spikeinterface_gui/viewlist.py).
280280

281+
You can also edit the initial settings for each view. There are two ways to do this.
282+
First, you can open the gui, edit the settings of any or all views, then press the
283+
"Save as default settings" button in the `mainsettings` view. This will save a
284+
`settings.json` file in your spikeinterface-gui config (saved in `~/.config/spikeinterface_gui/`).
285+
The saved settings will automatically load next time you start the gui.
286+
287+
If you want more direct control, you can pass your own setting json file
288+
to the `settings-file` flag (or by passing a `user_settings` dict to `run_mainwindow`).
289+
The settings file should contain a settings dict for each view. The view names are the
290+
same as used in the layout file, and each setting name can be seen in the settings
291+
tab of each view. Below is an example settings file:
292+
293+
**my_settings.json**
294+
295+
``` json
296+
{
297+
"probe": {
298+
"show_channel_id": true
299+
},
300+
"waveform": {
301+
"overlap": false,
302+
"plot_std": false
303+
},
304+
"spikeamplitude": {
305+
"max_spikes_per_unit": 1000,
306+
"alpha": 0.2
307+
}
308+
}
309+
```
310+
311+
You can then use this file by running e.g.
312+
313+
```bash
314+
sigui --settings_file=path/to/my_settings.json path/to/sorting_analyzer
315+
```
281316

282317
## Deploy the GUI in web mode
283318

docs/source/custom_layout.rst renamed to docs/source/custom_layout_settings.rst

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Customizing the layout
2-
======================
1+
Customizing layout and settings
2+
===============================
33

44
You can create your own custom layout by specifying which views you'd like
55
to see, and where they go. The basic window layout supports eight "zones",
@@ -26,7 +26,7 @@ E.g. suppose your layout is only non-empty in zones 1, 4, 5, 6 and 7:
2626
+---------------+--------------+
2727
2828
29-
Then zone1 will stretch right-wards to make a three-zone view. Zone4 will stretch
29+
Then zone1 will stretch right-wards to make a two-zone view. Zone4 will stretch
3030
downwards to make a long two-zone view.
3131

3232
To specify your own layout, put the specification in a ``.json`` file. This should
@@ -55,3 +55,40 @@ using the ``--layout_file`` flag:
5555
sigui --layout_file=path/to/my_layout.json path/to/sorting_analyzer
5656
5757
Find a list of available views `in this file <https://github.com/SpikeInterface/spikeinterface-gui/blob/main/spikeinterface_gui/viewlist.py>`_.
58+
59+
You can also edit the initial settings for each view. There are two ways to do this.
60+
First, you can open the gui, edit the settings of any or all views, then press the
61+
"Save as default settings" button in the ``mainsettings`` view. This will save a
62+
``settings.json`` file in your spikeinterface-gui config (saved in ``~/.config/spikeinterface_gui/``).
63+
The saved settings will automatically load next time you start the gui.
64+
65+
If you want more direct control, you can pass your own setting json file
66+
to the ``settings-file`` flag (or by passing a ``user_settings`` dict to ``run_mainwindow``).
67+
The settings file should contain a settings dict for each view. The view names are the
68+
same as used in the layout file, and each setting name can be seen in the settings
69+
tab of each view. Below is an example settings file:
70+
71+
**my_settings.json**
72+
73+
.. code-block:: json
74+
75+
{
76+
"probe": {
77+
"show_channel_id": true
78+
},
79+
"waveform": {
80+
"overlap": false,
81+
"plot_std": false
82+
},
83+
"spikeamplitude": {
84+
"max_spikes_per_unit": 1000,
85+
"alpha": 0.2
86+
}
87+
}
88+
89+
90+
You can then use this file by running e.g.
91+
92+
.. code-block:: bash
93+
94+
sigui --settings_file=path/to/my_settings.json path/to/sorting_analyzer

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ This is a web app internally using Panel, useful when the data is remote
5050
installation
5151
launch
5252
usage
53-
custom_layout
53+
custom_layout_settings
5454
deployments
5555
views
5656
developers

spikeinterface_gui/backend_panel.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,13 @@ def listen_setting_changes(view):
185185

186186
class PanelMainWindow:
187187

188-
def __init__(self, controller, layout_preset=None, layout=None):
188+
def __init__(self, controller, layout_preset=None, layout=None, user_settings=None):
189189
self.controller = controller
190190
self.layout_preset = layout_preset
191191
self.layout = layout
192192
self.verbose = controller.verbose
193193

194-
self.make_views()
194+
self.make_views(user_settings)
195195
self.create_main_layout()
196196

197197
# refresh all views wihtout notiying
@@ -202,7 +202,7 @@ def __init__(self, controller, layout_preset=None, layout=None):
202202
if view.is_view_visible():
203203
view.refresh()
204204

205-
def make_views(self):
205+
def make_views(self, user_settings):
206206
self.views = {}
207207
# this contains view layout + settings + compute
208208
self.view_layouts = {}
@@ -227,6 +227,13 @@ def make_views(self):
227227
sizing_mode="stretch_both"
228228
)
229229

230+
if user_settings is not None and user_settings.get(view_name) is not None:
231+
for setting_name, user_setting in user_settings.get(view_name).items():
232+
if setting_name not in view.settings.keys():
233+
raise KeyError(f"Setting {setting_name} is not a valid setting for View {view_name}. Check your settings file.")
234+
with param.parameterized.discard_events(view.settings._parameterized):
235+
view.settings[setting_name] = user_setting
236+
230237
tabs = [("📊", view.layout)]
231238
if view_class._settings is not None:
232239
settings = pn.Param(view.settings._parameterized, sizing_mode="stretch_height",

spikeinterface_gui/backend_qt.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,22 @@ def create_settings(view, parent):
148148
def listen_setting_changes(view):
149149
view.settings.sigTreeStateChanged.connect(view.on_settings_changed)
150150

151+
def stop_listen_setting_changes(view):
152+
view.settings.sigTreeStateChanged.disconnect(view.on_settings_changed)
153+
151154

152155
class QtMainWindow(QT.QMainWindow):
153156
main_window_closed = QT.pyqtSignal(object)
154157

155-
def __init__(self, controller, parent=None, layout_preset=None, layout=None):
158+
def __init__(self, controller, parent=None, layout_preset=None, layout=None, user_settings=None):
156159
QT.QMainWindow.__init__(self, parent)
157160

158161
self.controller = controller
159162
self.verbose = controller.verbose
160163
self.layout_preset = layout_preset
161164
self.layout = layout
162-
self.make_views()
165+
166+
self.make_views(user_settings)
163167
self.create_main_layout()
164168

165169
# refresh all views wihtout notiying
@@ -173,7 +177,7 @@ def __init__(self, controller, parent=None, layout_preset=None, layout=None):
173177
# for view_name, dock in self.docks.items():
174178
# dock.visibilityChanged.connect(self.views[view_name].refresh)
175179

176-
def make_views(self):
180+
def make_views(self, user_settings):
177181
self.views = {}
178182
self.docks = {}
179183
for view_name, view_class in possible_class_views.items():
@@ -190,6 +194,16 @@ def make_views(self):
190194

191195
widget = ViewWidget(view_class)
192196
view = view_class(controller=self.controller, parent=widget, backend='qt')
197+
198+
if user_settings is not None and user_settings.get(view_name) is not None:
199+
for setting_name, user_setting in user_settings.get(view_name).items():
200+
if setting_name not in view.settings.keys().keys():
201+
raise KeyError(f"Setting {setting_name} is not a valid setting for View {view_name}. Check your settings file.")
202+
stop_listen_setting_changes(view)
203+
view.settings[setting_name] = user_setting
204+
listen_setting_changes(view)
205+
206+
193207
widget.set_view(view)
194208
dock = QT.QDockWidget(view_name)
195209
dock.setWidget(widget)

spikeinterface_gui/controller.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ class Controller():
3535
def __init__(self, analyzer=None, backend="qt", parent=None, verbose=False, save_on_compute=False,
3636
curation=False, curation_data=None, label_definitions=None, with_traces=True,
3737
displayed_unit_properties=None,
38-
extra_unit_properties=None, skip_extensions=None):
38+
extra_unit_properties=None, skip_extensions=None, disable_save_settings_button=False):
3939
self.views = []
4040
skip_extensions = skip_extensions if skip_extensions is not None else []
4141
self.skip_extensions = skip_extensions
4242
self.backend = backend
43+
self.disable_save_settings_button = disable_save_settings_button
4344
if self.backend == "qt":
4445
from .backend_qt import SignalHandler
4546
self.signal_handler = SignalHandler(self, parent=parent)
@@ -443,6 +444,29 @@ def get_t_start_t_stop(self):
443444
else:
444445
return 0, self.get_num_samples(segment_index) / self.sampling_frequency
445446

447+
def get_times_chunk(self, segment_index, t1, t2):
448+
ind1, ind2 = self.get_chunk_indices(t1, t2, segment_index)
449+
if self.main_settings["use_times"]:
450+
recording = self.analyzer.recording
451+
times_chunk = recording.get_times(segment_index=segment_index)[ind1:ind2]
452+
else:
453+
times_chunk = np.arange(ind2 - ind1, dtype='float64') / self.controller.sampling_frequency + max(t1, 0)
454+
return times_chunk
455+
456+
def get_chunk_indices(self, t1, t2, segment_index):
457+
if self.main_settings["use_times"]:
458+
recording = self.analyzer.recording
459+
ind1, ind2 = recording.time_to_sample_index([t1, t2], segment_index=segment_index)
460+
else:
461+
t_start = 0.0
462+
sr = self.sampling_frequency
463+
ind1 = int((t1 - t_start) * sr)
464+
ind2 = int((t2 - t_start) * sr)
465+
466+
ind1 = max(0, ind1)
467+
ind2 = min(self.get_num_samples(segment_index), ind2)
468+
return ind1, ind2
469+
446470
def sample_index_to_time(self, sample_index):
447471
segment_index = self.time_info["segment_index"]
448472
if self.main_settings["use_times"] and self.analyzer.has_recording():
@@ -610,14 +634,33 @@ def get_traces(self, trace_source='preprocessed', **kargs):
610634
cache_key = (kargs.get("segment_index", None), kargs.get("start_frame", None), kargs.get("end_frame", None))
611635
if cache_key in self._traces_cached:
612636
return self._traces_cached[cache_key]
637+
else:
638+
# check if start_frame and end_frame are a subset interval of a cached one
639+
for cached_key in self._traces_cached.keys():
640+
cached_seg = cached_key[0]
641+
cached_start = cached_key[1]
642+
cached_end = cached_key[2]
643+
req_seg = kargs.get("segment_index", None)
644+
req_start = kargs.get("start_frame", None)
645+
req_end = kargs.get("end_frame", None)
646+
if cached_seg is not None and req_seg is not None:
647+
if cached_seg != req_seg:
648+
continue
649+
if cached_start is not None and cached_end is not None and req_start is not None and req_end is not None:
650+
if req_start >= cached_start and req_end <= cached_end:
651+
# subset found
652+
traces = self._traces_cached[cached_key]
653+
start_offset = req_start - cached_start
654+
end_offset = req_end - cached_start
655+
return traces[start_offset:end_offset, :]
613656

614657
if len(self._traces_cached) > 4:
615658
self._traces_cached.pop(list(self._traces_cached.keys())[0])
616659

617660
if trace_source == 'preprocessed':
618661
rec = self.analyzer.recording
619662
elif trace_source == 'raw':
620-
raise NotImplemented
663+
raise NotImplementedError("Raw traces not implemented yet")
621664
# TODO get with parent recording the non process recording
622665
kargs['return_in_uV'] = self.return_in_uV
623666
traces = rec.get_traces(**kargs)

0 commit comments

Comments
 (0)