• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

int-brain-lab / ibllib / 7961675356254463

pending completion
7961675356254463

Pull #557

continuous-integration/UCL

olivier
add test
Pull Request #557: Chained protocols

718 of 718 new or added lines in 27 files covered. (100.0%)

12554 of 18072 relevant lines covered (69.47%)

0.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.69
/ibllib/io/extractors/training_trials.py
1
import logging
1✔
2
import numpy as np
1✔
3
from pkg_resources import parse_version
1✔
4
from one.alf.io import AlfBunch
1✔
5

6
import ibllib.io.raw_data_loaders as raw
1✔
7
from ibllib.io.extractors.base import BaseBpodTrialsExtractor, run_extractor_classes
1✔
8
from ibllib.io.extractors.training_wheel import Wheel
1✔
9

10

11
_logger = logging.getLogger(__name__)
1✔
12

13

14
class FeedbackType(BaseBpodTrialsExtractor):
1✔
15
    """
1✔
16
    Get the feedback that was delivered to subject.
17
    **Optional:** saves _ibl_trials.feedbackType.npy
18

19
    Checks in raw datafile for error and reward state.
20
    Will raise an error if more than one of the mutually exclusive states have
21
    been triggered.
22

23
    Sets feedbackType to -1 if error state was triggered (applies to no-go)
24
    Sets feedbackType to +1 if reward state was triggered
25
    """
26
    save_names = '_ibl_trials.feedbackType.npy'
1✔
27
    var_names = 'feedbackType'
1✔
28

29
    def _extract(self):
1✔
30
        feedbackType = np.empty(len(self.bpod_trials))
1✔
31
        feedbackType.fill(np.nan)
1✔
32
        reward = []
1✔
33
        error = []
1✔
34
        no_go = []
1✔
35
        for t in self.bpod_trials:
1✔
36
            reward.append(~np.isnan(t['behavior_data']['States timestamps']['reward'][0][0]))
1✔
37
            error.append(~np.isnan(t['behavior_data']['States timestamps']['error'][0][0]))
1✔
38
            no_go.append(~np.isnan(t['behavior_data']['States timestamps']['no_go'][0][0]))
1✔
39

40
        if not all(np.sum([reward, error, no_go], axis=0) == np.ones(len(self.bpod_trials))):
1✔
41
            raise ValueError
×
42

43
        feedbackType[reward] = 1
1✔
44
        feedbackType[error] = -1
1✔
45
        feedbackType[no_go] = -1
1✔
46
        feedbackType = feedbackType.astype('int64')
1✔
47
        return feedbackType
1✔
48

49

50
class ContrastLR(BaseBpodTrialsExtractor):
1✔
51
    """
1✔
52
    Get left and right contrasts from raw datafile. Optionally, saves
53
    _ibl_trials.contrastLeft.npy and _ibl_trials.contrastRight.npy to alf folder.
54

55
    Uses signed_contrast to create left and right contrast vectors.
56
    """
57
    save_names = ('_ibl_trials.contrastLeft.npy', '_ibl_trials.contrastRight.npy')
1✔
58
    var_names = ('contrastLeft', 'contrastRight')
1✔
59

60
    def _extract(self):
1✔
61
        contrastLeft = np.array([t['contrast']['value'] if np.sign(
1✔
62
            t['position']) < 0 else np.nan for t in self.bpod_trials])
63
        contrastRight = np.array([t['contrast']['value'] if np.sign(
1✔
64
            t['position']) > 0 else np.nan for t in self.bpod_trials])
65

66
        return contrastLeft, contrastRight
1✔
67

68

69
class ProbabilityLeft(BaseBpodTrialsExtractor):
1✔
70
    save_names = '_ibl_trials.probabilityLeft.npy'
1✔
71
    var_names = 'probabilityLeft'
1✔
72

73
    def _extract(self, **kwargs):
1✔
74
        return np.array([t['stim_probability_left'] for t in self.bpod_trials])
1✔
75

76

77
class Choice(BaseBpodTrialsExtractor):
1✔
78
    """
1✔
79
    Get the subject's choice in every trial.
80
    **Optional:** saves _ibl_trials.choice.npy to alf folder.
81

82
    Uses signed_contrast and trial_correct.
83
    -1 is a CCW turn (towards the left)
84
    +1 is a CW turn (towards the right)
85
    0 is a no_go trial
86
    If a trial is correct the choice of the animal was the inverse of the sign
87
    of the position.
88

89
    >>> choice[t] = -np.sign(position[t]) if trial_correct[t]
90
    """
91
    save_names = '_ibl_trials.choice.npy'
1✔
92
    var_names = 'choice'
1✔
93

94
    def _extract(self):
1✔
95
        sitm_side = np.array([np.sign(t['position']) for t in self.bpod_trials])
1✔
96
        trial_correct = np.array([t['trial_correct'] for t in self.bpod_trials])
1✔
97
        trial_nogo = np.array(
1✔
98
            [~np.isnan(t['behavior_data']['States timestamps']['no_go'][0][0])
99
             for t in self.bpod_trials])
100
        choice = sitm_side.copy()
1✔
101
        choice[trial_correct] = -choice[trial_correct]
1✔
102
        choice[trial_nogo] = 0
1✔
103
        choice = choice.astype(int)
1✔
104
        return choice
1✔
105

106

107
class RepNum(BaseBpodTrialsExtractor):
1✔
108
    """
1✔
109
    Count the consecutive repeated trials.
110
    **Optional:** saves _ibl_trials.repNum.npy to alf folder.
111

112
    Creates trial_repeated from trial['contrast']['type'] == 'RepeatContrast'
113

114
    >>> trial_repeated = [0, 1, 1, 0, 1, 0, 1, 1, 1, 0]
115
    >>> repNum =         [0, 1, 2, 0, 1, 0, 1, 2, 3, 0]
116
    """
117
    save_names = '_ibl_trials.repNum.npy'
1✔
118
    var_names = 'repNum'
1✔
119

120
    def _extract(self):
1✔
121
        trial_repeated = np.array(
1✔
122
            [t['contrast']['type'] == 'RepeatContrast' for t in self.bpod_trials])
123
        trial_repeated = trial_repeated.astype(int)
1✔
124
        repNum = trial_repeated.copy()
1✔
125
        c = 0
1✔
126
        for i in range(len(trial_repeated)):
1✔
127
            if trial_repeated[i] == 0:
1✔
128
                c = 0
1✔
129
                repNum[i] = 0
1✔
130
                continue
1✔
131
            c += 1
1✔
132
            repNum[i] = c
1✔
133
        return repNum
1✔
134

135

136
class RewardVolume(BaseBpodTrialsExtractor):
1✔
137
    """
1✔
138
    Load reward volume delivered for each trial.
139
    **Optional:** saves _ibl_trials.rewardVolume.npy
140

141
    Uses reward_current to accumulate the amount of
142
    """
143
    save_names = '_ibl_trials.rewardVolume.npy'
1✔
144
    var_names = 'rewardVolume'
1✔
145

146
    def _extract(self):
1✔
147
        trial_volume = [x['reward_amount']
1✔
148
                        if x['trial_correct'] else 0 for x in self.bpod_trials]
149
        reward_volume = np.array(trial_volume).astype(np.float64)
1✔
150
        assert len(reward_volume) == len(self.bpod_trials)
1✔
151
        return reward_volume
1✔
152

153

154
class FeedbackTimes(BaseBpodTrialsExtractor):
1✔
155
    """
1✔
156
    Get the times the water or error tone was delivered to the animal.
157
    **Optional:** saves _ibl_trials.feedback_times.npy
158

159
    Gets reward  and error state init times vectors,
160
    checks if theintersection of nans is empty, then
161
    merges the 2 vectors.
162
    """
163
    save_names = '_ibl_trials.feedback_times.npy'
1✔
164
    var_names = 'feedback_times'
1✔
165

166
    @staticmethod
1✔
167
    def get_feedback_times_lt5(session_path, task_collection='raw_behavior_data', data=False):
1✔
168
        if not data:
1✔
169
            data = raw.load_data(session_path, task_collection=task_collection)
×
170
        rw_times = [tr['behavior_data']['States timestamps']['reward'][0][0]
1✔
171
                    for tr in data]
172
        err_times = [tr['behavior_data']['States timestamps']['error'][0][0]
1✔
173
                     for tr in data]
174
        nogo_times = [tr['behavior_data']['States timestamps']['no_go'][0][0]
1✔
175
                      for tr in data]
176
        assert sum(np.isnan(rw_times) &
1✔
177
                   np.isnan(err_times) & np.isnan(nogo_times)) == 0
178
        merge = np.array([np.array(times)[~np.isnan(times)] for times in
1✔
179
                          zip(rw_times, err_times, nogo_times)]).squeeze()
180

181
        return np.array(merge)
1✔
182

183
    @staticmethod
1✔
184
    def get_feedback_times_ge5(session_path, task_collection='raw_behavior_data', data=False):
1✔
185
        # ger err and no go trig times -- look for BNC2High of trial -- verify
186
        # only 2 onset times go tone and noise, select 2nd/-1 OR select the one
187
        # that is grater than the nogo or err trial onset time
188
        if not data:
1✔
189
            data = raw.load_data(session_path, task_collection=task_collection)
×
190
        missed_bnc2 = 0
1✔
191
        rw_times, err_sound_times, merge = [np.zeros([len(data), ]) for _ in range(3)]
1✔
192

193
        for ind, tr in enumerate(data):
1✔
194
            st = tr['behavior_data']['Events timestamps'].get('BNC2High', None)
1✔
195
            if not st:
1✔
196
                st = np.array([np.nan, np.nan])
1✔
197
                missed_bnc2 += 1
1✔
198
            # xonar soundcard duplicates events, remove consecutive events too close together
199
            st = np.delete(st, np.where(np.diff(st) < 0.020)[0] + 1)
1✔
200
            rw_times[ind] = tr['behavior_data']['States timestamps']['reward'][0][0]
1✔
201
            # get the error sound only if the reward is nan
202
            err_sound_times[ind] = st[-1] if st.size >= 2 and np.isnan(rw_times[ind]) else np.nan
1✔
203
        if missed_bnc2 == len(data):
1✔
204
            _logger.warning('No BNC2 for feedback times, filling error trials NaNs')
×
205
        merge *= np.nan
1✔
206
        merge[~np.isnan(rw_times)] = rw_times[~np.isnan(rw_times)]
1✔
207
        merge[~np.isnan(err_sound_times)] = err_sound_times[~np.isnan(err_sound_times)]
1✔
208

209
        return merge
1✔
210

211
    def _extract(self):
1✔
212
        # Version check
213
        if parse_version(self.settings['IBLRIG_VERSION_TAG']) >= parse_version('5.0.0'):
1✔
214
            merge = self.get_feedback_times_ge5(self.session_path, task_collection=self.task_collection, data=self.bpod_trials)
1✔
215
        else:
216
            merge = self.get_feedback_times_lt5(self.session_path, task_collection=self.task_collection, data=self.bpod_trials)
1✔
217
        return np.array(merge)
1✔
218

219

220
class Intervals(BaseBpodTrialsExtractor):
1✔
221
    """
1✔
222
    Trial start to trial end. Trial end includes 1 or 2 seconds after feedback,
223
    (depending on the feedback) and 0.5 seconds of iti.
224
    **Optional:** saves _ibl_trials.intervals.npy
225

226
    Uses the corrected Trial start and Trial end timestamp values form PyBpod.
227
    """
228
    save_names = '_ibl_trials.intervals.npy'
1✔
229
    var_names = 'intervals'
1✔
230

231
    def _extract(self):
1✔
232
        starts = [t['behavior_data']['Trial start timestamp'] for t in self.bpod_trials]
1✔
233
        ends = [t['behavior_data']['Trial end timestamp'] for t in self.bpod_trials]
1✔
234
        return np.array([starts, ends]).T
1✔
235

236

237
class ResponseTimes(BaseBpodTrialsExtractor):
1✔
238
    """
1✔
239
    Time (in absolute seconds from session start) when a response was recorded.
240
    **Optional:** saves _ibl_trials.response_times.npy
241

242
    Uses the timestamp of the end of the closed_loop state.
243
    """
244
    save_names = '_ibl_trials.response_times.npy'
1✔
245
    var_names = 'response_times'
1✔
246

247
    def _extract(self):
1✔
248
        rt = np.array([tr['behavior_data']['States timestamps']['closed_loop'][0][1]
1✔
249
                       for tr in self.bpod_trials])
250
        return rt
1✔
251

252

253
class ItiDuration(BaseBpodTrialsExtractor):
1✔
254
    """
1✔
255
    Calculate duration of iti from state timestamps.
256
    **Optional:** saves _ibl_trials.iti_duration.npy
257

258
    Uses Trial end timestamp and get_response_times to calculate iti.
259
    """
260
    save_names = '_ibl_trials.itiDuration.npy'
1✔
261
    var_names = 'iti_dur'
1✔
262

263
    def _extract(self):
1✔
264
        rt, _ = ResponseTimes(self.session_path).extract(
×
265
            save=False, task_collection=self.task_collection, bpod_trials=self.bpod_trials, settings=self.settings)
266
        ends = np.array([t['behavior_data']['Trial end timestamp'] for t in self.bpod_trials])
×
267
        iti_dur = ends - rt
×
268
        return iti_dur
×
269

270

271
class GoCueTriggerTimes(BaseBpodTrialsExtractor):
1✔
272
    """
1✔
273
    Get trigger times of goCue from state machine.
274

275
    Current software solution for triggering sounds uses PyBpod soft codes.
276
    Delays can be in the order of 10's of ms. This is the time when the command
277
    to play the sound was executed. To measure accurate time, either getting the
278
    sound onset from xonar soundcard sync pulse (latencies may vary).
279
    """
280
    save_names = '_ibl_trials.goCueTrigger_times.npy'
1✔
281
    var_names = 'goCueTrigger_times'
1✔
282

283
    def _extract(self):
1✔
284
        if parse_version(self.settings['IBLRIG_VERSION_TAG']) >= parse_version('5.0.0'):
1✔
285
            goCue = np.array([tr['behavior_data']['States timestamps']
1✔
286
                              ['play_tone'][0][0] for tr in self.bpod_trials])
287
        else:
288
            goCue = np.array([tr['behavior_data']['States timestamps']
1✔
289
                             ['closed_loop'][0][0] for tr in self.bpod_trials])
290
        return goCue
1✔
291

292

293
class TrialType(BaseBpodTrialsExtractor):
1✔
294
    save_names = '_ibl_trials.type.npy'
1✔
295
    var_name = 'trial_type'
1✔
296

297
    def _extract(self):
1✔
298
        trial_type = []
×
299
        for tr in self.bpod_trials:
×
300
            if ~np.isnan(tr["behavior_data"]["States timestamps"]["reward"][0][0]):
×
301
                trial_type.append(1)
×
302
            elif ~np.isnan(tr["behavior_data"]["States timestamps"]["error"][0][0]):
×
303
                trial_type.append(-1)
×
304
            elif ~np.isnan(tr["behavior_data"]["States timestamps"]["no_go"][0][0]):
×
305
                trial_type.append(0)
×
306
            else:
307
                _logger.warning("Trial is not in set {-1, 0, 1}, appending NaN to trialType")
×
308
                trial_type.append(np.nan)
×
309
        return np.array(trial_type)
×
310

311

312
class GoCueTimes(BaseBpodTrialsExtractor):
1✔
313
    """
1✔
314
    Get trigger times of goCue from state machine.
315

316
    Current software solution for triggering sounds uses PyBpod soft codes.
317
    Delays can be in the order of 10-100s of ms. This is the time when the command
318
    to play the sound was executed. To measure accurate time, either getting the
319
    sound onset from the future microphone OR the new xonar soundcard and
320
    setup developed by Sanworks guarantees a set latency (in testing).
321
    """
322
    save_names = '_ibl_trials.goCue_times.npy'
1✔
323
    var_names = 'goCue_times'
1✔
324

325
    def _extract(self):
1✔
326
        go_cue_times = np.zeros([len(self.bpod_trials), ])
1✔
327
        for ind, tr in enumerate(self.bpod_trials):
1✔
328
            if raw.get_port_events(tr, 'BNC2'):
1✔
329
                bnchigh = tr['behavior_data']['Events timestamps'].get('BNC2High', None)
1✔
330
                if bnchigh:
1✔
331
                    go_cue_times[ind] = bnchigh[0]
1✔
332
                    continue
1✔
333
                bnclow = tr['behavior_data']['Events timestamps'].get('BNC2Low', None)
1✔
334
                if bnclow:
1✔
335
                    go_cue_times[ind] = bnclow[0] - 0.1
1✔
336
                    continue
1✔
337
                go_cue_times[ind] = np.nan
×
338
            else:
339
                go_cue_times[ind] = np.nan
1✔
340

341
        nmissing = np.sum(np.isnan(go_cue_times))
1✔
342
        # Check if all stim_syncs have failed to be detected
343
        if np.all(np.isnan(go_cue_times)):
1✔
344
            _logger.warning(
1✔
345
                f'{self.session_path}: Missing ALL !! BNC2 TTLs ({nmissing} trials)')
346
        # Check if any stim_sync has failed be detected for every trial
347
        elif np.any(np.isnan(go_cue_times)):
1✔
348
            _logger.warning(f'{self.session_path}: Missing BNC2 TTLs on {nmissing} trials')
×
349

350
        return go_cue_times
1✔
351

352

353
class IncludedTrials(BaseBpodTrialsExtractor):
1✔
354
    save_names = '_ibl_trials.included.npy'
1✔
355
    var_names = 'included'
1✔
356

357
    def _extract(self):
1✔
358
        if parse_version(self.settings['IBLRIG_VERSION_TAG']) >= parse_version('5.0.0'):
1✔
359
            trials_included = self.get_included_trials_ge5(
1✔
360
                data=self.bpod_trials, settings=self.settings)
361
        else:
362
            trials_included = self.get_included_trials_lt5(data=self.bpod_trials)
1✔
363
        return trials_included
1✔
364

365
    @staticmethod
1✔
366
    def get_included_trials_lt5(data=False):
1✔
367
        trials_included = np.array([True for t in data])
1✔
368
        return trials_included
1✔
369

370
    @staticmethod
1✔
371
    def get_included_trials_ge5(data=False, settings=False):
1✔
372
        trials_included = np.array([True for t in data])
1✔
373
        if ('SUBJECT_DISENGAGED_TRIGGERED' in settings.keys() and settings[
1✔
374
                'SUBJECT_DISENGAGED_TRIGGERED'] is not False):
375
            idx = settings['SUBJECT_DISENGAGED_TRIALNUM'] - 1
1✔
376
            trials_included[idx:] = False
1✔
377
        return trials_included
1✔
378

379

380
class ItiInTimes(BaseBpodTrialsExtractor):
1✔
381
    var_names = 'itiIn_times'
1✔
382

383
    def _extract(self):
1✔
384
        if parse_version(self.settings["IBLRIG_VERSION_TAG"]) < parse_version("5.0.0"):
1✔
385
            iti_in = np.ones(len(self.bpod_trials)) * np.nan
×
386
        else:
387
            iti_in = np.array(
1✔
388
                [tr["behavior_data"]["States timestamps"]
389
                 ["exit_state"][0][0] for tr in self.bpod_trials]
390
            )
391
        return iti_in
1✔
392

393

394
class ErrorCueTriggerTimes(BaseBpodTrialsExtractor):
1✔
395
    var_names = 'errorCueTrigger_times'
1✔
396

397
    def _extract(self):
1✔
398
        errorCueTrigger_times = np.zeros(len(self.bpod_trials)) * np.nan
1✔
399
        for i, tr in enumerate(self.bpod_trials):
1✔
400
            nogo = tr["behavior_data"]["States timestamps"]["no_go"][0][0]
1✔
401
            error = tr["behavior_data"]["States timestamps"]["error"][0][0]
1✔
402
            if np.all(~np.isnan(nogo)):
1✔
403
                errorCueTrigger_times[i] = nogo
1✔
404
            elif np.all(~np.isnan(error)):
1✔
405
                errorCueTrigger_times[i] = error
1✔
406
        return errorCueTrigger_times
1✔
407

408

409
class StimFreezeTriggerTimes(BaseBpodTrialsExtractor):
1✔
410
    var_names = 'stimFreezeTrigger_times'
1✔
411

412
    def _extract(self):
1✔
413
        if parse_version(self.settings["IBLRIG_VERSION_TAG"]) < parse_version("6.2.5"):
1✔
414
            return np.ones(len(self.bpod_trials)) * np.nan
1✔
415
        freeze_reward = np.array(
1✔
416
            [
417
                True
418
                if np.all(~np.isnan(tr["behavior_data"]["States timestamps"]["freeze_reward"][0]))
419
                else False
420
                for tr in self.bpod_trials
421
            ]
422
        )
423
        freeze_error = np.array(
1✔
424
            [
425
                True
426
                if np.all(~np.isnan(tr["behavior_data"]["States timestamps"]["freeze_error"][0]))
427
                else False
428
                for tr in self.bpod_trials
429
            ]
430
        )
431
        no_go = np.array(
1✔
432
            [
433
                True
434
                if np.all(~np.isnan(tr["behavior_data"]["States timestamps"]["no_go"][0]))
435
                else False
436
                for tr in self.bpod_trials
437
            ]
438
        )
439
        assert (np.sum(freeze_error) + np.sum(freeze_reward) +
1✔
440
                np.sum(no_go) == len(self.bpod_trials))
441
        stimFreezeTrigger = np.array([])
1✔
442
        for r, e, n, tr in zip(freeze_reward, freeze_error, no_go, self.bpod_trials):
1✔
443
            if n:
1✔
444
                stimFreezeTrigger = np.append(stimFreezeTrigger, np.nan)
1✔
445
                continue
1✔
446
            state = "freeze_reward" if r else "freeze_error"
1✔
447
            stimFreezeTrigger = np.append(
1✔
448
                stimFreezeTrigger, tr["behavior_data"]["States timestamps"][state][0][0]
449
            )
450
        return stimFreezeTrigger
1✔
451

452

453
class StimOffTriggerTimes(BaseBpodTrialsExtractor):
1✔
454
    var_names = 'stimOffTrigger_times'
1✔
455

456
    def _extract(self):
1✔
457
        if parse_version(self.settings["IBLRIG_VERSION_TAG"]) >= parse_version("6.2.5"):
1✔
458
            stim_off_trigger_state = "hide_stim"
1✔
459
        elif parse_version(self.settings["IBLRIG_VERSION_TAG"]) >= parse_version("5.0.0"):
1✔
460
            stim_off_trigger_state = "exit_state"
1✔
461
        else:
462
            stim_off_trigger_state = "trial_start"
×
463

464
        stimOffTrigger_times = np.array(
1✔
465
            [tr["behavior_data"]["States timestamps"][stim_off_trigger_state][0][0]
466
             for tr in self.bpod_trials]
467
        )
468
        # If pre version 5.0.0 no specific nogo Off trigger was given, just return trial_starts
469
        if stim_off_trigger_state == "trial_start":
1✔
470
            return stimOffTrigger_times
×
471

472
        no_goTrigger_times = np.array(
1✔
473
            [tr["behavior_data"]["States timestamps"]["no_go"][0][0] for tr in self.bpod_trials]
474
        )
475
        # Stim off trigs are either in their own state or in the no_go state if the
476
        # mouse did not move, if the stim_off_trigger_state always exist
477
        # (exit_state or trial_start)
478
        # no NaNs will happen, NaNs might happen in at last trial if
479
        # session was stopped after response
480
        # if stim_off_trigger_state == "hide_stim":
481
        #     assert all(~np.isnan(no_goTrigger_times) == np.isnan(stimOffTrigger_times))
482
        # Patch with the no_go states trig times
483
        stimOffTrigger_times[~np.isnan(no_goTrigger_times)] = no_goTrigger_times[
1✔
484
            ~np.isnan(no_goTrigger_times)
485
        ]
486
        return stimOffTrigger_times
1✔
487

488

489
class StimOnTriggerTimes(BaseBpodTrialsExtractor):
1✔
490
    save_names = '_ibl_trials.stimOnTrigger_times.npy'
1✔
491
    var_names = 'stimOnTrigger_times'
1✔
492

493
    def _extract(self):
1✔
494
        # Get the stim_on_state that triggers the onset of the stim
495
        stim_on_state = np.array([tr['behavior_data']['States timestamps']
1✔
496
                                 ['stim_on'][0] for tr in self.bpod_trials])
497
        return stim_on_state[:, 0].T
1✔
498

499

500
class StimOnTimes_deprecated(BaseBpodTrialsExtractor):
1✔
501
    save_names = '_ibl_trials.stimOn_times.npy'
1✔
502
    var_names = 'stimOn_times'
1✔
503

504
    def _extract(self):
1✔
505
        """
506
        Find the time of the state machine command to turn on the stim
507
        (state stim_on start or rotary_encoder_event2)
508
        Find the next frame change from the photodiode after that TS.
509
        Screen is not displaying anything until then.
510
        (Frame changes are in BNC1 High and BNC1 Low)
511
        """
512
        # Version check
513
        _logger.warning("Deprecation Warning: this is an old version of stimOn extraction."
1✔
514
                        "From version 5., use StimOnOffFreezeTimes")
515
        if parse_version(self.settings['IBLRIG_VERSION_TAG']) >= parse_version('5.0.0'):
1✔
516
            stimOn_times = self.get_stimOn_times_ge5(self.session_path, data=self.bpod_trials,
1✔
517
                                                     task_collection=self.task_collection)
518
        else:
519
            stimOn_times = self.get_stimOn_times_lt5(self.session_path, data=self.bpod_trials,
1✔
520
                                                     task_collection=self.task_collection)
521
        return np.array(stimOn_times)
1✔
522

523
    @staticmethod
1✔
524
    def get_stimOn_times_ge5(session_path, data=False, task_collection='raw_behavior_data'):
1✔
525
        """
526
        Find first and last stim_sync pulse of the trial.
527
        stimOn_times should be the first after the stim_on state.
528
        (Stim updates are in BNC1High and BNC1Low - frame2TTL device)
529
        Check that all trials have frame changes.
530
        Find length of stim_on_state [start, stop].
531
        If either check fails the HW device failed to detect the stim_sync square change
532
        Substitute that trial's missing or incorrect value with a NaN.
533
        return stimOn_times
534
        """
535
        if not data:
1✔
536
            data = raw.load_data(session_path, task_collection=task_collection)
×
537
        # Get all stim_sync events detected
538
        stim_sync_all = [raw.get_port_events(tr, 'BNC1') for tr in data]
1✔
539
        stim_sync_all = [np.array(x) for x in stim_sync_all]
1✔
540
        # Get the stim_on_state that triggers the onset of the stim
541
        stim_on_state = np.array([tr['behavior_data']['States timestamps']
1✔
542
                                 ['stim_on'][0] for tr in data])
543

544
        stimOn_times = np.array([])
1✔
545
        for sync, on, off in zip(
1✔
546
                stim_sync_all, stim_on_state[:, 0], stim_on_state[:, 1]):
547
            pulse = sync[np.where(np.bitwise_and((sync > on), (sync <= off)))]
1✔
548
            if pulse.size == 0:
1✔
549
                stimOn_times = np.append(stimOn_times, np.nan)
1✔
550
            else:
551
                stimOn_times = np.append(stimOn_times, pulse)
1✔
552

553
        nmissing = np.sum(np.isnan(stimOn_times))
1✔
554
        # Check if all stim_syncs have failed to be detected
555
        if np.all(np.isnan(stimOn_times)):
1✔
556
            _logger.error(f'{session_path}: Missing ALL BNC1 TTLs ({nmissing} trials)')
×
557

558
        # Check if any stim_sync has failed be detected for every trial
559
        if np.any(np.isnan(stimOn_times)):
1✔
560
            _logger.warning(f'{session_path}: Missing BNC1 TTLs on {nmissing} trials')
1✔
561

562
        return stimOn_times
1✔
563

564
    @staticmethod
1✔
565
    def get_stimOn_times_lt5(session_path, data=False, task_collection='raw_behavior_data'):
1✔
566
        """
567
        Find the time of the statemachine command to turn on hte stim
568
        (state stim_on start or rotary_encoder_event2)
569
        Find the next frame change from the photodiodeafter that TS.
570
        Screen is not displaying anything until then.
571
        (Frame changes are in BNC1High and BNC1Low)
572
        """
573
        if not data:
1✔
574
            data = raw.load_data(session_path, task_collection=task_collection)
×
575
        stim_on = []
1✔
576
        bnc_h = []
1✔
577
        bnc_l = []
1✔
578
        for tr in data:
1✔
579
            stim_on.append(tr['behavior_data']['States timestamps']['stim_on'][0][0])
1✔
580
            if 'BNC1High' in tr['behavior_data']['Events timestamps'].keys():
1✔
581
                bnc_h.append(np.array(tr['behavior_data']
1✔
582
                                      ['Events timestamps']['BNC1High']))
583
            else:
584
                bnc_h.append(np.array([np.NINF]))
1✔
585
            if 'BNC1Low' in tr['behavior_data']['Events timestamps'].keys():
1✔
586
                bnc_l.append(np.array(tr['behavior_data']
1✔
587
                                      ['Events timestamps']['BNC1Low']))
588
            else:
589
                bnc_l.append(np.array([np.NINF]))
1✔
590

591
        stim_on = np.array(stim_on)
1✔
592
        bnc_h = np.array(bnc_h, dtype=object)
1✔
593
        bnc_l = np.array(bnc_l, dtype=object)
1✔
594

595
        count_missing = 0
1✔
596
        stimOn_times = np.zeros_like(stim_on)
1✔
597
        for i in range(len(stim_on)):
1✔
598
            hl = np.sort(np.concatenate([bnc_h[i], bnc_l[i]]))
1✔
599
            stot = hl[hl > stim_on[i]]
1✔
600
            if np.size(stot) == 0:
1✔
601
                stot = np.array([np.nan])
1✔
602
                count_missing += 1
1✔
603
            stimOn_times[i] = stot[0]
1✔
604

605
        if np.all(np.isnan(stimOn_times)):
1✔
606
            _logger.error(f'{session_path}: Missing ALL BNC1 TTLs ({count_missing} trials)')
1✔
607

608
        if count_missing > 0:
1✔
609
            _logger.warning(f'{session_path}: Missing BNC1 TTLs on {count_missing} trials')
1✔
610

611
        return np.array(stimOn_times)
1✔
612

613

614
class StimOnOffFreezeTimes(BaseBpodTrialsExtractor):
1✔
615
    """
1✔
616
    Extracts stim on / off and freeze times from Bpod BNC1 detected fronts
617
    """
618
    save_names = ('_ibl_trials.stimOn_times.npy', None, None)
1✔
619
    var_names = ('stimOn_times', 'stimOff_times', 'stimFreeze_times')
1✔
620

621
    def _extract(self):
1✔
622
        choice = Choice(self.session_path).extract(
1✔
623
            bpod_trials=self.bpod_trials, task_collection=self.task_collection, settings=self.settings, save=False
624
        )[0]
625
        f2TTL = [raw.get_port_events(tr, name='BNC1') for tr in self.bpod_trials]
1✔
626

627
        stimOn_times = np.array([])
1✔
628
        stimOff_times = np.array([])
1✔
629
        stimFreeze_times = np.array([])
1✔
630
        for tr in f2TTL:
1✔
631
            if tr and len(tr) == 2:
1✔
632
                stimOn_times = np.append(stimOn_times, tr[0])
1✔
633
                stimOff_times = np.append(stimOff_times, tr[-1])
1✔
634
                stimFreeze_times = np.append(stimFreeze_times, np.nan)
1✔
635
            elif tr and len(tr) >= 3:
1✔
636
                stimOn_times = np.append(stimOn_times, tr[0])
1✔
637
                stimOff_times = np.append(stimOff_times, tr[-1])
1✔
638
                stimFreeze_times = np.append(stimFreeze_times, tr[-2])
1✔
639
            else:
640
                stimOn_times = np.append(stimOn_times, np.nan)
1✔
641
                stimOff_times = np.append(stimOff_times, np.nan)
1✔
642
                stimFreeze_times = np.append(stimFreeze_times, np.nan)
1✔
643

644
        # In no_go trials no stimFreeze happens just stim Off
645
        stimFreeze_times[choice == 0] = np.nan
1✔
646
        # Check for trigger times
647
        # 2nd order criteria:
648
        # stimOn -> Closest one to stimOnTrigger?
649
        # stimOff -> Closest one to stimOffTrigger?
650
        # stimFreeze -> Closest one to stimFreezeTrigger?
651

652
        return stimOn_times, stimOff_times, stimFreeze_times
1✔
653

654

655
class PhasePosQuiescence(BaseBpodTrialsExtractor):
1✔
656
    """Extracts stimulus phase, position and quiescence from Bpod data.
1✔
657
    For extraction of pre-generated events, use the ProbaContrasts extractor instead.
658
    """
659
    save_names = (None, None, None)
1✔
660
    var_names = ('phase', 'position', 'quiescence')
1✔
661

662
    def _extract(self, **kwargs):
1✔
663
        phase = np.array([t['stim_phase'] for t in self.bpod_trials])
1✔
664
        position = np.array([t['position'] for t in self.bpod_trials])
1✔
665
        quiescence = np.array([t['quiescent_period'] for t in self.bpod_trials])
1✔
666
        return phase, position, quiescence
1✔
667

668

669
class TrialsTable(BaseBpodTrialsExtractor):
1✔
670
    """
1✔
671
    Extracts the following into a table from Bpod raw data:
672
        intervals, goCue_times, response_times, choice, stimOn_times, contrastLeft, contrastRight,
673
        feedback_times, feedbackType, rewardVolume, probabilityLeft, firstMovement_times
674
    Additionally extracts the following wheel data:
675
        wheel_timestamps, wheel_position, wheel_moves_intervals, wheel_moves_peak_amplitude
676
    """
677
    save_names = ('_ibl_trials.table.pqt', None, None, '_ibl_wheel.timestamps.npy', '_ibl_wheel.position.npy',
1✔
678
                  '_ibl_wheelMoves.intervals.npy', '_ibl_wheelMoves.peakAmplitude.npy', None, None)
679
    var_names = ('table', 'stimOff_times', 'stimFreeze_times', 'wheel_timestamps', 'wheel_position', 'wheel_moves_intervals',
1✔
680
                 'wheel_moves_peak_amplitude', 'peakVelocity_times', 'is_final_movement')
681

682
    def _extract(self, extractor_classes=None, **kwargs):
1✔
683
        base = [Intervals, GoCueTimes, ResponseTimes, Choice, StimOnOffFreezeTimes, ContrastLR, FeedbackTimes, FeedbackType,
1✔
684
                RewardVolume, ProbabilityLeft, Wheel]
685
        out, _ = run_extractor_classes(
1✔
686
            base, session_path=self.session_path, bpod_trials=self.bpod_trials, settings=self.settings, save=False,
687
            task_collection=self.task_collection)
688
        table = AlfBunch({k: v for k, v in out.items() if k not in self.var_names})
1✔
689
        assert len(table.keys()) == 12
1✔
690

691
        return table.to_df(), *(out.pop(x) for x in self.var_names if x != 'table')
1✔
692

693

694
def extract_all(session_path, save=False, bpod_trials=None, settings=None, task_collection='raw_behavior_data', save_path=None):
1✔
695
    """Extract trials and wheel data.
696

697
    For task versions >= 5.0.0, outputs wheel data and trials.table dataset (+ some extra datasets)
698

699
    Parameters
700
    ----------
701
    session_path : str, pathlib.Path
702
        The path to the session
703
    save : bool
704
        If true save the data files to ALF
705
    bpod_trials : list of dicts
706
        The Bpod trial dicts loaded from the _iblrig_taskData.raw dataset
707
    settings : dict
708
        The Bpod settings loaded from the _iblrig_taskSettings.raw dataset
709

710
    Returns
711
    -------
712
    A list of extracted data and a list of file paths if save is True (otherwise None)
713
    """
714
    if not bpod_trials:
1✔
715
        bpod_trials = raw.load_data(session_path, task_collection=task_collection)
1✔
716
    if not settings:
1✔
717
        settings = raw.load_settings(session_path, task_collection=task_collection)
1✔
718
    if settings is None or settings['IBLRIG_VERSION_TAG'] == '':
1✔
719
        settings = {'IBLRIG_VERSION_TAG': '100.0.0'}
×
720

721
    base = [RepNum, GoCueTriggerTimes]
1✔
722
    # Version check
723
    if parse_version(settings['IBLRIG_VERSION_TAG']) >= parse_version('5.0.0'):
1✔
724
        # We now extract a single trials table
725
        base.extend([
1✔
726
            StimOnTriggerTimes, ItiInTimes, StimOffTriggerTimes, StimFreezeTriggerTimes,
727
            ErrorCueTriggerTimes, TrialsTable, PhasePosQuiescence
728
        ])
729
    else:
730
        base.extend([
1✔
731
            Intervals, Wheel, FeedbackType, ContrastLR, ProbabilityLeft, Choice, IncludedTrials,
732
            StimOnTimes_deprecated, RewardVolume, FeedbackTimes, ResponseTimes, GoCueTimes, PhasePosQuiescence
733
        ])
734

735
    out, fil = run_extractor_classes(base, save=save, session_path=session_path, bpod_trials=bpod_trials, settings=settings,
1✔
736
                                     task_collection=task_collection, path_out=save_path)
737
    return out, fil
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc