Skip to content

Commit 9bda491

Browse files
k1o0juhuntenburgbimacGaelleChapuismayofaulkner
authored
Develop (#703)
* change behavior qc to truncate trials up until 400 and check if performance passes * add test for sleepless decorator * Add get_trials_tasks function * data release update * change number of parallel workflows * Issue #701 --------- Co-authored-by: juhuntenburg <[email protected]> Co-authored-by: Florian Rau <[email protected]> Co-authored-by: Gaelle <[email protected]> Co-authored-by: GaelleChapuis <[email protected]> Co-authored-by: Mayo Faulkner <[email protected]>
1 parent 0e78b96 commit 9bda491

File tree

14 files changed

+421
-69
lines changed

14 files changed

+421
-69
lines changed

.github/workflows/ibllib_ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ${{ matrix.os }}
1616
strategy:
1717
fail-fast: false # Whether to stop execution of other instances
18-
max-parallel: 4
18+
max-parallel: 2
1919
matrix:
2020
os: ["windows-latest", "ubuntu-latest"]
2121
python-version: ["3.8", "3.11"]

examples/data_release/data_release_brainwidemap.ipynb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
"source": [
1212
"# Data Release - Brain Wide Map\n",
1313
"\n",
14-
"IBL aims to understand the neural basis of decision-making in the mouse by gathering a whole-brain activity map composed of electrophysiological recordings pooled from multiple laboratories. We have systematically recorded from nearly all major brain areas with Neuropixels probes, using a grid system for unbiased sampling and replicating each recording site in at least two laboratories. These data have been used to construct a brain-wide map of activity at single-spike cellular resolution during a [decision-making task]((https://elifesciences.org/articles/63711)). In addition to the map, this data set contains other information gathered during the task: sensory stimuli presented to the mouse; mouse decisions and response times; and mouse pose information from video recordings and DeepLabCut analysis. Please read our accompanying [technical paper](https://doi.org/10.6084/m9.figshare.21400815) for details on the experiment and data processing pipelines. To explore the data, visit [our vizualisation website](https://viz.internationalbrainlab.org/)."
14+
"IBL aims to understand the neural basis of decision-making in the mouse by gathering a whole-brain activity map composed of electrophysiological recordings pooled from multiple laboratories. We have systematically recorded from nearly all major brain areas with Neuropixels probes, using a grid system for unbiased sampling and replicating each recording site in at least two laboratories. These data have been used to construct a brain-wide map of activity at single-spike cellular resolution during a [decision-making task]((https://elifesciences.org/articles/63711)). Please read the associated article [(IBL et al. 2023)](https://www.biorxiv.org/content/10.1101/2023.07.04.547681v2). In addition to the map, this data set contains other information gathered during the task: sensory stimuli presented to the mouse; mouse decisions and response times; and mouse pose information from video recordings and DeepLabCut analysis. Please read our accompanying [technical paper](https://doi.org/10.6084/m9.figshare.21400815) for details on the experiment and data processing pipelines. To explore the data, visit [our vizualisation website](https://viz.internationalbrainlab.org/)."
1515
]
1616
},
1717
{
1818
"cell_type": "markdown",
1919
"source": [
2020
"## Overview of the Data\n",
21-
"We have released data from 354 Neuropixel recording sessions, which encompass 547 probe insertions, obtained in 115 subjects performing the IBL task across 11 different laboratories. As output of spike-sorting, there are 295501 units; of which 32766 are considered to be of good quality. These units were recorded in overall 194 different brain regions.\n",
21+
"We have released data from 459 Neuropixel recording sessions, which encompass 699 probe insertions, obtained in 139 subjects performing the IBL task across 12 different laboratories. As output of spike-sorting, there are 376730 units; of which 45085 are considered to be of good quality. In total, 138 brain regions were recorded in sufficient numbers for inclusion in IBL’s analyses [(IBL et al. 2023)](https://www.biorxiv.org/content/10.1101/2023.07.04.547681v2).\n",
2222
"\n",
2323
"## Data structure and download\n",
2424
"The organisation of the data follows the standard IBL data structure.\n",
@@ -31,7 +31,10 @@
3131
"\n",
3232
"Note:\n",
3333
"\n",
34-
"* The tag associated to this release is `2022_Q4_IBL_et_al_BWM`"
34+
"* The tag associated to this release is `Brainwidemap`\n",
35+
"\n",
36+
"## Receive updates on the data\n",
37+
"To receive a notification that we released new datasets, please fill up [this form](https://forms.gle/9ex2vL1JwV4QXnf98)\n"
3538
],
3639
"metadata": {
3740
"collapsed": false

ibllib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import warnings
44

5-
__version__ = '2.27.1'
5+
__version__ = '2.28'
66
warnings.filterwarnings('always', category=DeprecationWarning, module='ibllib')
77

88
# if this becomes a full-blown library we should let the logging configuration to the discretion of the dev

ibllib/io/extractors/base.py

Lines changed: 127 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Base Extractor classes.
2+
23
A module for the base Extractor classes. The Extractor, given a session path, will extract the
34
processed data from raw hardware files and optionally save them.
45
"""
@@ -10,7 +11,6 @@
1011

1112
import numpy as np
1213
import pandas as pd
13-
from one.alf.files import get_session_path
1414
from ibllib.io import raw_data_loaders as raw
1515
from ibllib.io.raw_data_loaders import load_settings, _logger
1616

@@ -162,7 +162,8 @@ def extract(self, bpod_trials=None, settings=None, **kwargs):
162162

163163
def run_extractor_classes(classes, session_path=None, **kwargs):
164164
"""
165-
Run a set of extractors with the same inputs
165+
Run a set of extractors with the same inputs.
166+
166167
:param classes: list of Extractor class
167168
:param save: True/False
168169
:param path_out: (defaults to alf path)
@@ -195,12 +196,30 @@ def run_extractor_classes(classes, session_path=None, **kwargs):
195196

196197

197198
def _get_task_types_json_config():
199+
"""
200+
Return the extractor types map.
201+
202+
This function is only used for legacy sessions, i.e. those without an experiment description
203+
file and will be removed in favor of :func:`_get_task_extractor_map`, which directly returns
204+
the Bpod extractor class name. The experiment description file cuts out the need for pipeline
205+
name identifiers.
206+
207+
Returns
208+
-------
209+
Dict[str, str]
210+
A map of task protocol to task extractor identifier, e.g. 'ephys', 'habituation', etc.
211+
212+
See Also
213+
--------
214+
_get_task_extractor_map - returns a map of task protocol to Bpod trials extractor class name.
215+
"""
198216
with open(Path(__file__).parent.joinpath('extractor_types.json')) as fp:
199217
task_types = json.load(fp)
200218
try:
201219
# look if there are custom extractor types in the personal projects repo
202220
import projects.base
203221
custom_extractors = Path(projects.base.__file__).parent.joinpath('extractor_types.json')
222+
_logger.debug('Loading extractor types from %s', custom_extractors)
204223
with open(custom_extractors) as fp:
205224
custom_task_types = json.load(fp)
206225
task_types.update(custom_task_types)
@@ -210,8 +229,28 @@ def _get_task_types_json_config():
210229

211230

212231
def get_task_protocol(session_path, task_collection='raw_behavior_data'):
232+
"""
233+
Return the task protocol name from task settings.
234+
235+
If the session path and/or task collection do not exist, the settings file is missing or
236+
otherwise can not be parsed, or if the 'PYBPOD_PROTOCOL' key is absent, None is returned.
237+
A warning is logged if the session path or settings file doesn't exist. An error is logged if
238+
the settings file can not be parsed.
239+
240+
Parameters
241+
----------
242+
session_path : str, pathlib.Path
243+
The absolute session path.
244+
task_collection : str
245+
The session path directory containing the task settings file.
246+
247+
Returns
248+
-------
249+
str or None
250+
The Pybpod task protocol name or None if not found.
251+
"""
213252
try:
214-
settings = load_settings(get_session_path(session_path), task_collection=task_collection)
253+
settings = load_settings(session_path, task_collection=task_collection)
215254
except json.decoder.JSONDecodeError:
216255
_logger.error(f'Can\'t read settings for {session_path}')
217256
return
@@ -223,11 +262,26 @@ def get_task_protocol(session_path, task_collection='raw_behavior_data'):
223262

224263
def get_task_extractor_type(task_name):
225264
"""
226-
Returns the task type string from the full pybpod task name:
227-
_iblrig_tasks_biasedChoiceWorld3.7.0 returns "biased"
228-
_iblrig_tasks_trainingChoiceWorld3.6.0 returns "training'
229-
:param task_name:
230-
:return: one of ['biased', 'habituation', 'training', 'ephys', 'mock_ephys', 'sync_ephys']
265+
Returns the task type string from the full pybpod task name.
266+
267+
Parameters
268+
----------
269+
task_name : str
270+
The complete task protocol name from the PYBPOD_PROTOCOL field of the task settings.
271+
272+
Returns
273+
-------
274+
str
275+
The extractor type identifier. Examples include 'biased', 'habituation', 'training',
276+
'ephys', 'mock_ephys' and 'sync_ephys'.
277+
278+
Examples
279+
--------
280+
>>> get_task_extractor_type('_iblrig_tasks_biasedChoiceWorld3.7.0')
281+
'biased'
282+
283+
>>> get_task_extractor_type('_iblrig_tasks_trainingChoiceWorld3.6.0')
284+
'training'
231285
"""
232286
if isinstance(task_name, Path):
233287
task_name = get_task_protocol(task_name)
@@ -245,16 +299,30 @@ def get_task_extractor_type(task_name):
245299

246300
def get_session_extractor_type(session_path, task_collection='raw_behavior_data'):
247301
"""
248-
From a session path, loads the settings file, finds the task and checks if extractors exist
249-
task names examples:
250-
:param session_path:
251-
:return: bool
302+
Infer trials extractor type from task settings.
303+
304+
From a session path, loads the settings file, finds the task and checks if extractors exist.
305+
Examples include 'biased', 'habituation', 'training', 'ephys', 'mock_ephys', and 'sync_ephys'.
306+
Note this should only be used for legacy sessions, i.e. those without an experiment description
307+
file.
308+
309+
Parameters
310+
----------
311+
session_path : str, pathlib.Path
312+
The session path for which to determine the pipeline.
313+
task_collection : str
314+
The session path directory containing the raw task data.
315+
316+
Returns
317+
-------
318+
str or False
319+
The task extractor type, e.g. 'biased', 'habituation', 'ephys', or False if unknown.
252320
"""
253-
settings = load_settings(session_path, task_collection=task_collection)
254-
if settings is None:
255-
_logger.error(f'ABORT: No data found in "{task_collection}" folder {session_path}')
321+
task_protocol = get_task_protocol(session_path, task_collection=task_collection)
322+
if task_protocol is None:
323+
_logger.error(f'ABORT: No task protocol found in "{task_collection}" folder {session_path}')
256324
return False
257-
extractor_type = get_task_extractor_type(settings['PYBPOD_PROTOCOL'])
325+
extractor_type = get_task_extractor_type(task_protocol)
258326
if extractor_type:
259327
return extractor_type
260328
else:
@@ -263,28 +331,52 @@ def get_session_extractor_type(session_path, task_collection='raw_behavior_data'
263331

264332
def get_pipeline(session_path, task_collection='raw_behavior_data'):
265333
"""
266-
Get the pre-processing pipeline name from a session path
267-
:param session_path:
268-
:return:
334+
Get the pre-processing pipeline name from a session path.
335+
336+
Note this is only suitable for legacy sessions, i.e. those without an experiment description
337+
file. This function will be removed in the future.
338+
339+
Parameters
340+
----------
341+
session_path : str, pathlib.Path
342+
The session path for which to determine the pipeline.
343+
task_collection : str
344+
The session path directory containing the raw task data.
345+
346+
Returns
347+
-------
348+
str
349+
The pipeline name inferred from the extractor type, e.g. 'ephys', 'training', 'widefield'.
269350
"""
270351
stype = get_session_extractor_type(session_path, task_collection=task_collection)
271352
return _get_pipeline_from_task_type(stype)
272353

273354

274355
def _get_pipeline_from_task_type(stype):
275356
"""
276-
Returns the pipeline from the task type. Some tasks types directly define the pipeline
277-
:param stype: session_type or task extractor type
278-
:return:
357+
Return the pipeline from the task type.
358+
359+
Some task types directly define the pipeline. Note this is only suitable for legacy sessions,
360+
i.e. those without an experiment description file. This function will be removed in the future.
361+
362+
Parameters
363+
----------
364+
stype : str
365+
The session type or task extractor type, e.g. 'habituation', 'ephys', etc.
366+
367+
Returns
368+
-------
369+
str
370+
A task pipeline identifier.
279371
"""
280372
if stype in ['ephys_biased_opto', 'ephys', 'ephys_training', 'mock_ephys', 'sync_ephys']:
281373
return 'ephys'
282374
elif stype in ['habituation', 'training', 'biased', 'biased_opto']:
283375
return 'training'
284-
elif 'widefield' in stype:
376+
elif isinstance(stype, str) and 'widefield' in stype:
285377
return 'widefield'
286378
else:
287-
return stype
379+
return stype or ''
288380

289381

290382
def _get_task_extractor_map():
@@ -293,7 +385,7 @@ def _get_task_extractor_map():
293385
294386
Returns
295387
-------
296-
dict(str, str)
388+
Dict[str, str]
297389
A map of task protocol to Bpod trials extractor class.
298390
"""
299391
FILENAME = 'task_extractor_map.json'
@@ -315,34 +407,35 @@ def get_bpod_extractor_class(session_path, task_collection='raw_behavior_data'):
315407
"""
316408
Get the Bpod trials extractor class associated with a given Bpod session.
317409
410+
Note that unlike :func:`get_session_extractor_type`, this function maps directly to the Bpod
411+
trials extractor class name. This is hardware invariant and is purly to determine the Bpod only
412+
trials extractor.
413+
318414
Parameters
319415
----------
320416
session_path : str, pathlib.Path
321417
The session path containing Bpod behaviour data.
322418
task_collection : str
323-
The session_path subfolder containing the Bpod settings file.
419+
The session_path sub-folder containing the Bpod settings file.
324420
325421
Returns
326422
-------
327423
str
328424
The extractor class name.
329425
"""
330-
# Attempt to load settings files
331-
settings = load_settings(session_path, task_collection=task_collection)
332-
if settings is None:
333-
raise ValueError(f'No data found in "{task_collection}" folder {session_path}')
334-
# Attempt to get task protocol
335-
protocol = settings.get('PYBPOD_PROTOCOL')
426+
# Attempt to get protocol name from settings file
427+
protocol = get_task_protocol(session_path, task_collection=task_collection)
336428
if not protocol:
337-
raise ValueError(f'No task protocol found in {session_path/task_collection}')
429+
raise ValueError(f'No task protocol found in {Path(session_path) / task_collection}')
338430
return protocol2extractor(protocol)
339431

340432

341433
def protocol2extractor(protocol):
342434
"""
343435
Get the Bpod trials extractor class associated with a given Bpod task protocol.
344436
345-
The Bpod task protocol can be found in the 'PYBPOD_PROTOCOL' field of _iblrig_taskSettings.raw.json.
437+
The Bpod task protocol can be found in the 'PYBPOD_PROTOCOL' field of the
438+
_iblrig_taskSettings.raw.json file.
346439
347440
Parameters
348441
----------

ibllib/io/extractors/camera.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
""" Camera extractor functions.
2+
23
This module handles extraction of camera timestamps for both Bpod and DAQ.
34
"""
45
import logging
@@ -29,7 +30,7 @@
2930

3031
def extract_camera_sync(sync, chmap=None):
3132
"""
32-
Extract camera timestamps from the sync matrix
33+
Extract camera timestamps from the sync matrix.
3334
3435
:param sync: dictionary 'times', 'polarities' of fronts detected on sync trace
3536
:param chmap: dictionary containing channel indices. Default to constant.
@@ -45,7 +46,8 @@ def extract_camera_sync(sync, chmap=None):
4546

4647
def get_video_length(video_path):
4748
"""
48-
Returns video length
49+
Returns video length.
50+
4951
:param video_path: A path to the video
5052
:return:
5153
"""
@@ -58,9 +60,7 @@ def get_video_length(video_path):
5860

5961

6062
class CameraTimestampsFPGA(BaseExtractor):
61-
"""
62-
Extractor for videos using DAQ sync and channel map.
63-
"""
63+
"""Extractor for videos using DAQ sync and channel map."""
6464

6565
def __init__(self, label, session_path=None):
6666
super().__init__(session_path)

ibllib/io/extractors/ephys_fpga.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1483,7 +1483,7 @@ def extract_all(session_path, sync_collection='raw_ephys_data', save=True, save_
14831483
# Sync Bpod trials to FPGA
14841484
sync, chmap = get_sync_and_chn_map(session_path, sync_collection)
14851485
# sync, chmap = get_main_probe_sync(session_path, bin_exists=bin_exists)
1486-
trials = FpgaTrials(session_path, bpod_trials=bpod_trials | bpod_wheel)
1486+
trials = FpgaTrials(session_path, bpod_trials={**bpod_trials, **bpod_wheel}) # py3.9 -> |
14871487
outputs, files = trials.extract(
14881488
save=save, sync=sync, chmap=chmap, path_out=save_path,
14891489
task_collection=task_collection, protocol_number=protocol_number, **kwargs)

ibllib/pipes/behavior_tasks.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,17 +374,26 @@ def signature(self):
374374
}
375375
return signature
376376

377-
def _behaviour_criterion(self, update=True):
377+
def _behaviour_criterion(self, update=True, truncate_to_pass=True):
378378
"""
379379
Computes and update the behaviour criterion on Alyx
380380
"""
381381
from brainbox.behavior import training
382382

383-
trials = alfio.load_object(self.session_path.joinpath(self.output_collection), 'trials')
383+
trials = alfio.load_object(self.session_path.joinpath(self.output_collection), 'trials').to_df()
384384
good_enough = training.criterion_delay(
385-
n_trials=trials["intervals"].shape[0],
385+
n_trials=trials.shape[0],
386386
perf_easy=training.compute_performance_easy(trials),
387387
)
388+
if truncate_to_pass and not good_enough:
389+
n_trials = trials.shape[0]
390+
while not good_enough and n_trials > 400:
391+
n_trials -= 1
392+
good_enough = training.criterion_delay(
393+
n_trials=n_trials,
394+
perf_easy=training.compute_performance_easy(trials[:n_trials]),
395+
)
396+
388397
if update:
389398
eid = self.one.path2eid(self.session_path, query_type='remote')
390399
self.one.alyx.json_field_update(

0 commit comments

Comments
 (0)