From a08ce85b35319a16eb53fd58ef1a65331aef85ec Mon Sep 17 00:00:00 2001 From: mtringi Date: Thu, 2 Apr 2026 14:25:19 -0700 Subject: [PATCH 1/8] initial commit for single sine generation --- built_in_tasks/target_tracking_task.py | 81 ++++++++++++++++++++++++-- tests/test_tasks.py | 23 ++++++-- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 6f21fc01..238c9feb 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -1069,12 +1069,12 @@ 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): + def generate_trajectory(primes, num_trials = 2, base_period = 20, sample_rate = 60, 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 ''' - hz = 60 # Hz -- sampling rate + hz = sample_rate # Hz -- sampling rate dt = 1/hz # sec -- sampling period T0 = base_period # sec -- base period @@ -1095,10 +1095,15 @@ def generate_trajectory(primes, base_period, ramp = 0.0): N = t.size # = T/dt -- number of samples + trials = dict(id = np.arange(num_trials), times=np.tile(t,(num_trials,1)), ref=np.zeros((num_trials,N)), dis=np.zeros((num_trials,N))) + #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) normalized_trajectory = trajectory/np.sum(a) - return normalized_trajectory + + trials['ref'] = normalized_trajectory + trials['dis'] = normalized_trajectory*0.8 + return trials ### Generator functions #### @staticmethod @@ -1234,4 +1239,72 @@ def tracking_target_training(nblocks=1, ntrials=2, time_length=5, frequencies = trajectory[:,2] = 5*np.concatenate((sum_of_sins_path[0]*np.ones(buffer_space_bef),sum_of_sins_path,sum_of_sins_path[-1]*np.ones(buffer_space_aft))) pts.append(trajectory) yield idx, pts, disturbance, disturbance_path, None - idx += 1 \ No newline at end of file + idx += 1 + + @staticmethod + def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, primes = 2, seed=40, sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)): + ''' + Generates a sequence of 1D (z axis) target trajectories + + Parameters + ---------- + nblocks : int + The number of blocks in the session + ntrials : int + The number of trials in a block + time_length : int + The length of one trial in seconds + seed : int + The seed for the random generator + sample_rate : int + 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 + 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 + ------- + idx : [nblocks*ntrials x 1] array of trial indices + targs : [nblocks*ntrials x 1] array of 3D target coordinates + disturbance : boolean + dis_trajectory : [nblocks*ntrials x 1] array of 3D disturbance coordinates + ''' + idx = 0 + base_period = 20 + for block_id in range(nblocks): + if dimensions == 1: + trials, trial_order = ScreenTargetTracking.generate_trajectory( + num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, primes = 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 + + if dimensions == 2: + trials, trial_order = ScreenTargetTracking.generate_2D_trajectory( + 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) + 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] + + 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 \ No newline at end of file diff --git a/tests/test_tasks.py b/tests/test_tasks.py index d9641c90..c2151a25 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -89,17 +89,30 @@ def test_tracking(self): exp.trajectory_radius = 0.2 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, - num_primes=10, seed=42, sample_rate=60, dimensions=2, - disturbance=True, boundaries=(-10,10,-10,10), decay_rate = 0.1) + seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=20, time_length=20, ramp=1, ramp_down=1, + num_primes=2, seed=42, sample_rate=60, dimensions=2, + disturbance=True, boundaries=(-10,10,-10,10), decay_rate = 0) + exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, tracking_out_time = 10, + limit1d=False, trajectory_amplitude=5, lookahead_time=1) + exp.stereo_mode = 'projection' + exp.rotation = 'xzy' + exp.trajectory_type = '2d' + exp.run() + + @unittest.skip("") + def test_sine_trajectory(self): + print("Running tracking task test") + seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, + primes=2, seed=42, sample_rate=60, dimensions=1, + 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=1) exp.stereo_mode = 'projection' exp.rotation = 'xzy' - exp.trajectory_type = 'space' + exp.trajectory_type = '1d' exp.run() @unittest.skip("") From 54eb388cd5a996951c2cd661adab8bc3be4c4cb0 Mon Sep 17 00:00:00 2001 From: mtringi Date: Fri, 3 Apr 2026 11:19:03 -0700 Subject: [PATCH 2/8] add rotation matrices. Directly use calc_sum_sines in sine_chain generator --- built_in_tasks/rotation_matrices.py | 22 +++++ built_in_tasks/target_tracking_task.py | 131 ++++++++++++------------- tests/test_tasks.py | 23 ++--- 3 files changed, 96 insertions(+), 80 deletions(-) diff --git a/built_in_tasks/rotation_matrices.py b/built_in_tasks/rotation_matrices.py index 0fb31e05..c3c6b6c0 100644 --- a/built_in_tasks/rotation_matrices.py +++ b/built_in_tasks/rotation_matrices.py @@ -34,6 +34,28 @@ exp_rotations = dict( none = np.identity(4), + + mirror_x = np.array( + [[1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]] + ), + + mirror_y = np.array( + [[-1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]] + ), + + mirror_45 = np.array( + [[0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]] + ), + about_x_90 = np.array( [[1, 0, 0, 0], [0, 0, 1, 0], diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 05ffe1f0..e4ddda54 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -1073,12 +1073,12 @@ def generate_2D_trajectories(num_trials=2, time_length=20, seed=40, sample_rate= return trials, trial_order @staticmethod - def generate_trajectory(primes, num_trials = 2, base_period = 20, sample_rate = 60, ramp = 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 ''' - hz = sample_rate # Hz -- sampling rate + hz = 60 # Hz -- sampling rate dt = 1/hz # sec -- sampling period T0 = base_period # sec -- base period @@ -1099,15 +1099,11 @@ def generate_trajectory(primes, num_trials = 2, base_period = 20, sample_rate = N = t.size # = T/dt -- number of samples - trials = dict(id = np.arange(num_trials), times=np.tile(t,(num_trials,1)), ref=np.zeros((num_trials,N)), dis=np.zeros((num_trials,N))) - #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) normalized_trajectory = trajectory/np.sum(a) - - trials['ref'] = normalized_trajectory - trials['dis'] = normalized_trajectory*0.8 - return trials + return normalized_trajectory + ### Generator functions #### @staticmethod @@ -1246,69 +1242,66 @@ def tracking_target_training(nblocks=1, ntrials=2, time_length=5, frequencies = idx += 1 @staticmethod - def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, primes = 2, seed=40, sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)): + def single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, ramp=1.5, ramp_down=1.5, + ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, + sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)): ''' - Generates a sequence of 1D (z axis) target trajectories - - Parameters - ---------- - nblocks : int - The number of blocks in the session - ntrials : int - The number of trials in a block - time_length : int - The length of one trial in seconds - seed : int - The seed for the random generator - sample_rate : int - 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 - 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 - ------- - idx : [nblocks*ntrials x 1] array of trial indices - targs : [nblocks*ntrials x 1] array of 3D target coordinates - disturbance : boolean - dis_trajectory : [nblocks*ntrials x 1] array of 3D disturbance coordinates ''' + + base_period = base_period + dt = 1/sample_rate + t = np.arange(0, time_length+ramp+ramp_down, dt) idx = 0 - base_period = 20 - for block_id in range(nblocks): - if dimensions == 1: - trials, trial_order = ScreenTargetTracking.generate_trajectory( - num_trials=ntrials, time_length=time_length, seed=seed, sample_rate=sample_rate, base_period=base_period, ramp=ramp, ramp_down=ramp_down, primes = 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 + N = t.size - if dimensions == 2: - trials, trial_order = ScreenTargetTracking.generate_2D_trajectory( - 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) - 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] + #generate phase shifts for reference and disturbance trajectories + np.random.seed(seed) - 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 \ No newline at end of file + if dimensions == 2: + phase_shifts = 2*np.pi*np.random.rand(ntrials, 2) + phase_dis = phase_shifts*0.8 + else: + phase_shifts = 2*np.pi*np.random.rand(ntrials) + phase_dis = phase_shifts*0.8 + + trials = dict( + id=np.arange(ntrials), times=np.tile(t,(ntrials,1)), ref_x=np.zeros((ntrials,N)), dis_x=np.zeros((ntrials,N)),ref_y = np.zeros((ntrials,N)), dis_y = np.zeros((ntrials,N))) + + for block_id in range(nblocks): + + 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() + + + if dimensions == 1: + ref_phase = phase_shifts[trial_id] + dis_phase = phase_dis[trial_id] + + ref_traj, A_ref = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_y_freq]), np.array([ref_amp]), np.array([ref_phase])) + dis_traj, A_dis = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_y_freq]), np.array([dis_amp]), np.array([dis_phase])) + + ref_trajectory[:,2] = ref_traj/A_ref + dis_trajectory[:,2] = dis_traj/A_dis + + elif dimensions == 2: + + ref_x_phase = phase_shifts[trial_id][0] + ref_y_phase = phase_shifts[trial_id][1] + dis_x_phase = phase_dis[trial_id][0] + dis_y_phase = phase_dis[trial_id][1] + + ref_x_traj, A_ref_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_x_freq]), np.array([ref_amp]), np.array([ref_x_phase])) + ref_y_traj, A_ref_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_y_freq]), np.array([ref_amp]), np.array([ref_y_phase])) + dis_x_traj, A_dis_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_x_freq]), np.array([dis_amp]), np.array([dis_x_phase])) + dis_y_traj, A_dis_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_y_freq]), np.array([dis_amp]), np.array([dis_y_phase])) + + ref_trajectory[:,0] = ref_x_traj/A_ref_x + ref_trajectory[:,2] = ref_y_traj/A_ref_y + dis_trajectory[:,0] = dis_x_traj/A_dis_x + dis_trajectory[:,2] = dis_y_traj/A_dis_y + + yield idx, [ref_trajectory], disturbance, dis_trajectory, sample_rate, ramp, ramp_down + idx += 1 + \ No newline at end of file diff --git a/tests/test_tasks.py b/tests/test_tasks.py index f9bed939..58b1afc2 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -79,40 +79,41 @@ def test_example_task(self): def test_tracking(self): print("Running tracking task test") 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, + num_primes=2, 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.exp_rotation = 'mirror_45' exp.trajectory_type = '1d' exp.trajectory_amplitude = 5 exp.trajectory_radius = 0.2 exp.run() - #@unittest.skip("") + @unittest.skip("") def test_tracking_2d(self): print("Running tracking task test") seq = TrackingTask.tracking_target_chain(nblocks=1, ntrials=20, time_length=20, ramp=1, ramp_down=1, - num_primes=2, seed=42, sample_rate=60, dimensions=2, - disturbance=True, boundaries=(-10,10,-10,10), decay_rate = 0) + num_primes=4, seed=42, sample_rate=60, dimensions=2, + disturbance=True, boundaries=(-10,10,-10,10), decay_rate = None) exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, tracking_out_time = 10, limit1d=False, trajectory_amplitude=5, lookahead_time=1) - exp.stereo_mode = 'projection' + #exp.stereo_mode = 'projection' exp.rotation = 'xzy' exp.trajectory_type = '2d' exp.run() - @unittest.skip("") + #@unittest.skip("") def test_sine_trajectory(self): print("Running tracking task test") - seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=1, - primes=2, seed=42, sample_rate=60, dimensions=1, - disturbance=False, boundaries=(-10,10,-10,10)) + seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, ramp=1, ramp_down=0, + ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, + sample_rate=60, dimensions = 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=1) + limit1d=True, trajectory_amplitude=5, lookahead_time=1) exp.stereo_mode = 'projection' exp.rotation = 'xzy' - exp.trajectory_type = '2d' + exp.trajectory_type = '1d' exp.run() @unittest.skip("") From 719ebf1d2169487055a71164bcc5acd8d9a494ff Mon Sep 17 00:00:00 2001 From: mtringi Date: Fri, 3 Apr 2026 11:31:41 -0700 Subject: [PATCH 3/8] fft plot test added --- tests/test_tasks.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 58b1afc2..f7422dbb 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -103,10 +103,10 @@ def test_tracking_2d(self): exp.trajectory_type = '2d' exp.run() - #@unittest.skip("") + @unittest.skip("") def test_sine_trajectory(self): print("Running tracking task test") - seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, ramp=1, ramp_down=0, + seq = TrackingTask.single_sine_chain(nblocks=2, ntrials=2, time_length=20, base_period = 20, ramp=1, ramp_down=0, ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, sample_rate=60, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)) exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, @@ -227,6 +227,37 @@ def test_tracking_2d(self): plt.tight_layout() plt.show() + @unittest.skip("") + def test_single_sine(self): + seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=2, time_length=20, base_period = 20, ramp=1, ramp_down=0, + ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, + sample_rate=60, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)) + trajectories = [t[1][0] for t in seq] # pulls out trajectory. Can use t[3] to get disturbance array + print("Test-------") + print(np.shape(trajectories)) + print("Test-------") + fig, axs = plt.subplots(2,1, figsize=(10,8)) + for idx, trial in enumerate(trajectories): + ax = axs[idx] + trialx = np.fft.fft(trial[60:,0]) #ignore ramp period for FFT + 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[60:,2]) #ignore ramp period for FFT + 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): @unittest.skip("") From 46382ee20fd42980be1f6df6a6894ad36b88ac51 Mon Sep 17 00:00:00 2001 From: mtringi Date: Fri, 3 Apr 2026 13:10:20 -0700 Subject: [PATCH 4/8] fix rotation matrices --- built_in_tasks/rotation_matrices.py | 14 +++++++------- tests/test_tasks.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/built_in_tasks/rotation_matrices.py b/built_in_tasks/rotation_matrices.py index c3c6b6c0..0a4636c4 100644 --- a/built_in_tasks/rotation_matrices.py +++ b/built_in_tasks/rotation_matrices.py @@ -36,23 +36,23 @@ none = np.identity(4), mirror_x = np.array( - [[1, 0, 0, 0], - [0, -1, 0, 0], + [[-1, 0, 0, 0], + [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] ), - mirror_y = np.array( - [[-1, 0, 0, 0], + mirror_z = np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], - [0, 0, 1, 0], + [0, 0, -1, 0], [0, 0, 0, 1]] ), mirror_45 = np.array( - [[0, 1, 0, 0], + [[0, 0, 1, 0], + [0, 1, 0, 0], [1, 0, 0, 0], - [0, 0, 1, 0], [0, 0, 0, 1]] ), diff --git a/tests/test_tasks.py b/tests/test_tasks.py index f7422dbb..d2f27da7 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -113,6 +113,7 @@ def test_sine_trajectory(self): limit1d=True, trajectory_amplitude=5, lookahead_time=1) exp.stereo_mode = 'projection' exp.rotation = 'xzy' + exp.exp_rotation = 'mirror_z' exp.trajectory_type = '1d' exp.run() From 6a212bd21c0ef48ceac9754812014d3080921fda Mon Sep 17 00:00:00 2001 From: mtringi Date: Fri, 3 Apr 2026 13:17:34 -0700 Subject: [PATCH 5/8] add single sine back to generator --- built_in_tasks/target_tracking_task.py | 2 +- tests/test_tasks.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index e4ddda54..26950ded 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -392,7 +392,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', 'single_sine_chain' ] hidden_traits = ['cursor_color', 'trajectory_color', 'cursor_bounds', 'cursor_radius', 'plant_hide_rate', 'starting_pos'] diff --git a/tests/test_tasks.py b/tests/test_tasks.py index d2f27da7..c64bc6ba 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -107,8 +107,8 @@ def test_tracking_2d(self): def test_sine_trajectory(self): print("Running tracking task test") seq = TrackingTask.single_sine_chain(nblocks=2, ntrials=2, time_length=20, base_period = 20, ramp=1, ramp_down=0, - ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, - sample_rate=60, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)) + ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, seed=40, + sample_rate=60, dimensions = 1, disturbance=True) exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, limit1d=True, trajectory_amplitude=5, lookahead_time=1) exp.stereo_mode = 'projection' @@ -231,8 +231,8 @@ def test_tracking_2d(self): @unittest.skip("") def test_single_sine(self): seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=2, time_length=20, base_period = 20, ramp=1, ramp_down=0, - ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, - sample_rate=60, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)) + ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, seed=40, + sample_rate=60, dimensions = 1, disturbance=True) trajectories = [t[1][0] for t in seq] # pulls out trajectory. Can use t[3] to get disturbance array print("Test-------") print(np.shape(trajectories)) From 71d7d62b911934c6c2e37fbce9a8bac6378530d7 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 3 Apr 2026 14:17:51 -0700 Subject: [PATCH 6/8] display lookahead at start of trial, add phase offsets as inputs --- built_in_tasks/target_tracking_task.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 26950ded..789ef143 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -575,7 +575,7 @@ def setup_start_wait(self): self.trajectory = VirtualSnakeTarget(target_radius=self.trajectory_radius, target_color=target_colors[self.trajectory_color], trajectory=next_trajectory) 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) + self.trajectory.update_mask(0, self.lookahead) else: # 'none' next_trajectory = np.zeros((self.lookahead, 3)) self.trajectory = VirtualCircularTarget() @@ -1242,20 +1242,26 @@ def tracking_target_training(nblocks=1, ntrials=2, time_length=5, frequencies = idx += 1 @staticmethod - def single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, ramp=1.5, ramp_down=1.5, - ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, ref_amp = 1, dis_amp = 1, seed=40, - sample_rate=120, dimensions = 1, disturbance=True, boundaries=(-10,10,-10,10)): + def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, + ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, seed=40, + sample_rate=120, dimensions = 1, disturbance=True, ref_x_phase = 0, ref_y_phase = 0): ''' ''' - base_period = base_period dt = 1/sample_rate t = np.arange(0, time_length+ramp+ramp_down, dt) idx = 0 N = t.size + ref_x_phase = np.rad2deg(ref_x_phase) + ref_y_phase = np.rad2deg(ref_y_phase) + #generate phase shifts for reference and disturbance trajectories + np.random.seed(seed) + + ref_amp = 1 + dis_amp = 1 if dimensions == 2: phase_shifts = 2*np.pi*np.random.rand(ntrials, 2) @@ -1264,6 +1270,7 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, phase_shifts = 2*np.pi*np.random.rand(ntrials) phase_dis = phase_shifts*0.8 + trials = dict( id=np.arange(ntrials), times=np.tile(t,(ntrials,1)), ref_x=np.zeros((ntrials,N)), dis_x=np.zeros((ntrials,N)),ref_y = np.zeros((ntrials,N)), dis_y = np.zeros((ntrials,N))) @@ -1276,7 +1283,8 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, if dimensions == 1: - ref_phase = phase_shifts[trial_id] + + ref_phase = ref_y_phase dis_phase = phase_dis[trial_id] ref_traj, A_ref = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_y_freq]), np.array([ref_amp]), np.array([ref_phase])) @@ -1287,8 +1295,6 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, base_period = 20, elif dimensions == 2: - ref_x_phase = phase_shifts[trial_id][0] - ref_y_phase = phase_shifts[trial_id][1] dis_x_phase = phase_dis[trial_id][0] dis_y_phase = phase_dis[trial_id][1] From 253fc35b8512fac4640d8b3b1da424228566627f Mon Sep 17 00:00:00 2001 From: mtringi Date: Wed, 8 Apr 2026 16:05:24 -0700 Subject: [PATCH 7/8] add figure8 and circle generators --- built_in_tasks/target_tracking_task.py | 155 ++++++++++++++++++++++++- tests/test_tasks.py | 39 ++++++- 2 files changed, 185 insertions(+), 9 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 789ef143..1b4e6bd6 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -392,7 +392,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', 'single_sine_chain' + 'tracking_target_chain', 'tracking_target_debug', 'tracking_target_training', 'single_sine_chain', 'circle_chain', 'figure8_chain' ] hidden_traits = ['cursor_color', 'trajectory_color', 'cursor_bounds', 'cursor_radius', 'plant_hide_rate', 'starting_pos'] @@ -1246,6 +1246,44 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_dow ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, seed=40, sample_rate=120, dimensions = 1, disturbance=True, ref_x_phase = 0, ref_y_phase = 0): ''' + Generates a single sine wave for the target trajectory + + Parameters + ---------- + nblocks : int + The number of tracking trials in the sequence. + ntrials : int + The number trials in a block + time_length : int + Lenght of each trial + ramp: float + Ramp up time + ramp_down: float + Ramp down time + ref_y_freq: float + Frequency of the reference trajectory in the y dimension + ref_x_freq: float + Frequency of the reference trajectory in the x dimension (only used if dimensions = 2) + dis_y_freq: float + Frequency of the disturbance trajectory in the y dimension + dis_x_freq: float + Frequency of the disturbance trajectory in the x dimension (only used if dimensions = 2) + seed: int + The seed for the random generator + sample_rate: int + The sample rate of the generated trajectories + dimensions: int + Number of dimensions to generate trajectories for (1 or 2) + disturbance: boolean + Whether to add disturbance to the cursor + ref_x_phase: float + Phase of the reference trajectory in the x dimension + ref_y_phase: float + Phase of the reference trajectory in the y dimension + + Returns + ------- + [nblocks*ntrials x 1] array of tuples containing trial indices and [time_length*60 x 3] target coordinates ''' dt = 1/sample_rate @@ -1253,8 +1291,8 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_dow idx = 0 N = t.size - ref_x_phase = np.rad2deg(ref_x_phase) - ref_y_phase = np.rad2deg(ref_y_phase) + ref_x_phase = np.deg2rad(ref_x_phase) #convert phase to degrees + ref_y_phase = np.deg2rad(ref_y_phase) #convert phase to degrees #generate phase shifts for reference and disturbance trajectories @@ -1310,4 +1348,113 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_dow yield idx, [ref_trajectory], disturbance, dis_trajectory, sample_rate, ramp, ramp_down idx += 1 - \ No newline at end of file + + @staticmethod + def circle_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, + ref_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, seed=40, + sample_rate=120, disturbance=True): + + ''' + Generate circle trajectory for the target trajectory + ''' + + dt = 1/sample_rate + t = np.arange(0, time_length+ramp+ramp_down, dt) + idx = 0 + N = t.size + + ref_x_phase = 0 #convert phase to degrees + ref_y_phase = -np.pi/2 #convert degrees to radians + + #generate phase shifts for reference and disturbance trajectories + + np.random.seed(seed) + + ref_amp = 1 + dis_amp = 1 + + phase_shifts = 2*np.pi*np.random.rand(ntrials, 2) + phase_dis = phase_shifts*0.8 + + + trials = dict( + id=np.arange(ntrials), times=np.tile(t,(ntrials,1)), ref_x=np.zeros((ntrials,N)), dis_x=np.zeros((ntrials,N)),ref_y = np.zeros((ntrials,N)), dis_y = np.zeros((ntrials,N))) + + for block_id in range(nblocks): + + 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() + + dis_x_phase = phase_dis[trial_id][0] + dis_y_phase = phase_dis[trial_id][1] + + ref_x_traj, A_ref_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_freq]), np.array([ref_amp]), np.array([ref_x_phase])) + ref_y_traj, A_ref_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_freq]), np.array([ref_amp]), np.array([ref_y_phase])) + dis_x_traj, A_dis_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_x_freq]), np.array([dis_amp]), np.array([dis_x_phase])) + dis_y_traj, A_dis_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_y_freq]), np.array([dis_amp]), np.array([dis_y_phase])) + + ref_trajectory[:,0] = ref_x_traj/A_ref_x + ref_trajectory[:,2] = ref_y_traj/A_ref_y + dis_trajectory[:,0] = dis_x_traj/A_dis_x + dis_trajectory[:,2] = dis_y_traj/A_dis_y + + print(np.max(ref_x_traj), np.max(ref_y_traj)) + print(np.min(ref_x_traj), np.min(ref_y_traj)) + + yield idx, [ref_trajectory], disturbance, dis_trajectory, sample_rate, ramp, ramp_down + idx += 1 + + @staticmethod + def figure8_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5, + ref_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, seed=40, + sample_rate=120, disturbance=True, ref_phase = 0): + + ''' + Generate figure-8 trajectory for the target trajectory + ''' + + dt = 1/sample_rate + t = np.arange(0, time_length+ramp+ramp_down, dt) + idx = 0 + N = t.size + + + #generate phase shifts for reference and disturbance trajectories + + np.random.seed(seed) + + ref_amp = 1 + dis_amp = 1 + + + phase_shifts = 2*np.pi*np.random.rand(ntrials, 2) + phase_dis = phase_shifts*0.8 + + + trials = dict( + id=np.arange(ntrials), times=np.tile(t,(ntrials,1)), ref_x=np.zeros((ntrials,N)), dis_x=np.zeros((ntrials,N)),ref_y = np.zeros((ntrials,N)), dis_y = np.zeros((ntrials,N))) + + for block_id in range(nblocks): + + 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() + + dis_x_phase = phase_dis[trial_id][0] + dis_y_phase = phase_dis[trial_id][1] + + ref_x_traj, A_ref_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([2*ref_freq]), np.array([ref_amp]), np.array([ref_phase])) + ref_y_traj, A_ref_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([ref_freq]), np.array([ref_amp]), np.array([ref_phase])) + dis_x_traj, A_dis_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_x_freq]), np.array([dis_amp]), np.array([dis_x_phase])) + dis_y_traj, A_dis_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_y_freq]), np.array([dis_amp]), np.array([dis_y_phase])) + + ref_trajectory[:,0] = ref_x_traj/A_ref_x + ref_trajectory[:,2] = ref_y_traj/A_ref_y + dis_trajectory[:,0] = dis_x_traj/A_dis_x + dis_trajectory[:,2] = dis_y_traj/A_dis_y + + yield idx, [ref_trajectory], disturbance, dis_trajectory, sample_rate, ramp, ramp_down + idx += 1 \ No newline at end of file diff --git a/tests/test_tasks.py b/tests/test_tasks.py index c64bc6ba..65acf001 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -106,15 +106,44 @@ def test_tracking_2d(self): @unittest.skip("") def test_sine_trajectory(self): print("Running tracking task test") - seq = TrackingTask.single_sine_chain(nblocks=2, ntrials=2, time_length=20, base_period = 20, ramp=1, ramp_down=0, - ref_y_freq = 0.35, ref_x_freq = 0.5, dis_y_freq = 0.85, dis_x_freq = 0.15, seed=40, - sample_rate=60, dimensions = 1, disturbance=True) + seq = TrackingTask.single_sine_chain(nblocks=1, ntrials=2, time_length=20, ramp=1, ramp_down=0, + ref_y_freq = 0.5, ref_x_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, seed=40, + sample_rate=60, dimensions = 2, disturbance=False, ref_x_phase = 0, ref_y_phase = 270) exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, - limit1d=True, trajectory_amplitude=5, lookahead_time=1) + limit1d=False, trajectory_amplitude=5, lookahead_time=20) + exp.stereo_mode = 'projection' + exp.rotation = 'xzy' + #exp.exp_rotation = 'mirror_z' + exp.trajectory_type = '2d' + exp.run() + + @unittest.skip("") + def test_circle_trajectory(self): + print("Running tracking task test") + seq = TrackingTask.circle_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, + ref_freq = 0.5, dis_y_freq = 0, dis_x_freq = 0, seed=40, + sample_rate=60, disturbance=False) + exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, + limit1d=False, trajectory_amplitude=5, lookahead_time=20) exp.stereo_mode = 'projection' exp.rotation = 'xzy' exp.exp_rotation = 'mirror_z' - exp.trajectory_type = '1d' + exp.trajectory_type = '2d' + exp.run() + + + @unittest.skip("") + def test_figure8_trajectory(self): + print("Running tracking task test") + seq = TrackingTask.figure8_chain(nblocks=1, ntrials=2, time_length=20, ramp=0, ramp_down=0, + ref_freq = 0.5, dis_y_freq = 0.1, dis_x_freq = 0.15, seed=40, + sample_rate=60, disturbance=True, ref_phase = 0) + exp = init_exp(TrackingTask, [Window2D, MouseControl], seq, window_size=(1000,800), fullscreen=False, + limit1d=False, trajectory_amplitude=6, lookahead_time=20) + exp.stereo_mode = 'projection' + exp.rotation = 'xzy' + #exp.exp_rotation = 'mirror_z' + exp.trajectory_type = '2d' exp.run() @unittest.skip("") From 54474b096b2ce6626a3f606be939ba621302792c Mon Sep 17 00:00:00 2001 From: mtringi Date: Wed, 8 Apr 2026 16:42:24 -0700 Subject: [PATCH 8/8] convert phase values from radians to cycles to match math in calc_sum_sines --- built_in_tasks/target_tracking_task.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/built_in_tasks/target_tracking_task.py b/built_in_tasks/target_tracking_task.py index 1b4e6bd6..2a22c3ee 100644 --- a/built_in_tasks/target_tracking_task.py +++ b/built_in_tasks/target_tracking_task.py @@ -1291,8 +1291,8 @@ def single_sine_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_dow idx = 0 N = t.size - ref_x_phase = np.deg2rad(ref_x_phase) #convert phase to degrees - ref_y_phase = np.deg2rad(ref_y_phase) #convert phase to degrees + ref_x_phase = np.deg2rad(ref_x_phase)/ (2*np.pi) #convert phase to degrees and then to cycles for calc_sum_sines + ref_y_phase = np.deg2rad(ref_y_phase) / (2*np.pi) #convert phase to degrees #generate phase shifts for reference and disturbance trajectories @@ -1363,8 +1363,8 @@ def circle_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5 idx = 0 N = t.size - ref_x_phase = 0 #convert phase to degrees - ref_y_phase = -np.pi/2 #convert degrees to radians + ref_x_phase = 0 + ref_y_phase = 0.25 # a quarter of a cycle out of phase (calc_sum_sines uses cylces not radians) #generate phase shifts for reference and disturbance trajectories @@ -1395,14 +1395,13 @@ def circle_chain(nblocks=1, ntrials=500, time_length=20, ramp=1.5, ramp_down=1.5 dis_x_traj, A_dis_x = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_x_freq]), np.array([dis_amp]), np.array([dis_x_phase])) dis_y_traj, A_dis_y = ScreenTargetTracking.calc_sum_of_sines_ramp(t, ramp, ramp_down, np.array([dis_y_freq]), np.array([dis_amp]), np.array([dis_y_phase])) + #max_A = np.max(A_ref_x, A_ref_y) ref_trajectory[:,0] = ref_x_traj/A_ref_x ref_trajectory[:,2] = ref_y_traj/A_ref_y dis_trajectory[:,0] = dis_x_traj/A_dis_x dis_trajectory[:,2] = dis_y_traj/A_dis_y - print(np.max(ref_x_traj), np.max(ref_y_traj)) - print(np.min(ref_x_traj), np.min(ref_y_traj)) - + print(A_ref_x, A_ref_y) yield idx, [ref_trajectory], disturbance, dis_trajectory, sample_rate, ramp, ramp_down idx += 1