Skip to content

Commit 4f763c0

Browse files
authored
Merge pull request #724 from int-brain-lab/custom_task_qc
Custom task qc
2 parents 660f98e + 77334d0 commit 4f763c0

File tree

10 files changed

+232
-244
lines changed

10 files changed

+232
-244
lines changed

ibllib/io/extractors/base.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717

1818
class BaseExtractor(abc.ABC):
1919
"""
20-
Base extractor class
20+
Base extractor class.
21+
2122
Writing an extractor checklist:
22-
- on the child class, overload the _extract method
23-
- this method should output one or several numpy.arrays or dataframe with a consistent shape
24-
- save_names is a list or a string of filenames, there should be one per dataset
25-
- set save_names to None for a dataset that doesn't need saving (could be set dynamically
26-
in the _extract method)
23+
24+
- on the child class, overload the _extract method
25+
- this method should output one or several numpy.arrays or dataframe with a consistent shape
26+
- save_names is a list or a string of filenames, there should be one per dataset
27+
- set save_names to None for a dataset that doesn't need saving (could be set dynamically in
28+
the _extract method)
29+
2730
:param session_path: Absolute path of session folder
2831
:type session_path: str/Path
2932
"""
@@ -122,10 +125,11 @@ def _extract(self):
122125

123126
class BaseBpodTrialsExtractor(BaseExtractor):
124127
"""
125-
Base (abstract) extractor class for bpod jsonable data set
126-
Wrps the _extract private method
128+
Base (abstract) extractor class for bpod jsonable data set.
127129
128-
:param session_path: Absolute path of session folder
130+
Wraps the _extract private method.
131+
132+
:param session_path: Absolute path of session folder.
129133
:type session_path: str
130134
:param bpod_trials
131135
:param settings
@@ -159,6 +163,12 @@ def extract(self, bpod_trials=None, settings=None, **kwargs):
159163
self.settings["IBLRIG_VERSION"] = "100.0.0"
160164
return super(BaseBpodTrialsExtractor, self).extract(**kwargs)
161165

166+
@property
167+
def alf_path(self):
168+
"""pathlib.Path: The full task collection filepath."""
169+
if self.session_path:
170+
return self.session_path.joinpath(self.task_collection or '').absolute()
171+
162172

163173
def run_extractor_classes(classes, session_path=None, **kwargs):
164174
"""

ibllib/io/extractors/training_trials.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,9 +570,9 @@ def get_stimOn_times_ge5(session_path, data=False, task_collection='raw_behavior
570570
@staticmethod
571571
def get_stimOn_times_lt5(session_path, data=False, task_collection='raw_behavior_data'):
572572
"""
573-
Find the time of the statemachine command to turn on hte stim
573+
Find the time of the statemachine command to turn on the stim
574574
(state stim_on start or rotary_encoder_event2)
575-
Find the next frame change from the photodiodeafter that TS.
575+
Find the next frame change from the photodiode after that TS.
576576
Screen is not displaying anything until then.
577577
(Frame changes are in BNC1High and BNC1Low)
578578
"""

ibllib/io/globus.py

Lines changed: 0 additions & 127 deletions
This file was deleted.

ibllib/pipes/base_tasks.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,98 @@ def __init__(self, session_path, **kwargs):
9090
self.output_collection += f'/task_{self.protocol_number:02}'
9191

9292
def get_protocol(self, protocol=None, task_collection=None):
93-
return protocol if protocol else sess_params.get_task_protocol(self.session_params, task_collection)
93+
"""
94+
Return the task protocol name.
95+
96+
This returns the task protocol based on the task collection. If `protocol` is not None, this
97+
acts as an identity function. If both `task_collection` and `protocol` are None, returns
98+
the protocol defined in the experiment description file only if a single protocol was run.
99+
If the `task_collection` is not None, the associated protocol name is returned.
100+
101+
102+
Parameters
103+
----------
104+
protocol : str
105+
A task protocol name. If not None, the same value is returned.
106+
task_collection : str
107+
The task collection whose protocol name to return. May be None if only one protocol run.
108+
109+
Returns
110+
-------
111+
str, None
112+
The task protocol name, or None, if no protocol found.
113+
114+
Raises
115+
------
116+
ValueError
117+
For session with multiple task protocols, a task collection must be passed.
118+
"""
119+
if protocol:
120+
return protocol
121+
protocol = sess_params.get_task_protocol(self.session_params, task_collection) or None
122+
if isinstance(protocol, set):
123+
if len(protocol) == 1:
124+
protocol = next(iter(protocol))
125+
else:
126+
raise ValueError('Multiple task protocols for session. Task collection must be explicitly defined.')
127+
return protocol
94128

95129
def get_task_collection(self, collection=None):
130+
"""
131+
Return the task collection.
132+
133+
If `collection` is not None, this acts as an identity function. Otherwise loads it from
134+
the experiment description if only one protocol was run.
135+
136+
Parameters
137+
----------
138+
collection : str
139+
A task collection. If not None, the same value is returned.
140+
141+
Returns
142+
-------
143+
str, None
144+
The task collection, or None if no task protocols were run.
145+
146+
Raises
147+
------
148+
AssertionError
149+
Raised if multiple protocols were run and collection is None, or if experiment
150+
description file is improperly formatted.
151+
152+
"""
96153
if not collection:
97154
collection = sess_params.get_task_collection(self.session_params)
98155
# If inferring the collection from the experiment description, assert only one returned
99156
assert collection is None or isinstance(collection, str) or len(collection) == 1
100157
return collection
101158

102159
def get_protocol_number(self, number=None, task_protocol=None):
160+
"""
161+
Return the task protocol number.
162+
163+
Numbering starts from 0. If the 'protocol_number' field is missing from the experiment
164+
description, None is returned. If `task_protocol` is None, the first protocol number if n
165+
protocols == 1, otherwise returns None.
166+
167+
NB: :func:`ibllib.pipes.dynamic_pipeline.make_pipeline` will determine the protocol number
168+
from the order of the tasks in the experiment description if the task collection follows
169+
the pattern 'raw_task_data_XX'. If the task protocol does not follow this pattern, the
170+
experiment description file should explicitly define the number with the 'protocol_number'
171+
field.
172+
173+
Parameters
174+
----------
175+
number : int
176+
The protocol number. If not None, the same value is returned.
177+
task_protocol : str
178+
The task protocol name.
179+
180+
Returns
181+
-------
182+
int, None
183+
The task protocol number, if defined.
184+
"""
103185
if number is None: # Do not use "if not number" as that will return True if number is 0
104186
number = sess_params.get_task_protocol_number(self.session_params, task_protocol)
105187
# If inferring the number from the experiment description, assert only one returned (or something went wrong)

ibllib/pipes/behavior_tasks.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ class ChoiceWorldTrialsBpod(base_tasks.BehaviourTask):
277277
priority = 90
278278
job_size = 'small'
279279
extractor = None
280+
"""ibllib.io.extractors.base.BaseBpodTrialsExtractor: An instance of the Bpod trials extractor."""
280281

281282
@property
282283
def signature(self):
@@ -318,7 +319,24 @@ def _extract_behaviour(self, **kwargs):
318319
self.extractor.default_path = self.output_collection
319320
return self.extractor.extract(task_collection=self.collection, **kwargs)
320321

321-
def _run_qc(self, trials_data=None, update=True):
322+
def _run_qc(self, trials_data=None, update=True, QC=None):
323+
"""
324+
Run the task QC.
325+
326+
Parameters
327+
----------
328+
trials_data : dict
329+
The complete extracted task data.
330+
update : bool
331+
If True, updates the session QC fields on Alyx.
332+
QC : ibllib.qc.task_metrics.TaskQC
333+
An optional QC class to instantiate.
334+
335+
Returns
336+
-------
337+
ibllib.qc.task_metrics.TaskQC
338+
The task QC object.
339+
"""
322340
if not self.extractor or trials_data is None:
323341
trials_data, _ = self._extract_behaviour(save=False)
324342
if not trials_data:
@@ -328,10 +346,11 @@ def _run_qc(self, trials_data=None, update=True):
328346
qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one,
329347
sync_type=self.sync, task_collection=self.collection)
330348
qc_extractor.data = qc_extractor.rename_data(trials_data)
331-
if type(self.extractor).__name__ == 'HabituationTrials':
332-
qc = HabituationQC(self.session_path, one=self.one, log=_logger)
333-
else:
334-
qc = TaskQC(self.session_path, one=self.one, log=_logger)
349+
if not QC:
350+
QC = HabituationQC if type(self.extractor).__name__ == 'HabituationTrials' else TaskQC
351+
_logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__)
352+
qc = QC(self.session_path, one=self.one, log=_logger)
353+
if QC is not HabituationQC:
335354
qc_extractor.wheel_encoding = 'X1'
336355
qc_extractor.settings = self.extractor.settings
337356
qc_extractor.frame_ttls, qc_extractor.audio_ttls = load_bpod_fronts(
@@ -412,7 +431,7 @@ def _extract_behaviour(self, save=True, **kwargs):
412431
task_collection=self.collection, protocol_number=self.protocol_number, **kwargs)
413432
return outputs, files
414433

415-
def _run_qc(self, trials_data=None, update=False, plot_qc=False):
434+
def _run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None):
416435
if not self.extractor or trials_data is None:
417436
trials_data, _ = self._extract_behaviour(save=False)
418437
if not trials_data:
@@ -422,10 +441,11 @@ def _run_qc(self, trials_data=None, update=False, plot_qc=False):
422441
qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one,
423442
sync_type=self.sync, task_collection=self.collection)
424443
qc_extractor.data = qc_extractor.rename_data(trials_data.copy())
425-
if type(self.extractor).__name__ == 'HabituationTrials':
426-
qc = HabituationQC(self.session_path, one=self.one, log=_logger)
427-
else:
428-
qc = TaskQC(self.session_path, one=self.one, log=_logger)
444+
if not QC:
445+
QC = HabituationQC if type(self.extractor).__name__ == 'HabituationTrials' else TaskQC
446+
_logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__)
447+
qc = QC(self.session_path, one=self.one, log=_logger)
448+
if QC is not HabituationQC:
429449
# Add Bpod wheel data
430450
wheel_ts_bpod = self.extractor.bpod2fpga(self.extractor.bpod_trials['wheel_timestamps'])
431451
qc_extractor.data['wheel_timestamps_bpod'] = wheel_ts_bpod

ibllib/pipes/misc.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import time
1111
import logging
12+
import warnings
1213
from functools import wraps
1314
from pathlib import Path
1415
from typing import Union, List, Callable, Any
@@ -365,6 +366,8 @@ def load_params_dict(params_fname: str) -> dict:
365366

366367

367368
def load_videopc_params():
369+
"""(DEPRECATED) This will be removed in favour of iblrigv8 functions."""
370+
warnings.warn('load_videopc_params will be removed in favour of iblrigv8', FutureWarning)
368371
if not load_params_dict("videopc_params"):
369372
create_videopc_params()
370373
return load_params_dict("videopc_params")
@@ -472,6 +475,9 @@ def create_basic_transfer_params(param_str='transfer_params', local_data_path=No
472475

473476

474477
def create_videopc_params(force=False, silent=False):
478+
"""(DEPRECATED) This will be removed in favour of iblrigv8 functions."""
479+
url = 'https://github.com/int-brain-lab/iblrig/blob/videopc/docs/source/video.rst'
480+
warnings.warn(f'create_videopc_params is deprecated, see {url}', DeprecationWarning)
475481
if Path(params.getfile("videopc_params")).exists() and not force:
476482
print(f"{params.getfile('videopc_params')} exists already, exiting...")
477483
print(Path(params.getfile("videopc_params")).exists())

0 commit comments

Comments
 (0)