from iblutil.numerical import ismember, bincount2D
import numpy as np
[docs]
def find_trial_ids(trials, side='all', choice='all', order='trial num', sort='idx',
contrast=(1, 0.5, 0.25, 0.125, 0.0625, 0), event=None):
"""
Finds trials that match criterion
:param trials: trials object. Must contain attributes contrastLeft, contrastRight and
feedbackType
:param side: stimulus side, options are 'all', 'left' or 'right'
:param choice: trial choice, options are 'all', 'correct' or 'incorrect'
:param contrast: contrast of stimulus, pass in list/tuple of all contrasts that want to be
considered e.g [1, 0.5] would only look for trials with 100 % and 50 % contrast
:param order: how to order the trials, options are 'trial num' or 'reaction time'
:param sort: how to sort the trials, options are 'side' (split left right trials), 'choice'
(split correct incorrect trials), 'choice and side' (split left right and correct incorrect)
:param event: trial event to align to (in order to remove nan trials for this event)
:return: np.array of trial ids, list of dividers to indicate how trials are sorted
"""
if event:
idx = ~np.isnan(trials[event])
nan_idx = np.where(idx)[0]
else:
idx = np.ones_like(trials['feedbackType'], dtype=bool)
# Find trials that have specified contrasts
cont = np.bitwise_or(ismember(trials['contrastLeft'][idx], np.array(contrast))[0],
ismember(trials['contrastRight'][idx], np.array(contrast))[0])
# Find different permutations of trials
# correct right
cor_r = np.where(
np.bitwise_and(cont, np.bitwise_and(trials['feedbackType'][idx] == 1,
np.isfinite(trials['contrastRight'][idx]))))[0]
# correct left
cor_l = np.where(
np.bitwise_and(cont, np.bitwise_and(trials['feedbackType'][idx] == 1,
np.isfinite(trials['contrastLeft'][idx]))))[0]
# incorrect right
incor_r = np.where(
np.bitwise_and(cont, np.bitwise_and(trials['feedbackType'][idx] == -1,
np.isfinite(trials['contrastRight'][idx]))))[0]
# incorrect left
incor_l = np.where(
np.bitwise_and(cont, np.bitwise_and(trials['feedbackType'][idx] == -1,
np.isfinite(trials['contrastLeft'][idx]))))[0]
reaction_time = trials['response_times'][idx] - trials['goCue_times'][idx]
def _order_by(_trials, order):
# Returns subset of trials either ordered by trial number or by reaction time
sorted_trials = np.sort(_trials)
if order == 'trial num':
return sorted_trials
elif order == 'reaction time':
sorted_reaction = np.argsort(reaction_time[sorted_trials])
return sorted_trials[sorted_reaction]
dividers = []
# Find the trial id for all possible combinations
if side == 'all' and choice == 'all':
if sort == 'idx':
trial_id = _order_by(np.r_[cor_r, cor_l, incor_r, incor_l], order)
elif sort == 'choice':
trial_id = np.r_[_order_by(np.r_[cor_l, cor_r], order),
_order_by(np.r_[incor_l, incor_r], order)]
dividers.append(np.r_[cor_l, cor_r].shape[0])
elif sort == 'side':
trial_id = np.r_[_order_by(np.r_[cor_l, incor_l], order),
_order_by(np.r_[cor_r, incor_r], order)]
dividers.append(np.r_[cor_l, incor_l].shape[0])
elif sort == 'choice and side':
trial_id = np.r_[_order_by(cor_l, order), _order_by(incor_l, order),
_order_by(cor_r, order), _order_by(incor_r, order)]
dividers.append(cor_l.shape[0])
dividers.append(np.r_[cor_l, incor_l].shape[0])
dividers.append(np.r_[cor_l, incor_l, cor_r].shape[0])
if side == 'left' and choice == 'all':
if sort in ['idx', 'side']:
trial_id = _order_by(np.r_[cor_l, incor_l], order)
elif sort in ['choice', 'choice and side']:
trial_id = np.r_[_order_by(cor_l, order), _order_by(incor_l, order)]
dividers.append(cor_l.shape[0])
if side == 'right' and choice == 'all':
if sort in ['idx', 'side']:
trial_id = _order_by(np.r_[cor_r, incor_r], order)
elif sort in ['choice', 'choice and side']:
trial_id = np.r_[_order_by(cor_r, order), _order_by(incor_r, order)]
dividers.append(cor_r.shape[0])
if side == 'all' and choice == 'correct':
if sort in ['idx', 'choice']:
trial_id = _order_by(np.r_[cor_l, cor_r], order)
elif sort in ['side', 'choice and side']:
trial_id = np.r_[_order_by(cor_l, order), _order_by(cor_r, order)]
dividers.append(cor_l.shape[0])
if side == 'all' and choice == 'incorrect':
if sort in ['idx', 'choice']:
trial_id = _order_by(np.r_[incor_l, incor_r], order)
elif sort in ['side', 'choice and side']:
trial_id = np.r_[_order_by(incor_l, order), _order_by(incor_r, order)]
dividers.append(incor_l.shape[0])
if side == 'left' and choice == 'correct':
trial_id = _order_by(cor_l, order)
if side == 'left' and choice == 'incorrect':
trial_id = _order_by(incor_l, order)
if side == 'right' and choice == 'correct':
trial_id = _order_by(cor_r, order)
if side == 'right' and choice == 'incorrect':
trial_id = _order_by(incor_r, order)
if event:
trial_id = nan_idx[trial_id]
return trial_id, dividers
[docs]
def get_event_aligned_raster(times, events, tbin=0.02, values=None, epoch=[-0.4, 1], bin=True):
"""
Get event aligned raster
:param times: array of times e.g spike times or dlc points
:param events: array of events to epoch around
:param tbin: bin size to over which to count events
:param values: values to scale counts by
:param epoch: window around each event
:param bin: whether to bin times in tbin windows or not
:return:
"""
if bin:
vals, bin_times, _ = bincount2D(times, np.ones_like(times), xbin=tbin, weights=values)
vals = vals[0]
t = np.arange(epoch[0], epoch[1] + tbin, tbin)
nbin = t.shape[0]
else:
vals = values
bin_times = times
tbin = np.mean(np.diff(bin_times))
t = np.arange(epoch[0], epoch[1], tbin)
nbin = t.shape[0]
# remove nan trials
non_nan_events = events[~np.isnan(events)]
nan_idx = np.where(~np.isnan(events))
intervals = np.c_[non_nan_events + epoch[0], non_nan_events + epoch[1]]
# Remove any trials that are later than the last value in bin_times
out_intervals = intervals[:, 1] > bin_times[-1]
epoch_idx = np.searchsorted(bin_times, intervals)[np.invert(out_intervals)]
for ep in range(nbin):
if ep == 0:
event_raster = (vals[epoch_idx[:, 0] + ep]).astype(float)
else:
event_raster = np.c_[event_raster, vals[epoch_idx[:, 0] + ep]]
# Find any trials that are less than the first value time and fill with nans (case for example
# where spiking of cluster doesn't start till after start of first trial due to settling of
# brain)
event_raster[intervals[np.invert(out_intervals), 0] < bin_times[0]] = np.nan
# Add back in the trials that were later than last value with nans
if np.sum(out_intervals) > 0:
event_raster = np.r_[event_raster, np.full((np.sum(out_intervals),
event_raster.shape[1]), np.nan)]
assert event_raster.shape[0] == intervals.shape[0]
# Reindex if we have removed any nan values
all_event_raster = np.full((events.shape[0], event_raster.shape[1]), np.nan)
all_event_raster[nan_idx, :] = event_raster
return all_event_raster, t
[docs]
def get_psth(raster, trial_ids=None):
"""
Compute psth averaged over chosen trials
:param raster: output from event aligned raster, window of activity around event
:param trial_ids: the trials from the raster to average over
:return:
"""
if trial_ids is None:
mean = np.nanmean(raster, axis=0)
err = np.nanstd(raster, axis=0) / np.sqrt(raster.shape[0])
else:
raster = filter_by_trial(raster, trial_ids)
mean = np.nanmean(raster, axis=0)
err = np.nanstd(raster, axis=0) / np.sqrt(raster.shape[0])
return mean, err
[docs]
def filter_by_trial(raster, trial_id):
"""
Select trials of interest for raster
:param raster:
:param trial_id:
:return:
"""
return raster[trial_id, :]
[docs]
def filter_correct_incorrect_left_right(trials, event_raster, event, contrast, order='trial num'):
"""
Return psth for left correct, left incorrect, right correct, right incorrect and raster
sorted by these trials
:param trials: trials object
:param event_raster: output from get_event_aligned_activity
:param event: event to align to e.g 'goCue_times', 'stimOn_times'
:param contrast: contrast of stimulus, pass in list/tuple of all contrasts that want to be
considered e.g [1, 0.5] would only look for trials with 100 % and 50 % contrast
:param order: order to sort trials by either 'trial num' or 'reaction time'
:return:
"""
trials_sorted, div = find_trial_ids(trials, sort='choice and side', event=event, order=order, contrast=contrast)
trials_lc, _ = find_trial_ids(trials, side='left', choice='correct', event=event, order=order, contrast=contrast)
trials_li, _ = find_trial_ids(trials, side='left', choice='incorrect', event=event,
order=order, contrast=contrast)
trials_rc, _ = find_trial_ids(trials, side='right', choice='correct', event=event, order=order, contrast=contrast)
trials_ri, _ = find_trial_ids(trials, side='right', choice='incorrect', event=event,
order=order, contrast=contrast)
psth = dict()
mean, err = get_psth(event_raster, trials_lc)
psth['left_correct'] = {'vals': mean, 'err': err,
'linestyle': {'color': 'r'}}
mean, err = get_psth(event_raster, trials_li)
psth['left_incorrect'] = {'vals': mean, 'err': err,
'linestyle': {'color': 'r', 'linestyle': 'dashed'}}
mean, err = get_psth(event_raster, trials_rc)
psth['right_correct'] = {'vals': mean, 'err': err,
'linestyle': {'color': 'b'}}
mean, err = get_psth(event_raster, trials_ri)
psth['right_incorrect'] = {'vals': mean, 'err': err,
'linestyle': {'color': 'b', 'linestyle': 'dashed'}}
raster = {}
raster['vals'] = filter_by_trial(event_raster, trials_sorted)
raster['dividers'] = div
return raster, psth
[docs]
def filter_correct_incorrect(trials, event_raster, event, contrast, order='trial num'):
"""
Return psth for correct and incorrect trials and raster sorted by correct incorrect
:param trials: trials object
:param event_raster: output from get_event_aligned_activity
:param event: event to align to e.g 'goCue_times', 'stimOn_times'
:param contrast: contrast of stimulus, pass in list/tuple of all contrasts that want to be
considered e.g [1, 0.5] would only look for trials with 100 % and 50 % contrast
:param order: order to sort trials by either 'trial num' or 'reaction time'
:return:
"""
trials_sorted, div = find_trial_ids(trials, sort='choice', event=event, order=order, contrast=contrast)
trials_c, _ = find_trial_ids(trials, side='all', choice='correct', event=event, order=order, contrast=contrast)
trials_i, _ = find_trial_ids(trials, side='all', choice='incorrect', event=event, order=order, contrast=contrast)
psth = dict()
mean, err = get_psth(event_raster, trials_c)
psth['correct'] = {'vals': mean, 'err': err, 'linestyle': {'color': 'r'}}
mean, err = get_psth(event_raster, trials_i)
psth['incorrect'] = {'vals': mean, 'err': err, 'linestyle': {'color': 'b'}}
raster = {}
raster['vals'] = filter_by_trial(event_raster, trials_sorted)
raster['dividers'] = div
return raster, psth
[docs]
def filter_left_right(trials, event_raster, event, contrast, order='trial num'):
"""
Return psth for left and right trials and raster sorted by left right
:param trials: trials object
:param event_raster: output from get_event_aligned_activity
:param event: event to align to e.g 'goCue_times', 'stimOn_times'
:param contrast: contrast of stimulus, pass in list/tuple of all contrasts that want to be
considered e.g [1, 0.5] would only look for trials with 100 % and 50 % contrast
:param order: order to sort trials by either 'trial num' or 'reaction time'
:return:
"""
trials_sorted, div = find_trial_ids(trials, sort='side', event=event, order=order, contrast=contrast)
trials_l, _ = find_trial_ids(trials, side='left', choice='all', event=event, order=order, contrast=contrast)
trials_r, _ = find_trial_ids(trials, side='right', choice='all', event=event, order=order, contrast=contrast)
psth = dict()
mean, err = get_psth(event_raster, trials_l)
psth['left'] = {'vals': mean, 'err': err, 'linestyle': {'color': 'r'}}
mean, err = get_psth(event_raster, trials_r)
psth['right'] = {'vals': mean, 'err': err, 'linestyle': {'color': 'b'}}
raster = {}
raster['vals'] = filter_by_trial(event_raster, trials_sorted)
raster['dividers'] = div
return raster, psth
[docs]
def filter_trials(trials, event_raster, event, contrast=(1, 0.5, 0.25, 0.125, 0.0625, 0), order='trial num', sort='choice'):
"""
Wrapper to get out psth and raster for trial choice
:param trials: trials object
:param event_raster: output from get_event_aligned_activity
:param event: event to align to e.g 'goCue_times', 'stimOn_times'
:param contrast: contrast of stimulus, pass in list/tuple of all contrasts that want to be
considered e.g [1, 0.5] would only look for trials with 100 % and 50 % contrast
:param order: order to sort trials by either 'trial num' or 'reaction time'
:param sort: how to divide trials options are 'choice' (e.g correct vs incorrect), 'side'
(e.g left vs right') and 'choice and side' (e.g correct vs incorrect and left vs right)
:return:
"""
if sort == 'choice':
raster, psth = filter_correct_incorrect(trials, event_raster, event, contrast, order)
elif sort == 'side':
raster, psth = filter_left_right(trials, event_raster, event, contrast, order)
elif sort == 'choice and side':
raster, psth = filter_correct_incorrect_left_right(trials, event_raster, event, contrast, order)
return raster, psth