Skip to content

Commit aa370e2

Browse files
olichek1o0mayofaulkner
authored
Iblrigv8 (#641)
* small atlas fix * Prepare experiment takes in an optional device id parameter * typo * session_params: session creation creates stub in local and remote * Create stubs logging * copy ephys: update prints to logger and option to transfer single sessions * transfer ephys data logging and compatibility dynamic pipeline * Actually we don't want a status flag * fixing some lies in the type hints * bugfix get collection: yaml can interpret some keys as datestrings * un bon patch bien dégueulasse * Fix training CW extraction v8 * add atlas test and add temporary requirement for widefield package * add optional task_collection arg to compute training status * skimage deprecated in favour of scikit-image * typo * training status computation compatible with chained protocols * Dynamic pipeline training status * finish merge * add some logging to the jobs on server - make sure each run is separate * fix training_status task failing in training pipeline integration * dynamic pipeline lookups for specific subtask with sync suffix if not found * TaskQC no longer saves to disk; use get_bpod_extractor * training status task: do not raise on extraction error * Auto stash before checking out "origin/iblrigv8" stop training regression when two protocols on same day * fix for bpod camera extraction get correct collection * don't pass into qc * pop the task collection * fix tests * flake after merge --------- Co-authored-by: Miles Wells <[email protected]> Co-authored-by: Mayo Faulkner <[email protected]>
1 parent fc6fc05 commit aa370e2

19 files changed

+216
-133
lines changed

ibllib/atlas/atlas.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,16 +1162,19 @@ def from_dict(d, brain_atlas=None):
11621162
>>> tri = {'x': 544.0, 'y': 1285.0, 'z': 0.0, 'phi': 0.0, 'theta': 5.0, 'depth': 4501.0}
11631163
>>> ins = Insertion.from_dict(tri)
11641164
"""
1165+
assert brain_atlas, 'Input argument brain_atlas must be defined'
11651166
z = d['z'] / 1e6
1166-
if brain_atlas:
1167-
iy = brain_atlas.bc.y2i(d['y'] / 1e6)
1168-
ix = brain_atlas.bc.x2i(d['x'] / 1e6)
1169-
# Only use the brain surface value as z if it isn't NaN (this happens when the surface
1170-
# touches the edges of the atlas volume
1171-
if not np.isnan(brain_atlas.top[iy, ix]):
1172-
z = brain_atlas.top[iy, ix]
1173-
return Insertion(x=d['x'] / 1e6, y=d['y'] / 1e6, z=z, phi=d['phi'], theta=d['theta'],
1174-
depth=d['depth'] / 1e6, beta=d.get('beta', 0), label=d.get('label', ''))
1167+
if not hasattr(brain_atlas, 'top'):
1168+
brain_atlas.compute_surface()
1169+
iy = brain_atlas.bc.y2i(d['y'] / 1e6)
1170+
ix = brain_atlas.bc.x2i(d['x'] / 1e6)
1171+
# Only use the brain surface value as z if it isn't NaN (this happens when the surface touches the edges
1172+
# of the atlas volume
1173+
if not np.isnan(brain_atlas.top[iy, ix]):
1174+
z = brain_atlas.top[iy, ix]
1175+
return Insertion(x=d['x'] / 1e6, y=d['y'] / 1e6, z=z,
1176+
phi=d['phi'], theta=d['theta'], depth=d['depth'] / 1e6,
1177+
beta=d.get('beta', 0), label=d.get('label', ''))
11751178

11761179
@property
11771180
def trajectory(self):

ibllib/graphic.py

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

ibllib/io/extractors/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class BaseBpodTrialsExtractor(BaseExtractor):
128128
settings = None
129129
task_collection = None
130130

131-
def extract(self, task_collection='raw_behavior_data', bpod_trials=None, settings=None, **kwargs):
131+
def extract(self, bpod_trials=None, settings=None, **kwargs):
132132
"""
133133
:param: bpod_trials (optional) bpod trials from jsonable in a dictionary
134134
:param: settings (optional) bpod iblrig settings json file in a dictionary
@@ -139,7 +139,7 @@ def extract(self, task_collection='raw_behavior_data', bpod_trials=None, setting
139139
"""
140140
self.bpod_trials = bpod_trials
141141
self.settings = settings
142-
self.task_collection = task_collection
142+
self.task_collection = kwargs.pop('task_collection', 'raw_behavior_data')
143143
if self.bpod_trials is None:
144144
self.bpod_trials = raw.load_data(self.session_path, task_collection=self.task_collection)
145145
if not self.settings:
@@ -252,13 +252,13 @@ def get_session_extractor_type(session_path, task_collection='raw_behavior_data'
252252
return False
253253

254254

255-
def get_pipeline(session_path):
255+
def get_pipeline(session_path, task_collection='raw_behavior_data'):
256256
"""
257257
Get the pre-processing pipeline name from a session path
258258
:param session_path:
259259
:return:
260260
"""
261-
stype = get_session_extractor_type(session_path)
261+
stype = get_session_extractor_type(session_path, task_collection=task_collection)
262262
return _get_pipeline_from_task_type(stype)
263263

264264

ibllib/io/extractors/bpod_trials.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import importlib
77
from collections import OrderedDict
8+
import warnings
89

910
from pkg_resources import parse_version
1011
from ibllib.io.extractors import habituation_trials, training_trials, biased_trials, opto_trials
@@ -54,6 +55,7 @@ def extract_all(session_path, save=True, bpod_trials=None, settings=None,
5455
list of pathlib.Path
5556
The output files if save is true.
5657
"""
58+
warnings.warn('`extract_all` functions soon to be deprecated, use `bpod_trials.get_bpod_extractor` instead', FutureWarning)
5759
if not extractor_type:
5860
extractor_type = get_session_extractor_type(session_path, task_collection=task_collection)
5961
_logger.info(f'Extracting {session_path} as {extractor_type}')
@@ -101,6 +103,23 @@ def extract_all(session_path, save=True, bpod_trials=None, settings=None,
101103

102104

103105
def get_bpod_extractor(session_path, protocol=None, task_collection='raw_behavior_data') -> BaseBpodTrialsExtractor:
106+
"""
107+
Returns an extractor for a given session.
108+
109+
Parameters
110+
----------
111+
session_path : str, pathlib.Path
112+
The path to the session to be extracted.
113+
protocol : str, optional
114+
The protocol name, otherwise uses the PYBPOD_PROTOCOL key in iblrig task settings files.
115+
task_collection : str
116+
The folder within the session that contains the raw task data.
117+
118+
Returns
119+
-------
120+
BaseBpodTrialsExtractor
121+
An instance of the task extractor class, instantiated with the session path.
122+
"""
104123
builtins = {
105124
'HabituationTrials': HabituationTrials,
106125
'TrainingTrials': TrainingTrials,

ibllib/io/extractors/camera.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,9 @@ def _extract(self, video_path=None, display=False, extrapolate_missing=True, **k
238238
# is empty, or contains only one value (i.e. doesn't change)
239239
if gpio is not None and gpio['indices'].size > 1:
240240
_logger.info('Aligning to sync TTLs')
241-
task_collection = kwargs.get('task_collection', 'raw_behavior_data')
242241
# Extract audio TTLs
243-
_, audio = raw.load_bpod_fronts(self.session_path, data=self.bpod_trials, task_collection=task_collection)
242+
_, audio = raw.load_bpod_fronts(self.session_path, data=self.bpod_trials,
243+
task_collection=self.task_collection)
244244
_, ts = raw.load_camera_ssv_times(self.session_path, 'left')
245245
"""
246246
There are many sync TTLs that are for some reason missed by the GPIO. Conversely

ibllib/io/extractors/training_trials.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,18 @@ class ContrastLR(BaseBpodTrialsExtractor):
5252
var_names = ('contrastLeft', 'contrastRight')
5353

5454
def _extract(self):
55-
contrastLeft = np.array([t['contrast']['value'] if np.sign(
56-
t['position']) < 0 else np.nan for t in self.bpod_trials])
57-
contrastRight = np.array([t['contrast']['value'] if np.sign(
58-
t['position']) > 0 else np.nan for t in self.bpod_trials])
55+
# iblrigv8 has only flat values in the trial table so we can switch to parquet table when times come
56+
# and all the clutter here would fit in ~30 lines
57+
if isinstance(self.bpod_trials[0]['contrast'], float):
58+
contrastLeft = np.array([t['contrast'] if np.sign(
59+
t['position']) < 0 else np.nan for t in self.bpod_trials])
60+
contrastRight = np.array([t['contrast'] if np.sign(
61+
t['position']) > 0 else np.nan for t in self.bpod_trials])
62+
else:
63+
contrastLeft = np.array([t['contrast']['value'] if np.sign(
64+
t['position']) < 0 else np.nan for t in self.bpod_trials])
65+
contrastRight = np.array([t['contrast']['value'] if np.sign(
66+
t['position']) > 0 else np.nan for t in self.bpod_trials])
5967

6068
return contrastLeft, contrastRight
6169

@@ -112,9 +120,13 @@ class RepNum(BaseBpodTrialsExtractor):
112120
var_names = 'repNum'
113121

114122
def _extract(self):
115-
trial_repeated = np.array(
116-
[t['contrast']['type'] == 'RepeatContrast' for t in self.bpod_trials])
117-
trial_repeated = trial_repeated.astype(int)
123+
def get_trial_repeat(trial):
124+
if 'debias_trial' in trial:
125+
return trial['debias_trial']
126+
else:
127+
return trial['contrast']['type'] == 'RepeatContrast'
128+
129+
trial_repeated = np.array(list(map(get_trial_repeat, self.bpod_trials))).astype(int)
118130
repNum = trial_repeated.copy()
119131
c = 0
120132
for i in range(len(trial_repeated)):

ibllib/io/raw_data_loaders.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,12 @@ def _read_settings_json_compatibility_enforced(settings):
328328
# 2018-12-05 Version 3.2.3 fixes (permanent fixes in IBL_RIG from 3.2.4 on)
329329
if md['IBLRIG_VERSION_TAG'] == '':
330330
pass
331+
elif parse_version(md.get('IBLRIG_VERSION_TAG')) >= parse_version('8.0.0'):
332+
md['SESSION_NUMBER'] = str(md['SESSION_NUMBER']).zfill(3)
333+
md['PYBPOD_BOARD'] = md['RIG_NAME']
334+
md['PYBPOD_CREATOR'] = (md['ALYX_USER'], '')
335+
md['SESSION_DATE'] = md['SESSION_START_TIME'][:10]
336+
md['SESSION_DATETIME'] = md['SESSION_START_TIME']
331337
elif parse_version(md.get('IBLRIG_VERSION_TAG')) <= parse_version('3.2.3'):
332338
if 'LAST_TRIAL_DATA' in md.keys():
333339
md.pop('LAST_TRIAL_DATA')

ibllib/io/session_params.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from datetime import datetime
2828
import logging
2929
from pathlib import Path
30-
import warnings
3130
from copy import deepcopy
3231

3332
from one.converters import ConversionMixin
@@ -405,14 +404,12 @@ def get_collections(sess_params):
405404

406405
def iter_dict(d):
407406
for k, v in d.items():
408-
if not v or isinstance(v, str):
409-
continue
410407
if isinstance(v, list):
411408
for d in filter(lambda x: isinstance(x, dict), v):
412409
iter_dict(d)
413-
elif 'collection' in v:
410+
elif isinstance(v, dict) and 'collection' in v:
414411
collection_map[k] = v['collection']
415-
else:
412+
elif isinstance(v, dict):
416413
iter_dict(v)
417414

418415
iter_dict(sess_params)
@@ -459,7 +456,7 @@ def get_remote_stub_name(session_path, device_id=None):
459456
return session_path / '_devices' / remote_filename
460457

461458

462-
def prepare_experiment(session_path, acquisition_description=None, local=None, remote=None, overwrite=False):
459+
def prepare_experiment(session_path, acquisition_description=None, local=None, remote=None, device_id=None, overwrite=False):
463460
"""
464461
Copy acquisition description yaml to the server and local transfers folder.
465462
@@ -471,30 +468,46 @@ def prepare_experiment(session_path, acquisition_description=None, local=None, r
471468
The data to write to the experiment.description.yaml file.
472469
local : str, pathlib.Path
473470
The path to the local session folders.
471+
>>> C:\iblrigv8_data\cortexlab\Subjects # noqa
474472
remote : str, pathlib.Path
475473
The path to the remote server session folders.
474+
>>> Y:\Subjects # noqa
475+
device_id : str, optional
476+
A device name, if None the TRANSFER_LABEL parameter is used (defaults to this device's
477+
hostname with a unique numeric ID)
476478
overwrite : bool
477479
If true, overwrite any existing file with the new one, otherwise, update the existing file.
478480
"""
479481
if not acquisition_description:
480482
return
483+
481484
# Determine if user passed in arg for local/remote subject folder locations or pull in from
482-
# local param file or prompt user if missing
483-
params = misc.create_basic_transfer_params(local_data_path=local, remote_data_path=remote)
485+
# local param file or prompt user if missing data.
486+
if local is None or remote is None or device_id is None:
487+
params = misc.create_basic_transfer_params(local_data_path=local, remote_data_path=remote, TRANSFER_LABEL=device_id)
488+
local, device_id = (params['DATA_FOLDER_PATH'], params['TRANSFER_LABEL'])
489+
# if the user provides False as an argument, it means the intent is to not copy anything, this
490+
# won't be preserved by create_basic_transfer_params by default
491+
remote = False if remote is False else params['REMOTE_DATA_FOLDER_PATH']
492+
493+
# THis is in the docstring but still, if the session Path is absolute, we need to make it relative
494+
if Path(session_path).is_absolute():
495+
session_path = Path(*session_path.parts[-3:])
484496

485497
# First attempt to copy to server
486-
local_only = remote is False or params.get('REMOTE_DATA_FOLDER_PATH', False) is False
487-
if not local_only:
488-
remote_device_path = get_remote_stub_name(session_path, params['TRANSFER_LABEL'])
498+
if remote is not False:
499+
remote_session_path = Path(remote).joinpath(session_path)
500+
remote_device_path = get_remote_stub_name(remote_session_path, device_id=device_id)
489501
previous_description = read_params(remote_device_path) if remote_device_path.exists() and not overwrite else {}
490502
try:
491503
write_yaml(remote_device_path, merge_params(previous_description, acquisition_description))
504+
_logger.info(f'Written data to remote device at: {remote_device_path}.')
492505
except Exception as ex:
493-
warnings.warn(f'Failed to write data to {remote_device_path}: {ex}')
506+
_logger.warning(f'Failed to write data to remote device at: {remote_device_path}. \n {ex}')
494507

495-
# Now copy to local directory
496-
local = params.get('TRANSFERS_PATH', params['DATA_FOLDER_PATH'])
497-
filename = f'_ibl_experiment.description_{params["TRANSFER_LABEL"]}.yaml'
508+
# then create on the local machine
509+
filename = f'_ibl_experiment.description_{device_id}.yaml'
498510
local_device_path = Path(local).joinpath(session_path, filename)
499511
previous_description = read_params(local_device_path) if local_device_path.exists() and not overwrite else {}
500512
write_yaml(local_device_path, merge_params(previous_description, acquisition_description))
513+
_logger.info(f'Written data to local session at : {local_device_path}.')

ibllib/pipes/behavior_tasks.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,8 @@ def _run(self, update=True):
7777
"""
7878
Extracts an iblrig training session
7979
"""
80-
save_path = self.session_path.joinpath(self.output_collection)
81-
trials, wheel, output_files = bpod_trials.extract_all(
82-
self.session_path, save=True, task_collection=self.collection, save_path=save_path)
80+
extractor = bpod_trials.get_bpod_extractor(self.session_path, task_collection=self.collection)
81+
trials, output_files = extractor.extract(task_collection=self.collection, save=True)
8382

8483
if trials is None:
8584
return None
@@ -88,8 +87,8 @@ def _run(self, update=True):
8887
# Run the task QC
8988
# Compile task data for QC
9089
qc = HabituationQC(self.session_path, one=self.one)
91-
qc.extractor = TaskQCExtractor(self.session_path, one=self.one, sync_collection=self.sync_collection, sync_type=self.sync,
92-
task_collection=self.collection, save_path=save_path)
90+
qc.extractor = TaskQCExtractor(self.session_path, sync_collection=self.sync_collection,
91+
one=self.one, sync_type=self.sync, task_collection=self.collection)
9392
namespace = 'task' if self.protocol_number is None else f'task_{self.protocol_number:02}'
9493
qc.run(update=update, namespace=namespace)
9594
return output_files
@@ -239,24 +238,25 @@ def _run(self, update=True):
239238
"""
240239
Extracts an iblrig training session
241240
"""
242-
save_path = self.session_path.joinpath(self.output_collection)
243-
trials, wheel, output_files = bpod_trials.extract_all(
244-
self.session_path, save=True, task_collection=self.collection, save_path=save_path)
241+
extractor = bpod_trials.get_bpod_extractor(self.session_path, task_collection=self.collection)
242+
extractor.default_path = self.output_collection
243+
trials, output_files = extractor.extract(task_collection=self.collection, save=True)
245244
if trials is None:
246245
return None
247246
if self.one is None or self.one.offline:
248247
return output_files
249248
# Run the task QC
250249
# Compile task data for QC
251250
type = get_session_extractor_type(self.session_path, task_collection=self.collection)
251+
# FIXME Task data should not need re-extracting
252252
if type == 'habituation':
253253
qc = HabituationQC(self.session_path, one=self.one)
254254
qc.extractor = TaskQCExtractor(self.session_path, one=self.one, sync_collection=self.sync_collection,
255-
sync_type=self.sync, task_collection=self.collection, save_path=save_path)
255+
sync_type=self.sync, task_collection=self.collection)
256256
else: # Update wheel data
257257
qc = TaskQC(self.session_path, one=self.one)
258258
qc.extractor = TaskQCExtractor(self.session_path, one=self.one, sync_collection=self.sync_collection,
259-
sync_type=self.sync, task_collection=self.collection, save_path=save_path)
259+
sync_type=self.sync, task_collection=self.collection)
260260
qc.extractor.wheel_encoding = 'X1'
261261
# Aggregate and update Alyx QC fields
262262
namespace = 'task' if self.protocol_number is None else f'task_{self.protocol_number:02}'
@@ -323,8 +323,7 @@ def _run_qc(self, trials_data, update=True, plot_qc=True):
323323
# Run the task QC
324324
qc = TaskQC(self.session_path, one=self.one, log=_logger)
325325
qc.extractor = TaskQCExtractor(self.session_path, lazy=True, one=qc.one, sync_collection=self.sync_collection,
326-
sync_type=self.sync, task_collection=self.collection,
327-
save_path=self.session_path.joinpath(self.output_collection))
326+
sync_type=self.sync, task_collection=self.collection)
328327
# Extract extra datasets required for QC
329328
qc.extractor.data = trials_data # FIXME This line is pointless
330329
qc.extractor.extract_data()
@@ -423,8 +422,7 @@ def _run_qc(self, trials_data, update=True, **kwargs):
423422
# TODO Task QC extractor for Timeline
424423
qc = TaskQC(self.session_path, one=self.one, log=_logger)
425424
qc.extractor = TaskQCExtractor(self.session_path, lazy=True, one=qc.one, sync_collection=self.sync_collection,
426-
sync_type=self.sync, task_collection=self.collection,
427-
save_path=self.session_path.joinpath(self.output_collection))
425+
sync_type=self.sync, task_collection=self.collection)
428426
# Extract extra datasets required for QC
429427
qc.extractor.data = TaskQCExtractor.rename_data(trials_data.copy())
430428
qc.extractor.load_raw_data()
@@ -466,7 +464,8 @@ def _run(self, upload=True):
466464

467465
df = training_status.get_latest_training_information(self.session_path, one)
468466
if df is not None:
469-
training_status.make_plots(self.session_path, self.one, df=df, save=True, upload=upload)
467+
training_status.make_plots(
468+
self.session_path, self.one, df=df, save=True, upload=upload, task_collection=self.collection)
470469
# Update status map in JSON field of subjects endpoint
471470
if self.one and not self.one.offline:
472471
_logger.debug('Updating JSON field of subjects endpoint')

0 commit comments

Comments
 (0)