Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
fb2a5a8
outlines fast retract for stage
micahwoodard May 15, 2025
405b033
initializes timers
micahwoodard May 15, 2025
fa1bea9
fixes qtimer
micahwoodard May 16, 2025
ffc2f8b
debugs
micahwoodard May 16, 2025
ec255a3
connects signals'
micahwoodard May 16, 2025
13df210
debugs
micahwoodard May 16, 2025
6e08783
removes double signal
micahwoodard May 16, 2025
9899e0a
fixes timer bug
micahwoodard May 16, 2025
00f501c
debugs
micahwoodard May 16, 2025
0ec73ad
fixes stage widget
micahwoodard May 16, 2025
6e15161
degugs
micahwoodard May 19, 2025
b1ea235
debugs
micahwoodard May 19, 2025
5837066
inits timer with dummy timeout
micahwoodard May 19, 2025
53e0053
debugs
micahwoodard May 19, 2025
70dcf1c
debugs
micahwoodard May 19, 2025
b1385a4
debugs
micahwoodard May 19, 2025
d7a711a
debugs
micahwoodard May 19, 2025
7f47da6
debugs
micahwoodard May 19, 2025
0ebca0f
debugs
micahwoodard May 19, 2025
92ede10
debugs
micahwoodard May 19, 2025
252b818
changes stage widget release
micahwoodard May 21, 2025
4d540a5
disconnects signals
micahwoodard May 22, 2025
316869e
reconnects if retraction off
micahwoodard May 22, 2025
2466011
sets stage to normal speed after unretract
micahwoodard May 22, 2025
3ed7d75
comments out setting stage to normal speed
micahwoodard May 22, 2025
985a4e6
disconnects finished signal
micahwoodard May 22, 2025
ef31ffe
handles disconnects better
micahwoodard May 22, 2025
b2058fb
formats gui better
micahwoodard May 22, 2025
748d449
moves lick emit
micahwoodard May 22, 2025
eb51cd6
debugging
micahwoodard May 23, 2025
946ac29
debugging
micahwoodard May 23, 2025
9bc2f48
puts irregular timestamp in thread
micahwoodard May 23, 2025
43acfd2
removes simulation
micahwoodard May 23, 2025
a813165
create lick bonsai signal
micahwoodard May 27, 2025
5a614bb
catch lick at RigClient
micahwoodard May 28, 2025
f2fbcc8
reverting layout file
micahwoodard May 28, 2025
d31f473
removes extra folder
micahwoodard May 28, 2025
7522660
adds connection upon channel2 initialization
micahwoodard May 30, 2025
d1905ec
updates bonsai workflow
micahwoodard May 30, 2025
7be1775
try except to disconnect signals
micahwoodard May 30, 2025
e38f90c
adds conditional to bonsai workflow
micahwoodard May 30, 2025
e561638
checks if stage is at origin
micahwoodard May 30, 2025
999c12e
adds checks for connecting and disconnect mouse licked signals
micahwoodard May 30, 2025
d21b189
check y pos of lickspout
micahwoodard May 30, 2025
f1f8ac2
moves connection in start function
micahwoodard May 30, 2025
dea892f
adds logic for lickety split lickspout
micahwoodard May 30, 2025
568106c
reverts layout file
micahwoodard May 30, 2025
545ee42
Merge branch 'curriculum-dev-branch' into feat-fast-retract
micahwoodard Jun 10, 2025
75b824d
removes super init
micahwoodard Jun 10, 2025
5124f12
adds more info to log and passes in lickspout retracted
micahwoodard Jun 26, 2025
3f43e1c
debugs
micahwoodard Jun 27, 2025
7e94bec
debugs
micahwoodard Jun 27, 2025
0ba0f02
retracts
micahwoodard Jun 27, 2025
35d2698
simulating licks with keyboard
micahwoodard Jul 10, 2025
5160a07
adds debuging
micahwoodard Jul 10, 2025
8b85682
adds logs
micahwoodard Jul 10, 2025
4cbadf0
does not skip moving stage when stage is busy
micahwoodard Jul 10, 2025
201b981
formats
micahwoodard Jul 10, 2025
20679f3
reverts bonsai and layout file
micahwoodard Jul 10, 2025
f2c3b60
merges with curriculum bug branch
micahwoodard Jul 11, 2025
dc047c6
fixes merge issues
micahwoodard Jul 11, 2025
0127cc5
Revert "formats"
micahwoodard Jul 16, 2025
ac0ba6d
adds back lick subjects in bonsai workflow
micahwoodard Jul 16, 2025
8bfd571
don't enforce unique connection on setting stage speed back to normal
micahwoodard Jul 16, 2025
cfbcf19
changes logic for changing stage speed
micahwoodard Jul 17, 2025
185b608
changes logic to account for monument position
micahwoodard Jul 17, 2025
3c3e4cf
increases delta to 10
micahwoodard Jul 17, 2025
b2631e9
resets stage speed and fast retract once stage has stopped moving
micahwoodard Jul 17, 2025
e7ffa74
corrects wrong function name
micahwoodard Jul 17, 2025
1053780
creates thread to query if stage has made it back to original position
micahwoodard Jul 17, 2025
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies = [
"aind-data-schema==1.1.0",
"aind-data-schema-models==0.5.6",
"pydantic==2.10.6",
"stagewidget==1.0.4.dev11",
"stagewidget==1.0.5.dev0",
"python-logging-loki >=0.3.1, <2",
"pykeepass >=4.0.7, <5",
"pyyaml >=6, <7",
Expand Down
142 changes: 128 additions & 14 deletions src/foraging_gui/Foraging.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,6 @@
SessionParametersWidget,
)
from foraging_gui.settings_model import BonsaiSettingsModel, DFTSettingsModel
from foraging_gui.stage import Stage
from foraging_gui.Visualization import (
PlotLickDistribution,
PlotTimeDistribution,
PlotV,
)
from foraging_gui.warning_widget import WarningWidget
from foraging_gui.settings_model import BonsaiSettingsModel, DFTSettingsModel
from foraging_gui.sound_button import SoundButton
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these were duplicated imports so that's why they were removed

from foraging_gui.stage import Stage
from foraging_gui.Visualization import (
Expand Down Expand Up @@ -357,6 +349,11 @@ def __init__(self, parent=None, box_number=1, start_bonsai_ide=True):

# Stage Widget
self.stage_widget = None
# initialize empty timers
self.left_retract_timer = QTimer(timeout=lambda: None)
self.left_retract_timer.setSingleShot(True)
self.right_retract_timer = QTimer(timeout=lambda: None)
self.right_retract_timer.setSingleShot(True)
try:
self._load_stage()
except IOError as e:
Expand Down Expand Up @@ -624,6 +621,103 @@ def _insert_stage_widget(self, widget_to_replace: str) -> None:
self.stage_widget = get_stage_widget()
layout.addWidget(self.stage_widget)

def retract_lick_spout(self, lick_spout_licked: Literal["Left", "Right"], delta: float = -10) -> None:
"""
Fast retract lick spout based on lick spout licked

:param lick_spout_licked: lick spout that was licked. Opposite lickspout will be retracted
:param delta: change in pos to move lick spout to. Default is -10

"""
# disconnect so it's only triggered once
try:
self.Channel2.mouseLicked.disconnect(self.retract_lick_spout)
except TypeError:
pass

lick_spout_retract = "right" if lick_spout_licked == "Left" else "left"
timer = getattr(self, f"{lick_spout_retract}_retract_timer")
tp = self.task_logic.task_parameters
motor = 1 if lick_spout_licked == "Left" else 2
at_origin = list(self._GetPositions().values())[motor] == 0.0

if tp.lick_spout_retraction and self.stage_widget is not None and not at_origin:
logger.info(f"Retracting {lick_spout_retract} lick spout.")
curr_pos = self.stage_widget.stage_model.get_current_positions_mm(motor, rel_to_monument=True)
self.stage_widget.stage_model.quick_move(motor=motor, distance=delta, skip_if_busy=False)
logger.info(f"Retracting {lick_spout_retract} lick spout to pos {delta} from current pos {curr_pos}.")
# configure timer to un-retract lick spout
timer.timeout.disconnect()
timer.timeout.connect(lambda: self.un_retract_lick_spout(lick_spout_retract.title(), curr_pos))
timer.setInterval(self.operation_control_model.lick_spout_retraction_specs.wait_time*1000)
timer.setSingleShot(True)
timer.start()

elif self.stage_widget is None:
logger.info("Can't fast retract stage because AIND stage is not being used.",
extra={"tags": [self.warning_log_tag]})

elif tp.lick_spout_retraction or at_origin:
try:
self.Channel2.mouseLicked.connect(self.retract_lick_spout, type=Qt.UniqueConnection)
except TypeError: # signal already connected
logger.debug("Mouse lick signal already connected.")
logger.debug("Cannot retract stage because " + "lickspouts at origin." if at_origin
else "retraction turned off.")

def un_retract_lick_spout(self, lick_spout_retracted: Literal["Left", "Right"], pos: float) -> None:
"""
Un-retract specified lick spout

:param lick_spout_retracted: lick spout that was retracted.
:param pos: pos to move lick spout to.

"""

if self.stage_widget is not None:
logger.info(f"Un-retracting {lick_spout_retracted.lower()} lick spout. Moving back to position {pos}.")
speed = self.operation_control_model.lick_spout_retraction_specs.un_retract_speed.value
motor = 2 if lick_spout_retracted == "Left" else 1
# set un-retract speed. Set back to normal when user presses stop
self.stage_widget.stage_model.update_speed(value=speed)
self.stage_widget.stage_model.update_position(positions={motor: pos})

# create thread to track when stage has made it back to previous position and reconnect fast retract
Thread(target=self.reset_fast_retract, args=(motor, pos)).start()

else:
logger.info("Can't un retract lick spout because no AIND stage connected")

def reset_fast_retract(self, motor: int, pos: float):
""""
Resets AIND stage fast retract when spout has returned to original pos

:param motor: motor index to query
:param pos: original pos to wait for
"""

if self.stage_widget is not None:

while (curr := self.stage_widget.stage_model.get_current_positions_mm(motor, rel_to_monument=True)) < pos:
logger.info(f"Stage at {curr}")
time.sleep(.1)

logger.info("Setting stage to normal speed.")
try:
self.stage_widget.stage_model.move_worker.finished.disconnect(self.reset_fast_retract)
except TypeError: # signal isn't connected
pass
self.stage_widget.stage_model.update_speed(value=1)

logger.info("Reconnecting fast retract at pos.")
try:
self.Channel2.mouseLicked.connect(self.retract_lick_spout, type=Qt.UniqueConnection)
except TypeError: # signal already connected
logger.debug("Mouse lick signal already connected.")

else:
logger.info("Can't set stage speed because no AIND stage connected")

def _LoadUI(self):
"""
Determine which user interface to use
Expand Down Expand Up @@ -845,7 +939,7 @@ def update_loaded_mouse_offset(self):
return

elif list(current_positions.keys()) == ["x", "y", "z"]:
logging.info(
logging.debug(
"Can't update loaded mouse offset with non AIND stage coordinates."
)
else:
Expand Down Expand Up @@ -885,7 +979,7 @@ def update_operational_control_stage_positions(self, *args) -> None:
self.operation_control_model.stage_specs.step_size = step




def update_stage_positions_from_operational_control(
self, oc: OperationalControl = None
Expand Down Expand Up @@ -2759,6 +2853,7 @@ def _ConnectOSC(self):
self.client2 = OSCStreamingClient()
self.client2.connect((self.ip, self.request_port2))
self.Channel2 = rigcontrol.RigClient(self.client2)

# manually give water
self.client3 = OSCStreamingClient() # Create client
self.client3.connect((self.ip, self.request_port3))
Expand Down Expand Up @@ -3576,7 +3671,7 @@ def _Save(self, ForceSave=0, SaveAs=0, SaveContinue=0, BackupSave=0):
and self.InitializeBonsaiSuccessfully == 1
and BackupSave == 0
):
self.GeneratedTrials._get_irregular_timestamp(self.Channel2)
self.GeneratedTrials._get_irregular_timestamp(self.Channel2, self.data_lock)

# Create new folders.
if self.CreateNewFolder == 1:
Expand Down Expand Up @@ -4209,7 +4304,11 @@ def _LoadVisualization(self):
return

self.PlotM = PlotV(
win=self, GeneratedTrials=self.GeneratedTrials, width=5, height=4
win=self,
data_lock=self.data_lock,
GeneratedTrials=self.GeneratedTrials,
width=5,
height=4
)
self.PlotM.setSizePolicy(
QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding
Expand Down Expand Up @@ -5007,6 +5106,11 @@ def _Start(self):
# set flag to perform habituation period
self.behavior_baseline_period.set()

try: # connect signal for fast retraction
self.Channel2.mouseLicked.connect(self.retract_lick_spout, type=Qt.UniqueConnection)
except TypeError: # signal already connected
logger.debug("Mouse lick signal already connected.")

self.session_run = True # session has been started
else:
# Prompt user to confirm stopping trials
Expand Down Expand Up @@ -5047,6 +5151,12 @@ def _Start(self):
self.sound_button.setEnabled(True)
self.behavior_baseline_period.clear() # set flag to break out of habituation period

# disconnect fast retract signals if connected
try:
self.Channel2.mouseLicked.disconnect(self.retract_lick_spout)
except TypeError:
pass

if (self.StartANewSession == 1) and (self.ANewTrial == 0):
# If we are starting a new session, we should wait for the last trial to finish
self._StopCurrentSession()
Expand Down Expand Up @@ -5102,7 +5212,11 @@ def _Start(self):
self.GeneratedTrials = GeneratedTrials
self.StartANewSession = 0
PlotM = PlotV(
win=self, GeneratedTrials=GeneratedTrials, width=5, height=4
win=self,
data_lock=self.data_lock,
GeneratedTrials=GeneratedTrials,
width=5,
height=4
)
# PlotM.finish=1
self.PlotM = PlotM
Expand Down Expand Up @@ -5156,6 +5270,7 @@ def _Start(self):
GeneratedTrials._get_irregular_timestamp, self.Channel2
)
workerLick.signals.finished.connect(self._thread_complete2)

workerPlot = Worker(
PlotM._Update,
GeneratedTrials=GeneratedTrials,
Expand Down Expand Up @@ -5230,7 +5345,6 @@ def _Start(self):
"Running photometry baseline",
extra={"tags": [self.warning_log_tag]},
)

self._StartTrialLoop(GeneratedTrials, worker1, worker_save)

if self.actionDrawing_after_stopping.isChecked() == True:
Expand Down
110 changes: 66 additions & 44 deletions src/foraging_gui/MyFunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import datetime
from itertools import accumulate
from sys import platform as PLATFORM
from threading import Lock

import numpy as np
import requests
Expand Down Expand Up @@ -2883,7 +2884,7 @@ def _add_one_trial(self):
self.BlockLenHistory[i][-1] + 1
)

def _GetAnimalResponse(self, Channel1, Channel3, data_lock):
def _GetAnimalResponse(self, Channel1, Channel3, data_lock: Lock):
"""Get the animal's response"""
self._CheckSimulationSession()
if self.CurrentSimulation:
Expand Down Expand Up @@ -3114,67 +3115,88 @@ def _GiveRight(self, channel3):
channel3.ManualWater_Right(int(1))
channel3.RightValue1(float(self.win.right_valve_open_time * 1000))

def _get_irregular_timestamp(self, Channel2):
def _get_irregular_timestamp(self, Channel2, data_lock: Lock):
"""Get timestamps occurred irregularly (e.g. licks and reward delivery time)"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addd data_lock here since these variables are saved during the experiment


while not Channel2.msgs.empty():
Rec = Channel2.receive()

if Rec[0].address == "/LeftLickTime":
self.B_LeftLickTime = np.append(
self.B_LeftLickTime, Rec[1][1][0]
)
with data_lock:
self.B_LeftLickTime = np.append(
self.B_LeftLickTime, Rec[1][1][0]
)

elif Rec[0].address == "/RightLickTime":
self.B_RightLickTime = np.append(
self.B_RightLickTime, Rec[1][1][0]
)
with data_lock:
self.B_RightLickTime = np.append(
self.B_RightLickTime, Rec[1][1][0]
)
elif Rec[0].address == "/LeftRewardDeliveryTime":
self.B_LeftRewardDeliveryTime = np.append(
self.B_LeftRewardDeliveryTime, Rec[1][1][0]
)
with data_lock:
self.B_LeftRewardDeliveryTime = np.append(
self.B_LeftRewardDeliveryTime, Rec[1][1][0]
)
elif Rec[0].address == "/RightRewardDeliveryTime":
self.B_RightRewardDeliveryTime = np.append(
self.B_RightRewardDeliveryTime, Rec[1][1][0]
)
with data_lock:
self.B_RightRewardDeliveryTime = np.append(
self.B_RightRewardDeliveryTime, Rec[1][1][0]
)
elif Rec[0].address == "/LeftRewardDeliveryTimeHarp":
self.B_LeftRewardDeliveryTimeHarp = np.append(
self.B_LeftRewardDeliveryTimeHarp, Rec[1][1][0]
)
with data_lock:
self.B_LeftRewardDeliveryTimeHarp = np.append(
self.B_LeftRewardDeliveryTimeHarp, Rec[1][1][0]
)
elif Rec[0].address == "/RightRewardDeliveryTimeHarp":
self.B_RightRewardDeliveryTimeHarp = np.append(
self.B_RightRewardDeliveryTimeHarp, Rec[1][1][0]
)
with data_lock:
self.B_RightRewardDeliveryTimeHarp = np.append(
self.B_RightRewardDeliveryTimeHarp, Rec[1][1][0]
)
elif Rec[0].address == "/PhotometryRising":
self.B_PhotometryRisingTimeHarp = np.append(
self.B_PhotometryRisingTimeHarp, Rec[1][1][0]
)
with data_lock:
self.B_PhotometryRisingTimeHarp = np.append(
self.B_PhotometryRisingTimeHarp, Rec[1][1][0]
)
elif Rec[0].address == "/PhotometryFalling":
self.B_PhotometryFallingTimeHarp = np.append(
self.B_PhotometryFallingTimeHarp, Rec[1][1][0]
)
with data_lock:
self.B_PhotometryFallingTimeHarp = np.append(
self.B_PhotometryFallingTimeHarp, Rec[1][1][0]
)
elif Rec[0].address == "/OptogeneticsTimeHarp":
self.B_OptogeneticsTimeHarp = np.append(
self.B_OptogeneticsTimeHarp, Rec[1][1][0]
)
with data_lock:
self.B_OptogeneticsTimeHarp = np.append(
self.B_OptogeneticsTimeHarp, Rec[1][1][0]
)
elif Rec[0].address == "/ManualLeftWaterStartTime":
self.B_ManualLeftWaterStartTime = np.append(
self.B_ManualLeftWaterStartTime, Rec[1][1][0]
)
with data_lock:
self.B_ManualLeftWaterStartTime = np.append(
self.B_ManualLeftWaterStartTime, Rec[1][1][0]
)
elif Rec[0].address == "/ManualRightWaterStartTime":
self.B_ManualRightWaterStartTime = np.append(
self.B_ManualRightWaterStartTime, Rec[1][1][0]
)
with data_lock:
self.B_ManualRightWaterStartTime = np.append(
self.B_ManualRightWaterStartTime, Rec[1][1][0]
)
elif Rec[0].address == "/EarnedLeftWaterStartTime":
self.B_EarnedLeftWaterStartTime = np.append(
self.B_EarnedLeftWaterStartTime, Rec[1][1][0]
)
with data_lock:
self.B_EarnedLeftWaterStartTime = np.append(
self.B_EarnedLeftWaterStartTime, Rec[1][1][0]
)
elif Rec[0].address == "/EarnedRightWaterStartTime":
self.B_EarnedRightWaterStartTime = np.append(
self.B_EarnedRightWaterStartTime, Rec[1][1][0]
)
with data_lock:
self.B_EarnedRightWaterStartTime = np.append(
self.B_EarnedRightWaterStartTime, Rec[1][1][0]
)
elif Rec[0].address == "/AutoLeftWaterStartTime":
self.B_AutoLeftWaterStartTime = np.append(
self.B_AutoLeftWaterStartTime, Rec[1][1][0]
)
with data_lock:
self.B_AutoLeftWaterStartTime = np.append(
self.B_AutoLeftWaterStartTime, Rec[1][1][0]
)
elif Rec[0].address == "/AutoRightWaterStartTime":
with data_lock:
self.B_AutoRightWaterStartTime = np.append(
self.B_AutoRightWaterStartTime, Rec[1][1][0]
)
self.B_AutoRightWaterStartTime = np.append(
self.B_AutoRightWaterStartTime, Rec[1][1][0]
)
Expand Down
Loading