That's what the trial setter does. It reads the history of events in 'ardulines' and sends commands automatically to the arduino. For instance, it will force right trials until 5 rewards have been obtained, then force left trials until 5 rewards have been obtained, and so forth.
Here's what that code looks like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## ardulines stuff | |
# Find the most recent/current ardulines | |
filename = sorted(glob.glob('./ardulines.*'))[-1] | |
force_dir = 'R' | |
bout_beginning = 0 | |
REWARDS_PER_BOUT = 5 | |
## ardulines call | |
def update(): | |
global bout_beginning, force_dir, REWARDS_PER_BOUT | |
cmd = '' | |
# Read the files | |
with file(filename) as fi: | |
lines = fi.readlines() | |
# identify the current force | |
force_lines = filter( | |
lambda line: line.strip() == 'ACK FORCE L' or line.strip() == 'ACK FORCE R', | |
lines) | |
if len(force_lines) > 0: | |
inferred_force = force_lines[-1].strip()[-1] | |
#~ print inferred_force | |
if inferred_force != force_dir: | |
#~ print "warning: thought force_dir was %s but inferring it's %s" % ( | |
#~ force_dir, inferred_force) | |
force_dir = inferred_force | |
# Include only events since the most recent force | |
lines = lines[bout_beginning:] | |
# Find all the reward lines | |
rew_l_lines = np.where([ | |
line.strip().endswith('EVENT REWARD_L') for line in lines])[0] | |
rew_r_lines = np.where([ | |
line.strip().endswith('EVENT REWARD_R') for line in lines])[0] | |
# Decide whether to force | |
if force_dir == 'L': | |
if len(rew_l_lines) >= REWARDS_PER_BOUT: | |
cmd = 'echo "FORCE R" > TO_DEV' | |
os.system(cmd) | |
# Update variables for next bout | |
force_dir = 'R' | |
bout_beginning = len(lines) + bout_beginning | |
elif force_dir == 'R': | |
if len(rew_r_lines) >= REWARDS_PER_BOUT: | |
cmd = 'echo "FORCE L" > TO_DEV' | |
os.system(cmd) | |
# Update variables for next bout | |
force_dir = 'L' | |
bout_beginning = len(lines) + bout_beginning | |
return cmd |
It's a bit funny-looking, because it uses those persistent global variables. That's because the idea is that something calls this update() function over and over again, as often as possible, and on each call it needs to remember its current state (for instance, which side was most recently forced).
What's going to call that update() function over and over? And what if the user wants to change the number of rewarded trials necessary to trigger a switch? We commonly start with long strings of rewarding the same side, and then decrease the length of this string as the subject improves. To address these questions, I wrote a simple text-based UI using the curses module.
I had never used curses before, but it's a really easy way to make simple text-based UIs. It's also pleasingly anachronistic, at least to me, and reminds me of the way I used to write BASIC programs in the 90s. Later on I'll upgrade this to something graphical, but that's a whole new can of worms.
Anyway, the UI provides a menu of options which can be accessed by pressing a single key. Some of them then require additional input. For instance, the screen shot shows how a user can change the number of rewards necessary to trigger a switch, by first pressing s and then typing in 25 at the prompt.
I also included shortcuts for other common actions, like manually rewarding a certain side or echoing arbitrary text to the arduino. Now the user doesn't actually have to type in echo commands in the terminal, which actually ends up saving a lot of keystrokes and aggravation in the long run.
This is all a layer of abstraction above the language I had previously implemented for the user to communicate with the arduino. Basically, it takes in user-friendly actions ("press R to reward the right port") and converts them into text commands that are easier for the arduino to interpret.
Here's the main code for doing this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## UI main loop | |
try: | |
stdscr = curses.initscr() | |
#~ curses.noecho() | |
curses.cbreak() | |
stdscr.keypad(1) | |
stdscr.clear() | |
stdscr.addstr(0, 0, MENU) | |
stdscr.timeout(TIMEOUT) | |
while True: | |
# clear input line | |
clear_line(LINES.ENTRY) | |
# get input | |
c = stdscr.getch() | |
if c != -1: | |
c = chr(c) | |
# clear any error message | |
clear_line(LINES.ERROR) | |
# parse input | |
if c == 'q': | |
break | |
elif c == 'l': | |
cmd = 'echo "REWARD L" > TO_DEV' | |
stdscr.addstr(LINES.ERROR, 0, cmd) | |
os.system(cmd) | |
elif c == 'r': | |
cmd = 'echo "REWARD R" > TO_DEV' | |
stdscr.addstr(LINES.ERROR, 0, cmd) | |
os.system(cmd) | |
elif c == 'w': | |
cmd = 'echo "REWARD" > TO_DEV' | |
stdscr.addstr(LINES.ERROR, 0, cmd) | |
os.system(cmd) | |
elif c == 'f': | |
cmd = 'echo "FORCE L" > TO_DEV' | |
stdscr.addstr(LINES.ERROR, 0, cmd) | |
os.system(cmd) | |
elif c == 'g': | |
cmd = 'echo "FORCE R" > TO_DEV' | |
stdscr.addstr(LINES.ERROR, 0, cmd) | |
os.system(cmd) | |
elif c == 'e': | |
msg = get_additional_input("What do you want to echo?\n") | |
cmd = 'echo "%s" > TO_DEV' % msg.upper() | |
stdscr.addstr(LINES.ERROR, 0, cmd) | |
os.system(cmd) | |
elif c == 's': | |
msg = get_additional_input("How many rewards per bout?\n") | |
try: | |
val = int(msg) | |
except ValueError: | |
stdscr.addstr(LINES.ERROR, 0, 'invalid input') | |
continue | |
stdscr.addstr(LINES.ERROR, 0, | |
'setting rewards per bout to %d' % val) | |
REWARDS_PER_BOUT = val | |
else: | |
stdscr.addstr(LINES.ERROR, 0, "invalid input: %r" % c) | |
ardulines_cmd = update() | |
if ardulines_cmd != '': | |
timestr = datetime.datetime.now().strftime('%Y%m%d%H%M%S') | |
stdscr.addstr(LINES.ARDU_ECHO, 0, timestr + ' ' + ardulines_cmd) | |
finally: | |
curses.nocbreak() | |
stdscr.keypad(0) | |
curses.echo() | |
curses.endwin() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## UI stuff | |
MENU = """(q) quit | |
(e) echo | |
(s) set rewards | |
(l) reward L | |
(r) reward R | |
(w) reward current | |
(f) force L | |
(g) force R | |
""" | |
class LINES: | |
pass | |
LINES.ENTRY = 10 | |
LINES.ENTRY2 = 11 | |
LINES.ERROR = 12 | |
LINES.ARDU_ECHO = 15 | |
TIMEOUT = 1000 | |
def clear_line(line_num): | |
stdscr.move(line_num, 0) | |
stdscr.clrtoeol() | |
def get_additional_input(prompt): | |
clear_line(LINES.ENTRY) | |
clear_line(LINES.ENTRY2) | |
stdscr.addstr(LINES.ENTRY, 0, prompt) | |
stdscr.move(LINES.ENTRY2, 0) | |
stdscr.timeout(-1) | |
val = stdscr.getstr() | |
stdscr.timeout(TIMEOUT) | |
clear_line(LINES.ENTRY2) | |
return val |
The next steps are to build in some more interesting logic in the trial setter. As the behavioral task becomes more complicated, we'll want to do more than just alternate sides. I'll probably also put some time into packaging up all of the different components into something cleaner and better documented.