def __init__(self, units_per_mm, min_distance=0, angle_units=Units.Degrees, zero_angle=0, min_angle_change_per_curve=0): """ Constructor :param units_per_mm: See :attr:`~trajtracker.movement.DirectionMonitor.units_per_mm` :param min_distance: See :attr:`~trajtracker.movement.DirectionMonitor.min_distance` :param angle_units: See :attr:`~trajtracker.movement.DirectionMonitor.angle_units` :param min_angle_change_per_curve: See :attr:`~trajtracker.movement.DirectionMonitor.min_angle_change_per_curve` """ super(DirectionMonitor, self).__init__() _u.validate_func_arg_type(self, "__init__", "units_per_mm", units_per_mm, numbers.Number) _u.validate_func_arg_positive(self, "__init__", "units_per_mm", units_per_mm) self._units_per_mm = units_per_mm self.min_distance = min_distance self.angle_units = angle_units self.zero_angle = zero_angle self.min_angle_change_per_curve = min_angle_change_per_curve self.reset()
def get_traj_point(self, time): """ Generate the trajectory - get one time point data :param time: in seconds :return: (x, y, visible) """ _u.validate_func_arg_type(self, "get_traj_point", "time", time, numbers.Number) _u.validate_func_arg_not_negative(self, "get_traj_point", "time", time) #-- Handle time-too-large total_duration = self.duration if self._cyclic: time = time % total_duration elif time >= total_duration: last_segment = self._segments[-1] return last_segment['generator'].get_traj_point( last_segment['duration']) #-- Find relevant segment generator, time = self._get_generator_for(time) #-- Get point from the relevant segment return generator.get_traj_point(time)
def overlapping_with_position(self, pos): _u.validate_func_arg_is_collection(self, "overlapping_with_position", "pos", pos, 2, 2) _u.validate_func_arg_type(self, "overlapping_with_position", "pos[0]", pos[0], numbers.Number) _u.validate_func_arg_type(self, "overlapping_with_position", "pos[1]", pos[1], numbers.Number) x = pos[0] - self._position[0] y = pos[1] - self._position[1] height = self._size[1] width = self._size[0] if self._rotation != 0: #-- Instead of rotating the rectangle - rotate the point in the opposite direction # Get the point's position relatively to the rectangle's center as r, alpha. # alpha=0 means that the point is to the right of the rectangle center r = np.sqrt(x**2 + y**2) if (x == 0): alpha = np.pi / 2 if y > 0 else np.pi * 3 / 2 else: alpha = np.arctan(y / x) alpha += self._rotation_radians x = np.cos(alpha) * r y = np.sin(alpha) * r return -(width / 2) <= x <= width / 2 and -(height / 2) <= y <= height / 2
def __init__(self, x, y, radius, from_angle, to_angle): """ Constructor :param x: the circle's center :param y: the circle's center :param radius: :param from_angle: Left end of the sector (degrees) :param to_angle: Right end of the sector (degrees) """ _u.validate_func_arg_type(self, "__init__", "x", x, int) _u.validate_func_arg_type(self, "__init__", "y", y, int) _u.validate_func_arg_type(self, "__init__", "radius", radius, int) _u.validate_func_arg_type(self, "__init__", "from_angle", from_angle, int) _u.validate_func_arg_type(self, "__init__", "to_angle", to_angle, int) _u.validate_func_arg_positive(self, "__init__", "radius", radius) self.x = x self.y = y self.radius = radius self.from_angle = from_angle % 360 self.to_angle = to_angle % 360
def add(self, stimulus, stimulus_id=None, visible=True): """ Add a stimulus to the container. :param stimulus: An Expyriment stimulus, or any other object that has a similar present() method :param stimulus_id: Stimulus name. Use it later to set the stimulus as visible/invisible. If not provided or None, an arbitrary ID will be generated. :param visible: See The stimulus ID (as defined in :func:`~trajtracker.stimuli.StimulusContainer.set_visible`) :return: """ _u.validate_func_arg_type(self, "add", "visible", visible, bool) if "present" not in dir(stimulus): raise ttrk.TypeError( "invalid stimulus ({:}) in {:}.add() - expecting an expyriment stimulus" .format(stimulus, _u.get_type_name(self))) stimulus.visible = visible if stimulus_id is None: n = len(self._stimuli) + 1 while True: stimulus_id = "stimulus#{:}".format(n) if stimulus_id in self._stimuli: n += 1 else: break order = len(self._stimuli) if stimulus_id not in self._stimuli: order += 1 self._stimuli[stimulus_id] = dict(id=stimulus_id, stimulus=stimulus, order=order)
def add_stimulus(self, stim_id, stimulus): """ Add a stimulus to the set of available stimuli. :param stim_id: A logical name of the stimulus :type stim_id: str :param stimulus: an expyriment stimulus """ _u.validate_func_arg_type(self, "add_stimulus", "stim_id", stim_id, str) if "is_preloaded" not in dir(stimulus) or "present" not in dir( stimulus): raise ttrk.TypeError( "Invalid stimulus in {:}.add_stimulus() - {:}".format( _u.get_type_name(self), stimulus)) if stim_id in self._available_stimuli and self._should_log( ttrk.log_warn): self._log_write( 'WARNING: Stimulus "{:}" already exists in the {:}, definition will be overriden' .format(stim_id, _u.get_type_name(self))) if not stimulus.is_preloaded: stimulus.preload() self._available_stimuli[stim_id] = stimulus self._container.add(stimulus, stim_id, visible=False)
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 show_cursor(self, show): """ Show/hide the mouse pointer :param: show :type show: bool """ _u.validate_func_arg_type(self, "show_cursor", "show", show, bool) if show: self._xpy_mouse.show_cursor() else: self._xpy_mouse.hide_cursor()
def register_onset_offset_callback_func(self, func): """ Register a function that should be called when a stimulus is shown/hidden :param func: The function, which gets 4 parameters: 1. The MultiStimulus/MultiTextBox object 2. The stimulus/text number (0=first) 3. Whether the stimulus is presented (True) or hidden (False) 4. The time when the corresponding present() function returned """ _u.validate_func_arg_type(self, "add_onset_offset_callback_func", "func", func, ttrk.TYPE_CALLABLE) self._onset_offset_callbacks.append(func)
def add_segment(self, traj_generator, duration): _u.validate_func_arg_type(self, "add_segment", "duration", duration, numbers.Number) _u.validate_func_arg_positive(self, "add_segment", "duration", duration) if "get_traj_point" not in dir(traj_generator): raise trajtracker.TypeError( "{:}.add_segment() was called with an invalid traj_generator argument ({:})" .format(_u.get_type_name(self), traj_generator)) self._segments.append(dict(generator=traj_generator, duration=duration))
def extend(self, extend_by): """ Extend the rectangle by the given value :param extend_by: If a pair of values, the mean (w, h): extend the rectangle's width by w and its height by h. If this is a single value, it is used for extending the width as well as the height. """ if isinstance(extend_by, numbers.Number): extend_by = extend_by, extend_by else: _u.validate_func_arg_type(self, "extend", "extend_by", extend_by, ttrk.TYPE_SIZE) self.size = self._size[0] + extend_by[0], self._size[1] + extend_by[1]
def add_segments(self, segments): _u.validate_func_arg_is_collection(self, "add_segments", "segments", segments) for i in range(len(segments)): segment = segments[i] _u.validate_func_arg_is_collection(self, "add_segments", "segments[%d]" % i, segment, 2, 2) _u.validate_func_arg_type(self, "add_segments", "segments[%d][1]" % i, segment[1], numbers.Number) _u.validate_func_arg_positive(self, "add_segments", "segments[%d][1]" % i, segment[1]) self.add_segment(segment[0], segment[1])
def unregister_recurring_callback(self, func_id): """ Unregister a recurring listener function that was previously registered via :func:`~trajtracker.stimuli.StimulusContainer.register_callback` :param func_id: The function ID that was provided to :func:`~trajtracker.stimuli.StimulusContainer.register_callback` :return: *True* if unregistered, *False* if there is no registered recurring function with the given func_id """ _u.validate_func_arg_type(self, "unregister_recurring_callback", "func_id", func_id, str) if func_id in self._recurring_callbacks: del self._recurring_callbacks[func_id] return True else: return False
def xy_to_pixels(value, screen_size, parameter_name): """ Translate a stimulus size or position to pixels. The input is either one value (x or y) or a pair of values (x, y). It may denote either a stimulus size or a position. If x/y is an int value (or a pair of ints), it is left unchanged. If it is a float value, it is interpreted as percentage of the screen size. In this case, the value should be between 0.0 and 1.0 for size, or between -0.5 and 0.5 for position (but the function will accept any value between -0.5 and 1.0) :param value: The value to convert - either a number or a pair of numbers :param screen_size: The screen size - either a number or a pair of numbers (must match the "value" paremeter) :param parameter_name: If this is not None, errors will yield an exception, indicating this parameter :return: An int or a pair of ints (scale = pixels). If the input value is not a valid size, return None. """ if isinstance(value, numbers.Number): #-- A single value if isinstance(value, int): return value elif -0.5 <= value <= 1: _u.validate_func_arg_type(None, "common.xy_to_pixels", "screen_size", screen_size, int) return int(np.round(value * screen_size)) else: raise ttrk.TypeError('Invalid value of {:} ({:})'.format(parameter_name, value)) if u.is_collection(value) and len(value) == 2 and \ isinstance(value[0], numbers.Number) and isinstance(value[1], numbers.Number): #-- Pair of values _u.validate_func_arg_is_collection(None, "common.xy_to_pixels", "screen_size", screen_size, 2, 2) return xy_to_pixels(value[0], screen_size[0], "{:}[0]".format(parameter_name)), \ xy_to_pixels(value[1], screen_size[1], "{:}[1]".format(parameter_name)) else: #-- Not a valid xy raise ttrk.TypeError('Invalid value of {:} ({:})'.format(parameter_name, value))
def set_trajectory(self, traj_id, traj_data): """ Add a single trajectory (or replace an existing one) :param traj_id: A logical ID for this trajectory. :param traj_data: The trajectory data - a list/tuple of per-timepoint data. Per time point, specify a list/tuple of with 3 or 4 elements: time (> 0), x coordinate (int), y coordinate (int), and optional "visible" (bool) """ _u.validate_func_arg_anylist(self, "set_trajectory", "traj_data", traj_data, min_length=1) if traj_id is None: raise TypeError("trajtracker error: {:}.set_trajectory(traj_id=None) is invalid".format(type(self).__name__)) coords = [] times = [] visible = [] prev_time = -1 for i in range(len(traj_data)): time_point = traj_data[i] _u.validate_func_arg_anylist(self, "set_trajectory", "traj_data[%d]" % i, time_point, min_length=3, max_length=4) _u.validate_func_arg_type(self, "set_trajectory", "traj_data[%d][0]" % i, time_point[0], numbers.Number) _u.validate_func_arg_not_negative(self, "set_trajectory", "traj_data[%d][0]" % i, time_point[0]) _u.validate_func_arg_type(self, "set_trajectory", "traj_data[%d][1]" % i, time_point[1], int) _u.validate_func_arg_type(self, "set_trajectory", "traj_data[%d][2]" % i, time_point[2], int) time = time_point[0] if time <= prev_time: raise ValueError(("trajtracker error: {:}.set_trajectory() called with invalid value for trajectory '{:}' " + "- timepoint {:} appeared after {:}").format(type(self).__name__, traj_id, time, prev_time)) prev_time = time times.append(time) coords.append((time_point[1], time_point[2])) if len(time_point) == 4: _u.validate_func_arg_type(self, "set_trajectory", "traj_data[%d][3]" % i, time_point[3], bool) visible.append(time_point[3]) else: visible.append(True) self._trajectories[traj_id] = { 'times': np.array(times), 'coords': coords, 'visible': visible, 'duration': times[-1] } self._validation_err = None
def get_traj_point(self, time): """ Return the trajectory info at a certain time :param time: in seconds :returns: a dict with the coordinates ('x' and 'y' entries). """ _u.validate_func_arg_type(self, "get_xy", "time", time, numbers.Number) if self._start_point is None: raise trajtracker.InvalidStateError( "{:}.get_xy() was called without setting start_point".format( _u.get_type_name(self))) if self._end_point is None: raise trajtracker.InvalidStateError( "{:}.get_xy() was called without setting end_point".format( _u.get_type_name(self))) if self._duration is None: raise trajtracker.InvalidStateError( "{:}.get_xy() was called without setting duration".format( _u.get_type_name(self))) max_duration = self._duration * (2 if self._return_to_start else 1) if self._cyclic: time = time % max_duration else: time = min(time, max_duration) if time > self._duration: #-- Returning to start time -= self._duration start_pt = self._end_point end_pt = self._start_point else: start_pt = self._start_point end_pt = self._end_point time_ratio = time / self._duration x = start_pt[0] + time_ratio * (end_pt[0] - start_pt[0]) y = start_pt[1] + time_ratio * (end_pt[1] - start_pt[1]) return dict(x=x, y=y)
def update(self, clicked, xy): """ Update the slider according to mouse movement **Note**: Unlike other stimulus objects in TrajTracker, here you should call the update() function also when the mouse is unclicked. :param clicked: Whether the mouse is presently clicked or not :param xy: The coordinate to which the gauge is being dragged (x, y). This parameter is ignored in some situations, e.g., when clicked=False, when slider is locked, etc. """ _u.validate_func_arg_type(self, "update", "clicked", clicked, (bool, int)) clicked = bool(clicked) _u.validate_func_arg_type(self, "update", "xy", xy, ttrk.TYPE_COORD) if self.locked: return clicked = clicked and self._is_valid_mouse_pos(xy) if clicked: if not self._now_dragging: #-- Started clicking self._n_moves += 1 self._gauge_stimulus.visible = self.visible self._log_write_if(ttrk.log_trace, "Start moving slider") self._now_dragging = True coord = xy[self._orientation_ind] self._current_value = self._coord_to_value(coord) self._move_gauge_to_coord(coord) elif self._now_dragging: # and not clicked #-- Finger/mouse was just lifted self._now_dragging = False if self._max_moves is not None and self._n_moves >= self._max_moves: # Lifted too many times: lock the slider self.locked = True
def show(self, coord, line_mode): if self._guide_line is None: self._create_guide_line( ) # try creating again. Maybe the experiment was inactive if self._guide_line is None: raise trajtracker.InvalidStateError( "The visual guide for {:} cannot be created because the experiment is inactive" .format(GlobalSpeedValidator.__name__)) _u.validate_func_arg_type(self, "show", "coord", coord, int) _u.validate_func_arg_type(self, "show", "line_mode", line_mode, self.LineMode) self._guide_line.activate(line_mode) pos = (coord, 0) if self._validator.axis == ValidationAxis.x else (0, coord) self._guide_line.position = pos self._guide_line.present(clear=False, update=False)
def current_value(self, value): _u.validate_func_arg_type(self, "set_current_value", "value", value, Number, none_allowed=True) if self._locked: raise ttrk.InvalidStateError( "{:}.current_value cannot be changed - the slider is locked". format(_u.get_type_name(self))) self._current_value = None if value is None else self._crop_value( value) self.gauge_stimulus.visible = self.visible and (value is not None) if value is not None: self._move_gauge_to_coord(self._value_to_coord( self._current_value))
def show(self, coord, line_mode): self._log_func_enters("show", [coord, line_mode]) if self._guide_line is None: self._create_guide_line( ) # try creating again. Maybe the experiment was inactive if self._guide_line is None: raise trajtracker.InvalidStateError( "The visual guide for {:} cannot be created because the experiment is inactive" .format(_u.get_type_name(self))) _u.validate_func_arg_type(self, "show", "coord", coord, int) _u.validate_func_arg_type(self, "show", "line_mode", line_mode, self.LineMode) self._guide_line.activate(line_mode) pos = (coord, 0) if self._validator.axis == ValidationAxis.x else (0, coord) self._guide_line.position = pos
def __init__(self, event_id, extends=None): """ Constructor - invoked when you create a new object by writing Event() :param event_id: A string that uniquely identifies the event :type event_id: str :param extends: If this event extends another one (see details in :ref:`event-hierarchy`) :type extends: Event """ super(Event, self).__init__() _u.validate_func_arg_type(self, "__init__", "event_id", event_id, str) _u.validate_func_arg_type(self, "__init__", "extends", extends, Event, True) self._event_id = event_id self._offset = 0 self._extended = False self._extends = extends if extends is not None: extends._extended = True
def update_xyt(self, x_coord, y_coord, time): """ Track a point. If tracking is currently inactive, this function will do nothing. """ if not self._tracking_active: return _u.validate_func_arg_type(self, "update_xyt", "x_coord", x_coord, numbers.Number) _u.validate_func_arg_type(self, "update_xyt", "y_coord", y_coord, numbers.Number) _u.validate_func_arg_type(self, "update_xyt", "time", time, numbers.Number) _u.validate_func_arg_not_negative(self, "update_xyt", "time", time) self._trajectory['x'].append(x_coord) self._trajectory['y'].append(y_coord) self._trajectory['time'].append(time) if self._log_level: expyriment._active_exp._event_file_log( "Trajectory,Track_xyt,{0},{1},{2}".format( x_coord, y_coord, time), 2)
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 register_callback(self, callback_func, recurring=False, func_id=None): """ Register a "present callback" - a function that should be called when the StimulusContainer is present()ed :param callback_func: A function or another callable object, which will be called. This function takes these arguments - 1. The StimulusContainer object 2. a tuple with the IDs of the stimuli that were actually presented (i.e., stimuli that had stim.visible == True) 3. The time when present() returned :param recurring: True = invoke the function on each present() call. False = Invoke the function only on the next present(), and then forget this function. :param func_id: A logical ID for a recurring function (it can be used to unregister the function later) """ _u.validate_func_arg_type(self, "register_callback", "callback_func", callback_func, ttrk.TYPE_CALLABLE) _u.validate_func_arg_type(self, "register_callback", "recurring", recurring, bool) if recurring: if func_id == "": func_id = None _u.validate_func_arg_type(self, "register_callback", "func_id", func_id, str) self._recurring_callbacks[func_id] = callback_func else: self._non_recurring_callbacks.append(callback_func)
def update_xyt(self, x_coord, y_coord, time): """ Call this method whenever the finger/mouse moves :param time: use the same time scale provided to reset() """ _u.validate_func_arg_type(self, "update_xyt", "x_coord", x_coord, numbers.Number) _u.validate_func_arg_type(self, "update_xyt", "y_coord", y_coord, numbers.Number) _u.validate_func_arg_type(self, "update_xyt", "time", time, numbers.Number) self._validate_time(time) if self._time0 is None: self._time0 = time #-- Set coordinate space x_coord /= self._units_per_mm y_coord /= self._units_per_mm #-- Find distance to recent coordinate if len(self._recent_points) > 0: last_loc = self._recent_points[-1] distance = np.sqrt((x_coord - last_loc[0])**2 + (y_coord - last_loc[1])**2) else: distance = 0 self._remove_recent_points_older_than(time - self._calculation_interval) #-- Remember current coords & time self._recent_points.append((x_coord, y_coord, time, distance))
def check_xy(self, x_coord, y_coord): """ Check whether the new finger coordinates imply starting a trial :return: State.init - if the finger/mouse touched in the start area for the first time State.start - if the finger/mouse left the start area in a valid way (into the exit area) State.error - if the finger/mouse left the start area in an invalid way (not to the exit area) None - if the finger/mouse didn't cause any change in the "start" state """ _u.validate_func_arg_type(self, "check_xy", "x_coord", x_coord, numbers.Number) _u.validate_func_arg_type(self, "check_xy", "y_coord", y_coord, numbers.Number) if self._state == self.State.reset: #-- Trial not initialized yet: waiting for a touch inside start_area if self._start_area.overlapping_with_position((x_coord, y_coord)): self._state = self.State.init return self._state else: return None elif self._state == self.State.init: #-- Trial initialized but not started: waiting for a touch outside start_area if self._start_area.overlapping_with_position((x_coord, y_coord)): # still in the start area return None elif self._exit_area.overlapping_with_position((x_coord, y_coord)): # Left the start area into the exit area self._state = self.State.start else: # Left the start area into another (invalid) area self._state = self.State.error return self._state return None
def load_file(self, filename): """ Load data from the CSV file :return: a tuple with two elements: (1) list with one dict per row, transformed to the required types; (2) List of field names that were found in the file. """ _u.validate_func_arg_type(self, "load_file", "filename", filename, str) rows, fieldnames = self._read_data_from_file(filename) self._validate_field_names(fieldnames, filename) #-- Transform data result = [] for row in rows: if not self._case_sensitive_col_names: row = self._transform_lowercase(row) row = self._transform_types(row, filename) result.append(row) return result, fieldnames
def update(self, time): """ Call this function on each frame where you want the animated object to move. :param time: The time (typically - time from start of trial) """ _u.validate_func_arg_type(self, "update", "time", time, numbers.Number) if self._animated_object is None or self._trajectory_generator is None: return relative_time = time - self._time0 traj_point = self._trajectory_generator.get_traj_point(relative_time) x = traj_point[ 'x'] if 'x' in traj_point else self._animated_object.position[0] y = traj_point[ 'y'] if 'y' in traj_point else self._animated_object.position[1] visible = traj_point['visible'] if 'visible' in traj_point else True self._animated_object.position = x, y if visible: self._animated_object.present(update=self._do_update_screen, clear=self._do_clear_screen)
def get_traj_point(self, time): """ Return the trajectory info at a certain time :param time: in seconds :returns: a dict with the coordinates ('x' and 'y' entries). """ _u.validate_func_arg_type(self, "get_xy", "time", time, numbers.Number) if not hasattr(self, "_center"): raise trajtracker.InvalidStateError( "trajtracker error: {:}.get_xy() was called without setting center" .format(type(self).__name__)) if not hasattr(self, "_degrees_per_sec"): raise trajtracker.InvalidStateError( "trajtracker error: {:}.get_xy() was called without setting degrees_per_sec" .format(type(self).__name__)) if not hasattr(self, "_radius"): raise trajtracker.InvalidStateError( "trajtracker error: {:}.get_xy() was called without setting radius" .format(type(self).__name__)) curr_degrees = (self._degrees_at_t0 + self._degrees_per_sec * time) % 360 curr_degrees_rad = curr_degrees / 360 * np.pi * 2 x = int(np.abs(np.round(self._radius * np.sin(curr_degrees_rad)))) y = int(np.abs(np.round(self._radius * np.cos(curr_degrees_rad)))) if curr_degrees > 180: x = -x if 90 < curr_degrees < 270: y = -y return {'x': x + self._center[0], 'y': y + self._center[1]}
def _check_xyt_validate_and_log(self, x_coord, y_coord, time, time_used=True): _u.validate_func_arg_type(self, "check_xyt", "x_coord", x_coord, numbers.Number, type_name="numeric") _u.validate_func_arg_type(self, "check_xyt", "y_coord", y_coord, numbers.Number, type_name="numeric") if time_used: _u.validate_func_arg_type(self, "check_xyt", "time", time, numbers.Number, type_name="numeric") if self._should_log(self.log_trace): msg = "{0}.check_xyt,{1},{2}".format(type(self).__name__, x_coord, y_coord) if time_used: msg += ",{0}".format(time) self._log_write(msg)