Skip to content

Commit e376c96

Browse files
authored
Merge pull request #704 from int-brain-lab/task_qc_viewer
Task qc viewer
2 parents e1f720a + 75109e5 commit e376c96

24 files changed

+954
-133
lines changed

brainbox/behavior/dlc.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
Set of functions to deal with dlc data
3-
"""
1+
"""Set of functions to deal with dlc data."""
42
import logging
53
import pandas as pd
64
import warnings
@@ -48,21 +46,23 @@ def insert_idx(array, values):
4846

4947
def likelihood_threshold(dlc, threshold=0.9):
5048
"""
51-
Set dlc points with likelihood less than threshold to nan
49+
Set dlc points with likelihood less than threshold to nan.
50+
51+
FIXME Add unit test.
5252
:param dlc: dlc pqt object
5353
:param threshold: likelihood threshold
5454
:return:
5555
"""
5656
features = np.unique(['_'.join(x.split('_')[:-1]) for x in dlc.keys()])
5757
for feat in features:
5858
nan_fill = dlc[f'{feat}_likelihood'] < threshold
59-
dlc.loc[nan_fill, f'{feat}_x'] = np.nan
60-
dlc.loc[nan_fill, f'{feat}_y'] = np.nan
59+
dlc.loc[nan_fill, (f'{feat}_x', f'{feat}_y')] = np.nan
6160
return dlc
6261

6362

6463
def get_speed(dlc, dlc_t, camera, feature='paw_r'):
6564
"""
65+
FIXME Document and add unit test!
6666
6767
:param dlc: dlc pqt table
6868
:param dlc_t: dlc time points

ibllib/io/raw_data_loaders.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
#!/usr/bin/env python
2-
# -*- coding:utf-8 -*-
3-
# @Author: Niccolò Bonacchi, Miles Wells
4-
# @Date: Monday, July 16th 2018, 1:28:46 pm
51
"""
62
Raw Data Loader functions for PyBpod rig.
73

ibllib/misc/qt.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""PyQt5 helper functions."""
2+
import logging
3+
import sys
4+
from functools import wraps
5+
6+
from PyQt5 import QtWidgets
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
11+
def get_main_window():
12+
"""Get the Main window of a QT application."""
13+
app = QtWidgets.QApplication.instance()
14+
return [w for w in app.topLevelWidgets() if isinstance(w, QtWidgets.QMainWindow)][0]
15+
16+
17+
def create_app():
18+
"""Create a Qt application."""
19+
global QT_APP
20+
QT_APP = QtWidgets.QApplication.instance()
21+
if QT_APP is None: # pragma: no cover
22+
QT_APP = QtWidgets.QApplication(sys.argv)
23+
return QT_APP
24+
25+
26+
def require_qt(func):
27+
"""Function decorator to specify that a function requires a Qt application.
28+
29+
Use this decorator to specify that a function needs a running Qt application before it can run.
30+
An error is raised if that is not the case.
31+
"""
32+
@wraps(func)
33+
def wrapped(*args, **kwargs):
34+
if not QtWidgets.QApplication.instance():
35+
_logger.warning('Creating a Qt application.')
36+
create_app()
37+
return func(*args, **kwargs)
38+
return wrapped
39+
40+
41+
@require_qt
42+
def run_app(): # pragma: no cover
43+
"""Run the Qt application."""
44+
global QT_APP
45+
return QT_APP.exit(QT_APP.exec_())

ibllib/pipes/base_tasks.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def read_params_file(self):
7575

7676
class BehaviourTask(DynamicTask):
7777

78+
extractor = None
79+
"""ibllib.io.extractors.base.BaseBpodExtractor: A trials extractor object."""
80+
7881
def __init__(self, session_path, **kwargs):
7982
super().__init__(session_path, **kwargs)
8083

@@ -207,6 +210,70 @@ def _spacer_support(settings):
207210
ver = v(settings.get('IBLRIG_VERSION') or '100.0.0')
208211
return ver not in (v('100.0.0'), v('8.0.0')) and ver >= v('7.1.0')
209212

213+
def extract_behaviour(self, save=True):
214+
"""Extract trials data.
215+
216+
This is an abstract method called by `_run` and `run_qc` methods. Subclasses should return
217+
the extracted trials data and a list of output files. This method should also save the
218+
trials extractor object to the :prop:`extractor` property for use by `run_qc`.
219+
220+
Parameters
221+
----------
222+
save : bool
223+
Whether to save the extracted data as ALF datasets.
224+
225+
Returns
226+
-------
227+
dict
228+
A dictionary of trials data.
229+
list of pathlib.Path
230+
A list of output file paths if save == true.
231+
"""
232+
return None, None
233+
234+
def run_qc(self, trials_data=None, update=True):
235+
"""Run task QC.
236+
237+
Subclass method should return the QC object. This just validates the trials_data is not
238+
None.
239+
240+
Parameters
241+
----------
242+
trials_data : dict
243+
A dictionary of extracted trials data. The output of :meth:`extract_behaviour`.
244+
update : bool
245+
If true, update Alyx with the QC outcome.
246+
247+
Returns
248+
-------
249+
ibllib.qc.task_metrics.TaskQC
250+
A TaskQC object replete with task data and computed metrics.
251+
"""
252+
self._assert_trials_data(trials_data)
253+
return None
254+
255+
def _assert_trials_data(self, trials_data=None):
256+
"""Check trials data available.
257+
258+
Called by :meth:`run_qc`, this extracts the trial data if `trials_data` is None, and raises
259+
if :meth:`extract_behaviour` returns None.
260+
261+
Parameters
262+
----------
263+
trials_data : dict, None
264+
A dictionary of extracted trials data or None.
265+
266+
Returns
267+
-------
268+
trials_data : dict
269+
A dictionary of extracted trials data. The output of :meth:`extract_behaviour`.
270+
"""
271+
if not self.extractor or trials_data is None:
272+
trials_data, _ = self.extract_behaviour(save=False)
273+
if not (trials_data and self.extractor):
274+
raise ValueError('No trials data and/or extractor found')
275+
return trials_data
276+
210277

211278
class VideoTask(DynamicTask):
212279

ibllib/pipes/behavior_tasks.py

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,24 @@ def _run(self, update=True, save=True):
7575
"""
7676
Extracts an iblrig training session
7777
"""
78-
trials, output_files = self._extract_behaviour(save=save)
78+
trials, output_files = self.extract_behaviour(save=save)
7979

8080
if trials is None:
8181
return None
8282
if self.one is None or self.one.offline:
8383
return output_files
8484

8585
# Run the task QC
86-
self._run_qc(trials, update=update)
86+
self.run_qc(trials, update=update)
8787
return output_files
8888

89-
def _extract_behaviour(self, **kwargs):
89+
def extract_behaviour(self, **kwargs):
9090
self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection)
9191
self.extractor.default_path = self.output_collection
9292
return self.extractor.extract(task_collection=self.collection, **kwargs)
9393

94-
def _run_qc(self, trials_data=None, update=True):
95-
if not self.extractor or trials_data is None:
96-
trials_data, _ = self._extract_behaviour(save=False)
97-
if not trials_data:
98-
raise ValueError('No trials data found')
94+
def run_qc(self, trials_data=None, update=True):
95+
trials_data = self._assert_trials_data(trials_data) # validate trials data
9996

10097
# Compile task data for QC
10198
qc = HabituationQC(self.session_path, one=self.one)
@@ -130,10 +127,10 @@ def signature(self):
130127
('*.meta', self.sync_collection, True)]
131128
return signature
132129

133-
def _extract_behaviour(self, save=True, **kwargs):
130+
def extract_behaviour(self, save=True, **kwargs):
134131
"""Extract the habituationChoiceWorld trial data using NI DAQ clock."""
135132
# Extract Bpod trials
136-
bpod_trials, _ = super()._extract_behaviour(save=False, **kwargs)
133+
bpod_trials, _ = super().extract_behaviour(save=False, **kwargs)
137134

138135
# Sync Bpod trials to FPGA
139136
sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection)
@@ -146,13 +143,13 @@ def _extract_behaviour(self, save=True, **kwargs):
146143
task_collection=self.collection, protocol_number=self.protocol_number, **kwargs)
147144
return outputs, files
148145

149-
def _run_qc(self, trials_data=None, update=True, **_):
146+
def run_qc(self, trials_data=None, update=True, **_):
150147
"""Run and update QC.
151148
152149
This adds the bpod TTLs to the QC object *after* the QC is run in the super call method.
153150
The raw Bpod TTLs are not used by the QC however they are used in the iblapps QC plot.
154151
"""
155-
qc = super()._run_qc(trials_data=trials_data, update=update)
152+
qc = super().run_qc(trials_data=trials_data, update=update)
156153
qc.extractor.bpod_ttls = self.extractor.bpod
157154
return qc
158155

@@ -300,26 +297,24 @@ def signature(self):
300297
return signature
301298

302299
def _run(self, update=True, save=True):
303-
"""
304-
Extracts an iblrig training session
305-
"""
306-
trials, output_files = self._extract_behaviour(save=save)
300+
"""Extracts an iblrig training session."""
301+
trials, output_files = self.extract_behaviour(save=save)
307302
if trials is None:
308303
return None
309304
if self.one is None or self.one.offline:
310305
return output_files
311306

312307
# Run the task QC
313-
self._run_qc(trials)
308+
self.run_qc(trials)
314309

315310
return output_files
316311

317-
def _extract_behaviour(self, **kwargs):
312+
def extract_behaviour(self, **kwargs):
318313
self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection)
319314
self.extractor.default_path = self.output_collection
320315
return self.extractor.extract(task_collection=self.collection, **kwargs)
321316

322-
def _run_qc(self, trials_data=None, update=True, QC=None):
317+
def run_qc(self, trials_data=None, update=True, QC=None):
323318
"""
324319
Run the task QC.
325320
@@ -337,10 +332,7 @@ def _run_qc(self, trials_data=None, update=True, QC=None):
337332
ibllib.qc.task_metrics.TaskQC
338333
The task QC object.
339334
"""
340-
if not self.extractor or trials_data is None:
341-
trials_data, _ = self._extract_behaviour(save=False)
342-
if not trials_data:
343-
raise ValueError('No trials data found')
335+
trials_data = self._assert_trials_data(trials_data) # validate trials data
344336

345337
# Compile task data for QC
346338
qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one,
@@ -419,9 +411,9 @@ def _behaviour_criterion(self, update=True, truncate_to_pass=True):
419411
"sessions", eid, "extended_qc", {"behavior": int(good_enough)}
420412
)
421413

422-
def _extract_behaviour(self, save=True, **kwargs):
414+
def extract_behaviour(self, save=True, **kwargs):
423415
# Extract Bpod trials
424-
bpod_trials, _ = super()._extract_behaviour(save=False, **kwargs)
416+
bpod_trials, _ = super().extract_behaviour(save=False, **kwargs)
425417

426418
# Sync Bpod trials to FPGA
427419
sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection)
@@ -431,11 +423,8 @@ def _extract_behaviour(self, save=True, **kwargs):
431423
task_collection=self.collection, protocol_number=self.protocol_number, **kwargs)
432424
return outputs, files
433425

434-
def _run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None):
435-
if not self.extractor or trials_data is None:
436-
trials_data, _ = self._extract_behaviour(save=False)
437-
if not trials_data:
438-
raise ValueError('No trials data found')
426+
def run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None):
427+
trials_data = self._assert_trials_data(trials_data) # validate trials data
439428

440429
# Compile task data for QC
441430
qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one,
@@ -477,13 +466,13 @@ def _run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None):
477466
return qc
478467

479468
def _run(self, update=True, plot_qc=True, save=True):
480-
dsets, out_files = self._extract_behaviour(save=save)
469+
dsets, out_files = self.extract_behaviour(save=save)
481470

482471
if not self.one or self.one.offline:
483472
return out_files
484473

485474
self._behaviour_criterion(update=update)
486-
self._run_qc(dsets, update=update, plot_qc=plot_qc)
475+
self.run_qc(dsets, update=update, plot_qc=plot_qc)
487476
return out_files
488477

489478

@@ -508,10 +497,10 @@ def signature(self):
508497
for fn in filter(None, extractor.save_names)]
509498
return signature
510499

511-
def _extract_behaviour(self, save=True, **kwargs):
500+
def extract_behaviour(self, save=True, **kwargs):
512501
"""Extract the Bpod trials data and Timeline acquired signals."""
513502
# First determine the extractor from the task protocol
514-
bpod_trials, _ = ChoiceWorldTrialsBpod._extract_behaviour(self, save=False, **kwargs)
503+
bpod_trials, _ = ChoiceWorldTrialsBpod.extract_behaviour(self, save=False, **kwargs)
515504

516505
# Sync Bpod trials to DAQ
517506
self.extractor = TimelineTrials(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor)
@@ -544,11 +533,12 @@ def signature(self):
544533

545534
def _run(self, upload=True):
546535
"""
547-
Extracts training status for subject
536+
Extracts training status for subject.
548537
"""
549538

550539
lab = get_lab(self.session_path, self.one.alyx)
551-
if lab == 'cortexlab':
540+
if lab == 'cortexlab' and 'cortexlab' in self.one.alyx.base_url:
541+
_logger.info('Switching from cortexlab Alyx to IBL Alyx for training status queries.')
552542
one = ONE(base_url='https://alyx.internationalbrainlab.org')
553543
else:
554544
one = self.one

0 commit comments

Comments
 (0)