def __init__(self, bar, button_text, button_size, location, callback=None): super(Button, self).__init__() super(EnvAgent, self).__init__() self.bar = bar self.size = button_size self.button_text = button_text self.button_rtext_a = message(button_text, "button_active", blit_txt=False) self.button_rtext_i = message(button_text, "button_inactive", blit_txt=False) self.frame_i = Rectangle(button_size[0], button_size[1], fill=None, stroke=(5, (255, 255, 255))) self.frame_a = Rectangle(button_size[0], button_size[1], fill=None, stroke=(5, (150, 255, 150))) self.active = False self.location = location self.text_location = (self.location[0] + self.size[0] // 2, self.location[1] + self.size[1] // 2) self.create_boundary() self.callback = callback
def __init__(self, y_pos, bar_length, bar_height, handle_radius, bar_fill, handle_fill): BoundaryInspector.__init__(self) EnvAgent.__init__(self) self.boundaries = {} self.pos = (P.screen_c[0] - bar_length // 2, y_pos) # upper-left self.message_pos = (P.screen_c[0], y_pos - 50) self.__handle_pos = (self.pos[0], self.pos[1] + bar_height // 2) self.handle_color = handle_fill self.handle_radius = handle_radius self.handle_stroke = None self.handle_boundary = None self.bar = None self.bar_size = (bar_length, bar_height) self.bar_color = bar_fill self.bar_stroke = None self.show_increment_ticks = True self.show_increment_text = False self.increment_count = None self.increments = [] self.increment_by = 1 self.increment_surfs = {} self.lower_bound = None self.upper_bound = None self.handle = Ellipse(self.handle_radius * 2, fill=self.handle_color, stroke=self.handle_stroke) self.bar = Rectangle(self.bar_size[0], self.bar_size[1], fill=self.bar_color, stroke=self.bar_stroke) self.add_boundary("handle", [ (self.pos[0] + self.handle_radius, self.pos[1]), self.handle_radius ], CIRCLE_BOUNDARY) self.msg = message("How many corners did the dot traverse?", "default", blit_txt=False) self.lb_msg = None self.ub_msg = None self.ok_text = message("OK", blit_txt=False) self.ok_inactive_button = Rectangle(100, 50, stroke=(1, (255, 255, 255)), fill=(125, 125, 125)).render() self.ok_active_button = Rectangle(100, 50, stroke=(1, (255, 255, 255)), fill=(5, 175, 45)).render() self.button_active = False self.button_pos = (P.screen_c[0], y_pos + bar_height + 50) button_upper_left = (self.button_pos[0] - 50, self.button_pos[1] - 25) button_botton_right = (self.button_pos[0] + 50, self.button_pos[1] + 25) self.add_boundary("button", (button_upper_left, button_botton_right), RECT_BOUNDARY) self.response = None
def construct_placeholder(self): stroke = [self.rect_thickness, WHITE, STROKE_CENTER] # width = None # height = None # Horizontal/vertical indicates directionality of placeholders length if self.box_alignment == VERTICAL: width, height = self.rect_short_side, self.rect_long_side else: height, width = self.rect_short_side, self.rect_long_side return Rectangle(width=width, height=height, stroke=stroke)
def __init__(self, data): self.media_type = IMAGE_FILE self.height = None self.width = None self.duration = None if data.text: # todo: make style optional self.contents = message(data.text.string, data.text.style, align="center", blit_txt=False) elif data.drawbject: d = data.drawbject if d.shape == "rectangle": self.contents = Rectangle(d.width, d.height, d.stroke, d.fill).render() if d.shape == "ellipse": self.contents = Ellipse(d.width, d.height, d.stroke, d.fill).render() if d.shape == "annulus": self.contents = Annulus(d.diameter, d.ring_width, d.stroke, d.fill).render() else: self.media_type = data.file.media_type if self.is_audio: self.duration = data.file self.contents = AudioClip( join(P.resources_dir, "audio", data.file.filename)) else: # If asset is image file, import and scale for current screen size (animations # originally hard-coded at 1920x1080 so we scale relative to that) img = Image.open(join(P.image_dir, data.file.filename)) target_size = (P.screen_x, (P.screen_x / 16.0) * 9 ) # corrected for aspect ratio scaled_size = scale(img.size, (1920, 1080), target_size, center=False) self.contents = np.asarray( img.resize(scaled_size, Image.BILINEAR)) try: self.height = self.contents.height self.width = self.contents.width except AttributeError: try: self.height = self.contents.shape[0] self.width = self.contents.shape[1] except AttributeError: pass # ie. audio file
def setup(self): # Generate messages to be displayed during experiment self.err_msgs = {} if P.saccade_response_cond: self.err_msgs['eye'] = "Moved eyes too soon!" self.err_msgs['key'] = "Please respond with eye movements only." self.err_msgs['early'] = self.err_msgs[ 'key'] # for convenience in logic else: self.err_msgs['eye'] = "Moved eyes!" self.err_msgs['key'] = "Please respond with the spacebar only." self.err_msgs['early'] = "Responded too soon!" # Stimulus sizes self.target_width = deg_to_px( 0.5, even=True) # diameter of target circle (0.5 degrees) self.cue_size = deg_to_px( 0.5, even=True) # size of asterisk/fixations (0.5 degrees) self.box_size = deg_to_px( 0.8, even=True) # size of placeholder boxes (0.8 degrees) # Stimulus Drawbjects self.box = Rectangle(self.box_size, stroke=(2, WHITE)).render() self.cross_r = FixationCross(self.cue_size, 2, fill=RED).render() self.cross_w = FixationCross(self.cue_size, 2, fill=WHITE).render() self.circle = Circle(self.target_width, fill=WHITE).render() self.asterisk = SquareAsterisk(self.cue_size, 2, fill=WHITE).render() # Layout of stimuli # offset between centre of boxes and centre of screen, in degrees offset_size_deg = P.dm_offset_size if P.development_mode else 7.0 self.offset_size = deg_to_px(offset_size_deg) self.target_locs = { TOP: (P.screen_c[0], P.screen_c[1] - self.offset_size), RIGHT: (P.screen_c[0] + self.offset_size, P.screen_c[1]), BOTTOM: (P.screen_c[0], P.screen_c[1] + self.offset_size), LEFT: (P.screen_c[0] - self.offset_size, P.screen_c[1]) } # prepare all animation locations for both rotation directions and starting axes self.animation_frames = 15 animation_duration = 300 # ms self.frame_duration = animation_duration / self.animation_frames rotation_increment = (pi / 2) / self.animation_frames cx, cy = P.screen_c self.frames = { V_START_AXIS: { ROT_CW: [], ROT_CCW: [] }, H_START_AXIS: { ROT_CW: [], ROT_CCW: [] } } for i in range(0, self.animation_frames): l_x_cw = -self.offset_size * cos(i * rotation_increment) l_y_cw = self.offset_size * sin(i * rotation_increment) l_x_ccw = self.offset_size * cos(i * rotation_increment) l_y_ccw = -self.offset_size * sin(i * rotation_increment) cw_locs = [(cx + l_x_cw, cy + l_y_cw), (cx - l_x_cw, cy - l_y_cw)] ccw_locs = [(cx + l_x_ccw, cy - l_y_ccw), (cx - l_x_ccw, cy + l_y_ccw)] self.frames[H_START_AXIS][ROT_CW].append(ccw_locs) self.frames[H_START_AXIS][ROT_CCW].append(cw_locs) self.frames[V_START_AXIS][ROT_CW].insert(0, cw_locs) self.frames[V_START_AXIS][ROT_CCW].insert(0, ccw_locs) # Define keymap for ResponseCollector self.keymap = KeyMap( "speeded response", # Name ["spacebar"], # UI Label ["spacebar"], # Data Label [SDLK_SPACE] # SDL2 Keycode ) # Boundaries for stimuli self.fixation_boundary = deg_to_px( 3.0) # radius of 3 degrees of visual angle self.el.add_boundary("drift_correct", [P.screen_c, self.fixation_boundary], CIRCLE_BOUNDARY)
def setup(self): # Set up custom text styles for the experiment self.txtm.add_style('instructions', 18, [255, 255, 255, 255]) self.txtm.add_style('error', 18, [255, 0, 0, 255]) self.txtm.add_style('tiny', 12, [255, 255, 255, 255]) self.txtm.add_style('small', 14, [255, 255, 255, 255]) # Pre-render shape stimuli dot_stroke = [P.dot_stroke, P.dot_stroke_col, STROKE_OUTER ] if P.dot_stroke > 0 else None self.tracker_dot = Ellipse(P.dot_size, stroke=dot_stroke, fill=P.dot_color).render() self.origin_active = Ellipse(P.origin_size, fill=self.origin_active_color).render() self.origin_inactive = Ellipse( P.origin_size, fill=self.origin_inactive_color).render() # If capture figures mode, generate, view, and optionally save some figures if P.capture_figures_mode: self.fig_dir = os.path.join(P.resources_dir, "figures") self.capture_figures() self.quit() # Initialize participant ID and session options, reloading ID if it already exists self.session = TraceLabSession() self.user_id = self.session.user_id # Once session initialized, show loading screen and finish setup self.loading_msg = message("Loading...", "default", blit_txt=False) fill() blit(self.loading_msg, 5, P.screen_c) flip() # Scale UI size variables to current screen resolution P.btn_s_pad = scale((P.btn_s_pad, 0), (1920, 1080))[0] P.y_pad = scale((0, P.y_pad), (1920, 1080))[1] # Initialize messages and response buttons for control trials control_fail_txt = "Please keep your finger on the start area for the complete duration." self.control_fail_msg = message(control_fail_txt, 'error', blit_txt=False) ctrl_buttons = ["1", "2", "3", "4", "5"] self.control_bar = ButtonBar(buttons=[(i, P.btn_size, None) for i in ctrl_buttons], button_size=P.btn_size, screen_margins=P.btn_s_pad, y_offset=P.y_pad, message_txt=P.control_q) # Initialize 'next trial' button button_x = 250 if self.handedness == LEFT_HANDED else P.screen_x - 250 button_y = P.screen_y - 100 self.next_trial_msg = message(P.next_trial_message, 'default', blit_txt=False) self.next_trial_box = Rectangle(300, 75, stroke=(2, (255, 255, 255), STROKE_OUTER)) self.next_trial_button_loc = (button_x, button_y) bounds = [(button_x - 150, button_y - 38), (button_x + 150, button_y + 38)] self.add_boundary("next trial button", bounds, RECT_BOUNDARY) # Initialize instructions and practice button bar for each condition self.instruction_files = { PHYS: { 'text': "physical_group_instructions.txt", 'frames': "physical_key_frames" }, MOTR: { 'text': "imagery_group_instructions.txt", 'frames': "imagery_key_frames" }, CTRL: { 'text': "control_group_instructions.txt", 'frames': "control_key_frames" } } self.practice_instructions = message(P.practice_instructions, "instructions", align="center", blit_txt=False) practice_buttons = [('Replay', [200, 100], self.practice), ('Practice', [200, 100], self.__practice__), ('Begin', [200, 100], any_key)] self.practice_button_bar = ButtonBar(practice_buttons, [200, 100], P.btn_s_pad, P.y_pad, finish_button=False) # Import all pre-generated figures needed for the current session figures = list(set(self.trial_factory.exp_factors["figure_name"])) figures.append(P.practice_figure) for f in figures: if f != "random": ui_request() fig_path = os.path.join(P.resources_dir, "figures", f) self.test_figures[f] = TraceLabFigure(fig_path)
def setup(self): # Bandit Variables self.high_payout_baseline = 12 self.low_payout_baseline = 8 self.total_score = None self.penalty = -5 # Stimulus Sizes thick_rect_border = deg_to_px(0.5) thin_rect_border = deg_to_px(0.1) star_size = deg_to_px(0.6) star_thickness = deg_to_px(0.1) square_size = deg_to_px(3) text_size = deg_to_px(0.65) large_text_size = deg_to_px(0.85) # Generate bandit colours from colour wheel self.bandit_colour_combos = [] if P.blocks_per_experiment > 4: msg = ( "Only 4 sets of colours available, experiment script must be modified if more" "than 4 blocks total are wanted.") raise RuntimeError(msg) for angle in [0, 45, 90, 135]: combo = [const_lum[angle], const_lum[angle + 180]] self.bandit_colour_combos.append(combo) random.shuffle(self.bandit_colour_combos) # Stimulus Drawbjects self.thick_rect = Rectangle( square_size, stroke=[thick_rect_border, WHITE, STROKE_CENTER]) self.thin_rect = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.left_bandit = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.right_bandit = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.neutral_box = self.thin_rect.render() self.star = Asterisk(star_size, star_thickness, fill=WHITE) self.star_cueback = Asterisk(star_size * 2, star_thickness * 2, fill=WHITE) self.star_muted = Asterisk(star_size, star_thickness, fill=GREY) self.probe = Ellipse(int(0.75 * square_size), fill=WHITE).render() # Layout box_offset = deg_to_px(8.0) self.left_box_loc = (P.screen_c[0] - box_offset, P.screen_c[1]) self.right_box_loc = (P.screen_c[0] + box_offset, P.screen_c[1]) # Timing # Note: cotoa = cue-offset target-onset asynchrony self.cotoa_min = 700 # ms self.cotoa_max = 1000 # ms self.feedback_exposure_period = 1.25 # sec # EyeLink Boundaries fix_bounds = [P.screen_c, square_size / 2] self.el.add_boundary('fixation', fix_bounds, CIRCLE_BOUNDARY) # Experiment Messages self.txtm.styles[ 'default'].font_size = text_size # re-define default font size in degrees self.txtm.add_style("score up", large_text_size, PASTEL_GREEN) self.txtm.add_style("score down", large_text_size, PASTEL_RED) self.txtm.add_style("timeout", large_text_size, WHITE) err_txt = "{0}\n\nPress any key to continue." lost_fixation_txt = err_txt.format( "Eyes moved! Please keep your eyes on the asterisk.") too_soon_txt = err_txt.format( "Responded too soon! Please wait until the 'go' signal to " "make a response.") probe_timeout_txt = err_txt.format( "No response detected! Please answer louder or faster.") bandit_timeout_txt = err_txt.format("Bandit selection timed out!") wrong_response_txt = err_txt.format( "Wrong response type!\nPlease make vocal responses " "to probes and keypress responses to bandits.") self.err_msgs = { 'fixation': message(lost_fixation_txt, align='center', blit_txt=False), 'too_soon': message(too_soon_txt, align='center', blit_txt=False), 'probe_timeout': message(probe_timeout_txt, 'timeout', align='center', blit_txt=False), 'bandit_timeout': message(bandit_timeout_txt, 'timeout', align='center', blit_txt=False), 'wrong_response': message(wrong_response_txt, align='center', blit_txt=False) } # Initialize separate ResponseCollectors for probe and bandit responses self.probe_rc = ResponseCollector(uses=[RC_AUDIO, RC_KEYPRESS]) self.bandit_rc = ResponseCollector(uses=[RC_AUDIO, RC_KEYPRESS]) # Initialize ResponseCollector keymap self.keymap = KeyMap( 'bandit_response', # Name ['z', '/'], # UI labels ["left", "right"], # Data labels [sdl2.SDLK_z, sdl2.SDLK_SLASH] # SDL2 Keysyms ) # Add practice block of 20 trials to start of experiment if P.run_practice_blocks: self.insert_practice_block(1, trial_counts=20)
class IOR_Reward(klibs.Experiment): #TODO: Add checking for audio responses on bandit trials (keyboard might make this difficult?) def __init__(self, *args, **kwargs): super(IOR_Reward, self).__init__(*args, **kwargs) def setup(self): # Bandit Variables self.high_payout_baseline = 12 self.low_payout_baseline = 8 self.total_score = None self.penalty = -5 # Stimulus Sizes thick_rect_border = deg_to_px(0.5) thin_rect_border = deg_to_px(0.1) star_size = deg_to_px(0.6) star_thickness = deg_to_px(0.1) square_size = deg_to_px(3) text_size = deg_to_px(0.65) large_text_size = deg_to_px(0.85) # Generate bandit colours from colour wheel self.bandit_colour_combos = [] if P.blocks_per_experiment > 4: msg = ( "Only 4 sets of colours available, experiment script must be modified if more" "than 4 blocks total are wanted.") raise RuntimeError(msg) for angle in [0, 45, 90, 135]: combo = [const_lum[angle], const_lum[angle + 180]] self.bandit_colour_combos.append(combo) random.shuffle(self.bandit_colour_combos) # Stimulus Drawbjects self.thick_rect = Rectangle( square_size, stroke=[thick_rect_border, WHITE, STROKE_CENTER]) self.thin_rect = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.left_bandit = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.right_bandit = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.neutral_box = self.thin_rect.render() self.star = Asterisk(star_size, star_thickness, fill=WHITE) self.star_cueback = Asterisk(star_size * 2, star_thickness * 2, fill=WHITE) self.star_muted = Asterisk(star_size, star_thickness, fill=GREY) self.probe = Ellipse(int(0.75 * square_size), fill=WHITE).render() # Layout box_offset = deg_to_px(8.0) self.left_box_loc = (P.screen_c[0] - box_offset, P.screen_c[1]) self.right_box_loc = (P.screen_c[0] + box_offset, P.screen_c[1]) # Timing # Note: cotoa = cue-offset target-onset asynchrony self.cotoa_min = 700 # ms self.cotoa_max = 1000 # ms self.feedback_exposure_period = 1.25 # sec # EyeLink Boundaries fix_bounds = [P.screen_c, square_size / 2] self.el.add_boundary('fixation', fix_bounds, CIRCLE_BOUNDARY) # Experiment Messages self.txtm.styles[ 'default'].font_size = text_size # re-define default font size in degrees self.txtm.add_style("score up", large_text_size, PASTEL_GREEN) self.txtm.add_style("score down", large_text_size, PASTEL_RED) self.txtm.add_style("timeout", large_text_size, WHITE) err_txt = "{0}\n\nPress any key to continue." lost_fixation_txt = err_txt.format( "Eyes moved! Please keep your eyes on the asterisk.") too_soon_txt = err_txt.format( "Responded too soon! Please wait until the 'go' signal to " "make a response.") probe_timeout_txt = err_txt.format( "No response detected! Please answer louder or faster.") bandit_timeout_txt = err_txt.format("Bandit selection timed out!") wrong_response_txt = err_txt.format( "Wrong response type!\nPlease make vocal responses " "to probes and keypress responses to bandits.") self.err_msgs = { 'fixation': message(lost_fixation_txt, align='center', blit_txt=False), 'too_soon': message(too_soon_txt, align='center', blit_txt=False), 'probe_timeout': message(probe_timeout_txt, 'timeout', align='center', blit_txt=False), 'bandit_timeout': message(bandit_timeout_txt, 'timeout', align='center', blit_txt=False), 'wrong_response': message(wrong_response_txt, align='center', blit_txt=False) } # Initialize separate ResponseCollectors for probe and bandit responses self.probe_rc = ResponseCollector(uses=[RC_AUDIO, RC_KEYPRESS]) self.bandit_rc = ResponseCollector(uses=[RC_AUDIO, RC_KEYPRESS]) # Initialize ResponseCollector keymap self.keymap = KeyMap( 'bandit_response', # Name ['z', '/'], # UI labels ["left", "right"], # Data labels [sdl2.SDLK_z, sdl2.SDLK_SLASH] # SDL2 Keysyms ) # Add practice block of 20 trials to start of experiment if P.run_practice_blocks: self.insert_practice_block(1, trial_counts=20) def block(self): if self.total_score: fill() score_txt = "Total block score: {0} points!".format( self.total_score) msg = message(score_txt, 'timeout', blit_txt=False) blit(msg, 5, P.screen_c) flip() any_key() self.total_score = 0 # reset total bandit score each block # Change bandit colours between blocks if P.practicing: greys = [(0, 0, 0, 255), (96, 96, 96, 255)] random.shuffle(greys) self.high_value_color = greys[0] self.low_value_color = greys[1] else: bandit_colours = self.bandit_colour_combos.pop() random.shuffle(bandit_colours) self.high_value_color = bandit_colours[0] self.low_value_color = bandit_colours[1] # Calibrate microphone for audio responses (people get quieter over time) threshold = self.audio.calibrate() self.probe_rc.audio_listener.threshold = threshold self.bandit_rc.audio_listener.threshold = threshold def setup_response_collector(self): # Configure probe response collector self.probe_rc.terminate_after = [2000, TK_MS] self.probe_rc.display_callback = self.probe_callback self.probe_rc.display_args = [self.trial_type == BOTH] self.probe_rc.flip = True self.probe_rc.keypress_listener.key_map = self.keymap self.probe_rc.keypress_listener.interrupts = True self.probe_rc.audio_listener.interrupts = True # Configure bandit response collector self.bandit_rc.terminate_after = [2000, TK_MS] self.bandit_rc.display_callback = self.bandit_callback self.bandit_rc.flip = True self.bandit_rc.keypress_listener.key_map = self.keymap self.bandit_rc.keypress_listener.interrupts = True if self.trial_type == BANDIT and not P.ignore_vocal_for_bandits: self.bandit_rc.audio_listener.interrupts = True else: self.bandit_rc.audio_listener.interrupts = False def trial_prep(self): # Reset error flag self.targets_shown = False self.err = None # If probed trial, establish location of probe (default: left box) self.probe_loc = self.right_box_loc if self.probe_location == RIGHT else self.left_box_loc if self.high_value_location == LEFT: self.left_bandit.fill = self.high_value_color self.right_bandit.fill = self.low_value_color self.low_value_location = RIGHT else: self.left_bandit.fill = self.low_value_color self.right_bandit.fill = self.high_value_color self.low_value_location = LEFT self.left_bandit.render() self.right_bandit.render() # Randomly choose cue off-target on asynchrony (cotoa) on each trial self.cotoa = self.random_interval(self.cotoa_min, self.cotoa_max) # Add timecourse of events to EventManager events = [[1000, 'cue_on']] events.append([events[-1][0] + 200, 'cue_off']) events.append([events[-1][0] + 200, 'cueback_off']) events.append([events[-2][0] + self.cotoa, 'target_on']) # either probe or bandits if self.trial_type in [BANDIT, BOTH]: events.append([events[-1][0] + 500, 'nogo_end']) # should reduce to 500 or less for e in events: self.evm.register_ticket(ET(e[1], e[0])) # Perform drift correct on EyeLink before trial start self.el.drift_correct() def trial(self): if P.development_mode: trial_info = ( "\ntrial_type: '{0}', high_val_loc: '{1}', probe_loc: '{2}', " "cue_loc: '{3}', winning_bandit: '{4}'") print( trial_info.format(self.trial_type, self.high_value_location, self.probe_location, self.cue_location, self.winning_bandit)) while self.evm.before('target_on', True) and not self.err: self.confirm_fixation() self.present_neutral_boxes() if self.evm.between('cue_on', 'cue_off'): if self.cue_location in [LEFT, DOUBLE]: blit(self.thick_rect, 5, self.left_box_loc) if self.cue_location in [RIGHT, DOUBLE]: blit(self.thick_rect, 5, self.right_box_loc) elif self.evm.between('cue_off', 'cueback_off'): blit(self.star_cueback, 5, P.screen_c) flip() self.targets_shown = True # after bandits or probe shown, don't recycle trial on user error if self.trial_type in [BANDIT, BOTH] and not self.err: while self.evm.before('nogo_end') and not self.err: if key_pressed(): self.show_error_message('too_soon') self.err = "early_response" break self.confirm_fixation() self.bandit_callback(before_go=True) flip() # PROBE RESPONSE PERIOD if self.trial_type in [PROBE, BOTH] and not self.err: self.probe_rc.collect() if not self.err: if len(self.probe_rc.keypress_listener.responses): self.show_error_message('wrong_response') self.err = 'keypress_on_probe' elif len(self.probe_rc.audio_listener.responses) == 0: self.show_error_message('probe_timeout') if self.probe_rc.audio_listener.stream_error: self.err = 'microphone_error' else: self.err = 'probe_timeout' # BANDIT RESPONSE PERIOD if self.trial_type in [BANDIT, BOTH] and not self.err: self.bandit_rc.collect() if self.trial_type == BANDIT and P.ignore_vocal_for_bandits == False: if len(self.bandit_rc.audio_listener.responses): self.show_error_message('wrong_response') self.err = 'vocal_on_bandit' # Retrieve collected response data before logging to database if self.err: bandit_choice, bandit_rt, reward = ['NA', 'NA', 'NA'] probe_rt = 'NA' else: self.err = 'NA' # Retreive responses from RepsponseCollector(s) and record data if self.trial_type in [BANDIT, BOTH]: bandit_choice = self.bandit_rc.keypress_listener.response( value=True, rt=False) bandit_rt = self.bandit_rc.keypress_listener.response( value=False, rt=True) if bandit_rt == TIMEOUT: self.show_error_message('bandit_timeout') reward = 'NA' else: # determine bandit payout (reward) and display feedback to participant reward = self.feedback(bandit_choice) else: bandit_choice, bandit_rt, reward = ['NA', 'NA', 'NA'] if self.trial_type in [PROBE, BOTH]: probe_rt = self.probe_rc.audio_listener.response(value=False, rt=True) else: probe_rt = 'NA' # Clear any remaining stimuli from screen before trial end clear() return { "block_num": P.block_number, "trial_num": P.trial_number, "trial_type": self.trial_type, "cue_loc": self.cue_location, "cotoa": self.cotoa, "high_value_col": self.high_value_color[:3] if self.trial_type != PROBE else "NA", "low_value_col": self.low_value_color[:3] if self.trial_type != PROBE else "NA", "high_value_loc": self.high_value_location if self.trial_type != PROBE else "NA", "winning_bandit": self.winning_bandit if self.trial_type != PROBE else "NA", "bandit_choice": bandit_choice, "bandit_rt": bandit_rt, "reward": reward, "probe_loc": self.probe_location if self.trial_type != BANDIT else "NA", "probe_rt": probe_rt, "err": self.err } def trial_clean_up(self): # Clear responses from response collectors before next trial self.probe_rc.audio_listener.reset() self.probe_rc.keypress_listener.reset() self.bandit_rc.audio_listener.reset() self.bandit_rc.keypress_listener.reset() def clean_up(self): pass def feedback(self, response): if self.winning_bandit == HIGH: winning_bandit_loc = self.high_value_location else: winning_bandit_loc = self.low_value_location if response == winning_bandit_loc: points = self.bandit_payout(value=self.winning_bandit) msg = message("You won {0} points!".format(points), "score up", blit_txt=False) else: points = self.penalty # -5 msg = message("You lost 5 points!", "score down", blit_txt=False) self.total_score += points feedback = [points, msg] feedback_exposure = CountDown(self.feedback_exposure_period) while feedback_exposure.counting(): ui_request() fill() blit(feedback[1], location=P.screen_c, registration=5) flip() return feedback[0] def bandit_payout(self, value): mean = self.high_payout_baseline if value == HIGH else self.low_payout_baseline # sample from normal distribution with sd of 1 and round to nearest int return int(random.gauss(mean, 1) + 0.5) def confirm_fixation(self): if not self.el.within_boundary('fixation', EL_GAZE_POS): self.show_error_message('fixation') if self.targets_shown: self.err = 'left_fixation' else: raise TrialException('gaze left fixation') # recycle trial def show_error_message(self, msg_key): fill() blit(self.err_msgs[msg_key], location=P.screen_c, registration=5) flip() any_key() def random_interval(self, lower, upper): # utility function to generate random time intervals with a given range # that are multiples of the current refresh rate (e.g. 16.7ms for a 60Hz monitor) min_flips = int(round(lower / P.refresh_time)) max_flips = int(round(upper / P.refresh_time)) return random.choice(range(min_flips, max_flips + 1, 1)) * P.refresh_time def present_neutral_boxes(self): fill() blit(self.star, 5, P.screen_c) blit(self.neutral_box, 5, self.left_box_loc) blit(self.neutral_box, 5, self.right_box_loc) def bandit_callback(self, before_go=False): fill() blit(self.star if before_go else self.star_muted, 5, P.screen_c) blit(self.left_bandit, 5, self.left_box_loc) blit(self.right_bandit, 5, self.right_box_loc) def probe_callback(self, mixed=False): self.confirm_fixation() if mixed: self.bandit_callback(True) else: self.present_neutral_boxes() probe_loc = self.right_box_loc if self.probe_location == RIGHT else self.left_box_loc blit(self.probe, 5, probe_loc)
def setup(self): # Stimulus sizes thick_rect_border = deg_to_px(0.5) thin_rect_border = deg_to_px(0.1) star_size = deg_to_px(0.6) star_thickness = deg_to_px(0.1) square_size = deg_to_px(3) large_text_size = 0.65 # Stimulus drawbjects self.thick_rect = Rectangle( square_size, stroke=[thick_rect_border, WHITE, STROKE_CENTER]) self.thin_rect = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.neutral_box = self.thin_rect.render() self.star = Asterisk(star_size, star_thickness, fill=WHITE) self.star_cueback = Asterisk(star_size * 2, star_thickness * 2, fill=WHITE) self.go = FixationCross(star_size, star_thickness, fill=BLACK) self.go.render() self.nogo = FixationCross(star_size, star_thickness, fill=BLACK, rotation=45) self.nogo.render() self.left_bandit = Ellipse(int(0.75 * square_size)) self.right_bandit = Ellipse(int(0.75 * square_size)) self.probe = Ellipse(int(0.75 * square_size)) # Layout box_offset = deg_to_px(8.0) self.left_box_loc = (P.screen_c[0] - box_offset, P.screen_c[1]) self.right_box_loc = (P.screen_c[0] + box_offset, P.screen_c[1]) # Set cotoa self.cotoa = 800 # ms self.feedback_exposure_period = 1.25 # sec # Bandit payout variables self.high_payout_baseline = 12 self.low_payout_baseline = 8 self.total_score = None self.penalty = -5 # Generate colours from colour wheel self.target_colours = [const_lum[0], const_lum[120], const_lum[240]] random.shuffle(self.target_colours) # Assign to bandits & neutral probe self.high_value_colour = self.target_colours[0] self.low_value_colour = self.target_colours[1] self.neutral_value_colour = self.target_colours[2] # EyeLink Boundaries fix_bounds = [P.screen_c, square_size / 2] self.el.add_boundary('fixation', fix_bounds, CIRCLE_BOUNDARY) # Initialize response collectors self.probe_rc = ResponseCollector(uses=RC_KEYPRESS) self.bandit_rc = ResponseCollector(uses=RC_KEYPRESS) # Initialize ResponseCollector keymaps self.bandit_keymap = KeyMap( 'bandit_response', # Name ['z', '/'], # UI labels ["left", "right"], # Data labels [sdl2.SDLK_z, sdl2.SDLK_SLASH] # SDL2 Keysyms ) self.probe_keymap = KeyMap('probe_response', ['spacebar'], ["pressed"], [sdl2.SDLK_SPACE]) # Experiment Messages self.txtm.add_style("payout", large_text_size, WHITE) self.txtm.add_style("timeout", large_text_size, WHITE) err_txt = "{0}\n\nPress any key to continue." lost_fixation_txt = err_txt.format( "Eyes moved! Please keep your eyes on the asterisk.") probe_timeout_txt = err_txt.format( "No response detected! Please respond as fast and as accurately as possible." ) bandit_timeout_txt = err_txt.format("Bandit selection timed out!") response_on_nogo_txt = err_txt.format( "\'nogo\' signal (x) presented\nPlease only respond when you see " "the \'go\' signal (+).") self.err_msgs = { 'fixation': message(lost_fixation_txt, align='center', blit_txt=False), 'probe_timeout': message(probe_timeout_txt, 'timeout', align='center', blit_txt=False), 'bandit_timeout': message(bandit_timeout_txt, 'timeout', align='center', blit_txt=False), 'response_on_nogo': message(response_on_nogo_txt, align='center', blit_txt=False) } self.rest_break_txt = err_txt.format( "Whew! that was tricky eh? Go ahead and take a break before continuing." ) self.end_of_block_txt = "You're done the first task! Please buzz the researcher to let them know!" # Insert bandit block if P.run_practice_blocks: self.insert_practice_block(1, trial_counts=P.trials_bandit_block)
class IOR_Reward_V2(klibs.Experiment): def setup(self): # Stimulus sizes thick_rect_border = deg_to_px(0.5) thin_rect_border = deg_to_px(0.1) star_size = deg_to_px(0.6) star_thickness = deg_to_px(0.1) square_size = deg_to_px(3) large_text_size = 0.65 # Stimulus drawbjects self.thick_rect = Rectangle( square_size, stroke=[thick_rect_border, WHITE, STROKE_CENTER]) self.thin_rect = Rectangle( square_size, stroke=[thin_rect_border, WHITE, STROKE_CENTER]) self.neutral_box = self.thin_rect.render() self.star = Asterisk(star_size, star_thickness, fill=WHITE) self.star_cueback = Asterisk(star_size * 2, star_thickness * 2, fill=WHITE) self.go = FixationCross(star_size, star_thickness, fill=BLACK) self.go.render() self.nogo = FixationCross(star_size, star_thickness, fill=BLACK, rotation=45) self.nogo.render() self.left_bandit = Ellipse(int(0.75 * square_size)) self.right_bandit = Ellipse(int(0.75 * square_size)) self.probe = Ellipse(int(0.75 * square_size)) # Layout box_offset = deg_to_px(8.0) self.left_box_loc = (P.screen_c[0] - box_offset, P.screen_c[1]) self.right_box_loc = (P.screen_c[0] + box_offset, P.screen_c[1]) # Set cotoa self.cotoa = 800 # ms self.feedback_exposure_period = 1.25 # sec # Bandit payout variables self.high_payout_baseline = 12 self.low_payout_baseline = 8 self.total_score = None self.penalty = -5 # Generate colours from colour wheel self.target_colours = [const_lum[0], const_lum[120], const_lum[240]] random.shuffle(self.target_colours) # Assign to bandits & neutral probe self.high_value_colour = self.target_colours[0] self.low_value_colour = self.target_colours[1] self.neutral_value_colour = self.target_colours[2] # EyeLink Boundaries fix_bounds = [P.screen_c, square_size / 2] self.el.add_boundary('fixation', fix_bounds, CIRCLE_BOUNDARY) # Initialize response collectors self.probe_rc = ResponseCollector(uses=RC_KEYPRESS) self.bandit_rc = ResponseCollector(uses=RC_KEYPRESS) # Initialize ResponseCollector keymaps self.bandit_keymap = KeyMap( 'bandit_response', # Name ['z', '/'], # UI labels ["left", "right"], # Data labels [sdl2.SDLK_z, sdl2.SDLK_SLASH] # SDL2 Keysyms ) self.probe_keymap = KeyMap('probe_response', ['spacebar'], ["pressed"], [sdl2.SDLK_SPACE]) # Experiment Messages self.txtm.add_style("payout", large_text_size, WHITE) self.txtm.add_style("timeout", large_text_size, WHITE) err_txt = "{0}\n\nPress any key to continue." lost_fixation_txt = err_txt.format( "Eyes moved! Please keep your eyes on the asterisk.") probe_timeout_txt = err_txt.format( "No response detected! Please respond as fast and as accurately as possible." ) bandit_timeout_txt = err_txt.format("Bandit selection timed out!") response_on_nogo_txt = err_txt.format( "\'nogo\' signal (x) presented\nPlease only respond when you see " "the \'go\' signal (+).") self.err_msgs = { 'fixation': message(lost_fixation_txt, align='center', blit_txt=False), 'probe_timeout': message(probe_timeout_txt, 'timeout', align='center', blit_txt=False), 'bandit_timeout': message(bandit_timeout_txt, 'timeout', align='center', blit_txt=False), 'response_on_nogo': message(response_on_nogo_txt, align='center', blit_txt=False) } self.rest_break_txt = err_txt.format( "Whew! that was tricky eh? Go ahead and take a break before continuing." ) self.end_of_block_txt = "You're done the first task! Please buzz the researcher to let them know!" # Insert bandit block if P.run_practice_blocks: self.insert_practice_block(1, trial_counts=P.trials_bandit_block) def block(self): # Block type defaults to probe trials, overidden in practice block(s) self.block_type = PROBE # Show total score following completion of bandit task if self.total_score: fill() score_txt = "Total block score: {0} points!".format( self.total_score) msg = message(score_txt, 'timeout', blit_txt=False) blit(msg, 5, P.screen_c) flip() any_key() self.total_score = 0 # Reset score once presented # Bandit task if P.practicing: self.block_type == BANDIT # Initialize selection counters self.times_selected_high = 0 self.time_selected_low = 0 # End of block messaging if not P.practicing: self.block_type == PROBE fill() msg = message(self.end_of_block_txt, blit_txt=False) blit(msg, 5, P.screen_c) flip() any_key() def setup_response_collector(self): # Configure probe response collector self.probe_rc.terminate_after = [1500, TK_MS] self.probe_rc.display_callback = self.probe_callback self.probe_rc.flip = True self.probe_rc.keypress_listener.key_map = self.probe_keymap self.probe_rc.keypress_listener.interrupts = True # Configure bandit response collector self.bandit_rc.terminate_after = [1500, TK_MS] self.bandit_rc.display_callback = self.bandit_callback self.bandit_rc.flip = True self.bandit_rc.keypress_listener.key_map = self.bandit_keymap self.bandit_rc.keypress_listener.interrupts = True def trial_prep(self): # Reset error flag self.targets_shown = False self.err = None # BANDIT PROPERTIES if P.practicing: self.cotoa = 'NA' # Establish location & colour of bandits if self.high_value_location == LEFT: self.left_bandit.fill = self.high_value_colour self.right_bandit.fill = self.low_value_colour self.low_value_location = RIGHT else: self.left_bandit.fill = self.low_value_colour self.right_bandit.fill = self.high_value_colour self.low_value_location = LEFT self.left_bandit.render() self.right_bandit.render() # PROBE PROPERTIES else: # Rest breaks if P.trial_number % (P.trials_per_block / P.breaks_per_block) == 0: if P.trial_number < P.trials_per_block: fill() msg = message(self.rest_break_txt, 'timeout', blit_txt=False) blit(msg, 5, P.screen_c) flip() any_key() # Establish & assign probe location self.probe_loc = self.right_box_loc if self.probe_location == RIGHT else self.left_box_loc # go/nogo signal always presented w/probe self.go_nogo_loc = self.probe_loc # Establish & assign probe colour if self.probe_colour == HIGH: self.probe.fill = self.high_value_colour elif self.probe_colour == LOW: self.probe.fill = self.low_value_colour else: self.probe.fill = self.neutral_value_colour self.probe.render() # Add timecourse of events to EventManager if P.practicing: # Bandit trials events = [[1000, 'target_on']] else: # Probe trials events = [[1000, 'cue_on']] events.append([events[-1][0] + 200, 'cue_off']) events.append([events[-1][0] + 200, 'cueback_off']) events.append([events[-2][0] + 800, 'target_on']) for e in events: self.evm.register_ticket(ET(e[1], e[0])) # Perform drift correct on Eyelink before trial start self.el.drift_correct() def trial(self): # BANDIT TRIAL if P.practicing: cotoa, probe_rt = ['NA', 'NA'] # Don't occur in bandit blocks # Present placeholders while self.evm.before('target_on', True) and not self.err: self.confirm_fixation() self.present_neutral_boxes() flip() # BANDIT RESPONSE PERIOD self.targets_shown = True # After bandits shown, don't recycle trial # Present bandits and listen for response self.bandit_rc.collect() # If wrong response made if self.err: bandit_choice, bandit_rt, reward = ['NA', 'NA', 'NA'] else: self.err = 'NA' # Retrieve responses from ResponseCollector(s) & record data bandit_choice = self.bandit_rc.keypress_listener.response( value=True, rt=False) bandit_rt = self.bandit_rc.keypress_listener.response( value=False, rt=True) if bandit_rt == TIMEOUT: self.show_error_message('bandit_timeout') reward = 'NA' else: # Determine bandit payout & display reward = self.feedback(bandit_choice) # PROBE TRIAL else: bandit_choice, bandit_rt, reward = [ 'NA', 'NA', 'NA' ] # Don't occur in probe trials # Present placeholders & confirm fixation while self.evm.before('target_on', True): self.confirm_fixation() self.present_neutral_boxes() # Present cue if self.evm.between('cue_on', 'cue_off'): if self.cue_location == LEFT: blit(self.thick_rect, 5, self.left_box_loc) else: blit(self.thick_rect, 5, self.right_box_loc) # Present cueback elif self.evm.between('cue_off', 'cueback_off'): blit(self.star_cueback, 5, P.screen_c) flip() # PROBE RESPONSE PERIOD self.targets_shown = True # After probe shown, don't recycle trial # Present probes & listen for response self.probe_rc.collect() # If 'go' trial, check for response if self.go_no_go == GO: # If wrong response made if self.err: probe_rt = 'NA' # If correct response OR timeout else: self.err = 'NA' probe_rt = self.probe_rc.keypress_listener.response( value=False, rt=True) if probe_rt == TIMEOUT: self.show_error_message('probe_timeout') probe_rt = 'NA' # Similarly, for 'nogo' trials else: probe_rt = 'NA' # If response made, penalize if len(self.probe_rc.keypress_listener.responses): self.show_error_message('response_on_nogo') self.err = 'response_on_nogo' # If no response, continue as normal else: self.err = 'NA' # Return trial data return { "block_num": P.block_number, "trial_num": P.trial_number, "block_type": "BANDIT" if P.practicing else "PROBE", "high_value_col": self.high_value_colour[:3] if P.practicing else 'NA', "high_value_loc": self.high_value_location if P.practicing else 'NA', "low_value_col": self.low_value_colour[:3] if P.practicing else 'NA', "low_value_loc": self.low_value_location if P.practicing else 'NA', "winning_trial": self.winning_trial if P.practicing else 'NA', "bandit_selected": self.bandit_selected if P.practicing else 'NA', "bandit_rt": bandit_rt, "reward": reward, "cue_loc": self.cue_location if not P.practicing else 'NA', "cotoa": self.cotoa if not P.practicing else 'NA', "probe_loc": self.probe_location if not P.practicing else 'NA', "probe_col": self.probe_colour if not P.practicing else 'NA', "go_no_go": self.go_no_go if not P.practicing else 'NA', "probe_rt": probe_rt, "err": self.err } # Clear remaining stimuli from screen clear() def trial_clean_up(self): # Clear responses from responses collectors before next trial self.probe_rc.keypress_listener.reset() self.bandit_rc.keypress_listener.reset() def clean_up(self): # Let Ss know when experiment is over self.all_done_text = "You're all done! Now I get to take a break.\nPlease buzz the researcher to let them know you're done!" fill() msg = message(self.all_done_text, 'timeout', blit_txt=False) blit(msg, 5, P.screen_c) flip() any_key() # Determines & presents feedback def feedback(self, response): # Keep count of bandit choices if response == self.high_value_location: self.bandit_selected = HIGH self.times_selected_high = self.times_selected_high + 1 # Occasionally probe participant learning if self.times_selected_high in [5, 10, 15]: self.query_learning(HIGH) else: self.bandit_selected = LOW self.time_selected_low = self.time_selected_low + 1 if self.time_selected_low in [5, 10, 15]: self.query_learning(LOW) # Determine payout if self.winning_trial == YES: points = self.bandit_payout(value=self.bandit_selected) msg = message("You won {0} points!".format(points), "payout", blit_txt=False) else: points = self.penalty # -5 msg = message("You lost 5 points!", "payout", blit_txt=False) # Running point total self.total_score += points feedback = [points, msg] # Present payout feedback_exposure = CountDown(self.feedback_exposure_period) while feedback_exposure.counting(): ui_request() fill() blit(feedback[1], location=P.screen_c, registration=5) flip() return feedback[0] # Calculates bandit payout def bandit_payout(self, value): mean = self.high_payout_baseline if value == HIGH else self.low_payout_baseline # sample from normal distribution with sd of 1 and round to nearest int return int(random.gauss(mean, 1) + 0.5) # Confirms whether Ss are fixating def confirm_fixation(self): if not self.el.within_boundary('fixation', EL_GAZE_POS): self.show_error_message('fixation') if self.targets_shown: self.err = 'left_fixation' else: raise TrialException('gaze left fixation') # recycle trial # Presents error messages def show_error_message(self, msg_key): fill() blit(self.err_msgs[msg_key], location=P.screen_c, registration=5) flip() any_key() # Utility function to generate random time intervals with a given range # that are multiples of the current refresh rate (e.g. 16.7ms for a 60Hz monitor) def random_interval(self, lower, upper): min_flips = int(round(lower / P.refresh_time)) max_flips = int(round(upper / P.refresh_time)) return random.choice(range(min_flips, max_flips + 1, 1)) * P.refresh_time # Presents neutral boxes, duh def present_neutral_boxes(self): fill() blit(self.star, 5, P.screen_c) blit(self.neutral_box, 5, self.left_box_loc) blit(self.neutral_box, 5, self.right_box_loc) # Presents bandits def bandit_callback(self, before_go=False): self.confirm_fixation() self.present_neutral_boxes() blit(self.left_bandit, 5, self.left_box_loc) blit(self.right_bandit, 5, self.right_box_loc) # Presents probes def probe_callback(self): self.confirm_fixation() self.present_neutral_boxes() # Present probe & go/nogo stimulus if self.go_no_go == GO: blit(self.probe, 5, self.probe_loc) blit(self.go, 5, self.probe_loc) else: blit(self.probe, 5, self.probe_loc) blit(self.nogo, 5, self.probe_loc) # Assesses learning by asking Ss their anticipated trial earnings def query_learning(self, bandit): if bandit == HIGH: anticipated_reward_high = query(user_queries.experimental[0]) anticipated_reward_survey = { 'participant_id': P.participant_id, 'anticipated_reward_high': anticipated_reward_high, 'anticipated_reward_low': "NA" } else: anticipated_reward_low = query(user_queries.experimental[1]) anticipated_reward_survey = { 'participant_id': P.participant_id, 'anticipated_reward_high': "NA", 'anticipated_reward_low': anticipated_reward_low } self.db.insert(anticipated_reward_survey, table='surveys')