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)
Пример #2
0
def run_scrsaver(depthBits):
    mon = monitors.Monitor(name='VIEWPixx LITE', width=38, distance=57)
    mon.setSizePix((1920, 1200))
    mon.save()  # if the monitor info is not saved

    win = visual.Window(fullscr=True,
                        mouseVisible=False,
                        bpc=(depthBits, depthBits, depthBits),
                        depthBits=depthBits,
                        monitor=mon)
    kb = keyboard.Keyboard()

    if depthBits == 8:
        colorSpace = 'rgb255'
    elif depthBits == 10:
        colorSpace = 'rgb'
    else:
        raise ValueError("Invalid color depth!")

    while True:
        num = np.random.randint(5, high=10)
        rect = visual.ElementArrayStim(win,
                                       units='norm',
                                       nElements=num**2,
                                       elementMask=None,
                                       elementTex=None,
                                       sizes=(2 / num, 2 / num),
                                       colorSpace=colorSpace)
        rect.xys = [(x, y)
                    for x in np.linspace(-1, 1, num, endpoint=False) + 1 / num
                    for y in np.linspace(-1, 1, num, endpoint=False) + 1 / num]

        rect.colors = [
            ColorPicker(depthBits=depthBits, unit='deg').newcolor(theta=x)[1]
            for x in np.random.randint(0, high=360, size=num**2)
        ]
        rect.draw()
        win.mouseVisible = False
        win.flip()

        kb.clock.reset()
        if kb.getKeys():  # press any key to quit
            core.quit()
        else:
            time.sleep(3)  # change every 3 sec
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')
Пример #4
0
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()
Пример #5
0
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!")