def create_common_experiment_objects(exp_info): """ Create configuration for the experiment - object common to several paradigms. exp_info.trials must be set before calling this function. :param exp_info: :type exp_info: trajtracker.paradigms.num2pos.ExperimentInfo """ if exp_info.trials is None: raise ttrk.InvalidStateError('Please update exp_info.trials before calling create_common_experiment_objects()') create_start_point(exp_info) create_textbox_target(exp_info) create_generic_target(exp_info) create_fixation(exp_info) create_errmsg_textbox(exp_info) create_traj_tracker(exp_info) create_validators(exp_info, direction_validator=True, global_speed_validator=True, inst_speed_validator=True, zigzag_validator=True) create_confidence_slider(exp_info) #-- Initialize experiment-level data exp_info.exp_data['WindowWidth'] = exp_info.screen_size[0] exp_info.exp_data['WindowHeight'] = exp_info.screen_size[1] exp_info.exp_data['nExpectedTrials'] = len(exp_info.trials) exp_info.exp_data['nTrialsCompleted'] = 0 exp_info.exp_data['nTrialsFailed'] = 0 exp_info.exp_data['nTrialsSucceeded'] = 0
def right_resp_text(self, target): if self._right_resp_text is not None: raise ttrk.InvalidStateError( "ExperimentInfo.right_resp_text cannot be set twice") self._right_resp_text = target self.stimuli.add(target.stimulus, "right_resp_text")
def trial_configured_event(self, event): _u.validate_attr_type(self, "registration_event", event, Event) if self._event_manager is not None: raise ttrk.InvalidStateError(("{:}.trial_configured_event cannot be changed after " + "registering to the event manager".format(_u.get_type_name(self)))) self._trial_configured_event = event self._log_property_changed("trial_configured_event")
def save_to_file(self, trial_num): """ Save the tracked trajectory (ever since the last reset() call) to a CSV file :param trial_num: :return: The number of rows printed to the file """ if self._filename is None: raise trajtracker.InvalidStateError( 'TrajectoryTracker.save_to_file() was called before calling init_output_file()' ) fh = self._open_file(self._filename, 'a') rows = self.get_xyt() for x, y, t in rows: x = ('%d' % x) if isinstance(x, int) else '%.*f' % (self._xy_precision, x) y = ('%d' % y) if isinstance(y, int) else '%.*f' % (self._xy_precision, y) fh.write("%d,%.*f,%s,%s\n" % (trial_num, self._time_precision, t, x, y)) fh.close() if self._log_level: expyriment._active_exp._event_file_log( "Trajectory,SavedTrial,%s,%d,%d" % (self._filename, trial_num, len(rows)), 2) return len(rows)
def _validate_time(self, time): if len(self._prev_locations ) > 0 and self._prev_locations[-1][2] > time: raise trajtracker.InvalidStateError( "{0}.mouse_at() was called with time={1} after it was previously called with time={2}" .format(self.__class__, time, self._prev_locations[-1][2]))
def save_to_file(self, trial_num): """ Save the tracked trajectory (ever since the last reset() call) to a CSV file :param trial_num: :return: The number of rows printed to the file """ if not self._out_file_initialized: raise ttrk.InvalidStateError( 'TrajectoryTracker.save_to_file() was called before calling init_output_file()' ) fh = self._open_file(self._filename, 'a') rows = self.get_xyt() for x, y, t in rows: x = ('%d' % x) if isinstance(x, int) else '%.*f' % (self._xy_precision, x) y = ('%d' % y) if isinstance(y, int) else '%.*f' % (self._xy_precision, y) fh.write("%d,%.*f,%s,%s\n" % (trial_num, self._time_precision, t, x, y)) fh.close() self._log_write_if( ttrk.log_debug, "Saved trial #{:} (with {:} rows) to {:}".format( trial_num, len(rows), self._filename), True) return len(rows)
def target_pointer(self, value): if self._target_pointer is not None: raise ttrk.InvalidStateError( "ExperimentInfo.target_pointer cannot be set twice") self._target_pointer = value self.stimuli.add(value, "target_pointer", visible=False)
def numberline(self, nl): if self._numberline is not None: raise ttrk.InvalidStateError( "ExperimentInfo.numberline cannot be set twice") self._numberline = nl self.stimuli.add(nl, "numberline") self._trajectory_sensitive_objects.append(nl) self._event_sensitive_objects.append(nl)
def can_exit_in_any_direction(self, value): if self._preloaded: raise ttrk.InvalidStateError( "{:}.can_exit_in_any_direction cannot be set after the object was preloaded" .format(_u.get_type_name(self))) _u.validate_attr_type(self, "can_exit_in_any_direction", value, bool) self._can_exit_in_any_direction = value self._log_property_changed("can_exit_in_any_direction")
def _validate_time(self, time): #-- Validate that times are provided in increasing order prev_time = self._recent_points[-1][2] if len( self._recent_points) > 0 else self._time0 if prev_time is not None and prev_time > time: raise trajtracker.InvalidStateError( "{0}.update_xyt() was called with time={1} after it was previously called with time={2}" .format(self.__class__, time, prev_time))
def colour(self, value): if self._preloaded: raise ttrk.InvalidStateError( "{:}.colour cannot be set after the object was preloaded". format(_u.get_type_name(self))) _u.validate_attr_rgb(self, "colour", value) self._colour = value self._log_property_changed("colour")
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 rotation(self, value): if self._preloaded: raise ttrk.InvalidStateError( "{:}.rotation cannot be set after the object was preloaded". format(_u.get_type_name(self))) _u.validate_attr_numeric(self, "rotation", value) value = value % 360 self._rotation = value self._log_property_changed("rotation")
def mark_as_initialized(self): """ Force the StartPoint object into an "init" :attr:`~trajtracker.movement.StartPoint.State`, as if it was touched by the mouse/finger. This is useful in case trial initiation was triggered in another way. """ if self._state not in (StartPoint.State.reset, StartPoint.State.mouse_up): raise ttrk.InvalidStateError( "StartPoint.mark_as_initialized() called but the StartPoint's present state is {:}" .format(self._state)) self._state = StartPoint.State.init
def enable_events(self, value): value = _u.validate_attr_is_collection(self, "enable_events", value, none_allowed=True) for v in value: _u.validate_attr_type(self, "enable_events", v, Event) if self._registered: raise trajtracker.InvalidStateError( "{:}.enable_events cannot be set after the object was registered to the event manager" .format(_u.get_type_name(self))) self._enable_events = tuple(value) self._log_property_changed("enable_events")
def on_registered(self, event_manager): if self.enable_event is None: raise trajtracker.InvalidStateError( "{:} was registered to an event manager before updating enable_event" .format(_u.get_type_name(self))) #-- Intercept the event that indicates when the movement started if self._registered: raise trajtracker.InvalidStateError( "{:} cannot be registered twice to an event manager".format( _u.get_type_name(self))) # noinspection PyUnusedLocal def callback_start(time_in_trial, time_in_session): self.movement_started(time_in_trial) event_manager.register_operation( self.enable_event, callback_start, recurring=True, description="{:}.movement_started()".format( _u.get_type_name(self))) #-- Intercept the event that indicates when the movement terminates # noinspection PyUnusedLocal def callback_end(t1, t2): if self._show_guide: self._guide.stimulus.visible = False self._should_set_guide_visible = False event_manager.register_operation( self.disable_event, callback_end, recurring=True, description="{:}: hide speed guide".format(_u.get_type_name(self)))
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 show(self): """ Show the dots in the virtual rectangle's corners """ self._log_func_enters("show") if self._need_to_regenerate_dots: raise ttrk.InvalidStateError( '{:}.show() was called without calling reset() first'.format( _u.get_type_name(self))) self._set_dot_position_for_time(0) self._start_zoom_time = None self._now_visible = True for dot in self._dots: dot.visible = True
def position(self, value): if self._preloaded: raise ttrk.InvalidStateError( "{:}.position cannot be set after the object was preloaded". format(_u.get_type_name(self))) if value is None: self._position = None else: _u.validate_attr_is_collection(self, "size", value, 2, 2) _u.validate_attr_numeric(self, "size[0]", value[0]) _u.validate_attr_numeric(self, "size[1]", value[1]) self._position = (int(value[0]), int(value[1])) self._log_property_changed("position")
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 _validate_unlocked(self): if self._preloaded: raise trajtracker.InvalidStateError( 'An attempt was made to change the visual properties of a NumberLine after it was already plotted' )
def check_xyt(self, x_coord, y_coord, time): """ Validate movement. :param x_coord: :type x_coord: int :param y_coord: :type y_coord: int :param time: Time from start of trial :returns: None if all OK; ValidationFailed object if error """ self._check_xyt_validate_and_log(x_coord, y_coord, time) self._assert_initialized(self._origin_coord, "origin_coord") self._assert_initialized(self._end_coord, "end_coord") self._assert_initialized(self._max_trial_duration, "max_trial_duration") if not self._enabled: return None #-- If this is the first call in a trial: do nothing if self._time0 is None: self.reset(time) return None if time < self._time0: raise trajtracker.InvalidStateError( "{0}.check_xyt() was called with time={1}, but the trial started at time={2}" .format(self.__class__, time, self._time0)) time -= self._time0 #-- Get the expected and actual coordinates coord = x_coord if self._axis == ValidationAxis.x else y_coord expected_coord = int(self.get_expected_coord_at_time(time)) d_coord = coord - expected_coord #-- No validation during grace period if time <= self._grace_period: if self._show_guide: self._guide.show(expected_coord, GlobalSpeedGuide.LineMode.Grace) return None #-- Actual coordinate must be ahead of the expected minimum if d_coord != 0 and np.sign(d_coord) != np.sign(self._end_coord - self._origin_coord): return self._create_validation_error( self.err_too_slow, "You moved too slowly", { self.arg_expected_coord: expected_coord, self.arg_actual_coord: coord }) if self._show_guide: # Get the coordinate that the mouse/finger should reach shortly coord_expected_soon = self.get_expected_coord_at_time( time + self._guide_warning_time_delta) # check if mouse/finger already reached this coordinate reached_expected_soon = d_coord == 0 or np.sign( coord - coord_expected_soon) == np.sign(self._end_coord - self._origin_coord) # set guide color accordingly self._guide.show( expected_coord, GlobalSpeedGuide.LineMode.OK if reached_expected_soon else GlobalSpeedGuide.LineMode.Error) return None
def _assert_initialized(self, value, attr_name): if value is None: raise trajtracker.InvalidStateError( "{:}.check_xyt() was called before {:} was initalized".format( type(self).__name__, attr_name))
def wait_until_startpoint_touched(self, exp, on_loop_callback=None, on_loop_present=None, event_manager=None, trial_start_time=None, session_start_time=None, max_wait_time=None): """ Wait until the starting point is touched. The *on_loop_xxx* and *event_manager* parameters define what to do on each iteration of the loop that waits for the area to be touched. If neither on_loop_callback nor on_loop_present are provided, the function will wait for 15 ms on each loop iteration. If several on_loop parameters are provided, they will be invoked in this order: *callback - event manager.on_frame() - present()*. :param exp: The Expyriment experiment object :param on_loop_callback: A function (without arguments) to call on each loop iteration. If the function returns any value other than *None*, the waiting will be terminated and that value will be returned. :param on_loop_present: A visual object that will be present()ed on each loop iteration. :param event_manager: The event manager's on_frame() will be called on each loop iteration. If you provide an event manager, you also have to provide trial_start_time and session_start_time (whose values were obtained by :func:`trajtracker.utils.get_time` :param max_wait_time: Maximal time (in seconds) to wait :return: The value returned by the on_loop_callback function (in case it returned anything other than None). Otherwise the function returns None. Use :attr:`~trajtracker.movement.StartPoint.state` to learn about the StartPoint's exit status. """ self._log_func_enters( "wait_until_startpoint_touched", ["exp", on_loop_callback, on_loop_present, event_manager]) _u.validate_func_arg_type(self, "wait_until_startpoint_touched", "max_wait_time", max_wait_time, numbers.Number, none_allowed=True) _u.validate_func_arg_not_negative(self, "wait_until_startpoint_touched", "max_wait_time", max_wait_time) if event_manager is not None: _u.validate_func_arg_type(self, "wait_until_startpoint_touched", "trial_start_time", trial_start_time, numbers.Number, none_allowed=True) _u.validate_func_arg_type(self, "wait_until_startpoint_touched", "session_start_time", session_start_time, numbers.Number) if self._state != StartPoint.State.reset: raise ttrk.InvalidStateError( "{:}.wait_until_startpoint_touched() was called without calling reset() first" .format(_u.get_type_name(self))) time_started_waiting = u.get_time() # The "StartPoint" object is expected to run through these states, in this order: # State.reset - after the trial initialized # State.mouse_up - after the mouse/finger was unclicked/lifted # State.init - when the screen was touched/clicked (this is when this function returns) while True: if not ttrk.env.mouse.check_button_pressed( 0) and self._state == StartPoint.State.reset: # Mouse/finger is UP self._state = StartPoint.State.mouse_up self._log_write_if(ttrk.log_debug, "Mouse unclicked. Setting state=mouse_up", True) elif ttrk.env.mouse.check_button_pressed( 0) and self._state == StartPoint.State.mouse_up: # Mouse/finger touched the screen finger_pos = ttrk.env.mouse.position self.check_xy(finger_pos[0], finger_pos[1]) if max_wait_time is not None and u.get_time( ) - time_started_waiting >= max_wait_time: self._log_func_returns("wait_until_startpoint_touched", False) self._state = StartPoint.State.timeout return None if self._state == StartPoint.State.init: break # Screen touched - we're done here # Invoke custom operations on each loop iteration if on_loop_callback is not None: retval = on_loop_callback() if retval is not None: return retval if event_manager is not None: curr_time = u.get_time() event_manager.on_frame( None if trial_start_time is None else curr_time - trial_start_time, curr_time - session_start_time) if on_loop_present is not None: on_loop_present.present() if on_loop_present is None and on_loop_callback is None: exp.clock.wait(15) xpy.io.Keyboard.process_control_keys() self._log_func_returns("wait_until_startpoint_touched", True) return None
def get_traj_point(self, time): """ Generate the trajectory - get one time point data :param time: in seconds :return: (x, y, visible) """ self.validate() _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) if self._active_traj_id is None: if len(self._trajectories): self.active_traj_id = self._trajectories.keys()[0] else: raise trajtracker.InvalidStateError("{:}.get_traj_point() cannot be called before active_traj_id was set".format(type(self).__name__)) traj_inf = self._trajectories[self._active_traj_id] if time < traj_inf['times'][0]: raise ValueError("trajtracker error in {:}.get_traj_point(time={:}): the active trajectory ({:}) starts from time={:}".format( type(self).__name__, time, self._active_traj_id, traj_inf['times'][0])) #-- Time can't exceed the trajectory duration if time > traj_inf['duration']: time = time % traj_inf['duration'] if self._cyclic else traj_inf['duration'] traj_times = traj_inf['times'] traj_xy = traj_inf['coords'] traj_visible = traj_inf['visible'] ind_before_time = np.where(traj_times <= time)[0][-1] time_before = traj_times[ind_before_time] coord_before = traj_xy[ind_before_time] visible_before = traj_visible[ind_before_time] if time_before == time: ind_after_time = ind_before_time time_after = time_before else: ind_after_time = min(ind_before_time+1, len(traj_times)-1) time_after = traj_times[ind_after_time] if self._interpolate and time_before != time_after: #-- Coordinates: linear interpolation between the two relevant time points coord_after = traj_xy[ind_after_time] weight_of_after_ind = (time - time_before) / (time_after - time_before) weight_of_before_ind = 1 - weight_of_after_ind x = int( np.round(coord_before[0] * weight_of_before_ind + coord_after[0] * weight_of_after_ind) ) y = int( np.round(coord_before[1] * weight_of_before_ind + coord_after[1] * weight_of_after_ind) ) #-- Visibility: use the value from the closest available time point visible_after = traj_visible[ind_after_time] visible = visible_before if weight_of_before_ind > weight_of_after_ind else visible_after return x, y, visible else: #-- Return the value of the last time point before "time" return coord_before + (visible_before, )
def update_xyt(self, position, time_in_trial, time_in_session=None): """ Validate movement. :type position: tuple (x,y) :param time_in_trial: Time from start of trial :param time_in_session: ignored :returns: None if all OK; ExperimentError object if error """ _u.update_xyt_validate_and_log(self, position, time_in_trial) self._assert_initialized(self._origin_coord, "origin_coord") self._assert_initialized(self._end_coord, "end_coord") self._assert_initialized(self._max_movement_time, "max_movement_time") if not self._enabled: return None #-- If this is the first call in a trial: do nothing if self._time0 is None: return if time_in_trial < self._time0: raise trajtracker.InvalidStateError( "{0}.update_xyt() was called with time={1}, but the movement started at time={2}" .format(self.__class__, time_in_trial, self._time0)) time_in_trial -= self._time0 #-- Get the expected and actual coordinates coord = position[0] if self._axis == ValidationAxis.x else position[1] expected_coord = int(self.get_expected_coord_at_time(time_in_trial)) d_coord = coord - expected_coord #-- No validation during grace period if time_in_trial <= self._grace_period: if self._show_guide: self._guide.show(expected_coord, GlobalSpeedGuide.LineMode.Grace) if self._should_set_guide_visible: self._guide.stimulus.visible = True self._should_set_guide_visible = False return None #-- Actual coordinate must be ahead of the expected minimum if d_coord != 0 and np.sign(d_coord) != np.sign(self._end_coord - self._origin_coord): return trajtracker.validators.create_experiment_error( self, self.err_too_slow, "You moved too slowly", { self.arg_expected_coord: expected_coord, self.arg_actual_coord: coord }) if self._show_guide: # Get the coordinate that the mouse/finger should reach shortly coord_expected_soon = self.get_expected_coord_at_time( time_in_trial + self._guide_warning_time_delta) # check if mouse/finger already reached this coordinate reached_expected_soon = d_coord == 0 or np.sign( coord - coord_expected_soon) == np.sign(self._end_coord - self._origin_coord) # set guide color accordingly self._guide.show( expected_coord, GlobalSpeedGuide.LineMode.OK if reached_expected_soon else GlobalSpeedGuide.LineMode.Error) if self._should_set_guide_visible: self._guide.stimulus.visible = True self._should_set_guide_visible = False return None
def _assert_initialized(self, value, attr_name): if value is None: raise trajtracker.InvalidStateError( "{:}.update_xyt() was called before {:} was initalized".format( _u.get_type_name(self), attr_name))