Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
cbf5c8f
play three separate tones for readyset go task and add color change
mtringi Oct 30, 2025
427ca2d
add toneplayer class to riglib audio. edit readyset task so that tone…
mtringi Oct 31, 2025
c41e4df
try to send a nonblocking POST request
katherineperks Nov 4, 2025
cbe67c4
change printed url
katherineperks Nov 4, 2025
a0d4681
delete hard-coded freeze time at reward onset
leoscholl Nov 4, 2025
0e6cb54
set penalty index
resatomo Nov 4, 2025
51ff234
fix bug in how arg is passed
leoscholl Nov 4, 2025
0b1da7e
system triggers but only works for 1x reward
leoscholl Nov 4, 2025
fec2678
system triggers but only works for 1x reward
leoscholl Nov 4, 2025
b8798df
system triggers for any number of rewards
katherineperks Nov 5, 2025
fc52564
update comments/print statements
katherineperks Nov 5, 2025
714c20c
update web server messages
katherineperks Nov 5, 2025
08270d7
set reward time based on # pellets per reward, and move hard coded sl…
katherineperks Nov 6, 2025
79a116a
Added sound for fixation penalty
resatomo Nov 6, 2025
89f50a1
Fixed
resatomo Nov 6, 2025
e934b1f
Leo's comments from pull request. Update the readyset task to be driv…
mtringi Nov 8, 2025
3ef93ab
modify _cycle state for better integration with the colorchange feature
mtringi Nov 8, 2025
7f6318d
update comments to describe color change logic
mtringi Nov 12, 2025
87225f2
Add parameter assertation errors
mtringi Nov 12, 2025
7245a80
fix target off logic.
mtringi Nov 12, 2025
8cb0b5c
add check to make sure that the tone cues are not within one frame of…
mtringi Nov 12, 2025
e7dfb68
move tooslow audio penalty to PenaltyAudioMulti & PenaltyAudio features
mtringi Nov 12, 2025
808491d
move some lines to fix reward time
katherineperks Nov 17, 2025
ef9188e
online_eye_plot and fix reward bug
resatomo Dec 4, 2025
10987c4
Added a new file for eye related task
resatomo Dec 4, 2025
875a9d3
Modified
resatomo Dec 4, 2025
aefe833
removed old tasks
resatomo Dec 4, 2025
ea4da30
make changes to graphics
katherineperks Jan 9, 2026
11448fd
undo changes to graphics
katherineperks Jan 9, 2026
1c3f742
started new two chocie center out task
katherineperks Jan 12, 2026
c73ce64
outline new task
katherineperks Jan 12, 2026
74a035b
added task structure and variables
ajm2605 Jan 12, 2026
791fb78
some smalle updates
ajm2605 Jan 13, 2026
0b84099
updated the two choice task
ajm2605 Jan 13, 2026
4863bad
most recent updates to two choice
katherineperks Jan 14, 2026
040d922
small updates
katherineperks Jan 14, 2026
0282fa3
errors in the duaal peripheral generator
katherineperks Jan 14, 2026
a2af762
added initial colors, incorrect behavior on selection of second perip…
katherineperks Jan 15, 2026
52359a9
Can now enter the second target without issue and get rewarded for bo…
katherineperks Jan 15, 2026
c4520b0
Fixed thetarget behavior, now working on the correct sequence generator
katherineperks Jan 15, 2026
3e25f1a
Fixed thetarget behavior, now working on the correct sequence generator
katherineperks Jan 15, 2026
fd0023e
more updaes generator
katherineperks Jan 15, 2026
c485bde
need to fix the sync events
katherineperks Jan 15, 2026
e5ef409
added mirror_x parameter
katherineperks Jan 22, 2026
194090b
added 180 generator for alt task
katherineperks Jan 22, 2026
2aa4516
system triggers but only works for 1x reward
leoscholl Nov 4, 2025
0d27d89
started new two chocie center out task
katherineperks Jan 12, 2026
247353a
outline new task
katherineperks Jan 12, 2026
3c09ccf
added task structure and variables
ajm2605 Jan 12, 2026
02aeb9f
some smalle updates
ajm2605 Jan 13, 2026
d219844
updated the two choice task
ajm2605 Jan 13, 2026
840bcb1
most recent updates to two choice
katherineperks Jan 14, 2026
94a4439
small updates
katherineperks Jan 14, 2026
a7dd5b9
errors in the duaal peripheral generator
katherineperks Jan 14, 2026
1c2f721
added initial colors, incorrect behavior on selection of second perip…
katherineperks Jan 15, 2026
23f7f3f
Can now enter the second target without issue and get rewarded for bo…
katherineperks Jan 15, 2026
8f3e73f
Fixed thetarget behavior, now working on the correct sequence generator
katherineperks Jan 15, 2026
38fb7cd
Fixed thetarget behavior, now working on the correct sequence generator
katherineperks Jan 15, 2026
dcb9885
more updaes generator
katherineperks Jan 15, 2026
35e999c
need to fix the sync events
katherineperks Jan 15, 2026
5a556b2
added mirror_x parameter
katherineperks Jan 22, 2026
ba33feb
added 180 generator for alt task
katherineperks Jan 22, 2026
733c83d
wellp
katherineperks Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions analysis/online_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,28 @@ def cleanup(self):

class SaccadeAnalysisWorker(BehaviorAnalysisWorker):
'''
<<<<<<< HEAD
Plots calibrated_eye, cursor, and target data from experiments that have them.
This is for eye-related task that requires calibrated eye position
'''

def init(self):
super().init()
self.calibrated_eye_pos = np.zeros(2)
=======
Plots eye, cursor, and target data from experiments that have them. Performs automatic
calibration of eye data to target locations when the cursor enters the target if no
calibration coefficients are available.
'''

def __init__(self, task_params, data_queue, calibration_dir='/var/tmp', buffer_time=1, ylim=1, px_per_cm=51.67, **kwargs):
super().__init__(task_params, data_queue, **kwargs)

def init(self):
super().init()
self.calibrated_eye_pos = np.zeros(2)
self.calibration_flag = False
>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f

def get_current_pos(self):
'''
Expand All @@ -319,7 +334,10 @@ def get_current_pos(self):
targets = [(self.target_pos[k], radius, color if v == 1 else 'green') for k, v in self.targets.items() if v]
except:
targets = []
<<<<<<< HEAD

=======
>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f
return self.cursor_pos, self.calibrated_eye_pos, targets

def draw(self):
Expand All @@ -329,7 +347,11 @@ def draw(self):
buffer = self.task_params['fixation_radius_buffer']
elif 'fixation_dist' in self.task_params:
buffer = self.task_params['fixation_dist'] - self.task_params['target_radius']
<<<<<<< HEAD
eye_radius = 0.1
=======
eye_radius = 0.2
>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f

patches1 = [plt.Circle(pos, radius+buffer) for pos, radius, _ in targets]
patches2 = [plt.Circle(cursor_pos, cursor_radius), plt.Circle(calibrated_eye_pos, eye_radius)]
Expand All @@ -348,6 +370,7 @@ def draw(self):
self.diam_plot.set_data(np.arange(len(self.eye_diam)) * 1/(int(self.task_params['fps'])) - self.buffer_time,
self.eye_diam[:, 2]/self.px_per_cm)

<<<<<<< HEAD
class EyeHandAnalysisWorker(BehaviorAnalysisWorker):
'''
Plots calibrated_eye, cursor, and target data from experiments that have them.
Expand Down Expand Up @@ -577,6 +600,8 @@ def draw(self):
self.eye_diam[:, 2]/self.px_per_cm)


=======
>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f
class ERPAnalysisWorker(AnalysisWorker):
'''
Plots ERP data from experiments with an ECoG244 array. Automatically calculates
Expand Down Expand Up @@ -870,6 +895,7 @@ def init(self):

elif self.task_params['experiment_name'] == 'SaccadeTask':
self.analysis_workers.append((SaccadeAnalysisWorker(self.task_params, data_queue), data_queue))
<<<<<<< HEAD

elif self.task_params['experiment_name'] == 'HandConstrainedSaccadeTask':
self.analysis_workers.append((EyeHandAnalysisWorker(self.task_params, data_queue), data_queue))
Expand All @@ -882,6 +908,8 @@ def init(self):

elif self.task_params['experiment_name'] == 'EyeHandSequenceTask':
self.analysis_workers.append((EyeHandSequenceAnalysisWorker(self.task_params, data_queue), data_queue))
=======
>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f

# Is there ecube neural data?
if 'record_headstage' in self.task_params and self.task_params['record_headstage']:
Expand Down
12 changes: 11 additions & 1 deletion built_in_tasks/manualcontrolmultitasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
from .target_graphics import *
from .target_capture_task import ScreenTargetCapture
from .target_capture_task_xt import ScreenReachAngle, ScreenReachLine, SequenceCapture, ScreenTargetCapture_ReadySet
<<<<<<< HEAD
from .target_capture_task_eye import EyeConstrainedTargetCapture, HandConstrainedEyeCapture, EyeConstrainedHandCapture, \
EyeHandSequenceCapture, ScreenTargetCapture_Saccade, EyeHandCaptureBlock
=======
from .target_capture_task_eye import EyeConstrainedTargetCapture, HandConstrainedEyeCapture, ScreenTargetCapture_Saccade
>>>>>>> 194090bde25517e7fb916c964facffc71de3e99f
from .target_tracking_task import ScreenTargetTracking
from .rotation_matrices import *
from .target_capture_multiple_choice import TwoChoiceTargetCapture

class ManualControlMixin(traits.HasTraits):
'''Target capture task where the subject operates a joystick
Expand Down Expand Up @@ -248,4 +253,9 @@ class ReadySetGoTask(ManualControlMixin, ScreenTargetCapture_ReadySet):
Center out task with ready set go auditory cues. Cues separated by 500 ms and participant is expected to move on final go cue. Additionally, participant must move out
of center circle (mustmv_time) parameter or there will be an error.
'''
pass
pass

class TwoChoiceManualControl(ManualControlMixin, TwoChoiceTargetCapture):
'''
Center out task with two peripheral targets
'''
6 changes: 6 additions & 0 deletions built_in_tasks/rotation_matrices.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@

exp_rotations = dict(
none = np.identity(4),
mirror_x = np.array(
[[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]
),
about_x_90 = np.array(
[[1, 0, 0, 0],
[0, 0, 1, 0],
Expand Down
256 changes: 256 additions & 0 deletions built_in_tasks/target_capture_multiple_choice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import numpy as np
import random
import os

from .target_graphics import *
from .target_capture_task import ScreenTargetCapture
from riglib.experiment import traits

class TwoChoiceTargetCapture(ScreenTargetCapture):
'''
#Add a penalty state when subjects looks away.
'''

periph_targ1_color = traits.OptionsList("red", *target_colors, desc="Color of peripheral target 1", bmi3d_input_options=list(target_colors.keys()))
periph_targ2_color = traits.OptionsList("blue", *target_colors, desc="Color of peripheral target 1", bmi3d_input_options=list(target_colors.keys()))
#hide(
#reward_time = traits.Float(.5, desc="Length of reward dispensation")
reward_multiplier = traits.Float(2.0, desc="Select the reward differential between high and low reward")
#pulses_per_total_reward = traits.Int(5, desc='the nubmer of iterations for reward pulse')

status = dict(
wait = dict(start_trial="target"),
target = dict(timeout="timeout_penalty",
enter_target="hold"),
hold = dict(leave_target="hold_penalty", hold_complete="delay"),
delay = dict(leave_target="delay_penalty",
delay_complete="targ_transition"),
targ_transition = dict(trial_complete="reward",
trial_abort="wait",
trial_incomplete="target"),
timeout_penalty = dict(timeout_penalty_end="wait",
end_state=True),
hold_penalty = dict(hold_penalty_end="wait",
end_state=True),
delay_penalty = dict(delay_penalty_end="wait",
end_state=True),
reward = dict(reward_end = "wait",
stoppable=False,
end_state=True),
)

sequence_generators = ['dual_centerout_2D_mirror', 'dual_centerout_2D_180', 'dual_peripheral']

def __init__(self, *args, **kwargs):
kwargs['instantiate_targets'] = False
super().__init__(*args, **kwargs)

# Create three targets: center + two peripherals
target_center = VirtualCircularTarget(target_radius=self.target_radius,
target_color=target_colors[self.target_color])
target_periph1 = VirtualCircularTarget(target_radius=self.target_radius,
target_color=target_colors["red"])
target_periph2 = VirtualCircularTarget(target_radius=self.target_radius,
target_color=target_colors["blue"])

self.targets = [target_center, target_periph1, target_periph2]
self.chosen_target = None
self.base_reward_time = self.reward_time

if hasattr(self, "pulses_per_total_reward"):
self.base_pulses_per_total_reward = self.pulses_per_total_reward
else:
self.base_pulses_per_total_reward = 0


def _start_target(self):
super()._start_target()

if self.target_index == 0:
# Show only the center target initially
self.targets[0].move_to_position(self.targs[0])
self.targets[0].show()
self.sync_event('TARGET_ON', 0)


def _start_hold(self):
super()._start_hold()

if self.target_index == 0:
# Just entered center target
self.sync_event('CURSOR_ENTER_TARGET', 0)
else:
# Entered one of the peripheral targets - record which one
cursor_pos = self.plant.get_endpoint_pos()
d1 = np.linalg.norm(cursor_pos - self.targs[1])
d2 = np.linalg.norm(cursor_pos - self.targs[2])

if d1 < d2:
self.chosen_target = 1
self.reward_time = self.base_reward_time * self.reward_multiplier
self.pulses_per_total_reward = int(np.ceil(self.reward_multiplier)*self.base_pulses_per_total_reward)

self.targets[2].hide() # Hide unchosen target
else:
self.chosen_target = 2
self.reward_time = self.base_reward_time
self.targets[1].hide() # Hide unchosen target
self.pulses_per_total_reward = int(self.base_pulses_per_total_reward)


self.sync_event('CURSOR_ENTER_TARGET', self.chosen_target)

def _start_delay(self):
#super()._start_delay()
# After holding center, show BOTH peripheral targets
if self.target_index == 0: # Just finished holding center
self.targets[1].move_to_position(self.targs[1])
self.targets[1].show()

self.targets[2].move_to_position(self.targs[2])
self.targets[2].show()

self.sync_event('TARGET_ON', 1)#Convert this index to position index
#self.sync_event('TARGET_ON', 2)

def _start_targ_transition(self):
#super()._start_targ_transition()
if self.target_index == -1:

# Came from a penalty state
pass
elif self.target_index == 0:
self.targets[0].hide()
self.sync_event('TARGET_OFF', self.gen_indices[self.target_index])

def _start_reward(self):
super()._start_reward()
self.targets[self.chosen_target].cue_trial_end_success()
self.sync_event('REWARD')

@staticmethod
def dual_peripheral(nblocks=100, distance=10, origin=(0,0,0)):
'''
#Generates center target + two peripheral targets
'''
rng = np.random.default_rng()
for _ in range(nblocks):
# Generate two random angles for peripheral targets
angles = rng.uniform(0, 2*np.pi, size=2)

# Target 0: center
center = np.array(origin)

# Target 1: first peripheral
pos1 = np.array([
distance*np.cos(angles[0]),
0,
distance*np.sin(angles[0])
]) + origin

# Target 2: second peripheral
pos2 = np.array([
distance*np.cos(angles[1]),
0,
distance*np.sin(angles[1])
]) + origin
targs = np.array([center, pos1, pos2])
# Yield indices and positions for all three targets
yield [0, 1, 2], targs

@staticmethod
def dual_centerout_2D_mirror(nblocks=100, ntargets=8, distance=10, origin=(0,0,0)):
'''
triplets of central targets at the origin and 2 peripheral targets centered around the origin

Returns
-------
[nblocks*ntargets x 1] array of tuples containing trial indices and [2 x 3] target coordinates
'''
gen = ScreenTargetCapture.out_2D(nblocks, ntargets, distance, origin)
for _ in range(nblocks*ntargets):

idx, pos = next(gen)
while abs(pos[0][0]) < 0.1:
idx, pos = next(gen)

targs = np.zeros([3, 3]) + origin
targs[1,:] = pos[0]
targs[2,:] = pos[0]*[-1,1,1] #flip the position
indices = np.zeros([3,1])
indices[1] = idx[0]
indices[2] = 10 - idx[0]
yield indices, targs

@staticmethod
def dual_centerout_2D_180(nblocks=100, ntargets=8, distance=10, origin=(0,0,0)):
'''
triplets of central targets at the origin and 2 peripheral targets centered around the origin

Returns
-------
[nblocks*ntargets x 1] array of tuples containing trial indices and [2 x 3] target coordinates
'''
gen = ScreenTargetCapture.out_2D(nblocks, ntargets, distance, origin)
for _ in range(nblocks*ntargets):

idx, pos = next(gen)
#while abs(pos[0][0]) < 0.1:
# idx, pos = next(gen)

targs = np.zeros([3, 3]) + origin
targs[1,:] = pos[0]
targs[2,:] = pos[0]*[-1,1,-1] #flip the position
indices = np.zeros([3,1])
indices[1] = idx[0]
indices[2] = (idx[0] + 4) % 8
yield indices, targs

def _test_enter_target(self, ts):
'''
#Check if cursor is in the appropriate target(s)
'''
cursor_pos = self.plant.get_endpoint_pos()

if self.target_index == 0:
# First target: check only center
d = np.linalg.norm(cursor_pos - self.targs[0])
return d <= (self.target_radius - self.cursor_radius)

else:
# After center hold: check if in EITHER peripheral target
d1 = np.linalg.norm(cursor_pos - self.targs[1])
in_target1 = d1 <= (self.target_radius - self.cursor_radius)

d2 = np.linalg.norm(cursor_pos - self.targs[2])
in_target2 = d2 <= (self.target_radius - self.cursor_radius)

return in_target1 or in_target2


def _test_hold_complete(self, time_in_state):
'''
#Hold complete after holding center OR after holding chosen peripheral
'''
return time_in_state > self.hold_time

def _test_trial_complete(self, time_in_state):
'''
#Trial complete after acquiring either peripheral target (index 1)
'''
return self.target_index > 0

def _test_leave_target(self, ts):
cursor_pos = self.plant.get_endpoint_pos()

if self.target_index == 0:
d = np.linalg.norm(cursor_pos - self.targs[0])
elif self.chosen_target is not None: # Add safety check
d = np.linalg.norm(cursor_pos - self.targs[self.chosen_target])
else:
return False # No target chosen yet, can't have left it

rad = self.target_radius - self.cursor_radius
return d > rad #or super()._test_leave_target(ts)

#def
Loading