Coverage for ibllib/pipes/behavior_tasks.py: 84%

269 statements  

« prev     ^ index     » next       coverage.py v7.5.4, created at 2024-07-08 17:16 +0100

1"""Standard task protocol extractor dynamic pipeline tasks.""" 

2import logging 

3import traceback 

4 

5from packaging import version 

6import one.alf.io as alfio 

7from one.alf.files import session_path_parts 

8from one.api import ONE 

9 

10from ibllib.oneibl.registration import get_lab 

11from ibllib.oneibl.data_handlers import ServerDataHandler 

12from ibllib.pipes import base_tasks 

13from ibllib.io.raw_data_loaders import load_settings, load_bpod_fronts 

14from ibllib.qc.task_extractors import TaskQCExtractor 

15from ibllib.qc.task_metrics import HabituationQC, TaskQC, update_dataset_qc 

16from ibllib.io.extractors.ephys_passive import PassiveChoiceWorld 

17from ibllib.io.extractors.bpod_trials import get_bpod_extractor 

18from ibllib.io.extractors.ephys_fpga import FpgaTrials, FpgaTrialsHabituation, get_sync_and_chn_map 

19from ibllib.io.extractors.mesoscope import TimelineTrials 

20from ibllib.pipes import training_status 

21from ibllib.plots.figures import BehaviourPlots 

22 

23_logger = logging.getLogger('ibllib') 

24 

25 

26class HabituationRegisterRaw(base_tasks.RegisterRawDataTask, base_tasks.BehaviourTask): 

27 priority = 100 

28 job_size = 'small' 

29 

30 @property 

31 def signature(self): 

32 signature = { 1z

33 'input_files': [], 

34 'output_files': [ 

35 ('_iblrig_taskData.raw.*', self.collection, True), 

36 ('_iblrig_taskSettings.raw.*', self.collection, True), 

37 ('_iblrig_encoderEvents.raw*', self.collection, False), 

38 ('_iblrig_encoderPositions.raw*', self.collection, False), 

39 ('_iblrig_encoderTrialInfo.raw*', self.collection, False), 

40 ('_iblrig_stimPositionScreen.raw*', self.collection, False), 

41 ('_iblrig_syncSquareUpdate.raw*', self.collection, False), 

42 ('_iblrig_ambientSensorData.raw*', self.collection, False) 

43 ] 

44 } 

45 return signature 1z

46 

47 

48class HabituationTrialsBpod(base_tasks.BehaviourTask): 

49 priority = 90 

50 job_size = 'small' 

51 

52 @property 

53 def signature(self): 

54 signature = { 1r

55 'input_files': [ 

56 ('_iblrig_taskData.raw.*', self.collection, True), 

57 ('_iblrig_taskSettings.raw.*', self.collection, True), 

58 ], 

59 'output_files': [ 

60 ('*trials.contrastLeft.npy', self.output_collection, True), 

61 ('*trials.contrastRight.npy', self.output_collection, True), 

62 ('*trials.feedback_times.npy', self.output_collection, True), 

63 ('*trials.feedbackType.npy', self.output_collection, True), 

64 ('*trials.goCue_times.npy', self.output_collection, True), 

65 ('*trials.goCueTrigger_times.npy', self.output_collection, True), 

66 ('*trials.intervals.npy', self.output_collection, True), 

67 ('*trials.rewardVolume.npy', self.output_collection, True), 

68 ('*trials.stimOff_times.npy', self.output_collection, True), 

69 ('*trials.stimOn_times.npy', self.output_collection, True), 

70 ('*trials.stimOnTrigger_times.npy', self.output_collection, True), 

71 ] 

72 } 

73 return signature 1r

74 

75 def _run(self, update=True, save=True): 

76 """Extracts an iblrig training session.""" 

77 trials, output_files = self.extract_behaviour(save=save) 1r

78 

79 if trials is None: 1r

80 return None 

81 if self.one is None or self.one.offline: 1r

82 return output_files 1r

83 

84 # Run the task QC 

85 self.run_qc(trials, update=update) 

86 return output_files 

87 

88 def extract_behaviour(self, **kwargs): 

89 self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection) 1rb

90 self.extractor.default_path = self.output_collection 1rb

91 return self.extractor.extract(task_collection=self.collection, **kwargs) 1rb

92 

93 def run_qc(self, trials_data=None, update=True): 

94 trials_data = self._assert_trials_data(trials_data) # validate trials data 1b

95 

96 # Compile task data for QC 

97 qc = HabituationQC(self.session_path, one=self.one) 1b

98 qc.extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, 1b

99 one=self.one, sync_type=self.sync, task_collection=self.collection) 

100 

101 # Update extractor fields 

102 qc.extractor.data = qc.extractor.rename_data(trials_data.copy()) 1b

103 qc.extractor.frame_ttls = self.extractor.frame2ttl # used in iblapps QC viewer 1b

104 qc.extractor.audio_ttls = self.extractor.audio # used in iblapps QC viewer 1b

105 qc.extractor.settings = self.extractor.settings 1b

106 

107 namespace = 'task' if self.protocol_number is None else f'task_{self.protocol_number:02}' 1b

108 qc.run(update=update, namespace=namespace) 1b

109 return qc 1b

110 

111 

112class HabituationTrialsNidq(HabituationTrialsBpod): 

113 priority = 90 

114 job_size = 'small' 

115 

116 @property 

117 def signature(self): 

118 signature = super().signature 

119 signature['input_files'] = [ 

120 ('_iblrig_taskData.raw.*', self.collection, True), 

121 ('_iblrig_taskSettings.raw.*', self.collection, True), 

122 (f'_{self.sync_namespace}_sync.channels.npy', self.sync_collection, True), 

123 (f'_{self.sync_namespace}_sync.polarities.npy', self.sync_collection, True), 

124 (f'_{self.sync_namespace}_sync.times.npy', self.sync_collection, True), 

125 ('*wiring.json', self.sync_collection, False), 

126 ('*.meta', self.sync_collection, True)] 

127 return signature 

128 

129 def extract_behaviour(self, save=True, **kwargs): 

130 """Extract the habituationChoiceWorld trial data using NI DAQ clock.""" 

131 # Extract Bpod trials 

132 bpod_trials, _ = super().extract_behaviour(save=False, **kwargs) 1b

133 

134 # Sync Bpod trials to FPGA 

135 sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection) 1b

136 self.extractor = FpgaTrialsHabituation( 1b

137 self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) 

138 

139 # NB: The stimOff times are called stimCenter times for habituation choice world 

140 outputs, files = self.extractor.extract( 1b

141 save=save, sync=sync, chmap=chmap, path_out=self.session_path.joinpath(self.output_collection), 

142 task_collection=self.collection, protocol_number=self.protocol_number, **kwargs) 

143 return outputs, files 1b

144 

145 def run_qc(self, trials_data=None, update=True, **_): 

146 """Run and update QC. 

147 

148 This adds the bpod TTLs to the QC object *after* the QC is run in the super call method. 

149 The raw Bpod TTLs are not used by the QC however they are used in the iblapps QC plot. 

150 """ 

151 qc = super().run_qc(trials_data=trials_data, update=update) 1b

152 qc.extractor.bpod_ttls = self.extractor.bpod 1b

153 return qc 1b

154 

155 

156class TrialRegisterRaw(base_tasks.RegisterRawDataTask, base_tasks.BehaviourTask): 

157 priority = 100 

158 job_size = 'small' 

159 

160 @property 

161 def signature(self): 

162 signature = { 1aABCcijklmhnd

163 'input_files': [], 

164 'output_files': [ 

165 ('_iblrig_taskData.raw.*', self.collection, True), 

166 ('_iblrig_taskSettings.raw.*', self.collection, True), 

167 ('_iblrig_encoderEvents.raw*', self.collection, False), 

168 ('_iblrig_encoderPositions.raw*', self.collection, False), 

169 ('_iblrig_encoderTrialInfo.raw*', self.collection, False), 

170 ('_iblrig_stimPositionScreen.raw*', self.collection, False), 

171 ('_iblrig_syncSquareUpdate.raw*', self.collection, False), 

172 ('_iblrig_ambientSensorData.raw*', self.collection, False) 

173 ] 

174 } 

175 return signature 1aABCcijklmhnd

176 

177 

178class PassiveRegisterRaw(base_tasks.RegisterRawDataTask, base_tasks.BehaviourTask): 

179 priority = 100 

180 job_size = 'small' 

181 

182 @property 

183 def signature(self): 

184 signature = { 1aDijklmhn

185 'input_files': [], 

186 'output_files': [('_iblrig_taskSettings.raw.*', self.collection, True), 

187 ('_iblrig_encoderEvents.raw*', self.collection, False), 

188 ('_iblrig_encoderPositions.raw*', self.collection, False), 

189 ('_iblrig_encoderTrialInfo.raw*', self.collection, False), 

190 ('_iblrig_stimPositionScreen.raw*', self.collection, False), 

191 ('_iblrig_syncSquareUpdate.raw*', self.collection, False), 

192 ('_iblrig_RFMapStim.raw*', self.collection, True)] 

193 } 

194 return signature 1aDijklmhn

195 

196 

197class PassiveTaskNidq(base_tasks.BehaviourTask): 

198 priority = 90 

199 job_size = 'small' 

200 

201 @property 

202 def signature(self): 

203 signature = { 1awxijklmny

204 'input_files': [('_iblrig_taskSettings.raw*', self.collection, True), 

205 ('_iblrig_RFMapStim.raw*', self.collection, True), 

206 (f'_{self.sync_namespace}_sync.channels.*', self.sync_collection, True), 

207 (f'_{self.sync_namespace}_sync.polarities.*', self.sync_collection, True), 

208 (f'_{self.sync_namespace}_sync.times.*', self.sync_collection, True), 

209 ('*.wiring.json', self.sync_collection, False), 

210 ('*.meta', self.sync_collection, False)], 

211 'output_files': [('_ibl_passiveGabor.table.csv', self.output_collection, False), 

212 ('_ibl_passivePeriods.intervalsTable.csv', self.output_collection, True), 

213 ('_ibl_passiveRFM.times.npy', self.output_collection, True), 

214 ('_ibl_passiveStims.table.csv', self.output_collection, False)] 

215 } 

216 return signature 1awxijklmny

217 

218 def _run(self, **kwargs): 

219 """returns a list of pathlib.Paths. """ 

220 data, paths = PassiveChoiceWorld(self.session_path).extract( 1wxy

221 sync_collection=self.sync_collection, task_collection=self.collection, save=True, 

222 path_out=self.session_path.joinpath(self.output_collection), protocol_number=self.protocol_number) 

223 

224 if any(x is None for x in paths): 1wxy

225 self.status = -1 

226 

227 return paths 1wxy

228 

229 

230class PassiveTaskTimeline(base_tasks.BehaviourTask, base_tasks.MesoscopeTask): 

231 """TODO should be mesoscope invariant, using wiring file""" 

232 priority = 90 

233 job_size = 'small' 

234 

235 @property 

236 def signature(self): 

237 signature = { 1h

238 'input_files': [('_iblrig_taskSettings.raw*', self.collection, True), 

239 ('_iblrig_RFMapStim.raw*', self.collection, True), 

240 (f'_{self.sync_namespace}_sync.channels.*', self.sync_collection, False), 

241 (f'_{self.sync_namespace}_sync.polarities.*', self.sync_collection, False), 

242 (f'_{self.sync_namespace}_sync.times.*', self.sync_collection, False)], 

243 'output_files': [('_ibl_passiveGabor.table.csv', self.output_collection, False), 

244 ('_ibl_passivePeriods.intervalsTable.csv', self.output_collection, True), 

245 ('_ibl_passiveRFM.times.npy', self.output_collection, True), 

246 ('_ibl_passiveStims.table.csv', self.output_collection, False)] 

247 } 

248 return signature 1h

249 

250 def _run(self, **kwargs): 

251 """returns a list of pathlib.Paths. 

252 This class exists to load the sync file and set the protocol_number to None 

253 """ 

254 settings = load_settings(self.session_path, self.collection) 

255 ver = settings.get('IBLRIG_VERSION') or '100.0.0' 

256 if ver == '100.0.0' or version.parse(ver) <= version.parse('7.1.0'): 

257 _logger.warning('Protocol spacers not supported; setting protocol_number to None') 

258 self.protocol_number = None 

259 

260 sync, chmap = self.load_sync() 

261 data, paths = PassiveChoiceWorld(self.session_path).extract( 

262 sync_collection=self.sync_collection, task_collection=self.collection, save=True, 

263 path_out=self.session_path.joinpath(self.output_collection), 

264 protocol_number=self.protocol_number, sync=sync, sync_map=chmap) 

265 

266 if any(x is None for x in paths): 

267 self.status = -1 

268 

269 return paths 

270 

271 

272class ChoiceWorldTrialsBpod(base_tasks.BehaviourTask): 

273 priority = 90 

274 job_size = 'small' 

275 extractor = None 

276 """ibllib.io.extractors.base.BaseBpodTrialsExtractor: An instance of the Bpod trials extractor.""" 

277 

278 @property 

279 def signature(self): 

280 signature = { 1opqcd

281 'input_files': [ 

282 ('_iblrig_taskData.raw.*', self.collection, True), 

283 ('_iblrig_taskSettings.raw.*', self.collection, True), 

284 ('_iblrig_encoderEvents.raw*', self.collection, True), 

285 ('_iblrig_encoderPositions.raw*', self.collection, True)], 

286 'output_files': [ 

287 ('*trials.goCueTrigger_times.npy', self.output_collection, True), 

288 ('*trials.stimOnTrigger_times.npy', self.output_collection, False), 

289 ('*trials.table.pqt', self.output_collection, True), 

290 ('*wheel.position.npy', self.output_collection, True), 

291 ('*wheel.timestamps.npy', self.output_collection, True), 

292 ('*wheelMoves.intervals.npy', self.output_collection, True), 

293 ('*wheelMoves.peakAmplitude.npy', self.output_collection, True) 

294 ] 

295 } 

296 return signature 1opqcd

297 

298 def _run(self, update=True, save=True, **kwargs): 

299 """Extracts an iblrig training session.""" 

300 trials, output_files = self.extract_behaviour(save=save) 1ogpqcfd

301 if trials is None: 1ogpqcfd

302 return None 

303 if self.one is None or self.one.offline: 1ogpqcfd

304 return output_files 1ogpqf

305 

306 # Run the task QC 

307 qc = self.run_qc(trials, update=update, **kwargs) 1cd

308 if update and not self.one.offline: 1cd

309 on_server = self.location == 'server' and isinstance(self.data_handler, ServerDataHandler) 1cd

310 if not on_server: 1cd

311 _logger.warning('Updating dataset QC only supported on local servers') 

312 else: 

313 labs = get_lab(self.session_path, self.one.alyx) 1cd

314 # registered_dsets = self.register_datasets(labs=labs) 

315 datasets = self.data_handler.uploadData(output_files, self.version, labs=labs) 1cd

316 update_dataset_qc(qc, datasets, self.one) 1cd

317 

318 return output_files 1cd

319 

320 def extract_behaviour(self, **kwargs): 

321 self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection) 1ogpqcfbestduv

322 _logger.info('Bpod trials extractor: %s.%s', 1ogpqcfbestduv

323 self.extractor.__module__, self.extractor.__class__.__name__) 

324 self.extractor.default_path = self.output_collection 1ogpqcfbestduv

325 return self.extractor.extract(task_collection=self.collection, **kwargs) 1ogpqcfbestduv

326 

327 def run_qc(self, trials_data=None, update=True, QC=None): 

328 """ 

329 Run the task QC. 

330 

331 Parameters 

332 ---------- 

333 trials_data : dict 

334 The complete extracted task data. 

335 update : bool 

336 If True, updates the session QC fields on Alyx. 

337 QC : ibllib.qc.task_metrics.TaskQC 

338 An optional QC class to instantiate. 

339 

340 Returns 

341 ------- 

342 ibllib.qc.task_metrics.TaskQC 

343 The task QC object. 

344 """ 

345 trials_data = self._assert_trials_data(trials_data) # validate trials data 1cd

346 

347 # Compile task data for QC 

348 qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, 1cd

349 sync_type=self.sync, task_collection=self.collection) 

350 qc_extractor.data = qc_extractor.rename_data(trials_data) 1cd

351 if not QC: 1cd

352 QC = HabituationQC if type(self.extractor).__name__ == 'HabituationTrials' else TaskQC 1cd

353 _logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__) 1cd

354 qc = QC(self.session_path, one=self.one, log=_logger) 1cd

355 if QC is not HabituationQC: 1cd

356 qc_extractor.wheel_encoding = 'X1' 1cd

357 qc_extractor.settings = self.extractor.settings 1cd

358 qc_extractor.frame_ttls, qc_extractor.audio_ttls = load_bpod_fronts( 1cd

359 self.session_path, task_collection=self.collection) 

360 qc.extractor = qc_extractor 1cd

361 

362 # Aggregate and update Alyx QC fields 

363 namespace = 'task' if self.protocol_number is None else f'task_{self.protocol_number:02}' 1cd

364 qc.run(update=update, namespace=namespace) 1cd

365 return qc 1cd

366 

367 

368class ChoiceWorldTrialsNidq(ChoiceWorldTrialsBpod): 

369 priority = 90 

370 job_size = 'small' 

371 

372 @property 

373 def signature(self): 

374 signature = { 1agijklmhnf

375 'input_files': [ 

376 ('_iblrig_taskData.raw.*', self.collection, True), 

377 ('_iblrig_taskSettings.raw.*', self.collection, True), 

378 ('_iblrig_encoderEvents.raw*', self.collection, True), 

379 ('_iblrig_encoderPositions.raw*', self.collection, True), 

380 (f'_{self.sync_namespace}_sync.channels.npy', self.sync_collection, True), 

381 (f'_{self.sync_namespace}_sync.polarities.npy', self.sync_collection, True), 

382 (f'_{self.sync_namespace}_sync.times.npy', self.sync_collection, True), 

383 ('*wiring.json', self.sync_collection, False), 

384 ('*.meta', self.sync_collection, True)], 

385 'output_files': [ 

386 ('*trials.goCueTrigger_times.npy', self.output_collection, True), 

387 ('*trials.intervals_bpod.npy', self.output_collection, False), 

388 ('*trials.stimOff_times.npy', self.output_collection, False), 

389 ('*trials.table.pqt', self.output_collection, True), 

390 ('*wheel.position.npy', self.output_collection, True), 

391 ('*wheel.timestamps.npy', self.output_collection, True), 

392 ('*wheelMoves.intervals.npy', self.output_collection, True), 

393 ('*wheelMoves.peakAmplitude.npy', self.output_collection, True) 

394 ] 

395 } 

396 return signature 1agijklmhnf

397 

398 def _behaviour_criterion(self, update=True, truncate_to_pass=True): 

399 """ 

400 Computes and update the behaviour criterion on Alyx 

401 """ 

402 from brainbox.behavior import training 

403 

404 trials = alfio.load_object(self.session_path.joinpath(self.output_collection), 'trials').to_df() 

405 good_enough = training.criterion_delay( 

406 n_trials=trials.shape[0], 

407 perf_easy=training.compute_performance_easy(trials), 

408 ) 

409 if truncate_to_pass and not good_enough: 

410 n_trials = trials.shape[0] 

411 while not good_enough and n_trials > 400: 

412 n_trials -= 1 

413 good_enough = training.criterion_delay( 

414 n_trials=n_trials, 

415 perf_easy=training.compute_performance_easy(trials[:n_trials]), 

416 ) 

417 

418 if update: 

419 eid = self.one.path2eid(self.session_path, query_type='remote') 

420 self.one.alyx.json_field_update( 

421 "sessions", eid, "extended_qc", {"behavior": int(good_enough)} 

422 ) 

423 

424 def extract_behaviour(self, save=True, **kwargs): 

425 # Extract Bpod trials 

426 bpod_trials, _ = super().extract_behaviour(save=False, **kwargs) 1gbestuv

427 

428 # Sync Bpod trials to FPGA 

429 sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection) 1gbestuv

430 self.extractor = FpgaTrials(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) 1gbestuv

431 outputs, files = self.extractor.extract( 1gbestuv

432 save=save, sync=sync, chmap=chmap, path_out=self.session_path.joinpath(self.output_collection), 

433 task_collection=self.collection, protocol_number=self.protocol_number, **kwargs) 

434 return outputs, files 1gbestuv

435 

436 def run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None): 

437 trials_data = self._assert_trials_data(trials_data) # validate trials data 1be

438 

439 # Compile task data for QC 

440 qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, 1be

441 sync_type=self.sync, task_collection=self.collection) 

442 qc_extractor.data = qc_extractor.rename_data(trials_data.copy()) 1be

443 if not QC: 1be

444 QC = HabituationQC if type(self.extractor).__name__ == 'HabituationTrials' else TaskQC 1be

445 _logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__) 1be

446 qc = QC(self.session_path, one=self.one, log=_logger) 1be

447 if QC is not HabituationQC: 1be

448 # Add Bpod wheel data 

449 wheel_ts_bpod = self.extractor.bpod2fpga(self.extractor.bpod_trials['wheel_timestamps']) 1be

450 qc_extractor.data['wheel_timestamps_bpod'] = wheel_ts_bpod 1be

451 qc_extractor.data['wheel_position_bpod'] = self.extractor.bpod_trials['wheel_position'] 1be

452 qc_extractor.wheel_encoding = 'X4' 1be

453 qc_extractor.frame_ttls = self.extractor.frame2ttl 1be

454 qc_extractor.audio_ttls = self.extractor.audio 1be

455 qc_extractor.bpod_ttls = self.extractor.bpod 1be

456 qc_extractor.settings = self.extractor.settings 1be

457 qc.extractor = qc_extractor 1be

458 

459 # Aggregate and update Alyx QC fields 

460 namespace = 'task' if self.protocol_number is None else f'task_{self.protocol_number:02}' 1be

461 qc.run(update=update, namespace=namespace) 1be

462 

463 if plot_qc: 1be

464 _logger.info('Creating Trials QC plots') 

465 try: 

466 session_id = self.one.path2eid(self.session_path) 

467 plot_task = BehaviourPlots( 

468 session_id, self.session_path, one=self.one, task_collection=self.output_collection) 

469 _ = plot_task.run() 

470 self.plot_tasks.append(plot_task) 

471 except Exception: 

472 _logger.error('Could not create Trials QC Plot') 

473 _logger.error(traceback.format_exc()) 

474 self.status = -1 

475 return qc 1be

476 

477 def _run(self, update=True, plot_qc=True, save=True): 

478 output_files = super()._run(update=update, save=save, plot_qc=plot_qc) 1gf

479 if update and not self.one.offline: 1gf

480 self._behaviour_criterion(update=update) 

481 

482 return output_files 1gf

483 

484 

485class ChoiceWorldTrialsTimeline(ChoiceWorldTrialsNidq): 

486 """Behaviour task extractor with DAQdata.raw NPY datasets.""" 

487 @property 

488 def signature(self): 

489 signature = super().signature 1hf

490 signature['input_files'] = [ 1hf

491 ('_iblrig_taskData.raw.*', self.collection, True), 

492 ('_iblrig_taskSettings.raw.*', self.collection, True), 

493 ('_iblrig_encoderEvents.raw*', self.collection, True), 

494 ('_iblrig_encoderPositions.raw*', self.collection, True), 

495 (f'_{self.sync_namespace}_DAQdata.raw.npy', self.sync_collection, True), 

496 (f'_{self.sync_namespace}_DAQdata.timestamps.npy', self.sync_collection, True), 

497 (f'_{self.sync_namespace}_DAQdata.meta.json', self.sync_collection, True), 

498 ] 

499 if self.protocol: 1hf

500 extractor = get_bpod_extractor(self.session_path, protocol=self.protocol) 1hf

501 if extractor.save_names: 1hf

502 signature['output_files'] = [(fn, self.output_collection, True) 1hf

503 for fn in filter(None, extractor.save_names)] 

504 return signature 1hf

505 

506 def extract_behaviour(self, save=True, **kwargs): 

507 """Extract the Bpod trials data and Timeline acquired signals.""" 

508 # First determine the extractor from the task protocol 

509 bpod_trials, _ = ChoiceWorldTrialsBpod.extract_behaviour(self, save=False, **kwargs) 1f

510 

511 # Sync Bpod trials to DAQ 

512 self.extractor = TimelineTrials(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) 1f

513 save_path = self.session_path / self.output_collection 1f

514 if not self._spacer_support(self.extractor.settings): 1f

515 _logger.warning('Protocol spacers not supported; setting protocol_number to None') 1f

516 self.protocol_number = None 1f

517 

518 dsets, out_files = self.extractor.extract( 1f

519 save=save, path_out=save_path, sync_collection=self.sync_collection, 

520 task_collection=self.collection, protocol_number=self.protocol_number, **kwargs) 

521 

522 return dsets, out_files 1f

523 

524 

525class TrainingStatus(base_tasks.BehaviourTask): 

526 priority = 90 

527 job_size = 'small' 

528 

529 @property 

530 def signature(self): 

531 signature = { 1acijklmhnd

532 'input_files': [ 

533 ('_iblrig_taskData.raw.*', self.collection, True), 

534 ('_iblrig_taskSettings.raw.*', self.collection, True), 

535 ('*trials.table.pqt', self.output_collection, True)], 

536 'output_files': [] 

537 } 

538 return signature 1acijklmhnd

539 

540 def _run(self, upload=True): 

541 """ 

542 Extracts training status for subject. 

543 """ 

544 

545 lab = get_lab(self.session_path, self.one.alyx) 1cd

546 if lab == 'cortexlab' and 'cortexlab' in self.one.alyx.base_url: 1cd

547 _logger.info('Switching from cortexlab Alyx to IBL Alyx for training status queries.') 

548 one = ONE(base_url='https://alyx.internationalbrainlab.org') 

549 else: 

550 one = self.one 1cd

551 

552 df = training_status.get_latest_training_information(self.session_path, one) 1cd

553 if df is not None: 1cd

554 training_status.make_plots( 1cd

555 self.session_path, self.one, df=df, save=True, upload=upload, task_collection=self.collection) 

556 # Update status map in JSON field of subjects endpoint 

557 if self.one and not self.one.offline: 1cd

558 _logger.debug('Updating JSON field of subjects endpoint') 1cd

559 status = (df.set_index('date')[['training_status', 'session_path']].drop_duplicates( 1cd

560 subset='training_status', keep='first').to_dict()) 

561 date, sess = status.items() 1cd

562 data = {'trained_criteria': {v.replace(' ', '_'): (k, self.one.path2eid(sess[1][k])) 1cd

563 for k, v in date[1].items()}} 

564 _, subject, *_ = session_path_parts(self.session_path) 1cd

565 self.one.alyx.json_field_update('subjects', subject, data=data) 1cd

566 output_files = [] 1cd

567 return output_files 1cd