def _set_property(self, prop_name, value, prop_type, allow_single_value=True, allow_none=True): multiple_values = False #convert CSV input string to expyriment colour object if prop_type == 'RGB' and type(value[0])== str: temp = () for i in value: temp = temp + (int(i),) newValue = Colour(temp) value = newValue if value is None and not allow_none: raise ttrk.TypeError("{:}.{:} cannot be set to None".format(_u.get_type_name(self), prop_name)) if value is not None: multiple_values = self._is_multiple_values(value, prop_type) if multiple_values and (prop_type != 'RGB'): for v in value: _u.validate_attr_type(self, prop_name, v, prop_type) value = list(value) elif allow_single_value: _u.validate_attr_type(self, prop_name, value, prop_type, none_allowed=True) else: raise ttrk.TypeError("{:}.{:} must be set to a list of values; a single {:} is invalid".format( _u.get_type_name(self), prop_name, prop_type.__name__ if isinstance(prop_type, type) else prop_type)) setattr(self, "_" + prop_name, value) setattr(self, "_" + prop_name + "_multiple", multiple_values)
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 _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 init_for_trial(self): """ Initialize when a trial starts. Do not use this function if you are working with the events mechanism. """ self._log_func_enters("init_for_trial") if self._event_manager is not None: self._log_write_if(ttrk.log_warn, "init_for_trial() was called although the {:} was registered to an event manager".format( _u.get_type_name(self))) self._configure_and_preload() n_stim = self.n_stim show_ops = list(zip(self._onset_time[:n_stim], [True] * n_stim, range(n_stim))) duration = self._duration if self._duration_multiple else ([self._duration] * n_stim) offset_times = [self._onset_time[i] + duration[i] for i in range(n_stim)] hide_ops = list(zip(offset_times, [False] * n_stim, range(n_stim))) if self._last_stimulus_remains: hide_ops = hide_ops[:-1] # don't hide the last one self._show_hide_operations = sorted(show_ops + hide_ops, key=itemgetter(0)) self._start_showing_time = None
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 _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 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 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 orientation(self, value): if not isinstance(value, ttrk.stimuli.Orientation): raise ttrk.TypeError( "invalid value for {:}.orientation ({:}) - expecting Orientation.Horizontal or Orientation.Vertical" .format(_u.get_type_name(self), value)) self._orientation = value self._log_property_changed("orientation")
def create_experiment_error(self, err_code, message, err_args=None): self._log_write_if( ttrk.log_info, "Experiment error detected by {:}: errcode={:}, message = {:}. Error parameters: {:}" .format(_u.get_type_name(self), err_code, message, err_args)) return ExperimentError(err_code, message, self, err_args)
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 on_registered(self, event_manager): self._event_manager = event_manager #-- Whenever the trial starts: register specifc events event_manager.register_operation(event=self._trial_configured_event, operation=lambda t1, t2: self._init_trial_events(), recurring=True, description="Setup {:}".format(_u.get_type_name(self)))
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( "{:}.update_xyt() was called with time={:} after it was previously called with time={:}" .format(_u.get_type_name(self), 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 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 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 _is_multiple_values(self, value, prop_type): if type(prop_type) == type: return isinstance(value, (tuple, list, np.ndarray)) elif prop_type == ttrk.TYPE_RGB: return isinstance(value, (tuple, list, np.ndarray)) and \ (len(value) == 0 or u.is_rgb(value[0])) elif prop_type == ttrk.TYPE_COORD: return isinstance(value, (tuple, list, np.ndarray)) and \ (len(value) == 0 or isinstance(value[0], (tuple, list, XYPoint, np.ndarray))) else: raise Exception("Trajtracker internal error: {:}._validate_attr_type() does not support type={:}".format( _u.get_type_name(self), prop_type))
def present(self, clear=True, update=True): """ Present all visible stimuli in the container Stimuli marked as invisible will not be shown :param clear: See in `Expyriment <http://docs.expyriment.org/expyriment.stimuli.Rectangle.html#expyriment.stimuli.Rectangle.present>` :param update: See in `Expyriment <http://docs.expyriment.org/expyriment.stimuli.Rectangle.html#expyriment.stimuli.Rectangle.present>` :return: The duration (in seconds) this function took to run """ self._log_func_enters( "{:}({:}).present".format(_u.get_type_name(self), self._name), [clear, update]) start_time = ttrk.utils.get_time() visible_stims = [ stim for stim in self._stimuli.values() if stim['stimulus'].visible ] visible_stims.sort(key=itemgetter('order')) if self._should_log(ttrk.log_debug): self._log_write("going to present() these stimuli: {:}".format( ", ".join([str(s['id']) for s in visible_stims])), prepend_self=True) if clear: _u.display_clear() self._log_write_if(ttrk.log_trace, "screen cleared") #-- Present stimuli marked as visible for i in range(len(visible_stims)): duration = visible_stims[i]['stimulus'].present(clear=False, update=False) if self._should_log(ttrk.log_trace): self._log_write( "{:}.present(): stimulus#{:}({:}).present() took {:.4f} sec" .format(self._myname(), visible_stims[i]['order'], visible_stims[i]['id'], c, u, duration)) if update: _u.display_update() self._log_write_if(ttrk.log_trace, "screen updated (flip)") total_duration = ttrk.utils.get_time() - start_time self._invoke_callbacks(visible_stims) self._log_func_returns("present", total_duration) return total_duration
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 _init_trial_events(self): self._log_func_enters("_init_trial_events") if self._onset_event is None: raise ttrk.ValueError('{:}.onset_event was not set'.format(_u.get_type_name(self))) n_stim = self.n_stim self._configure_and_preload() duration = self._duration if self._duration_multiple else ([self._duration] * n_stim) op_ids = set() for i in range(n_stim): onset_event = self._onset_event + self._onset_time[i] op = StimulusEnableDisableOp(self, i, True) id1 = self._event_manager.register_operation(event=onset_event, recurring=False, description=str(op), operation=op, cancel_pending_operation_on=self.terminate_event) op_ids.add(id1) if i == n_stim - 1 and self._last_stimulus_remains: break offset_event = self._onset_event + self._onset_time[i] + duration[i] op = StimulusEnableDisableOp(self, i, False) id2 = self._event_manager.register_operation(event=offset_event, recurring=False, description=str(op), operation=op, cancel_pending_operation_on=self.terminate_event) op_ids.add(id2) self._registered_ops = op_ids if self._terminate_event is not None: self._event_manager.register_operation(event=self._terminate_event, recurring=False, description="Terminate " + _u.get_type_name(self), operation=lambda t1, t2: self.terminate_display())
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 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 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 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._center is None: raise trajtracker.InvalidStateError( "{:}.get_xy() was called without setting center".format( _u.get_type_name(self))) if self._degrees_per_sec is None: raise trajtracker.InvalidStateError( "{:}.get_xy() was called without setting degrees_per_sec". format(_u.get_type_name(self))) if self._radius is None: raise trajtracker.InvalidStateError( "{:}.get_xy() was called without setting radius".format( _u.get_type_name(self))) dps = self._degrees_per_sec * (1 if self._clockwise else -1) curr_degrees = (self._degrees_at_t0 + dps * 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 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): """ 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