def show_colorcircle(subject, contrast, depthBits, numStim): if contrast is None: contrast = 0.15 if depthBits is None: depthBits = 10 if numStim is None: numStim = 16 theta = np.linspace(0, 2 * np.pi, numStim, endpoint=False) cp = ColorPicker(c=contrast, sscale=2.6, unit='rad', depthBits=depthBits, subject=None) Msml = [] Mrgb = [] for t in theta: sml, rgb = cp.newcolor(theta=t) Msml.append(sml) Mrgb.append(rgb) sub_cp = ColorPicker(c=contrast, sscale=2.6, unit='rad', depthBits=depthBits, subject=subject) sub_Msml = [] sub_Mrgb = [] for t in theta: sml, rgb = sub_cp.newcolor(theta=t) sub_Msml.append(sml) sub_Mrgb.append(rgb) winM = visual.Window(fullscr=True, allowGUI=True, bpc=(cp.depthBits, cp.depthBits, cp.depthBits), depthBits=cp.depthBits, colorSpace=cp.colorSpace, color=cp.sml2rgb(cp.center())) rectsize = 0.2 * winM.size[0] * 2 / numStim radius = 0.1 * winM.size[0] alphas = np.linspace(0, 360, numStim, endpoint=False) rect = visual.Rect(win=winM, units="pix", width=int(rectsize), height=int(rectsize)) winM.flip() for t in range(50): for wait_keys in event.waitKeys(): if wait_keys == 'left': for i_rect in range(numStim): rect.fillColorSpace = cp.colorSpace rect.lineColorSpace = cp.colorSpace rect.fillColor = Mrgb[i_rect] rect.lineColor = Mrgb[i_rect] rect.pos = misc.pol2cart(alphas[i_rect], radius) rect.draw() winM.flip() elif wait_keys == 'right': sub_rect = rect for i_rect in range(numStim): sub_rect.fillColorSpace = sub_cp.colorSpace sub_rect.lineColorSpace = sub_cp.colorSpace sub_rect.fillColor = sub_Mrgb[i_rect] sub_rect.lineColor = sub_Mrgb[i_rect] sub_rect.pos = misc.pol2cart(alphas[i_rect], radius) sub_rect.draw() text = visual.TextStim(winM, text=subject, pos=[0.3, -0.5], height=0.05, color='black') text.draw() winM.flip() elif wait_keys == 'escape': core.quit()
class Exp: """ Class for performing the experiment. """ def __init__(self, subject, par_file, cfg_file, res_dir, priors_file_path): self.subject = subject self.idx = time.strftime("%Y%m%dT%H%M", time.localtime()) # add the current date if not res_dir: res_dir = 'data/' self.res_dir = res_dir self.priors_file_path = priors_file_path self.cfg_file = cfg_file self.cfg = config_tools.read_yml(cfg_file) self.par_file = par_file self.param = config_tools.read_yml(par_file) self.patch_nmb = self.cfg['patch_nmb'] self.trial_nmb = self.cfg['trial_nmb'] self.trial_dur = self.cfg['trial_dur'] self.depthBits = self.cfg['depthBits'] self.ColorPicker = ColorPicker(c=self.param['c'], sscale=self.param['sscale'], unit='deg', depthBits=self.depthBits, subject=self.subject) self.ColorSpace = self.ColorPicker.colorSpace hue_list_path = 'config/colorlist/' + self.subject if not os.path.exists(hue_list_path): self.ColorPicker.gencolorlist(0.2) self.hue_list = hue_list_path + '/hue-list-10bit-res0.2-sub-' + self.subject + '.npy' self.Csml = self.ColorPicker.center() self.Crgb = self.ColorPicker.sml2rgb(self.ColorPicker.center()) self.mon = monitors.Monitor(name=self.cfg['monitor']['name'], width=self.cfg['monitor']['width'], distance=self.cfg['monitor']['distance']) self.mon.setSizePix((self.cfg['monitor']['size'])) self.win = visual.Window(monitor=self.mon, unit='deg', colorSpace=self.ColorSpace, color=self.Crgb, allowGUI=True, fullscr=True, bpc=(self.depthBits, self.depthBits, self.depthBits), depthBits=self.depthBits) """stimulus features""" def patch_ref(self, theta, pos): # reference patches ref = visual.Circle(win=self.win, units='deg', pos=pos, radius=self.cfg['ref_size'], fillColorSpace=self.ColorSpace, lineColorSpace=self.ColorSpace) ref.fillColor = self.ColorPicker.newcolor(theta=theta)[1] ref.lineColor = ref.fillColor return ref def patch_stim(self, xlim, ylim): # standard and test stimuli n = int(np.sqrt(self.patch_nmb)) pos = [[x, y] for x in np.linspace(xlim[0], xlim[1], n) for y in np.linspace(ylim[0], ylim[1], n)] patch = visual.ElementArrayStim(win=self.win, units='deg', fieldSize=self.cfg['field_size'], xys=pos, nElements=self.patch_nmb, elementMask='circle', elementTex=None, sizes=self.cfg['patch_size'], colorSpace=self.ColorSpace) return patch """color noise & noise conditions""" def rand_color(self, theta, std, npatch): # generate color noise noise = np.random.normal(theta, std, npatch) color = [self.ColorPicker.newcolor(theta=n) for n in noise] sml, rgb = zip(*color) return sml, rgb def choose_con(self, standard, test, std): # choose noise condition sColor = None tColor = None if self.param['noise_condition'] == 'L-L': # low - low noise sColor = self.ColorPicker.newcolor(theta=standard)[1] tColor = self.ColorPicker.newcolor(theta=test)[1] elif self.param[ 'noise_condition'] == 'L-H': # low - high noise: only test stimulus has high noise sColor = self.ColorPicker.newcolor(theta=standard)[1] tColor = self.rand_color(test, std, self.patch_nmb)[1] elif self.param['noise_condition'] == 'H-H': # high - high noise sColor = self.rand_color(standard, std, self.patch_nmb)[1] tColor = self.rand_color(test, std, self.patch_nmb)[1] else: print("No noise condition corresponds to the input!") return sColor, tColor """tool fucntion""" def take_closest(self, arr, val): """ Assumes arr is sorted. Returns closest value to val (could be itself). If two numbers are equally close, return the smallest number. """ pos = bisect_left(arr, val) if pos == 0: return arr[0] if pos == len(arr): return arr[-1] before = arr[pos - 1] after = arr[pos] if after - val < val - before: return after else: return before """main experiment""" def run_trial(self, rot, cond, std, count): # set two reference leftRef = self.patch_ref(theta=cond['leftRef'], pos=self.cfg['leftRef.pos']) rightRef = self.patch_ref(theta=cond['rightRef'], pos=self.cfg['rightRef.pos']) # randomly assign patch positions: upper (+) or lower (-) patchpos = [self.cfg['standard.ylim'], self.cfg['test.ylim']] rndpos = patchpos.copy() np.random.shuffle(rndpos) sPatch = self.patch_stim(self.cfg['standard.xlim'], rndpos[0]) tPatch = self.patch_stim(self.cfg['test.xlim'], rndpos[1]) # set colors of two stimuli standard = cond['standard'] # standard should be fixed test = standard + rot sPatch.colors, tPatch.colors = self.choose_con(standard, test, std) # fixation cross fix = visual.TextStim(self.win, text="+", units='deg', pos=[0, 0], height=0.5, color='black', colorSpace=self.ColorSpace) # number of trial num = visual.TextStim(self.win, text="trial " + str(count), units='deg', pos=[12, -10], height=0.4, color='black', colorSpace=self.ColorSpace) trial_time_start = time.time() # first present references fix.draw() num.draw() leftRef.draw() rightRef.draw() self.win.flip() core.wait(1.0) # then present the standard and the test stimuli as well fix.draw() num.draw() leftRef.draw() rightRef.draw() sPatch.draw() tPatch.draw() self.win.flip() core.wait(self.trial_dur) fix.draw() self.win.flip() core.wait(0.2) # 0.2 sec gray background react_time_start = time.time() # refresh the window and show a colored checkerboard horiz_n = 30 vertic_n = 20 rect = visual.ElementArrayStim(self.win, units='norm', nElements=horiz_n * vertic_n, elementMask=None, elementTex=None, sizes=(2 / horiz_n, 2 / vertic_n), colorSpace=self.ColorSpace) rect.xys = [ (x, y) for x in np.linspace(-1, 1, horiz_n, endpoint=False) + 1 / horiz_n for y in np.linspace(-1, 1, vertic_n, endpoint=False) + 1 / vertic_n ] rect.colors = [ self.ColorPicker.newcolor(theta=x)[1] for x in np.random.randint(0, high=360, size=horiz_n * vertic_n) ] rect.draw() self.win.flip() core.wait(0.5) # 0.5 sec checkerboard judge = None react_time_stop = -1 kb = keyboard.Keyboard() get_keys = kb.getKeys(['right', 'left', 'escape' ]) # if response during the checkerboard if ('left' in get_keys and rot * rndpos[0][0] > 0) or ('right' in get_keys and rot * rndpos[0][0] < 0): judge = 1 # correct react_time_stop = time.time() elif ('left' in get_keys and rot * rndpos[0][0] < 0) or ('right' in get_keys and rot * rndpos[0][0] > 0): judge = 0 # incorrect react_time_stop = time.time() if 'escape' in get_keys: config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() self.win.flip() fix.draw() self.win.flip() if judge is None: # if response after the checkerboard for wait_keys in event.waitKeys(): if (wait_keys == 'left' and rot * rndpos[0][0] > 0) or ( wait_keys == 'right' and rot * rndpos[0][0] < 0): judge = 1 # correct react_time_stop = time.time() elif (wait_keys == 'left' and rot * rndpos[0][0] < 0) or ( wait_keys == 'right' and rot * rndpos[0][0] > 0): judge = 0 # incorrect react_time_stop = time.time() elif wait_keys == 'escape': config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() react_time = react_time_stop - react_time_start return judge, react_time, trial_time_start def run_session(self): path = os.path.join(self.res_dir, self.subject) if not os.path.exists(path): os.makedirs(path) psydat_path = os.path.join(path, 'psydat') if not os.path.exists(psydat_path): os.makedirs(psydat_path) # welcome msg = visual.TextStim(self.win, 'Welcome!' + '\n' + ' Press any key to start this session :)', color='black', units='deg', pos=(0, 0), height=0.8) msg.draw() self.win.mouseVisible = False self.win.flip() event.waitKeys() # read staircase parameters conditions = [ dict({'stimulus': key}, **value) for key, value in self.param.items() if key.startswith('stimulus') ] if conditions[0]['stairType'] == 'simple': stairs = data.MultiStairHandler(stairType='simple', conditions=conditions, nTrials=self.trial_nmb, method='sequential') elif conditions[0]['stairType'] == 'quest': stairs = [] for cond in conditions: if self.priors_file_path: prior_file = self.priors_file_path + cond[ 'label'] + '.psydat' print(prior_file) prior_handler = misc.fromFile(prior_file) else: prior_handler = None cur_handler = data.QuestHandler(cond['startVal'], cond['startValSd'], pThreshold=cond['pThreshold'], nTrials=self.trial_nmb, minVal=cond['min_val'], maxVal=cond['max_val'], staircase=prior_handler, extraInfo=cond, grain=0.02) stairs.append(cur_handler) elif conditions[0]['stairType'] == 'psi': stairs = [] for cond in conditions: if self.priors_file_path: prior_file = self.priors_file_path + cond['label'] + '.npy' else: prior_file = None print(prior_file) cur_handler = data.PsiHandler(nTrials=self.trial_nmb, intensRange=[1, 10], alphaRange=[1, 10], betaRange=[0.01, 10], intensPrecision=0.1, alphaPrecision=0.1, betaPrecision=0.01, delta=0.01, extraInfo=cond, prior=prior_file, fromFile=(prior_file is not None)) stairs.append(cur_handler) # write configuration files xpp = config_tools.WriteXpp(self.subject, self.idx) xpp_file = xpp.head(self.cfg_file, self.par_file) config_tools.write_xrl(self.subject, cfg_file=self.cfg_file, par_file=self.par_file, xpp_file=xpp_file) xlsname = path + '/' + self.idx + self.param[ 'noise_condition'] + '.xlsx' """ running staircase """ if isinstance(stairs, data.MultiStairHandler): # start running the staircase using the MultiStairHandler for the up-down method count = 0 for rot, cond in stairs: count += 1 direction = (-1)**(cond['label'].endswith('m') ) # direction as -1 if for minus stim rot = rot * direction # rotation for this trial judge, react_time, trial_time_start = self.run_trial( rot, cond, cond['std'], count) # check whether the theta is valid - if not, the rotation given by staircase should be corrected by # realizable values valid_theta = np.round(np.load(self.hue_list), decimals=1) disp_standard = self.take_closest( valid_theta, cond['standard']) # theta actually displayed stair_test = cond[ 'standard'] + stairs._nextIntensity * direction if stair_test < 0: stair_test += 360 disp_test = self.take_closest(valid_theta, stair_test) disp_intensity = disp_test - disp_standard if disp_intensity > 300: disp_intensity = (disp_test + disp_standard) - 360 stairs.addResponse(judge, abs(disp_intensity)) xpp.task(count, cond, rot, float(disp_intensity), judge, react_time, trial_time_start) if 'escape' in event.waitKeys(): config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() config_tools.write_xrl(self.subject, xls_file=xlsname) stairs.saveAsExcel(xlsname) # save results misc.toFile( os.path.join( psydat_path, self.idx + self.param['noise_condition'] + '.psydat'), stairs) elif isinstance(stairs, list): # start running the staircase using custom interleaving stairs for the quest and psi methods count = 0 rot_all = [] rot_all_disp = [] judge_all = [] estimates = {s.extraInfo['label']: [] for s in stairs} for trial_n in range(self.trial_nmb): for handler_idx, cur_handler in enumerate(stairs): count += 1 direction = (-1)**( cur_handler.extraInfo['label'].endswith('m') ) # direction as -1 if for minus stim if cur_handler._nextIntensity >= 10.0: sys.exit( "Hue difference is out of range! Please enlarge the testing range or take more training!" ) rot = cur_handler._nextIntensity * direction # rotation for this trial if trial_n >= 5: # avoid repeating an intensity more than 3 times last_rots = [ np.round(r, decimals=1) for r in [ rot_all_disp[handler_idx][trial_n - 1], rot_all_disp[handler_idx][trial_n - 2], rot_all_disp[handler_idx][trial_n - 3] ] ] last_resp = [ judge_all[handler_idx][trial_n - 1], judge_all[handler_idx][trial_n - 2], judge_all[handler_idx][trial_n - 3] ] if last_rots[0] == last_rots[1] == last_rots[2] \ and last_resp[0] == last_resp[1] == last_resp[2]: if cur_handler._nextIntensity > 0.5: rot = (cur_handler._nextIntensity - 0.5) * direction print('Intensity decreases by 0.5!') if cur_handler._nextIntensity <= 0.5: rot = (cur_handler._nextIntensity + 0.5) * direction print('Intensity increases by 0.5!') cond = cur_handler.extraInfo judge, react_time, trial_time_start = self.run_trial( rot, cond, cond['std'], count) if len(rot_all) <= handler_idx: rot_all.append([]) rot_all[handler_idx].append(rot) if len(judge_all) <= handler_idx: judge_all.append([]) judge_all[handler_idx].append(judge) valid_theta = np.round(np.load(self.hue_list), decimals=1) disp_standard = self.take_closest(valid_theta, cond['standard']) stair_test = cond[ 'standard'] + rot # calculated test hue for this trial if stair_test < 0: stair_test += 360 disp_test = self.take_closest( valid_theta, stair_test) # actual displayed test hue for this trial disp_intensity = disp_test - disp_standard # actual displayed hue difference if disp_intensity > 300: disp_intensity = (disp_test + disp_standard) - 360 cur_handler.addResponse( judge, abs(disp_intensity) ) # only positive number is accepted by addResponse if len(rot_all_disp ) <= handler_idx: # add displayed intensities rot_all_disp.append([]) rot_all_disp[handler_idx].append(disp_intensity) if isinstance(cur_handler, data.PsiHandler): estimates[cur_handler.extraInfo['label']].append([ cur_handler.estimateLambda()[0], # location cur_handler.estimateLambda768()[1], # slope cur_handler.estimateThreshold(0.75) ]) elif isinstance(cur_handler, data.QuestHandler): estimates[cur_handler.extraInfo['label']].append([ cur_handler.mean(), cur_handler.mode(), cur_handler.quantile(0.5) ]) xpp.task(count, cond, rot, disp_intensity, judge, react_time, trial_time_start) if 'escape' in event.waitKeys(): config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() config_tools.write_xrl(self.subject, xls_file=xlsname) # save results in xls-file workbook = xlsxwriter.Workbook(xlsname) for handler_idx, cur_handler in enumerate(stairs): worksheet = workbook.add_worksheet( cur_handler.extraInfo['label']) worksheet.write('A1', 'Reversal Intensities') worksheet.write('B1', 'Reversal Indices') worksheet.write('C1', 'All Intensities') worksheet.write('D1', 'All Responses') for i in range(len(rot_all[handler_idx])): # worksheet.write('C' + str(i + 2), rot_all[handler_idx][i]) worksheet.write('C' + str(i + 2), rot_all_disp[handler_idx][i]) worksheet.write('D' + str(i + 2), judge_all[handler_idx][i]) workbook.close() # print resulting parameters and estimates for each step res_file_path = os.path.join(path, self.idx + '_estimates.csv') res_writer = csv.writer(open(res_file_path, 'w')) for res_stim, res_vals in estimates.items(): for res_val_id, res_val in enumerate(res_vals): res_writer.writerow([ res_stim, res_val_id, res_val[0], res_val[1], res_val[2] ]) # save each handler into a psydat-file and save posterior into a numpy-file for cur_handler in stairs: file_name = os.path.join( psydat_path, self.idx + self.param['noise_condition'] + cur_handler.extraInfo['label']) misc.toFile(file_name + '.psydat', cur_handler) if isinstance(cur_handler, data.PsiHandler): cur_handler.savePosterior(file_name + '.npy')
class Exp: """ Class for performing the experiment. """ def __init__(self, subject, par_file, cfg_file, res_dir, priors_file_path): self.subject = subject self.idx = time.strftime("%Y%m%dT%H%M", time.localtime()) # add the current date if not res_dir: res_dir = 'data/' self.res_dir = res_dir self.priors_file_path = priors_file_path self.cfg_file = cfg_file self.cfg = config_tools.read_yml(cfg_file) self.par_file = par_file self.param = config_tools.read_yml(par_file) self.patch_nmb = self.cfg['patch_nmb'] self.trial_nmb = self.cfg['trial_nmb'] self.trial_dur = self.cfg['trial_dur'] self.depthBits = self.cfg['depthBits'] self.ColorPicker = ColorPicker(c=self.param['c'], sscale=self.param['sscale'], unit='deg', depthBits=self.depthBits, subject=self.subject) self.ColorSpace = self.ColorPicker.colorSpace hue_list_path = 'config/colorlist/' + self.subject if not os.path.exists(hue_list_path): self.ColorPicker.gencolorlist(0.2) self.hue_list = hue_list_path + '/hue-list-10bit-res0.2-sub-' + self.subject + '.npy' self.Csml = self.ColorPicker.center() self.Crgb = self.ColorPicker.sml2rgb(self.ColorPicker.center()) self.mon = monitors.Monitor(name=self.cfg['monitor']['name'], width=self.cfg['monitor']['width'], distance=self.cfg['monitor']['distance']) self.mon.setSizePix((self.cfg['monitor']['size'])) self.win = visual.Window(monitor=self.mon, unit='deg', colorSpace=self.ColorSpace, color=self.Crgb, allowGUI=True, fullscr=True, bpc=(self.depthBits, self.depthBits, self.depthBits), depthBits=self.depthBits) """stimulus features""" def patch_ref(self, theta, pos): # reference patches ref = visual.Circle(win=self.win, units='deg', pos=pos, radius=self.cfg['ref_size'], fillColorSpace=self.ColorSpace, lineColorSpace=self.ColorSpace) ref.fillColor = self.ColorPicker.newcolor(theta=theta)[1] ref.lineColor = ref.fillColor return ref def patch_stim(self, xlim, ylim): # standard and test stimuli n = int(np.sqrt(self.patch_nmb)) pos = [[x, y] for x in np.linspace(xlim[0], xlim[1], n) for y in np.linspace(ylim[0], ylim[1], n)] patch = visual.ElementArrayStim(win=self.win, units='deg', fieldSize=self.cfg['field_size'], xys=pos, nElements=self.patch_nmb, elementMask='circle', elementTex=None, sizes=self.cfg['patch_size'], colorSpace=self.ColorSpace) return patch """color noise & noise conditions""" def rand_color(self, theta, std, npatch): # generate color noise noise = np.random.normal(theta, std, npatch) color = [self.ColorPicker.newcolor(theta=n) for n in noise] sml, rgb = zip(*color) return sml, rgb def choose_con(self, standard, test, std): # choose noise condition sColor = None tColor = None if self.param['noise_condition'] == 'L-L': # low - low noise sColor = self.ColorPicker.newcolor(theta=standard)[1] tColor = self.ColorPicker.newcolor(theta=test)[1] elif self.param[ 'noise_condition'] == 'L-H': # low - high noise: only test stimulus has high noise sColor = self.ColorPicker.newcolor(theta=standard)[1] tColor = self.rand_color(test, std, self.patch_nmb)[1] elif self.param['noise_condition'] == 'H-H': # high - high noise sColor = self.rand_color(standard, std, self.patch_nmb)[1] tColor = self.rand_color(test, std, self.patch_nmb)[1] else: print("No noise condition corresponds to the input!") return sColor, tColor """tool fucntion""" def take_closest(self, arr, val): """ Assumes arr is sorted. Returns closest value to val (could be itself). If two numbers are equally close, return the smallest number. """ pos = bisect_left(arr, val) if pos == 0: return arr[0] if pos == len(arr): return arr[-1] before = arr[pos - 1] after = arr[pos] if after - val < val - before: return after else: return before """main experiment""" def run_trial(self, rot, cond, count): ref = self.patch_ref(theta=cond['ref'], pos=self.cfg['ref.pos']) # randomly assign patch positions: upper (+) or lower (-) patchpos = [self.cfg['standard.ylim'], self.cfg['test.ylim']] rndpos = patchpos.copy() np.random.shuffle(rndpos) sPatch = self.patch_stim(self.cfg['standard.xlim'], rndpos[0]) tPatch = self.patch_stim(self.cfg['test.xlim'], rndpos[1]) # set colors of two stimuli standard = cond['standard'] # standard should be fixed test = standard + rot sPatch.colors, tPatch.colors = self.choose_con(standard, test, cond['std']) # fixation cross fix = visual.TextStim(self.win, text="+", units='deg', pos=[0, 0], height=0.5, color='black', colorSpace=self.ColorSpace) # number of trial num = visual.TextStim(self.win, text="trial " + str(count), units='deg', pos=[12, -10], height=0.4, color='black', colorSpace=self.ColorSpace) trial_time_start = time.time() # present the standard and the test stimuli as well fix.draw() num.draw() ref.draw() sPatch.draw() tPatch.draw() self.win.flip() core.wait(self.trial_dur) fix.draw() self.win.flip() core.wait(0.2) # 0.2 sec gray background react_time_start = time.time() # refresh the window and show a colored checkerboard horiz_n = 30 vertic_n = 20 rect = visual.ElementArrayStim(self.win, units='norm', nElements=horiz_n * vertic_n, elementMask=None, elementTex=None, sizes=(2 / horiz_n, 2 / vertic_n), colorSpace=self.ColorSpace) rect.xys = [ (x, y) for x in np.linspace(-1, 1, horiz_n, endpoint=False) + 1 / horiz_n for y in np.linspace(-1, 1, vertic_n, endpoint=False) + 1 / vertic_n ] rect.colors = [ self.ColorPicker.newcolor(theta=x)[1] for x in np.random.randint(0, high=360, size=horiz_n * vertic_n) ] rect.draw() self.win.flip() core.wait(0.5) # 0.5 sec checkerboard judge = None react_time_stop = -1 kb = keyboard.Keyboard() get_keys = kb.getKeys(['up', 'down', 'escape' ]) # if response during the checkerboard if ('up' in get_keys and rndpos[1][0] > 0) or ('down' in get_keys and rndpos[1][0] < 0): judge = 1 # correct react_time_stop = time.time() elif ('up' in get_keys and rndpos[1][0] < 0) or ('down' in get_keys and rndpos[1][0] > 0): judge = 0 # incorrect react_time_stop = time.time() if 'escape' in get_keys: config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() self.win.flip() fix.draw() self.win.flip() if judge is None: # if response after the checkerboard for wait_keys in event.waitKeys(): if (wait_keys == 'up' and rndpos[1][0] > 0) or (wait_keys == 'down' and rndpos[1][0] < 0): judge = 1 # correct react_time_stop = time.time() elif (wait_keys == 'up' and rndpos[1][0] < 0) or (wait_keys == 'down' and rndpos[1][0] > 0): judge = 0 # incorrect react_time_stop = time.time() elif wait_keys == 'escape': config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() react_time = react_time_stop - react_time_start return judge, react_time, trial_time_start def run_session(self): path = os.path.join(self.res_dir, self.subject) if not os.path.exists(path): os.makedirs(path) psydat_path = os.path.join(path, 'psydat') if not os.path.exists(psydat_path): os.makedirs(psydat_path) # welcome msg = visual.TextStim(self.win, 'Welcome!' + '\n' + ' Press any key to start this session :)', color='black', units='deg', pos=(0, 0), height=0.8) msg.draw() self.win.mouseVisible = False self.win.flip() event.waitKeys() # read staircase parameters conditions = [ dict({'stimulus': key}, **value) for key, value in self.param.items() if key.startswith('stimulus') ] if conditions[0]['stairType'] == 'constant': stimuli = [] rot_all_disp = [] judge_all = [] for cond in conditions: for diff in np.linspace(cond['minVal'], 0, 3, endpoint=False): stimuli.append({'cond': cond, 'diff': diff}) for diff in np.linspace(0, cond['maxVal'], 3, endpoint=False): stimuli.append({'cond': cond, 'diff': diff}) stimuli.append({'cond': cond, 'diff': 0}) repeats_nmb = 20 stairs = data.TrialHandler(stimuli, repeats_nmb, method='random') else: sys.exit("The stimuli are not constant!") # write configuration files xpp = config_tools.WriteXpp(self.subject, self.idx) xpp_file = xpp.head(self.cfg_file, self.par_file) config_tools.write_xrl(self.subject, cfg_file=self.cfg_file, par_file=self.par_file, xpp_file=xpp_file) xlsname = path + '/' + self.idx + self.param[ 'noise_condition'] + '.xlsx' """ running staircase """ if isinstance(stairs, data.TrialHandler): count = 0 results = {cond['label']: [] for cond in conditions} for trial in stairs: count += 1 judge, react_time, trial_time_start = self.run_trial( trial['diff'], trial['cond'], count) valid_theta = np.round(np.load(self.hue_list), decimals=1) disp_standard = self.take_closest(valid_theta, cond['standard']) stair_test = cond['standard'] + trial['diff'] if stair_test < 0: stair_test += 360 disp_test = self.take_closest(valid_theta, stair_test) disp_intensity = disp_test - disp_standard if disp_intensity > 300: disp_intensity = (disp_test + disp_standard) - 360 xpp.task(count, cond, cond['diff'], float(disp_intensity), judge, react_time, trial_time_start) results[trial['label']].append((trial['diff'], judge)) if 'escape' in event.waitKeys(): config_tools.write_xrl(self.subject, break_info='userbreak') core.quit() config_tools.write_xrl(self.subject, xls_file=xlsname) # save results in xls-file workbook = xlsxwriter.Workbook(xlsname) for res_label, res_data in results.items(): worksheet = workbook.add_worksheet(res_label) worksheet.write('A1', 'Reversal Intensities') worksheet.write('B1', 'Reversal Indices') worksheet.write('C1', 'All Intensities') worksheet.write('D1', 'All Responses') for i, (res_diff, res_resp) in enumerate(res_data): worksheet.write('C' + str(i + 2), res_diff) worksheet.write('D' + str(i + 2), res_resp) workbook.close() else: sys.exit("The stimuli are not constant!")