def sync_probe_front_times(t, tref, sr, display=False): """ From 2 timestamps vectors of equivalent length, output timestamps array to be used for linear interpolation :param t: time-serie to be synchronized :param tref: time-serie of the reference :param sr: sampling rate of the slave probe :return: a 2 columns by n-sync points array where each row corresponds to a sync point: sample_index (0 based), tref """ COMPUTE_RESIDUAL = True # the main drift is computed through linear regression. A further step compute a smoothed # version of the residual to add to the linear drift. The precision is enforced # by ensuring that each point lies less than one sampling rate away from the predicted. pol = np.polyfit(t, tref, 1) # higher order terms first: slope / int for linear residual = (tref - np.polyval(pol, t)) if COMPUTE_RESIDUAL: # the interp function from camera fronts is not smooth due to the locking of detections # to the sampling rate of digital channels. The residual is fit using frequency domain # smoothing import ibllib.dsp as dsp CAMERA_UPSAMPLING_RATE_HZ = 300 PAD_LENGTH_SECS = 60 STAT_LENGTH_SECS = 30 # median length to compute padding value SYNC_SAMPLING_RATE_SECS = 20 t_upsamp = np.arange(tref[0], tref[-1], 1 / CAMERA_UPSAMPLING_RATE_HZ) res_upsamp = np.interp(t_upsamp, tref, residual) # padding needs extra care as the function oscillates lpad = int(sr * PAD_LENGTH_SECS) res_filt = np.pad(res_upsamp, lpad, mode='median', stat_length=sr * STAT_LENGTH_SECS) fbounds = 1 / SYNC_SAMPLING_RATE_SECS * np.array([2, 4]) res_filt = dsp.lp(res_filt, 1 / sr, fbounds)[lpad:-lpad] tout = np.arange(0, np.max(tref) + SYNC_SAMPLING_RATE_SECS, 20) sync_points = np.c_[tout * sr, np.polyval(pol, tout) - np.interp(tout, t_upsamp, res_filt)] if display: plt.plot(tref, residual * sr) plt.plot(t_upsamp, res_filt * sr) plt.plot(tout, np.interp(tout, t_upsamp, res_filt) * sr, '*') plt.ylabel('Residual drift (samples @ 30kHz)') plt.xlabel('time (sec)') else: sync_points = np.c_[np.array([0, sr]), np.polyval(pol, np.array([0, 1]))] if display: plt.plot(tref, residual * sr) plt.ylabel('Residual drift (samples @ 30kHz)') plt.xlabel('time (sec)') return sync_points
def sync_probe_front_times(t, tref, sr, display=False, type='smooth', tol=2.0): """ From 2 timestamps vectors of equivalent length, output timestamps array to be used for linear interpolation :param t: time-serie to be synchronized :param tref: time-serie of the reference :param sr: sampling rate of the slave probe :return: a 2 columns by n-sync points array where each row corresponds to a sync point: sample_index (0 based), tref :return: quality Bool. False if tolerance is exceeded """ qc = True """ the main drift is computed through linear regression. A further step compute a smoothed version of the residual to add to the linear drift. The precision is enforced by ensuring that each point lies less than one sampling rate away from the predicted. """ pol = np.polyfit(t, tref, 1) # higher order terms first: slope / int for linear residual = tref - np.polyval(pol, t) if type == 'smooth': """ the interp function from camera fronts is not smooth due to the locking of detections to the sampling rate of digital channels. The residual is fit using frequency domain smoothing """ import ibllib.dsp as dsp CAMERA_UPSAMPLING_RATE_HZ = 300 PAD_LENGTH_SECS = 60 STAT_LENGTH_SECS = 30 # median length to compute padding value SYNC_SAMPLING_RATE_SECS = 20 t_upsamp = np.arange(tref[0], tref[-1], 1 / CAMERA_UPSAMPLING_RATE_HZ) res_upsamp = np.interp(t_upsamp, tref, residual) # padding needs extra care as the function oscillates and numpy fft performance is # abysmal for non prime sample sizes nech = res_upsamp.size + (CAMERA_UPSAMPLING_RATE_HZ * PAD_LENGTH_SECS) lpad = 2**np.ceil(np.log2(nech)) - res_upsamp.size lpad = [int(np.floor(lpad / 2) + lpad % 2), int(np.floor(lpad / 2))] res_filt = np.pad(res_upsamp, lpad, mode='median', stat_length=CAMERA_UPSAMPLING_RATE_HZ * STAT_LENGTH_SECS) fbounds = [0.001, 0.002] res_filt = dsp.lp(res_filt, 1 / CAMERA_UPSAMPLING_RATE_HZ, fbounds)[lpad[0]:-lpad[1]] tout = np.arange(0, np.max(tref) + SYNC_SAMPLING_RATE_SECS, 20) sync_points = np.c_[tout, np.polyval(pol, tout) + np.interp(tout, t_upsamp, res_filt)] if display: if isinstance(display, matplotlib.axes.Axes): ax = display else: ax = plt.axes() ax.plot(tref, residual * sr, label='residual') ax.plot(t_upsamp, res_filt * sr, label='smoothed residual') ax.plot(tout, np.interp(tout, t_upsamp, res_filt) * sr, '*', label='interp timestamps') ax.legend() ax.set_xlabel('time (sec)') ax.set_ylabel('Residual drift (samples @ 30kHz)') elif type == 'exact': sync_points = np.c_[t, tref] if display: plt.plot(tref, residual * sr, label='residual') plt.ylabel('Residual drift (samples @ 30kHz)') plt.xlabel('time (sec)') pass elif type == 'linear': sync_points = np.c_[np.array([0., 1.]), np.polyval(pol, np.array([0., 1.]))] if display: plt.plot(tref, residual * sr) plt.ylabel('Residual drift (samples @ 30kHz)') plt.xlabel('time (sec)') # test that the interp is within tol sample fcn = interp1d(sync_points[:, 0], sync_points[:, 1], fill_value='extrapolate') if np.any(np.abs((tref - fcn(t)) * sr) > (tol)): _logger.error( f'Synchronization check exceeds tolerance of {tol} samples. Check !!' ) qc = False # plt.plot((tref - fcn(t)) * sr) # plt.plot( (sync_points[:, 0] - fcn(sync_points[:, 1])) * sr) return sync_points, qc