def _get_feedback_rect_colors(exp_info): colors_param = exp_info.config.feedback_btn_colours if colors_param is None: if exp_info.config.feedback_place == 'button': return xpy.misc.constants.C_GREEN, xpy.misc.constants.C_GREEN elif exp_info.config.feedback_place == 'middle': return xpy.misc.constants.C_GREEN, xpy.misc.constants.C_RED else: raise ttrk.ValueError( 'Unsupported config.feedback_place ({:})'.format( exp_info.config.feedback_place)) if u.is_rgb(colors_param): return colors_param, colors_param if u.is_collection(colors_param) and u.is_rgb( colors_param[0]) and u.is_rgb(colors_param[1]): return tuple(colors_param) else: raise ttrk.ValueError( 'Invalid config.feedback_btn_colours ({:}) - expecting a color (Red,Green,Blue) or an array.tuple with two colors' .format(colors_param))
def _get_feedback_rect_sizes(exp_info): size_param = exp_info.config.feedback_rect_size #-- If size was explicitly provided: use it if size_param is not None and not isinstance(size_param, numbers.Number): try: #-- If "feedback_rect_size" is a pair of numbers - just translate it to pixels single_size = common.xy_to_pixels(size_param, exp_info.screen_size, 'config.feedback_rect_size') return single_size, single_size except ttrk.TypeError: pass # -- The "feedback_rect_size" argument is NOT a pair of numbers. # -- So we expect it to be an array with two sets of coordinates. common.validate_config_param_type("feedback_rect_size", (list, tuple, np.ndarray), size_param, type_name="array/list/tuple") result = [ common.xy_to_pixels(s, exp_info.screen_size, 'config.feedback_rect_size') for s in size_param ] if len(size_param) != 2 or result[0] is None or result[1] is None: raise ttrk.ValueError( 'Invalid config.feedback_rect_size: expecting either a size or a pair of sizes' ) return result #-- feedback_rect_size was not specified explicitly: set it to default values if exp_info.config.feedback_place == 'button': #-- The feedback area overlaps with the buttons return exp_info.response_button_size, exp_info.response_button_size elif exp_info.config.feedback_place == 'middle': #-- The feedback area is between the buttons width = exp_info.screen_size[0] - 2 * exp_info.response_button_size[0] if isinstance(size_param, int): height = size_param elif isinstance(size_param, numbers.Number): height = int(np.round(size_param * exp_info.screen_size[1])) else: height = int(np.round(exp_info.response_button_size[1] / 4)) return (width, height), (width, height) else: raise ttrk.ValueError("Unsupported feedback_place({:})".format( exp_info.config.feedback_place))
def _validate_property(self, prop_name, n_stim=0): value = getattr(self, prop_name) if value is None: raise ttrk.ValueError('{:}.{:} was not set'.format(_u.get_type_name(self), prop_name)) is_multiple_values = getattr(self, "_" + prop_name + "_multiple") if is_multiple_values and len(value) < n_stim: raise ttrk.ValueError('{:}.{:} has {:} values, but there are {:} values to present'.format( _u.get_type_name(self), prop_name, len(value), n_stim))
def animated_object(self, obj): if "present" not in dir(obj): raise trajtracker.ValueError( "{:}.animated_object must be an object with a present() method" .format(_u.get_type_name(self))) if "position" not in dir(obj): raise trajtracker.ValueError( "{:}.animated_object must be an object with a 'position' property" .format(_u.get_type_name(self))) self._animated_object = obj self._log_property_changed("animated_object")
def _get_feedback_stim_positions(exp_info, sizes=None): if sizes is None: sizes = [s.size for s in exp_info.feedback_stimuli] pos_param = exp_info.config.feedback_stim_position #-- If position was explicitly provided: use it if pos_param is not None: common.validate_config_param_type("feedback_stim_position", (list, tuple, np.ndarray), pos_param, type_name="array/list/tuple") if u.is_coord(pos_param): #-- One position given: use for both feedback areas pos_param = tuple(pos_param) return pos_param, pos_param elif len(pos_param) == 2 and u.is_coord(pos_param[0]) and u.is_coord( pos_param[1]): return tuple(pos_param) else: raise ttrk.ValueError( "Invalid config.feedback_stim_position: expecting (x,y) or [(x1,y1), (x2,y2)]" ) #-- Position was not explicitly provided: use default position scr_width, scr_height = exp_info.screen_size if exp_info.config.feedback_place == 'button': #-- Position is top screen corners x1 = -(scr_width / 2 - sizes[0][0] / 2) y1 = scr_height / 2 - sizes[0][1] / 2 x2 = (scr_width / 2 - sizes[1][0] / 2) y2 = scr_height / 2 - sizes[1][1] / 2 return (x1, y1), (x2, y2) elif exp_info.config.feedback_place == 'middle': #-- Position is top-middle of screen y1 = scr_height / 2 - sizes[0][1] / 2 y2 = scr_height / 2 - sizes[1][1] / 2 return (0, y1), (0, y2) else: raise ttrk.ValueError("Unsupported config.feedback_place ({:})".format( config.feedback_place))
def coord_to_pixels(coord, col_name, filename, is_x): """ Convert screen coordinates, which were provided as percentage of the screen width/height, into pixels. :param coord: The coordinate, on a 0-1 scale; or a list of coordinates :param col_name: The column name in the CSV file (just for logging) :param filename: CSV file name (just for logging) :param is_x: (bool) whether it's an x or y coordinate :return: coordinate as pixels (int); or a list of coords """ screen_size = ttrk.env.screen_size[0 if is_x else 1] coord_list = coord if isinstance(coord, list) else [coord] # noinspection PyTypeChecker if sum([not (-1 <= c <= 1) for c in coord_list]) > 0: # Some coordinates are way off the screen bounds raise ttrk.ValueError("Invalid {:} in the CSV file {:} ({:}): when position is specified as %, its values must be between -1 and 1". format(col_name, filename, coord_list)) if isinstance(coord, list): return [int(np.round(c * screen_size)) for c in coord] else: return int(np.round(coord * screen_size))
def init_output_file(self, filename=None, xy_precision=5, time_precision=3): """ Initialize a new CSV output file for saving the results :param filename: Full path :param xy_precision: Precision of x,y coordinates (default: 5) :param time_precision: Precision of time (default: 3) """ if filename is not None: self._filename = filename if self._filename is None: raise ttrk.ValueError( "filename was not provided to {:}.init_output_file()".format( _u.get_type_name(self))) self._xy_precision = xy_precision self._time_precision = time_precision fh = self._open_file(self._filename, 'w') fh.write('trial,time,x,y\n') fh.close() self._out_file_initialized = True self._log_write_if(ttrk.log_debug, "Initializing output file %s" % self._filename, True)
def create_confidence_slider(exp_info): """ Create a :class:`~trajtracker.stimuli.Slider` for measuring subjective confidence rating :param exp_info: The experiment-level objects :type exp_info: trajtracker.paradigms.num2pos.ExperimentInfo """ config = exp_info.config y = config.confidence_slider_y validate_config_param_type("confidence_slider_y", numbers.Number, y) y = xy_to_pixels(y, exp_info.screen_size[1], 'config.confidence_slider_y') if y is None: raise ttrk.ValueError('Invalid config.confidence_slider_y ({:}): '.format(config.confidence_slider_y) + 'expecting either an integer or ratio of screen height (between -0.5 and 0.5)') validate_config_param_type("confidence_rating", bool, config.confidence_rating) if not exp_info.config.confidence_rating: return #-- Create the confidence slider slider_bgnd = _create_confidence_slider_background(exp_info) gauge = _create_slider_gauge(slider_bgnd.surface_size[0]) slider = ttrk.stimuli.Slider(slider_bgnd, gauge, orientation=ttrk.stimuli.Orientation.Vertical, min_value=0, max_value=100, position=(0, y)) exp_info.stimuli.add(slider.stimulus, "confidence_slider", visible=False) exp_info.confidence_slider = slider exp_info.exported_trial_result_fields['confidence'] = "" exp_info.exported_trial_result_fields['confidence_n_moves'] = 0
def _set_dot_position_for_time(self, time): if time < 0 or time > 10000: raise ttrk.ValueError( '{:}._set_dot_position_for_time(): invalid "time" argument ({:})' .format(_u.get_type_name(self), time)) remaining_time_ratio = 1 - (min(time, self._zoom_duration) / self._zoom_duration) dx = int(np.round(self._box_size[0] / 2 * remaining_time_ratio)) dy = int(np.round(self._box_size[1] / 2 * remaining_time_ratio)) self._dots[0].position = self.position[0] - dx, self.position[ 1] - dy # top-left self._dots[1].position = self.position[0] + dx, self.position[ 1] - dy # top-right self._dots[2].position = self.position[0] - dx, self.position[ 1] + dy # bottom-left self._dots[3].position = self.position[0] + dx, self.position[ 1] + dy # bottom-right if self._should_log(ttrk.log_trace): self._log_write( "Set dots location, remaining time = {:.0f}%".format( remaining_time_ratio * 100), True)
def get_feedback_stim_num(exp_info, trial, user_response): """ Return the number of the feedback stimulus to show (0 or 1) :param exp_info: :type exp_info: trajtracker.paradigms.dchoice.ExperimentInfo :param trial: :type trial: trajtracker.paradigms.dchoice.TrialInfo :param user_response: The button selected by the user (0=left, 1=right) """ selectby = exp_info.config.feedback_select_by if selectby == 'accuracy': correct = trial.expected_response == user_response return 1 - correct elif selectby == 'response': return user_response elif selectby == 'expected': return trial.expected_response else: raise ttrk.ValueError( "Unsupported config.feedback_select_by ({:})".format(selectby))
def get_color_at(self, x_coord, y_coord, use_mapping=None): """ Return the color at a given coordinate :param x_coord: :param y_coord: :param use_mapping: :return: The color in the given place, or None if the coordinate is out of the image range """ _u.validate_func_arg_type(self, "get_color_at", "x_coord", x_coord, int) _u.validate_func_arg_type(self, "get_color_at", "y_coord", y_coord, int) _u.validate_func_arg_type(self, "get_color_at", "use_mapping", use_mapping, numbers.Number, none_allowed=True) if use_mapping is None: use_mapping = self._use_mapping if self._color_to_code is None and use_mapping: raise trajtracker.ValueError("a call to %s.get_color_at(use_mapping=True) is invalid because color_codes were not specified" % self.__class__) if x_coord < self._top_left_x or x_coord >= self._top_left_x+self._width or \ y_coord < self._top_left_y or y_coord >= self._top_left_y + self._height: return None x_coord -= self._top_left_x y_coord -= self._top_left_y #y_coord = len(self._image) - 1 - y_coord # reverse up/down v = self._image[-(y_coord+1)][x_coord] return self._color_to_code[v] if use_mapping else v
def create_fixation_zoom(exp_info): """ Create a :class:`~trajtracker.stimuli.FixationZoom` fixation :param exp_info: The experiment-level objects :type exp_info: trajtracker.paradigms.num2pos.ExperimentInfo """ config = exp_info.config y, height = exp_info.get_default_target_y() if config.fixzoom_start_zoom_event is None: start_zoom_event = ttrk.events.TRIAL_STARTED + 0.2 elif config.fixzoom_start_zoom_event == ttrk.events.TRIAL_STARTED or config.fixzoom_start_zoom_event == ttrk.events.TRIAL_INITIALIZED: raise ttrk.ValueError(('config.fixzoom_start_zoom_event was set to an invalid value ({:}): ' + 'it can only be set to times AFTER the trial has already started (e.g., ttrk.events.TRIAL_STARTED + 0.1)'). format(config.fixzoom_start_zoom_event)) else: start_zoom_event = config.fixzoom_start_zoom_event fixation = ttrk.stimuli.FixationZoom( position=(config.text_target_x_coord, y), box_size=config.fixzoom_box_size, dot_radius=config.fixzoom_dot_radius, dot_colour=config.fixzoom_dot_colour, zoom_duration=config.fixzoom_zoom_duration, stay_duration=config.fixzoom_stay_duration, show_event=config.fixzoom_show_event, start_zoom_event=start_zoom_event) exp_info.fixation = fixation exp_info.add_event_sensitive_object(exp_info.fixation)
def axis(self, value): _u.validate_attr_type(self, "axis", value, ValidationAxis) if value == ValidationAxis.xy: raise trajtracker.ValueError( _u.ErrMsg.attr_invalid_value(self.__class__, "axis", value)) self._axis = value self._log_property_changed("axis")
def _get_response_buttons_positions(exp_info, button_size): config = exp_info.config position = config.resp_btn_positions if position is None: #-- Set default position: top-left and top-right of screen max_x = int(exp_info.screen_size[0] / 2) max_y = int(exp_info.screen_size[1] / 2) x = max_x - int(button_size[0] / 2) y = max_y - int(button_size[1] / 2) return (-x, y), (x, y) if u.is_coord(position, allow_float=True): #-- One pair of coords given: this is for the left side pos_left = -position[0], position[1] position = pos_left, position elif not trajtracker.utils.is_collection(position) or len(position) != 2 or \ not u.is_coord(position[0], allow_float=True) or not u.is_coord(position[1], allow_float=True): raise ttrk.ValueError( "config.resp_btn_positions should be either (x,y) coordinates " + "or a pair of coordinates [(xleft, yleft), (xright, yright)]. " + "The value provided is invalid: {:}".format(position)) #-- If x/y are between [-1,1], they mean percentage of screen size result = [] screen_width, screen_height = exp_info.screen_size for x, y in position: if -1 < x < 1: x = int(x * screen_width) elif not isinstance(x, int): raise ttrk.ValueError( "Invalid config.resp_btn_positions: a non-integer x was provided ({:})" .format(x)) if -1 < y < 1: y = int(y * screen_height) elif not isinstance(y, int): raise ttrk.ValueError( "Invalid config.resp_btn_positions: a non-integer y was provided ({:})" .format(y)) result.append((x, y)) return result
def trajectory_generator(self, obj): if "get_traj_point" not in dir(obj): raise trajtracker.ValueError( "{:}.trajectory_generator must be an object with a get_traj_point() method" .format(_u.get_type_name(self))) self._trajectory_generator = obj
def parse(text): """ Parse a string into an event object. "None" is acceptable. """ if not isinstance(text, str): raise trajtracker.ValueError("invalid event format ({:}) - expecting a string value".format(text)) if re.match('^\s*none\s*$', text.lower()): return None m = re.match('^\s*(\w+)\s*(\+\s*((\d+)|(\d*.\d+)))?\s*$', text) if m is None: raise trajtracker.ValueError("invalid event format ({:}) - expecting event_id or event_id+offset".format(text)) event = Event(m.group(1)) if m.group(2) is not None: event += float(m.group(3)) return event
def parse_text_justification(s): s = s.lower() if s == "left": return 0 elif s == "center": return 1 elif s == "right": return 2 else: raise ttrk.ValueError("Invalid text justification ({:})".format(s))
def parse_size(s): orig_s = s if re.match("^\((.*)\)$", s) is not None: s = s[1:-1] m = re.match("\s*(\d+)\s*:\s*(\d+)\s*", s) if m is None: raise ttrk.ValueError("Invalid format for size: {:}".format(orig_s)) return m.group(1), m.group(2)
def __add__(self, rhs): """Define a new event, in a time offset relatively to an existing event""" _u.validate_func_arg_type(self, "+", "right operand", rhs, numbers.Number) if rhs < 0: raise trajtracker.ValueError("Invalid offset ({:}) for event {:}. Only events with positive offset are acceptable".format( rhs, self._event_id)) new_event = Event(self._event_id) new_event._offset = self._offset + rhs return new_event
def get_response_buttons_size(self): width, height = validate_config_param_type("resp_btn_size", ttrk.TYPE_SIZE, self.config.resp_btn_size) # -- If width/height are between [-1,1], they mean percentage of screen size if -1 < width < 1: width = int(width * self.screen_size[0]) elif not isinstance(width, int): raise ttrk.ValueError( "Invalid config.resp_btn_size: a non-integer width was provided ({:})" .format(width)) if -1 < height < 1: height = int(height * self.screen_size[1]) elif not isinstance(height, int): raise ttrk.ValueError( "Invalid config.resp_btn_size: a non-integer height was provided ({:})" .format(height)) return width, height
def parse_rgb(s): orig_s = s if re.match("^\((.*)\)$", s) is not None: s = s[1:-1] m = re.match('\s*(\d+)\s*:\s*(\d+)\s*:\s*(\d+)\s*', s) if m is None: raise ttrk.ValueError("Invalid RGB format: {:}".format(orig_s)) return m.group(1), m.group(2), m.group(3)
def colormap(self, value): if value is None: #-- No mapping: use default colors self._color_to_code = None elif isinstance(value, str) and value.lower() == "default": #-- Use arbitrary coding self._color_to_code = {} n = 0 for color in sorted(list(self._available_colors)): self._color_to_code[color] = n n += 1 elif isinstance(value, str) and value.lower() == "rgb": # Translate each triplet to an RGB code self._color_to_code = { color: color_rgb_to_num(color) for color in self._available_colors } elif isinstance(value, dict): #-- Use this mapping; but make sure that all colors from the image were defined missing_colors = set() for color in self._available_colors: if color not in value: missing_colors.add(color) if len(missing_colors) > 0: raise trajtracker.ValueError("Invalid value for {:}.color_codes - some colors are missing: {:}".format( _u.get_type_name(self), missing_colors)) self._color_to_code = value.copy() elif isinstance(value, type(lambda:1)): #-- A function that maps each color to a code self._color_to_code = { color: value(color) for color in self._available_colors } else: raise trajtracker.ValueError( "{:}.color_codes can only be set to None, 'default', or a dict. Invalid value: {:}".format( _u.get_type_name(self), value)) self._log_property_changed("colormap", value)
def activate(self, key): """ Set one of the stimuli as the active one. :param key: The key of the stimulus, as set in :func:`~trajtracker.stimuli.ChangingStimulus.add_stimulus` """ if key is None or key in self._stimuli: if self._should_log(ttrk.log_trace): self._log_write("Activate,{:}".format(key), True) self._active_key = key else: raise ttrk.ValueError( "{:}.select(key={:}) - this stimulus was not defined".format( _u.get_type_name(self), key))
def parse_rgb(value): if isinstance(value, tuple): return value if not isinstance(value, str): raise ttrk.TypeError('Invalid RGB "{:}" - expecting a string'.format(value)) m = re.match('^\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$', value) if m is None: raise ttrk.ValueError('Invalid RGB "{:}"'.format(value)) return int(m.group(1)), int(m.group(2)), int(m.group(3))
def exit_area(self, value): if value is None: self._exit_area = None elif isinstance(value, str): self._exit_area = self._create_default_exit_area(value) self._log_property_changed("exit_area", value=value) elif "overlapping_with_position" in dir(value): self._exit_area = value self._log_property_changed("exit_area", value="shape") else: raise ttrk.ValueError("invalid value for %s.exit_area" % _u.get_type_name(self)) self._log_property_changed("exit_area")
def load_sound(config, filename): """ Load a sound file :param config: The experiment configuration object :param filename: No path needed. The file is expected to be under config.sounds_dir """ full_path = config.sounds_dir + "/" + filename if not os.path.isfile(full_path): raise ttrk.ValueError('Sound file {:} does not exist. Please check the file name (or perhaps you need to change config.sounds_dir)'. format(full_path)) sound = xpy.stimuli.Audio(full_path) sound.preload() return sound
def _get_response_buttons_colors(exp_info): color = exp_info.config.resp_btn_colours if u.is_rgb(color): #-- One color given return color, color if not trajtracker.utils.is_collection(color) or len(color) != 2 or \ not u.is_rgb(color[0]) or not u.is_rgb(color[1]): raise ttrk.ValueError( "config.resp_btn_colours should be either a color (R,G,B) " + "or a pair of colors (left_rgb, right_rgb). " + "The value provided is invalid: {:}".format(color)) return color
def _validate(self): self._validate_property("shown_stimuli") missing_pics = [ s_id for s_id in self._shown_stimuli if s_id not in self._available_stimuli ] if len(missing_pics) > 0: raise ttrk.ValueError( "shown_stimuli includes unknown stimulus IDs: {:}".format( ", ".join(missing_pics))) n_stim = len(self._shown_stimuli) self._validate_property("position", n_stim) self._validate_property("onset_time", n_stim) self._validate_property("duration", n_stim)
def movement_started(self, time): """ Called when the finger/mouse starts moving """ self._log_func_enters("finger_started_moving", [time]) if not isinstance(time, numbers.Number): raise trajtracker.ValueError( _u.ErrMsg.invalid_method_arg_type(self.__class__, "reset", "numeric", "time", time)) self._time0 = time if self._show_guide: #-- Guide should appear. Don't present it immediately, because its position is #-- not updated yet; just mark that it should be changed to visible self._should_set_guide_visible = True
def _create_default_exit_area(self, name): name = name.lower() if name == "above": f, t = -45, 45 elif name == "right": f, t = 45, 135 elif name == "below": f, t = 135, 215 elif name == "left": f, t = 215, -45 else: raise ttrk.ValueError("unsupported exit area '%s'" % name) return nvshapes.Sector(self._start_area.position[0], self._start_area.position[1], 10000, f, t)