def sync_rotary_encoder(session_path, bpod_data=None, re_events=None): if not bpod_data: bpod_data = raw.load_data(session_path) evt = re_events or raw.load_encoder_events(session_path) # we work with stim_on (2) and closed_loop (3) states for the synchronization with bpod tre = evt.re_ts.values / 1e6 # convert to seconds # the first trial on the rotary encoder is a dud rote = {'stim_on': tre[evt.sm_ev == 2][:-1], 'closed_loop': tre[evt.sm_ev == 3][:-1]} bpod = { 'stim_on': np.array([tr['behavior_data']['States timestamps'] ['stim_on'][0][0] for tr in bpod_data]), 'closed_loop': np.array([tr['behavior_data']['States timestamps'] ['closed_loop'][0][0] for tr in bpod_data]), } if rote['closed_loop'].size <= 1: raise err.SyncBpodWheelException("Not enough Rotary Encoder events to perform wheel" " synchronization. Wheel data not extracted") # bpod bug that spits out events in ms instead of us if np.diff(bpod['closed_loop'][[-1, 0]])[0] / np.diff(rote['closed_loop'][[-1, 0]])[0] > 900: _logger.error("Rotary encoder stores values in ms instead of us. Wheel timing inaccurate") rote['stim_on'] *= 1e3 rote['closed_loop'] *= 1e3 # just use the closed loop for synchronization # handle different sizes in synchronization: sz = min(rote['closed_loop'].size, bpod['closed_loop'].size) # if all the sample are contiguous and first samples match diff_first_match = np.diff(rote['closed_loop'][:sz]) - np.diff(bpod['closed_loop'][:sz]) # if all the sample are contiguous and last samples match diff_last_match = np.diff(rote['closed_loop'][-sz:]) - np.diff(bpod['closed_loop'][-sz:]) # 99% of the pulses match for a first sample lock DIFF_THRESHOLD = 0.005 if np.mean(np.abs(diff_first_match) < DIFF_THRESHOLD) > 0.99: re = rote['closed_loop'][:sz] bp = bpod['closed_loop'][:sz] indko = np.where(np.abs(diff_first_match) >= DIFF_THRESHOLD)[0] # 99% of the pulses match for a last sample lock elif np.mean(np.abs(diff_last_match) < DIFF_THRESHOLD) > 0.99: re = rote['closed_loop'][-sz:] bp = bpod['closed_loop'][-sz:] indko = np.where(np.abs(diff_last_match) >= DIFF_THRESHOLD)[0] # last resort is to use ad-hoc sync function else: bp, re = raw.sync_trials_robust(bpod['closed_loop'], rote['closed_loop'], diff_threshold=DIFF_THRESHOLD, max_shift=5) indko = np.array([]) # raise ValueError("Can't sync bpod and rotary encoder: non-contiguous sync pulses") # remove faulty indices due to missing or bad syncs indko = np.int32(np.unique(np.r_[indko + 1, indko])) re = np.delete(re, indko) bp = np.delete(bp, indko) # check the linear drift assert bp.size > 1 poly = np.polyfit(bp, re, 1) assert np.all(np.abs(np.polyval(poly, bp) - re) < 0.002) return interpolate.interp1d(re, bp, fill_value="extrapolate")
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