def align_with_bpod(session_path): """ Reads in trials.intervals ALF dataset from bpod and fpga. Asserts consistency between datasets and compute the median time difference :param session_path: :return: dt: median time difference of trial start times (fpga - bpod) """ ITI_DURATION = 0.5 # check consistency output_path = Path(session_path) / 'alf' trials = alf.io.load_object(output_path, '_ibl_trials') if alf.io.check_dimensions(trials) != 0: # patching things up if the bpod and FPGA don't have the same recording span _logger.warning( "BPOD/FPGA synchronization: Bpod and FPGA don't have the same amount of" " trial start events. Patching alf files.") _, _, ibpod, ifpga = raw.sync_trials_robust( trials['intervals_bpod'][:, 0], trials['intervals'][:, 0], return_index=True) if ibpod.size == 0: raise err.SyncBpodFpgaException( 'Can not sync BPOD and FPGA - no matching sync pulses ' 'found.') for k in trials: if 'bpod' in k: trials[k] = trials[k][ibpod] else: trials[k] = trials[k][ibpod] alf.io.save_object_npy(output_path, trials, '_ibl_trials') assert (alf.io.check_dimensions(trials) == 0) tlen = (np.diff(trials['intervals_bpod']) - np.diff(trials['intervals']))[:-1] - ITI_DURATION assert (np.all(np.abs(tlen[np.invert(np.isnan(tlen))])[:-1] < 5 * 1e-3)) # dt is the delta to apply to bpod times in order to be on the ephys clock dt = trials['intervals'][:, 0] - trials['intervals_bpod'][:, 0] # compute the clock drift bpod versus dt ppm = np.polyfit(trials['intervals'][:, 0], dt, 1)[0] * 1e6 if ppm > BPOD_FPGA_DRIFT_THRESHOLD_PPM: _logger.warning( 'BPOD/FPGA synchronization shows values greater than 150 ppm') # plt.plot(trials['intervals'][:, 0], dt, '*') # so far 2 datasets concerned: goCueTrigger_times_bpod and response_times_bpod for k in trials: if not k.endswith('_times_bpod'): continue np.save(output_path.joinpath(f'_ibl_trials.{k[:-5]}.npy'), trials[k] + dt) return interpolate.interp1d(trials['intervals_bpod'][:, 0], trials['intervals'][:, 0], fill_value="extrapolate")
def bpod_fpga_sync(bpod_intervals=None, ephys_intervals=None, iti_duration=None): """ Computes synchronization function from bpod to fpga :param bpod_intervals :param ephys_intervals :return: interpolation function """ if iti_duration is None: iti_duration = 0.5 # check consistency if bpod_intervals.size != ephys_intervals.size: # patching things up if the bpod and FPGA don't have the same recording span _logger.warning( "BPOD/FPGA synchronization: Bpod and FPGA don't have the same amount of" " trial start events. Patching alf files.") _, _, ibpod, ifpga = raw_data_loaders.sync_trials_robust( bpod_intervals[:, 0], ephys_intervals[:, 0], return_index=True) if ibpod.size == 0: raise err.SyncBpodFpgaException( 'Can not sync BPOD and FPGA - no matching sync pulses ' 'found.') bpod_intervals = bpod_intervals[ibpod, :] ephys_intervals = ephys_intervals[ifpga, :] else: ibpod, ifpga = [ np.arange(bpod_intervals.shape[0]) for _ in np.arange(2) ] tlen = (np.diff(bpod_intervals) - np.diff(ephys_intervals))[:-1] - iti_duration assert np.all(np.abs(tlen[np.invert(np.isnan(tlen))])[:-1] < 5 * 1e-3) # dt is the delta to apply to bpod times in order to be on the ephys clock dt = bpod_intervals[:, 0] - ephys_intervals[:, 0] # compute the clock drift bpod versus dt ppm = np.polyfit(bpod_intervals[:, 0], dt, 1)[0] * 1e6 if ppm > BPOD_FPGA_DRIFT_THRESHOLD_PPM: _logger.warning( 'BPOD/FPGA synchronization shows values greater than %i ppm', BPOD_FPGA_DRIFT_THRESHOLD_PPM) # plt.plot(trials['intervals'][:, 0], dt, '*') # so far 2 datasets concerned: goCueTrigger_times_bpod and response_times_bpod fcn_bpod2fpga = interpolate.interp1d(bpod_intervals[:, 0], ephys_intervals[:, 0], fill_value="extrapolate") return ibpod, ifpga, fcn_bpod2fpga
def extract_behaviour_sync(sync, chmap=None, display=False, bpod_trials=None, tmax=np.inf): """ Extract wheel positions and times from sync fronts dictionary :param sync: dictionary 'times', 'polarities' of fronts detected on sync trace for all 16 chans :param chmap: dictionary containing channel index. Default to constant. chmap = {'bpod': 7, 'frame2ttl': 12, 'audio': 15} :param display: bool or matplotlib axes: show the full session sync pulses display defaults to False :return: trials dictionary """ bpod = _get_sync_fronts(sync, chmap['bpod'], tmax=tmax) if bpod.times.size == 0: raise err.SyncBpodFpgaException( 'No Bpod event found in FPGA. No behaviour extraction. ' 'Check channel maps.') frame2ttl = _get_sync_fronts(sync, chmap['frame2ttl'], tmax=tmax) frame2ttl = _clean_frame2ttl(frame2ttl) audio = _get_sync_fronts(sync, chmap['audio'], tmax=tmax) # extract events from the fronts for each trace t_trial_start, t_valve_open, t_iti_in = _assign_events_bpod( bpod['times'], bpod['polarities']) # one issue is that sometimes bpod pulses may not have been detected, in this case # perform the sync bpod/FPGA, and add the start that have not been detected if bpod_trials: bpod_start = bpod_trials['intervals_bpod'][:, 0] fcn, drift, ibpod, ifpga = dsp.utils.sync_timestamps( bpod_start, t_trial_start, return_indices=True) # if it's drifting too much if drift > 200 and bpod_start.size != t_trial_start.size: raise err.SyncBpodFpgaException("sync cluster f*ck") missing_bpod = fcn(bpod_start[np.setxor1d(ibpod, np.arange(len(bpod_start)))]) t_trial_start = np.sort(np.r_[t_trial_start, missing_bpod]) else: _logger.warning( "Deprecation Warning: calling FPGA trials extraction without a bpod trials" " dictionary will result in an error.") t_ready_tone_in, t_error_tone_in = _assign_events_audio( audio['times'], audio['polarities']) trials = Bunch({ 'goCue_times': _assign_events_to_trial(t_trial_start, t_ready_tone_in, take='first'), 'errorCue_times': _assign_events_to_trial(t_trial_start, t_error_tone_in), 'valveOpen_times': _assign_events_to_trial(t_trial_start, t_valve_open), 'stimFreeze_times': _assign_events_to_trial(t_trial_start, frame2ttl['times'], take=-2), 'stimOn_times': _assign_events_to_trial(t_trial_start, frame2ttl['times'], take='first'), 'stimOff_times': _assign_events_to_trial(t_trial_start, frame2ttl['times']), 'itiIn_times': _assign_events_to_trial(t_trial_start, t_iti_in) }) # feedback times are valve open on good trials and error tone in on error trials trials['feedback_times'] = np.copy(trials['valveOpen_times']) ind_err = np.isnan(trials['valveOpen_times']) trials['feedback_times'][ind_err] = trials['errorCue_times'][ind_err] trials['intervals'] = np.c_[t_trial_start, trials['itiIn_times']] if display: width = 0.5 ymax = 5 if isinstance(display, bool): plt.figure("Ephys FPGA Sync") ax = plt.gca() else: ax = display r0 = _get_sync_fronts(sync, chmap['rotary_encoder_0']) plots.squares(bpod['times'], bpod['polarities'] * 0.4 + 1, ax=ax, color='k') plots.squares(frame2ttl['times'], frame2ttl['polarities'] * 0.4 + 2, ax=ax, color='k') plots.squares(audio['times'], audio['polarities'] * 0.4 + 3, ax=ax, color='k') plots.squares(r0['times'], r0['polarities'] * 0.4 + 4, ax=ax, color='k') plots.vertical_lines(t_ready_tone_in, ymin=0, ymax=ymax, ax=ax, label='goCue_times', color='b', linewidth=width) plots.vertical_lines(t_trial_start, ymin=0, ymax=ymax, ax=ax, label='start_trial', color='m', linewidth=width) plots.vertical_lines(t_error_tone_in, ymin=0, ymax=ymax, ax=ax, label='error tone', color='r', linewidth=width) plots.vertical_lines(t_valve_open, ymin=0, ymax=ymax, ax=ax, label='valveOpen_times', color='g', linewidth=width) plots.vertical_lines(trials['stimFreeze_times'], ymin=0, ymax=ymax, ax=ax, label='stimFreeze_times', color='y', linewidth=width) plots.vertical_lines(trials['stimOff_times'], ymin=0, ymax=ymax, ax=ax, label='stim off', color='c', linewidth=width) plots.vertical_lines(trials['stimOn_times'], ymin=0, ymax=ymax, ax=ax, label='stimOn_times', color='tab:orange', linewidth=width) c = _get_sync_fronts(sync, chmap['left_camera']) plots.squares(c['times'], c['polarities'] * 0.4 + 5, ax=ax, color='k') c = _get_sync_fronts(sync, chmap['right_camera']) plots.squares(c['times'], c['polarities'] * 0.4 + 6, ax=ax, color='k') c = _get_sync_fronts(sync, chmap['body_camera']) plots.squares(c['times'], c['polarities'] * 0.4 + 7, ax=ax, color='k') ax.legend() ax.set_yticklabels(['', 'bpod', 'f2ttl', 'audio', 're_0', '']) ax.set_yticks([0, 1, 2, 3, 4, 5]) ax.set_ylim([0, 5]) return trials
def extract_behaviour_sync(sync, output_path=None, save=False, chmap=None, display=False, tmax=np.inf): """ Extract wheel positions and times from sync fronts dictionary :param sync: dictionary 'times', 'polarities' of fronts detected on sync trace for all 16 chans :param output_path: where to save the data :param save: True/False :param chmap: dictionary containing channel index. Default to constant. chmap = {'bpod': 7, 'frame2ttl': 12, 'audio': 15} :param display: bool or matplotlib axes: show the full session sync pulses display defaults to False :return: trials dictionary """ bpod = _get_sync_fronts(sync, chmap['bpod'], tmax=tmax) if bpod.times.size == 0: raise err.SyncBpodFpgaException( 'No Bpod event found in FPGA. No behaviour extraction. ' 'Check channel maps.') frame2ttl = _get_sync_fronts(sync, chmap['frame2ttl'], tmax=tmax) audio = _get_sync_fronts(sync, chmap['audio'], tmax=tmax) # extract events from the fronts for each trace t_trial_start, t_valve_open, t_iti_in = _bpod_events_extraction( bpod['times'], bpod['polarities']) t_ready_tone_in, t_error_tone_in = _audio_events_extraction( audio['times'], audio['polarities']) # stim off time is the first frame2ttl rise/fall after the trial start # does not apply for 1st trial ind = np.searchsorted(frame2ttl['times'], t_iti_in, side='left') t_stim_off = frame2ttl['times'][np.minimum(ind, frame2ttl.times.size - 1)] t_stim_freeze = frame2ttl['times'][np.maximum(ind - 1, 0)] # stimOn_times: first fram2ttl change after trial start trials = Bunch({ 'ready_tone_in': _assign_events_to_trial(t_trial_start, t_ready_tone_in, take='first'), 'error_tone_in': _assign_events_to_trial(t_trial_start, t_error_tone_in), 'valve_open': _assign_events_to_trial(t_trial_start, t_valve_open), 'stim_freeze': _assign_events_to_trial(t_trial_start, t_stim_freeze), 'stimOn_times': _assign_events_to_trial(t_trial_start, frame2ttl['times'], take='first'), 'stimOff_times': _assign_events_to_trial(t_trial_start, t_stim_off), 'iti_in': _assign_events_to_trial(t_trial_start, t_iti_in) }) # goCue_times corresponds to the tone_in event trials['goCue_times'] = np.copy(trials['ready_tone_in']) # feedback times are valve open on good trials and error tone in on error trials trials['feedback_times'] = np.copy(trials['valve_open']) ind_err = np.isnan(trials['valve_open']) trials['feedback_times'][ind_err] = trials['error_tone_in'][ind_err] trials['intervals'] = np.c_[t_trial_start, trials['iti_in']] if display: width = 0.5 ymax = 5 if isinstance(display, bool): plt.figure("Ephys FPGA Sync") ax = plt.gca() else: ax = display r0 = _get_sync_fronts(sync, chmap['rotary_encoder_0']) plots.squares(bpod['times'], bpod['polarities'] * 0.4 + 1, ax=ax, color='k') plots.squares(frame2ttl['times'], frame2ttl['polarities'] * 0.4 + 2, ax=ax, color='k') plots.squares(audio['times'], audio['polarities'] * 0.4 + 3, ax=ax, color='k') plots.squares(r0['times'], r0['polarities'] * 0.4 + 4, ax=ax, color='k') plots.vertical_lines(t_ready_tone_in, ymin=0, ymax=ymax, ax=ax, label='ready tone in', color='b', linewidth=width) plots.vertical_lines(t_trial_start, ymin=0, ymax=ymax, ax=ax, label='start_trial', color='m', linewidth=width) plots.vertical_lines(t_error_tone_in, ymin=0, ymax=ymax, ax=ax, label='error tone', color='r', linewidth=width) plots.vertical_lines(t_valve_open, ymin=0, ymax=ymax, ax=ax, label='valve open', color='g', linewidth=width) plots.vertical_lines(t_stim_freeze, ymin=0, ymax=ymax, ax=ax, label='stim freeze', color='y', linewidth=width) plots.vertical_lines(t_stim_off, ymin=0, ymax=ymax, ax=ax, label='stim off', color='c', linewidth=width) plots.vertical_lines(trials['stimOn_times'], ymin=0, ymax=ymax, ax=ax, label='stim on', color='tab:orange', linewidth=width) ax.legend() ax.set_yticklabels(['', 'bpod', 'f2ttl', 'audio', 're_0', '']) ax.set_ylim([0, 5]) if save and output_path: output_path = Path(output_path) np.save(output_path / '_ibl_trials.goCue_times.npy', trials['goCue_times']) np.save(output_path / '_ibl_trials.stimOn_times.npy', trials['stimOn_times']) np.save(output_path / '_ibl_trials.intervals.npy', trials['intervals']) np.save(output_path / '_ibl_trials.feedback_times.npy', trials['feedback_times']) return trials
def extract_behaviour_sync(sync, chmap=None, display=False, tmax=np.inf): """ Extract wheel positions and times from sync fronts dictionary :param sync: dictionary 'times', 'polarities' of fronts detected on sync trace for all 16 chans :param chmap: dictionary containing channel index. Default to constant. chmap = {'bpod': 7, 'frame2ttl': 12, 'audio': 15} :param display: bool or matplotlib axes: show the full session sync pulses display defaults to False :return: trials dictionary """ bpod = _get_sync_fronts(sync, chmap['bpod'], tmax=tmax) if bpod.times.size == 0: raise err.SyncBpodFpgaException( 'No Bpod event found in FPGA. No behaviour extraction. ' 'Check channel maps.') frame2ttl = _get_sync_fronts(sync, chmap['frame2ttl'], tmax=tmax) audio = _get_sync_fronts(sync, chmap['audio'], tmax=tmax) # extract events from the fronts for each trace t_trial_start, t_valve_open, t_iti_in = _assign_events_bpod( bpod['times'], bpod['polarities']) t_ready_tone_in, t_error_tone_in = _assign_events_audio( audio['times'], audio['polarities']) trials = Bunch({ 'goCue_times': _assign_events_to_trial(t_trial_start, t_ready_tone_in, take='first'), 'errorCue_times': _assign_events_to_trial(t_trial_start, t_error_tone_in), 'valveOpen_times': _assign_events_to_trial(t_trial_start, t_valve_open), 'stimFreeze_times': _assign_events_to_trial(t_trial_start, frame2ttl['times'], take=-2), 'stimOn_times': _assign_events_to_trial(t_trial_start, frame2ttl['times'], take='first'), 'stimOff_times': _assign_events_to_trial(t_trial_start, frame2ttl['times']), 'itiIn_times': _assign_events_to_trial(t_trial_start, t_iti_in) }) # feedback times are valve open on good trials and error tone in on error trials trials['feedback_times'] = np.copy(trials['valveOpen_times']) ind_err = np.isnan(trials['valveOpen_times']) trials['feedback_times'][ind_err] = trials['errorCue_times'][ind_err] trials['intervals'] = np.c_[t_trial_start, trials['itiIn_times']] if display: width = 0.5 ymax = 5 if isinstance(display, bool): plt.figure("Ephys FPGA Sync") ax = plt.gca() else: ax = display r0 = _get_sync_fronts(sync, chmap['rotary_encoder_0']) plots.squares(bpod['times'], bpod['polarities'] * 0.4 + 1, ax=ax, color='k') plots.squares(frame2ttl['times'], frame2ttl['polarities'] * 0.4 + 2, ax=ax, color='k') plots.squares(audio['times'], audio['polarities'] * 0.4 + 3, ax=ax, color='k') plots.squares(r0['times'], r0['polarities'] * 0.4 + 4, ax=ax, color='k') plots.vertical_lines(t_ready_tone_in, ymin=0, ymax=ymax, ax=ax, label='goCue_times', color='b', linewidth=width) plots.vertical_lines(t_trial_start, ymin=0, ymax=ymax, ax=ax, label='start_trial', color='m', linewidth=width) plots.vertical_lines(t_error_tone_in, ymin=0, ymax=ymax, ax=ax, label='error tone', color='r', linewidth=width) plots.vertical_lines(t_valve_open, ymin=0, ymax=ymax, ax=ax, label='valveOpen_times', color='g', linewidth=width) plots.vertical_lines(trials['stimFreeze_times'], ymin=0, ymax=ymax, ax=ax, label='stimFreeze_times', color='y', linewidth=width) plots.vertical_lines(trials['stimOff_times'], ymin=0, ymax=ymax, ax=ax, label='stim off', color='c', linewidth=width) plots.vertical_lines(trials['stimOn_times'], ymin=0, ymax=ymax, ax=ax, label='stimOn_times', color='tab:orange', linewidth=width) c = _get_sync_fronts(sync, chmap['left_camera']) plots.squares(c['times'], c['polarities'] * 0.4 + 5, ax=ax, color='k') c = _get_sync_fronts(sync, chmap['right_camera']) plots.squares(c['times'], c['polarities'] * 0.4 + 6, ax=ax, color='k') c = _get_sync_fronts(sync, chmap['body_camera']) plots.squares(c['times'], c['polarities'] * 0.4 + 7, ax=ax, color='k') ax.legend() ax.set_yticklabels(['', 'bpod', 'f2ttl', 'audio', 're_0', '']) ax.set_yticks([0, 1, 2, 3, 4, 5]) ax.set_ylim([0, 5]) return trials