From d92133850532ff741dd8b08c805e3e2acb0e9c2a Mon Sep 17 00:00:00 2001 From: mtringi Date: Mon, 21 Jul 2025 11:15:34 -0700 Subject: [PATCH 01/18] generate trajectories in two dimensions --- built_in_tasks/target_tracking_task.py | 129 +++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index c41b02f9..6c54757e 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -926,7 +926,100 @@ def generate_trajectories(num_trials=2, time_length=20, seed=40, sample_rate=120 return trials, trial_order @staticmethod - def generate_trajectory(primes, base_period, ramp = .0): + def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate=120, base_period=20, ramp=0, ramp_down=0, num_primes=8): + ''' + Sets up variables and uses prime numbers to call the above functions and generate trajectories in both x & y + ramp is time length for preparatory lines + ''' + np.random.seed(seed) + hz = sample_rate # Hz -- sampling rate + dt = 1/hz # sec -- sampling period + + T0 = base_period # sec -- base period + w0 = 1./T0 # Hz -- base frequency + + r = ramp # "ramp up" duration (see sum_of_sines_ramp) + rd = ramp_down # "ramp down" duration (see sum_of_sines_ramp) + P = time_length/T0 # number of periods in signal + T = P*T0+r+rd # sec -- signal duration + dw = 1./T # Hz -- frequency resolution + W = 1./dt/2 # Hz -- signal bandwidth + + full_primes = np.asarray([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199]) + + xprimes = full_primes[:num_primes:2] + yprimes = full_primes[1:num_primes:2] + + f_x = xprimes*w0 # stimulated frequencies + f_y = yprimes*w0 + + a_x = 1/(1+np.arange(f_x.size)) # amplitude + a_y = 1/(1+np.arange(f_y.size)) + + # phase offset + o_x = np.random.rand(num_trials, f_x.size) + o_xdis = o_x*0.8 + + o_y = np.random.rand(num_trials, f_y.size) + o_ydis = o_y*0.8 + + t = np.arange(0,T,dt) # time samplesseed + w = np.arange(0,W,dw) # frequency samples + + N = t.size # = T/dt -- number of samples + + # create trials dictionary with x & y + trials = dict( + id=np.arange(num_trials), + times=np.tile(t,(num_trials,1)), + ref_x=np.zeros((num_trials,N)), + dis_x=np.zeros((num_trials,N)), + ref_y=np.zeros((num_trials,N)), + dis_y=np.zeros((num_trials,N)) + ) + + # randomize order of first two trials to provide random starting point + order = np.random.choice([0,1]) + if order == 0: + trial_order = [(1,'E','O'),(1,'O','E')] + elif order == 1: + trial_order = [(1,'O','E'),(1,'E','O')] + + # generate reference and disturbance trajectories for all trials + for trial_id, (num_reps,ref_ind,dis_ind) in enumerate(trial_order*int(num_trials/2)): + if ref_ind == 'E': + sines_r = np.arange(len(xprimes))[0::2] # use even indices + elif ref_ind == 'O': + sines_r = np.arange(len(xprimes))[1::2] # use odd indices + else: + sines_r = np.arange(len(xprimes)) + if dis_ind == 'E': + sines_d = np.arange(len(xprimes))[0::2] + elif dis_ind == 'O': + sines_d = np.arange(len(xprimes))[1::2] + else: + sines_d = np.arange(len(xprimes)) + + # generate X-dimension + refx_traj, ref_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_x[sines_r], a_x[sines_r], o_x[trial_id][sines_r]) + disx_traj, dis_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t,r,rd, f_x[sines_d], a_x[sines_d], o_xdis[trial_id][sines_d]) + # generate Y-dimension + refy_traj, ref_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_r], a_y[sines_r], o_y[trial_id][sines_r]) + disy_traj, dis_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_d], a_y[sines_d], o_ydis[trial_id][sines_d]) + + # normalized trajectories + trials['ref_x'][trial_id] = refx_traj/ref_Ax # previously, denominator was np.sum(a_ref) + trials['dis_x'][trial_id] = disx_traj/dis_Ax # previously, denominator was np.sum(a_dis) + trials['ref_y'][trial_id] = refy_traj/ref_Ay # previously, denominator was np.sum(a_ref) + trials['dis_y'][trial_id] = disy_traj/dis_Ay # previously, denominator was np.sum(a_dis) + + # print(trial_order, ref_A, dis_A) + + return trials, trial_order + + @staticmethod + def generate_trajectory(primes, base_period, ramp = 0.0, rd = 0.0): ''' Sets up variables and uses prime numbers to call the above functions and generate then trajectories ramp is time length for preparatory lines @@ -953,13 +1046,13 @@ def generate_trajectory(primes, base_period, ramp = .0): N = t.size # = T/dt -- number of samples #trajectory = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, f, a, o/(2*np.pi)) - trajectory = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, f, a, o) + trajectory = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f, a, o) normalized_trajectory = trajectory/np.sum(a) return normalized_trajectory ### Generator functions #### @staticmethod - def tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, num_primes=8, seed=40, sample_rate=120, disturbance=True, boundaries=(-10,10,-10,10)): + def tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, num_primes=8, seed=40, sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)): ''' Generates a sequence of 1D (z axis) target trajectories @@ -977,6 +1070,8 @@ def tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_dow The sample rate of the generated trajectories ramp : float The length of ramp up into a trial in seconds + dimensions: int + Number of dimensions to generate trajectories for (1 or 2) disturbance : boolean Whether to add disturbance to the cursor (disturbance is generated regardless) boundaries: 4 element tuple @@ -991,20 +1086,38 @@ def tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_dow ''' idx = 0 base_period = 20 - for block_id in range(nblocks): - trials, trial_order = ScreenTargetTracking.generate_trajectories( - num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, num_primes=num_primes - ) + for block_id in range(nblocks): + if dimensions == 1: + trials, trial_order = ScreenTargetTracking.generate_trajectories( + num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, num_primes=num_primes + ) for trial_id in range(ntrials): targs = [] ref_trajectory = np.zeros((int((time_length+ramp+ramp_down)*sample_rate),3)) dis_trajectory = ref_trajectory.copy() - ref_trajectory[:,2] = trials['ref'][trial_id] + ref_trajectory[:,2] = trials['ref'][trial_id] dis_trajectory[:,2] = trials['dis'][trial_id] # scale will determine lower limit of target size for perfect tracking targs.append(ref_trajectory) yield idx, targs, disturbance, dis_trajectory, sample_rate, ramp, ramp_down idx += 1 + if dimensions == 2: + trials, trial_order = ScreenTargetTracking.generate_2D_trajectories( + num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, num_primes=num_primes + ) + for trial_id in range(ntrials): + targs = [] + ref_trajectory = np.zeros((int((time_length+ramp+ramp_down)*sample_rate),3)) + dis_trajectory = ref_trajectory.copy() + ref_trajectory[:,2] = trials['ref_x'][trial_id] #y is out of the screen, x is left and right and z is up and down + ref_trajectory[:,0] = trials['ref_y'][trial_id] + + dis_trajectory[:,2] = trials['dis_x'][trial_id] + dis_trajectory[:,0] = trials['dis_y'][trial_id] + targs.append(ref_trajectory) + yield idx, targs, disturbance, dis_trajectory, sample_rate, ramp, ramp_down + idx += 1 + @staticmethod def tracking_target_debug(nblocks=1, ntrials=2, time_length=20, seed=40, sample_rate=60, ramp=0, disturbance=True, boundaries=(-10,10,-10,10)): ''' From 01f4d385b9087d0ec3cf21112ed9d666db072dac Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 21 Jul 2025 16:45:23 -0700 Subject: [PATCH 02/18] add black and invisible? update target to move in two directions --- built_in_tasks/target_graphics.py | 2 ++ built_in_tasks/target_tracking_task.py | 29 ++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/built_in_tasks/target_graphics.py b/built_in_tasks/target_graphics.py index 4f6a4a3f..41dc1157 100644 --- a/built_in_tasks/target_graphics.py +++ b/built_in_tasks/target_graphics.py @@ -35,6 +35,8 @@ "gold": (0.941,0.637,0.25,0.75), "elephant":(0.5,0.5,0.5,0.5), "white": (1, 1, 1, 0.75), + "black": (0, 0, 0, 0.75), + 'invisible': (0, 0, 0, 0.0), } class CircularTarget(object): diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 6c54757e..9eb05ba1 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -393,7 +393,7 @@ class ScreenTargetTracking(TargetTracking, Window): limit1d = traits.Bool(True, desc="Limit cursor movement to 1D") sequence_generators = [ - 'tracking_target_chain', 'tracking_target_debug', 'tracking_target_training' + 'tracking_target_chain', 'tracking_target_debug', 'tracking_target_training', 'generate_2D_trajectories' ] hidden_traits = ['cursor_color', 'trajectory_color', 'cursor_bounds', 'cursor_radius', 'plant_hide_rate', 'starting_pos'] @@ -524,7 +524,8 @@ def update_plant_visibility(self): self.plant.set_visibility(self.plant_visible) def update_frame(self): - self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # xzy + #self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # xzy + self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) self.trajectory.move_to_position(np.array([-self.frame_index/3,0,0])) # same update constant works for 60 and 120 hz self.target.show() self.trajectory.show() @@ -565,7 +566,7 @@ def setup_start_wait(self): self.trajectory = VirtualCableTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) for model in self.trajectory.graphics_models: - self.add_model(model) + self.add_model(model) self.target.hide() self.trajectory.hide() @@ -644,7 +645,9 @@ def _while_wait_retry(self): def _start_trajectory(self): super()._start_trajectory() if self.frame_index == 0: - self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # tablet screen x-axis ranges -19,19, center 0 + #self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # tablet screen x-axis ranges -19,19, center 0 + self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) # tablet screen x-axis ranges -19,19, center 0 + self.trajectory.move_to_position(np.array([0,0,0])) # tablet screen x-axis ranges 0,41.33333, center 22ish # print(self.target.get_position()) # print(self.trajectory.get_position()) @@ -1091,15 +1094,15 @@ def tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_dow trials, trial_order = ScreenTargetTracking.generate_trajectories( num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, num_primes=num_primes ) - for trial_id in range(ntrials): - targs = [] - ref_trajectory = np.zeros((int((time_length+ramp+ramp_down)*sample_rate),3)) - dis_trajectory = ref_trajectory.copy() - ref_trajectory[:,2] = trials['ref'][trial_id] - dis_trajectory[:,2] = trials['dis'][trial_id] # scale will determine lower limit of target size for perfect tracking - targs.append(ref_trajectory) - yield idx, targs, disturbance, dis_trajectory, sample_rate, ramp, ramp_down - idx += 1 + for trial_id in range(ntrials): + targs = [] + ref_trajectory = np.zeros((int((time_length+ramp+ramp_down)*sample_rate),3)) + dis_trajectory = ref_trajectory.copy() + ref_trajectory[:,2] = trials['ref'][trial_id] + dis_trajectory[:,2] = trials['dis'][trial_id] # scale will determine lower limit of target size for perfect tracking + targs.append(ref_trajectory) + yield idx, targs, disturbance, dis_trajectory, sample_rate, ramp, ramp_down + idx += 1 if dimensions == 2: trials, trial_order = ScreenTargetTracking.generate_2D_trajectories( From c7f6cc028c2e9eb6615400e85d17330d912bc5e8 Mon Sep 17 00:00:00 2001 From: mtringi Date: Sat, 26 Jul 2025 15:37:59 -0700 Subject: [PATCH 03/18] change original function to match master --- built_in_tasks/target_tracking_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 6c54757e..afd82619 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -1019,7 +1019,7 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= return trials, trial_order @staticmethod - def generate_trajectory(primes, base_period, ramp = 0.0, rd = 0.0): + def generate_trajectory(primes, base_period, ramp = 0.0): ''' Sets up variables and uses prime numbers to call the above functions and generate then trajectories ramp is time length for preparatory lines @@ -1046,7 +1046,7 @@ def generate_trajectory(primes, base_period, ramp = 0.0, rd = 0.0): N = t.size # = T/dt -- number of samples #trajectory = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, f, a, o/(2*np.pi)) - trajectory = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f, a, o) + trajectory = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, f, a, o) normalized_trajectory = trajectory/np.sum(a) return normalized_trajectory From 653cbbedb8c618e00a1abfbc9b819991f3830d50 Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Tue, 29 Jul 2025 14:47:30 -0700 Subject: [PATCH 04/18] fix lookahead --- built_in_tasks/target_graphics.py | 17 +++- built_in_tasks/target_tracking_task.py | 38 +++++-- riglib/stereo_opengl/primitives.py | 117 ++++++++++++++++++---- riglib/stereo_opengl/shaders/none.f.glsl | 2 + riglib/stereo_opengl/shaders/phong.f.glsl | 2 + riglib/stereo_opengl/textures.py | 14 +-- tests/test_graphics.py | 11 +- tests/test_tasks.py | 17 +++- 8 files changed, 172 insertions(+), 46 deletions(-) diff --git a/built_in_tasks/target_graphics.py b/built_in_tasks/target_graphics.py index 41dc1157..e813238b 100644 --- a/built_in_tasks/target_graphics.py +++ b/built_in_tasks/target_graphics.py @@ -2,7 +2,7 @@ Base tasks for generic point-to-point reaching ''' import numpy as np -from riglib.stereo_opengl.primitives import Cable, Sphere, Cube, Torus, Text +from riglib.stereo_opengl.primitives import Cable, Snake, Sphere, Cube, Torus, Text from riglib.stereo_opengl.primitives import Cylinder, Plane, Sphere, Cube from riglib.stereo_opengl.models import FlatMesh, Group from riglib.stereo_opengl.textures import Texture, TexModel @@ -175,7 +175,7 @@ def __init__(self, target_radius=1, target_color=(1, 0, 0, .5), starting_pos=np. self._pickle_init() def _pickle_init(self): - self.cable = Cable(radius=self.target_radius,trajectory = self.trajectory, color=self.target_color) + self.cable = Cable(radius=self.target_radius, xyz=self.trajectory, color=self.target_color) self.graphics_models = [self.cable] self.cable.translate(*self.position) @@ -227,6 +227,19 @@ def reset(self): def get_position(self): return self.cable.xfm.move +class VirtualSnakeTarget(VirtualCableTarget): + + def _pickle_init(self): + self.cable = Snake(radius=self.target_radius, trajectory=self.trajectory, color=self.target_color) + self.graphics_models = [self.cable] + self.cable.translate(*self.position) + + def update_mask(self, start_frame, end_frame): + ''' + Update the texture mask of the snake target. + ''' + self.cable.update_texture(start_frame, end_frame) + class VirtualTorusTarget(VirtualCircularTarget): def __init__(self, inner_radius=2, outer_radius=3, target_color=(1, 0, 0, .5), starting_pos=np.zeros(3)): diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index e5936efc..6d0a09b6 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -405,6 +405,8 @@ class ScreenTargetTracking(TargetTracking, Window): target_radius = traits.Float(2, desc="Radius of targets in cm") #2,0.75 trajectory_radius = traits.Float(.5, desc="Radius of targets in cm") trajectory_color = traits.OptionsList("gold", *target_colors, desc="Color of the trajectory", bmi3d_input_options=list(target_colors.keys())) + trajectory_type = traits.OptionsList("time", ["time", "space", "none"], desc="Type of trajectory to use", bmi3d_input_options=["time", "space", "none"]) + lookahead_time = traits.Float(0.5, desc="Time in seconds to display the future trajectory") target_color = traits.OptionsList("yellow", *target_colors, desc="Color of the target", bmi3d_input_options=list(target_colors.keys())) plant_hide_rate = traits.Float(0.0, desc='If the plant is visible, specifies a percentage of trials where it will be hidden') plant_type = traits.OptionsList(*plantlist, bmi3d_input_options=list(plantlist.keys())) @@ -429,7 +431,7 @@ def __init__(self, *args, **kwargs): self.plant.set_cursor_radius(self.cursor_radius) self.plant_vis_prev = True self.cursor_vis_prev = True - self.lookahead = 30 # number of frames to create a "lookahead" window of 0.5 seconds (half the screen) + self.lookahead = int(self.fps * self.lookahead_time) self.original_limit1d = self.limit1d # keep track of original settable trait if not self.always_1d: @@ -526,9 +528,13 @@ def update_plant_visibility(self): def update_frame(self): #self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # xzy self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) - self.trajectory.move_to_position(np.array([-self.frame_index/3,0,0])) # same update constant works for 60 and 120 hz self.target.show() - self.trajectory.show() + if self.trajectory_type == 'time': + self.trajectory.move_to_position(np.array([-self.frame_index/3,0,0])) # same update constant works for 60 and 120 hz + self.trajectory.show() + elif self.trajectory_type == 'space': + self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) + self.trajectory.show() self.frame_index +=1 def setup_start_wait(self): @@ -555,15 +561,27 @@ def setup_start_wait(self): self.tracking_frame_index = 0 # Set up the next trajectory - next_trajectory = np.array(np.squeeze(self.targs)[:,2]) - next_trajectory[:self.lookahead] = next_trajectory[self.lookahead] - if hasattr(self, 'trajectory'): for model in self.trajectory.graphics_models: self.remove_model(model) del self.trajectory - - self.trajectory = VirtualCableTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) + if self.trajectory_type == 'time': + next_trajectory = np.array(np.squeeze(self.targs)[:,2]) + next_trajectory[:self.lookahead] = next_trajectory[self.lookahead] + next_trajectory = np.vstack([ + np.arange(len(next_trajectory))/3-self.lookahead/3, # x-axis is in cm, so divide by 3? to get time? idk + np.zeros(len(next_trajectory)), + next_trajectory + ]).T + self.trajectory = VirtualCableTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) + elif self.trajectory_type == 'space': + next_trajectory = self.targs[self.lookahead:] + self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) + print(target_colors[self.trajectory_color]) + self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) + else: # 'none' + next_trajectory = np.zeros((self.lookahead, 3)) + self.trajectory = VirtualCircularTarget() for model in self.trajectory.graphics_models: self.add_model(model) @@ -647,13 +665,13 @@ def _start_trajectory(self): if self.frame_index == 0: #self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # tablet screen x-axis ranges -19,19, center 0 self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) # tablet screen x-axis ranges -19,19, center 0 - self.trajectory.move_to_position(np.array([0,0,0])) # tablet screen x-axis ranges 0,41.33333, center 22ish # print(self.target.get_position()) # print(self.trajectory.get_position()) self.target.show() - self.trajectory.show() + if self.trajectory_type != "none": + self.trajectory.show() # print('SHOW TRAJ') self.sync_event('TARGET_ON') diff --git a/riglib/stereo_opengl/primitives.py b/riglib/stereo_opengl/primitives.py index 85c42812..d948a5d6 100644 --- a/riglib/stereo_opengl/primitives.py +++ b/riglib/stereo_opengl/primitives.py @@ -154,33 +154,72 @@ def __init__(self, height=1, radius=1, segments=36, **kwargs): super().__init__(total_pts, total_polys, tcoords=total_tcoords, normals=total_normals, **kwargs) class Cable(TriMesh): - def __init__(self,radius=.5, trajectory = np.array([np.sin(x) for x in range(60)]), segments=12,**kwargs): - self.trial_trajectory = trajectory + def __init__(self,radius=.5, xyz = np.array([np.sin(x) for x in range(60)]), segments=12,**kwargs): + self.xyz = xyz + if np.ndim(xyz) == 1: + self.xyz = np.array([[x,0,xyz[x]] for x in range(len(xyz))]) self.center_value = [0,0,0] self.radius = radius self.segments = segments self.update(**kwargs) def update(self, **kwargs): - theta = np.linspace(0, 2*np.pi, self.segments, endpoint=False) - unit = np.array([np.ones(self.segments),np.cos(theta) ,np.sin(theta)]).T - intial = np.array([[0,0,self.trial_trajectory[x]] for x in range(len(self.trial_trajectory))]) - self.pts = (unit*[-30/1.36,self.radius,self.radius])+intial[0] - for i in range(1,len(intial)): - self.pts = np.vstack([self.pts, (unit*[(i-30)/3,self.radius,self.radius])+intial[i]]) - - self.normals = np.vstack([unit*[1,1,0], unit*[1,1,0]]) - self.polys = [] - for i in range(self.segments-1): - for j in range(len(intial)-1): - self.polys.append((i+j*self.segments, i+1+j*self.segments, i+self.segments+j*self.segments)) - self.polys.append((i+self.segments+j*self.segments, i+1+j*self.segments, i+1+self.segments+j*self.segments)) - - tcoord = np.array([np.arange(self.segments), np.ones(self.segments)]).T - n = 1./self.segments - self.tcoord = np.vstack([tcoord*[n,1], tcoord*[n,0]]) - super(Cable, self).__init__(self.pts, np.array(self.polys), - tcoords=self.tcoord, normals=self.normals, **kwargs) + theta = np.linspace(0, 2 * np.pi, self.segments, endpoint=False) + circle = np.stack([np.cos(theta), np.sin(theta)], axis=1) # (segments, 2) + + pts = [] + normals = [] + tcoords = [] + n_path = len(self.xyz) + + y_axis = np.array([0, 1, 0]) # fixed up direction + + # Compute tangents along path + tangents = np.gradient(self.xyz, axis=0) + tangents = tangents / np.linalg.norm(tangents, axis=1, keepdims=True) + + for i in range(n_path): + p = self.xyz[i] + t = tangents[i] + + # Project tangent onto XZ plane to control ring orientation + t_xz = t - np.dot(t, y_axis) * y_axis # remove Y component + if np.linalg.norm(t_xz) < 1e-6: + t_xz = np.array([1, 0, 0]) # fallback + else: + t_xz = t_xz / np.linalg.norm(t_xz) + + # Use t_xz as forward direction in ring plane (angle 0) + b = t_xz # X axis of ring + a = np.cross(y_axis, b) # Z axis of ring + + for j in range(self.segments): + cx, cy = circle[j] + offset = self.radius * (cx * b + cy * a) + pts.append(p + offset) + normals.append(offset / np.linalg.norm(offset)) + tcoords.append([j / self.segments, i / (n_path - 1)]) + + self.pts = np.array(pts) + self.normals = np.array(normals) + self.tcoord = np.array(tcoords) + + # Create triangle strips between rings + polys = [] + for i in range(n_path - 1): + for j in range(self.segments): + i0 = i * self.segments + j + i1 = i * self.segments + (j + 1) % self.segments + i2 = (i + 1) * self.segments + j + i3 = (i + 1) * self.segments + (j + 1) % self.segments + + polys.append((i0, i1, i2)) + polys.append((i2, i1, i3)) + + self.polys = np.array(polys) + + super().__init__(self.pts, self.polys, tcoords=self.tcoord, + normals=self.normals, **kwargs) class Torus(TriMesh): ''' @@ -447,6 +486,42 @@ def __init__(self, id, size, alpha=1, **kwargs): super().__init__(size, size, color=[0,0,0,alpha], specular_color=[0,0,0,0], tex=apriltag) self.rotate_x(90) +class Snake(Cable, TexModel): + ''' + A Cable with a gradient texture applied along its length. + ''' + def __init__(self, radius=.5, trajectory=np.array([np.sin(x) for x in range(100)]), segments=12, n_colors=1000, **kwargs): + self.trajectory = trajectory + self.n_colors = n_colors + color = kwargs.pop('color', [1, 1, 1, 1]) # Default color if not provided + self.color = color + tex = self.get_texture(0, len(trajectory), n_colors) + super().__init__(radius, trajectory, segments, tex=tex, color=[1, 0, 0, 1], **kwargs) + self.color = color # Store the color for later use + + def get_texture(self, start_frame, end_frame, n_colors): + mask = np.zeros(n_colors) + start_frame_idx = int(float(start_frame) / len(self.trajectory) * n_colors) + end_frame_idx = int(float(end_frame) / len(self.trajectory) * n_colors) + if start_frame_idx >= n_colors: + start_frame_idx = n_colors + if end_frame_idx >= n_colors: + end_frame_idx = n_colors + mask[start_frame_idx:end_frame_idx] = 1 + mask = np.tile(mask, (4, 1)).T # Repeat for RGBA + mask = self.color * mask # Apply color + tex = Texture(mask.reshape((1, len(mask), 4))) # Reshape to (1, n_colors, 4) + return tex + + def update_texture(self, start_frame, end_frame): + ''' + Update the texture of the snake based on the new trajectory. + ''' + self.tex.delete() # Delete the old texture + tex = self.get_texture(start_frame, end_frame, self.n_colors) + self.tex = tex + self.tex.init() + ##### 2-D primitives ##### class Shape2D(object): diff --git a/riglib/stereo_opengl/shaders/none.f.glsl b/riglib/stereo_opengl/shaders/none.f.glsl index 675df621..ebc74cff 100644 --- a/riglib/stereo_opengl/shaders/none.f.glsl +++ b/riglib/stereo_opengl/shaders/none.f.glsl @@ -13,5 +13,7 @@ void main() { texcolor.rgb + basecolor.rgb, texcolor.a * basecolor.a ); + if (frag_diffuse.a < 0.01) + discard; FragColor = frag_diffuse; } diff --git a/riglib/stereo_opengl/shaders/phong.f.glsl b/riglib/stereo_opengl/shaders/phong.f.glsl index b6ae4f84..d7c872e6 100644 --- a/riglib/stereo_opengl/shaders/phong.f.glsl +++ b/riglib/stereo_opengl/shaders/phong.f.glsl @@ -58,6 +58,8 @@ vec4 phong() { texcolor.rgb + basecolor.rgb, texcolor.a * basecolor.a ); + if (frag_diffuse.a < 0.01) + discard; vec4 diffuse_factor = max(-dot(normal, mv_light_direction), 0.0) * light_diffuse; diff --git a/riglib/stereo_opengl/textures.py b/riglib/stereo_opengl/textures.py index 7217946f..dcc7109c 100644 --- a/riglib/stereo_opengl/textures.py +++ b/riglib/stereo_opengl/textures.py @@ -26,11 +26,13 @@ def __init__(self, tex, size=None, if isinstance(tex, np.ndarray): if tex.max() <= 1: - tex *= 255 - if len(tex.shape) < 3: - tex = np.tile(tex, [3, 1, 1]).T - if tex.shape[-1] == 3: - tex = np.dstack([tex, np.ones(tex.shape[:-1])]) + tex = (tex * 255).astype(np.uint8) + else: + tex = tex.astype(np.uint8) + if tex.ndim == 2: + tex = np.stack([tex]*3, axis=-1) # grayscale → RGB + elif tex.shape[-1] == 1: + tex = np.repeat(tex, 3, axis=-1) size = tex.shape[:2] tex = tex.astype(np.uint8).tobytes() elif isinstance(tex, str): @@ -87,8 +89,6 @@ def init(self): error = glGetError() if error != GL_NO_ERROR: print(f"OpenGL error after texture creation: {error}") - else: - print(f"Texture initialized successfully: {gltex}") self.tex = gltex diff --git a/tests/test_graphics.py b/tests/test_graphics.py index be4c1bc7..5c0ab64d 100644 --- a/tests/test_graphics.py +++ b/tests/test_graphics.py @@ -11,7 +11,7 @@ from riglib.stereo_opengl.environment import Grid from riglib.stereo_opengl.window import Window, Window2D, FPScontrol -from riglib.stereo_opengl.primitives import AprilTag, Cylinder, Cube, Plane, Sphere, Cone, Text, TexSphere, TexCube, TexPlane +from riglib.stereo_opengl.primitives import AprilTag, Cylinder, Cube, Plane, Snake, Sphere, Cone, Text, Cable, TexSphere, TexCube, TexPlane from features.optitrack_features import SpheresToCylinders from riglib.stereo_opengl.window import Window, Window2D, FPScontrol, WindowSSAO from riglib.stereo_opengl.openxr import WindowVR @@ -39,13 +39,14 @@ planet = Sphere(3, color=[0.75,0.25,0.25,0.75]) orbit_radius = 4 orbit_speed = 1 -wobble_radius = 0 +wobble_radius = 5 wobble_speed = 0.5 #TexSphere = type("TexSphere", (Sphere, TexModel), {}) #TexPlane = type("TexPlane", (Plane, TexModel), {}) -#reward_text = Text(7.5, "123", justify='right', color=[1,0,1,1]) +reward_text = Text(7.5, "123", justify='right', color=[1,0,1,1]) # center_out_gen = ScreenTargetCapture.centerout_2D(1) # center_out_positions = [pos[1] for _, pos in center_out_gen] +cable = Snake(2, 2*np.sin(np.arange(100)/5)) center_out_gen = ScreenTargetCapture.centerout_tabletop(1) center_out_positions = [(pos[1][0], pos[1][1], -10) for _, pos in center_out_gen] center_out_targets = [ @@ -67,6 +68,7 @@ def _start_draw(self): #arm4j.set_joint_pos([0,0,np.pi/2,np.pi/2]) #arm4j.get_endpoint_pos() self.add_model(Grid(50)) + self.add_model(cable) # self.add_model(moon) # self.add_model(planet) # self.add_model(arm4j) @@ -75,7 +77,7 @@ def _start_draw(self): # self.add_model(TexPlane(5,5, tex=cloudy_tex(), specular_color=(0.,0,0,1)).rotate_x(90)) # self.add_model(TexPlane(5,5, specular_color=(0.,0,0,1), tex=cloudy_tex()).rotate_x(90)) # reward_text = Text(7.5, "123", justify='right', color=[1,0,1,0.75]) - # self.add_model(reward_text) + self.add_model(reward_text) # self.add_model(TexPlane(4,4,color=[0,0,0,0.9], tex=cloudy_tex()).rotate_x(90).translate(0,0,-5)) #self.screen_init() #self.draw_world() @@ -85,6 +87,7 @@ def _start_draw(self): def _while_draw(self): ts = time.time() - self.start_time + cable.update_texture(int(ts*10), len(cable.trajectory)) x = travel_radius * np.cos(ts * travel_speed) y = travel_radius * np.sin(ts * travel_speed) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 7bd12579..281b6447 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -35,6 +35,7 @@ def init_exp(base_class, feats, seq=None, **kwargs): class TestManualControlTasks(unittest.TestCase): + @unittest.skip("") def test_readysetgo(self): seq = ManualControl.centerout_2D() exp = init_exp(ReadySetGoTask, [MouseControl, Window2D], seq, window_size=(1200,800), fullscreen=False) @@ -64,10 +65,22 @@ def test_example_task(self): def test_tracking(self): print("Running tracking task test") seq = TrackingTask.tracking_target_debug(nblocks=1, ntrials=6, time_length=5, seed=40, sample_rate=60, ramp=1) # sample_rate needs to match fps in ScreenTargetTracking - exp = init_exp(TrackingTask, [MouseControl], seq) # , window_size=(1000,800) + exp = init_exp(TrackingTask, [MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False) exp.rotation = 'xzy' exp.run() + def test_tracking_2d(self): + print("Running tracking task test") + seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, + num_primes=8, seed=42, sample_rate=120, dimensions=2, + disturbance=False, boundaries=(-10,10,-10,10)) + exp = init_exp(TrackingTask, [MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False, + limit1d=False, trajectory_amplitude=5, lookahead_time=1) + exp.stereo_mode = 'projection' + exp.rotation = 'xzy' + exp.trajectory_type = 'space' + exp.run() + @unittest.skip("") def test_sequence(self): print("Running sequence task test") @@ -88,7 +101,7 @@ def test_force_task(self): exp.end_task() @unittest.skip("only to test progress bar") - def test_tracking(self): + def test_progress_bar(self): seq = TrackingTask.tracking_target_debug(nblocks=1, ntrials=6, time_length=5, seed=40, sample_rate=60, ramp=1) # sample_rate needs to match fps in ScreenTargetTracking exp = init_exp(TrackingTask, [MouseControl, Window2D, ProgressBar], seq) exp.rotation = 'xzy' From 5de36900dd9b3445cece302671625d2208df9ada Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Fri, 1 Aug 2025 12:52:50 -0700 Subject: [PATCH 05/18] cleanup --- built_in_tasks/target_graphics.py | 2 +- built_in_tasks/target_tracking_task.py | 23 +++++------------------ riglib/stereo_opengl/primitives.py | 18 +++++++----------- tests/test_graphics.py | 10 ++++++---- tests/test_tasks.py | 4 ++-- 5 files changed, 21 insertions(+), 36 deletions(-) diff --git a/built_in_tasks/target_graphics.py b/built_in_tasks/target_graphics.py index e813238b..84cea76d 100644 --- a/built_in_tasks/target_graphics.py +++ b/built_in_tasks/target_graphics.py @@ -36,7 +36,7 @@ "elephant":(0.5,0.5,0.5,0.5), "white": (1, 1, 1, 0.75), "black": (0, 0, 0, 0.75), - 'invisible': (0, 0, 0, 0.0), + "invisible": (0, 0, 0, 0.0), } class CircularTarget(object): diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 6d0a09b6..0ce55e56 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -393,7 +393,7 @@ class ScreenTargetTracking(TargetTracking, Window): limit1d = traits.Bool(True, desc="Limit cursor movement to 1D") sequence_generators = [ - 'tracking_target_chain', 'tracking_target_debug', 'tracking_target_training', 'generate_2D_trajectories' + 'tracking_target_chain', 'tracking_target_debug', 'tracking_target_training' ] hidden_traits = ['cursor_color', 'trajectory_color', 'cursor_bounds', 'cursor_radius', 'plant_hide_rate', 'starting_pos'] @@ -447,13 +447,14 @@ def __init__(self, *args, **kwargs): if instantiate_targets: # This is the center target being followed by the user self.target = VirtualCircularTarget(target_radius=self.target_radius, target_color=target_colors[self.target_color]) - - # This is the trajectory that spans the screen - self.trajectory = VirtualCableTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color]) + for model in self.target.graphics_models: + self.add_model(model) # This is the progress bar self.bar = VirtualRectangularTarget(target_width=1, target_height=0, target_color=(0., 1., 0., 0.75), starting_pos=[0,0,9]) # print('INIT TRAJ') + for model in self.bar.graphics_models: + self.add_model(model) # Declare any plant attributes which must be saved to the HDF file at the _cycle rate for attr in self.plant.hdf_attrs: @@ -526,7 +527,6 @@ def update_plant_visibility(self): self.plant.set_visibility(self.plant_visible) def update_frame(self): - #self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # xzy self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) self.target.show() if self.trajectory_type == 'time': @@ -538,19 +538,6 @@ def update_frame(self): self.frame_index +=1 def setup_start_wait(self): - if self.calc_trial_num() == 0: - # Instantiate the targets here so they don't show up in any states that might come before "wait" - for model in self.target.graphics_models: - self.add_model(model) - self.target.hide() - - for model in self.trajectory.graphics_models: - self.add_model(model) - self.trajectory.hide() - - for model in self.bar.graphics_models: - self.add_model(model) - self.bar.hide() # Allow 2d movement if not self.always_1d: diff --git a/riglib/stereo_opengl/primitives.py b/riglib/stereo_opengl/primitives.py index d948a5d6..02c14c31 100644 --- a/riglib/stereo_opengl/primitives.py +++ b/riglib/stereo_opengl/primitives.py @@ -172,7 +172,7 @@ def update(self, **kwargs): tcoords = [] n_path = len(self.xyz) - y_axis = np.array([0, 1, 0]) # fixed up direction + a = np.array([0, 1, 0]) # fixed up direction # Compute tangents along path tangents = np.gradient(self.xyz, axis=0) @@ -182,16 +182,12 @@ def update(self, **kwargs): p = self.xyz[i] t = tangents[i] - # Project tangent onto XZ plane to control ring orientation - t_xz = t - np.dot(t, y_axis) * y_axis # remove Y component - if np.linalg.norm(t_xz) < 1e-6: - t_xz = np.array([1, 0, 0]) # fallback + # Ring orientation + b = np.cross(t, a) + if np.linalg.norm(b) < 1e-6: + b = np.array([0, 0, 1]) # fallback else: - t_xz = t_xz / np.linalg.norm(t_xz) - - # Use t_xz as forward direction in ring plane (angle 0) - b = t_xz # X axis of ring - a = np.cross(y_axis, b) # Z axis of ring + b = b / np.linalg.norm(b) for j in range(self.segments): cx, cy = circle[j] @@ -496,7 +492,7 @@ def __init__(self, radius=.5, trajectory=np.array([np.sin(x) for x in range(100) color = kwargs.pop('color', [1, 1, 1, 1]) # Default color if not provided self.color = color tex = self.get_texture(0, len(trajectory), n_colors) - super().__init__(radius, trajectory, segments, tex=tex, color=[1, 0, 0, 1], **kwargs) + super().__init__(radius, trajectory, segments, tex=tex, color=[0, 0, 0, 1], **kwargs) self.color = color # Store the color for later use def get_texture(self, start_frame, end_frame, n_colors): diff --git a/tests/test_graphics.py b/tests/test_graphics.py index 5c0ab64d..4c8baaa1 100644 --- a/tests/test_graphics.py +++ b/tests/test_graphics.py @@ -46,7 +46,7 @@ reward_text = Text(7.5, "123", justify='right', color=[1,0,1,1]) # center_out_gen = ScreenTargetCapture.centerout_2D(1) # center_out_positions = [pos[1] for _, pos in center_out_gen] -cable = Snake(2, 2*np.sin(np.arange(100)/5)) +cable = Snake(0.5, 2*np.sin(np.arange(200)/2), color=(1,0,1,0.75)).translate(-15, 0, -10) center_out_gen = ScreenTargetCapture.centerout_tabletop(1) center_out_positions = [(pos[1][0], pos[1][1], -10) for _, pos in center_out_gen] center_out_targets = [ @@ -68,7 +68,6 @@ def _start_draw(self): #arm4j.set_joint_pos([0,0,np.pi/2,np.pi/2]) #arm4j.get_endpoint_pos() self.add_model(Grid(50)) - self.add_model(cable) # self.add_model(moon) # self.add_model(planet) # self.add_model(arm4j) @@ -83,11 +82,14 @@ def _start_draw(self): #self.draw_world() for model in center_out_targets: self.add_model(model) - self.add_model(Sphere(radius=1, color=target_colors['purple']).translate(3,3,-10)) + for model in center_out_targets: + self.add_model(model) + self.add_model(Sphere(radius=1, color=target_colors['purple']).translate(3,0,-10)) + self.add_model(cable) def _while_draw(self): ts = time.time() - self.start_time - cable.update_texture(int(ts*10), len(cable.trajectory)) + # cable.update_texture(int(ts*10), len(cable.trajectory)) x = travel_radius * np.cos(ts * travel_speed) y = travel_radius * np.sin(ts * travel_speed) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 281b6447..114d15b4 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -43,7 +43,6 @@ def test_readysetgo(self): exp.ready_set_sound = 'tones.wav' exp.run() - @unittest.skip("") def test_exp(self): seq = ManualControl.centerout_2D() @@ -69,12 +68,13 @@ def test_tracking(self): exp.rotation = 'xzy' exp.run() + @unittest.skip("") def test_tracking_2d(self): print("Running tracking task test") seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, num_primes=8, seed=42, sample_rate=120, dimensions=2, disturbance=False, boundaries=(-10,10,-10,10)) - exp = init_exp(TrackingTask, [MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False, + exp = init_exp(TrackingTask, [MouseControl], seq, window_size=(1000,800), fullscreen=False, limit1d=False, trajectory_amplitude=5, lookahead_time=1) exp.stereo_mode = 'projection' exp.rotation = 'xzy' From 0459c034f4dee19a87aee00afa3d43496dacb0d0 Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Mon, 4 Aug 2025 12:59:16 -0700 Subject: [PATCH 06/18] snake always in front --- built_in_tasks/target_graphics.py | 1 + tests/test_tasks.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/built_in_tasks/target_graphics.py b/built_in_tasks/target_graphics.py index 84cea76d..aeacd281 100644 --- a/built_in_tasks/target_graphics.py +++ b/built_in_tasks/target_graphics.py @@ -230,6 +230,7 @@ def get_position(self): class VirtualSnakeTarget(VirtualCableTarget): def _pickle_init(self): + self.trajectory = np.array(self.trajectory) + np.array([0, -10, 0]) # Hack to make sure the snake is always in front self.cable = Snake(radius=self.target_radius, trajectory=self.trajectory, color=self.target_color) self.graphics_models = [self.cable] self.cable.translate(*self.position) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 114d15b4..67524c22 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -66,19 +66,23 @@ def test_tracking(self): seq = TrackingTask.tracking_target_debug(nblocks=1, ntrials=6, time_length=5, seed=40, sample_rate=60, ramp=1) # sample_rate needs to match fps in ScreenTargetTracking exp = init_exp(TrackingTask, [MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False) exp.rotation = 'xzy' + # exp.trajectory_type = 'space' + # exp.trajectory_amplitude = 5 + # exp.lookahead_time = 1 exp.run() - @unittest.skip("") + # @unittest.skip("") def test_tracking_2d(self): print("Running tracking task test") seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, num_primes=8, seed=42, sample_rate=120, dimensions=2, - disturbance=False, boundaries=(-10,10,-10,10)) - exp = init_exp(TrackingTask, [MouseControl], seq, window_size=(1000,800), fullscreen=False, - limit1d=False, trajectory_amplitude=5, lookahead_time=1) + disturbance=True, boundaries=(-10,10,-10,10)) + exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, + limit1d=False, trajectory_amplitude=5, lookahead_time=2) exp.stereo_mode = 'projection' exp.rotation = 'xzy' exp.trajectory_type = 'space' + exp.pertubation_rotation = 25 exp.run() @unittest.skip("") From 71165266d7fbaf7dcbe0f1fa544b9412dad84de7 Mon Sep 17 00:00:00 2001 From: mtringi Date: Mon, 11 Aug 2025 17:40:09 -0700 Subject: [PATCH 07/18] add logic to distribute frequencies --- built_in_tasks/target_tracking_task.py | 48 ++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index e5936efc..36b66698 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -951,8 +951,8 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= full_primes = np.asarray([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199]) - xprimes = full_primes[:num_primes:2] - yprimes = full_primes[1:num_primes:2] + xprimes = full_primes[:num_primes:2] #even elements of full primes + yprimes = full_primes[1:num_primes:2] #odd elements of full primes f_x = xprimes*w0 # stimulated frequencies f_y = yprimes*w0 @@ -989,20 +989,54 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= elif order == 1: trial_order = [(1,'O','E'),(1,'E','O')] + # generate pairing of elements + first_set = [] #initialize empty first list + second_set = [] #initialize second list + nele = len(xprimes) # number of elements in xprimes (and yprimes) + #set up logic for final two elements + if nele % 2: #if number of elements divided by 2 has remainder (ie if num of elements is odd) + ele2 = 0 + else: + ele2 = 1 #set up to be able to skip the last two elements + + for ele in range((nele//2) - ele2): #floor division to get half the length of elments + left_side = ele + right_side = nele-1-ele + + if ele % 2 == 0: #remainder division for odds and evens + first_set.extend([left_side, right_side]) + else: + second_set.extend([left_side, right_side]) + + + if nele % 2 == 1: #if odd number of elements in xprime or yprime + mid_ele = nele// 2 #grab element in middle + second_set.append(mid_ele) + else: + mid1 = nele // 2 - 1 + mid2 = nele // 2 + first_set.append(mid1) + second_set.append(mid2) + + # generate reference and disturbance trajectories for all trials for trial_id, (num_reps,ref_ind,dis_ind) in enumerate(trial_order*int(num_trials/2)): if ref_ind == 'E': - sines_r = np.arange(len(xprimes))[0::2] # use even indices + #sines_r = np.arange(len(xprimes))[0::2] # use even indices + sines_r = first_set elif ref_ind == 'O': - sines_r = np.arange(len(xprimes))[1::2] # use odd indices + #sines_r = np.arange(len(xprimes))[1::2] # use odd indices + sines_r = second_set else: sines_r = np.arange(len(xprimes)) if dis_ind == 'E': - sines_d = np.arange(len(xprimes))[0::2] + #sines_d = np.arange(len(xprimes))[0::2] + sines_d = first_set elif dis_ind == 'O': - sines_d = np.arange(len(xprimes))[1::2] + #sines_d = np.arange(len(xprimes))[1::2] + sines_d = second_set else: - sines_d = np.arange(len(xprimes)) + sines_d = np.arange(len(xprimes)) #every element in vector # generate X-dimension refx_traj, ref_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_x[sines_r], a_x[sines_r], o_x[trial_id][sines_r]) From c2f7b500190e78e7bb215c3be5041ef2ffd39862 Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Tue, 12 Aug 2025 13:49:13 -0700 Subject: [PATCH 08/18] graphics fixes --- built_in_tasks/target_graphics.py | 2 +- built_in_tasks/target_tracking_task.py | 7 ++----- riglib/stereo_opengl/models.py | 16 +++++++++++++--- riglib/stereo_opengl/primitives.py | 11 ++++++++--- riglib/stereo_opengl/window.py | 2 +- tests/test_graphics.py | 4 ++-- tests/test_tasks.py | 11 +++++------ 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/built_in_tasks/target_graphics.py b/built_in_tasks/target_graphics.py index aeacd281..0c17b33f 100644 --- a/built_in_tasks/target_graphics.py +++ b/built_in_tasks/target_graphics.py @@ -230,7 +230,7 @@ def get_position(self): class VirtualSnakeTarget(VirtualCableTarget): def _pickle_init(self): - self.trajectory = np.array(self.trajectory) + np.array([0, -10, 0]) # Hack to make sure the snake is always in front + self.trajectory = np.array(self.trajectory) self.cable = Snake(radius=self.target_radius, trajectory=self.trajectory, color=self.target_color) self.graphics_models = [self.cable] self.cable.translate(*self.position) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index f76d24ef..86a94451 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -528,13 +528,10 @@ def update_plant_visibility(self): def update_frame(self): self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) - self.target.show() if self.trajectory_type == 'time': - self.trajectory.move_to_position(np.array([-self.frame_index/3,0,0])) # same update constant works for 60 and 120 hz - self.trajectory.show() + self.trajectory.move_to_position(np.array([-self.frame_index,0,0])) # same update constant works for 60 and 120 hz elif self.trajectory_type == 'space': self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) - self.trajectory.show() self.frame_index +=1 def setup_start_wait(self): @@ -556,7 +553,7 @@ def setup_start_wait(self): next_trajectory = np.array(np.squeeze(self.targs)[:,2]) next_trajectory[:self.lookahead] = next_trajectory[self.lookahead] next_trajectory = np.vstack([ - np.arange(len(next_trajectory))/3-self.lookahead/3, # x-axis is in cm, so divide by 3? to get time? idk + np.arange(len(next_trajectory))-self.lookahead, # x-axis is in cm, so divide by 3? to get time? idk np.zeros(len(next_trajectory)), next_trajectory ]).T diff --git a/riglib/stereo_opengl/models.py b/riglib/stereo_opengl/models.py index 6dc06254..4a4d57c9 100644 --- a/riglib/stereo_opengl/models.py +++ b/riglib/stereo_opengl/models.py @@ -37,6 +37,9 @@ def __init__(self, shader="default", color=(0.5, 0.5, 0.5, 1), # The orientation of the object, in the world frame self._xfm = self.xfm self.allocated = False + + # Keep track of the model's size for rendering + self.bounding_radius = 0.0 def __setattr__(self, attr, xfm): '''Checks if the xfm was changed, and recaches the _xfm which is sent to the shader''' @@ -143,13 +146,18 @@ def init(self): model.init() def render_queue(self, xfm=np.eye(4), **kwargs): - for model in self.models: + def sort_key(obj): + pos = obj.xfm.move[1] + radius = obj.bounding_radius + dist = pos - radius + return -dist # Negative for far-to-near sorting + sorted_models = sorted(self.models, key=sort_key) + for model in sorted_models: for out in model.render_queue(**kwargs): yield out def draw(self, ctx, **kwargs): - sorted_models = sorted(self.models, key=lambda obj: -obj.xfm.move[1]) - for model in sorted_models: + for model in self.models: model.draw(ctx, **kwargs) def __getitem__(self, idx): @@ -190,6 +198,8 @@ def __init__(self, verts, polys, normals=None, tcoords=None, **kwargs): self.polys = polys self.tcoords = tcoords self.normals = normals + + self.bounding_radius = abs(np.min(self.verts[:, 1])) def init(self): allocated = super(TriMesh, self).init() diff --git a/riglib/stereo_opengl/primitives.py b/riglib/stereo_opengl/primitives.py index 02c14c31..404ed041 100644 --- a/riglib/stereo_opengl/primitives.py +++ b/riglib/stereo_opengl/primitives.py @@ -15,7 +15,7 @@ from .models import TriMesh from .textures import Texture, TexModel -from OpenGL.GL import GL_NEAREST +from OpenGL.GL import * from PIL import Image, ImageDraw, ImageFont import matplotlib.font_manager as fm @@ -209,8 +209,8 @@ def update(self, **kwargs): i2 = (i + 1) * self.segments + j i3 = (i + 1) * self.segments + (j + 1) % self.segments - polys.append((i0, i1, i2)) - polys.append((i2, i1, i3)) + polys.append((i2, i1, i0)) + polys.append((i3, i1, i2)) self.polys = np.array(polys) @@ -518,6 +518,11 @@ def update_texture(self, start_frame, end_frame): self.tex = tex self.tex.init() + def draw(self, ctx): + glDisable(GL_DEPTH_TEST) + super().draw(ctx) + glEnable(GL_DEPTH_TEST) + ##### 2-D primitives ##### class Shape2D(object): diff --git a/riglib/stereo_opengl/window.py b/riglib/stereo_opengl/window.py index e877855c..9da27513 100644 --- a/riglib/stereo_opengl/window.py +++ b/riglib/stereo_opengl/window.py @@ -92,7 +92,7 @@ def screen_init(self): glDisable(GL_FRAMEBUFFER_SRGB) # disable gamma correction glEnable(GL_BLEND) - glDepthFunc(GL_LESS) + glDepthFunc(GL_LEQUAL) glEnable(GL_DEPTH_TEST) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glClearColor(*self.background) diff --git a/tests/test_graphics.py b/tests/test_graphics.py index 4c8baaa1..dade55c3 100644 --- a/tests/test_graphics.py +++ b/tests/test_graphics.py @@ -68,8 +68,8 @@ def _start_draw(self): #arm4j.set_joint_pos([0,0,np.pi/2,np.pi/2]) #arm4j.get_endpoint_pos() self.add_model(Grid(50)) - # self.add_model(moon) - # self.add_model(planet) + self.add_model(moon) + self.add_model(planet) # self.add_model(arm4j) #self.add_model(reward_text.translate(5,0,-5)) # self.add_model(TexSphere(radius=3, specular_color=[1,1,1,1], tex=cloudy_tex()).translate(5,0,0)) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 67524c22..b60faf7b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -67,22 +67,21 @@ def test_tracking(self): exp = init_exp(TrackingTask, [MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False) exp.rotation = 'xzy' # exp.trajectory_type = 'space' - # exp.trajectory_amplitude = 5 + exp.trajectory_amplitude = 5 # exp.lookahead_time = 1 exp.run() # @unittest.skip("") def test_tracking_2d(self): print("Running tracking task test") - seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, - num_primes=8, seed=42, sample_rate=120, dimensions=2, - disturbance=True, boundaries=(-10,10,-10,10)) + seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, + num_primes=10, seed=42, sample_rate=60, dimensions=2, + disturbance=False, boundaries=(-10,10,-10,10)) exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, - limit1d=False, trajectory_amplitude=5, lookahead_time=2) + limit1d=False, trajectory_amplitude=5, lookahead_time=1) exp.stereo_mode = 'projection' exp.rotation = 'xzy' exp.trajectory_type = 'space' - exp.pertubation_rotation = 25 exp.run() @unittest.skip("") From a9a6a1f280057a18706b88331205e8dc0fe36ff2 Mon Sep 17 00:00:00 2001 From: mtringi Date: Tue, 12 Aug 2025 14:53:24 -0700 Subject: [PATCH 09/18] add fft plots to test_tasks --- tests/test_tasks.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index b60faf7b..9a35c5a7 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -18,6 +18,7 @@ from riglib.stereo_opengl.window import Window, Window2D import unittest import numpy as np +import matplotlib as plt import os import socket @@ -164,6 +165,28 @@ def test_corners(self): print(loc) print("---------------corners") + # @unittest.skip("") + def test_tracking_2d(self): + seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, + num_primes=10, seed=42, sample_rate=60, dimensions=2, + disturbance=False, boundaries=(-10,10,-10,10)) + trajectories = seq[1] + ft_len = np.arange(len(trajectories[0])) + st_len = np.arange(len(trajectories[1])) + first_trial_x = np.fft.fft(trajectories[0][:,2]) + second_trial_x = np.fft.fft(trajectories[1][:,2]) + first_trial_y = np.fft.fft(trajectories[0][:,0]) + second_trial_y = np.fft.fft(trajectories[1][:,0]) + fig, axs = plt.subplots(2,2, figsize = (10,8)) + axs[0,0].plot(ft_len, np.abs(first_trial_x)) + axs[0,0].set_title("First Trial X") + axs[0,1].plot(ft_len, np.abs(first_trial_y)) + axs[0,1].set_title("First Trial Y") + axs[1,0].plot(st_len, np.abs(second_trial_x)) + axs[1,0].set_title("Second Trial X") + axs[1,1].plot(st_len, np.abs(second_trial_y)) + axs[1,1].set_title("Second Trial Y") + class TestYouTube(unittest.TestCase): @unittest.skip("") From 697b84e6cbf215ddb1be35263c87c01f4c21bbee Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Wed, 13 Aug 2025 18:27:28 -0700 Subject: [PATCH 10/18] marios fix --- tests/test_tasks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 9a35c5a7..a605d6af 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -18,7 +18,7 @@ from riglib.stereo_opengl.window import Window, Window2D import unittest import numpy as np -import matplotlib as plt +import matplotlib.pyplot as plt import os import socket @@ -72,7 +72,7 @@ def test_tracking(self): # exp.lookahead_time = 1 exp.run() - # @unittest.skip("") + @unittest.skip("") def test_tracking_2d(self): print("Running tracking task test") seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, @@ -170,9 +170,10 @@ def test_tracking_2d(self): seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, num_primes=10, seed=42, sample_rate=60, dimensions=2, disturbance=False, boundaries=(-10,10,-10,10)) - trajectories = seq[1] + trajectories = [t[1][0] for t in seq] ft_len = np.arange(len(trajectories[0])) st_len = np.arange(len(trajectories[1])) + print(trajectories[0].shape, trajectories[1].shape) first_trial_x = np.fft.fft(trajectories[0][:,2]) second_trial_x = np.fft.fft(trajectories[1][:,2]) first_trial_y = np.fft.fft(trajectories[0][:,0]) @@ -186,6 +187,7 @@ def test_tracking_2d(self): axs[1,0].set_title("Second Trial X") axs[1,1].plot(st_len, np.abs(second_trial_y)) axs[1,1].set_title("Second Trial Y") + plt.show() class TestYouTube(unittest.TestCase): From bebc2f0afdea6ae3ba257daeb0c4d07e158d9acf Mon Sep 17 00:00:00 2001 From: mtringi Date: Fri, 10 Oct 2025 15:35:22 -0700 Subject: [PATCH 11/18] try exponential decay for amplitude of sine waves --- built_in_tasks/target_tracking_task.py | 21 +++++++---- tests/test_tasks.py | 51 +++++++++++++++----------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 86a94451..2b0ae583 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -959,8 +959,12 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= f_x = xprimes*w0 # stimulated frequencies f_y = yprimes*w0 - a_x = 1/(1+np.arange(f_x.size)) # amplitude - a_y = 1/(1+np.arange(f_y.size)) + #a_x = 1/(1+np.arange(f_x.size)) # amplitude linear/reciprocal + #a_y = 1/(1+np.arange(f_y.size)) + + k = 0.1 #decay rate + a_x = np.exp(-k*np.arange(f_x.size)) # amplitude exponential decay + a_y = np.exp(-k*np.arange(f_y.size)) # phase offset o_x = np.random.rand(num_trials, f_x.size) @@ -1019,8 +1023,11 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= mid2 = nele // 2 first_set.append(mid1) second_set.append(mid2) - + # #check with disturbance adn reference sharing primes + # sines_r = np.arange(len(xprimes)) + # sines_d = np.arange(len(xprimes)) + # generate reference and disturbance trajectories for all trials for trial_id, (num_reps,ref_ind,dis_ind) in enumerate(trial_order*int(num_trials/2)): if ref_ind == 'E': @@ -1148,11 +1155,11 @@ def tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_dow targs = [] ref_trajectory = np.zeros((int((time_length+ramp+ramp_down)*sample_rate),3)) dis_trajectory = ref_trajectory.copy() - ref_trajectory[:,2] = trials['ref_x'][trial_id] #y is out of the screen, x is left and right and z is up and down - ref_trajectory[:,0] = trials['ref_y'][trial_id] + ref_trajectory[:,2] = trials['ref_y'][trial_id] #y is out of the screen, x is left and right and z is up and down + ref_trajectory[:,0] = trials['ref_x'][trial_id] - dis_trajectory[:,2] = trials['dis_x'][trial_id] - dis_trajectory[:,0] = trials['dis_y'][trial_id] + dis_trajectory[:,2] = trials['dis_y'][trial_id] + dis_trajectory[:,0] = trials['dis_x'][trial_id] targs.append(ref_trajectory) yield idx, targs, disturbance, dis_trajectory, sample_rate, ramp, ramp_down idx += 1 diff --git a/tests/test_tasks.py b/tests/test_tasks.py index a605d6af..278db798 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -121,12 +121,12 @@ def test_3d(self): class TestSeqGenerators(unittest.TestCase): - # @unittest.skip("") + @unittest.skip("") def test_gen_ascending(self): seq = Conditions.gen_conditions(3, [1, 2], ascend=True) self.assertSequenceEqual(seq[0], [0, 0, 0, 1, 1, 1]) - # @unittest.skip("") + @unittest.skip("") def test_gen_out_2D(self): seq = ScreenTargetCapture.out_2D(nblocks=1, ) seq = list(seq) @@ -144,11 +144,12 @@ def test_gen_out_2D(self): self.assertAlmostEqual(loc[idx == 3, 0][0], 10) self.assertAlmostEqual(loc[idx == 3, 2][0], 0) - # @unittest.skip("") + @unittest.skip("") def test_dual_laser_wave(self): seq = LaserConditions.dual_laser_square_wave(duty_cycle_1=0.025, duty_cycle_2=0.025, phase_delay_2=0.1) print(seq[0]) + @unittest.skip("") def test_swept_laser_pulse(self): seq = SweptLaserConditions.single_laser_pulse() print(seq[0]) @@ -165,28 +166,34 @@ def test_corners(self): print(loc) print("---------------corners") - # @unittest.skip("") + #@unittest.skip("") def test_tracking_2d(self): - seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, - num_primes=10, seed=42, sample_rate=60, dimensions=2, + seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, + num_primes=12, seed=42, sample_rate=60, dimensions=2, disturbance=False, boundaries=(-10,10,-10,10)) trajectories = [t[1][0] for t in seq] - ft_len = np.arange(len(trajectories[0])) - st_len = np.arange(len(trajectories[1])) - print(trajectories[0].shape, trajectories[1].shape) - first_trial_x = np.fft.fft(trajectories[0][:,2]) - second_trial_x = np.fft.fft(trajectories[1][:,2]) - first_trial_y = np.fft.fft(trajectories[0][:,0]) - second_trial_y = np.fft.fft(trajectories[1][:,0]) - fig, axs = plt.subplots(2,2, figsize = (10,8)) - axs[0,0].plot(ft_len, np.abs(first_trial_x)) - axs[0,0].set_title("First Trial X") - axs[0,1].plot(ft_len, np.abs(first_trial_y)) - axs[0,1].set_title("First Trial Y") - axs[1,0].plot(st_len, np.abs(second_trial_x)) - axs[1,0].set_title("Second Trial X") - axs[1,1].plot(st_len, np.abs(second_trial_y)) - axs[1,1].set_title("Second Trial Y") + print("2D Test-------") + print(np.shape(trajectories)) + print("2D Test-------") + fig, axs = plt.subplots(2,1, figsize=(10,8)) + for idx, trial in enumerate(trajectories): + ax = axs[idx] + trialx = np.fft.fft(trial[:,0]) + trial_length = np.shape(trialx)[0] + freq = np.fft.fftfreq(trial_length, d=1./60) + non_neg_freq = freq[freq >= 0] #get positive frequencies + non_neg_x = trialx[freq >= 0] / complex(trial_length, 0) #normalize + non_neg_x[1:] = 2*non_neg_x[1:] #account for negative frequencies + trialy = np.fft.fft(trial[:,2]) + non_neg_y = trialy[freq >= 0] / complex(trial_length, 0) #normalize + non_neg_y[1:] = 2*non_neg_y[1:] #account for negative frequencies + ax.plot(non_neg_freq, np.abs(non_neg_x), 'o-', label = 'X') + ax.plot(non_neg_freq, np.abs(non_neg_y), 'o-', label = 'Y') + ax.set_title(f'Trial {idx}') + ax.set_xlim(0, 3) + ax.set_xlabel('Frequency (Hz)') + plt.legend() + plt.tight_layout() plt.show() class TestYouTube(unittest.TestCase): From e969887260fc3662434e9d9783f5f18b123aa4b2 Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Mon, 17 Nov 2025 14:59:36 -0800 Subject: [PATCH 12/18] fix hideleft feature --- built_in_tasks/target_tracking_task.py | 15 ++------------- features/generator_features.py | 9 ++++----- riglib/stereo_opengl/primitives.py | 2 +- tests/test_tasks.py | 6 +++--- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 3a5b92c9..eb579b44 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -456,10 +456,6 @@ def __init__(self, *args, **kwargs): for model in self.bar.graphics_models: self.add_model(model) - # This is a black cube that optionally hides the "lookbehind" of trajectory (off by default) - self.box = VirtualRectangularTarget(target_width=20, target_height=10, target_color=(0, 0, 0, 1), starting_pos=[-10,-1,0]) - # target_width of RectangularTarget is total height, target_height is 1/2 of total width (from center to edge) - # Declare any plant attributes which must be saved to the HDF file at the _cycle rate for attr in self.plant.hdf_attrs: self.add_dtype(*attr) @@ -533,17 +529,13 @@ def update_plant_visibility(self): def update_frame(self): self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) if self.trajectory_type == 'time': - self.trajectory.move_to_position(np.array([-self.frame_index,0,0])) # same update constant works for 60 and 120 hz + self.trajectory.move_to_position(np.array([-self.frame_index,0,0])) elif self.trajectory_type == 'space': self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) self.frame_index +=1 def setup_start_wait(self): - for model in self.box.graphics_models: - self.add_model(model) - self.box.hide() - # Allow 2d movement if not self.always_1d: self.limit1d = False @@ -565,11 +557,10 @@ def setup_start_wait(self): np.zeros(len(next_trajectory)), next_trajectory ]).T - self.trajectory = VirtualCableTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) + self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) elif self.trajectory_type == 'space': next_trajectory = self.targs[self.lookahead:] self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) - print(target_colors[self.trajectory_color]) self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) else: # 'none' next_trajectory = np.zeros((self.lookahead, 3)) @@ -589,8 +580,6 @@ def setup_screen_reset(self): self.trajectory.reset() self.bar.hide() self.bar.reset() - self.box.hide() - self.box.reset() def setup_start_tracking_in(self): # Revert to settable trait diff --git a/features/generator_features.py b/features/generator_features.py index 811b30dc..2da036b2 100644 --- a/features/generator_features.py +++ b/features/generator_features.py @@ -328,15 +328,14 @@ def _start_reward(self): class HideLeftTrajectory(traits.HasTraits): ''' - Cover left side of tracking task screen with a black box. + Hide the left side of the tracking trajectory. This will cover the 'lookbehind' of the target trajectory. Useful for task with bumpers. ''' - def _start_trajectory(self): - super()._start_trajectory() - if self.frame_index == 0: - self.box.show() + def update_frame(self): + super().update_frame() + self.trajectory.update_mask(self.frame_index+self.lookahead, self.frame_index+2*self.lookahead) class ReadysetMedley(traits.HasTraits): diff --git a/riglib/stereo_opengl/primitives.py b/riglib/stereo_opengl/primitives.py index 8adc522e..2f8d5a5b 100644 --- a/riglib/stereo_opengl/primitives.py +++ b/riglib/stereo_opengl/primitives.py @@ -512,7 +512,7 @@ class Snake(Cable, TexModel): ''' A Cable with a gradient texture applied along its length. ''' - def __init__(self, radius=.5, trajectory=np.array([np.sin(x) for x in range(100)]), segments=12, n_colors=1000, **kwargs): + def __init__(self, radius=.5, trajectory=np.array([np.sin(x) for x in range(100)]), segments=12, n_colors=10000, **kwargs): self.trajectory = trajectory self.n_colors = n_colors color = kwargs.pop('color', [1, 1, 1, 1]) # Default color if not provided diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 54c085bb..33ab8a03 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -75,11 +75,11 @@ def test_example_task(self): exp = init_exp(ExampleSequenceTask, [], seq, window_size=(1200,800), fullscreen=False) exp.run() - @unittest.skip("") + # @unittest.skip("") def test_tracking(self): print("Running tracking task test") seq = TrackingTask.tracking_target_debug(nblocks=1, ntrials=6, time_length=5, seed=40, sample_rate=60, ramp=1) # sample_rate needs to match fps in ScreenTargetTracking - exp = init_exp(TrackingTask, [MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False) + exp = init_exp(TrackingTask, [HideLeftTrajectory, MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False) exp.rotation = 'xzy' # exp.trajectory_type = 'space' exp.trajectory_amplitude = 5 @@ -180,7 +180,7 @@ def test_corners(self): print(loc) print("---------------corners") - #@unittest.skip("") + @unittest.skip("") def test_tracking_2d(self): seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, num_primes=12, seed=42, sample_rate=60, dimensions=2, From 6af1654c2eb2aa7480156b95e6e06b3c7b837213 Mon Sep 17 00:00:00 2001 From: mtringi Date: Fri, 5 Dec 2025 17:01:32 -0800 Subject: [PATCH 13/18] update 2d tracking to include optional exponential amplitudes and use full set of prime frequencies in reference trajectory if disturbance is off. --- built_in_tasks/target_tracking_task.py | 92 ++++++++++---------------- tests/test_tasks.py | 8 +-- 2 files changed, 40 insertions(+), 60 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index eb579b44..6db657e0 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -930,7 +930,7 @@ def generate_trajectories(num_trials=2, time_length=20, seed=40, sample_rate=120 return trials, trial_order @staticmethod - def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate=120, base_period=20, ramp=0, ramp_down=0, num_primes=8): + def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate=120, base_period=20, ramp=0, ramp_down=0, num_primes=8, use_disturb = True, decay_rate = None): ''' Sets up variables and uses prime numbers to call the above functions and generate trajectories in both x & y ramp is time length for preparatory lines @@ -958,12 +958,14 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= f_x = xprimes*w0 # stimulated frequencies f_y = yprimes*w0 - #a_x = 1/(1+np.arange(f_x.size)) # amplitude linear/reciprocal - #a_y = 1/(1+np.arange(f_y.size)) + if decay_rate is None: #use linear decay rate to generate amplitudes if no decay rate is specified + a_x = 1/(1+np.arange(f_x.size)) # amplitude linear/reciprocal + a_y = 1/(1+np.arange(f_y.size)) - k = 0.1 #decay rate - a_x = np.exp(-k*np.arange(f_x.size)) # amplitude exponential decay - a_y = np.exp(-k*np.arange(f_y.size)) + if decay_rate is not None: #if the decay rate is a value then use exponential decay to generate amplitiudes + k = decay_rate + a_x = np.exp(-k*np.arange(f_x.size)) # amplitude exponential decay + a_y = np.exp(-k*np.arange(f_y.size)) # phase offset o_x = np.random.rand(num_trials, f_x.size) @@ -993,73 +995,48 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= trial_order = [(1,'E','O'),(1,'O','E')] elif order == 1: trial_order = [(1,'O','E'),(1,'E','O')] - - # generate pairing of elements - first_set = [] #initialize empty first list - second_set = [] #initialize second list - nele = len(xprimes) # number of elements in xprimes (and yprimes) - #set up logic for final two elements - if nele % 2: #if number of elements divided by 2 has remainder (ie if num of elements is odd) - ele2 = 0 - else: - ele2 = 1 #set up to be able to skip the last two elements - - for ele in range((nele//2) - ele2): #floor division to get half the length of elments - left_side = ele - right_side = nele-1-ele - - if ele % 2 == 0: #remainder division for odds and evens - first_set.extend([left_side, right_side]) - else: - second_set.extend([left_side, right_side]) - - - if nele % 2 == 1: #if odd number of elements in xprime or yprime - mid_ele = nele// 2 #grab element in middle - second_set.append(mid_ele) - else: - mid1 = nele // 2 - 1 - mid2 = nele // 2 - first_set.append(mid1) - second_set.append(mid2) - - # #check with disturbance adn reference sharing primes - # sines_r = np.arange(len(xprimes)) - # sines_d = np.arange(len(xprimes)) # generate reference and disturbance trajectories for all trials for trial_id, (num_reps,ref_ind,dis_ind) in enumerate(trial_order*int(num_trials/2)): if ref_ind == 'E': - #sines_r = np.arange(len(xprimes))[0::2] # use even indices - sines_r = first_set + sines_r = np.arange(len(xprimes))[0::2] # use even indices + elif ref_ind == 'O': - #sines_r = np.arange(len(xprimes))[1::2] # use odd indices - sines_r = second_set + sines_r = np.arange(len(xprimes))[1::2] # use odd indices + else: sines_r = np.arange(len(xprimes)) if dis_ind == 'E': - #sines_d = np.arange(len(xprimes))[0::2] - sines_d = first_set + sines_d = np.arange(len(xprimes))[0::2] + elif dis_ind == 'O': - #sines_d = np.arange(len(xprimes))[1::2] - sines_d = second_set + sines_d = np.arange(len(xprimes))[1::2] + else: sines_d = np.arange(len(xprimes)) #every element in vector + if use_disturb == False: #use both odd and even indices for reference trajectory if distrubance is turned off. # generate X-dimension - refx_traj, ref_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_x[sines_r], a_x[sines_r], o_x[trial_id][sines_r]) - disx_traj, dis_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t,r,rd, f_x[sines_d], a_x[sines_d], o_xdis[trial_id][sines_d]) + refx_traj, ref_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_x[np.arange(len(xprimes))], a_x[np.arange(len(xprimes))], o_x[trial_id][np.arange(len(xprimes))]) + disx_traj, dis_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t,r,rd, f_x[sines_d], a_x[sines_d], o_xdis[trial_id][sines_d]) + # generate Y-dimension + refy_traj, ref_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[np.arange(len(xprimes))], a_y[np.arange(len(xprimes))], o_y[trial_id][np.arange(len(xprimes))]) + disy_traj, dis_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_d], a_y[sines_d], o_ydis[trial_id][sines_d]) + + else: + # generate X-dimension + refx_traj, ref_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_x[sines_r], a_x[sines_r], o_x[trial_id][sines_r]) + disx_traj, dis_Ax = ScreenTargetTracking.calc_sum_of_sines_ramp(t,r,rd, f_x[sines_d], a_x[sines_d], o_xdis[trial_id][sines_d]) # generate Y-dimension - refy_traj, ref_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_r], a_y[sines_r], o_y[trial_id][sines_r]) - disy_traj, dis_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_d], a_y[sines_d], o_ydis[trial_id][sines_d]) + refy_traj, ref_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_r], a_y[sines_r], o_y[trial_id][sines_r]) + disy_traj, dis_Ay = ScreenTargetTracking.calc_sum_of_sines_ramp(t, r, rd, f_y[sines_d], a_y[sines_d], o_ydis[trial_id][sines_d]) + # normalized trajectories trials['ref_x'][trial_id] = refx_traj/ref_Ax # previously, denominator was np.sum(a_ref) trials['dis_x'][trial_id] = disx_traj/dis_Ax # previously, denominator was np.sum(a_dis) trials['ref_y'][trial_id] = refy_traj/ref_Ay # previously, denominator was np.sum(a_ref) trials['dis_y'][trial_id] = disy_traj/dis_Ay # previously, denominator was np.sum(a_dis) - - # print(trial_order, ref_A, dis_A) return trials, trial_order @@ -1097,7 +1074,7 @@ def generate_trajectory(primes, base_period, ramp = 0.0): ### Generator functions #### @staticmethod - def tracking_target_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, num_primes=8, seed=40, sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)): + def tracking_target_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, num_primes=8, seed=40, sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10), decay_rate = None): ''' Generates a sequence of 1D (z axis) target trajectories @@ -1121,6 +1098,8 @@ def tracking_target_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp Whether to add disturbance to the cursor (disturbance is generated regardless) boundaries: 4 element tuple The limits of the allowed target locations (-x, x, -z, z) + decay_rate: None or float + This generates amplitudes using a decay_rate. Used for 2d trajectories. If set to None (default), amplitudes are generated using a linear decay. Returns ------- @@ -1148,12 +1127,13 @@ def tracking_target_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp if dimensions == 2: trials, trial_order = ScreenTargetTracking.generate_2D_trajectories( - num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, num_primes=num_primes - ) + num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, num_primes=num_primes, use_disturb = disturbance, + decay_rate = decay_rate) for trial_id in range(ntrials): targs = [] ref_trajectory = np.zeros((int((time_length+ramp+ramp_down)*sample_rate),3)) dis_trajectory = ref_trajectory.copy() + ref_trajectory[:,2] = trials['ref_y'][trial_id] #y is out of the screen, x is left and right and z is up and down ref_trajectory[:,0] = trials['ref_x'][trial_id] diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 33ab8a03..f219695e 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -75,7 +75,7 @@ def test_example_task(self): exp = init_exp(ExampleSequenceTask, [], seq, window_size=(1200,800), fullscreen=False) exp.run() - # @unittest.skip("") + @unittest.skip("") def test_tracking(self): print("Running tracking task test") seq = TrackingTask.tracking_target_debug(nblocks=1, ntrials=6, time_length=5, seed=40, sample_rate=60, ramp=1) # sample_rate needs to match fps in ScreenTargetTracking @@ -91,7 +91,7 @@ def test_tracking_2d(self): print("Running tracking task test") seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, num_primes=10, seed=42, sample_rate=60, dimensions=2, - disturbance=False, boundaries=(-10,10,-10,10)) + disturbance=True, boundaries=(-10,10,-10,10), decay_rate = 0.1) exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, limit1d=False, trajectory_amplitude=5, lookahead_time=1) exp.stereo_mode = 'projection' @@ -184,8 +184,8 @@ def test_corners(self): def test_tracking_2d(self): seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, num_primes=12, seed=42, sample_rate=60, dimensions=2, - disturbance=False, boundaries=(-10,10,-10,10)) - trajectories = [t[1][0] for t in seq] + disturbance=False, boundaries=(-10,10,-10,10), decay_rate = None) + trajectories = [t[1][0] for t in seq] # pulls out trajectory. Can use t[3] to get disturbance array print("2D Test-------") print(np.shape(trajectories)) print("2D Test-------") From 65159ca860558dae81e90ed22a4545c1e94e42c8 Mon Sep 17 00:00:00 2001 From: Leo Scholl Date: Fri, 5 Dec 2025 17:15:39 -0800 Subject: [PATCH 14/18] fix lookahead --- built_in_tasks/target_graphics.py | 4 +-- built_in_tasks/target_tracking_task.py | 45 +++++++++++++------------- features/generator_features.py | 7 +++- riglib/stereo_opengl/primitives.py | 27 ++++++++-------- tests/test_tasks.py | 9 ++++-- 5 files changed, 49 insertions(+), 43 deletions(-) diff --git a/built_in_tasks/target_graphics.py b/built_in_tasks/target_graphics.py index e9eecb7a..1fca55da 100644 --- a/built_in_tasks/target_graphics.py +++ b/built_in_tasks/target_graphics.py @@ -248,11 +248,11 @@ def _pickle_init(self): self.graphics_models = [self.cable] self.cable.translate(*self.position) - def update_mask(self, start_frame, end_frame): + def update_mask(self, start_frame, end_frame, inverse=False): ''' Update the texture mask of the snake target. ''' - self.cable.update_texture(start_frame, end_frame) + self.cable.update_texture(start_frame, end_frame, inverse=inverse) class VirtualTorusTarget(VirtualCircularTarget): diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 6db657e0..5096d155 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -92,9 +92,6 @@ def _parse_next_trial(self): self.targs = np.squeeze(self.targs,axis=0) self.disturbance_path = np.squeeze(self.disturbance_path) - WIDTH, HEIGHT = self.window_size[0], self.window_size[1] - lookahead = np.zeros((self.lookahead,np.shape(self.targs)[1])) # (30,3) - self.targs = self.trajectory_amplitude*self.targs self.disturbance_path = self.disturbance_amplitude*self.disturbance_path # print(np.amax(self.targs), np.amax(self.disturbance_path)) @@ -104,18 +101,16 @@ def _parse_next_trial(self): self.ramp_counter[:int(self.ramp_up_time*self.sample_rate)] = 1 if self.ramp_down_time > 0: self.ramp_counter[-int(self.ramp_down_time*self.sample_rate):] = 2 - - self.targs = np.concatenate((lookahead, self.targs),axis=0) # (time_length*sample_rate+30,3) # targs and disturbance are no longer same length def tracking_task_start_wait(self): - print(self.gen_index) + # print(self.gen_index) self.trial_record['trial'] = self.calc_trial_num() self.trial_record['index'] = self.gen_index self.trial_record['is_disturbance'] = self.disturbance_trial for i in range(len(self.disturbance_path)): # Update the data sinks with trial information --> bmi3d_trials - self.trial_record['target'] = self.targs[i+self.lookahead] + self.trial_record['target'] = self.targs[i] self.trial_record['disturbance'] = self.disturbance_path[i] self.sinks.send("trials", self.trial_record) @@ -334,15 +329,15 @@ def _test_ramp_complete(self, time_in_state): def _test_traj_complete(self, time_in_state): '''Test whether the trajectory is finished and whether there is a ramp down before the trial ends''' - return (self.frame_index + self.lookahead == self.trajectory_length - self.ramp_down_time*self.sample_rate) and (self.ramp_down_time > 0) + return (self.frame_index == self.trajectory_length - self.ramp_down_time*self.sample_rate) and (self.ramp_down_time > 0) def _test_ramp_and_trial_complete(self, time_in_state): '''Test whether the ramp down is finished, ending the trial''' - return (self.frame_index + self.lookahead == self.trajectory_length) and (self.ramp_down_time > 0) + return (self.frame_index == self.trajectory_length) and (self.ramp_down_time > 0) def _test_trial_complete(self, time_in_state): '''Test whether the trajectory is finished, ending the trial''' - return (self.frame_index + self.lookahead == self.trajectory_length) and (self.ramp_down_time == 0) + return (self.frame_index == self.trajectory_length) and (self.ramp_down_time == 0) def _test_tracking_out_timeout(self, time_in_state): return time_in_state > self.tracking_out_time @@ -432,6 +427,7 @@ def __init__(self, *args, **kwargs): self.plant_vis_prev = True self.cursor_vis_prev = True self.lookahead = int(self.fps * self.lookahead_time) + self.lookahead_scale = (2 * self.screen_cm[0]) / self.lookahead # cm per frame self.original_limit1d = self.limit1d # keep track of original settable trait if not self.always_1d: @@ -496,7 +492,7 @@ def _cycle(self): self.task_data['current_disturbance'] = [np.nan,np.nan,np.nan] self.task_data['current_target_validate'] = self.target.get_position() # default VirtualCircularTarget position is [0,0,0] else: - self.task_data['current_target'] = self.targs[self.frame_index+self.lookahead] + self.task_data['current_target'] = self.targs[self.frame_index] self.task_data['current_disturbance'] = self.disturbance_path[self.frame_index] self.task_data['current_target_validate'] = self.target.get_position() @@ -527,9 +523,9 @@ def update_plant_visibility(self): self.plant.set_visibility(self.plant_visible) def update_frame(self): - self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) + self.target.move_to_position(self.targs[self.frame_index]) if self.trajectory_type == 'time': - self.trajectory.move_to_position(np.array([-self.frame_index,0,0])) + self.trajectory.move_to_position(np.array([-self.frame_index*self.lookahead_scale - self.lookahead*self.lookahead_scale,0,0])) elif self.trajectory_type == 'space': self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) self.frame_index +=1 @@ -550,17 +546,18 @@ def setup_start_wait(self): self.remove_model(model) del self.trajectory if self.trajectory_type == 'time': - next_trajectory = np.array(np.squeeze(self.targs)[:,2]) - next_trajectory[:self.lookahead] = next_trajectory[self.lookahead] + + lookbehind = np.zeros((self.lookahead, np.shape(self.targs)[1])) + next_trajectory = np.concatenate((lookbehind, self.targs), axis=0) + next_trajectory = np.array(np.squeeze(next_trajectory)[:,2]) next_trajectory = np.vstack([ - np.arange(len(next_trajectory))-self.lookahead, # x-axis is in cm, so divide by 3? to get time? idk + self.lookahead_scale * np.arange(len(next_trajectory)), # set the lookahead by scaling the trajectory to fit in the screen np.zeros(len(next_trajectory)), next_trajectory ]).T self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) elif self.trajectory_type == 'space': - next_trajectory = self.targs[self.lookahead:] - self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) + self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=self.targs) self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) else: # 'none' next_trajectory = np.zeros((self.lookahead, 3)) @@ -606,7 +603,7 @@ def setup_while_tracking(self): self.update_frame() # Check if the trial is over and there are no more target frames to display - if self.frame_index+self.lookahead >= np.shape(self.targs)[0]: + if self.frame_index >= np.shape(self.targs)[0]: self.trial_timed_out = True #### TEST FUNCTIONS #### @@ -645,10 +642,10 @@ def _while_wait_retry(self): def _start_trajectory(self): super()._start_trajectory() - if self.frame_index == 0: - #self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # tablet screen x-axis ranges -19,19, center 0 - self.target.move_to_position(self.targs[self.frame_index+self.lookahead]) # tablet screen x-axis ranges -19,19, center 0 - self.trajectory.move_to_position(np.array([0,0,0])) # tablet screen x-axis ranges 0,41.33333, center 22ish + if self.frame_index == 0: # why would this ever not be 0? + self.target.move_to_position(self.targs[self.frame_index]) + if self.trajectory_type == 'time': + self.trajectory.move_to_position(np.array([-self.lookahead*self.lookahead_scale,0,0])) # print(self.target.get_position()) # print(self.trajectory.get_position()) @@ -657,6 +654,8 @@ def _start_trajectory(self): self.trajectory.show() # print('SHOW TRAJ') self.sync_event('TARGET_ON') + else: + print('WARNING: trajectory state started with frame_index != 0', self.frame_index) def _while_trajectory(self): super()._while_trajectory() diff --git a/features/generator_features.py b/features/generator_features.py index 2da036b2..187d3286 100644 --- a/features/generator_features.py +++ b/features/generator_features.py @@ -333,9 +333,14 @@ class HideLeftTrajectory(traits.HasTraits): Useful for task with bumpers. ''' + def setup_start_wait(self): + super().setup_start_wait() + print(self.frame_index) + self.trajectory.update_mask(self.lookahead+2, self.lookahead*2) + def update_frame(self): super().update_frame() - self.trajectory.update_mask(self.frame_index+self.lookahead, self.frame_index+2*self.lookahead) + self.trajectory.update_mask(self.frame_index+self.lookahead+1, self.frame_index+2*self.lookahead) class ReadysetMedley(traits.HasTraits): diff --git a/riglib/stereo_opengl/primitives.py b/riglib/stereo_opengl/primitives.py index 2f8d5a5b..6414425b 100644 --- a/riglib/stereo_opengl/primitives.py +++ b/riglib/stereo_opengl/primitives.py @@ -512,35 +512,34 @@ class Snake(Cable, TexModel): ''' A Cable with a gradient texture applied along its length. ''' - def __init__(self, radius=.5, trajectory=np.array([np.sin(x) for x in range(100)]), segments=12, n_colors=10000, **kwargs): + def __init__(self, radius=.5, trajectory=np.array([np.sin(x) for x in range(100)]), segments=12, **kwargs): self.trajectory = trajectory - self.n_colors = n_colors color = kwargs.pop('color', [1, 1, 1, 1]) # Default color if not provided self.color = color - tex = self.get_texture(0, len(trajectory), n_colors) + tex = self.get_texture(0, len(trajectory)) super().__init__(radius, trajectory, segments, tex=tex, color=[0, 0, 0, 1], **kwargs) self.color = color # Store the color for later use - def get_texture(self, start_frame, end_frame, n_colors): - mask = np.zeros(n_colors) - start_frame_idx = int(float(start_frame) / len(self.trajectory) * n_colors) - end_frame_idx = int(float(end_frame) / len(self.trajectory) * n_colors) - if start_frame_idx >= n_colors: - start_frame_idx = n_colors - if end_frame_idx >= n_colors: - end_frame_idx = n_colors - mask[start_frame_idx:end_frame_idx] = 1 + def get_texture(self, start_frame, end_frame, inverse=False): + mask = np.zeros((len(self.trajectory))) + if start_frame >= len(self.trajectory): + start_frame = len(self.trajectory) + if end_frame >= len(self.trajectory): + end_frame = len(self.trajectory) + mask[start_frame:end_frame] = 1 + if inverse: + mask = 1 - mask mask = np.tile(mask, (4, 1)).T # Repeat for RGBA mask = self.color * mask # Apply color tex = Texture(mask.reshape((1, len(mask), 4))) # Reshape to (1, n_colors, 4) return tex - def update_texture(self, start_frame, end_frame): + def update_texture(self, start_frame, end_frame, inverse=False): ''' Update the texture of the snake based on the new trajectory. ''' self.tex.delete() # Delete the old texture - tex = self.get_texture(start_frame, end_frame, self.n_colors) + tex = self.get_texture(start_frame, end_frame, inverse=inverse) self.tex = tex self.tex.init() diff --git a/tests/test_tasks.py b/tests/test_tasks.py index f219695e..9f60c60b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -78,12 +78,15 @@ def test_example_task(self): @unittest.skip("") def test_tracking(self): print("Running tracking task test") - seq = TrackingTask.tracking_target_debug(nblocks=1, ntrials=6, time_length=5, seed=40, sample_rate=60, ramp=1) # sample_rate needs to match fps in ScreenTargetTracking - exp = init_exp(TrackingTask, [HideLeftTrajectory, MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False) + seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=2, time_length=5, ramp=1, ramp_down=1, + num_primes=8, seed=42, sample_rate=60, + disturbance=False, boundaries=(-10,10,-10,10)) + exp = init_exp(TrackingTask, [HideLeftTrajectory, MouseControl, Window2D], seq, window_size=(1000,800), fullscreen=False, + lookahead_time=1, screen_half_height=10) exp.rotation = 'xzy' # exp.trajectory_type = 'space' exp.trajectory_amplitude = 5 - # exp.lookahead_time = 1 + exp.trajectory_radius = 0.2 exp.run() @unittest.skip("") From 8cfb6f99fdd689dfc3920094f5cfb6b573f0a484 Mon Sep 17 00:00:00 2001 From: katherineperks Date: Tue, 16 Dec 2025 16:49:47 -0800 Subject: [PATCH 15/18] fix frame shift in traj bug, added user_screen to task data, saved test data without ramps --- built_in_tasks/manualcontrolmultitasks.py | 3 + built_in_tasks/target_tracking_task.py | 94 +++++++++++++---------- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/built_in_tasks/manualcontrolmultitasks.py b/built_in_tasks/manualcontrolmultitasks.py index 92ed268e..d2478745 100644 --- a/built_in_tasks/manualcontrolmultitasks.py +++ b/built_in_tasks/manualcontrolmultitasks.py @@ -45,6 +45,7 @@ def __init__(self, *args, **kwargs): def init(self): self.add_dtype('manual_input', 'f8', (3,)) + self.add_dtype('user_screen', 'f8', (3,)) super().init() self.no_data_counter = np.zeros((self._quality_window_size,), dtype='?') @@ -121,6 +122,7 @@ def move_effector(self, pos_offset=[0,0,0], vel_offset=[0,0,0]): self.no_data_counter[self.cycle_count % self._quality_window_size] = 1 self.update_report_stats() self.task_data['manual_input'] = np.ones((3,))*np.nan + self.task_data['user_screen'] = np.ones((3,))*np.nan return self.task_data['manual_input'] = raw_coords.copy() @@ -128,6 +130,7 @@ def move_effector(self, pos_offset=[0,0,0], vel_offset=[0,0,0]): # Transform coordinates coords = self._transform_coords(raw_coords) + self.task_data['user_screen'] = coords try: if self.limit2d: diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index a67f11cb..cabb1f00 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -69,7 +69,7 @@ def init(self): self.trial_dtype = np.dtype([('trial', 'u4'), ('index', 'u4'), ('target', 'f8',(3,)), ('disturbance', 'f8',(3,)), ('is_disturbance', '?')]) super().init() - self.frame_index = 0 # index in the frame in a trial + self.frame_index = -1 # index in the frame in a trial self.total_distance_error = 0 # Euclidian distance between cursor and target during each trial self.trial_timed_out = True # check if the trial is finished self.plant_position = [] @@ -330,19 +330,19 @@ def _test_hold_complete_no_ramp(self, time_in_state): def _test_ramp_complete(self, time_in_state): '''Test whether the ramp up is finished''' - return self.frame_index == self.ramp_up_time*self.sample_rate + return self.frame_index > self.ramp_up_time*self.sample_rate def _test_traj_complete(self, time_in_state): '''Test whether the trajectory is finished and whether there is a ramp down before the trial ends''' - return (self.frame_index + self.lookahead == self.trajectory_length - self.ramp_down_time*self.sample_rate) and (self.ramp_down_time > 0) + return (self.frame_index + self.lookahead > self.trajectory_length - self.ramp_down_time*self.sample_rate) and (self.ramp_down_time > 0) def _test_ramp_and_trial_complete(self, time_in_state): '''Test whether the ramp down is finished, ending the trial''' - return (self.frame_index + self.lookahead == self.trajectory_length) and (self.ramp_down_time > 0) + return (self.frame_index + self.lookahead > self.trajectory_length) and (self.ramp_down_time > 0) def _test_trial_complete(self, time_in_state): '''Test whether the trajectory is finished, ending the trial''' - return (self.frame_index + self.lookahead == self.trajectory_length) and (self.ramp_down_time == 0) + return (self.frame_index + self.lookahead > self.trajectory_length) and (self.ramp_down_time == 0) def _test_tracking_out_timeout(self, time_in_state): return time_in_state > self.tracking_out_time @@ -465,9 +465,9 @@ def init(self): self.add_dtype('trial', 'u4', (1,)) self.add_dtype('gen_idx', 'int', (1,)) # dtype needs to be able to represent -1 self.add_dtype('plant_visible', '?', (1,)) - self.add_dtype('current_target', 'f8', (3,)) - self.add_dtype('current_disturbance', 'f8', (3,)) # see task_data['manual_input'] for cursor position without added disturbance - self.add_dtype('current_target_validate', 'f8', (3,)) + self.add_dtype('target', 'f8', (3,)) + self.add_dtype('disturbance', 'f8', (3,)) + self.add_dtype('intended_cursor', 'f8', (3,)) # cursor position without added disturbance super().init() self.plant.set_endpoint_pos(np.array(self.starting_pos)) @@ -475,6 +475,10 @@ def _cycle(self): ''' Calls any update functions necessary and redraws screen ''' + if self.frame_index >= 0: + print('FRAME ', self.frame_index, self.get_state(), self.trial_timed_out) + print(self.target.get_position()[2], self.pos_offset[2]) + self.move_effector(pos_offset=np.asarray(self.pos_offset), vel_offset=np.asarray(self.vel_offset)) # Run graphics commands to show/hide the plant if the visibility has changed @@ -489,17 +493,10 @@ def _cycle(self): # Update the trial index self.task_data['trial'] = self.calc_trial_num() self.task_data['gen_idx'] = self.gen_index - # print(self.task_data['gen_idx']) - # Save the target position at each cycle. - if self.trial_timed_out: - self.task_data['current_target'] = [np.nan,np.nan,np.nan] - self.task_data['current_disturbance'] = [np.nan,np.nan,np.nan] - self.task_data['current_target_validate'] = self.target.get_position() # default VirtualCircularTarget position is [0,0,0] - else: - self.task_data['current_target'] = self.targs[self.frame_index+self.lookahead] - self.task_data['current_disturbance'] = self.disturbance_path[self.frame_index] - self.task_data['current_target_validate'] = self.target.get_position() + # Save the target position and disturbance value at each cycle + self.task_data['target'] = self.target.get_position() + self.task_data['disturbance'] = self.pos_offset super()._cycle() @@ -528,11 +525,16 @@ def update_plant_visibility(self): self.plant.set_visibility(self.plant_visible) def update_frame(self): - self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # xzy - self.trajectory.move_to_position(np.array([-self.frame_index/3,10,0])) # same update constant works for 60 and 120 hz + if self.trial_timed_out: + use_frame_index = self.frame_index - 1 + else: + use_frame_index = self.frame_index + + self.target.move_to_position(np.array([0,0,self.targs[use_frame_index+self.lookahead][2]])) # xzy + self.trajectory.move_to_position(np.array([-use_frame_index/3,10,0])) # same update constant works for 60 and 120 hz self.target.show() self.trajectory.show() - self.frame_index +=1 + self.frame_index += 1 # increment the frame_index for the following cycle def setup_start_wait(self): if self.calc_trial_num() == 0: @@ -595,14 +597,9 @@ def setup_start_tracking_in(self): # Cue successful tracking self.target.cue_trial_end_success() - def setup_start_tracking_out(self): - # Reset target color - self.target.reset() - - def setup_while_tracking(self): # Add disturbance - cursor_pos = self.plant.get_endpoint_pos() if self.disturbance_trial == True: + cursor_pos = self.plant.get_endpoint_pos() if self.velocity_control: # TODO check manualcontrolmixin for how to implement velocity control self.vel_offset = (cursor_pos + self.disturbance_path[self.frame_index])*1/self.fps @@ -613,9 +610,30 @@ def setup_while_tracking(self): # Move target and trajectory to next frame so it appears to be moving self.update_frame() - # Check if the trial is over and there are no more target frames to display - if self.frame_index+self.lookahead >= np.shape(self.targs)[0]: + def setup_start_tracking_out(self): + # Reset target color + self.target.reset() + + def setup_while_tracking(self): + # Check whether there are no more target frames to display + if self.frame_index + self.lookahead == self.trajectory_length: self.trial_timed_out = True + self.pos_offset = [0,0,0] + self.vel_offset = [0,0,0] + + else: + # Add disturbance + if self.disturbance_trial == True: + cursor_pos = self.plant.get_endpoint_pos() + if self.velocity_control: + # TODO check manualcontrolmixin for how to implement velocity control + self.vel_offset = (cursor_pos + self.disturbance_path[self.frame_index])*1/self.fps + else: + # position control + self.pos_offset = self.disturbance_path[self.frame_index] + + # Move target and trajectory to next frame so it appears to be moving + self.update_frame() #### TEST FUNCTIONS #### def _test_enter_target(self, time_in_state): @@ -656,12 +674,10 @@ def _start_trajectory(self): if self.frame_index == 0: self.target.move_to_position(np.array([0,0,self.targs[self.frame_index+self.lookahead][2]])) # tablet screen x-axis ranges -19,19, center 0 self.trajectory.move_to_position(np.array([0,10,0])) # tablet screen x-axis ranges 0,41.33333, center 22ish - # print(self.target.get_position()) - # print(self.trajectory.get_position()) self.target.show() self.trajectory.show() - # print('SHOW TRAJ') + print('SHOW TRAJ') self.sync_event('TARGET_ON') def _while_trajectory(self): @@ -669,7 +685,7 @@ def _while_trajectory(self): def _start_hold(self): super()._start_hold() - # print('START HOLD') + print('START HOLD') self.sync_event('TRIAL_START') # Cue successful tracking self.target.cue_trial_end_success() @@ -680,8 +696,8 @@ def _while_hold(self): def _start_tracking_in_ramp(self): super()._start_tracking_in_ramp() self.setup_start_tracking_in() - # print('START TRACKING RAMP', self.ramp_counter[self.frame_index]) - self.sync_event('CURSOR_ENTER_TARGET', self.ramp_counter[self.frame_index]) + print('START TRACKING RAMP', self.ramp_counter[self.frame_index-1]) + self.sync_event('CURSOR_ENTER_TARGET', self.ramp_counter[self.frame_index-1]) def _while_tracking_in_ramp(self): super()._while_tracking_in_ramp() @@ -690,7 +706,7 @@ def _while_tracking_in_ramp(self): def _start_tracking_in(self): super()._start_tracking_in() self.setup_start_tracking_in() - # print('START TRACKING') + print('START TRACKING') self.sync_event('CURSOR_ENTER_TARGET') def _while_tracking_in(self): @@ -700,8 +716,8 @@ def _while_tracking_in(self): def _start_tracking_out_ramp(self): super()._start_tracking_out_ramp() self.setup_start_tracking_out() - # print('STOP TRACKING RAMP', self.ramp_counter[self.frame_index]) - self.sync_event('CURSOR_LEAVE_TARGET', self.ramp_counter[self.frame_index]) + print('STOP TRACKING RAMP', self.ramp_counter[self.frame_index-1]) + self.sync_event('CURSOR_LEAVE_TARGET', self.ramp_counter[self.frame_index-1]) def _while_tracking_out_ramp(self): super()._while_tracking_out_ramp() @@ -772,7 +788,7 @@ def _end_tracking_out_penalty(self): def _start_reward(self): super()._start_reward() - # print('REWARD') + print('REWARD') self.sync_event('REWARD') # Cue successful trial From 32aeca273c403326e13a2313d2b64aa24e8f6316 Mon Sep 17 00:00:00 2001 From: katherineperks Date: Fri, 19 Dec 2025 17:17:19 -0800 Subject: [PATCH 16/18] fix transition test funcs, save test data with ramps --- built_in_tasks/target_tracking_task.py | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index cabb1f00..acc0bb08 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -330,11 +330,11 @@ def _test_hold_complete_no_ramp(self, time_in_state): def _test_ramp_complete(self, time_in_state): '''Test whether the ramp up is finished''' - return self.frame_index > self.ramp_up_time*self.sample_rate + return self.frame_index-1 == self.ramp_up_time*self.sample_rate def _test_traj_complete(self, time_in_state): '''Test whether the trajectory is finished and whether there is a ramp down before the trial ends''' - return (self.frame_index + self.lookahead > self.trajectory_length - self.ramp_down_time*self.sample_rate) and (self.ramp_down_time > 0) + return (self.frame_index-1 + self.lookahead == self.trajectory_length - self.ramp_down_time*self.sample_rate) and (self.ramp_down_time > 0) def _test_ramp_and_trial_complete(self, time_in_state): '''Test whether the ramp down is finished, ending the trial''' @@ -467,7 +467,6 @@ def init(self): self.add_dtype('plant_visible', '?', (1,)) self.add_dtype('target', 'f8', (3,)) self.add_dtype('disturbance', 'f8', (3,)) - self.add_dtype('intended_cursor', 'f8', (3,)) # cursor position without added disturbance super().init() self.plant.set_endpoint_pos(np.array(self.starting_pos)) @@ -597,18 +596,19 @@ def setup_start_tracking_in(self): # Cue successful tracking self.target.cue_trial_end_success() - # Add disturbance - if self.disturbance_trial == True: - cursor_pos = self.plant.get_endpoint_pos() - if self.velocity_control: - # TODO check manualcontrolmixin for how to implement velocity control - self.vel_offset = (cursor_pos + self.disturbance_path[self.frame_index])*1/self.fps - else: - # position control - self.pos_offset = self.disturbance_path[self.frame_index] + if self.frame_index == 0: + # Add disturbance + if self.disturbance_trial == True: + cursor_pos = self.plant.get_endpoint_pos() + if self.velocity_control: + # TODO check manualcontrolmixin for how to implement velocity control + self.vel_offset = (cursor_pos + self.disturbance_path[self.frame_index])*1/self.fps + else: + # position control + self.pos_offset = self.disturbance_path[self.frame_index] - # Move target and trajectory to next frame so it appears to be moving - self.update_frame() + # Move target and trajectory to next frame so it appears to be moving + self.update_frame() def setup_start_tracking_out(self): # Reset target color @@ -696,7 +696,7 @@ def _while_hold(self): def _start_tracking_in_ramp(self): super()._start_tracking_in_ramp() self.setup_start_tracking_in() - print('START TRACKING RAMP', self.ramp_counter[self.frame_index-1]) + print('START TRACKING IN RAMP', self.ramp_counter[self.frame_index-1]) self.sync_event('CURSOR_ENTER_TARGET', self.ramp_counter[self.frame_index-1]) def _while_tracking_in_ramp(self): @@ -716,7 +716,7 @@ def _while_tracking_in(self): def _start_tracking_out_ramp(self): super()._start_tracking_out_ramp() self.setup_start_tracking_out() - print('STOP TRACKING RAMP', self.ramp_counter[self.frame_index-1]) + print('START TRACKING OUT RAMP', self.ramp_counter[self.frame_index-1]) self.sync_event('CURSOR_LEAVE_TARGET', self.ramp_counter[self.frame_index-1]) def _while_tracking_out_ramp(self): From b481c7e87912bf504a3f9d29c399285f5b8d9bb6 Mon Sep 17 00:00:00 2001 From: katherineperks Date: Mon, 12 Jan 2026 14:05:40 -0800 Subject: [PATCH 17/18] increment bmi3d version # and comment out print statements --- built_in_tasks/target_tracking_task.py | 18 +++++++++--------- setup.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index acc0bb08..1d5185b2 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -474,9 +474,9 @@ def _cycle(self): ''' Calls any update functions necessary and redraws screen ''' - if self.frame_index >= 0: - print('FRAME ', self.frame_index, self.get_state(), self.trial_timed_out) - print(self.target.get_position()[2], self.pos_offset[2]) + # if self.frame_index >= 0: + # print('FRAME ', self.frame_index, self.get_state(), self.trial_timed_out) + # print(self.target.get_position()[2], self.pos_offset[2]) self.move_effector(pos_offset=np.asarray(self.pos_offset), vel_offset=np.asarray(self.vel_offset)) @@ -677,7 +677,7 @@ def _start_trajectory(self): self.target.show() self.trajectory.show() - print('SHOW TRAJ') + # print('SHOW TRAJ') self.sync_event('TARGET_ON') def _while_trajectory(self): @@ -685,7 +685,7 @@ def _while_trajectory(self): def _start_hold(self): super()._start_hold() - print('START HOLD') + # print('START HOLD') self.sync_event('TRIAL_START') # Cue successful tracking self.target.cue_trial_end_success() @@ -696,7 +696,7 @@ def _while_hold(self): def _start_tracking_in_ramp(self): super()._start_tracking_in_ramp() self.setup_start_tracking_in() - print('START TRACKING IN RAMP', self.ramp_counter[self.frame_index-1]) + # print('START TRACKING IN RAMP', self.ramp_counter[self.frame_index-1]) self.sync_event('CURSOR_ENTER_TARGET', self.ramp_counter[self.frame_index-1]) def _while_tracking_in_ramp(self): @@ -706,7 +706,7 @@ def _while_tracking_in_ramp(self): def _start_tracking_in(self): super()._start_tracking_in() self.setup_start_tracking_in() - print('START TRACKING') + # print('START TRACKING') self.sync_event('CURSOR_ENTER_TARGET') def _while_tracking_in(self): @@ -716,7 +716,7 @@ def _while_tracking_in(self): def _start_tracking_out_ramp(self): super()._start_tracking_out_ramp() self.setup_start_tracking_out() - print('START TRACKING OUT RAMP', self.ramp_counter[self.frame_index-1]) + # print('START TRACKING OUT RAMP', self.ramp_counter[self.frame_index-1]) self.sync_event('CURSOR_LEAVE_TARGET', self.ramp_counter[self.frame_index-1]) def _while_tracking_out_ramp(self): @@ -788,7 +788,7 @@ def _end_tracking_out_penalty(self): def _start_reward(self): super()._start_reward() - print('REWARD') + # print('REWARD') self.sync_event('REWARD') # Cue successful trial diff --git a/setup.py b/setup.py index e1a4154b..10bbec2f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="aolab-bmi3d", - version="1.2.4", + version="1.2.5", author="Lots of people", description="electrophysiology experimental rig library", packages=setuptools.find_packages(), From 64b2acaa65264bb9694ffdf06585210fd3223c05 Mon Sep 17 00:00:00 2001 From: katherineperks Date: Mon, 12 Jan 2026 15:07:08 -0800 Subject: [PATCH 18/18] update trajectory names and scale --- built_in_tasks/target_tracking_task.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 0b12713f..8994f61e 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -400,7 +400,7 @@ class ScreenTargetTracking(TargetTracking, Window): target_radius = traits.Float(2, desc="Radius of targets in cm") #2,0.75 trajectory_radius = traits.Float(.5, desc="Radius of targets in cm") trajectory_color = traits.OptionsList("gold", *target_colors, desc="Color of the trajectory", bmi3d_input_options=list(target_colors.keys())) - trajectory_type = traits.OptionsList("time", ["time", "space", "none"], desc="Type of trajectory to use", bmi3d_input_options=["time", "space", "none"]) + trajectory_type = traits.OptionsList("1d", ["1d", "2d", "none"], desc="Type of trajectory to use", bmi3d_input_options=["1d", "2d", "none"]) lookahead_time = traits.Float(0.5, desc="Time in seconds to display the future trajectory") target_color = traits.OptionsList("yellow", *target_colors, desc="Color of the target", bmi3d_input_options=list(target_colors.keys())) plant_hide_rate = traits.Float(0.0, desc='If the plant is visible, specifies a percentage of trials where it will be hidden') @@ -426,8 +426,8 @@ def __init__(self, *args, **kwargs): self.plant.set_cursor_radius(self.cursor_radius) self.plant_vis_prev = True self.cursor_vis_prev = True - self.lookahead = int(self.fps * self.lookahead_time) - self.lookahead_scale = (2 * self.screen_cm[0]) / self.lookahead # cm per frame + self.lookahead = int(self.fps * self.lookahead_time) # convert to frames + self.lookahead_scale = (0.5 * self.screen_cm[0]) / self.lookahead # cm per frame self.original_limit1d = self.limit1d # keep track of original settable trait if not self.always_1d: @@ -525,9 +525,9 @@ def update_frame(self): use_frame_index = self.frame_index self.target.move_to_position(self.targs[use_frame_index]) - if self.trajectory_type == 'time': + if self.trajectory_type == '1d': self.trajectory.move_to_position(np.array([-use_frame_index*self.lookahead_scale - self.lookahead*self.lookahead_scale,0,0])) - elif self.trajectory_type == 'space': + elif self.trajectory_type == '2d': self.trajectory.update_mask(use_frame_index, use_frame_index+self.lookahead) self.frame_index += 1 # increment the frame_index for the following cycle @@ -546,9 +546,9 @@ def setup_start_wait(self): for model in self.trajectory.graphics_models: self.remove_model(model) del self.trajectory - if self.trajectory_type == 'time': + if self.trajectory_type == '1d': - lookbehind = np.zeros((self.lookahead, np.shape(self.targs)[1])) + lookbehind = self.targs[1,:]*np.ones((self.lookahead, np.shape(self.targs)[1])) next_trajectory = np.concatenate((lookbehind, self.targs), axis=0) next_trajectory = np.array(np.squeeze(next_trajectory)[:,2]) next_trajectory = np.vstack([ @@ -557,7 +557,7 @@ def setup_start_wait(self): next_trajectory ]).T self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) - elif self.trajectory_type == 'space': + elif self.trajectory_type == '2d': self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=self.targs) self.trajectory.update_mask(self.frame_index, self.frame_index+self.lookahead) else: # 'none' @@ -662,7 +662,7 @@ def _start_trajectory(self): super()._start_trajectory() if self.frame_index == 0: self.target.move_to_position(self.targs[self.frame_index]) - if self.trajectory_type == 'time': + if self.trajectory_type == '1d': self.trajectory.move_to_position(np.array([-self.lookahead*self.lookahead_scale,0,0])) self.target.show()