def test_sync(self): # Test casting non-uniformly-sampled data to a evenly-sampled TimeSeries. # Begin by defining sampling intervals of random half-normally distributed length times = np.cumsum(np.abs(np.random.normal(loc=4., scale=6., size=100))) # take sample values as though the value was increasing as a cube of sample time samples = times**3 # Use cubic interpolation to resample to uniform interval cubes = core.TimeSeries(times=times, values=samples, columns=('cubic',)) resamp = processing.sync(0.1, cubes, interp='cubic', fillval='extrapolate') # Check that the sync function is returning a new time series object self.assertTrue(isinstance(resamp, core.TimeSeries)) # Test that all returned sample times are uniformly spaced # We need to use np.isclose because of floating point arithematic problems instead of ==0.1 # Since the actual diff returns 0.09999999999999964 self.assertTrue(np.all(np.isclose(np.diff(resamp.times), 0.1))) # Check that we're within a margin of error on the interpolation err_margin = 1e-3 # Maximum percent error allowed err_percs = np.abs(resamp.times**3 - resamp.values.T) / (resamp.times**3) self.assertTrue(np.all(err_percs < err_margin)) # Make a second timeseries of square-law increasing samples times2 = np.cumsum(np.abs(np.random.normal(loc=2., scale=1., size=200))) samples2 = times2**2 squares = core.TimeSeries(times=times2, values=samples2, columns=('square',)) # Use cubic interpolation again, this time on both timeseries resamp2 = processing.sync(0.1, [squares, cubes], interp='cubic', fillval='extrapolate') # Check that the new TS has both squares and cubes as keys and attribs self.assertTrue(hasattr(resamp2, 'cubic')) self.assertTrue(hasattr(resamp2, 'square')) # Check that both timeseries are fully contained in the resampled TS self.assertTrue(cubes.times.min() >= resamp2.times.min()) self.assertTrue(cubes.times.max() <= resamp2.times.max()) self.assertTrue(squares.times.min() >= resamp2.times.min()) self.assertTrue(squares.times.max() <= resamp2.times.max()) # Check that all interpolated values are within the margin of error against the known func sq_errperc = np.abs(resamp2.times**2 - resamp2.square) / resamp2.times**2 cu_errperc = np.abs(resamp2.times**3 - resamp2.cubic) / resamp2.times**3 self.assertTrue(np.all(sq_errperc < err_margin) & np.all(cu_errperc < err_margin)) # Now check the numpy array behavior of sync. # Try running sync on the cubic times and values only. resamp = processing.sync(0.1, times=times, values=samples, interp='cubic', fillval='extrapolate') # Do all the tests we did for the instance created using TimeSeries objects self.assertTrue(isinstance(resamp, core.TimeSeries)) self.assertTrue(np.all(np.isclose(np.diff(resamp.times), 0.1))) err_margin = 1e-3 # Maximum percent error allowed err_percs = np.abs(resamp.times**3 - resamp.values.T) / (resamp.times**3) self.assertTrue(np.all(err_percs < err_margin)) # Try the multiple-arrays case in which we pass two times and two values resamp2 = processing.sync(0.1, times=(times, times2), values=(samples, samples2), interp='cubic', fillval='extrapolate') self.assertTrue(times.min() >= resamp2.times.min()) self.assertTrue(times.max() <= resamp2.times.max()) self.assertTrue(times2.min() >= resamp2.times.min()) self.assertTrue(times2.max() <= resamp2.times.max())
def load_trials_df(eid, one=None, maxlen=None, t_before=0., t_after=0., ret_wheel=False, ret_abswheel=False, wheel_binsize=0.02): """ Generate a pandas dataframe of per-trial timing information about a given session. Each row in the frame will correspond to a single trial, with timing values indicating timing session-wide (i.e. time in seconds since session start). Can optionally return a resampled wheel velocity trace of either the signed or absolute wheel velocity. The resulting dataframe will have a new set of columns, trial_start and trial_end, which define via t_before and t_after the span of time assigned to a given trial. (useful for bb.modeling.glm) Parameters ---------- eid : str Session UUID string to pass to ONE one : oneibl.one.OneAlyx, optional one object to use for loading. Will generate internal one if not used, by default None maxlen : float, optional Maximum trial length for inclusion in df. Trials where feedback - response is longer than this value will not be included in the dataframe, by default None t_before : float, optional Time before stimulus onset to include for a given trial, as defined by the trial_start column of the dataframe. If zero, trial_start will be identical to stimOn, by default 0. t_after : float, optional Time after feedback to include in the trail, as defined by the trial_end column of the dataframe. If zero, trial_end will be identical to feedback, by default 0. ret_wheel : bool, optional Whether to return the time-resampled wheel velocity trace, by default False ret_abswheel : bool, optional Whether to return the time-resampled absolute wheel velocity trace, by default False wheel_binsize : float, optional Time bins to resample wheel velocity to, by default 0.02 Returns ------- pandas.DataFrame Dataframe with trial-wise information. Indices are the actual trial order in the original data, preserved even if some trials do not meet the maxlen criterion. As a result will not have a monotonic index. Has special columns trial_start and trial_end which define start and end times via t_before and t_after """ if not one: one = ONE() if ret_wheel and ret_abswheel: raise ValueError('ret_wheel and ret_abswheel cannot both be true.') # Define which datatypes we want to pull out trialstypes = [ 'trials.choice', 'trials.probabilityLeft', 'trials.feedbackType', 'trials.feedback_times', 'trials.contrastLeft', 'trials.contrastRight', 'trials.goCue_times', 'trials.stimOn_times', ] # A quick function to remap probabilities in those sessions where it was not computed correctly def remap_trialp(probs): # Block probabilities in trial data aren't accurate and need to be remapped validvals = np.array([0.2, 0.5, 0.8]) diffs = np.abs(np.array([x - validvals for x in probs])) maps = diffs.argmin(axis=1) return validvals[maps] starttimes = one.load(eid, dataset_types=['trials.stimOn_times'])[0] endtimes = one.load(eid, dataset_types=['trials.feedback_times'])[0] tmp = one.load(eid, dataset_types=trialstypes) if maxlen is not None: with np.errstate(invalid='ignore'): keeptrials = (endtimes - starttimes) <= maxlen else: keeptrials = range(len(starttimes)) trialdata = { x.split('.')[1]: tmp[i][keeptrials] for i, x in enumerate(trialstypes) } trialdata['probabilityLeft'] = remap_trialp(trialdata['probabilityLeft']) trialsdf = pd.DataFrame(trialdata) if maxlen is not None: trialsdf.set_index(np.nonzero(keeptrials)[0], inplace=True) trialsdf['trial_start'] = trialsdf['stimOn_times'] - t_before trialsdf['trial_end'] = trialsdf['feedback_times'] + t_after if not ret_wheel and not ret_abswheel: return trialsdf wheel = one.load_object(eid, 'wheel') whlpos, whlt = wheel.position, wheel.timestamps starttimes = trialsdf['trial_start'] endtimes = trialsdf['trial_end'] wh_endlast = 0 trials = [] for (start, end) in np.vstack((starttimes, endtimes)).T: wh_startind = np.searchsorted(whlt[wh_endlast:], start) + wh_endlast wh_endind = np.searchsorted(whlt[wh_endlast:], end, side='right') + wh_endlast + 4 wh_endlast = wh_endind tr_whlpos = whlpos[wh_startind - 1:wh_endind + 1] tr_whlt = whlt[wh_startind - 1:wh_endind + 1] - start tr_whlt[0] = 0. # Manual previous-value interpolation whlseries = TimeSeries(tr_whlt, tr_whlpos, columns=['whlpos']) whlsync = sync(wheel_binsize, timeseries=whlseries, interp='previous') trialstartind = np.searchsorted(whlsync.times, 0) trialendind = np.ceil((end - start) / wheel_binsize).astype(int) trpos = whlsync.values[trialstartind:trialendind + trialstartind] whlvel = trpos[1:] - trpos[:-1] whlvel = np.insert(whlvel, 0, 0) if np.abs((trialendind - len(whlvel))) > 0: raise IndexError( 'Mismatch between expected length of wheel data and actual.') if ret_wheel: trials.append(whlvel) elif ret_abswheel: trials.append(np.abs(whlvel)) trialsdf['wheel_velocity'] = trials return trialsdf