diff --git a/analysis/online_analysis.py b/analysis/online_analysis.py index 595804f9..c4608dea 100644 --- a/analysis/online_analysis.py +++ b/analysis/online_analysis.py @@ -296,6 +296,7 @@ def cleanup(self): class SaccadeAnalysisWorker(BehaviorAnalysisWorker): ''' +<<<<<<< HEAD Plots calibrated_eye, cursor, and target data from experiments that have them. This is for eye-related task that requires calibrated eye position ''' @@ -303,6 +304,20 @@ class SaccadeAnalysisWorker(BehaviorAnalysisWorker): def init(self): super().init() self.calibrated_eye_pos = np.zeros(2) +======= + Plots eye, cursor, and target data from experiments that have them. Performs automatic + calibration of eye data to target locations when the cursor enters the target if no + calibration coefficients are available. + ''' + + def __init__(self, task_params, data_queue, calibration_dir='/var/tmp', buffer_time=1, ylim=1, px_per_cm=51.67, **kwargs): + super().__init__(task_params, data_queue, **kwargs) + + def init(self): + super().init() + self.calibrated_eye_pos = np.zeros(2) + self.calibration_flag = False +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def get_current_pos(self): ''' @@ -319,7 +334,10 @@ def get_current_pos(self): targets = [(self.target_pos[k], radius, color if v == 1 else 'green') for k, v in self.targets.items() if v] except: targets = [] +<<<<<<< HEAD +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f return self.cursor_pos, self.calibrated_eye_pos, targets def draw(self): @@ -329,7 +347,11 @@ def draw(self): buffer = self.task_params['fixation_radius_buffer'] elif 'fixation_dist' in self.task_params: buffer = self.task_params['fixation_dist'] - self.task_params['target_radius'] +<<<<<<< HEAD eye_radius = 0.1 +======= + eye_radius = 0.2 +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f patches1 = [plt.Circle(pos, radius+buffer) for pos, radius, _ in targets] patches2 = [plt.Circle(cursor_pos, cursor_radius), plt.Circle(calibrated_eye_pos, eye_radius)] @@ -348,6 +370,7 @@ def draw(self): self.diam_plot.set_data(np.arange(len(self.eye_diam)) * 1/(int(self.task_params['fps'])) - self.buffer_time, self.eye_diam[:, 2]/self.px_per_cm) +<<<<<<< HEAD class EyeHandAnalysisWorker(BehaviorAnalysisWorker): ''' Plots calibrated_eye, cursor, and target data from experiments that have them. @@ -577,6 +600,8 @@ def draw(self): self.eye_diam[:, 2]/self.px_per_cm) +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f class ERPAnalysisWorker(AnalysisWorker): ''' Plots ERP data from experiments with an ECoG244 array. Automatically calculates @@ -870,6 +895,7 @@ def init(self): elif self.task_params['experiment_name'] == 'SaccadeTask': self.analysis_workers.append((SaccadeAnalysisWorker(self.task_params, data_queue), data_queue)) +<<<<<<< HEAD elif self.task_params['experiment_name'] == 'HandConstrainedSaccadeTask': self.analysis_workers.append((EyeHandAnalysisWorker(self.task_params, data_queue), data_queue)) @@ -882,6 +908,8 @@ def init(self): elif self.task_params['experiment_name'] == 'EyeHandSequenceTask': self.analysis_workers.append((EyeHandSequenceAnalysisWorker(self.task_params, data_queue), data_queue)) +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f # Is there ecube neural data? if 'record_headstage' in self.task_params and self.task_params['record_headstage']: diff --git a/built_in_tasks/manualcontrolmultitasks.py b/built_in_tasks/manualcontrolmultitasks.py index 73a0edd3..d66c5fcd 100644 --- a/built_in_tasks/manualcontrolmultitasks.py +++ b/built_in_tasks/manualcontrolmultitasks.py @@ -10,10 +10,15 @@ from .target_graphics import * from .target_capture_task import ScreenTargetCapture from .target_capture_task_xt import ScreenReachAngle, ScreenReachLine, SequenceCapture, ScreenTargetCapture_ReadySet +<<<<<<< HEAD from .target_capture_task_eye import EyeConstrainedTargetCapture, HandConstrainedEyeCapture, EyeConstrainedHandCapture, \ EyeHandSequenceCapture, ScreenTargetCapture_Saccade, EyeHandCaptureBlock +======= +from .target_capture_task_eye import EyeConstrainedTargetCapture, HandConstrainedEyeCapture, ScreenTargetCapture_Saccade +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f from .target_tracking_task import ScreenTargetTracking from .rotation_matrices import * +from .target_capture_multiple_choice import TwoChoiceTargetCapture class ManualControlMixin(traits.HasTraits): '''Target capture task where the subject operates a joystick @@ -248,4 +253,9 @@ class ReadySetGoTask(ManualControlMixin, ScreenTargetCapture_ReadySet): Center out task with ready set go auditory cues. Cues separated by 500 ms and participant is expected to move on final go cue. Additionally, participant must move out of center circle (mustmv_time) parameter or there will be an error. ''' - pass \ No newline at end of file + pass + +class TwoChoiceManualControl(ManualControlMixin, TwoChoiceTargetCapture): + ''' + Center out task with two peripheral targets + ''' \ No newline at end of file diff --git a/built_in_tasks/rotation_matrices.py b/built_in_tasks/rotation_matrices.py index 0fb31e05..216cf058 100644 --- a/built_in_tasks/rotation_matrices.py +++ b/built_in_tasks/rotation_matrices.py @@ -34,6 +34,12 @@ exp_rotations = dict( none = np.identity(4), + mirror_x = np.array( + [[-1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]] + ), about_x_90 = np.array( [[1, 0, 0, 0], [0, 0, 1, 0], diff --git a/built_in_tasks/target_capture_multiple_choice.py b/built_in_tasks/target_capture_multiple_choice.py new file mode 100644 index 00000000..c39fd72f --- /dev/null +++ b/built_in_tasks/target_capture_multiple_choice.py @@ -0,0 +1,256 @@ +import numpy as np +import random +import os + +from .target_graphics import * +from .target_capture_task import ScreenTargetCapture +from riglib.experiment import traits + +class TwoChoiceTargetCapture(ScreenTargetCapture): + ''' + #Add a penalty state when subjects looks away. + ''' + + periph_targ1_color = traits.OptionsList("red", *target_colors, desc="Color of peripheral target 1", bmi3d_input_options=list(target_colors.keys())) + periph_targ2_color = traits.OptionsList("blue", *target_colors, desc="Color of peripheral target 1", bmi3d_input_options=list(target_colors.keys())) + #hide( + #reward_time = traits.Float(.5, desc="Length of reward dispensation") + reward_multiplier = traits.Float(2.0, desc="Select the reward differential between high and low reward") + #pulses_per_total_reward = traits.Int(5, desc='the nubmer of iterations for reward pulse') + + status = dict( + wait = dict(start_trial="target"), + target = dict(timeout="timeout_penalty", + enter_target="hold"), + hold = dict(leave_target="hold_penalty", hold_complete="delay"), + delay = dict(leave_target="delay_penalty", + delay_complete="targ_transition"), + targ_transition = dict(trial_complete="reward", + trial_abort="wait", + trial_incomplete="target"), + timeout_penalty = dict(timeout_penalty_end="wait", + end_state=True), + hold_penalty = dict(hold_penalty_end="wait", + end_state=True), + delay_penalty = dict(delay_penalty_end="wait", + end_state=True), + reward = dict(reward_end = "wait", + stoppable=False, + end_state=True), + ) + + sequence_generators = ['dual_centerout_2D_mirror', 'dual_centerout_2D_180', 'dual_peripheral'] + + def __init__(self, *args, **kwargs): + kwargs['instantiate_targets'] = False + super().__init__(*args, **kwargs) + + # Create three targets: center + two peripherals + target_center = VirtualCircularTarget(target_radius=self.target_radius, + target_color=target_colors[self.target_color]) + target_periph1 = VirtualCircularTarget(target_radius=self.target_radius, + target_color=target_colors["red"]) + target_periph2 = VirtualCircularTarget(target_radius=self.target_radius, + target_color=target_colors["blue"]) + + self.targets = [target_center, target_periph1, target_periph2] + self.chosen_target = None + self.base_reward_time = self.reward_time + + if hasattr(self, "pulses_per_total_reward"): + self.base_pulses_per_total_reward = self.pulses_per_total_reward + else: + self.base_pulses_per_total_reward = 0 + + + def _start_target(self): + super()._start_target() + + if self.target_index == 0: + # Show only the center target initially + self.targets[0].move_to_position(self.targs[0]) + self.targets[0].show() + self.sync_event('TARGET_ON', 0) + + + def _start_hold(self): + super()._start_hold() + + if self.target_index == 0: + # Just entered center target + self.sync_event('CURSOR_ENTER_TARGET', 0) + else: + # Entered one of the peripheral targets - record which one + cursor_pos = self.plant.get_endpoint_pos() + d1 = np.linalg.norm(cursor_pos - self.targs[1]) + d2 = np.linalg.norm(cursor_pos - self.targs[2]) + + if d1 < d2: + self.chosen_target = 1 + self.reward_time = self.base_reward_time * self.reward_multiplier + self.pulses_per_total_reward = int(np.ceil(self.reward_multiplier)*self.base_pulses_per_total_reward) + + self.targets[2].hide() # Hide unchosen target + else: + self.chosen_target = 2 + self.reward_time = self.base_reward_time + self.targets[1].hide() # Hide unchosen target + self.pulses_per_total_reward = int(self.base_pulses_per_total_reward) + + + self.sync_event('CURSOR_ENTER_TARGET', self.chosen_target) + + def _start_delay(self): + #super()._start_delay() + # After holding center, show BOTH peripheral targets + if self.target_index == 0: # Just finished holding center + self.targets[1].move_to_position(self.targs[1]) + self.targets[1].show() + + self.targets[2].move_to_position(self.targs[2]) + self.targets[2].show() + + self.sync_event('TARGET_ON', 1)#Convert this index to position index + #self.sync_event('TARGET_ON', 2) + + def _start_targ_transition(self): + #super()._start_targ_transition() + if self.target_index == -1: + + # Came from a penalty state + pass + elif self.target_index == 0: + self.targets[0].hide() + self.sync_event('TARGET_OFF', self.gen_indices[self.target_index]) + + def _start_reward(self): + super()._start_reward() + self.targets[self.chosen_target].cue_trial_end_success() + self.sync_event('REWARD') + + @staticmethod + def dual_peripheral(nblocks=100, distance=10, origin=(0,0,0)): + ''' + #Generates center target + two peripheral targets + ''' + rng = np.random.default_rng() + for _ in range(nblocks): + # Generate two random angles for peripheral targets + angles = rng.uniform(0, 2*np.pi, size=2) + + # Target 0: center + center = np.array(origin) + + # Target 1: first peripheral + pos1 = np.array([ + distance*np.cos(angles[0]), + 0, + distance*np.sin(angles[0]) + ]) + origin + + # Target 2: second peripheral + pos2 = np.array([ + distance*np.cos(angles[1]), + 0, + distance*np.sin(angles[1]) + ]) + origin + targs = np.array([center, pos1, pos2]) + # Yield indices and positions for all three targets + yield [0, 1, 2], targs + + @staticmethod + def dual_centerout_2D_mirror(nblocks=100, ntargets=8, distance=10, origin=(0,0,0)): + ''' + triplets of central targets at the origin and 2 peripheral targets centered around the origin + + Returns + ------- + [nblocks*ntargets x 1] array of tuples containing trial indices and [2 x 3] target coordinates + ''' + gen = ScreenTargetCapture.out_2D(nblocks, ntargets, distance, origin) + for _ in range(nblocks*ntargets): + + idx, pos = next(gen) + while abs(pos[0][0]) < 0.1: + idx, pos = next(gen) + + targs = np.zeros([3, 3]) + origin + targs[1,:] = pos[0] + targs[2,:] = pos[0]*[-1,1,1] #flip the position + indices = np.zeros([3,1]) + indices[1] = idx[0] + indices[2] = 10 - idx[0] + yield indices, targs + + @staticmethod + def dual_centerout_2D_180(nblocks=100, ntargets=8, distance=10, origin=(0,0,0)): + ''' + triplets of central targets at the origin and 2 peripheral targets centered around the origin + + Returns + ------- + [nblocks*ntargets x 1] array of tuples containing trial indices and [2 x 3] target coordinates + ''' + gen = ScreenTargetCapture.out_2D(nblocks, ntargets, distance, origin) + for _ in range(nblocks*ntargets): + + idx, pos = next(gen) + #while abs(pos[0][0]) < 0.1: + # idx, pos = next(gen) + + targs = np.zeros([3, 3]) + origin + targs[1,:] = pos[0] + targs[2,:] = pos[0]*[-1,1,-1] #flip the position + indices = np.zeros([3,1]) + indices[1] = idx[0] + indices[2] = (idx[0] + 4) % 8 + yield indices, targs + + def _test_enter_target(self, ts): + ''' + #Check if cursor is in the appropriate target(s) + ''' + cursor_pos = self.plant.get_endpoint_pos() + + if self.target_index == 0: + # First target: check only center + d = np.linalg.norm(cursor_pos - self.targs[0]) + return d <= (self.target_radius - self.cursor_radius) + + else: + # After center hold: check if in EITHER peripheral target + d1 = np.linalg.norm(cursor_pos - self.targs[1]) + in_target1 = d1 <= (self.target_radius - self.cursor_radius) + + d2 = np.linalg.norm(cursor_pos - self.targs[2]) + in_target2 = d2 <= (self.target_radius - self.cursor_radius) + + return in_target1 or in_target2 + + + def _test_hold_complete(self, time_in_state): + ''' + #Hold complete after holding center OR after holding chosen peripheral + ''' + return time_in_state > self.hold_time + + def _test_trial_complete(self, time_in_state): + ''' + #Trial complete after acquiring either peripheral target (index 1) + ''' + return self.target_index > 0 + + def _test_leave_target(self, ts): + cursor_pos = self.plant.get_endpoint_pos() + + if self.target_index == 0: + d = np.linalg.norm(cursor_pos - self.targs[0]) + elif self.chosen_target is not None: # Add safety check + d = np.linalg.norm(cursor_pos - self.targs[self.chosen_target]) + else: + return False # No target chosen yet, can't have left it + + rad = self.target_radius - self.cursor_radius + return d > rad #or super()._test_leave_target(ts) + + #def \ No newline at end of file diff --git a/built_in_tasks/target_capture_task.py b/built_in_tasks/target_capture_task.py index af19a1a5..2625a735 100644 --- a/built_in_tasks/target_capture_task.py +++ b/built_in_tasks/target_capture_task.py @@ -55,7 +55,7 @@ class TargetCapture(Sequence): delay_time = traits.Float(0, desc="Length of time after a hold while the next target is on before the go cue") delay_penalty_time = traits.Float(1, desc="Length of penalty time for delay error") timeout_time = traits.Float(10, desc="Time allowed to go between targets") - timeout_penalty_time = traits.Float(1, desc="Length of penalty time for timeout error") + timeout_penalty_ScreenTargetCapturetime = traits.Float(1, desc="Length of penalty time for timeout error") max_attempts = traits.Int(10, desc='The number of attempts of a target chain before skipping to the next one') num_targets_per_attempt = traits.Int(2, desc="Minimum number of target acquisitions to be counted as an attempt") @@ -797,7 +797,7 @@ def rand_target_chain_3D(ntrials=100, chain_length=1, boundaries=(-12,12,-10,10, rng = np.random.default_rng() idx = 0 for t in range(ntrials): - + self.sync_event('TARGET_ON', 1) # Choose a random sequence of points within the boundaries pts = rng.uniform(size=(chain_length, 3))*((boundaries[1]-boundaries[0]), (boundaries[3]-boundaries[2]), (boundaries[5]-boundaries[4])) diff --git a/built_in_tasks/target_capture_task_eye.py b/built_in_tasks/target_capture_task_eye.py index deaf976b..c9581b59 100644 --- a/built_in_tasks/target_capture_task_eye.py +++ b/built_in_tasks/target_capture_task_eye.py @@ -6,6 +6,7 @@ import os from .target_graphics import * +<<<<<<< HEAD from riglib.stereo_opengl.window import Window from .target_capture_task import ScreenTargetCapture from riglib.experiment import traits, Sequence @@ -76,6 +77,44 @@ def _test_gaze_enter_target(self,ts): return (eye_d <= self.target_radius + self.fixation_radius_buffer) and (hand_d <= self.target_radius - self.cursor_radius) else: return hand_d <= self.target_radius - self.cursor_radius +======= +from .target_capture_task import ScreenTargetCapture +from riglib.experiment import traits + +class EyeConstrainedTargetCapture(ScreenTargetCapture): + ''' + Add a penalty state when subjects looks away. + ''' + + fixation_penalty_time = traits.Float(0., desc="Time in fixation penalty state") + fixation_target_color = traits.OptionsList("cyan", *target_colors, desc="Color of the center target under fixation state", bmi3d_input_options=list(target_colors.keys())) + fixation_radius_buffer = traits.Float(.5, desc="additional radius for eye target") + + status = dict( + wait = dict(start_trial="target"), + target = dict(timeout="timeout_penalty",gaze_target="fixation"), + fixation = dict(enter_target="hold", fixation_break="target"), + hold = dict(leave_target="hold_penalty", hold_complete="delay", fixation_break="fixation_penalty"), + delay = dict(leave_target="delay_penalty", delay_complete="targ_transition", fixation_break="fixation_penalty"), + targ_transition = dict(trial_complete="reward", trial_abort="wait", trial_incomplete="target"), + timeout_penalty = dict(timeout_penalty_end="targ_transition", end_state=True), + hold_penalty = dict(hold_penalty_end="targ_transition", end_state=True), + delay_penalty = dict(delay_penalty_end="targ_transition", end_state=True), + fixation_penalty = dict(fixation_penalty_end="targ_transition",end_state=True), + reward = dict(reward_end="wait", stoppable=False, end_state=True) + ) + + def _test_gaze_target(self,ts): + ''' + Check whether eye positions are within the fixation distance + Only apply this to the first target (1st target) + ''' + if self.target_index <= 0: + d = np.linalg.norm(self.calibrated_eye_pos) + return d < self.target_radius + self.fixation_radius_buffer + else: + return True +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _test_fixation_break(self,ts): ''' @@ -83,6 +122,7 @@ def _test_fixation_break(self,ts): Only apply this to the first hold and delay period ''' if self.target_index <= 0: +<<<<<<< HEAD eye_d = np.linalg.norm(self.calibrated_eye_pos) return (eye_d > self.target_radius + self.fixation_radius_buffer) @@ -154,21 +194,61 @@ def _start_fixation_penalty(self): self.sync_event('FIXATION_PENALTY') self.penalty_index = 1 self.num_fixation_state = 0 +======= + d = np.linalg.norm(self.calibrated_eye_pos) + return (d > self.target_radius + self.fixation_radius_buffer) + + def _test_fixation_penalty_end(self,ts): + return (ts > self.fixation_penalty_time) + + def _start_wait(self): + super()._start_wait() + self.num_fixation_state = 0 # Initializa fixation state + + def _start_target(self): + if self.num_fixation_state == 0: + super()._start_target() # target index shouldn't be incremented after fixation break loop + else: + self.sync_event('FIXATION', 0) + self.targets[0].reset() # reset target color after fixation break + + def _start_fixation(self): + self.num_fixation_state = 1 + self.targets[0].sphere.color = target_colors[self.fixation_target_color] # change target color in fixation state + if self.target_index == 0: + self.sync_event('FIXATION', 1) + + def _start_timeout_penalty(self): + super()._start_timeout_penalty() + self.num_fixation_state = 0 + + def _start_hold(self): + super()._start_hold() + self.num_fixation_state = 0 # because target state comes again after hold state in a trial + + def _start_fixation_penalty(self): + self._increment_tries() + self.sync_event('FIXATION_PENALTY') +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f # Hide targets for target in self.targets: target.hide() target.reset() +<<<<<<< HEAD self.targets_eye[0].hide() self.targets_eye[0].reset() +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _end_fixation_penalty(self): self.sync_event('TRIAL_END') class HandConstrainedEyeCapture(ScreenTargetCapture): ''' Saccade task with holding another target with hand. Subjects need to hold an initial target with their hand. +<<<<<<< HEAD Then they need to fixate the first eye target and make a saccade for the second eye target. The eye target is a square whose width is adjusted by fixation_radius. The acceprance radius for eye fixation is fixation_radius (width) + fixation_radius_buffer. The buffer radius is invisible for subjects. 2 of chain_length is only tested. @@ -199,6 +279,28 @@ class HandConstrainedEyeCapture(ScreenTargetCapture): delay_penalty = dict(delay_penalty_end="wait", start_pause="pause", end_state=True), fixation_penalty = dict(fixation_penalty_end="wait", start_pause="pause", end_state=True), incorrect_target_penalty = dict(incorrect_target_penalty_end="wait", start_pause="pause", end_state=True), +======= + Then they need to fixate the first eye target and make a saccade for the second eye target + ''' + + fixation_radius = traits.Float(2.5, desc="Distance from center that is considered a broken fixation") + fixation_penalty_time = traits.Float(1.0, desc="Time in fixation penalty state") + fixation_target_color = traits.OptionsList("cyan", *target_colors, desc="Color of the eye target under fixation state", bmi3d_input_options=list(target_colors.keys())) + eye_target_color = traits.OptionsList("white", *target_colors, desc="Color of the eye target", bmi3d_input_options=list(target_colors.keys())) + fixation_radius_buffer = traits.Float(.5, desc="additional radius for eye target") + + status = dict( + wait = dict(start_trial="target", start_pause="pause"), + target = dict(start_pause="pause", leave_target2="hold_penalty",timeout="timeout_penalty",enter_target="hold"), + hold = dict(start_pause="pause", leave_target2="hold_penalty",leave_target="target", gaze_target="fixation"), # must hold an initial hand-target and eye-target + fixation = dict(start_pause="pause", leave_target="delay_penalty",hold_complete="delay", fixation_break="fixation_penalty"), # must hold an initial hand-target and eye-target to initiate a trial + delay = dict(leave_target="delay_penalty", delay_complete="targ_transition", fixation_break="fixation_penalty", start_pause="pause"), + targ_transition = dict(trial_complete="reward", trial_abort="wait", trial_incomplete="target", start_pause="pause"), + timeout_penalty = dict(timeout_penalty_end="targ_transition", start_pause="pause", end_state=True), + hold_penalty = dict(hold_penalty_end="targ_transition", start_pause="pause", end_state=True), + delay_penalty = dict(delay_penalty_end="targ_transition", start_pause="pause", end_state=True), + fixation_penalty = dict(fixation_penalty_end="targ_transition", start_pause="pause", end_state=True), +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f reward = dict(reward_end="wait", start_pause="pause", stoppable=False, end_state=True), pause = dict(end_pause="wait", end_state=True), ) @@ -213,12 +315,18 @@ def __init__(self, *args, **kwargs): if instantiate_targets: # Target 1 and 2 are for saccade. Target 3 is for hand +<<<<<<< HEAD target1 = VirtualRectangularTarget(target_width=self.fixation_radius, target_height=self.fixation_radius/2, target_color=target_colors[self.eye_target_color]) target2 = VirtualRectangularTarget(target_width=self.fixation_radius, target_height=self.fixation_radius/2, target_color=target_colors[self.eye_target_color]) +======= + target1 = VirtualCircularTarget(target_radius=self.fixation_radius, target_color=target_colors[self.eye_target_color]) + target2 = VirtualCircularTarget(target_radius=self.fixation_radius, target_color=target_colors[self.eye_target_color]) +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f target3 = VirtualCircularTarget(target_radius=self.target_radius, target_color=target_colors[self.target_color]) self.targets = [target1, target2] self.targets_hand = [target3] +<<<<<<< HEAD self.offset_cube = np.array([0,0,self.fixation_radius/2]) # To center the cube target @@ -229,11 +337,21 @@ def _parse_next_trial(self): # Update the data sinks with trial information self.trial_record['trial'] = self.calc_trial_num() +======= + + def _parse_next_trial(self): + '''Check that the generator has the required data''' + self.gen_indices, self.targs = self.next_trial # 2 target positions for hand and eye + + # Update the data sinks with trial information + self.trial_record['trial'] = self.calc_trial_num() # TODO save both eye and hand target positions +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f for i in range(len(self.gen_indices)): self.trial_record['index'] = self.gen_indices[i] self.trial_record['target'] = self.targs[i] self.sinks.send("trials", self.trial_record) +<<<<<<< HEAD def _test_gaze_enter_target(self,ts): ''' Check whether eye positions and hand cursor are within the target radius @@ -264,12 +382,25 @@ def _test_gaze_incorrect_target(self, ts): return eye_d <= self.target_radius + self.incorrect_target_radius_buffer +======= + def _test_gaze_target(self,ts): + ''' + Check whether eye positions from a target are within the fixation distance + ''' + # Distance of an eye position from a target position + eye_pos = self.calibrated_eye_pos + target_pos = np.delete(self.targs[self.target_index],1) + d_eye = np.linalg.norm(eye_pos - target_pos) + return (d_eye <= self.fixation_radius + self.fixation_radius_buffer) or self.pause + +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _test_fixation_break(self,ts): ''' Triggers the fixation_penalty state when eye positions are outside fixation distance ''' # Distance of an eye position from a target position eye_pos = self.calibrated_eye_pos +<<<<<<< HEAD d_eye = np.linalg.norm(eye_pos - self.targs[self.target_index,[0,2]]) return d_eye > self.fixation_radius + self.fixation_radius_buffer @@ -278,6 +409,14 @@ def _test_fixation_complete(self,ts): def _test_fixation_penalty_end(self,ts): return ts > self.fixation_penalty_time +======= + target_pos = np.delete(self.targs[self.target_index],1) + d_eye = np.linalg.norm(eye_pos - target_pos) + return (d_eye > self.fixation_radius + self.fixation_radius_buffer) or self.pause + + def _test_fixation_penalty_end(self,ts): + return (ts > self.fixation_penalty_time) +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _test_enter_target(self, ts): ''' @@ -285,7 +424,11 @@ def _test_enter_target(self, ts): ''' cursor_pos = self.plant.get_endpoint_pos() d = np.linalg.norm(cursor_pos - self.targs[-1]) # hand must be within the initial target +<<<<<<< HEAD return d <= self.target_radius - self.cursor_radius +======= + return d <= (self.target_radius - self.cursor_radius) or self.pause +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _test_leave_target(self, ts): ''' @@ -293,6 +436,7 @@ def _test_leave_target(self, ts): ''' cursor_pos = self.plant.get_endpoint_pos() d = np.linalg.norm(cursor_pos - self.targs[-1]) # hand must be within the initial target +<<<<<<< HEAD return d > self.target_radius - self.cursor_radius def _test_return_init_target(self, ts): @@ -308,12 +452,30 @@ def _test_trial_incomplete(self, ts): def _test_incorrect_target_penalty_end(self, ts): return ts > self.incorrect_target_penalty_time +======= + return d > (self.target_radius - self.cursor_radius) or self.pause + + def _test_leave_target2(self, ts): + ''' + return true if cursor moves outside the exit radius (This is for the second target state) + ''' + if self.target_index > 0: + cursor_pos = self.plant.get_endpoint_pos() + d = np.linalg.norm(cursor_pos - self.targs[-1]) # hand must be within the initial target + return d > (self.target_radius - self.cursor_radius) or self.pause +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _start_wait(self): super()._start_wait() # Redefine chain length because targs in this task has both eye and hand targets self.chain_length = len(self.targets) +<<<<<<< HEAD self.fixation_passed = False +======= + + # Initialize fixation state + self.num_hold_state = 0 +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f if self.calc_trial_num() == 0: @@ -328,6 +490,7 @@ def _start_wait(self): self.add_model(model) target.hide() +<<<<<<< HEAD def _start_init_target(self): # Only show the hand target if self.target_index == -1: @@ -363,10 +526,51 @@ def _start_fixation(self): self.targets[self.target_index].cube.color = target_colors[self.fixation_target_color] # change target color in fixation state self.sync_event('FIXATION', self.gen_indices[self.target_index]) +======= + def _start_target(self): + if self.num_hold_state == 0: + self.target_index += 1 # target index shouldn't be incremented after hold break loop + + # Show target if it is hidden (this is the first target, or previous state was a penalty) + target_hand = self.targets_hand[0] + if self.target_index == 0: + target_hand.move_to_position(self.targs[-1]) + target_hand.show() + self.sync_event('TARGET_ON', self.gen_indices[-1]) + + else: + target = self.targets[self.target_index % 2] + target.hide() # hide hand target + self.sync_event('EYE_TARGET_OFF', self.gen_indices[self.target_index % 2]) + + def _start_hold(self): + #self.sync_event('CURSOR_ENTER_TARGET', self.gen_indices[self.target_index]) + self.num_hold_state = 1 + + # Show target if it is hidden (this is the first target, or previous state was a penalty) + target = self.targets[self.target_index % 2] + if self.target_index == 0: + target.move_to_position(self.targs[self.target_index]) + target.show() + self.sync_event('EYE_TARGET_ON', self.gen_indices[self.target_index]) + + def _start_fixation(self): + self.num_hold_state = 0 + self.targets[self.target_index].sphere.color = target_colors[self.fixation_target_color] # change target color in fixation state + self.sync_event('FIXATION', self.gen_indices[self.target_index]) + + def _while_fixation(self): + pass + + def _end_fixation(self): + pass + +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _start_delay(self): # Make next target visible unless this is the final target in the trial next_idx = (self.target_index + 1) if next_idx < self.chain_length: +<<<<<<< HEAD target = self.targets[next_idx] target.move_to_position(self.targs[next_idx] - self.offset_cube) target.show() @@ -377,16 +581,43 @@ def _start_targ_transition(self): # Hide the current target if there are more self.targets[self.target_index].hide() +======= + target = self.targets[next_idx % 2] + target.move_to_position(self.targs[next_idx % 2]) + target.show() + self.sync_event('EYE_TARGET_ON', self.gen_indices[next_idx % 2]) + else: + # This delay state should only last 1 cycle, don't sync anything + pass + + def _start_targ_transition(self): + if self.target_index == -1: + + # Came from a penalty state + pass + elif self.target_index + 1 < self.chain_length: + + # Hide the current target if there are more + self.targets[self.target_index % 2].hide() +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f self.sync_event('EYE_TARGET_OFF', self.gen_indices[self.target_index]) def _start_timeout_penalty(self): super()._start_timeout_penalty() +<<<<<<< HEAD +======= + self.num_hold_state = 0 +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f for target in self.targets_hand: target.hide() target.reset() def _start_hold_penalty(self): super()._start_hold_penalty() +<<<<<<< HEAD +======= + self.num_hold_state = 0 +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f # Hide targets for target in self.targets_hand: target.hide() @@ -402,7 +633,10 @@ def _start_delay_penalty(self): def _start_fixation_penalty(self): self._increment_tries() self.sync_event('FIXATION_PENALTY') +<<<<<<< HEAD self.penalty_index = 1 +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f # Hide targets for target in self.targets: @@ -416,6 +650,7 @@ def _start_fixation_penalty(self): def _end_fixation_penalty(self): self.sync_event('TRIAL_END') +<<<<<<< HEAD def _start_incorrect_target_penalty(self): self._increment_tries() self.sync_event('OTHER_PENALTY') @@ -445,6 +680,10 @@ def _start_reward(self): def _end_reward(self): super()._end_reward() +======= + def _start_reward(self): + super()._start_reward() +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f # Hide targets for target in self.targets_hand: target.hide() @@ -460,7 +699,11 @@ def _start_pause(self): # Generator functions @staticmethod +<<<<<<< HEAD def row_target(nblocks=20, ntargets=3, dx=5.,offset1=(0,0,-2),offset2=(0,0,6.),offset3=(0,0,-7.5),origin=(0,0,0)): +======= + def row_target(nblocks=20, ntargets=3, dx=5.,offset1=(0,0,-2),offset2=(0,0,6.),offset3=(0,0,-7.5),origin=(0,0,0),seed=0): +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f ''' Generates a sequence of 3D for 2 eye targets and 1 hand target at a given distance from the origin @@ -486,7 +729,11 @@ def row_target(nblocks=20, ntargets=3, dx=5.,offset1=(0,0,-2),offset2=(0,0,6.),o [nblocks*ntargets x 1] array of tuples containing trial indices and [1 x 3] target coordinates ''' +<<<<<<< HEAD rng = np.random.default_rng() +======= + rng = np.random.default_rng(seed=seed) +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f for _ in range(nblocks): order = np.arange(ntargets**3) rng.shuffle(order) @@ -510,7 +757,11 @@ def row_target(nblocks=20, ntargets=3, dx=5.,offset1=(0,0,-2),offset2=(0,0,6.),o yield [idx1],[idx2],[idx3],[pos1+offset1+origin],[pos2+offset2+origin],[pos3+offset3+origin] @staticmethod +<<<<<<< HEAD def sac_hand_2d(nblocks=20, ntargets=3, dx=10,offset1=(0,0,-2),offset2=(0,0,6.),offset3=(0,0,-7.5),origin=(0,0,0)): +======= + def sac_hand_2d(nblocks=20, ntargets=3, dx=10,offset1=(0,0,-2),offset2=(0,0,6.),offset3=(0,0,-7.5),origin=(0,0,0),seed=0): +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f ''' Pairs of hand targets and eye targets @@ -519,7 +770,11 @@ def sac_hand_2d(nblocks=20, ntargets=3, dx=10,offset1=(0,0,-2),offset2=(0,0,6.), [nblocks*ntargets x 1] array of tuples containing trial indices and [3 x 3] target coordinates ''' +<<<<<<< HEAD gen = HandConstrainedEyeCapture.row_target(nblocks=nblocks,ntargets=ntargets,dx=dx,offset1=offset1,offset2=offset2,offset3=offset3,origin=origin) +======= + gen = HandConstrainedEyeCapture.row_target(nblocks=nblocks,ntargets=ntargets,dx=dx,offset1=offset1,offset2=offset2,offset3=offset3,origin=origin,seed=seed) +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f for _ in range(nblocks*(ntargets**3)): idx1,idx2,idx3,pos1,pos2,pos3 = next(gen) @@ -535,6 +790,7 @@ def sac_hand_2d(nblocks=20, ntargets=3, dx=10,offset1=(0,0,-2),offset2=(0,0,6.), yield indices, targs +<<<<<<< HEAD class EyeConstrainedHandCapture(HandConstrainedEyeCapture): ''' Saccade and reaching task. Subjects need to hold an initial hand target and fixate the initial eye target, separately. @@ -1868,4 +2124,39 @@ def _start_delay(self): self.sync_event('TARGET_ON', self.gen_indices[next_idx]) else: # This delay state should only last 1 cycle, don't sync anything - pass \ No newline at end of file + pass +======= +class ScreenTargetCapture_Saccade(ScreenTargetCapture): + ''' + Center-out saccade task. The controller for the cursor position is eye position. + Hand cursor is also visible. You should remove the hand cursor by setting cursor_radius to 0 as needed. + ''' + + fixation_radius_buffer = traits.Float(.5, desc="additional radius for eye target") + target_color = traits.OptionsList("white", *target_colors, desc="Color of the target", bmi3d_input_options=list(target_colors.keys())) + fixation_target_color = traits.OptionsList("cyan", *target_colors, desc="Color of the eye target under fixation state", bmi3d_input_options=list(target_colors.keys())) + + def _test_enter_target(self, ts): + ''' + Check whether eye positions from a target are within the fixation distance + ''' + # Distance of an eye position from a target position + eye_pos = self.calibrated_eye_pos + target_pos = np.delete(self.targs[self.target_index],1) + d_eye = np.linalg.norm(eye_pos - target_pos) + return (d_eye <= self.target_radius + self.fixation_radius_buffer) or self.pause + + def _test_leave_target(self, ts): + ''' + Check whether eye positions from a target are outside the fixation distance + ''' + # Distance of an eye position from a target position + eye_pos = self.calibrated_eye_pos + target_pos = np.delete(self.targs[self.target_index],1) + d_eye = np.linalg.norm(eye_pos - target_pos) + return (d_eye > self.target_radius + self.fixation_radius_buffer) or self.pause + + def _start_hold(self): + super()._start_hold() + self.targets[self.target_index].sphere.color = target_colors[self.fixation_target_color] # change target color in fixating the target +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f diff --git a/built_in_tasks/target_capture_task_xt.py b/built_in_tasks/target_capture_task_xt.py index 2ee166b3..46d56a71 100644 --- a/built_in_tasks/target_capture_task_xt.py +++ b/built_in_tasks/target_capture_task_xt.py @@ -705,7 +705,13 @@ def color_go_cue(self): #do nothing in normal version (no color change) pass def _start_targ_transition(self): +<<<<<<< HEAD pass +======= + super()._start_targ_transition() + if self.target_index == -1: # Came from a penalty state + pass +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _start_hold_penalty(self): self.pseudo_success() #run before increment trials to prevent reseting of trial index diff --git a/features/__init__.py b/features/__init__.py index 5726b0f4..f072c423 100644 --- a/features/__init__.py +++ b/features/__init__.py @@ -8,9 +8,13 @@ from features.quattrocento_features import QuattBMI from riglib.stereo_opengl.window import WindowWithExperimenterDisplay, Window2D from riglib.stereo_opengl.openxr import WindowVR +<<<<<<< HEAD from .generator_features import Autostart, RandomDelay, AdaptiveGenerator, IgnoreCorrectness, PoissonWait, Progressbar_fixation, \ HideLeftTrajectory, StartTrialBelowSpeedThr, ReadysetMedley, ReadysetColorChange, DiscreteRandomDelay_EyeHandSequence, HideCursorReturn +======= +from .generator_features import Autostart, RandomDelay, AdaptiveGenerator, IgnoreCorrectness, PoissonWait, Progressbar_fixation, HideLeftTrajectory, StartTrialBelowSpeedThr, ReadysetMedley, ReadysetColorChange +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f from .peripheral_device_features import Button, Joystick, DualJoystick, Joystick_plus_TouchSensor, KeyboardControl, MouseControl, ForceControl from .reward_features import RewardSystem, RewardSystemPulse, TTLReward, JuiceLogging, PelletReward, JackpotRewards, ProgressBar, TrackingRewards, RewardAudio, PenaltyAudio, ScoreRewards, ConsecutiveJackpot from .eyetracker_features import EyeCursor, EyeData, CalibratedEyeData, PupilLabStreaming, SimulatedEyeData, FixationStart, EyeConstrained, \ @@ -104,9 +108,12 @@ start_trial_below_speed_thr=StartTrialBelowSpeedThr, reward_system_pulse = RewardSystemPulse, readyset_colorchange = ReadysetColorChange, +<<<<<<< HEAD random_delay_eye_hand_sequence = DiscreteRandomDelay_EyeHandSequence, hide_cursor_return = HideCursorReturn, auto_eye_calibration = AutomaticEyeCalibration, +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f ) # >>> features.built_in_features['autostart'].__module__ diff --git a/features/debug_features.py b/features/debug_features.py index 4151435c..6fd3d791 100644 --- a/features/debug_features.py +++ b/features/debug_features.py @@ -129,7 +129,10 @@ def _cycle(self): self._send_online_analysis_msg('eye_pos', self.eye_pos) if hasattr(self, 'calibrated_eye_pos'): self._send_online_analysis_msg('calibrated_eye_pos', self.calibrated_eye_pos) +<<<<<<< HEAD +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f if hasattr(self, 'task_data') and 'decoder_state' in self.task_data.dtype.names: self._send_online_analysis_msg('decoder_state', self.task_data['decoder_state'].flatten().tolist()) if hasattr(self, 'task_data') and hasattr(self, 'extractor') and self.extractor.feature_type in self.task_data.dtype.names: diff --git a/features/eyetracker_features.py b/features/eyetracker_features.py index 3236bff2..5d431ff3 100644 --- a/features/eyetracker_features.py +++ b/features/eyetracker_features.py @@ -36,9 +36,12 @@ class EyeCalibration(traits.HasTraits): show_eye_pos = traits.Bool(False, desc="Whether to show eye positions") eye_target_calibration = traits.Bool(False, desc="Whether to regress eye positions against target positions") center_eye_data = traits.Bool(False, desc="Whether to demean eye data with eye position for the center target") +<<<<<<< HEAD offset_time_eye_calibration = traits.Float(0.1, desc="Data after this offset_time is only used for eye calibration") duration_eye_calibration = traits.Float(0.2, desc="Data within this duration after offset_time is only used for eye calibration") +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def __init__(self, *args, **kwargs): #, start_pos, calibration): super(EyeCalibration,self).__init__(*args, **kwargs) @@ -109,7 +112,23 @@ def get_target_locations(data, target_indices): bmi3d_metadata['cursor_interp_samplerate'], events['timestamp'], events['code'], target_pos, \ offset=self.offset_time_eye_calibration, duration=self.duration_eye_calibration) +<<<<<<< HEAD print("Calibration complete:", self.eye_coeff) +======= + # Get eye_pos data when subjects gaze at the center. Target position doesn't matter for this computation + if self.center_eye_data: + _, _, eye_center = aopy.preproc.calc_eye_target_calibration(eye_interp[:,:4], \ + bmi3d_metadata['cursor_interp_samplerate'], events['timestamp'], events['code'], target_pos, \ + offset=0.1, duration=0.2, align_events=80, return_datapoints=True) + + self.eye_center = np.nanmedian(eye_center, axis=0) + + # Calculate coefficient by linear regression between targets and centered eye positions + self.eye_coeff, _ = aopy.preproc.calc_eye_target_calibration(eye_interp[:,:4]-self.eye_center, \ + bmi3d_metadata['cursor_interp_samplerate'], events['timestamp'], events['code'], target_pos) + + print("Calibration complete:", self.eye_coeff) +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f # Set up eye cursor self.eye_cursor = VirtualCircularTarget(target_radius=.25, target_color=(0., 1., 0., 0.5)) diff --git a/features/generator_features.py b/features/generator_features.py index 6d47af26..54abf073 100644 --- a/features/generator_features.py +++ b/features/generator_features.py @@ -425,6 +425,7 @@ def _while_tooslow_penalty(self): def _end_tooslow_penalty(self): self.sync_event('TRIAL_END') +<<<<<<< HEAD class HideCursorReturn(traits.HasTraits): @@ -465,3 +466,5 @@ def _start_delay_penalty(self): def _start_tooslow_penalty(self): super()._start_tooslow_penalty() self.plant_visible = False +======= +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f diff --git a/features/reward_features.py b/features/reward_features.py index 009d85d9..a8afe29a 100644 --- a/features/reward_features.py +++ b/features/reward_features.py @@ -122,7 +122,16 @@ def _start_reward(self): self.reportstats['Reward #'] += 1 if self.reportstats['Reward #'] % self.trials_per_reward == 0: +<<<<<<< HEAD +<<<<<<< 8005149be4e11667fe4a76d17510adbb17bce7c1 self.reward.trigger(self.ip_address, self.pellet_dispense_time, self.pellets_per_reward) # triggers as many times as pellets_per_reward +======= + for _ in range(self.pellets_per_reward): # call trigger num of pellets_per_reward time + self.reward.trigger(self.ip_address) +>>>>>>> system triggers but only works for 1x reward +======= + self.reward.trigger(self.ip_address, self.pellet_dispense_time, self.pellets_per_reward) # triggers as many times as pellets_per_reward +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f def _end_reward(self): if hasattr(super(RewardSystem, self), '_end_reward'): diff --git a/tests/test_tasks.py b/tests/test_tasks.py index d9641c90..c8000ae8 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -5,7 +5,11 @@ from built_in_tasks.target_capture_task import ScreenTargetCapture from built_in_tasks.passivetasks import YouTube from built_in_tasks.example_task import ExampleSequenceTask +<<<<<<< HEAD from features.generator_features import Autostart, HideLeftTrajectory, ReadysetMedley, ReadysetColorChange, HideCursorReturn +======= +from features.generator_features import Autostart, HideLeftTrajectory, ReadysetMedley, ReadysetColorChange +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f from features.hdf_features import SaveHDF from features.touch_features import MouseEmulateTouch from riglib.stereo_opengl.environment import Grid @@ -41,9 +45,15 @@ class TestManualControlTasks(unittest.TestCase): @unittest.skip("") def test_readysetgo(self): seq = ManualControl.centerout_2D() +<<<<<<< HEAD exp = init_exp(ReadySetGoTask, [MouseControl, Window2D, HideCursorReturn], seq, early_move_time = 0.1, delay_time = 0.45, mustmv_time = 0.3, ready_freq = 320, set_freq = 360, go_freq = 400, show_cursor_return = 2.5, window_size=(1200,800), +======= + exp = init_exp(ReadySetGoTask, [MouseControl, Window2D], seq, early_move_time = 0.1, + delay_time = 0.45, mustmv_time = 0.3, ready_freq = 320, set_freq = 360, go_freq = 400, + window_size=(1200,800), +>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f fullscreen=False) exp.rotation = 'xzy' exp.run()