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

292 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-07 14:26 +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.path import session_path_parts 

8from one.api import ONE 

9 

10from ibllib.oneibl.registration import get_lab 

11from ibllib.oneibl.data_handlers import ServerDataHandler, ExpectedDataset 

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, TimelineTrialsHabituation 

20from ibllib.pipes import training_status 

21from ibllib.plots.figures import BehaviourPlots 

22 

23_logger = logging.getLogger(__name__) 

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 = { 1D

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 1D

46 

47 

48class HabituationTrialsBpod(base_tasks.BehaviourTask): 

49 priority = 90 

50 job_size = 'small' 

51 

52 @property 

53 def signature(self): 

54 signature = { 1ogt

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.stimOffTrigger_times.npy', self.output_collection, False), 

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

72 ] 

73 } 

74 return signature 1ogt

75 

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

77 """Extracts an iblrig training session.""" 

78 trials, output_files = self.extract_behaviour(save=save) 1ot

79 

80 if trials is None: 1ot

81 return None 1t

82 if self.one is None or self.one.offline: 1o

83 return output_files 1o

84 

85 # Run the task QC 

86 self.run_qc(trials, update=update) 

87 return output_files 

88 

89 def extract_behaviour(self, **kwargs): 

90 settings = load_settings(self.session_path, self.collection) 1ogbt

91 if version.parse(settings['IBLRIG_VERSION'] or '100.0.0') < version.parse('5.0.0'): 1ogbt

92 _logger.error('No extraction of legacy habituation sessions') 1t

93 return None, None 1t

94 self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection) 1ogb

95 self.extractor.default_path = self.output_collection 1ogb

96 return self.extractor.extract(task_collection=self.collection, **kwargs) 1ogb

97 

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

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

100 

101 # Compile task data for QC 

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

103 qc.extractor = TaskQCExtractor(self.session_path) 1b

104 

105 # Update extractor fields 

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

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

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

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

110 

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

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

113 return qc 1b

114 

115 

116class HabituationTrialsNidq(HabituationTrialsBpod): 

117 priority = 90 

118 job_size = 'small' 

119 

120 @property 

121 def signature(self): 

122 signature = super().signature 1g

123 signature['input_files'] = [ 1g

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

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

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

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

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

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

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

131 return signature 1g

132 

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

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

135 # Extract Bpod trials 

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

137 

138 # Sync Bpod trials to FPGA 

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

140 self.extractor = FpgaTrialsHabituation( 1b

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

142 

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

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

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

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

147 return outputs, files 1b

148 

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

150 """Run and update QC. 

151 

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

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

154 """ 

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

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

157 return qc 1b

158 

159 

160class HabituationTrialsTimeline(HabituationTrialsNidq): 

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

162 @property 

163 def signature(self): 

164 signature = super().signature 1g

165 signature['input_files'] = [ 1g

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

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

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

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

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

171 ] 

172 return signature 1g

173 

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

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

176 # First determine the extractor from the task protocol 

177 bpod_trials, _ = HabituationTrialsBpod.extract_behaviour(self, save=False, **kwargs) 1g

178 

179 # Sync Bpod trials to DAQ 

180 self.extractor = TimelineTrialsHabituation(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) 1g

181 save_path = self.session_path / self.output_collection 1g

182 if not self._spacer_support(self.extractor.settings): 1g

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

184 self.protocol_number = None 

185 

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

187 dsets, out_files = self.extractor.extract( 1g

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

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

190 

191 return dsets, out_files 1g

192 

193 

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

195 priority = 100 

196 job_size = 'small' 

197 

198 @property 

199 def signature(self): 

200 signature = { 1aEFGcijklmhn

201 'input_files': [], 

202 'output_files': [ 

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

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

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

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

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

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

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

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

211 ] 

212 } 

213 return signature 1aEFGcijklmhn

214 

215 

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

217 priority = 100 

218 job_size = 'small' 

219 

220 @property 

221 def signature(self): 

222 signature = { 1aHijklmn

223 'input_files': [], 

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

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

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

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

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

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

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

231 } 

232 return signature 1aHijklmn

233 

234 

235class PassiveTaskNidq(base_tasks.BehaviourTask): 

236 priority = 90 

237 job_size = 'small' 

238 

239 @property 

240 def signature(self): 

241 signature = { 1azAijklmnB

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

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

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

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

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

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

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

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

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

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

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

253 } 

254 return signature 1azAijklmnB

255 

256 def _run(self, **kwargs): 

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

258 data, paths = PassiveChoiceWorld(self.session_path).extract( 1zAB

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

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

261 

262 if any(x is None for x in paths): 1zAB

263 self.status = -1 

264 

265 return paths 1zAB

266 

267 

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

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

270 priority = 90 

271 job_size = 'small' 

272 

273 @property 

274 def signature(self): 

275 signature = { 

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

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

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

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

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

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

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

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

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

285 } 

286 return signature 

287 

288 def _run(self, **kwargs): 

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

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

291 """ 

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

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

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

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

296 self.protocol_number = None 

297 

298 sync, chmap = self.load_sync() 

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

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

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

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

303 

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

305 self.status = -1 

306 

307 return paths 

308 

309 

310class ChoiceWorldTrialsBpod(base_tasks.BehaviourTask): 

311 priority = 90 

312 job_size = 'small' 

313 extractor = None 

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

315 

316 @property 

317 def signature(self): 

318 signature = { 1IJpqrcs

319 'input_files': [ 

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

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

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

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

324 'output_files': [ 

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

326 ('*trials.stimOffTrigger_times.npy', self.output_collection, False), 

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

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

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

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

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

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

333 ] 

334 } 

335 return signature 1IJpqrcs

336 

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

338 """Extracts an iblrig training session.""" 

339 trials, output_files = self.extract_behaviour(save=save) 1pfqrcds

340 if trials is None: 1pfqrcds

341 return None 

342 if self.one is None or self.one.offline: 1pfqrcds

343 return output_files 1pfqrds

344 

345 # Run the task QC 

346 qc = self.run_qc(trials, update=update, **kwargs) 1c

347 if update and not self.one.offline: 1c

348 on_server = self.location == 'server' and isinstance(self.data_handler, ServerDataHandler) 1c

349 if not on_server: 1c

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

351 else: 

352 labs = get_lab(self.session_path, self.one.alyx) 1c

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

354 datasets = self.data_handler.uploadData(output_files, self.version, labs=labs) 1c

355 update_dataset_qc(qc, datasets, self.one) 1c

356 

357 return output_files 1c

358 

359 def extract_behaviour(self, **kwargs): 

360 self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection) 1aCpfqrcdbeuvwsxy

361 _logger.info('Bpod trials extractor: %s.%s', 1aCpfqrcdbeuvwsxy

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

363 self.extractor.default_path = self.output_collection 1aCpfqrcdbeuvwsxy

364 return self.extractor.extract(task_collection=self.collection, **kwargs) 1aCpfqrcdbeuvwsxy

365 

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

367 """ 

368 Run the task QC. 

369 

370 Parameters 

371 ---------- 

372 trials_data : dict 

373 The complete extracted task data. 

374 update : bool 

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

376 QC : ibllib.qc.task_metrics.TaskQC 

377 An optional QC class to instantiate. 

378 

379 Returns 

380 ------- 

381 ibllib.qc.task_metrics.TaskQC 

382 The task QC object. 

383 """ 

384 trials_data = self._assert_trials_data(trials_data) # validate trials data 1ac

385 

386 # Compile task data for QC 

387 qc_extractor = TaskQCExtractor(self.session_path) 1ac

388 qc_extractor.data = qc_extractor.rename_data(trials_data) 1ac

389 if not QC: 1ac

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

391 _logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__) 1ac

392 qc = QC(self.session_path, one=self.one, log=_logger) 1ac

393 if QC is not HabituationQC: 1ac

394 qc_extractor.wheel_encoding = 'X1' 1ac

395 qc_extractor.settings = self.extractor.settings 1ac

396 qc_extractor.frame_ttls, qc_extractor.audio_ttls = load_bpod_fronts( 1ac

397 self.session_path, task_collection=self.collection) 

398 qc.extractor = qc_extractor 1ac

399 

400 # Aggregate and update Alyx QC fields 

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

402 qc.run(update=update, namespace=namespace) 1ac

403 return qc 1ac

404 

405 

406class ChoiceWorldTrialsNidq(ChoiceWorldTrialsBpod): 

407 priority = 90 

408 job_size = 'small' 

409 

410 @property 

411 def signature(self): 

412 I = ExpectedDataset.input # noqa 1afijklmhnd

413 ns = self.sync_namespace 1afijklmhnd

414 # Neuropixels 3A sync data are kept in individual probe collections 

415 v3A = ( 1afijklmhnd

416 I(f'_{ns}_sync.channels.probe??.npy', f'{self.sync_collection}/probe??', True, unique=False) & 

417 I(f'_{ns}_sync.polarities.probe??.npy', f'{self.sync_collection}/probe??', True, unique=False) & 

418 I(f'_{ns}_sync.times.probe??.npy', f'{self.sync_collection}/probe??', True, unique=False) & 

419 I(f'_{ns}_*.ap.meta', f'{self.sync_collection}/probe??', True, unique=False) & 

420 I(f'_{ns}_*wiring.json', f'{self.sync_collection}/probe??', False, unique=False) 

421 ) 

422 # Neuropixels 3B sync data are kept in probe-independent datasets 

423 v3B = ( 1afijklmhnd

424 I(f'_{ns}_sync.channels.npy', self.sync_collection, True) & 

425 I(f'_{ns}_sync.polarities.npy', self.sync_collection, True) & 

426 I(f'_{ns}_sync.times.npy', self.sync_collection, True) & 

427 I(f'_{ns}_*.meta', self.sync_collection, True) & 

428 I(f'_{ns}_*wiring.json', self.sync_collection, False) 

429 ) 

430 signature = { 1afijklmhnd

431 'input_files': [ 

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

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

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

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

436 v3B | (~v3B & v3A) # either 3B datasets OR 3A datasets must be present 

437 ], 

438 'output_files': [ 

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

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

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

442 ('*trials.stimOffTrigger_times.npy', self.output_collection, False), 

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

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

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

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

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

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

449 ] 

450 } 

451 return signature 1afijklmhnd

452 

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

454 """ 

455 Computes and update the behaviour criterion on Alyx 

456 """ 

457 from brainbox.behavior import training 

458 

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

460 good_enough, _ = training.criterion_delay( 

461 n_trials=trials.shape[0], 

462 perf_easy=training.compute_performance_easy(trials), 

463 ) 

464 if truncate_to_pass and not good_enough: 

465 n_trials = trials.shape[0] 

466 while not good_enough and n_trials > 400: 

467 n_trials -= 1 

468 good_enough, _ = training.criterion_delay( 

469 n_trials=n_trials, 

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

471 ) 

472 

473 if update: 

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

475 self.one.alyx.json_field_update( 

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

477 ) 

478 

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

480 # Extract Bpod trials 

481 bpod_trials, _ = super().extract_behaviour(save=False, **kwargs) 1fbeuvwxy

482 

483 # Sync Bpod trials to FPGA 

484 sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection) 1fbeuvwxy

485 self.extractor = FpgaTrials(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) 1fbeuvwxy

486 outputs, files = self.extractor.extract( 1fbeuvwxy

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

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

489 return outputs, files 1fbeuvwxy

490 

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

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

493 

494 # Compile task data for QC 

495 qc_extractor = TaskQCExtractor(self.session_path) 1be

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

497 if not QC: 1be

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

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

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

501 if QC is not HabituationQC: 1be

502 # Add Bpod wheel data 

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

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

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

506 qc_extractor.wheel_encoding = 'X4' 1be

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

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

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

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

511 qc.extractor = qc_extractor 1be

512 

513 # Aggregate and update Alyx QC fields 

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

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

516 

517 if plot_qc: 1be

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

519 try: 

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

521 plot_task = BehaviourPlots( 

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

523 _ = plot_task.run() 

524 self.plot_tasks.append(plot_task) 

525 except Exception: 

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

527 _logger.error(traceback.format_exc()) 

528 self.status = -1 

529 return qc 1be

530 

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

532 output_files = super()._run(update=update, save=save, plot_qc=plot_qc) 1fd

533 if update and not self.one.offline: 1fd

534 self._behaviour_criterion(update=update) 

535 

536 return output_files 1fd

537 

538 

539class ChoiceWorldTrialsTimeline(ChoiceWorldTrialsNidq): 

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

541 @property 

542 def signature(self): 

543 signature = super().signature 1hd

544 signature['input_files'] = [ 1hd

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

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

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

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

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

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

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

552 ] 

553 if self.protocol: 1hd

554 extractor = get_bpod_extractor(self.session_path, protocol=self.protocol) 1hd

555 if extractor.save_names: 1hd

556 signature['output_files'] = [(fn, self.output_collection, True) 1hd

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

558 return signature 1hd

559 

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

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

562 # First determine the extractor from the task protocol 

563 bpod_trials, _ = ChoiceWorldTrialsBpod.extract_behaviour(self, save=False, **kwargs) 1d

564 

565 # Sync Bpod trials to DAQ 

566 self.extractor = TimelineTrials(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) 1d

567 save_path = self.session_path / self.output_collection 1d

568 if not self._spacer_support(self.extractor.settings): 1d

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

570 self.protocol_number = None 1d

571 

572 dsets, out_files = self.extractor.extract( 1d

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

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

575 

576 return dsets, out_files 1d

577 

578 

579class TrainingStatus(base_tasks.BehaviourTask): 

580 priority = 90 

581 job_size = 'small' 

582 

583 @property 

584 def signature(self): 

585 signature = { 1c

586 'input_files': [ 

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

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

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

590 'output_files': [] 

591 } 

592 return signature 1c

593 

594 def _run(self, upload=True): 

595 """ 

596 Extracts training status for subject. 

597 """ 

598 

599 lab = get_lab(self.session_path, self.one.alyx) 1c

600 if lab == 'cortexlab' and 'cortexlab' in self.one.alyx.base_url: 1c

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

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

603 else: 

604 one = self.one 1c

605 

606 df = training_status.get_latest_training_information(self.session_path, one) 1c

607 if df is not None: 1c

608 training_status.make_plots( 1c

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

610 # Update status map in JSON field of subjects endpoint 

611 if self.one and not self.one.offline: 1c

612 _logger.debug('Updating JSON field of subjects endpoint') 1c

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

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

615 date, sess = status.items() 1c

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

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

618 _, subject, *_ = session_path_parts(self.session_path) 1c

619 self.one.alyx.json_field_update('subjects', subject, data=data) 1c

620 output_files = [] 1c

621 return output_files 1c