Exemplo n.º 1
0
    def __init__(self, id, finger, trial_table):
        ShowBase.__init__(self)
        IndividuationStateMachine.__init__(self)
        props = WindowProperties()
        props.setTitle('Individuation Demo')
        self.win.requestProperties(props)
        self.render.setAntialias(AntialiasAttrib.MMultisample)
        self.render.setShaderAuto()  # allows shadows
        self.setFrameRateMeter(True)

        self.dev = MpI(Hand, clock=mono_clock.get_time)
        self.finger = int(finger) * 3  # offset
        self.f2 = int(finger)
        # self.disableMouse()
        self.countdown_timer = CountdownTimer()

        self.table = pd.read_table(trial_table)  # trial table
        self.setup_lights()
        self.setup_camera()
        self.load_models()
        self.load_audio()

        # add tasks (run every frame)
        taskMgr.add(self.get_user_input, 'move')
        taskMgr.add(self.update_target_color, 'target_color')
        taskMgr.add(self.update_state, 'update_state')
        taskMgr.add(self.update_feedback_bar, 'update_feedback_bar')
        self.accept('space', self.space_on)  # toggle a boolean somewhere

        # helpers
        self.space = False
        self.trial_counter = 0
        self.dist = 100
        self.queue = list()
        self.med_data = None
        self.noise = 0.0
 def setup_input(self):
     keys = list(self.static_settings['key_options'])
     self.keys = keys[:int(self.static_settings['n_choices'])]
     self.device = MpI(Keyboard, keys=keys, clock=self.global_clock.getTime)
     self.keyboard_state = [False] * len(keys)
class Practice(StateMachine):
    def __init__(self, settings=None, generator=None, static_settings=None):
        super(Practice, self).__init__()
        self.settings = settings
        self.trial_generator = generator
        self.static_settings = static_settings

        self.global_clock = mono_clock
        self.feedback_timer = core.CountdownTimer()  # feedback duration
        self.post_timer = core.CountdownTimer()  # intertrial pause
        # how long key must be released before allowing continue
        self.release_timer = core.CountdownTimer()

        self.subject_rng = np.random.RandomState(
            seed=int(self.settings['subject']))

        self.setup_data()
        self.setup_window()
        self.setup_visuals()
        self.setup_input()
        self.setup_audio()

        self.frame_period = self.win.monitorFramePeriod
        self.this_trial_choice = None
        self.this_trial_rt = None
        self.trial_start = None
        self.first_press = None
        self.first_rt = None
        self.trial_counter = 0
        self.any_pressed = False
        self.already_released = False
        self.stim_onset = None
        self.t_feedback = 0
        self.valid_presses = []
        self.pause_presses = []

    def setup_data(self):
        # figure out the subject-specific remaps
        all_switch_hands = list(itertools.product(range(0, 5), range(5, 10)))
        homologous = [(0, 9), (1, 8), (2, 7), (3, 6), (4, 5)]
        heterologous = all_switch_hands.copy()
        for i in heterologous:
            if i in homologous:
                heterologous.remove(i)
        same_hand_l = list(
            itertools.product(list(range(0, 5)), list(range(0, 5))))
        same_hand_l = [(i, j) for i, j in same_hand_l if i != j]
        same_hand_r = [(i + 5, j + 5) for i, j in same_hand_l]
        same_hand = same_hand_l + same_hand_r

        # choose one homologous, heterologous, same hand
        self.subject_rng = np.random.RandomState(
            seed=int(self.settings['subject']))
        hom_choice = self.subject_rng.choice(len(homologous))
        hom_pair = homologous[hom_choice]
        # make sure we can't pick an already engaged finger
        het_subset = [
            i for i in heterologous if not set(i).intersection(hom_pair)
        ]
        het_choice = self.subject_rng.choice(len(het_subset))
        het_pair = het_subset[het_choice]
        same_hand_subset = [
            i for i in same_hand if not set(i).intersection(hom_pair)
            and not set(i).intersection(het_pair)
        ]
        same_hand_choice = self.subject_rng.choice(len(same_hand_subset))
        same_hand_pair = same_hand_subset[same_hand_choice]

        self.all_swaps = [hom_pair] + [het_pair] + [same_hand_pair]
        self.settings.update({'swap_pairs': self.all_swaps})

        #
        self.start_datetime = datetime.now().strftime('%y%m%d_%H%M%S')
        self.data_path = os.path.join(self.static_settings['root'],
                                      self.settings['subject'],
                                      self.start_datetime)
        self.settings.update({'datetime': self.start_datetime})

        # log from psychopy (to log here, call something like `logging.warning('bad thing')`)
        if not os.path.exists(self.data_path):
            os.makedirs(self.data_path)
        self.logfile = logging.LogFile(os.path.join(self.data_path,
                                                    'psychopy_log.log'),
                                       filemode='w',
                                       level=logging.INFO)

        # log block-level settings
        with open(os.path.join(self.data_path, 'block_settings.json'),
                  'w') as f:
            json.dump(self.settings, f)
        with open(os.path.join(self.data_path, 'static_block_settings.json'),
                  'w') as f:
            json.dump(self.static_settings, f)

        # trial-specific data (will be serialized via json.dump)
        self.trial_data = {
            'index': None,
            'real_prep_time': None,
            'proposed_choice': None,
            'real_choice': None,
            'correct': None,
            'datetime': None,
            'presses': [],
            'rts': [],
            'will_remap': None,
            'is_remapped': None,
            'remapped_from': None,
            'stim_val': None
        }

    def setup_window(self):
        self.win = visual.Window(size=(800, 800),
                                 pos=(0, 0),
                                 fullscr=self.static_settings['fullscreen'],
                                 screen=1,
                                 units='height',
                                 allowGUI=False,
                                 colorSpace='rgb',
                                 color=-1.0)
        self.win.recordFrameIntervals = True
        # bump up the number of dropped frames reported
        visual.window.reportNDroppedFrames = 50

    def setup_visuals(self):
        self.targets = list()
        # all possible targets
        tmp = list(self.static_settings['symbol_options'])

        # compute per-person mapping
        self.subject_rng = np.random.RandomState(
            seed=int(self.settings['subject']))
        self.subject_rng.shuffle(tmp)
        tmp = ''.join(tmp)  # convert from list to string
        self.settings.update({'reordered_symbols': tmp})

        tmp = tmp[:int(self.static_settings['n_choices'])]
        if self.settings['stim_type'] == 'symbol':
            for i in tmp:
                self.targets.append(
                    visual.TextStim(self.win,
                                    i,
                                    height=0.25,
                                    autoLog=True,
                                    font='FreeMono',
                                    name='stim ' + i))
        elif self.settings['stim_type'] == 'letter':
            for i in list(self.static_settings['key_options']):
                self.targets.append(
                    visual.TextStim(self.win,
                                    i,
                                    height=0.25,
                                    autoLog=True,
                                    font='FreeMono',
                                    name='stim ' + i))
        elif self.settings['stim_type'] == 'hand':
            right_hand = visual.ImageStim(self.win,
                                          image='media/hand.png',
                                          size=(0.3, 0.3),
                                          pos=(0.14, 0))
            left_hand = visual.ImageStim(self.win,
                                         image='media/hand.png',
                                         size=(0.3, 0.3),
                                         pos=(-0.14, 0),
                                         flipHoriz=True)
            self.background = visual.BufferImageStim(
                self.win, stim=[left_hand, right_hand])
            # pinky, ring, middle, index, thumb
            pos_l = [[-0.255, 0.0375], [-0.2075, 0.08875], [-0.1575, 0.1125],
                     [-0.095, 0.09], [-0.03, -0.0075]]
            pos_r = [[-x, y] for x, y in pos_l]
            pos_r.reverse()
            pos_l.extend(pos_r)
            pos_l = pos_l[:int(self.static_settings['n_choices'])]

            self.targets = [
                visual.Circle(self.win,
                              fillColor=(1, 1, 1),
                              pos=x,
                              size=0.03,
                              opacity=1.0,
                              name='stim %d' % c) for c, x in enumerate(pos_l)
            ]
        else:
            raise ValueError('Unknown stimulus option...')

        if self.settings['remap']:
            # remap heterologous, homologous, and same-finger pairs?
            # swap the stimuli
            for i, j in self.all_swaps:
                self.targets[j], self.targets[i] = self.targets[
                    i], self.targets[j]

        # push feedback
        self.push_feedback = visual.Rect(self.win,
                                         width=0.6,
                                         height=0.6,
                                         lineWidth=3,
                                         name='push_feedback',
                                         autoLog=False)

        # text
        self.instruction_text = visual.TextStim(self.win,
                                                text='Press a key to start.',
                                                pos=(0, 0),
                                                units='norm',
                                                color=(1, 1, 1),
                                                height=0.1,
                                                alignHoriz='center',
                                                alignVert='center',
                                                name='wait_text',
                                                autoLog=False,
                                                wrapWidth=2)
        self.instruction_text.autoDraw = True

        self.pause_text = visual.TextStim(self.win,
                                          text=u'Take a break!',
                                          pos=(0, 0.8),
                                          units='norm',
                                          color=(1, 1, 1),
                                          height=0.1,
                                          alignHoriz='center',
                                          alignVert='center',
                                          autoLog=True,
                                          name='pause_text')
        self.pause_text2 = visual.TextStim(
            self.win,
            text=u'Press ten times to continue.',
            pos=(0, 0.7),
            units='norm',
            color=(1, 1, 1),
            height=0.1,
            alignHoriz='center',
            alignVert='center',
            autoLog=True,
            name='pause_text')

    def setup_audio(self):
        self.coin = sound.Sound('sounds/coin.wav', stereo=True)
        self._play_reward()

    def _play_reward(self):
        self.coin.play()

    def setup_input(self):
        keys = list(self.static_settings['key_options'])
        self.keys = keys[:int(self.static_settings['n_choices'])]
        self.device = MpI(Keyboard, keys=keys, clock=self.global_clock.getTime)
        self.keyboard_state = [False] * len(keys)

    # State-specific methods
    # wait state
    # conditions
    def wait_for_press(self):
        return self.any_pressed

    # after
    def remove_instruction_text(self):
        self.instruction_text.autoDraw = False

    def add_fix_n_feedback(self):
        # always keep hands on if we're doing the hand stimuli
        if self.settings['stim_type'] == 'hand':
            self.background.autoDraw = True
        self.push_feedback.autoDraw = True

    # pretrial state
    # conditions
    def wait_for_release(self):
        tmp = not self.any_pressed
        if tmp and not self.already_released:
            self.release_timer.reset(0.3)
            self.already_released = True
        return tmp

    def wait_n_ms_after_release(self):
        return self.release_timer.getTime() <= 0

    # after
    def get_next_rt_n_resp(self):
        self.this_trial_rt, self.this_trial_choice = self.trial_generator.next(
        )

    def sched_record_trial_start(self):
        self.win.callOnFlip(self._get_trial_start)

    def _get_trial_start(self):
        self.trial_start = self.win.lastFrameT
        logging.info('Trial %d start: %f' %
                     (self.trial_counter, self.trial_start))

    def first_press_reset(self):
        self.first_press = None
        self.first_rt = None
        self.trial_data['presses'] = []
        self.trial_data['rts'] = []
        self.valid_presses = []

    def add_stim(self):
        self.targets[int(self.this_trial_choice)].autoDraw = True
        self.win.callOnFlip(self.record_stim_time)

    # enter_trial state
    # conditions
    def wait_for_trial_press(self):
        if self.valid_presses:
            return True
        return False

    def record_stim_time(self):
        self.stim_onset = self.win.lastFrameT - self.trial_start
        logging.exp('Drew image %d at %f' %
                    (int(self.this_trial_choice), self.stim_onset))

    # after
    def add_feedback(self):
        # draw feedback (text, change colors...)
        # if not too slow, red or green stimulus (incorrect or correct)
        # if too slow, show text
        if self.valid_presses:
            correct = self.valid_presses[-1] == int(self.this_trial_choice)
            if correct:
                if self.settings['stim_type'] != 'hand':
                    self.targets[int(
                        self.this_trial_choice)].color = [-1, 1, -1]
                else:
                    self.targets[int(
                        self.this_trial_choice)].fillColor = [-1, 1, -1]
                self._play_reward()
                self.t_feedback = 0.3
            else:
                if self.settings['stim_type'] != 'hand':
                    self.targets[int(
                        self.this_trial_choice)].color = [1, -1, -1]
                else:
                    self.targets[int(
                        self.this_trial_choice)].fillColor = [1, -1, -1]
                self.t_feedback = 1.0

    def start_feedback_timer(self):
        self.feedback_timer.reset(self.t_feedback)

    # feedback state
    # conditions
    def feedback_timer_passed(self):
        return self.feedback_timer.getTime() - self.frame_period <= 0

    def is_choice_correct(self):
        return self.t_feedback < 0.5
        # return self.trial_data['presses'][-1] == self.this_trial_choice

    def is_choice_incorrect(self):
        was_incorrect = self.t_feedback > 0.5
        if was_incorrect:
            self.valid_presses = []
        return was_incorrect

    # after
    def remove_stim(self):
        self.targets[int(self.this_trial_choice)].autoDraw = False

    def remove_feedback(self):
        if self.settings['stim_type'] != 'hand':
            self.targets[int(self.this_trial_choice)].color = [1, 1, 1]
        else:
            self.targets[int(self.this_trial_choice)].fillColor = [1, 1, 1]
        self.valid_presses = []

    def start_post_timer(self):
        self.post_timer.reset(0.2)

    def record_data(self):
        now = datetime.now().strftime('%y%m%d_%H%M%S%f')
        self.trial_data['index'] = self.trial_counter
        self.trial_data['real_prep_time'] = float(
            self.first_rt - self.stim_onset) if self.first_rt else None
        self.trial_data['proposed_choice'] = int(
            self.this_trial_choice
        ) if self.this_trial_choice is not None else None
        self.trial_data['real_choice'] = int(
            self.first_press) if self.first_press is not None else None
        self.trial_data['correct'] = bool(self.first_press == int(
            self.this_trial_choice)) if self.first_press is not None else False
        self.trial_data['datetime'] = now
        self.trial_data['presses'] = list(self.trial_data['presses'])
        self.trial_data['rts'] = [
            x - self.stim_onset for x in self.trial_data['rts']
        ]
        # remapping things
        self.trial_data['will_remap'] = any(
            self.trial_data['proposed_choice'] in r for r in self.all_swaps)
        self.trial_data['is_remapped'] = self.trial_data[
            'will_remap'] and self.settings['remap']
        if self.trial_data['is_remapped']:
            pair = [r for r in self.all_swaps if self.this_trial_choice in r
                    ][0]  # should only be one list
            pair = list(pair)
            pair.remove(self.this_trial_choice)
        self.trial_data['remapped_from'] = int(
            pair[0]) if self.trial_data['is_remapped'] else None
        if self.settings['stim_type'] == 'hand':
            x = str(self.this_trial_choice)
        else:
            x = self.targets[int(self.this_trial_choice)].text
        #elif self.settings['stim_type'] == 'symbol':
        #    x = hex(ord(self.targets[int(self.this_trial_choice)].text))
        self.trial_data['stim_val'] = x
        #pp.pprint(self.trial_data)
        trial_name = 'trial' + str(self.trial_counter) + '_summary.json'
        with open(os.path.join(self.data_path, trial_name), 'w') as f:
            json.dump(self.trial_data, f)
        for k in self.trial_data:
            self.trial_data[k] = None
        self.trial_data['presses'] = []
        self.trial_data['rts'] = []

    def increment_trial_counter(self):
        self.trial_counter += 1

    def update_rt_gen(self):
        self.trial_generator.update(self.first_press, self.first_rt)

    def reset_release_flag(self):
        self.already_released = False

    # post_trial state
    # conditions
    def stopping_conditions(self):
        return self.trial_generator.should_terminate()

    # conditions, part 2
    def post_timer_passed(self):
        return self.post_timer.getTime() - self.frame_period <= 0

    # conditions, part 3
    def mult_of_100_passed(self):
        return self.trial_counter % 100 == 0

    def reset_pause_press_list(self):
        self.pause_presses = []

    def ten_keys_pressed(self):
        lpp = len(self.pause_presses)
        self.pause_text2.text = 'Press %d times to continue.' % (10 - lpp)
        return lpp >= 10

    def draw_pause_text(self):
        self.pause_text.autoDraw = True
        self.pause_text2.autoDraw = True

    def rm_pause_text(self):
        self.pause_text.autoDraw = False
        self.pause_text2.autoDraw = False

    def input(self):
        timestamp, data = self.device.read()
        if timestamp is not None:
            for i, j in zip(data[0], data[1]):
                self.keyboard_state[j[0]] = i[0]
            self.any_pressed = any(self.keyboard_state)
            if not self.first_press and self.any_pressed and self.trial_start:
                self.first_press = data[1][0][0]
                self.first_rt = (timestamp - self.trial_start)[0]
            if self.trial_start:
                for i in range(len(data[0][0])):
                    if data[0][0][i]:
                        self.trial_data['presses'].append(int(data[1][0][i]))
                        self.trial_data['rts'].append(
                            float(timestamp[i] - self.trial_start))
                        self.valid_presses.append(int(data[1][0][i]))
            if self.state == 'wait_till_10_pressed':
                for i in range(len(data[0][0])):
                    if data[0][0][i]:
                        self.pause_presses.append(int(data[1][0][i]))

    def draw_input(self):
        self.push_feedback.lineColor = [0, 0, 0
                                        ] if self.any_pressed else [1, 1, 1]
Exemplo n.º 4
0
    def __init__(self, settings=None):

        super(TwoChoice, self).__init__()
        # clocks and timers
        self.global_clock = mono_clock
        # gives us the time until the end of the trial (counts down)
        self.trial_timer = core.CountdownTimer()
        # gives time that feedback shows (counts down)
        self.feedback_timer = core.CountdownTimer()
        # gives time between trials (counts down)
        self.post_timer = core.CountdownTimer()

        # trial table
        try:
            self.trial_table = pd.read_csv(settings['trial_table'])
        except FileNotFoundError:
            core.quit()

        self.win = visual.Window(size=(800, 800),
                                 pos=(0, 0),
                                 fullscr=settings['fullscreen'],
                                 screen=1,
                                 units='height',
                                 allowGUI=False,
                                 colorSpace='rgb',
                                 color=(-1, -1, -1))
        self.win.recordFrameIntervals = True

        self.setup_visuals()  # decouple for the sake of the other exp
        # audio
        tmp = beep_sequence(click_freq=(523.251, 659.255, 783.991, 1046.5),
                            inter_click_interval=0.4,
                            num_clicks=4,
                            dur_clicks=0.04)
        self.last_beep_time = round(0.1 + (0.4 * 3), 2)

        self.beep = sound.Sound(tmp, blockSize=16, hamming=False)
        # TODO: check bug in auto-config of sounddevice (stereo = -1)
        self.coin = sound.Sound('media/coin.wav', stereo=True)
        # Input device
        if settings['forceboard']:
            self.device = MultiprocessInput(ForceTransducers,
                                            clock=self.global_clock.getTime)
        else:
            keys = 'awefvbhuil'
            self.device = MultiprocessInput(Keyboard,
                                            keys=list(keys),
                                            clock=self.global_clock.getTime)
            self.keyboard_state = [False] * 10
        # by-trial data
        data_path = 'data/' + settings['subject'] + '/'
        if not op.exists(data_path):
            os.makedirs(data_path)
        # copy the trial table to the data folder
        adaptstring = '_adapt' if settings['adaptive'] else ''
        shutil.copyfile(settings['trial_table'],
                        data_path + op.basename(settings['trial_table']))
        self.summary_file_name = data_path + 'id_' + settings['subject'] + '_' + \
            op.splitext(op.basename(settings['trial_table']))[0] + \
            adaptstring + dt.now().strftime('_%H%M%S') + '.csv'
        self.csv_header = [
            'index', 'subject', 'first_target', 'second_target',
            'real_switch_time', 'first_press', 'first_press_time', 'correct',
            'prep_time'
        ]
        with open(self.summary_file_name, 'w') as f:
            writer = csv.DictWriter(f,
                                    fieldnames=self.csv_header,
                                    lineterminator='\n')
            writer.writeheader()

        self.trial_data = {
            'index': np.nan,
            'subject': settings['subject'],
            'first_target': np.nan,
            'second_target': np.nan,
            'real_switch_time': np.nan,
            'first_press': np.nan,
            'first_press_time': np.nan,
            'correct': np.nan,
            'prep_time': np.nan
        }

        # extras
        self.frame_period = self.win.monitorFramePeriod
        self.trial_start = 0
        self.trial_counter = 0  # start at zero b/c zero indexing
        self.trial_input_buffer = np.full((600, 10), np.nan)
        self.trial_input_time_buffer = np.full((600, 1), np.nan)
        self.first_press = np.nan
        self.first_press_time = np.nan
        self.left_val = self.trial_table[['first', 'second']].min(axis=0).min()
        self.right_val = self.trial_table[['first',
                                           'second']].max(axis=0).max()
        self.device_on = False
        self.correct_answer = False

        # things related to the adaptive version
        self.adaptive = settings['adaptive']
        self.sign_switch_count = 0  # number of times the correctness switched
        self.curr_sign = False
        self.prev_sign = False
        self.curr_prep_time = 0.5  # start at 500ms
Exemplo n.º 5
0
class TwoChoice(StateMachine):
    """
    `sched_` denotes things that are scheduled to coincide w/ the frame buffer flip
    Remember to account for lag in drawing (plan on drawing ~ 1 frame (e.g. 15ms) before onset)

    """
    def __init__(self, settings=None):

        super(TwoChoice, self).__init__()
        # clocks and timers
        self.global_clock = mono_clock
        # gives us the time until the end of the trial (counts down)
        self.trial_timer = core.CountdownTimer()
        # gives time that feedback shows (counts down)
        self.feedback_timer = core.CountdownTimer()
        # gives time between trials (counts down)
        self.post_timer = core.CountdownTimer()

        # trial table
        try:
            self.trial_table = pd.read_csv(settings['trial_table'])
        except FileNotFoundError:
            core.quit()

        self.win = visual.Window(size=(800, 800),
                                 pos=(0, 0),
                                 fullscr=settings['fullscreen'],
                                 screen=1,
                                 units='height',
                                 allowGUI=False,
                                 colorSpace='rgb',
                                 color=(-1, -1, -1))
        self.win.recordFrameIntervals = True

        self.setup_visuals()  # decouple for the sake of the other exp
        # audio
        tmp = beep_sequence(click_freq=(523.251, 659.255, 783.991, 1046.5),
                            inter_click_interval=0.4,
                            num_clicks=4,
                            dur_clicks=0.04)
        self.last_beep_time = round(0.1 + (0.4 * 3), 2)

        self.beep = sound.Sound(tmp, blockSize=16, hamming=False)
        # TODO: check bug in auto-config of sounddevice (stereo = -1)
        self.coin = sound.Sound('media/coin.wav', stereo=True)
        # Input device
        if settings['forceboard']:
            self.device = MultiprocessInput(ForceTransducers,
                                            clock=self.global_clock.getTime)
        else:
            keys = 'awefvbhuil'
            self.device = MultiprocessInput(Keyboard,
                                            keys=list(keys),
                                            clock=self.global_clock.getTime)
            self.keyboard_state = [False] * 10
        # by-trial data
        data_path = 'data/' + settings['subject'] + '/'
        if not op.exists(data_path):
            os.makedirs(data_path)
        # copy the trial table to the data folder
        adaptstring = '_adapt' if settings['adaptive'] else ''
        shutil.copyfile(settings['trial_table'],
                        data_path + op.basename(settings['trial_table']))
        self.summary_file_name = data_path + 'id_' + settings['subject'] + '_' + \
            op.splitext(op.basename(settings['trial_table']))[0] + \
            adaptstring + dt.now().strftime('_%H%M%S') + '.csv'
        self.csv_header = [
            'index', 'subject', 'first_target', 'second_target',
            'real_switch_time', 'first_press', 'first_press_time', 'correct',
            'prep_time'
        ]
        with open(self.summary_file_name, 'w') as f:
            writer = csv.DictWriter(f,
                                    fieldnames=self.csv_header,
                                    lineterminator='\n')
            writer.writeheader()

        self.trial_data = {
            'index': np.nan,
            'subject': settings['subject'],
            'first_target': np.nan,
            'second_target': np.nan,
            'real_switch_time': np.nan,
            'first_press': np.nan,
            'first_press_time': np.nan,
            'correct': np.nan,
            'prep_time': np.nan
        }

        # extras
        self.frame_period = self.win.monitorFramePeriod
        self.trial_start = 0
        self.trial_counter = 0  # start at zero b/c zero indexing
        self.trial_input_buffer = np.full((600, 10), np.nan)
        self.trial_input_time_buffer = np.full((600, 1), np.nan)
        self.first_press = np.nan
        self.first_press_time = np.nan
        self.left_val = self.trial_table[['first', 'second']].min(axis=0).min()
        self.right_val = self.trial_table[['first',
                                           'second']].max(axis=0).max()
        self.device_on = False
        self.correct_answer = False

        # things related to the adaptive version
        self.adaptive = settings['adaptive']
        self.sign_switch_count = 0  # number of times the correctness switched
        self.curr_sign = False
        self.prev_sign = False
        self.curr_prep_time = 0.5  # start at 500ms

    def setup_visuals(self):
        # visually-related things
        # targets
        poses = [(-0.6, 0), (0.6, 0)]  # vary just on x-axis
        names = ['left_target', 'right_target']
        self.targets = [
            visual.Rect(self.win,
                        width=0.5,
                        height=0.5,
                        fillColor=[0, 0, 0],
                        pos=p,
                        lineWidth=0,
                        name=n) for p, n in zip(poses, names)
        ]

        fingers = ['pinky', 'ring', 'middle', 'index', 'thumb']
        left_hand = ['left ' + f for f in fingers]
        fingers.reverse()
        right_hand = ['right ' + f for f in fingers]
        both_hands = left_hand + right_hand
        uniques = pd.unique(self.trial_table['first']).astype(int)
        unique_finger_names = [both_hands[i] for i in uniques]
        unique_str = ", ".join(
            unique_finger_names
        )  # gives something like "left pinky, left thumb"

        # push feedback
        self.push_feedback = visual.Circle(self.win,
                                           size=0.1,
                                           fillColor=[-1, -1, -1],
                                           pos=(0, 0),
                                           autoDraw=False,
                                           autoLog=False,
                                           name='push_feedback')
        # fixation
        self.fixation = visual.Circle(self.win,
                                      size=0.05,
                                      fillColor=[1, 1, 1],
                                      pos=(0, 0),
                                      autoDraw=False,
                                      name='fixation')

        # text
        self.wait_text = visual.TextStim(
            self.win,
            text='Press a key to start.\nKeys are:\n' + unique_str,
            pos=(0, 0),
            units='norm',
            color=(1, 1, 1),
            height=0.1,
            alignHoriz='center',
            alignVert='center',
            name='wait_text',
            autoLog=False,
            wrapWidth=2)
        self.wait_text.autoDraw = True
        self.good = visual.TextStim(self.win,
                                    text=u'Good timing!',
                                    pos=(0, 0.4),
                                    units='norm',
                                    color=(-1, 1, 0.2),
                                    height=0.1,
                                    alignHoriz='center',
                                    alignVert='center',
                                    autoLog=True,
                                    name='good_text')
        self.too_slow = visual.TextStim(self.win,
                                        text=u'Too slow.',
                                        pos=(0, 0.4),
                                        units='norm',
                                        color=(1, -1, -1),
                                        height=0.1,
                                        alignHoriz='center',
                                        alignVert='center',
                                        autoLog=True,
                                        name='slow_text')
        self.too_fast = visual.TextStim(self.win,
                                        text=u'Too fast.',
                                        pos=(0, 0.4),
                                        units='norm',
                                        color=(1, -1, -1),
                                        height=0.1,
                                        alignHoriz='center',
                                        alignVert='center',
                                        autoLog=True,
                                        name='fast_text')

    # wait functions
    def remove_text(self):
        self.wait_text.autoDraw = False
        self.push_feedback.autoDraw = True
        self.fixation.autoDraw = True

    # pretrial functions
    def wait_for_release(self):
        # Wait until all keys released
        return not self.device_on

    def sched_beep(self):
        self.beep.seek(
            self.beep.stream.latency
        )  # 100ms of silence is built into the click train, so we can seek in without affecting the actual stimulus
        self.win.callOnFlip(self.beep.play)

    def sched_trial_timer_reset(self):
        # trial ends 200 ms after last beep
        self.win.callOnFlip(self.trial_timer.reset,
                            self.last_beep_time + 0.2 - self.frame_period)

    def sched_record_trial_start(self):
        self.win.callOnFlip(self._get_trial_start)

    def _get_trial_start(self):
        self.trial_start = self.win.lastFrameT

    def first_press_reset(self):
        self.first_press = np.nan
        self.first_press_time = np.nan

    # enter_trial functions
    def trial_timer_passed_first(self):
        # determine if 100 ms has elapsed
        # The timer is started at last_beep_time + 0.2
        # TODO: Think about this one
        return (self.last_beep_time + 0.2 - self.trial_timer.getTime() +
                self.frame_period) >= 0.1

    def show_first_target(self):
        # This is tricky -- if the condition evaluates to false, draw the left target
        self.targets[int(self.trial_table['first'][self.trial_counter] ==
                         self.right_val)].setAutoDraw(True)

    # first_target functions
    def trial_timer_passed_second(self):
        # this timer is the other way around
        return ((self.trial_timer.getTime() - 0.2 - self.frame_period) <=
                self.trial_table['switch_time'][self.trial_counter])

    def show_second_target(self):
        self.targets[int(self.trial_table['first'][self.trial_counter] ==
                         self.right_val)].setAutoDraw(False)
        self.targets[int(self.trial_table['second'][self.trial_counter] ==
                         self.right_val)].setAutoDraw(True)
        self.win.callOnFlip(self.log_switch_time)

    def log_switch_time(self):
        self.trial_data[
            'real_switch_time'] = self.win.lastFrameT - self.trial_start
        # print(self.trial_table['switch_time'][self.trial_counter])
        # print(self.last_beep_time - self.trial_data['real_switch_time'])

    # second_target functions
    def trial_timer_elapsed(self):
        return self.trial_timer.getTime() <= 0

    def record_data(self):
        self.trial_data['index'] = self.trial_counter
        self.trial_data['first_target'] = int(
            self.trial_table['first'][self.trial_counter])
        self.trial_data['second_target'] = int(
            self.trial_table['second'][self.trial_counter])
        # real_switch_time logged in log_switch_time
        self.trial_data['first_press'] = self.first_press
        self.trial_data['first_press_time'] = self.first_press_time
        self.trial_data['correct'] = int(self.correct_answer)
        self.trial_data['prep_time'] = self.first_press_time - self.trial_data[
            'real_switch_time']
        # now write data
        with open(self.summary_file_name, 'a') as f:
            writer = csv.DictWriter(f,
                                    fieldnames=self.csv_header,
                                    lineterminator='\n')
            writer.writerow(self.trial_data)

        self.trial_data.update({
            'index': np.nan,
            'first_target': np.nan,
            'second_target': np.nan,
            'real_switch_time': np.nan,
            'first_press': np.nan,
            'first_press_time': np.nan,
            'correct': np.nan,
            'prep_time': np.nan
        })

    def check_answer(self):
        correct_answer = self.trial_table['second'][
            self.trial_counter] == self.first_press
        delta = self.first_press_time - self.last_beep_time
        good_timing = False
        if delta > 0.075:
            self.too_slow.autoDraw = True
        elif delta < -0.075:
            self.too_fast.autoDraw = True
        elif np.isnan(self.first_press):
            self.too_slow.autoDraw = True
        else:
            good_timing = True
            self.good.autoDraw = True

        self.correct_answer = correct_answer
        if correct_answer and good_timing:
            self.coin.play()

    def draw_feedback(self):
        # text for timing, correctness
        [
            t.setFillColor((-0.3, 0.7,
                            -0.3) if self.correct_answer else (0.7, -0.3,
                                                               -0.3))
            for t in self.targets
        ]

    def sched_feedback_timer_reset(self):
        self.win.callOnFlip(self.feedback_timer.reset,
                            0.3 - self.frame_period)  # 300 ms feedback?

    # feedback functions
    def feedback_timer_elapsed(self):
        return self.feedback_timer.getTime() <= 0

    def remove_feedback(self):
        # remove targets, make sure everything is proper colour
        [t.setAutoDraw(False) for t in self.targets]
        [t.setFillColor([0, 0, 0]) for t in self.targets]
        self.good.autoDraw = False
        self.too_fast.autoDraw = False
        self.too_slow.autoDraw = False

    def increment_trial_counter(self):
        self.trial_counter += 1

    def sched_post_timer_reset(self):
        self.win.callOnFlip(self.post_timer.reset, 0.1 - self.frame_period)

    # post_trial functions
    def post_timer_elapsed(self):
        return self.post_timer.getTime() <= 0

    def trial_counter_exceed_table(self):
        return self.trial_counter >= len(self.trial_table.index)

    def wait_for_press(self):
        return not np.isnan(self.first_press)

    def calc_adapt(self):
        if self.adaptive and self.trial_table['first'][
                self.trial_counter] != self.trial_table['second'][
                    self.trial_counter]:
            #self.prev_sign = self.correct_answer
            if self.trial_counter == 0:
                pass  # initial point, 500 ms
            elif self.trial_counter == 1:
                self.curr_sign = -1.0 if self.correct_answer else 1.0  # shrink if right, grow if wrong
                self.curr_prep_time += (16 / 60) * self.curr_sign
                self.prev_sign = self.curr_sign
            elif self.trial_counter == 2:
                self.curr_sign = -1.0 if self.correct_answer else 1.0  # shrink if right, grow if wrong
                self.sign_switch_count += 1 if self.curr_sign != self.prev_sign else 0
                self.curr_prep_time += (8 / 60) * self.curr_sign
                self.prev_sign = self.curr_sign
            else:
                self.curr_sign = -1.0 if self.correct_answer else 1.0
                self.sign_switch_count += 1 if self.curr_sign != self.prev_sign else 0
                self.curr_prep_time += max(
                    1 / 60,
                    (2**(3 - self.sign_switch_count) / 60)) * self.curr_sign
            # apply bounds
            self.curr_prep_time = min(0.6, self.curr_prep_time)
            self.curr_prep_time = max(0.05, self.curr_prep_time)
            self.trial_table.loc[self.trial_counter,
                                 'switch_time'] = self.curr_prep_time
            print('Trial: ' + str(self.trial_counter) + ', prep: ' +
                  str(self.curr_prep_time))

    # cleanup functions
    def close_n_such(self):
        pass

    def input(self):
        # collect input
        timestamp, data = self.device.read()  # need to correct timestamp
        if timestamp is not None:
            if self.device.device.__name__ is 'Keyboard':
                for i, j in zip(data[0], data[1]):
                    self.keyboard_state[j[0]] = i[0]
                # colour in if any buttons pressed
                self.device_on = any(self.keyboard_state)
                if np.isnan(self.first_press) and self.device_on:
                    self.first_press = data[1][0][0]
                    self.first_press_time = (timestamp - self.trial_start)[0]

            elif self.device.device.__name__ is 'ForceTransducers':
                # see sg.medfilt(trial_input_buffer, kernel_size=(odd, 1))
                pass
                current_nans = np.isnan(self.trial_input_buffer)
                if current_nans.any():
                    next_index = np.where(current_nans)[0][0]
                    self.trial_input_buffer[next_index, :] = data
                    self.trial_input_time_buffer[next_index, :] = timestamp

    def draw_input(self):
        self.push_feedback.setFillColor(
            [0, 0, 0] if self.device_on else [-1, -1, -1])
Exemplo n.º 6
0
class Individuation(ShowBase, IndividuationStateMachine):
    def __init__(self, id, finger, trial_table):
        ShowBase.__init__(self)
        IndividuationStateMachine.__init__(self)
        props = WindowProperties()
        props.setTitle('Individuation Demo')
        self.win.requestProperties(props)
        self.render.setAntialias(AntialiasAttrib.MMultisample)
        self.render.setShaderAuto()  # allows shadows
        self.setFrameRateMeter(True)

        self.dev = MpI(Hand, clock=mono_clock.get_time)
        self.finger = int(finger) * 3  # offset
        self.f2 = int(finger)
        # self.disableMouse()
        self.countdown_timer = CountdownTimer()

        self.table = pd.read_table(trial_table)  # trial table
        self.setup_lights()
        self.setup_camera()
        self.load_models()
        self.load_audio()

        # add tasks (run every frame)
        taskMgr.add(self.get_user_input, 'move')
        taskMgr.add(self.update_target_color, 'target_color')
        taskMgr.add(self.update_state, 'update_state')
        taskMgr.add(self.update_feedback_bar, 'update_feedback_bar')
        self.accept('space', self.space_on)  # toggle a boolean somewhere

        # helpers
        self.space = False
        self.trial_counter = 0
        self.dist = 100
        self.queue = list()
        self.med_data = None
        self.noise = 0.0

    def load_models(self):
        self.axes_model = self.loader.loadModel('models/axes')
        self.target = self.loader.loadModel('models/target')
        self.player = self.loader.loadModel('models/player')

        self.axes_model.reparentTo(self.render)

        self.target.reparentTo(self.render)
        self.target.setPos(10, 10, 10)
        self.target.setScale(0.08, 0.08, 0.08)
        self.target.setColorScale(0, 0, 0, 1)
        self.target.setTransparency(TransparencyAttrib.MAlpha)
        self.target.setAlphaScale(0.6)
        self.target.hide()

        self.player.reparentTo(self.render)
        self.player.setPos(0, 0, 0)
        self.player.setScale(0.05, 0.05, 0.05)

        self.cam2dp.node().getDisplayRegion(0).setSort(-20)
        OnscreenImage(parent=self.cam2dp, image='models/background.jpg')

        self.text = OnscreenText(text='Press space to start',
                                 pos=(-0.8, 0.8),
                                 scale=0.08,
                                 fg=(1, 1, 1, 1),
                                 bg=(0, 0, 0, 1),
                                 frame=(0.2, 0.2, 0.8, 1),
                                 align=TextNode.ACenter)
        self.text.reparentTo(self.aspect2d)

        self.move_feedback = MeshDrawer2D()
        self.move_feedback.setBudget(
            4)  # this is the number of triangles needed (two per rect)
        feed_node = self.move_feedback.getRoot()
        feed_node.setTwoSided(True)
        feed_node.setDepthWrite(False)
        feed_node.setTransparency(True)
        feed_node.setBin('fixed', 0)
        feed_node.setLightOff(True)
        self.node = feed_node
        self.node.reparentTo(self.render2d)

    def setup_lights(self):
        pl = PointLight('pl')
        pl.setColor((1, 1, 1, 1))
        plNP = self.render.attachNewNode(pl)
        plNP.setPos(-0.5, -0.5, 0.5)
        self.render.setLight(plNP)

        pos = [[[0, 0, 3], [0, 0, -1]], [[0, -3, 0], [0, 1, 0]],
               [[-3, 0, 0], [1, 0, 0]]]
        for i in pos:
            dl = Spotlight('dl')
            dl.setColor((1, 1, 1, 1))
            dlNP = self.render.attachNewNode(dl)
            dlNP.setPos(*i[0])
            dlNP.lookAt(*i[1])
            dlNP.node().setShadowCaster(True)
            self.render.setLight(dlNP)

    def setup_camera(self):
        self.cam.setPos(-2, -4, 2)
        self.cam.lookAt(0, 0, 0)

    def load_audio(self):
        self.pop = self.loader.loadSfx('audio/Blop-Mark_DiAngelo-79054334.wav')

    def space_on(self):
        self.space = True

    def get_user_input(self, task):
        ts, data = self.dev.read()
        if ts is not None:
            data *= 2
            if self.med_data is None:
                self.med_data = np.median(data, axis=0)
            self.data = data
            # NB: I *think* this means I screwed up the axes when making the axes model...
            self.player.setPos(
                -data[-1, self.finger] + self.med_data[self.finger],
                data[-1, self.finger + 1] - self.med_data[self.finger + 1],
                -data[-1, self.finger + 2] + self.med_data[self.finger + 2])
            noise = 0.0
            for i in range(5):
                if i == self.f2:
                    # ignore involved finger
                    continue
                #print('number %i, noise %f' % (i, noise))
                i *= 3
                noise += np.sqrt(
                    np.square(-data[-1, i] + self.med_data[i]) +
                    np.square(data[-1, i + 1] - self.med_data[i + 1]) +
                    np.square(-data[-1, i + 2] + self.med_data[i + 2]))
            self.noise = noise
        return task.cont

    def update_target_color(self, task):
        dist = np.sqrt((self.player.get_x() - self.target.get_x())**2 +
                       (self.player.get_y() - self.target.get_y())**2 +
                       (self.player.get_z() - self.target.get_z())**2)
        d2 = 1 - dist
        self.dist = dist
        self.target.setColorScale(d2, d2, d2, 0.7)
        if dist < 0.05:
            self.player.setColorScale(0.1, 1, 0.2, 1)
        else:
            self.player.setColorScale(0.0, 0.0, 0.0, 1)
        return task.cont

    def update_feedback_bar(self, task):
        self.move_feedback.begin()
        self.move_feedback.rectangle_raw(-0.9, -0.9, 0.05, self.noise, 0, 0, 0,
                                         0, Vec4(0.9, .2, .1, 1))
        self.move_feedback.rectangle_raw(-0.925, -0.9, 0.1, 0.01, 0, 0, 0, 0,
                                         Vec4(1, 1, 1, 1))
        self.move_feedback.end()
        return task.cont

    def update_state(self, task):
        self.step()
        return task.cont

    # state machine functions
    def wait_for_space(self):
        return self.space

    def start_trial_countdown(self):
        self.countdown_timer.reset(10)

    def show_target(self):
        self.target.show()
        self.target.setPos(self.table.x[self.trial_counter],
                           self.table.y[self.trial_counter],
                           self.table.z[self.trial_counter])

    def trial_text(self):
        self.text.setText('Move for the target!')

    def close_to_target(self):
        return self.dist < 0.05

    def start_hold_countdown(self):
        self.countdown_timer.reset(2)

    def hold_text(self):
        self.text.setText('HOLD IT')

    def time_elapsed(self):
        return self.countdown_timer.elapsed() < 0

    def hide_target(self):
        self.target.hide()
        self.target.setPos(
            10, 10, 10)  # move far away so collision-y things don't work

    def start_post_countdown(self):
        self.countdown_timer.reset(2)

    def queue_distance(self):
        self.queue.append(self.close_to_target())

    def check_distance(self):
        tmp = np.sum(self.queue) / len(self.queue)
        self.queue = list()
        if tmp > 0.5:
            self.pop.play()

    def increment_trial_counter(self):
        self.trial_counter += 1

    def write_trial_data(self):
        pass

    def trial_counter_exceeded(self):
        return (self.trial_counter + 1) > self.table.shape[0]

    def reset_keyboard_bool(self):
        self.space = False

    def post_text(self):
        self.text.setText('Relax.')

    def kb_text(self):
        self.text.setText('Press [space] to start.')

    def clean_up(self):
        sys.exit()