Skip to content

Commit

Permalink
DESIGN: add a CL phidget ball-spinning experiment
Browse files Browse the repository at this point in the history
which also serves as a hardware-in-the-loop latency
(well really synchronicity) tester.
  • Loading branch information
nzjrs committed Dec 29, 2020
1 parent 93125f1 commit 7b11fda
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 21 deletions.
77 changes: 77 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,83 @@ to be included in the configuration file for the rig used
item is played on any backend, thus the simultaneous starting of two stimuli on both backends
will generate two stacks.


#### An example closed-loop randomized audio+optogenetic experiment

This example uses the same stimuli as the last experiment but instead of randomly
switching corresponding-pairs of audio+daq playlist items every number of seconds,
in this experiment we instead play a random audio and optogenetic stimulus every time
the fly (ball) speed exceeds a threshold value.

You should note that this experiment uses the same [playlist](playlists/audio_daq_paused.yml)
as [the previous](#a-dynamic-paired-random-audiooptogenetic-experiment), but a different
[experiment](experiments/cl_random_audio_daq.py).

#### Testing Closed Loop Experiments

Extending on the previous experiment where the fly (ball) speed was used to trigger a closed
loop response, it is possible to use a stepper motor which rotates the ball to test the
experiment logic and aspects of the system latency or synchronicity.

Pre-requisites:
1) a stepper motor should be attached to the ball and connected to a phidgets
stepper motor controller
2) the stepper motor controller is connected to port 0 on the phidget
3) FicTrac is calibrated
4) The _Phidgets Network Server_ is enabled ([instructions here](https://www.phidgets.com/docs/Phidget_Control_Panel#Network_Server_Tab))
In 'normal' operation (which is lowest latency) the Phidget device is opened
and controlled only by FlyVR (because it is used to signal scanimage).
However, in this case, we need to also use the Phidget device to control the
stepper motor.

First test the Phidget, stepper motor, and network server configration you should run
the ball controller alone (without FlyVR running). You can do this by
`$ python tests\ball_control_phidget\ball_control.py` which will randomly spin the ball
at two speeds in either direction (or stop it). If the ball reliably spins in both directions
and does not get stuck, then you can proceed to test the speed trigger.

The sample [experiment](experiments/cl_random_audio_daq.py) triggers when an empirically
determined speed threshold is exceeded. This value `SPEED_THRESHOLD` might need to be changed
to correspond to a true fly, or likely also to your ball+stepper mounting. To simply play
with the value, you can run only FicTrac and a very
[similar experiment](experiments/print_ball_speed.py) which just prints
when the threshold is exceeded. To do this perform the following

1. Launch FicTrac
`$ flyvr-fictrac -f FicTracPGR_ConfigMaster.txt -m log.txt`
where `-f FicTracPGR_ConfigMaster.txt` is the same value you would use in
a 'real' FlyVR experiment configuration yaml
2. Launch the 'experiment'
`$ flyvr-experiment.exe -e experiments\print_ball_speed.py`
which will print every time the `SPEED_THRESHOLD` is exceeded. you can/should
simply modify this experiment to determine your `SPEED_THRESHOLD` and `FILTER_LEN`
values
3. Set the ball to different speeds
`$ python tests\ball_control_phidget\ball_control.py 123`
where 123 can be replaced with other values (max 3000) to spin the ball faster

Once you have determine the values you wish to use, you can launch the CL experiment, and
instead of needing a real fly, you can manually spin the ball using `ball_control.py` to
trigger the closed loop condition.

**Important**: Because the phidget is being operated in Network mode by the ball controller,
an additional configuration option must be added to your FlyVR config for this test or provided
on the command line `--phidget_network`

Finally, to launch and test your closed loop experiment you can do
1. Launch FlyVR
`flyvr.exe --phidget_network -c your_rig.yml -p playlists/audio_daq_paused.yml -e experiments/cl_random_audio_daq.py`
as always, `your_reg.yml` should contain the necessary configuration options specific to
your hardware
2. Manually trigger the CL condition to spinning the ball
`$ python tests\ball_control_phidget\ball_control.py 123`
where `123` was the empirically determined value which exceeds the `SPEED_THRESHOLD` you
determined earlier.

Note and Future Extensions: A new experiment that controlled the ball itself also from
within the experiment (using the same API as in `ball_control.py`) could be written, which
would eliminate the need to launch the `ball_control.py` in a separate terminal.

## Stimuli Randomization and Repeat

Random and repeat modes are specified in the playlist for each backend, within a special 'playlist item'
Expand Down
20 changes: 0 additions & 20 deletions experiments/ball_speed.py

This file was deleted.

60 changes: 60 additions & 0 deletions experiments/cl_random_audio_daq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import collections

import numpy as np

from flyvr.control.experiment import Experiment

# this is an example 'closed-loop' experiment that plays a random
# item from the configured video and DAQ playlists every time the
# fly (ball) speed exceeds 0.03.
#
# note: it is likely that the SPEED_THRESHOLD and FILTER_LEN
# might need to be tuned based on the actual fly and/or
# the attachment of the stepper motor to the ball. you can
# use experiments/print_ball_speed.py to test these values
#
# For testing, you can control the speed of the ball using
# the tests/ball_control_phidget/ball_control.py and passing it
# a desired speed. If it is run without arguments then it will
# randomly alternate between 2 speeds in both directions and stopped.
# if run with an argument, it will set the ball to that speed
# e.g.
# $ python tests/ball_control_phidget/ball_control.py 600


class _MyExperiment(Experiment):

SPEED_THRESHOLD = 0.03
FILTER_LEN = 33

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._rand = np.random.RandomState()
# a rolling list of the last N speed measurements, filtered
# with np.mean to remove noise
self._speed_hist = collections.deque(maxlen=self.FILTER_LEN)

@property
def is_started_and_ready_audio_daq(self):
return self.is_started() and \
self.is_backend_ready(Experiment.BACKEND_AUDIO) and \
self.is_backend_ready(Experiment.BACKEND_DAQ)

@property
def is_started_and_ready_video(self):
return self.is_started() and self.is_backend_ready(Experiment.BACKEND_VIDEO)

def process_state(self, state):
if self.is_started_and_ready_audio_daq:
self._speed_hist.append(state.speed)
if np.mean(state.speed) > self.SPEED_THRESHOLD:
item_daq = self._rand.choice(self.configured_playlist_items[Experiment.BACKEND_DAQ])
self.play_playlist_item(Experiment.BACKEND_DAQ, item_daq)
item_audio = self._rand.choice(self.configured_playlist_items[Experiment.BACKEND_AUDIO])
self.play_playlist_item(Experiment.BACKEND_AUDIO, item_audio)

self.log.info('switched to daq:%s and audio:%s (speed=%s frame=%s)' % (
item_daq, item_audio, np.mean(state.speed), state.frame_cnt))


experiment = _MyExperiment()
33 changes: 33 additions & 0 deletions experiments/print_ball_speed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import collections

import numpy as np

from flyvr.control.experiment import Experiment

# A small experiment for printing when the fly (ball) speed
# exceeds on average SPEED_THRESHOLD over FILTER_LEN periods
# (1 period = 1 frame = 1/fps)

# This small 'experiment' should be run by the single
# flyvr-experiment program - while the rest of flyvr is
# running. That means
# $ flyvr.exe -c XX.yaml -p YY.yaml
# (where neither the config XX.yml nor YY.yaml)


class _MyExperiment(Experiment):

SPEED_THRESHOLD = 0.03
FILTER_LEN = 33

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._speed_hist = collections.deque(maxlen=self.FILTER_LEN)

def process_state(self, state):
self._speed_hist.append(state.speed)
if np.mean(state.speed) > self.SPEED_THRESHOLD:
print("SPIN ", state.frame_cnt, "=", np.mean(state.speed))


experiment = _MyExperiment()
16 changes: 15 additions & 1 deletion tests/ball_control_phidget/ball_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@

from flyvr.hwio.phidget import DEFAULT_REMOTE

# FOR THIS SCRIPT TO RUN THE PHIDGET NETWORK SERVER
# MUST BE ENABLED IN THE PHIDGET CONTROL PANEL
# https://www.phidgets.com/docs/Phidget_Control_Panel

# this is a simple script which controls the speed of
# a phidget controlled stepper motor attached to port 0
# of a VINT phidget hub. when launched with no arguments
# the script randomly switches between two speeds in both
# directions, or stopped. when run with an argument it sets
# the stepper to that speed only
# e.g.
# $ python tests/ball_control_phidget/ball_control.py 600


def main(initial_vel, remote_details=None):
if remote_details:
Expand Down Expand Up @@ -45,11 +58,12 @@ def main(initial_vel, remote_details=None):
stepper0.setVelocityLimit(0)
stepper0.close()


if __name__ == '__main__':
try:
vel = int(sys.argv[1])
assert vel < 3000
assert vel > -3000
except IndexError:
vel = None
main(vel, remote_details=DEFAULT_REMOTE)
main(vel, remote_details=DEFAULT_REMOTE)

0 comments on commit 7b11fda

Please sign in to comment.