1
2
3 """pype: python physiology environment
4
5 This is the main module for pype. It gets imported by both data
6 collection and data analysis programs/tasks. In general, any
7 pype-related program or task should probably import this module as
8 early on as possible.
9
10 Author -- James A. Mazer (mazerj@gmail.com)
11
12 """
13
14
15
16
17 import sys
18 import os
19 import posixpath
20 import posix
21 import time
22 import thread
23 import string
24 import re
25 import socket
26 import asyncore
27 import threading
28 import glob
29 import cPickle
30 import math
31 import numpy as np
32
33 import Tkinter
34
35
36
37
38
39
40
41
42
43
44
45
46
47 euid = None
48 try:
49 euid = os.geteuid()
50 if euid == 0:
51 os.seteuid(os.getuid())
52 import matplotlib
53 matplotlib.use('TkAgg')
54
55
56
57 os.environ['MPLCONFIGDIR'] = '%s-%s' % (matplotlib.get_configdir(),
58 matplotlib.__version__)
59
60 try:
61 os.mkdir(os.environ['MPLCONFIGDIR'])
62 except OSError:
63 pass
64 finally:
65 if not euid is None:
66 os.seteuid(euid)
67 del euid
68
69 from types import *
70 from Tkinter import *
71 import Pmw
72
73
74
75
76 from pypedebug import *
77 from rootperm import *
78 from pype_aux import *
79 from ptable import *
80 from beep import *
81 from sprite import *
82 from spritetools import *
83 from events import *
84 from guitools import *
85 from pypeerrors import *
86 from pypedata import *
87 if sys.platform.startswith('linux'):
88 from dacq import *
89 else:
90 from dacqfallback import *
91 import pypeversion
92
93 import prand
94 prand.validate(exit=True)
95
97 """Get name of calling function (or, if level=1: caller of caller, etc.
98
99 """
100
101 import traceback
102 stack = traceback.extract_stack()
103 filename, codeline, funcName, text = stack[-2-level]
104 return funcName
105
107 common = (
108 ptitle('Session Data'),
109 pyesno('warningbeeps', 0,
110 'use sounds to cue beginning and end of runs'),
111 pslot('subject', '', is_any,
112 'subject id for filenames (ie, nickname)'),
113 pslot('full_subject', '', is_any,
114 'full (unique) subject name'),
115 pslot('owner', '', is_any,
116 'datafile owner'),
117 pslot('cell', '', is_any,
118 'unique (per subj) cell id #'),
119 pyesno('acute', 0,
120 'acute experiment'),
121 pyesno('save_tmp', 1,
122 '0 to write to /dev/null'),
123 pyesno('fast_tmp', 1,
124 'super fast tmp mode -- write to /dev/null'),
125
126 ptitle('Fixation Window Params'),
127 pslot('win_size', '50', is_int,
128 'fixation window radius (pix)'),
129 pslot('win_scale', '0.0', is_float,
130 'additive ecc. adj for win_size (rad-pixels/ecc-pixels)'),
131 pslot('vbias', '1.0', is_float,
132 'fixwin vertical elongation factor (1.0=none)'),
133
134 ptitle('Recording Info'),
135 pslot('site.well', '', is_any,
136 'well number'),
137 pslot('site.probe', '', is_any,
138 'probe location'),
139 pslot('site.depth', '', is_any,
140 'electrode depth (um)'),
141
142 ptitle('Reward Params'),
143 pslot('dropsize', '100', is_int,
144 'mean drop size in ms (for continuous flow systems)'),
145 pslot('mldropsize', '0', is_float,
146 'estimated dropsize in ml (user enters!)'),
147 pslot('dropvar', '10', is_int,
148 'reward variance (sigma^2)'),
149 pslot('maxreward', '500', is_gteq_zero,
150 'maximum reward duration (hard limit)'),
151 pslot('minreward', '0', is_gteq_zero,
152 'minimum reward duration (hard limit)'),
153
154 ptitle('Pype Blocking Params'),
155 pslot('max_trials', '0', is_int,
156 'trials before stopping (0 for no limit)'),
157 pslot('max_correct', '0', is_int,
158 'correct trials before stopping (0 for no limit)'),
159 pslot('max_ui', '0', is_int,
160 'sequential UIs before stopping (0 for no limit)'),
161
162 ptitle('Fixspot/Screen Appearance'),
163 pslot('fix_size', '2', is_int,
164 'fixspot radius (pix)'),
165 pslot('fix_ring', '10', is_int,
166 'blank zone radius around fixspot (pix)'),
167 pslot('fix_x', '0', is_int,
168 'default X pos of fixspot (pix)'),
169 pslot('fix_y', '0', is_int,
170 'default Y pos of fixspot (pix)'),
171
172 ptitle('Timing Params'),
173 pslot('minrt', '0', is_int,
174 'minimum allowed manual RT (ms; 0 for none)'),
175 pslot('maxrt', '600', is_int,
176 'maximum allowed manual RT (ms)'),
177 pslot('minsrt', '0', is_int,
178 'minimum allowed saccade RT (ms; 0 for none)'),
179 pslot('maxsrt', '600', is_int,
180 'maximum allowed saccade RT (ms)'),
181 pslot('iti', '4000+-20%', is_iparam,
182 'inter-trial interval (ms)'),
183 pslot('timeout', '10000+-20%', is_iparam,
184 'penalty timeout for errors (ms)'),
185 pslot('uitimeout', '20000+-20%', is_iparam,
186 'uninitiated trial timeout (ms)'),
187
188 ptitle('Eye Candy Params'),
189 pyesno('show_noise', 1,
190 'noise background during slides'),
191 )
192
193 rig = (
194 ptitle('DACQ Params'),
195 pslot('fixbreak_tau_ms', '5', is_int,
196 '# ms before fixbreak counts;'
197 ' press EyeTracker:UPDATE to take effect'),
198 pslot('eye_smooth', '3', is_int,
199 'smoothing length (DACQ ticks aka ms);'
200 ' press EyeTracker:UPDATE to take effect'),
201 pslot('dacq_pri', '-10', is_int,
202 'priority of DACQ process pslot (<0 higher)'),
203 pyesno('rt_sched', 0,
204 'try to use real time (rt) scheduler'),
205 pslot('fb_pri', '-10', is_int,
206 'priority of the framebuffer process pslot (<0 higher)'),
207 pslot('photo_thresh', '500', is_int,
208 'threshold for photodiode detection'),
209 pslot('photo_polarity', '1', is_int,
210 'sign of threshold for photodiode detection'),
211 pslot('spike_thresh', '500', is_int,
212 'threshold for spike detection'),
213 pslot('spike_polarity', '1', is_int,
214 'sign of threshold for spike detection'),
215 pyesno('save_ain0', 0, 'save raw AIN 0'),
216 pyesno('save_ain1', 0, 'save raw AIN 1'),
217
218
219 pyesno('save_ain4', 0, 'save raw AIN 4'),
220 pyesno('save_ain5', 0, 'save raw AIN 5'),
221 pyesno('save_ain6', 0, 'save raw AIN 6'),
222 pyesno('save_ain7', 0, 'save raw AIN 7'),
223
224 ptitle('Monitor (read-only: set in Config file)'),
225 pslot_ro('mon_id', '', is_any,
226 'monitor id string'),
227 pslot_ro('viewdist', '0', is_float,
228 'viewing distance (cm)'),
229 pslot_ro('mon_width', '0', is_float,
230 'monitor width (cm)'),
231 pslot_ro('mon_height', '0', is_float,
232 'monitor height (cm)'),
233 pslot_ro('mon_dpyw', '0', is_int,
234 'monitor virtual width (pix)'),
235 pslot_ro('mon_dpyh', '0', is_int,
236 'monitor virtual height (pix)'),
237 pslot_ro('mon_phys_dpyw', '0', is_int,
238 'monitor physical width (pix)'),
239 pslot_ro('mon_phys_dpyh', '0', is_int,
240 'monitor physical height (pix)'),
241 pslot_ro('mon_h_ppd', '0', is_float,
242 'horizontal pixels_per_deg'),
243 pslot_ro('mon_v_ppd', '0', is_float,
244 'vertical pixels_per_deg'),
245 pslot_ro('mon_ppd', '0', is_float,
246 'overall/mean pixels_per_deg'),
247 pslot_ro('mon_fps', '0', is_float,
248 'monitor frame rate (verified)'),
249
250 ptitle('Eye Tracker Info (Config file or automatic)'),
251 pslot_ro('eyetracker', '', is_any,
252 'set Config::EYETRACKER -- tracker device'),
253 pslot_ro('eyefreq', '', is_int,
254 'set Config::EYEFREQ -- tracker sampling frequency'),
255 pslot_ro('eyelag', '0', is_int,
256 'eye tracker lag (if any -- set automatically)'),
257
258 ptitle('Internal Only -- don\'t change'),
259 pslot_ro('note_x', '0', is_int, 'userdpy'),
260 pslot_ro('note_y', '0', is_int, 'userdpy'),
261 )
262
263 ical = (
264 ptitle('Tracker Cal'),
265 pslot('affinem_', '1,0,0,0,1,0,0,0,1', is_any, 'matrix'),
266 pslot('xgain_', '1.0', is_float, 'mult scale'),
267 pslot('ygain_', '1.0', is_float, 'mult scale'),
268 pslot('xoff_', '0', is_int, 'pixels'),
269 pslot('yoff_', '0', is_int, 'pixels'),
270 pslot('rot_', '0', is_float, 'degrees'),
271 )
272 return (common, rig, ical)
273
275 from PIL import Image, ImageTk
276 p = ImageTk.PhotoImage(Image.open(os.path.join(app.pypedir,'lib', file)))
277 button._image = p
278 button.config(image=button._image)
279
281 """Pype Application Class.
282
283 Toplevel class for all pype programs -- one instance of this
284 class is instantiated when the pype control gui is loaded. More
285 than one of these per system is a fatal error.
286
287 The PypeApp class has methods & instance variables for just about
288 everything a user would want to do.
289
290 """
291 _instance = None
292
293 - def __new__(cls, *args, **kwargs):
302
303 - def __init__(self, pypedir=None, psych=None, training=None, nogeo=None):
304 """Initialize a PypeApp instance, with side effects:
305
306 - FrameBuffer instance is created, which means the hardware
307 screen will be opened automatically. Right now, PypeApp
308 guesses about the size & depth of the display to simplify
309 things for the user. This could change.
310
311 - The DACQ hardware is opened, if possible.
312
313 - PypeApp gui is built and opened on screen
314
315 Don't initialize more than one instance of this class. If you
316 do an exception will be raised. An alternative (for later
317 consideration) would be to allow multiple instances which share
318 an underlying framebuffer & dacq hardware; however, this would
319 really require some sort of underlying locking method to prevent
320 collision.
321
322 """
323
324 import signal
325
326
327 import PlexNet, pype2tdt, tdt, candy, userdpy, configvars
328
329 if not PypeApp._init:
330 return
331
332 Logger("pype: '%s'\n" % (sys.version,))
333
334 self.pypedir = pypedir
335 self.psych = psych
336 self.training = training
337
338
339 self.conwin = None
340
341
342
343 self.uname = pwd.getpwuid(os.getuid())[0]
344
345
346
347
348 cfile = _hostconfigfile()
349 if not posixpath.exists(cfile):
350 Logger("pype: making new host config file '%s'\n" % cfile)
351 configvars.mkconfig(cfile)
352
353 self.config = configvars.defaults(cfile)
354 Logger("pype: config loaded from '%s'\n" % cfile)
355
356 try:
357 root_take()
358 self.init_dacq()
359 finally:
360 root_drop()
361
362 if self.psych is None:
363 self.psych = self.config.iget('PSYCH', 0)
364
365 if self.psych:
366 Logger("pype: psych mode")
367
368
369
370
371
372 if (self.config.iget('FULLSCREEN') and
373 self.config.get('SDLDPY') == os.environ['DISPLAY']):
374 self.config.set('FULLSCREEN', '0', override=1)
375 Logger("pype: FULLSCREEN ignored -- single display mode")
376
377
378
379
380
381 if 'PYPEDEBUG' in os.environ:
382 self.config.set('DEBUG', '1', override=1)
383
384 debug(self.config.iget('DEBUG'))
385 if debug():
386 Logger("pype: running in debug mode")
387 sys.stderr.write('config settings:\n')
388 self.config.show(sys.stderr)
389
390
391 monw = self.config.fget('MONW', -1)
392 monh = self.config.fget('MONH', -1)
393 viewdist = self.config.fget('VIEWDIST', -1)
394 self.config.set('MON_ID', 'notset')
395 if sys.platform.startswith('darwin'):
396 self.config.set('AUDIODRIVER', 'sndmgr')
397
398 if monw < 0 or monh < 0 or viewdist < 0:
399 Logger('pype: set MONW, MONW & VIEWDIST in %s\n' % cfile)
400 raise PypeStartupError
401
402 self.terminate = 0
403 self.running = 0
404 self._startfn = None
405 self._updatefn = None
406 self._validatefn = None
407 self.dropcount = 0
408 self.record_id = 1
409 self.record_buffer = []
410 self.record_file = None
411 self._last_eyepos = 0
412 self._allowabort = 0
413 self._rewardlock = thread.allocate_lock()
414 self._eye_x = 0
415 self._eye_y = 0
416 self._eyetarg_x = 0
417 self._eyetarg_y = 0
418 self._eyetrace = 0
419 self.taskidle = None
420
421 if nogeo:
422 self.setgeo(loadempty=1)
423 else:
424 self.setgeo(load=1)
425
426 self.tk = Pmw.initialise(useTkOptionDb=1)
427 self.tk.resizable(0, 0)
428 self.tk.title('pype')
429 self.tk.protocol("WM_DELETE_WINDOW", self._shutdown)
430 self.setgeo(self.tk, default='+20+20', posonly=1)
431
432 if self.config.iget('SPLASH'):
433 self.about(1000)
434
435 self.conwin = ConsoleWindow()
436 self.conwin.showhide()
437
438
439
440
441 Logger(window=self.conwin)
442
443 if self.config.iget('ONE_WINDOW'):
444 leftpane = Frame(self.tk)
445 leftpane.pack(side=LEFT, anchor=NW)
446 rightpane = Frame(self.tk, border=3, relief=GROOVE)
447 rightpane.pack(side=RIGHT, padx=10, anchor=CENTER)
448 else:
449 leftpane = Frame(self.tk)
450 leftpane.pack(side=LEFT, anchor=NW)
451 rightpane = None
452
453 f1 = Frame(leftpane, borderwidth=1, relief=GROOVE)
454 f1.pack(expand=0, fill=X)
455
456 self.balloon = Pmw.Balloon(self.tk, master=1, relmouse='both')
457
458 self._show_eyetrace_start = None
459 self._show_eyetrace_stop = None
460 self._show_eyetrace = IntVar()
461 self._show_eyetrace.set(0)
462 self._eyetrace_window = None
463 self._show_xt_traces = IntVar()
464 self._show_xt_traces.set(0)
465
466 mb = Pmw.MenuBar(f1)
467 mb.pack(side=LEFT, expand=0)
468
469 M = 'File'
470 mb.addmenu('File', '', '')
471 mb.addmenuitem('File', 'command', label='Save state',
472 command=self._savestate)
473 mb.addmenuitem('File', 'command', label='Show/hide log',
474 command=self.conwin.showhide)
475 mb.addmenuitem('File', 'command', label='Keyboard',
476 command=self.keyboard)
477 mb.addmenuitem('File', 'command', label='Remote Debug',
478 command=remotedebug)
479 mb.addmenuitem('File', 'command', label='Record Movie',
480 command=lambda s=self: s.fb.recordtog())
481 mb.addmenuitem('File', 'command', label='sprite benchmark',
482 command=lambda s=self: benchmark(s.fb))
483
484 mb.addmenu('Misc', '', '')
485 mb.addmenuitem('Misc', 'command', label='Find param',
486 command=self._findparam)
487 mb.addmenuitem('Misc', 'command', label='show pypefile params',
488 command=self._getparams_frompypefile)
489 mb.addmenuitem('Misc', 'separator')
490 mb.addmenuitem('Misc', 'command', label='show framebuffer',
491 command=lambda s=self: s.fb.screen_open())
492 mb.addmenuitem('Misc', 'command', label='hide framebuffer',
493 command=lambda s=self: s.fb.screen_close())
494 mb.addmenuitem('Misc', 'separator')
495 if 0:
496 mb.addmenuitem('Misc', 'command', label='Drain juice',
497 command=self._drain)
498 mb.addmenuitem('Misc', 'separator')
499 mb.addmenuitem('Misc', 'command', label='start beep',
500 command=self.warn_run_start)
501 mb.addmenuitem('Misc', 'command', label='stop beep',
502 command=self.warn_run_stop)
503 mb.addmenuitem('Misc', 'command', label='correct beep',
504 command=self.warn_trial_correct)
505 mb.addmenuitem('Misc', 'command', label='error beep',
506 command=self.warn_trial_incorrect)
507
508 mb.addmenu('Set', '', '')
509 mb.addmenuitem('Set', 'checkbutton', label='show trace window',
510 variable=self._show_eyetrace)
511 mb.addmenuitem('Set', 'checkbutton', label='X-T eye traces',
512 variable=self._show_xt_traces)
513
514
515
516 self._loadmenu = Pmw.MenuBar(f1)
517 self._loadmenu.pack(side=RIGHT, expand=1, fill=X)
518 self.make_taskmenu(self._loadmenu)
519
520 self._loadmenu.addmenu('Help', '', side=RIGHT)
521 self._loadmenu.addmenuitem('Help', 'command',
522 label='About Pype',
523 command=self.about)
524
525 f1b = Frame(leftpane, borderwidth=1, relief=GROOVE)
526 f1b.pack(expand=0, fill=X)
527
528 self.taskmodule = None
529 tmp = Frame(f1b, borderwidth=1, relief=GROOVE)
530 tmp.pack(expand=1, fill=X)
531
532 self._tasknamew = Label(tmp)
533 self._tasknamew.pack(side=LEFT)
534
535 self.task_name = None
536 self.task_dir = None
537 self._taskname(None, None)
538
539
540
541
542 if self.training:
543 Label(tmp, text='train mode', relief=SUNKEN).pack(side=RIGHT)
544 else:
545 Label(tmp, text='record mode', relief=SUNKEN).pack(side=RIGHT)
546
547 f = Frame(f1b, borderwidth=1, relief=GROOVE)
548 f.pack(expand=1, fill=X)
549
550 self._recfile = Label(f)
551 self._recfile.pack(side=LEFT)
552 self._set_recfile()
553 self.balloon.bind(self._recfile, "current datafile (if any)")
554
555 self._repinfo = Label(f, text=None)
556 self._repinfo.pack(side=RIGHT)
557
558 self._stateinfo = Label(f, text=None, font=('Andale Mono', 10))
559 self._stateinfo.pack(side=RIGHT)
560 self.mldown = None
561 self.balloon.bind(self._stateinfo, "state info: bar, juice etc")
562
563 self.disable_on_start = []
564 self.enable_on_start = []
565
566 bb = Frame(f1b)
567 bb.pack(expand=1, side=TOP, anchor=W)
568
569 f2 = Frame(leftpane)
570 f2.pack(expand=1)
571
572 f = Frame(f2)
573 f.grid(row=0, column=0, sticky=N+S)
574
575 c1pane = Frame(f, borderwidth=1, relief=RIDGE)
576 c1pane.pack(expand=0, fill=X, side=TOP, pady=10)
577
578 c2pane = Frame(f, borderwidth=1, relief=RIDGE)
579 c2pane.pack(expand=0, fill=X, side=TOP, pady=5)
580
581 c3pane = Frame(f, borderwidth=1, relief=RIDGE)
582 c3pane.pack(expand=0, fill=X, side=TOP, pady=10)
583 self._userbuttonframe = c3pane
584
585 b = Button(c1pane, text='reload', command=self.loadtask)
586 b.pack(expand=0, fill=X, side=TOP)
587 self.balloon.bind(b, "reload current task")
588 self.disable_on_start.append(b)
589
590 b = Button(c1pane, text='<<<', command=self.prevtask)
591 b.pack(expand=0, fill=X, side=TOP)
592 self.balloon.bind(b, "load previous task")
593 self.disable_on_start.append(b)
594
595 tog_udpy = Checkbutton(c2pane, text='udpy',
596 relief=RAISED, anchor=W,
597 background='lightblue')
598 tog_udpy.pack(expand=0, fill=X, side=TOP)
599 self.balloon.bind(tog_udpy, "show/hide USER display window")
600
601
602 b = Checkbutton(c2pane, text='RT hist',
603 relief=RAISED, anchor=W,
604 background='lightblue')
605 b.pack(expand=0, fill=X, side=TOP, pady=2)
606 pw = DockWindow(checkbutton=b, title='Reaction Times')
607 Button(pw, text='Clear',
608 command=self.update_rt).pack(side=TOP, expand=1, fill=X)
609 self.rthist = EmbeddedFigure(pw)
610 self.update_rt()
611
612 if not self.training and not self.psych:
613
614 b = Checkbutton(c2pane, text='psth', relief=RAISED, anchor=W,
615 background='lightblue')
616 b.pack(expand=0, fill=X, side=TOP, pady=2)
617 pw = DockWindow(checkbutton=b, title='psth')
618 Button(pw, text='Clear',
619 command=self.update_psth).pack(side=TOP, expand=1, fill=X)
620 self.psth = EmbeddedFigure(pw)
621 self.update_psth()
622 else:
623 self.psth = None
624
625 if self.config.iget('ELOG', 1) and ('ELOG' in os.environ):
626
627 _addpath(os.environ['ELOG'])
628 try:
629 import elogapi
630 self.use_elog = 1
631 Logger("pype: connecting to ELOG database system.\n")
632 except ImportError:
633 Logger("pype: warning -- ELOG api not available.\n")
634 self.use_elog = 0
635 else:
636
637 self.use_elog = 0
638
639 if self.use_elog:
640 mb.addmenuitem('File', 'command', label='elog',
641 command=lambda s=self: s.open_elog())
642
643
644 mb.addmenuitem('File', 'separator')
645 mb.addmenuitem('File', 'command', label='Quit',
646 command=self._shutdown)
647
648 (commonp, rigp, icalp) = _base_ptables()
649
650 hostname = self._gethostname()
651 b = Checkbutton(c2pane, text='subject', relief=RAISED, anchor=W)
652 b.pack(expand=0, fill=X, side=TOP, pady=2)
653 self.balloon.bind(b, "subject specific parameters")
654
655 sub_common = DockWindow(checkbutton=b, title='Subject Params')
656 self.sub_common = ParamTable(sub_common, commonp, file='subject.par',\
657 loadable=0)
658 if self.use_elog:
659
660 self.sub_common.lockfield('cell')
661
662 b = Checkbutton(c2pane, text='rig', relief=RAISED, anchor=W)
663 b.pack(expand=0, fill=X, side=TOP, pady=2)
664 self.balloon.bind(b, "rig/computer specific parameters")
665 rig_common = DockWindow(checkbutton=b,
666 title='Rig Params (%s)' % hostname)
667 self.rig_common = ParamTable(rig_common, rigp,
668 file='rig-%s.par' % hostname,
669 loadable=0)
670
671 b = Checkbutton(c2pane, text='ical', relief=RAISED, anchor=W)
672 b.pack(expand=0, fill=X, side=TOP, pady=2)
673 self.balloon.bind(b, "eye calibration params")
674 ical = DockWindow(checkbutton=b, title='ical')
675 self.ical = ParamTable(ical, icalp,
676 file='%s-%s-ical.par' % (hostname, subject(),),
677 loadable=0)
678
679
680
681
682 fps = self.init_framebuffer()
683
684 s = 180.0 * 2.0 * math.atan2(monw/2, viewdist) / np.pi
685 xppd = self.fb.w / s;
686
687 s = 180.0 * 2.0 * math.atan2(monh/2, viewdist) / np.pi
688 yppd = self.fb.h / s;
689
690 ppd = (xppd + yppd) / 2.0
691
692 self.rig_common.set('mon_id', self.config.get('MON_ID', ''))
693 self.rig_common.set('viewdist', '%g' % viewdist)
694 self.rig_common.set('mon_width', '%g' % monw)
695 self.rig_common.set('mon_height', '%g' % monh)
696 self.rig_common.set('mon_dpyw', self.fb.w)
697 self.rig_common.set('mon_dpyh', self.fb.h)
698 self.rig_common.set('mon_phys_dpyw', self.fb.physicalw)
699 self.rig_common.set('mon_phys_dpyh', self.fb.physicalh)
700 self.rig_common.set('mon_h_ppd', '%g' % xppd)
701 self.rig_common.set('mon_v_ppd', '%g' % yppd)
702 self.rig_common.set('mon_ppd', '%g' % ppd)
703
704 trackertype = self.config.get('EYETRACKER', 'NONE')
705 self.itribe = None
706 self.eyemouse = None
707 self.eyebar = 0
708
709 if trackertype == 'ISCAN':
710 self.rig_common.set('eyetracker', trackertype)
711 self.rig_common.set('eyelag', '16')
712 elif trackertype == 'EYELINK':
713 self.rig_common.set('eyetracker', trackertype)
714 self.rig_common.set('eyelag', '0')
715 mb.addmenuitem('Misc', 'separator')
716 mb.addmenuitem('Misc', 'command', label='reconnect to eyelink',
717 command=self.elrestart)
718 elif trackertype == 'ANALOG':
719 self.rig_common.set('eyetracker', trackertype)
720 self.rig_common.set('eyelag', '0')
721 elif trackertype == 'NONE':
722 self.rig_common.set('eyetracker', trackertype)
723 self.rig_common.set('eyelag', '0')
724 elif trackertype == 'MOUSE':
725 self.rig_common.set('eyetracker', trackertype)
726 self.rig_common.set('eyelag', '0')
727 elif trackertype == 'EYEMOUSE':
728 self.rig_common.set('eyetracker', trackertype)
729 self.rig_common.set('eyelag', '0')
730 self.eyemouse = 1
731 self.fb.cursor(on=1)
732 elif trackertype == 'EYETRIBE':
733 try:
734 self.itribe = EyeTribeThreadRunner(host='pippin')
735 self.rig_common.set('eyetracker', trackertype)
736 self.rig_common.set('eyelag', '0')
737 except socket.error:
738 Logger("pype: can't connect to eyetribe device\n")
739 raise PypeStartupError
740 else:
741 Logger("pype: %s is not a valid EYETRACKER.\n" % trackertype)
742 raise PypeStartupError
743 self.rig_common.set('eyefreq', self.config.get('EYEFREQ', '-1'))
744
745 startstopf = Frame(f, borderwidth=3, relief=RIDGE)
746 startstopf.pack(expand=0, fill=X, side=TOP)
747
748 w = Button(bb, command=self._start, state=DISABLED)
749 addicon(self, w, 'run.gif')
750 self.balloon.bind(w, 'start, saving data')
751 w.pack(expand=1, fill=Y, side=LEFT)
752 self.disable_on_start.append(w)
753
754 w = Button(bb, command=self._starttmp, state=DISABLED)
755 addicon(self, w, 'runtemp.gif')
756 self.balloon.bind(w, "start w/o saving data")
757 w.pack(expand=1, fill=Y, side=LEFT)
758 self.disable_on_start.append(w)
759
760 w = Button(bb, command=lambda s=self: s.pause(state=True),
761 state=DISABLED)
762 addicon(self, w, 'pause.gif')
763 self.balloon.bind(w, 'pause run')
764 w.pack(expand=1, fill=Y, side=LEFT)
765 self.enable_on_start.append(w)
766 self.pause(state=False)
767
768 w = Button(bb, command=self._start_helper, state=DISABLED)
769 addicon(self, w, 'stop.gif')
770 w.pack(expand=1, fill=Y, side=LEFT)
771 self.balloon.bind(w, "stop run at end of trial")
772 self.enable_on_start.append(w)
773
774 w = Button(bb, command=self._stopabort, state=DISABLED)
775 addicon(self, w, 'cancel.gif')
776 w.pack(expand=1, fill=Y, side=LEFT)
777 self.balloon.bind(w, "stop run immediately")
778 self.enable_on_start.append(w)
779 self._doabort = 0
780
781 if not self.psych:
782 w = b = Button(bb, command=self.reward)
783 addicon(self, w, 'drop.gif')
784
785 b.pack(expand=1, fill=Y, side=LEFT)
786 self.balloon.bind(b, "deliver a reward (also F4)")
787
788 if not self.use_elog and not self.training:
789 w = b = Button(bb, command=self._new_cell)
790 addicon(self, w, 'cell.gif')
791 b.pack(expand=1, fill=Y, side=LEFT)
792 self.balloon.bind(b, "increment 'cell' counter")
793
794 if not self.psych:
795 self._candyon = 0
796 mb.addmenu('Candy', '', '')
797 for (s, fn) in candy.list_():
798 mb.addmenuitem('Candy', 'command', label=s,
799 command=lambda s=self,f=fn: s._candyplay(f))
800
801 self.make_toolbar(bb)
802 if not sys.platform.startswith('darwin'):
803 mb.addmenu('|', '', '')
804
805 self.recent = []
806
807 book = Pmw.NoteBook(f2)
808 book.grid(row=0, column=1, sticky=N+S+E)
809
810 runlog = book.add('Pype')
811 triallog = book.add('Trial')
812 stats = book.add('Perf')
813 itrack1 = book.add('iTrak')
814 tally = book.add('Tally')
815
816 tallyf = Frame(tally)
817 tallyf.pack(side=BOTTOM, fill=X)
818
819 self._console = LogWindow(runlog)
820 self._info = LogWindow(triallog)
821
822 self._whereami()
823
824 self.statsw = Label(stats, text='',
825 anchor=W, justify=LEFT,
826 font=('Andale Mono', 10))
827 self.statsw.pack(expand=0, fill=BOTH, side=TOP)
828
829 Button(tallyf, text="clear all",
830 command=lambda s=self: s._tally(clear=1)).pack(side=LEFT)
831 b = Button(tallyf, text="clear task",
832 command=lambda s=self: s._tally(cleartask=1)).pack(side=LEFT)
833 self.tallyw = LogWindow(tally, bg='gray90')
834 self._runstats_update(clear=1)
835
836 f = Frame(itrack1)
837 f.pack(expand=0, fill=X, side=TOP)
838
839 fbar = Frame(f)
840 fbar.pack(expand=1, fill=X, side=TOP)
841
842 b = Button(fbar, text="update\ncalib",
843 command=self.eyeset)
844 b.pack(expand=0, fill=Y, side=LEFT, pady=0)
845 self.balloon.bind(b, "send tracker settings to dacq processes")
846
847 b = Button(fbar, text='zero now\n(F8)',
848 command=lambda s=self: s.eyeshift(zero=1))
849 b.pack(expand=0, fill=Y, side=LEFT)
850 self.balloon.bind(b, "subject is looking at (fx,fy) NOW")
851
852 b = Button(fbar, text="clear\naffine",
853 command=self.clearaffine)
854 b.pack(expand=0, fill=Y, side=LEFT, pady=0)
855 self.balloon.bind(b, "reset affine cal")
856
857 b = Button(fbar, text="clear\ngain",
858 command=self.cleargains)
859 b.pack(expand=0, fill=Y, side=LEFT, pady=0)
860 self.balloon.bind(b, "reset gains cal")
861
862 b = Button(fbar, text='clear\noffsets',
863 command=lambda s=self: s.eyeshift(reset=1))
864 b.pack(expand=0, fill=Y, side=LEFT)
865 self.balloon.bind(b, "reset offset's to 0,0")
866
867 fbar = Frame(f)
868 fbar.pack(expand=1, fill=X, side=TOP)
869
870 w = b = Button(fbar, command=lambda s=self: s.eyeshift(x=1, y=0))
871 addicon(self, w, 'left.gif')
872 b.pack(expand=0, fill=Y, side=LEFT)
873 self.balloon.bind(b, "shift offsets left (immediate effect)")
874 w = b = Button(fbar, command=lambda s=self: s.eyeshift(x=-1, y=0))
875 addicon(self, w, 'right.gif')
876 b.pack(expand=0, fill=Y, side=LEFT)
877 self.balloon.bind(b, "shift offsets right (immediate effect)")
878 w = b = Button(fbar, command=lambda s=self: s.eyeshift(x=0, y=-1))
879 addicon(self, w, 'up.gif')
880 b.pack(expand=0, fill=Y, side=LEFT)
881 self.balloon.bind(b, "shift offsets up (immediate effect)")
882 w = b = Button(fbar, command=lambda s=self: s.eyeshift(x=0, y=1))
883 addicon(self, w, 'down.gif')
884 b.pack(expand=0, side=LEFT)
885 self.balloon.bind(b, "shift offsets down (immediate effect)")
886
887 Label(itrack1,
888 text="Use Shift/Ctrl/Meta-Arrows in UserDisplay\n"+
889 "window to adjust offsets in real-time",
890 relief=SUNKEN).pack(expand=0, fill=X, side=TOP, pady=10)
891
892
893
894 book.setnaturalsize(pageNames=None)
895
896
897 self.pix_per_dva = float(self.rig_common.queryv('mon_ppd'))
898
899
900 self.tkkeyque = EventQueue(self.tk, '<Key>')
901
902
903 self._hist = Label(f2, text="", anchor=W, borderwidth=1, relief=RIDGE)
904 self._hist.grid(row=3, column=0, columnspan=3, sticky=W+E)
905 self.balloon.bind(self._hist, "recent trial result codes")
906
907
908 root_take()
909
910 self.dacq_going = 1
911 self.eyeset()
912 if self.eyemouse:
913
914
915 self.eyeset(xgain=1.0 * self.fb.w / self.fb.physicalw,
916 ygain=-1.0 * self.fb.h / self.fb.physicalh,
917 xoff=self.fb.w/2.0, yoff=-self.fb.h/2.0)
918
919
920
921
922 self.flip_bar = self.config.iget('FLIP_BAR')
923
924
925 self.reward_beep = self.config.iget('REWARD_BEEP')
926
927
928
929
930
931 try:
932 root_drop()
933 if not self.config.iget('SOUND'):
934 beep(disable=1)
935 Logger('pype: audio disabled in config file.\n')
936 else:
937 if self.config.get('AUDIODRIVER'):
938 audiodriver = self.config.get('AUDIODRIVER')
939 beep()
940 finally:
941 root_take()
942
943
944
945
946
947 if self.config.iget('FPS') and self.config.iget('FPS') != fps:
948 Logger('pype: error actually FPS does not match requested rate\n' +
949 ' requested=%dhz actual=%dhz\n' %
950 (self.config.iget('FPS'), fps))
951 raise PypeStartupError
952 self.rig_common.set('mon_fps', '%g' % fps)
953 Logger('pype: estimated fps = %g\n' % fps)
954
955
956
957
958 self.udpy = userdpy.UserDisplay(rightpane,
959 fbsize=(self.fb.w, self.fb.h),
960 pix_per_dva=self.pix_per_dva,
961 eyemouse=self.eyemouse,
962 app=self)
963
964 tog_udpy.config(command=lambda b=tog_udpy: \
965 self.udpy.showhide(button=b, toggle=True))
966 if self.config.iget('USERDISPLAY_HIDE'):
967 self.udpy.showhide(button=tog_udpy, toggle=True)
968 else:
969 self.udpy.showhide(button=tog_udpy, toggle=False)
970 if not rightpane:
971 self.udpy.master.protocol("WM_DELETE_WINDOW", self._shutdown)
972
973 if posixpath.exists(subjectrc('last.fid')):
974 self.udpy.loadfidmarks(file=subjectrc('last.fid'))
975 if posixpath.exists(subjectrc('last.pts')):
976 self.udpy.loadpoints(subjectrc('last.pts'))
977 self.dpy_w = self.fb.w
978 self.dpy_h = self.fb.h
979
980
981 self.udpy.drawaxis()
982
983
984
985
986
987
988 root_drop()
989 Logger('pype: dropped root access\n')
990
991
992
993
994
995
996
997
998
999 self.clear_pending_ints()
1000 self.lastint_ts = None
1001 self.idle_queue = []
1002
1003
1004
1005
1006
1007 dacq_bar_genint(0)
1008 dacq_joy_genint(0)
1009
1010
1011 self.interupts(enable=0, queue=1)
1012
1013
1014 signal.signal(signal.SIGUSR1, self._int_handler)
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026 self.xdacq = None
1027 self.xdacq_data_store = None
1028 self.plex = None
1029 self.tdt = None
1030
1031 plexhost = self.config.get('PLEXHOST')
1032 tdthost = self.config.get('TDTHOST')
1033
1034 if self.training:
1035 Logger('pype: warning -- xdacq disabled in training mode\n')
1036 elif len(plexhost) > 0 and len(tdthost) > 0:
1037 Logger('pype: either PLEXHOST *or* TDTHOST (disabled!!)\n', popup=1)
1038 elif self.xdacq is None and len(plexhost) > 0:
1039 try:
1040 self.plex = PlexNet.PlexNet(plexhost,
1041 self.config.iget('PLEXPORT'))
1042 Logger('pype: connected to plexnet @ %s.\n' % plexhost)
1043 self.xdacq = 'plexon'
1044
1045 mb.addmenu('PlexNet', '', '')
1046 mb.addmenuitem('PlexNet', 'command',
1047 label='query plexnet',
1048 command=self.status_plex)
1049 mb.addmenuitem('PlexNet', 'separator')
1050 except socket.error:
1051 Logger('pype: failed connect to plexnet @ %s.\n' % plexhost,
1052 popup=1)
1053 elif self.xdacq is None and len(tdthost) > 0:
1054 tankdir = self.config.get('TDTTANKDIR')
1055 if len(tankdir) == 0:
1056 Logger('pype: no TDTANKDIR set, using C:\\', popup=1)
1057 tankdir = 'C:\\'
1058 if not tankdir[-1] == '\\':
1059
1060 tankdir = tankdir + '\\'
1061
1062 tankname = subject()
1063 tankname += '%04d%02d%02d' % time.localtime(time.time())[0:3]
1064
1065 try:
1066 self.tdt = pype2tdt.Controller(self, tdthost)
1067 Logger('pype: connected to tdt @ %s.\n' % tdthost)
1068 t = self.tdt.settank(tankdir, tankname)
1069 if t is None:
1070 Logger("pype: is TDT running? Can't set tank.\n", popup=1)
1071 self.tdt = None
1072 else:
1073 Logger('pype: tdt tank = "%s"\n' % t)
1074 self.con('tdt tank = "%s"\n' % t)
1075 self.xdacq = 'tdt'
1076 except (socket.error, tdt.TDTError):
1077 self.tdt = None
1078 Logger('pype: no connection to tdt @ %s.\n' % tdthost, popup=1)
1079
1080 Logger('pype: build %s by %s on %s\n' %
1081 (pypeversion.PypeBuildDate, pypeversion.PypeBuildBy,
1082 pypeversion.PypeBuildHost) +
1083 'pype: PYPERC=%s\n' % pyperc() +
1084 'pype: CWD=%s\n' % os.getcwd())
1085
1086 if dacq_jsbut(-1):
1087 self._console.writenl("Joystick button #1 is BAR!", color='blue')
1088
1089 self.recording = 0
1090 self.record_state(0)
1091
1092 self.tallycount = {}
1093 self._loadstate()
1094 self._tally(type=None)
1095
1096 self.migrate_pypestate()
1097
1098 self._testpat = None
1099 self.showtestpat()
1100
1101 self._show_stateinfo()
1102
1103 if self.psych:
1104 self.fb.screen_close()
1105
1106 if self.itribe:
1107 self.itribe.start()
1108
1109
1110 if self.config.iget('HTTP_SERVER'):
1111 import pypehttpd
1112 self.server = pypehttpd.PypeHTTPServer(self)
1113 self.server.start()
1114 else:
1115 self.server = None
1116
1118
1119 animal = self.sub_common.queryv('full_subject')
1120 os.system('elog -y -animal=%s -today &' % (animal,))
1121
1124
1125 - def about(self, timeout=None):
1126 show_about(os.path.join(self.pypedir,'lib', 'logo.gif'), timeout)
1127
1129 """Queue an action to happen about inms from now. Call with
1130 no args to clear the actio queue.
1131
1132 Action will be dispatched from the idlefn() on or after the
1133 specified deadline.
1134
1135 """
1136
1137 if inms is None:
1138 self.idle_queue = []
1139 else:
1140 self.idle_queue.append((dacq_ts()+inms, action,))
1141
1143 self._console.writenl('ver: pype %s' % (pypeversion.PypeVersion,),
1144 'blue')
1145 self._console.writenl('id: %s' % (pypeversion.PypeVersionID),
1146 'blue')
1147 self._console.writenl('pypedir=%s' % self.pypedir, 'blue')
1148 self._console.writenl('pyperc=%s' % pyperc(), 'blue')
1149 self._console.writenl('cwd=%s' % os.getcwd(), 'blue')
1150 self._console.writenl('_'*60)
1151
1153 fname = subjectrc('pypestate.%s' % self._gethostname())
1154 if (posixpath.exists(fname) and
1155 ask('migrate_pypestate', 'Automigrate %s?' %
1156 fname, ['yes', 'no']) == 0):
1157 try:
1158 d = cPickle.load(open(fname, 'r'))
1159 self.tallycount = d['tallycount']
1160 self._tally()
1161
1162
1163 xgain = d['eye_xgain'] * self.rig_common.queryv('mon_h_ppd')
1164 ygain = d['eye_ygain'] * self.rig_common.queryv('mon_v_ppd')
1165 xoff = d['eye_xoff']
1166 yoff = d['eye_yoff']
1167
1168
1169
1170
1171 self.eyeset(xgain=xgain, ygain=ygain, xoff=xoff, yoff=yoff)
1172 self.clearaffine()
1173
1174 fname2 = fname+'.obsolete'
1175 os.rename(fname, fname2)
1176
1177 warn(MYNAME(), 'Success; remove %s at will.' % fname2)
1178 except:
1179 reporterror(gui=False, db=self.config.iget('DBERRS'))
1180 warn(MYNAME(), 'failed!')
1181
1182 - def con(self, msg=None, color='black', nl=1):
1183 """Write message to *console* window.
1184
1185 *Console* is running log window
1186
1187 :param msg: (string) message to print
1188
1189 :param color: (string) color to use
1190
1191 :param nl: (boolean) add newline at end?
1192
1193 :return: nothing
1194
1195 """
1196 if msg is None:
1197 self._console.clear()
1198 else:
1199 if nl:
1200 self._console.writenl(msg, color)
1201 else:
1202 self._console.write(msg, color)
1203
1204 - def info(self, msg=None, color='black', nl=1):
1205 """Write message to *info* window.
1206
1207 *Info* is the window that gets cleared at the start of each trial.
1208
1209 :param msg: (string) message to print
1210
1211 :param color: (string) color to use
1212
1213 :param nl: (boolean) add newline at end?
1214
1215 :return: nothing
1216
1217 """
1218 if msg is None:
1219 self._info.clear()
1220 else:
1221 if nl:
1222 self._info.writenl(msg, color)
1223 else:
1224 self._info.write(msg, color)
1225
1227 try:
1228 fn(self)
1229 finally:
1230 self.showtestpat()
1231
1233 import filebox
1234 (file, mode) = filebox.Open(initialdir=os.getcwd(),
1235 pattern='*.[0-9][0-9][0-9]',
1236 initialfile='', datafiles=1)
1237 if file:
1238 pf = PypeFile(file)
1239 rec = pf.nth(0)
1240 if rec:
1241 s = 'from: %s\n\n' % file
1242 avoid = self.sub_common.keys() + \
1243 self.rig_common.keys() + self.ical.keys()
1244 keys = rec.params.keys()
1245 keys.sort()
1246 for p in keys:
1247 if p.endswith('_raw_'):
1248 p = p[:-5]
1249 if not p in avoid:
1250 s = s + '%s = %s\n' % (p, rec.params[p])
1251 warn('task params', s, astext=1)
1252
1254 s = Entry_(self.tk, 'find parameter:', '').go()
1255 if s:
1256 results = ParamTable(None, None).find(s)
1257 if not len(results):
1258 self.con('%s: not found.' % s)
1259 else:
1260 self.con('found "%s":' % s)
1261 for (pt, slot, n) in results:
1262 d = pt._get(evaluate=0)
1263 try:
1264 self.con(' %s (row %d): %s=''%s'' %s' %
1265 (posixpath.basename(pt._file),
1266 n, slot, d[1][slot], type(d[1][slot]),))
1267 except KeyError:
1268
1269 pass
1270
1272 app = self
1273 sys.stderr.write('Dropping into keyboard shell\n')
1274 sys.stderr.write('"app" should be defined!\n')
1275 keyboard()
1276
1277 - def pause(self, state=None):
1278 """Call this to automatically pause task if user has requested pause.
1279
1280 """
1281 if state is None:
1282 if self._paused:
1283 self._paused = False
1284 warn(MYNAME(), 'Task paused; close to continue', wait=1)
1285 else:
1286 self._paused = state
1287 if state:
1288 self.con("[pause @ trial end]", color='red')
1289
1291 """Setting running state flags.
1292
1293 Use this instead of tweaking internal vars in the app object!
1294
1295 """
1296 if not (running is -1):
1297 self.running = running
1298
1299 if self.running:
1300 try:
1301 self._recfile.oldbg
1302 except AttributeError:
1303 self._recfile.oldbg = self._recfile.cget('bg')
1304 self._recfile.configure(bg='lightgreen')
1305 else:
1306 try:
1307 self._recfile.configure(bg=self._recfile.oldbg)
1308 except AttributeError:
1309 pass
1310
1312 barstate = self.bardown()
1313 if self.itribe:
1314 t = self.itribe.status + ' '
1315 else:
1316 t = ''
1317
1318 if not self.udpy.isvisible():
1319
1320 if barstate:
1321 t = t+'BAR:DN '
1322 else:
1323 t = t+'BAR:UP '
1324 else:
1325 if barstate:
1326 self.udpy.set_bar_indic('DOWN')
1327 else:
1328 self.udpy.set_bar_indic(' UP ')
1329
1330 if dacq_jsbut(-1):
1331 t = t + " "
1332 for n in range(10):
1333 if dacq_jsbut(n):
1334 t = t+('%d'%n)
1335 else:
1336 t = t+'.'
1337
1338 try:
1339 last = self._last_stateinfo
1340 except AttributeError:
1341 last = None
1342
1343 if self.mldown is not None:
1344 t = t + '%dml %s' % (self.mldown, t)
1345
1346 if not last == t:
1347 self._stateinfo.configure(text=t)
1348 self._last_stateinfo = last
1349
1351 """Query to see if a task is running.
1352
1353 :return: (bool)
1354
1355 """
1356 return self.running
1357
1359 """Save the result of current trial.
1360
1361 This should be called at the end of each trial to update
1362 the history stack.
1363
1364 :param type: if type is None, clear saved results, otherwise
1365 type should be something like 'C' or 'E' to indicate
1366 the trial result flag.
1367
1368 """
1369
1370 if type is None:
1371 self._results = []
1372 self._history()
1373 else:
1374 self._results.append(type)
1375 self._history(type[0])
1376 s1 = self._tally(type=type)
1377 s2 = self._runstats_update(resultcode=type)
1378
1380 """Get the nth to last saved trial result code.
1381
1382 *NB* get_result(1) is result from last trial, not get_result(0)
1383
1384 """
1385 if len(self._results):
1386 return self._results[-nback]
1387 else:
1388 return None
1389
1390 - def _tally(self, type=None, clear=None, cleartask=None, reward=None):
1391 ctask = self.task_name
1392 if not clear is None:
1393
1394 self.tallycount = {}
1395 elif not cleartask is None:
1396
1397 for (task,type) in self.tallycount.keys():
1398 if ctask == task:
1399 del self.tallycount[(task,type)]
1400 elif not type is None:
1401
1402 try:
1403 self.tallycount[ctask,type] = self.tallycount[ctask,type] + 1
1404 except KeyError:
1405 self.tallycount[ctask,type] = 1
1406 try:
1407 del self.tally_recent[0]
1408 self.tally_recent.append(type)
1409 except AttributeError:
1410 self.tally_recent = [''] * 5
1411 elif not reward is None:
1412 try:
1413 self.tallycount['reward_n'] += 1
1414 self.tallycount['reward_ms'] += reward
1415 except KeyError:
1416 self.tallycount['reward_n'] = 1
1417 self.tallycount['reward_ms'] = reward
1418
1419 ks = self.tallycount.keys()
1420 ks.sort()
1421
1422 (ntot, ncorr, s) = (0, 0, '')
1423 keys = self.tallycount.keys()
1424 keys.sort()
1425 for k in keys:
1426 if not len(k) == 2: continue
1427
1428 (task, type) = k
1429 d = type.split()
1430 if len(d) > 1:
1431 d = "%s (%s)" % (d[0], string.join(d[1:]))
1432 else:
1433 d = d[0]
1434 s = s + '%s %s: %d\n' % (task, d, self.tallycount[(task,type)])
1435 if d[0][0] == 'C':
1436 ncorr = ncorr + self.tallycount[(task,type)]
1437 ntot = ntot + self.tallycount[(task,type)]
1438 s = s + '\nncorr: %d trials' % ncorr
1439 s = s + '\ntotal: %d trials' % ntot
1440 s = s + '\n'
1441
1442 try:
1443 rn = self.tallycount['reward_n']
1444 rms = self.tallycount['reward_ms']
1445 except KeyError:
1446 rn = 0
1447 rms = 0
1448 s = s + '\nreward: %d drops (%d ms)' % (rn, rms,)
1449 ml = self.sub_common.queryv('mldropsize')
1450 if ml > 0:
1451 self.mldown = rn * ml
1452 s = s + '\nestimated: %.1f ml' % (self.mldown,)
1453 else:
1454 self.mldown = None
1455
1456 s = s + '\n'
1457 s = s + '\n'
1458
1459 try:
1460 N = len(self.tally_recent)
1461 for n in range(N):
1462 if n == 0:
1463 s = s + 'History\n'
1464 s = s + '%d: %s\n' % (-(N-n), self.tally_recent[n])
1465 except AttributeError:
1466 pass
1467
1468 self.tallyw.clear()
1469 self.tallyw.write(s)
1470
1471 self.last_tally = s
1472
1473 return s
1474
1476 """Get common params, extend with eyecoil settings.
1477
1478 """
1479 d = self.rig_common.check()
1480 d = self.sub_common.check(mergewith=d)
1481 d = self.ical.check(mergewith=d)
1482
1483
1484 d['@eye_xgain'] = self.ical.queryv('xgain_')
1485 d['@eye_ygain'] = self.ical.queryv('ygain_')
1486 d['@eye_xoff'] = self.ical.queryv('xoff_')
1487 d['@eye_yoff'] = self.ical.queryv('yoff_')
1488 d['@eye_rot'] = self.ical.queryv('rot_')
1489
1490 d['@pwd'] = os.getcwd()
1491 d['@host'] = self._gethostname()
1492
1493
1494
1495 if self.xdacq:
1496 d['datasrc'] = self.xdacq
1497 else:
1498 d['datasrc'] = 'None'
1499
1500 return d
1501
1503
1504 self.tasklist = {}
1505
1506 self.add_tasks(menubar, '~pyperc', pyperc('Tasks'))
1507
1508
1509 files = glob.glob(pyperc('Tasks/*'))
1510 for d in files:
1511 m = posixpath.basename(d)
1512
1513 if os.path.isdir(d) and not (m[0] == '_'):
1514 self.add_tasks(menubar, "~"+m, d)
1515
1516
1517 self.add_tasks(menubar, 'cwd', '.')
1518
1519
1520 taskpathlist = []
1521 if self.config.get('TASKPATH', None):
1522 taskpathlist.append(self.config.get('TASKPATH', None))
1523
1524 for p in taskpathlist:
1525 for d in p.split(':'):
1526 if len(posixpath.basename(d)) == 0:
1527 d = d[:-1]
1528 try:
1529 self.add_tasks(menubar, posixpath.basename(d), d)
1530 except ValueError:
1531 Logger("pype: skipped dup %s in TASKPATH\n" % d)
1532
1534
1535
1536
1537 majik = re.compile('^#pypeinfo;.*')
1538 f = open(filename, 'r')
1539 lines = f.readlines(80*50)
1540 f.close()
1541 d = {}
1542 for l in lines:
1543 if majik.search(l):
1544 for w in l.split(';')[1:]:
1545 if ':' in w:
1546 (tag, val) = w.split(':')
1547 d[string.strip(tag)] = string.strip(val)
1548 else:
1549 d[string.strip(w)] = 1
1550 return d
1551
1553 try:
1554 self._loadmenu.deletemenu('Recent')
1555 except KeyError:
1556 pass
1557 self._loadmenu.addmenu('Recent', '', '')
1558 for (t, d) in self.recent:
1559 self._loadmenu.addmenuitem('Recent', 'command',
1560 font=('Andale Mono', 10),
1561 label=t,
1562 command=lambda s=self,t=t,d=d: \
1563 s.loadtask(t, d))
1564
1565 - def add_tasks(self, menubar, menulabel, dirname):
1566 tasks = []
1567 taskdescrs = {}
1568
1569 dirname = os.path.join(dirname, '')
1570 filelist = glob.glob(os.path.join(dirname, '*.py'))
1571
1572 for fname in filelist:
1573 d = self._get_taskheader(fname)
1574 if 'task' in d:
1575 tasks.append(posixpath.basename(fname)[:-3])
1576 if 'descr' in d:
1577 taskdescrs[tasks[-1]] = '%s' % d['descr']
1578 else:
1579 taskdescrs[tasks[-1]] = ''
1580 if len(tasks) == 0:
1581 if debug():
1582 Logger('-tasks from: %s\n' % dirname)
1583 return
1584 else:
1585 if debug():
1586 Logger('+tasks from: %s\n' % dirname)
1587 for t in tasks:
1588 Logger(' %s\n' % t)
1589
1590
1591 _addpath(dirname, atend=1)
1592
1593 tasks.sort()
1594
1595 menubar.addmenu(menulabel, '', '', direction=RIGHT)
1596 menubar.addmenuitem(menulabel, 'command',
1597 label=dirname, foreground='blue')
1598 menubar.addmenuitem(menulabel, 'command', label='Reload current',
1599 command=self.loadtask)
1600 menubar.addmenuitem(menulabel, 'separator')
1601 for t in tasks:
1602 if t in self.tasklist:
1603
1604 c = 'blue'
1605 else:
1606 c = 'black'
1607 self.tasklist[t] = 1
1608 tasklabel = '%-15s %s' % (t, taskdescrs[t])
1609 menubar.addmenuitem(menulabel, 'command', label=tasklabel,
1610 font=('Andale Mono', 10),
1611 foreground=c,
1612 command=lambda s=self,t=t,d=dirname:
1613 s.loadtask(t, d))
1614 menubar.addmenuitem(menulabel, 'separator')
1615 menubar.addmenuitem(menulabel, 'command', label='Reload current',
1616 command=self.loadtask)
1617
1641
1643 if len(self.recent) > 1:
1644 (name, dir) = self.recent[1]
1645 self.loadtask(name, dir)
1646
1648 if self.taskmodule:
1649 self.set_startfn(None)
1650 if hasattr(self.taskmodule, 'cleanup'):
1651 self.taskmodule.cleanup(self)
1652 del self.taskmodule
1653 self.taskmodule = None
1654 for w in self.disable_on_start:
1655 w.config(state=DISABLED)
1656
1657 - def loadtask(self, taskname=None, path=None):
1658 """(Re)load task from file.
1659
1660 Load a task, if no task is specified, try to reload
1661 current task.
1662
1663 :param taskname: (string) task name without .py suffice
1664
1665 :param path: (string) directory where task is stored
1666
1667 :return: None for error, task module on success
1668
1669 """
1670 import imp
1671
1672 if self.isrunning():
1673
1674 return
1675
1676 if taskname is None:
1677 if self.task_name is None:
1678 return None
1679 taskname = self.task_name
1680 path = self.task_dir
1681
1682 try:
1683 if path:
1684 (file, pathname, descr) = imp.find_module(taskname, [path])
1685 else:
1686 path = posixpath.dirname(taskname)
1687 if len(path) == 0:
1688 path = None
1689 (file, pathname, descr) = imp.find_module(taskname)
1690 else:
1691 taskname = posixpath.basename(taskname)
1692 if taskname[-3:] == '.py':
1693 taskname = taskname[:-3]
1694 (file, pathname, descr) = imp.find_module(taskname, [path])
1695 except ImportError:
1696 warn(MYNAME(),
1697 "Can't find task '%s' on search path.\n" % \
1698 taskname + "Try specifying a full path!")
1699 return None
1700
1701 key = (taskname, path,)
1702 if key in self.recent:
1703 self.recent.remove(key)
1704 self.recent = [key] + self.recent
1705 self.recent = self.recent[:5]
1706 self.make_recent()
1707 self.unloadtask()
1708
1709 try:
1710 try:
1711 Logger("pype: loaded '%s' (%s)\n" % (taskname, pathname))
1712 taskmod = imp.load_module(taskname, file, pathname, descr)
1713 mtime = os.stat(pathname).st_mtime
1714 except:
1715 err = ('Error loading ''%s'' -- \n' % taskname) + get_exception()
1716 sys.stderr.write(err)
1717 warn(MYNAME(), err, wait=0, astext=1)
1718 return None
1719
1720 finally:
1721
1722 if file:
1723 file.close()
1724
1725 if path is None:
1726 path, base = os.path.split(pathname)
1727
1728 if taskmod:
1729 self._task_taskname = taskname
1730 self._task_dir = path
1731
1732 self.taskmodule = taskmod
1733 self._taskname(taskname, path)
1734 self._task_pathname = pathname
1735 self._task_mtime = mtime
1736
1737 if hasattr(self.taskmodule, 'main'):
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748 self.taskmodule.main(self)
1749 else:
1750 Logger("pype: warning -- no 'main' in %s\n" % taskname)
1751
1752 if self._startfn:
1753 for w in self.disable_on_start:
1754 w.config(state=NORMAL)
1755 else:
1756 warn(MYNAME(), 'no start function set!')
1757
1758 return taskmod
1759
1761 """
1762 Set function to call when unbound key is pressed in the
1763 udy canvas window. Function should take three arguments,
1764 e.g., **def hookfn(data, key, ev):...**.
1765
1766 Where data is whatever you want the function to have
1767 and key is the string containing the key pressed and
1768 ev is the full event, in case you want (x,y) etc.
1769
1770 The hook function should return 1 or 0 to indicate if
1771 the keypress was actually consumed.
1772
1773 This function returns the old hookfn and hookdata values so
1774 they can be saved and restored.
1775
1776 """
1777 oldfn = self.udpy.userhook
1778 olddata = self.udpy.userhook_data
1779 self.udpy.userhook = fn
1780 self.udpy.userhook_data = data
1781 return (oldfn, olddata)
1782
1785
1787 """Clear eyecal gains.
1788
1789 :return: nothing
1790
1791 """
1792 self.eyeset(xgain=1.0, ygain=1.0)
1793
1795 """Clear eyecal affine transform (set to identity matrix).
1796
1797 :return: nothing
1798
1799 """
1800 self.eyeset(affine=np.identity(3))
1801
1802 - def eyeset(self, xgain=None, ygain=None, xoff=None, yoff=None,
1803 rot=None, affine=None):
1804 """Update eye tracking params from pype -> comedi_server
1805
1806 :param xgain, ygain: (float)
1807
1808 :param xoff, yoff: (int)
1809
1810 :param rot: (float)
1811
1812 :param affine: (3x3 matrix)
1813
1814 :return: nothing
1815
1816 """
1817
1818 if not xgain is None: self.ical.set('xgain_', '%f' % xgain)
1819 if not ygain is None: self.ical.set('ygain_', '%f' % ygain)
1820 if not xoff is None: self.ical.set('xoff_', '%d' % xoff)
1821 if not yoff is None: self.ical.set('yoff_', '%d' % yoff)
1822 if not rot is None: self.ical.set('rot_', '%f' % rot)
1823 if not affine is None:
1824
1825 a = string.join(map(lambda f: '%g'%f, affine.flatten()), ',')
1826 self.ical.set('affinem_', a)
1827
1828 dacq_eye_smooth(self.rig_common.queryv('eye_smooth'))
1829 dacq_fixbreak_tau_ms(self.rig_common.queryv('fixbreak_tau_ms'))
1830
1831 self._xg = self.ical.queryv('xgain_')
1832 self._yg = self.ical.queryv('ygain_')
1833 self._xo = -self.ical.queryv('xoff_')
1834 self._yo = -self.ical.queryv('yoff_')
1835 self._rot = self.ical.queryv('rot_')
1836
1837 dacq_eye_params(self._xg, self._yg, self._xo, self._yo, self._rot)
1838
1839
1840
1841
1842
1843
1844
1845
1846 try:
1847 A = map(float, self.ical.query('affinem_').split(','))
1848 A = np.array(A).reshape(3,3)
1849 for r in range(3):
1850 for c in range(3):
1851 dacq_eye_setaffine_coef(r, c, A[r,c])
1852 except:
1853 warn(MYNAME(), 'affine matrix should be 9-element vector')
1854
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865 if sys.platform.startswith('linux'):
1866 if self.config.iget('PUPILXTALK', default=-666) != -666:
1867 Logger("pype: change PUPILXTALK to EYELINK_XTALK "
1868 "in Config file\n")
1869 raise PypeStartupError
1870
1871 eyelink_opts = self.config.get('EYELINK_OPTS')
1872 if len(eyelink_opts) > 0:
1873 eyelink_opts = eyelink_opts + ':'
1874 eyelink_opts = eyelink_opts + ('pupil_crosstalk_fixup=%s' %
1875 self.config.get('EYELINK_XTALK'))
1876 eyelink_opts = eyelink_opts + ':active_eye=both'
1877 eyelink_opts = eyelink_opts + ':link_sample_data=PUPIL,AREA'
1878 eyelink_opts = eyelink_opts + ':heuristic_filter=0 0'
1879 eyelink_opts = eyelink_opts + ':pup_size_diameter=NO'
1880
1881 s = dacq_start('comedi_server',
1882 '--tracker=%s' % self.config.get('EYETRACKER'),
1883 '--port=%s' % self.config.get('EYETRACKER_DEV'),
1884 '--elopt="%s"' % eyelink_opts,
1885 '--elcam=%s' % self.config.get('EYELINK_CAMERA'),
1886 '--swapxy=%d' % self.config.iget('SWAP_XY'),
1887 '--usbjs=%s' % self.config.get('USB_JS_DEV'), 0)
1888
1889
1890
1891
1892 if s <= 0:
1893 Logger("init_dacq: comedi_server can't start.\n")
1894 Logger("init_dacq: try running 'pypekill' or 'pypekill -s'\n")
1895 raise PypeStartupError
1896
1897 self.u3test = False
1898
1899 if self.u3test:
1900
1901
1902
1903
1904
1905
1906
1907 try:
1908 import labjack
1909 Logger("pype: found LabJack drivers\n")
1910 lj = True
1911 except ImportError:
1912 Logger("pype: LabJack drivers not installed.\n")
1913 lj = False
1914
1915 if lj:
1916 try:
1917 self.u3 = labjack.SamplerU3()
1918 Logger("pype: labjack %s H:v%s F:v%s\n" % \
1919 (self.u3.config['DeviceName'],
1920 self.u3.config['HardwareVersion'],
1921 self.u3.config['FirmwareVersion'],))
1922 self.u3.count = 0
1923 except:
1924 self.u3 = None
1925 Logger("pype: can't find a labjack.\n")
1926
1927 if lj and self.u3:
1928
1929 self.u3.start()
1930 time.sleep(0.01)
1931 self.u3.stop(wait=1)
1932
1934 sx = self.config.get('SYNCX', None)
1935 if not sx is None:
1936 sx = int(sx)
1937 sy = self.config.get('SYNCY', None)
1938 if not sy is None:
1939 sy = int(sy)
1940
1941 self.fb = FrameBuffer(self.config.get('SDLDPY'),
1942 self.config.iget('DPYW'),
1943 self.config.iget('DPYH'),
1944 self.config.iget('FULLSCREEN'),
1945 syncsize=self.config.iget('SYNCSIZE'),
1946 syncx=sx, syncy=sy,
1947 synclevel=self.config.iget('SYNCLEVEL'),
1948 mouse=self.config.iget('MOUSE'),
1949 fbw=self.config.iget('FBW'),
1950 fbh=self.config.iget('FBH'),
1951 xscale=self.config.fget('XSCALE'),
1952 yscale=self.config.fget('YSCALE'),
1953 app=self)
1954
1955 fps = self.fb.calcfps(duration=250)
1956
1957 self.fb.app = self
1958 gstr = self.config.get('GAMMA')
1959 g = map(float, gstr.split(','))
1960 if len(g) == 1:
1961 g = [g[0], g[0], g[0]]
1962 if self.fb.set_gamma(g[0], g[1], g[2]):
1963 Logger("pype: gamma set to (%.1f,%.1f,%.1f)\n" % tuple(g))
1964 else:
1965 Logger("pype: warning, no support for gamma correction\n")
1966
1967 if sys.platform.startswith('linux'):
1968
1969 d = self.config.get('SDLDPY')
1970 if os.system("xset -display %s b off" % d):
1971 Logger("Can't run xset to turn off bell!\n")
1972 if os.system("xset -display %s s off" % d):
1973 Logger("Can't run xset to turn off screen saver!\n")
1974 if os.system("xset -display %s -dpms" % d):
1975 Logger("Can't run xset to turn off DPMS!\n")
1976
1977 return fps
1978
1980 """Safe version of socket.gethostname().
1981
1982 :return: first part of socket.gethostname(), if a hostname is
1983 actually defined, otherwise returns 'NOHOST'.
1984
1985 """
1986 try:
1987 if fqdn:
1988 return socket.getfqdn()
1989 else:
1990 return socket.gethostname().split('.')[0]
1991 except:
1992 return 'no-host'
1993
1995 """load/save tallyfile
1996
1997 :return: age of last tallyfile in days
1998
1999 """
2000
2001 fname = subjectrc('tally.%s.%s' % (subject(), self._gethostname(),))
2002 if int(self.sub_common.query('acute')):
2003 age = -1
2004 else:
2005 try:
2006 age = (time.time()-os.path.getmtime(fname)) / (60.*60.*24.)
2007 except OSError:
2008 age = -1
2009 try:
2010 if save:
2011 cPickle.dump(self.tallycount, open(fname, 'w'))
2012 else:
2013 if age < 0.5 or ask('loadstate', 'Clear old tally data?',
2014 ['yes', 'no']) == 1:
2015 self.tallycount = cPickle.load(open(fname, 'r'))
2016 except IOError:
2017 Logger("Can't read/write tally file.\n")
2018
2019 return age
2020
2022 self.setgeo(save=1)
2023 self.rig_common.save()
2024 self.sub_common.save()
2025 self.ical.save()
2026 self._tallyfile(save=1)
2027
2029
2030 self._tallyfile(save=0)
2031
2033 if clear is not None:
2034 self._runstats = {}
2035 self._runstats['ncorrect'] = 0
2036 self._runstats['nerror'] = 0
2037 self._runstats['nui'] = 0
2038 elif resultcode is not None:
2039 r = resultcode[0]
2040
2041 if r in 'C':
2042 self._runstats['ncorrect'] = self._runstats['ncorrect'] + 1
2043 self._runstats['nui'] = 0
2044 elif r in 'EM':
2045 self._runstats['nerror'] = self._runstats['nerror'] + 1
2046 self._runstats['nui'] = 0
2047 elif r in 'U':
2048 self._runstats['nui'] = self._runstats['nui'] + 1
2049
2050 nmax = self.sub_common.queryv('max_trials')
2051 n = self._runstats['ncorrect'] + self._runstats['nerror']
2052 if (nmax > 0) and n > nmax:
2053 self.set_state(running=0)
2054 warn(MYNAME(),
2055 '%d total trials reached -- stopping.' % n, wait=0)
2056
2057 nmax = self.sub_common.queryv('max_correct')
2058 n = self._runstats['ncorrect']
2059 if (nmax > 0) and n > nmax:
2060 self.set_state(running=0)
2061 warn(MYNAME(),
2062 '%d correct trials reached -- stopping.' % n, wait=0)
2063
2064 nmax = self.sub_common.queryv('max_ui')
2065 n = self._runstats['nui']
2066 if (nmax > 0) and n > nmax:
2067 self.set_state(running=0)
2068 warn(MYNAME(),
2069 '%d sequential UI trials -- stopping.' % n, wait=0)
2070
2071 ne = self._runstats['nerror']
2072 nc = self._runstats['ncorrect']
2073 nu = self._runstats['nui']
2074 nt = nc + ne
2075 s = string.join((
2076 ' running = %d' % (self.isrunning(),),
2077 '----------------------------------',
2078 'Corrects = %d [%d]' % (nc, self.sub_common.queryv('max_correct'),),
2079 ' Errors = %d' % (ne,),
2080 ' UIs = %d [%d]' % (nu, self.sub_common.queryv('max_ui'),),
2081 ' Trials = %d [%d]' % (nt, self.sub_common.queryv('max_trials'),),
2082 '----------------------------------'), '\n')
2083 self.statsw.configure(text=s)
2084 return s
2085
2087 self._start_helper(temp=None)
2088
2090 self._start_helper(temp=1)
2091
2093 self._doabort = 1
2094 self.running = 0
2095
2097 if self.running:
2098 for w in self.enable_on_start:
2099 w.config(state=DISABLED)
2100 self.running = 0
2101 else:
2102 if (os.stat(self._task_pathname).st_mtime <> self._task_mtime and
2103 ask('run task', 'Task has changed\nRun anyway?',
2104 ['yes', 'no']) == 1):
2105 return
2106
2107 self._allowabort = 1
2108
2109 if self._validatefn:
2110
2111 if not self._validatefn(self):
2112 return
2113
2114 try:
2115 self._savestate()
2116 self._loadmenu.disableall()
2117 for w in self.disable_on_start:
2118 w.config(state=DISABLED)
2119 for w in self.enable_on_start:
2120 w.config(state=NORMAL)
2121
2122 if temp:
2123 if self.sub_common.queryv('save_tmp'):
2124 fname = './%s.tmp' % self.uname
2125 if posixpath.exists(fname):
2126 posix.unlink(fname)
2127 else:
2128 fname = '/dev/null'
2129 self._record_selectfile(fname)
2130 else:
2131 if self._record_selectfile() is None:
2132 Logger('pype: run aborted\n')
2133 return
2134
2135
2136
2137
2138
2139
2140 if self.use_elog and not temp:
2141 import elogapi
2142 (ok, ecode) = elogapi.AddDatafile(
2143 self._exper,
2144 self.sub_common.queryv('full_subject'),
2145 self.uname,
2146 self.record_file,
2147 self.task_name,
2148 force=1)
2149 if not ok:
2150 warn(MYNAME(), ecode)
2151
2152
2153 self.con()
2154
2155 if self.xdacq == 'plexon':
2156 warn('pype:_start_helper:xdacq',
2157 'Start plexon now', wait=1)
2158 elif self.xdacq == 'tdt':
2159
2160
2161 (server, tank, block) = self.tdt.newblock(record=1)
2162 Logger('pype: tdt data = %s %s\n' % (tank, block))
2163 self.con('tdt data = %s %s\n' % (tank, block))
2164
2165
2166 self.set_result()
2167
2168
2169 self.update_rt()
2170 self.update_psth()
2171 self._runstats_update(clear=1)
2172
2173
2174 if self.psych:
2175 self.fb.screen_open()
2176
2177
2178 self.set_state(running=1)
2179 self.warn_run_start()
2180 self._startfn(self)
2181 except:
2182 reporterror(dbug=self.config.iget('DBERRS'))
2183 finally:
2184 self.record_done()
2185 self.set_state(running=0)
2186 self.warn_run_stop()
2187 if self.psych:
2188 self.fb.screen_close()
2189
2190
2191
2192
2193 dacq_set_pri(0)
2194 dacq_set_mypri(0)
2195 dacq_set_rt(0)
2196 for w in self.disable_on_start:
2197 w.config(state=NORMAL)
2198 for w in self.enable_on_start:
2199 w.config(state=DISABLED)
2200 self.showtestpat()
2201 self._allowabort = 0
2202 self._loadmenu.enableall()
2203
2204 if self.xdacq == 'plexon':
2205 warn(MYNAME(),
2206 'Stop plexon now', wait=0)
2207 elif self.xdacq == 'tdt':
2208
2209 self.tdt.newblock(record=0)
2210
2212 """Get number of correct (C) trials since run start
2213
2214 :return: (int) C count
2215
2216 """
2217 return self._runstats['ncorrect']
2218
2220 """Get number of errors (E,M) trials since run start
2221
2222 :return: (int) E|M count
2223
2224 """
2225 return self._runstats['nerror']
2226
2228 """Get number of (sequential) uninitiated (U) trials since run start
2229
2230 :return: (int) UI count
2231
2232 """
2233
2234 return self._runstats['nui']
2235
2237 """Get number of trials since run start.
2238
2239 Number of trials is sum of C, E and M types (corrects, errors
2240 and maxrt_exceeded, typically). Doesn't count UI or USERABORT
2241 trials.
2242
2243 :return: (int) trial count
2244
2245 """
2246 return self._runstats['ncorrect'] + self._runstats['nerror']
2247
2249 if self.use_elog:
2250 warn(MYNAME(),
2251 'Use elog "Edit>New Experiment" and then File>Save.')
2252 else:
2253 try:
2254 n = int(self.sub_common.queryv('cell'))
2255 n = n + 1
2256 self.sub_common.set('cell', "%d" % n)
2257 except ValueError:
2258 warn(MYNAME(),
2259 'cell field is non-numeric, can''t increment.')
2260
2261 - def set_startfn(self, startfn, updatefn=None, validatefn=None):
2262 """Set start run function/hook.
2263
2264 The task_module.main() function must call this function to bind
2265 the startup function for the task. The main function can optionall
2266 also bind an update function that gets called after each trial. The
2267 update function can be used to update task-specific plots etc.
2268
2269 The startfn must be a function that takes at least one argument,
2270 namely the PypeApp struction that acts as a handle for everything
2271 else (aka 'app'). Additional parameters can be included by using
2272 a lambda expression, for example:
2273
2274 >>> t = Task(...)
2275 >>> app.set_startfn(lambda app, mytask=t: mystart(app, mytask))
2276
2277 The return value from the start function is ignored.
2278
2279 The updatefn is similar. It should be a function takes at least
2280 two parameters, first is 'app', second either None or a tuple.
2281 None for a pre-run initialization stage and a tuple with
2282 the info provided to record_write(), i.e.:
2283
2284 >>> resultcode, rt, params, taskinfo = info
2285
2286 You can do whatever you want, but if you are overriding the
2287 built-in RT histogram stuff (in which case it might be useful
2288 to know that app.rtdata contains a list of all the RTs for the
2289 current run) the updater shoudl return False. If it returns
2290 True, the built-in histogram will get updated IN ADDITION to
2291 whatever you did..
2292
2293 validatefn (new 10/4/2013) is called before the run actually
2294 starts to give task chance to validate parameters, cache stimuli
2295 etc. Should return 'True' if pype should continue on and start
2296 task, 'False' to abort run.
2297
2298 """
2299
2300 self._startfn = startfn
2301 self._updatefn = updatefn
2302 self._validatefn = validatefn
2303
2305 """Internal shutdown function.
2306
2307 Sets the terminate flag in this instance of PypeApp. This
2308 will eventually cause the PypeApp to exit, once the current
2309 run is stopped.
2310
2311 Should *never* be called by user/application directly.
2312
2313 """
2314 if self.running:
2315 if self.tk:
2316 self.tk.bell()
2317 else:
2318 Logger('pype: terminal save state.\n')
2319 self._savestate()
2320 Logger('pype: user shutdown/close -- terminating.\n')
2321 if self.server:
2322 self.server.shutdown()
2323 self.terminate = 1
2324
2326 """Get current timestamp.
2327
2328 :return: (ms) current 'time', as determined by the DACQ module.
2329
2330 """
2331 return dacq_ts()
2332
2334 try:
2335 tag = self.trialtag
2336 except AttributeError:
2337 self.trialtag = 0
2338 tag = self.trialtag
2339
2340 if reset is None:
2341 ts = self.encode('TRIAL_TAG')
2342 self.con('[trial tagged @ %d]' % ts)
2343 self.trialtag = 1
2344 else:
2345 self.trialtag = 0
2346 return tag
2347
2349 """Query current eye position (last sample and time of sample)
2350
2351 :return: measurement_time_ms, xpos_pix, ypos_pix
2352
2353 """
2354
2355 ts = dacq_ts()
2356 if (self._last_eyepos) is None or (ts > self._last_eyepos):
2357 self._eye_x = dacq_eye_read(1)
2358 self._eye_y = dacq_eye_read(2)
2359 self._last_eyepos = ts
2360 return (self._last_eyepos, self._eye_x, self._eye_y)
2361
2363 """Query current eye position (last sample, no time)
2364
2365 :return: xpos_pix, ypos_pix
2366
2367 """
2368 (t, x, y) = self.eye_txy()
2369 return (x, y)
2370
2372 """Tell pype where the monkey's supposed to be looking.
2373
2374 This is crucial for the F8 key -- when you hit F8 telling
2375 pype to rezero the eyeposition, it assumes that the monkey
2376 is looking at this location (in pixels) when you strike the
2377 key.
2378
2379 :param x: (int or FixWin) - x position (pix) or a fixwin object
2380
2381 :param y: (int) - y position (pix); only if x is not a fixwin!
2382
2383 :return: nothing
2384
2385 """
2386 if x is None and y is None:
2387 x = y = 0
2388 elif y is None:
2389
2390 x, y, sz = x.get()
2391 self._eyetarg_x = x
2392 self._eyetarg_y = y
2393
2394 - def eyeshift(self, x=0, y=0, reset=False, zero=False, rel=True):
2395 """Adjust X & Y offsets to set DC-offsets for eye position.
2396
2397 - if (reset) --> set x/y offsets to (0,0)
2398
2399 - if (zero) --> set x/y offset to current gaze position
2400
2401 - otherwise --> shift the offsets by (x*xincr, y*yincr); use a
2402 lambda expression if you want to shift in a callback
2403 function to set the options.
2404
2405 """
2406 if reset:
2407 x = 0
2408 y = 0
2409 elif zero:
2410 (x0, y0) = (dacq_eye_read(1), dacq_eye_read(2))
2411 x = float(self.ical.queryv('xoff_')) + x0 - self._eyetarg_x
2412 y = float(self.ical.queryv('yoff_')) + y0 - self._eyetarg_y
2413 elif rel:
2414 x = float(self.ical.queryv('xoff_')) + x
2415 y = float(self.ical.queryv('yoff_')) + y
2416 else:
2417
2418 (x0, y0) = (dacq_eye_read(1), dacq_eye_read(2))
2419 x = float(self.ical.queryv('xoff_')) + x0 - x
2420 y = float(self.ical.queryv('yoff_')) + y0 - y
2421 self.eyeset(xoff=x, yoff=y)
2422 self.encode(EYESHIFT)
2423
2424 - def mainloop(self):
2425 while not self.terminate:
2426 try:
2427 self._allowabort = 0
2428 self.idlefn()
2429 except UserAbort:
2430 Logger('pype: mainloop caught abort\n')
2431 pass
2432 except FixBreak:
2433
2434
2435
2436 Logger('pype: stray fixbreak caught\n')
2437 pass
2438
2439
2440 - def idlefn(self, ms=None, update=1, fast=None):
2441 """Idle function -- call when there's nothing to do to update GUI.
2442
2443 The idle function -- this function should be call periodically
2444 by everything. Whenever the program's looping or busy waiting
2445 for something (bar to go up/down, timer to expire etc), the
2446 app should just call idlefn(). The optional ms arg will run
2447 the idle function for the indicated amount of time -- this is
2448 not accurate; it just uses the tk after() command, so it's
2449 X11 limited and only approximate.
2450
2451 This function is also responsible for monitoring the GUI's
2452 keyboard queue and handling key events. Right now only some
2453 basics are implemented -- give a drop of juice, open/close
2454 solenoid, run/stop etc.. more can be implemented and eventually
2455 there should be a way to set user/app specific keybindings
2456 just like the user buttons.
2457
2458 *NB* anywhere this is called you **MUST** catch the UserAbort
2459 exception!!!
2460
2461 """
2462 if self._post_fixbreak:
2463 self._post_fixbreak = 0
2464 raise FixBreak
2465 if self._post_bartransition:
2466 self._post_bartransition = 0
2467 raise BarTransition
2468 if self._post_joytransition:
2469 self._post_joytransition = 0
2470 raise JoyTransition
2471 if self._post_alarm:
2472 self._post_alarm = 0
2473 raise Alarm
2474
2475 if len(self.idle_queue):
2476 now = dacq_ts()
2477
2478 for (deadline, action) in self.idle_queue[:]:
2479 if (deadline - now) <= 0:
2480 self.idle_queue.remove((deadline, action))
2481 action()
2482
2483 if fast:
2484 return
2485
2486 if (not self.recording) and (self.plex is not None):
2487
2488
2489 tank, ndropped = self.plex.drain()
2490 if tank is None:
2491 Logger('pype: lost plexon signal.. this is bad..')
2492
2493 if self.tk is None:
2494 if not ms is None:
2495 t = Timer()
2496 while t.ms() < ms:
2497 pass
2498 elif ms is None:
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511 if dacq_jsbut(-1):
2512 if dacq_jsbut(4):
2513 if self.running:
2514 self._start_helper()
2515 elif dacq_jsbut(5):
2516 self.eyeshift(zero=1)
2517 elif dacq_jsbut(6) and dacq_jsbut(7):
2518 sys.stderr.write('JOYSTICK PARACHUTE DEPLOYED!\n')
2519 sys.exit(0)
2520
2521 if self._doabort and self._allowabort:
2522
2523
2524 self._doabort = 0
2525 raise UserAbort
2526
2527
2528 (c, ev) = self.tkkeyque.pop()
2529 if c:
2530 c = string.lower(c)
2531 if c == 'escape':
2532 if self._allowabort:
2533 self.encode(ABORT)
2534 self.con("[esc]", color='red')
2535 raise UserAbort
2536 elif c == 'f4':
2537 self.reward()
2538 elif c == 'f8':
2539 self.eyeshift(zero=1)
2540 elif c == 'f7' and self.running:
2541 self.dotrialtag()
2542 elif c == 'f1':
2543 self._findparam()
2544
2545
2546 if 'f8' in self.fb.checkkeys():
2547 self.eyeshift(zero=1)
2548 self.con('[f8]', color='red')
2549
2550 while not self.fb.checkkeys() == []:
2551 pass
2552 elif 'escape' in self.fb.checkkeys():
2553 if self._allowabort:
2554 self.encode(ABORT)
2555 self.con("[esc]", color='red')
2556
2557 while not self.fb.checkkeys() == []:
2558 pass
2559 raise UserAbort
2560
2561 if self.eyemouse:
2562
2563
2564
2565
2566
2567
2568 (mx, my, b1, b2, b3, lshift, rshift) = self.fb.cursorpos()
2569 if self._allowabort and lshift and rshift:
2570 self.con("stopping run", color='red')
2571 self.running = 0
2572 raise UserAbort
2573
2574
2575 dacq_set_xtracker(mx, my, 0)
2576 try:
2577
2578 if not b1 == self._lastb1:
2579 self.eyebar = b1
2580 self._lastb1 = b1
2581 self._int_handler(None, None, iclass=1, iarg=b1)
2582 except AttributeError:
2583
2584 self._lastb1 = b1
2585 self.eyebar = b1
2586
2587 while 0:
2588 key = self.fb.getkey()
2589 if key == 0:
2590 break
2591 elif key == 27 and self._allowabort:
2592 self.con("stopping run", color='red')
2593 self.running = 0
2594 raise UserAbort
2595 elif self.eyemouse and key == 32:
2596 self.eyebar = 1
2597 self._int_handler(None, None, iclass=1, iarg=0)
2598 elif self.eyemouse and key == -32:
2599 self.eyebar = 0
2600 self._int_handler(None, None, iclass=1, iarg=0)
2601
2602 x, y = self.eyepos()
2603 if (x is not None) and (y is not None):
2604 self.udpy.eye_at(x, y,
2605 barup=self.barup(),
2606 xt=self._show_xt_traces.get())
2607
2608 if update:
2609 self.tk.update()
2610
2611 if self.taskidle:
2612 self.taskidle(self)
2613
2614 self._show_stateinfo()
2615
2616 else:
2617 t = Timer()
2618 while t.ms() < ms:
2619 self.idlefn()
2620
2622 """Open solenoid to drain juicer.
2623
2624 """
2625 if not self.running:
2626 self._juice_on()
2627 w = warn(MYNAME(), 'Juicer is open!')
2628 self._juice_off()
2629
2630 - def _history(self, code=None):
2631 """
2632 Displays subject's recent history (based on result
2633 codes). Call with no args to clear/reset, string/char
2634 to push a trial onto the stack. Shouldn't be called
2635 by users; users should call set_result instead.
2636
2637 """
2638
2639 MAXHIST = 40
2640 if (code is None) or (len(code) == 0):
2641 self._histstr = ''
2642 else:
2643 self._histstr = (self._histstr + code[0])[-MAXHIST:]
2644
2645 if self.tk:
2646 self._hist.config(text='results: ' + self._histstr)
2647
2649 if self.sub_common.queryv('warningbeeps'):
2650 beep(500, 50, wait=0)
2651 beep(1000, 50, wait=0)
2652
2654 if self.sub_common.queryv('warningbeeps'):
2655 beep(1000, 50, wait=0)
2656 beep(500, 50, wait=0)
2657
2659 """Give positive (auditory+visual) feedback for correct response.
2660
2661 :param flash: (ms) duration of GREEN flash or None for no flash
2662
2663 :return: nothing
2664
2665 """
2666 beep(3000, 250, wait=0)
2667 if flash:
2668 self.fb.clear((0, 255, 0), flip=1)
2669 self.idlefn(flash)
2670 self.fb.clear((1, 1, 1), flip=1)
2671 pass
2672
2674 """Give negative (auditory+visual) feedback for error response.
2675
2676 :param flash: (ms) duration of RED flash or None for no flash
2677
2678 :return: nothing
2679
2680 """
2681 beep(500, 500, wait=0)
2682 if flash:
2683 self.fb.clear((255, 0, 0), flip=1)
2684 self.idlefn(flash)
2685 self.fb.clear((1, 1, 1), flip=1)
2686
2688 """Query dropsize.
2689
2690 :return: (ms) mean dropsize
2691
2692 """
2693 return self.sub_common.queryv('dropsize')
2694
2696 """Query dropsize variance.
2697
2698 :return: (ms) dropsize variance
2699
2700 """
2701 return self.sub_common.queryv('dropvar')
2702
2703 - def reward(self, dobeep=1, multiplier=1.0, ms=None):
2704 """Give reward.
2705
2706 Deliver a squirt of juice based on current dropsize and
2707 dropvar settings.
2708
2709 :return: (ms) actual size of delivered reward
2710
2711 """
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721 if ms is None:
2722 ms = int(round(multiplier * float(self.dropsize())))
2723 sigma = self.dropvar()**0.5
2724 else:
2725
2726 sigma = 0
2727
2728 if ms == 0:
2729 return
2730
2731 maxreward = self.sub_common.queryv('maxreward')
2732 minreward = self.sub_common.queryv('minreward')
2733
2734 if sigma > 0:
2735 while 1:
2736 t = nrand(mean=ms, sigma=sigma)
2737 if (t > minreward) and (t < maxreward):
2738 break
2739 if dobeep and self.reward_beep:
2740 beep(1000, 40, wait=0)
2741 thread.start_new_thread(self._reward_finisher, (t,))
2742 if self.tk:
2743 self.con("[ran-reward=%dms]" % t, color='black')
2744 actual_reward_size = t
2745 else:
2746 if dobeep and self.reward_beep:
2747 beep(1000, 100, wait=0)
2748 self._juice_drip(ms)
2749 if self.tk:
2750 self.con("[reward=%dms]" % ms, color='black')
2751 actual_reward_size = ms
2752 self._tally(reward=actual_reward_size)
2753 self.dropcount = self.dropcount + 1
2754
2755
2756
2757
2758 self.encode('ACT_' + REWARD + '%d' % actual_reward_size)
2759
2760 return actual_reward_size
2761
2763 """
2764 This is ONLY to be called from inside reward(), not a user-visble
2765 function. The point is to call _juice_drip() in a separate thread
2766 and to block over rewards from being delivered until this one's
2767 finished.
2768
2769 """
2770 self._rewardlock.acquire()
2771 self._juice_drip(t)
2772 self._rewardlock.release()
2773
2775 """Open juice solenoid.
2776
2777 """
2778 dacq_juice(1)
2779
2781 """Close juice solenoid.
2782
2783 """
2784 dacq_juice(0)
2785
2787 dacq_juice_drip(int(round(ms)))
2788
2790 """Count touch bar transitions.
2791
2792 Reset (ie, barchanges(reset=1)) once the bar's been grabbed
2793 and then monitor with barchanges() to see if the count
2794 increases. Count incremewnts each time there's a state change
2795 (0->1 or 1->0). Sampled at DACQ frequency (~1khz), so as to
2796 avoid loosing signals during CPU intensive graphics.
2797
2798 :return: (boolean)
2799
2800 """
2801 if reset:
2802 return dacq_bar_transitions(1)
2803 else:
2804 return dacq_bar_transitions(0)
2805
2807 """Enable/disable exceptions (aka interrupts) when the touch
2808 bar is touched.
2809
2810 If this is enabled, then the das_server will generate a SIGUSR1
2811 interupt each time the bar changes state. This gets caught
2812 by an internal function and converted into a python
2813 exception: BarTransition.
2814
2815 **Don't even think about calling this outside a try/except
2816 wrapper to catch the BarTransition exception, or you will have
2817 serious problems!**
2818
2819 """
2820 if enable:
2821 self._post_bartransition = 0
2822 dacq_bar_genint(1)
2823 else:
2824 dacq_bar_genint(0)
2825
2826
2827
2828 self._post_bartransition = 0
2829
2832
2834 """Enable/disable exceptions (aka interrupts) for joypad input
2835
2836 If this is enabled, then the das_server will generate a SIGUSR1
2837 interupt each time a joypad button, other than #1, is pressed.
2838 When you call with enable=1, it will check to make sure no
2839 buttons are currently pressed -- if there are, it will return
2840 None and you need to try again..
2841 **Don't even think about calling this outside a try/except
2842 wrapper to catch the JoyTransition exception, or you will have
2843 serious problems!**
2844
2845 :param enable: (bool) enable interupt generation -- when enabling
2846 ints, check return value to make sure call succeeded.
2847
2848 :return: if enable is set, flag indicating whether or not ints were
2849 actuall enabled (see notes above)
2850 """
2851
2852 if enable:
2853 for n in range(10):
2854 if dacq_jsbut(n):
2855 return None
2856 self._joypad_intbut = None
2857 self._post_joytransition = 0
2858 dacq_joy_genint(1)
2859 return 1
2860 else:
2861 dacq_joy_genint(0)
2862
2863
2864
2865 self._post_joytransition = 0
2866
2868 """Set an interupt/exception alarm to go off.
2869
2870 An **Alarm** exception will be generated when the timer
2871 expires. Exception will be raised from idlefn() at next
2872 available opportunity, so you must periodicall call idlfn()
2873 once you set the alarm.
2874
2875 *NB* This requires app.allow_ints to be true in order to work!
2876
2877 *NB* only one alarm is allowed at a time.
2878
2879 :param ms: (ms) ms from now for alarm to go off
2880
2881 :param clear: (boolean) clear existing alarm
2882
2883 :return: nothing
2884
2885 """
2886 if ms:
2887 dacq_set_alarm(ms)
2888 elif clear:
2889 dacq_set_alarm(0)
2890
2892 self._post_fixbreak = 0
2893 self._post_bartransition = 0
2894 self._post_joytransition = 0
2895 self._post_alarm = 0
2896 self._joypad_intbut = None
2897
2898 - def interupts(self, enable=None, queue=None):
2899 """Enable or disable interupts from comedi_server.
2900
2901 If queue'ing is enabled, then interupts are cued to be handled
2902 the next time idlefn() is called instead of raised. By default,
2903 when pype is started, interupts are set to queue!
2904
2905 :param enable: (boolean) Enable or disable interupts. Use None to
2906 leave current setting alone
2907
2908 :param queue: (boolean) if true, then interupts are queued and
2909 handled by idlefn(). Otherwise they are raised
2910 immediately and must be caught. Use None to
2911 leave current setting alone
2912
2913 :return: tuple of current interupt status: (enable, queue)
2914
2915 """
2916
2917 if enable is not None:
2918 self.allow_ints = enable
2919 if not self.allow_ints:
2920 self.clear_pending_ints()
2921 if queue is not None:
2922 self._queue_ints = queue
2923
2924 return (self.allow_ints, self._queue_ints)
2925
2926 - def _int_handler(self, signal, frame, iclass=None, iarg=None):
2927 """This is for catching SIGUSR1's from the dacq process.
2928
2929 """
2930
2931 if iclass is None:
2932 iclass = dacq_int_class()
2933 if iarg is None:
2934 iarg = dacq_int_arg()
2935
2936
2937
2938 if iclass == 666:
2939 self.running = 0
2940 warn(MYNAME(), 'Lost eyelink connection!', wait=0)
2941 return
2942
2943
2944
2945 if not self.allow_ints:
2946
2947 return
2948
2949
2950 self.allow_ints = 0
2951
2952
2953
2954
2955
2956
2957
2958 self.lastint_ts = dacq_ts()
2959
2960 if iclass == 1:
2961 if iarg == 0:
2962 dacq_release()
2963 if self._queue_ints:
2964 self._post_bartransition = 1
2965 else:
2966 raise BarTransition
2967 elif iclass == 4:
2968 dacq_release()
2969 self._joypad_intbut = iarg+1
2970 if self._queue_ints:
2971 self._post_joytransition = 1
2972 else:
2973 raise JoyTransition
2974 elif iclass == 2:
2975 dacq_release()
2976 if self._queue_ints:
2977 self._post_fixbreak = 1
2978 else:
2979 raise FixBreak
2980 elif iclass == 3:
2981 dacq_release()
2982 if self._queue_ints:
2983 self._post_alarm = 1
2984 else:
2985 raise Alarm
2986 else:
2987 sys.stderr.write('Stray SIGUSR1: iclass=%d iarg=%d\n',
2988 (iclass, iarg))
2989 self.allow_ints = 1
2990 return
2991
2993 """Query to see if touchbar is touched (aka 'down').
2994
2995 """
2996 if self.flip_bar:
2997 if self.eyemouse:
2998 return not self.eyebar
2999 else:
3000 return not dacq_bar()
3001 else:
3002 if self.eyemouse:
3003 return self.eyebar
3004 else:
3005 return dacq_bar()
3006
3008 """Query to see if touchbar is touched (aka 'down').
3009
3010 """
3011 return not self.bardown()
3012
3014 """Query the nth joystick button (starting with #0, aka B1).
3015
3016 Joystick labels are labeled starting with B1, while indexing
3017 in pype starts at 0. So to query the button labeled "1", you
3018 need to do *joybut(0)*.
3019
3020 To make life easier -- this will also accept keys 1,2,3.. as
3021 standins for B1,B2 so you can run without a joystick/keypad.
3022
3023 :return: (bool) up or down status
3024
3025 """
3026 return dacq_jsbut(n) or ('%d'%(n+1)) in self.fb.checkkeys()
3027
3029 """Query the joystick axis state.
3030
3031 :return: (xpos, ypos)
3032
3033 """
3034 return (dacq_js_x(), dacq_js_y())
3035
3036 - def setgeo(self, w=None, default=None, load=None, loadempty=None,
3037 save=None, posonly=0):
3038 """Manage window geometry database (sticky across sessions).
3039
3040 :param w: (widget) widget to query -- title string is use as key!
3041
3042 :param default: (str) default geom if not in DB (WxH+X+Y)
3043
3044 :param load: (bool) load database from ~/.pyperc/winpos
3045
3046 :param loadempty: (bool) initialize empty database. This is to
3047 force a reset of all window positions to retrieve windows
3048 that might appear off screen!
3049
3050 :param save: (bool) save database to ~/.pyperc/winpos
3051
3052 :param posonly: (bool) only use position info
3053
3054 :return: nothing
3055 """
3056
3057 filename = pyperc('winpos')
3058
3059 if loadempty:
3060 self._winpos = {}
3061 elif load:
3062 try:
3063 self._winpos = cPickle.load(open(filename, 'r'))
3064 except IOError:
3065 self._winpos = {}
3066 pass
3067 except EOFError:
3068 self._winpos = {}
3069 pass
3070 elif save:
3071 wlist = []
3072 wlist.append(self.tk)
3073 for k in self.tk.children.keys():
3074 wlist.append(self.tk.children[k])
3075
3076 for w in wlist:
3077 try:
3078 geo = w.geometry()
3079 if not geo[0:3] == '1x1':
3080 self._winpos[w.title()] = geo
3081 except AttributeError:
3082 pass
3083
3084 cPickle.dump(self._winpos, open(filename, 'w'))
3085 else:
3086 try:
3087 geo = self._winpos[w.title()]
3088 if geo[0:3] == '1x1':
3089 raise KeyError
3090 if posonly:
3091 geo = "".join(re.compile('[+-][0-9]+').findall(geo)[-2:])
3092 w.geometry(geo)
3093 except KeyError:
3094 if default:
3095 w.geometry(default)
3096
3098 """Call to initiate pype shutdown.
3099
3100 Completely close and cleanup application -- shutdown the DACQ
3101 interface and framebuffer and restore the X11 bell etc.
3102
3103 """
3104 self.unloadtask()
3105
3106 if self._testpat: del self._testpat
3107
3108 if self.fb:
3109 del self.fb
3110
3111
3112
3113 if sys.platform.startswith('linux'):
3114 d = self.config.get('SDLDPY')
3115 os.system("xset -display %s b on" % d)
3116 os.system("xset -display %s s on" % d)
3117 os.system("xset -display %s +dpms" % d)
3118
3119 if self.dacq_going:
3120 dacq_stop()
3121 dacq_going = 0
3122
3123 if self.plex is not None:
3124 self.plex.drain(terminate=1)
3125 Logger('pype: closed connection to plexon.\n')
3126
3127 try:
3128 self.udpy.fidinfo(file=subjectrc('last.fid'))
3129 self.udpy.savepoints(subjectrc('last.pts'))
3130 except AttributeError:
3131 pass
3132
3133 if self.tk:
3134
3135 self._savestate()
3136 Logger('pype: saved state.\n')
3137 Logger('pype: bye bye.\n')
3138
3140 """Set trial/block/repetition information in the GUI window.
3141
3142 :param msg: (string) Short bit of text stick in the window. If
3143 called with no args or msg==None, clear the window.
3144
3145 :return: nothing
3146
3147 """
3148 self._repinfo.configure(text=msg)
3149
3151 """Query name of task.
3152
3153 :return: (string) current task name (no extension) or none.
3154
3155 """
3156 return self._taskname()
3157
3159 """Query or set name of task.
3160
3161 :param taskname: (string) set/clear task name
3162
3163 :param path: (string) set/clear task source directory/path
3164
3165 :return: (string) task name (no extension)
3166
3167 """
3168 if taskname == []:
3169 return self.task_name
3170
3171 if taskname is None:
3172 self.task_name = None
3173 if self.tk:
3174 self._tasknamew.configure(text="task: <none>")
3175 self.balloon.bind(self._tasknamew, 'no task loaded')
3176 else:
3177 self.task_name = taskname
3178 self.task_dir = path
3179 if self.tk:
3180 self._tasknamew.configure(text="task: %s" % self.task_name)
3181 self.balloon.bind(self._tasknamew,
3182 'src: %s/%s.py' % (self.task_dir,
3183 self.task_name))
3184
3186 if self.tk:
3187 if self.record_file is None:
3188 self._recfile.config(text="datafile: <none>")
3189 self.balloon.bind(self._recfile, 'no datafile open')
3190 else:
3191 if len(self.record_file) > 40:
3192 spacer = "..."
3193 else:
3194 spacer = ""
3195 self._recfile.config(text="datafile: %s%s"
3196 % (spacer, self.record_file[-25:]))
3197 self.balloon.bind(self._recfile, self.record_file)
3198
3211
3225
3227 """Start recording data.
3228
3229 Clear the per-trial recording buffer and reset the per-trial
3230 timer. This should be called at the start of every trial,
3231 always at the same point in the trial. All per-trial
3232 timestamps (encodes and datastreams) will be timestamped
3233 relative to this call (which will be t=0).
3234
3235 """
3236
3237
3238 dacq_set_pri(self.rig_common.queryv('dacq_pri'))
3239 dacq_set_mypri(self.rig_common.queryv('fb_pri'))
3240 if self.rig_common.queryv('rt_sched'):
3241
3242
3243 dacq_set_rt(1)
3244
3245 self.record_buffer = []
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258 self.record_state(1)
3259 if self.u3test:
3260 self.u3.start()
3261 t = self.encode(START)
3262
3263
3264
3265 self.encode('TOD_START %f' % time.time())
3266
3267 self.recording = 1
3268
3269
3270
3271
3273 """Stop recording data.
3274
3275 End of trial clean up.
3276
3277 """
3278
3279
3280
3281
3282
3283
3284
3285 self.recording = 0
3286 self.record_state(0)
3287 t = self.encode(STOP)
3288 if self.u3test:
3289 self.u3.stop(wait=1)
3290
3291
3292
3293 self.encode('TOD_STOP %f' % time.time())
3294
3295
3296 dacq_set_pri(0)
3297 dacq_set_mypri(0)
3298 if self.rig_common.queryv('rt_sched'):
3299 dacq_set_rt(0)
3300
3301 if self.plex is not None:
3302 self.xdacq_data_store = _get_plexon_events(self.plex, fc=40000)
3303
3305 if self.plex is not None:
3306 Logger("pype: plexon units --")
3307 for (chan, unit) in self.plex.neuronlist():
3308 Logger(" sig%03d%c" % (chan, chr(ord('a')+unit)))
3309 Logger("\n")
3310 else:
3311 Logger("pype: plexnet not enabled.")
3312
3314 """Enable or disable plexon (or other auxilliary recording
3315 system) state.
3316
3317 This basically triggers the external recording system (plexon,
3318 TDT, etc) to start recording by changing the polarity on
3319 digitial output line 2.
3320
3321 In the case of the plexon, a 250ms ITI imposed as well to
3322 make sure the plexon can keep up and there are no drop outs.
3323 This is known problem with the plexon -- if you strobe the
3324 gating signal faster than 250ms it can loose the strobe.
3325
3326 """
3327
3328 try:
3329 l = self._last_recstate
3330 except:
3331 self._last_recstate = dacq_ts()
3332
3333 if self.xdacq is 'plexon':
3334 warn = 1
3335 while (dacq_ts() - self._last_recstate) < 250:
3336 if warn:
3337 sys.stderr.write('warning: short ITI, stalling\n')
3338 warn = 0
3339
3340 dacq_dig_out(2, state)
3341 self._last_recstate = dacq_ts()
3342
3344 """Start/stop recording eye position data.
3345
3346 Begin recording eye trace data now. Or, stop recording eye
3347 data now. Be sure to call this before you save data with
3348 record_write() below!
3349
3350 This idea is that you may not want to record eye position
3351 data until the fixation spot is acquired or the touchbar
3352 touched etc, so this provides fine grained control over
3353 storage of the eye pos data stream. You can start saving
3354 spike data first with record_start() and later start
3355 saving eye traces (although not *vice versa*).
3356
3357 :param on: (boolean)
3358
3359 :return: nothing
3360
3361 """
3362 if on:
3363 dacq_adbuf_toggle(1)
3364 self.encode(EYE_START)
3365 self._eyetrace = 1
3366 elif self._eyetrace:
3367
3368 self.encode(EYE_STOP)
3369 if dacq_adbuf_toggle(0):
3370 self.encode(EYE_OVERFLOW)
3371 Logger('pype: warning -- eyetrace overflowed\n')
3372 warn(MYNAME(), 'eye trace overflow')
3373 self._eyetrace = 0
3374
3375 - def encode(self, code=None, ts=None):
3376 """Insert event code into the per-trial timestream.
3377
3378 :param code: (string or tuple/lsit) Any string can be used, but
3379 it's best to use the constants defined in the
3380 pype.events module or a modified version of these for
3381 portability and to facilitate data analysis. If a
3382 sequence of strings is provided, all events will be
3383 inserted into the timesteam with the same (current)
3384 timestamp. If code is None, then it's a dummy encode
3385 that can be used to retrieve the current time.
3386
3387 :param ts: (int) optional time in ms for enodes -- by default
3388 the current timestamp is used, but a timestamp can
3389 be provided to override (allowing multiple seqeuential
3390 encodes with the same timestamp)
3391
3392 :return: (ms) actual event timestamp
3393
3394 """
3395
3396 if ts is None:
3397 ts = dacq_ts()
3398
3399 if code is not None:
3400 if (type(code) is TupleType) or (type(code) is ListType):
3401
3402
3403 for acode in code:
3404 if len(acode) > 0:
3405 self.record_buffer.append((ts, acode))
3406 else:
3407 if len(code): self.record_buffer.append((ts, code))
3408
3409 return ts
3410
3412 return self.record_buffer
3413
3415 """Query current set of spike times for this trial.
3416
3417 This is gets the spike data out from the ad/ad server in
3418 mid-trial. You can use this to do on-line statistics or
3419 generate dynamic stimuli etc. It should not be considered
3420 final data -- this is only an approximation, particularly if
3421 you're still recording data when you call this.
3422
3423 :return: (array) array of spike times
3424
3425 """
3426 n = dacq_adbuf_size()
3427 t = np.zeros(n, np.float)
3428 s0 = np.zeros(n, np.int)
3429
3430 for i in range(0,n):
3431 t[i] = dacq_adbuf_t(i) / 1000.0
3432 s0[i] = dacq_adbuf_c3(i)
3433
3434 spike_thresh = int(self.rig_common.queryv('spike_thresh'))
3435 spike_polarity = int(self.rig_common.queryv('spike_polarity'))
3436 spike_times = _find_ttl(t, s0, spike_thresh, spike_polarity)
3437
3438 return spike_times
3439
3440 - def find_saccades(self, thresh=2, mindur=25, maxthresh=None,
3441 start=None, stop=None):
3442 """Find saccades in mid-trial.
3443
3444 Retreives the eye trace data using get_eyetrace_now() and then
3445 calls pypedata.find_saccades with the specified parameters. *See
3446 documentation on pypedata.find_saccades() for algorithm/parameter
3447 details.*
3448
3449 :param thresh: (pix/time) velocity threshold for saccade detection
3450
3451 :param mindur: (ms) minimum allowable separation between saccades
3452
3453 :param maxthresh: (pix/time) anything over this velocity (if
3454 specified) is a blink.
3455
3456 :param start: (ms) time/timestamp to start looking
3457
3458 :param stop: (ms) time/timestamp to stop looking
3459
3460 :returns: (list of tuples) See pypedata.find_saccades().
3461
3462 """
3463
3464 (t, x, y) = self.get_eyetrace_now()
3465
3466
3467
3468
3469 if (not start is None) or (not stop is None):
3470 if start is None: start = 0
3471 if stop is None: stop = len(t)
3472 ix = np.greater_equal(t, start) & np.less(t, stop)
3473 t = t[ix]
3474 x = x[ix]
3475 y = y[ix]
3476 slist = find_saccades((t, x, y, None),
3477 thresh=thresh,
3478 mindur=mindur,
3479 maxthresh=maxthresh)
3480 return slist
3481
3483 """Query current eye trace - note: was get_eyepos_now().
3484
3485 This function extracts the current state of the x/y eye
3486 position buffers from the dacq_server NOW. Like get_spikes()
3487 you can use this in mid-trial or at the end of the trial to
3488 adapt the task based on his eye movements. This should NOT be
3489 considered the final data, particularly if you're still in
3490 record mode when you call this function
3491
3492 :param raw: (bool) if true, then return the raw, uncalibrated
3493 eye trace data 'stored' in the first two analog channel
3494 streams.
3495
3496 :return: (mult-val-ret) time, xpos, ypos arrays (up to current
3497 time)
3498
3499 """
3500 n = dacq_adbuf_size()
3501 t = np.zeros(n, np.float)
3502 x = np.zeros(n, np.int)
3503 y = np.zeros(n, np.int)
3504
3505 for i in range(0,n):
3506 t[i] = dacq_adbuf_t(i) / 1000.0
3507 if raw:
3508 x[i] = dacq_adbuf_c0(i)
3509 y[i] = dacq_adbuf_c1(i)
3510 else:
3511 x[i] = dacq_adbuf_x(i)
3512 y[i] = dacq_adbuf_y(i)
3513
3514 return (t, x, y)
3515
3517 """Query current photo trace - note: was get_photo_now().
3518
3519 :return: (mult-val-ret) time, photodiode-voltage (up to
3520 current time)
3521
3522 """
3523 n = dacq_adbuf_size()
3524 t = np.zeros(n, np.float)
3525 p = np.zeros(n, np.int)
3526
3527 for i in range(0,n):
3528 t[i] = dacq_adbuf_t(i) / 1000.0
3529 p[i] = dacq_adbuf_c2(i)
3530
3531 return (t, p)
3532
3534 """Query current event stream (encodes).
3535
3536 :return: (array) *copy* of current encode buffer
3537
3538 """
3539 return self.record_buffer[::]
3540
3541 - def record_write(self, resultcode=None, rt=None,
3542 params=None, taskinfo=None, returnall=False):
3543 """Write the current record to the specified datafile.
3544
3545 Call this at the end of each trial in your task to do
3546 post-processing and flush all the recorded data to disk.
3547
3548 :param resultcode: (string)
3549
3550 :param rt: (number) Reaction time in ns (or -1 for "not applicable")
3551
3552 :param params: (dict) final dictionary of all parameters (including
3553 params added by the task as part of the data flow)
3554
3555 :param taskinfo: (tuple) anything "extra" you want saved along with
3556 the rest of the trial data.
3557
3558 :param returnall: (bool; def=False) return a copy of the full data
3559 struct written to the datafile.
3560
3561 :return: if returnall is False, then return values is a tuple (time,
3562 photodiode-waveform, spike-waveform). Otherwise, it's simply
3563 a complete copy of the object written to the datafile, which
3564 includes ALL the available data.
3565
3566 """
3567
3568 if (self.record_file == '/dev/null' and
3569 self.sub_common.queryv('fast_tmp')):
3570 fast_tmp = 1
3571 else:
3572 fast_tmp = 0
3573
3574
3575 if type(taskinfo) != TupleType:
3576 taskinfo = (taskinfo,)
3577
3578
3579 self.eyetrace(0)
3580
3581
3582 self.queue_action()
3583
3584 tag = self.dotrialtag(reset=1)
3585
3586 n = dacq_adbuf_size()
3587 self.eyebuf_t = np.zeros(n, np.float)
3588 self.eyebuf_x = np.zeros(n, np.int)
3589 self.eyebuf_y = np.zeros(n, np.int)
3590 self.eyebuf_pa = np.zeros(n, np.int)
3591 self.eyebuf_new = np.zeros(n, np.int)
3592 p0 = np.zeros(n, np.int)
3593 s0 = np.zeros(n, np.int)
3594
3595 if self.rig_common.queryv('save_ain0'):
3596 ain0 = np.zeros(n, np.int)
3597 else:
3598 ain0 = None
3599
3600 if self.rig_common.queryv('save_ain1'):
3601 ain1 = np.zeros(n, np.int)
3602 else:
3603 ain1 = None
3604
3605 if self.rig_common.queryv('save_ain5'):
3606 ain5 = np.zeros(n, np.int)
3607 else:
3608 ain5 = None
3609
3610 if self.rig_common.queryv('save_ain6'):
3611 ain6 = np.zeros(n, np.int)
3612 else:
3613 ain6 = None
3614
3615 if self.rig_common.queryv('save_ain7'):
3616 ain7 = np.zeros(n, np.int)
3617 else:
3618 ain7 = None
3619
3620
3621
3622 ndups = 0
3623 if not fast_tmp or self._show_eyetrace.get():
3624 for i in range(0,n):
3625
3626 self.eyebuf_t[i] = dacq_adbuf_t(i) / 1000.0
3627 self.eyebuf_x[i] = dacq_adbuf_x(i)
3628 self.eyebuf_y[i] = dacq_adbuf_y(i)
3629 self.eyebuf_pa[i] = dacq_adbuf_pa(i)
3630 self.eyebuf_new[i] = dacq_adbuf_new(i)
3631 if not ain0 is None:
3632 ain0[i] = dacq_adbuf_c0(i)
3633 if not ain1 is None:
3634 ain1[i] = dacq_adbuf_c1(i)
3635 p0[i] = dacq_adbuf_c2(i)
3636 s0[i] = dacq_adbuf_c3(i)
3637 if not ain5 is None:
3638 ain5[i] = dacq_adbuf_c5(i)
3639 if not ain6 is None:
3640 ain6[i] = dacq_adbuf_c6(i)
3641 if not ain7 is None:
3642 ain7[i] = dacq_adbuf_c7(i)
3643
3644
3645
3646
3647
3648
3649 for i in range(1, n):
3650 if self.eyebuf_t[i-1] == self.eyebuf_t[i]:
3651 ndups = ndups + 1
3652 if ndups > 0:
3653 sys.stderr.write("warning: %d duplicate timestamp(s)\n" % ndups)
3654
3655
3656
3657 photo_thresh = int(self.rig_common.queryv('photo_thresh'))
3658 photo_polarity = int(self.rig_common.queryv('photo_polarity'))
3659 self.photo_times = _find_ttl(self.eyebuf_t, p0,
3660 photo_thresh, photo_polarity)
3661
3662 spike_thresh = int(self.rig_common.queryv('spike_thresh'))
3663 spike_polarity = int(self.rig_common.queryv('spike_polarity'))
3664 self.spike_times = _find_ttl(self.eyebuf_t, s0,
3665 spike_thresh, spike_polarity)
3666
3667 ut = []
3668 a0 = []
3669 if self.u3test and self.u3 and len(self.eyebuf_t) > 0:
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683 (rawut, ut, _rt, a0, a1, a2, a3) = self.u3.get()
3684 ut = ut - dacq_ts0()
3685 tf = open('u3-%04d.asc' % self.u3.count, 'w')
3686 ut = ut * 1000.
3687 ix = np.where(np.logical_and(ut >= self.eyebuf_t[0],
3688 ut <= self.eyebuf_t[-1]))[0]
3689 for n in ix:
3690 tf.write('%f %f\n' % (ut[n], a0[n],))
3691 tf.close()
3692
3693 tf = open('com-%04d.asc' % self.u3.count, 'w')
3694 for n in range(len(self.eyebuf_t)):
3695 tf.write('%f %f\n' % (self.eyebuf_t[n], p0[n],))
3696 tf.close()
3697 self.u3.count = (self.u3.count + 1) % 10000
3698
3699 self.update_rt((resultcode, rt, params, taskinfo))
3700 if 1 or resultcode[0] == CORRECT_RESPONSE:
3701 self.update_psth((self.spike_times, self.record_buffer))
3702 if self._updatefn:
3703 self._updatefn((resultcode, rt, params, taskinfo),
3704 (self.spike_times, self.record_buffer))
3705
3706 if self._show_eyetrace.get():
3707 self._plotEyetraces(self.eyebuf_t,
3708 self.eyebuf_x, self.eyebuf_y,
3709 ((self.eyebuf_t, p0),),
3710 ((self.eyebuf_t, s0), (ut, a0), ),
3711 self.spike_times)
3712
3713 self.udpy.info("|spikes:%3d|syncs:%3d|dups:%3d|" %
3714 (len(self.spike_times), len(self.photo_times), ndups,))
3715
3716
3717
3718
3719 dacq_adbuf_clear()
3720
3721
3722 params['PypeBuildDate'] = pypeversion.PypeBuildDate
3723 params['PypeBuildHost'] = pypeversion.PypeBuildHost
3724
3725
3726 params['PypeVersionInfo'] = pypeversion.PypeVersionInfo
3727 params['PypeVersionID'] = pypeversion.PypeVersionID
3728
3729
3730 params['PypeVersion'] = pypeversion.PypeVersion
3731
3732
3733 params['piTrialTag'] = tag
3734
3735
3736 params['record_id'] = tag
3737
3738 rec = None
3739
3740 if not fast_tmp and self.record_file:
3741
3742 info = (resultcode, rt, params) + taskinfo
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776 if self.xdacq == 'tdt':
3777
3778
3779 (server, tank, block, tnum) = self.tdt.getblock()
3780
3781
3782
3783
3784
3785
3786
3787 info[2]['tdt_server'] = str(server.encode('ascii'))
3788 info[2]['tdt_tank'] = str(tank.encode('ascii'))
3789 info[2]['tdt_block'] = str(block.encode('ascii'))
3790 info[2]['tdt_tnum'] = tnum
3791
3792 rec = [
3793 ENCODE,
3794 info,
3795 self.record_buffer,
3796 _tolist(self.eyebuf_t),
3797 _tolist(self.eyebuf_x),
3798 _tolist(self.eyebuf_y),
3799 _tolist(self.photo_times),
3800 _tolist(self.spike_times),
3801 self.record_id,
3802 _tolist(p0),
3803 _tolist(s0),
3804 (
3805 _tolist(ain0),
3806 _tolist(ain1),
3807 None,
3808 None,
3809 _tolist(ain5),
3810 _tolist(ain6),
3811 _tolist(ain7),
3812 ),
3813 _tolist(self.eyebuf_pa),
3814 self.xdacq_data_store,
3815 _tolist(self.eyebuf_new),
3816 ]
3817
3818 f = open(self.record_file, 'a')
3819 labeled_dump('encode', rec, f, 1)
3820 f.close()
3821
3822 self.record_id = self.record_id + 1
3823
3824 if returnall:
3825 if rec is not None:
3826 p = PypeRecord(None, 0, rec)
3827 p.compute()
3828 return p
3829 else:
3830 return None
3831 else:
3832 return (self.eyebuf_t, p0, s0)
3833
3835 class Record(object):
3836 pass
3837
3838 r = Record()
3839 r.resultcode = rec[1][0]
3840 r.rt = rec[1][1]
3841 r.params = rec[1][2]
3842 r.taskinfo = rec[1][3]
3843 r.evt = rec[2]
3844 r.t = rec[3]
3845 r.x = rec[4]
3846 r.y = rec[5]
3847 r.pa = rec[12]
3848 r.photo_times = rec[6]
3849 r.spike_times = rec[7]
3850
3851 return r
3852
3854 """Insert note into current datafile.
3855
3856 Try not to use this -- it's really obsolete and hard to parse!
3857
3858 """
3859 if self.record_file:
3860 rec = [NOTE, tag, note]
3861 f = open(self.record_file, 'a')
3862 labeled_dump('note', rec, f, 1)
3863 f.close()
3864
3866 """Guess next filename/number by looking at files in CWD.
3867
3868 """
3869
3870 subject = self.sub_common.queryv('subject')
3871
3872 if self.training:
3873 cell = 0
3874 else:
3875 cell = self.sub_common.queryv('cell')
3876
3877 try:
3878 pat = "%s%04d.*.[0-9][0-9][0-9]" % (subject, int(cell))
3879 except ValueError:
3880 pat = "%s%s.*.[0-9][0-9][0-9]" % (subject, cell)
3881
3882 flist = glob.glob(pat)+glob.glob(pat+'.gz')
3883
3884 next = 0
3885 for f in flist:
3886 try:
3887 n = int(f.split('.')[2])
3888 if n >= next:
3889 next = n + 1
3890 except ValueError:
3891 pass
3892
3893 try:
3894 return "%s%04d.%s.%03d" % (subject, int(cell),
3895 self.task_name, next)
3896 except ValueError:
3897 return "%s%s.%s.%03d" % (subject, cell,
3898 self.task_name, next)
3899
3901 """Guess next filename/number using elog.
3902
3903 """
3904 import elogapi
3905
3906 animal = self.sub_common.queryv('subject')
3907 full_animal = self.sub_common.queryv('full_subject')
3908
3909 if len(animal) == 0 or len(full_animal) == 0:
3910 warn(MYNAME(),
3911 "Set 'subject' and 'full_subject' parameters.")
3912 return None
3913
3914
3915 exper = elogapi.GetExper(full_animal)
3916 if exper is None:
3917
3918 exper = "%s%04d" % (animal, 1)
3919
3920 self.sub_common.set('cell', exper)
3921
3922 if self.training:
3923 exper = exper[:-4] + '0000'
3924 self._exper = exper
3925
3926
3927 pat = "%s.*.[0-9][0-9][0-9]" % (exper, )
3928
3929 flist = glob.glob(pat)+glob.glob(pat+'.gz')
3930
3931 next = 0
3932 for f in flist:
3933 try:
3934 n = int(f.split('.')[2])
3935 if n >= next:
3936 next = n + 1
3937 except ValueError:
3938 pass
3939
3940 return "%s.%s.%03d" % (exper, self.task_name, next)
3941
3943 if self.use_elog:
3944 return self._guess_elog()
3945 else:
3946 return self._guess_fallback()
3947
3949 """Let pype know run (*not* trial) is over.
3950
3951 Call this function at the end of each run (ie, datafile) to
3952 let pype know it's ok to clean up and reset things for the
3953 next run.
3954
3955 This really should be called "run_done".
3956
3957 """
3958
3959 self.record_note('pype', 'run ends')
3960 self.record_file = None
3961 self._set_recfile()
3962
3964 import filebox
3965
3966 if not fname is None:
3967 self.record_file = fname
3968 else:
3969 self.record_file = None
3970 while 1:
3971 g = self._guess()
3972 if g is None:
3973 return None
3974 (file, mode) = filebox.SaveAs(initialdir=os.getcwd(),
3975 pattern='*.[0-9][0-9][0-9]',
3976 initialfile=g,
3977 datafiles=1)
3978 if file is None:
3979 return None
3980 else:
3981 self.record_file = file
3982
3983 if posixpath.exists(self.record_file):
3984 if mode == 'w':
3985 Logger('pype: unlinking: %s\n' % self.record_file)
3986 posix.unlink(self.record_file)
3987 elif mode == 'a':
3988 Logger('pype: appending to: %s\n' %
3989 self.record_file)
3990 break
3991
3992 self._set_recfile()
3993 self.record_note('pype', 'run starts')
3994
3995
3996
3997
3998 self.write_userparams()
3999
4000
4001 return 1
4002
4005
4007 """Idle the framebuffer (put up test pattern display).
4008
4009 Not really for users -- this is really only used by the
4010 candy module to clear the screen when candy exits.
4011
4012 Note: test pattern is scaled to fill entire screeen
4013
4014 """
4015 if not self.fb: return
4016
4017 if self._testpat is None:
4018 t = self.config.get('TESTPAT')
4019 if t and posixpath.exists(t):
4020 fname = t
4021 elif posixpath.exists(pyperc('testpat')):
4022 fname = pyperc('testpat');
4023 else:
4024 fname = self.pypedir + '/lib/testpat.png'
4025 self._testpat = ScaledSprite(x=0, y=0, fname=fname, fb=self.fb,
4026 depth=99, on=1, name='testpat',
4027 width=self.fb.w, height=self.fb.h)
4028 drawtest(self.fb, self._testpat)
4029
4031 """Set time range for useful portion of the eye trace.
4032
4033 If these are set, then when the eye trace is plotted, the time
4034 range is restricted to this time frame to speed up
4035 plotting. Typically this is used to prevent looking at all the
4036 time while the animal's trying to acquire the fixspot, which
4037 isn't very interesting and may slow things down.
4038
4039 """
4040
4041 self._show_eyetrace_start = start
4042 self._show_eyetrace_stop = stop
4043
4045 import pylab
4046
4047 if len(t) < 1:
4048 return
4049
4050 if self._show_eyetrace_start is not None:
4051 start = self._show_eyetrace_start
4052 else:
4053 start = t[0]
4054
4055 if self._show_eyetrace_stop is not None:
4056 stop = self._show_eyetrace_stop
4057 else:
4058 stop = t[-1]
4059
4060 t0 = t[0]
4061 skip = 1
4062
4063 pylab.ion()
4064 pylab.figure(99)
4065 pylab.clf()
4066
4067 pylab.subplot(4, 1, 1)
4068 pylab.plot(t[::skip] - t0, x[::skip], 'r-',
4069 t[::skip] - t0, y[::skip], 'g-')
4070 pylab.xlim(start - t0, stop - t0)
4071 pylab.ylabel('X=RED Y=GRN')
4072
4073 colors = 'krgbckrgbckrgbc'
4074
4075 pylab.subplot(4, 1, 2)
4076 n = 0
4077 for (tt, p) in p0:
4078 pylab.plot(tt[::skip] - t0, p, colors[n]+'-')
4079 pylab.hold(True)
4080 n += 1
4081 pylab.hold(False)
4082 pylab.xlim(start - t0, stop - t0)
4083 pylab.ylabel('photo')
4084
4085 pylab.subplot(4, 1, 3)
4086 n = 0
4087 for (tt, s) in s0:
4088 pylab.plot(tt[::skip] - t0, s, colors[n]+'-')
4089 pylab.hold(True)
4090 n += 1
4091 pylab.hold(False)
4092
4093 pylab.ylabel('spikes')
4094
4095 pylab.subplot(4, 1, 4)
4096 raster = np.array(raster) - t0
4097 pylab.plot(raster, 0.0 * raster, 'k.')
4098 pylab.ylim(-1,1)
4099 pylab.xlim(start - t0, stop - t0)
4100 pylab.ylabel('raster')
4101 pylab.draw()
4102
4104 import pylab
4105
4106 if infotuple is None:
4107 self.rtdata = []
4108 else:
4109 resultcode, rt, params, taskinfo = infotuple
4110 if rt > 0:
4111 self.rtdata.append(rt)
4112
4113 self.rthist.fig.clf()
4114
4115 a = self.rthist.fig.add_subplot(1,1,1)
4116 h = np.array(self.rtdata)
4117 if len(h) == 0 or np.std(h) <= 0:
4118 a.text(0.5, 0.5, 'SPACE INTENTIONALLY BLANK',
4119 transform=a.transAxes, color='red',
4120 horizontalalignment='center', verticalalignment='center')
4121 else:
4122 n, bins, patches = a.hist(h, facecolor='grey')
4123 a.text(0.02, 1-0.02,
4124 '$\\mu=%.0fms$\n$\\sigma=%.0fms$' % (np.mean(h), np.std(h),),
4125 horizontalalignment='left', verticalalignment='top',
4126 color='red', transform=a.transAxes)
4127
4128 x = np.linspace(bins[0], bins[-1], 25)
4129 g = pylab.normpdf(x, np.mean(h), np.std(h))
4130 g = g * np.sum(n) / np.sum(g)
4131 a.plot(x, g, 'r-', linewidth=2)
4132 a.axvspan(self.sub_common.queryv('minrt'),
4133 self.sub_common.queryv('maxrt'),
4134 color='b', alpha=0.25)
4135 a.axis([-10, 1.25*self.sub_common.queryv('maxrt'), None, None])
4136
4137 a.set_ylabel('n=%d' % len(h))
4138 a.set_xlabel('Reaction Time (ms)')
4139
4140 try:
4141 self.rthist.drawnow()
4142 except:
4143 if warn(MYNAME(),
4144 'Probably must delete ~/.matlibplot', once=True):
4145 reporterror()
4146
4148 """
4149 Note: this only adds to the psth if the trigger event is present.
4150 """
4151 import pylab
4152
4153 if self.psth is None: return
4154
4155 if data is None:
4156 self.psthdata = np.array([])
4157 else:
4158 spike_times = np.array(data[0])
4159 events = data[1]
4160 t0 = find_events(events, trigger)
4161 if t0 == []:
4162
4163 return
4164 else:
4165 self.psthdata = np.concatenate((self.psthdata, spike_times-t0,))
4166
4167 self.psth.fig.clf()
4168 a = self.psth.fig.add_subplot(1,1,1)
4169 if len(self.psthdata) == 0:
4170 a.text(0.5, 0.5, 'NO SPIKE DATA',
4171 transform=a.transAxes, color='red',
4172 horizontalalignment='center', verticalalignment='center')
4173 else:
4174
4175
4176 a.hist(self.psthdata, facecolor='blue',
4177 bins=40, range=(-100, 1900))
4178 a.set_xlabel('Time (ms)')
4179 a.set_ylabel('nspikes')
4180
4181 try:
4182 self.psth.drawnow()
4183 except:
4184 if warn('matlibplot error',
4185 'Probably must delete ~/.matlibplot', once=True):
4186 reporterror()
4187
4189 """Helper function for creating new fixation window in std way.
4190
4191 Actual window size will be scaled up automatically from the
4192 base radius (subject_param:win_size) using subject_param:win_scale
4193 and eccentricity of the fixation window (relative to montior ctr).
4194
4195 :param x,y: (pixels) center of fixation window
4196
4197 :param tweak: (pixels) task-specific additive adjustment to radius
4198
4199 :return: FixWin object
4200
4201 """
4202
4203
4204 r = self.sub_common.queryv('win_size') + tweak
4205
4206
4207 r = r + (self.sub_common.queryv('win_scale') * ((x**2 + y**2)**0.5))
4208
4209
4210 vbias = self.sub_common.queryv('vbias')
4211
4212 return FixWin(x=x, y=y, size=int(round(r + adj)), app=self, vbias=vbias)
4213
4215 """Get global handle to running PypeApp.
4216
4217 :returns: PypeApp instance or None (if PypeApp hasn't been instantiated)
4218
4219 """
4220 return PypeApp._instance
4221
4223 """Get host-specific config file.
4224
4225 Typically this is: $(PYPERC)/Config.{HOST_NAME}
4226
4227 :return: (string) config filename
4228
4229 """
4230 h = socket.gethostname().split('.')[0]
4231 return pyperc('Config.%s' % h)
4232
4233 -def _find_ttl(t, x, thresh=500, polarity=1):
4234 """Find TTL pulses in x.
4235
4236 *NB* this is backwards (ie, polarity=1 means negative going), but
4237 it's too late to change now..
4238
4239 :param x: (vector) numpy waveform from dacq device
4240
4241 :param thresh: (a2d units) threshold for detecting events in 'x'
4242
4243 :param polarity: (in) >0 for negative going, <=0 for positive
4244 going pulses.
4245
4246 :return: (list) time in ms of first samples above or below
4247 threshold (depending on value of polarity)
4248
4249 """
4250
4251 times = []
4252 inpulse = 0
4253 if polarity > 0:
4254 for i in range(0, len(t)):
4255 if (not inpulse) and (x[i] < thresh):
4256 times.append(t[i])
4257 inpulse = 1
4258 elif inpulse and (x[i] > thresh):
4259 inpulse = 0
4260 else:
4261 for i in range(0, len(t)):
4262 if (not inpulse) and (x[i] > thresh):
4263 times.append(t[i])
4264 inpulse = 1
4265 elif inpulse and (x[i] < thresh):
4266 inpulse = 0
4267 return times
4268
4270 """Dictionary look up with a default value.
4271
4272 :return: dict[key], if available, else default
4273
4274 """
4275 try:
4276 return dict[key]
4277 except (TypeError, KeyError):
4278 return default
4279
4281 """Query subject id string.
4282
4283 This is usually supplied by starting pype with the -s argument.
4284
4285 :return: (string) subject id
4286
4287 """
4288 try:
4289 return os.environ['SUBJECT']
4290 except KeyError:
4291 return 'none'
4292
4294 """Query subject-specific config *directory*.
4295
4296 Each subject should have a private directory for config and state
4297 files etc. By default this is ~/.pyperc/subj_id. The default can
4298 be altered by setting the $PYPERC environment variable.
4299
4300 :return: (string) name of subject config directory
4301
4302 """
4303 return pyperc(os.path.join('_' + subject(), file))
4304
4306 """Query config *directory*.
4307
4308 By default this is ~/.pyperc, but can be overridden by
4309 setting the $PYPERC envronment var.
4310
4311 :return: (string) name of .pyperc config directory
4312
4313 """
4314 if 'PYPERC' in os.environ:
4315 rcdir = os.environ['PYPERC']
4316 else:
4317 rcdir = os.path.join(os.path.expanduser('~'), '.pyperc')
4318
4319 return os.path.join(rcdir, file)
4320
4322 """Fixation Window Encapsulation
4323
4324 Although this is a class and comedi_server supports multiple
4325 fixation windows, this class is hard coded to use window #0,
4326 so there can really only be one instantation at a time.
4327
4328 User's should not really instantiate these directly any longer, but
4329 rather use app.makeFixWin() method.
4330
4331 """
4332 - def __init__(self, x=0, y=0, size=10, app=None, vbias=1.0):
4333 self.app = app
4334 self.icon = None
4335 self.set(x, y, size)
4336 self.vbias = vbias
4337 self.fwnum = 0
4338
4342
4344 """Enable or disable interupts from this fixwin.
4345
4346 Note: this simply allows one of the fixwins to generate
4347 interupts AS ALONG AS PYPE HAS INTERUPTS ENABLED! So, to
4348 activate this, you need to also make sure you call
4349 app.interupts(enable=1) as well, to enable global interupt
4350 handling.
4351
4352 """
4353
4354 if enable:
4355
4356 self._post_fixbreak = 0
4357 dacq_fixwin_genint(self.fwnum, enable)
4358 else:
4359
4360 dacq_fixwin_genint(self.fwnum, enable)
4361 self._post_fixbreak = 0
4362
4363 - def genint(self, enable=None):
4364 """use interupts() method instead!"""
4365 self.interupts(enable=enable)
4366 warn('genint',
4367 'fixwin.genint() obsolete -- change to fixwin.interupts()',
4368 wait=None, action=None, once=1)
4369
4371 return self.x, self.y, self.size
4372
4373 - def set(self, x=None, y=None, size=None):
4374 """Change (or set) size and position of fix window.
4375
4376 Call on() method to update dacq process.
4377
4378 :param x,y: (pixels) position parameters
4379
4380 :param size: (pixels) window radius
4381
4382 :return: nothing
4383
4384 """
4385 if not x is None: self.x = x
4386 if not y is None: self.y = y
4387 if not size is None: self.size = size
4388
4389 - def move(self, x, y, size=-1):
4390 """Move fixwin
4391
4392 Llike set, but for a live, active fixwin -- updates setting
4393 immediately without chaning the interupt/active state of the
4394 window. This is for implementing something like a pursuit
4395 tracker.
4396
4397 :param x,y: (pixels) new position parameters (absolute, not relative!)
4398
4399 :param size: (pixels) window radius (-1 for no change)
4400
4401 :return: nothing
4402
4403 """
4404 if not x is None: self.x = x
4405 if not y is None: self.y = y
4406 if not size is None: self.size = size
4407 dacq_fixwin_move(self.fwnum, x, y, size)
4408
4410 dacq_fixwin_reset(self.fwnum)
4411
4413 """Tell comedi_server to monitor fixwin.
4414
4415 :return: nothing
4416
4417 """
4418 dacq_fixwin(self.fwnum, self.x, self.y, self.size, self.vbias)
4419
4421 """Tell comedi_server to stop monitoring fixwin
4422
4423 :return: nothing
4424
4425 """
4426 dacq_fixwin(self.fwnum, 0, 0, -1, 0.0)
4427
4429 """Check to see if eye is inside fixwin.
4430
4431 :return: (boolean) in/out
4432
4433 """
4434 return dacq_fixwin_state(self.fwnum)
4435
4437 """Check to see if eye moved out of window, since acquired.
4438
4439 :return: (boolean) fixation broken since last check?
4440
4441 """
4442 return dacq_fixwin_broke(self.fwnum)
4443
4445 """Get the exact time fixation was broken.
4446
4447 :return: (ms)
4448
4449 """
4450 return dacq_fixwin_break_time(self.fwnum)
4451
4452 - def draw(self, color='grey', dash=None, clear=None):
4453 """Draw fixation window on user display.
4454
4455 :return: nothing
4456
4457 """
4458 if self.icon:
4459 self.app.udpy.icon(self.icon)
4460
4461 if clear:
4462 self.icon = None
4463 else:
4464 self.icon = self.app.udpy.icon(self.x, self.y,
4465 2*self.size, 2*self.size*self.vbias,
4466 color=color, type=2, dash=dash)
4467
4474
4476 self._start_at = None
4477
4479 """Reset timer.
4480
4481 :return: nothing
4482
4483 """
4484 self._start_at = dacq_ts()
4485
4487 """Query timer.
4488
4489 :return: (ms) elapsed time
4490
4491 """
4492 if self._start_at is None:
4493 return 0
4494 else:
4495 return dacq_ts() - self._start_at
4496
4498 """Dummy class.
4499
4500 This is just a handle to hold task-related information. You
4501 can hang things off it and then pickle it or stash it for
4502 later use.
4503
4504 """
4505 pass
4506
4507 -def now(military=1):
4508 """Get current timestring.
4509
4510 :return: (strin) timestring in form HHMM or HHMM[am|pm] depending
4511 on whether 'military' is true or false.
4512
4513 """
4514 (year, month, day, h, m, s, x1, x2, x3) = time.localtime(time.time())
4515 if military:
4516 return "%02d:%02d" % (h, m)
4517 else:
4518 if h > 12:
4519 return "%02d:%02d AM" % (h-12, m)
4520 else:
4521 return "%02d:%02d PM" % (h-12, m)
4522
4524 """
4525 Drain the Plexon network-based database of timestamp events
4526 until we hit a StopExtChannel event (this is from the pype-plexon
4527 TTL sync line). All events left in the tank (post-trial) will
4528 be discarded.
4529
4530 """
4531 import PlexHeaders
4532
4533 events = None
4534
4535 hit_stop = 0
4536 while not hit_stop:
4537 tank, ndropped = plex.drain()
4538 if tank is None:
4539 Logger("pype: oh no.. lost plexon signal during run\n")
4540 return None
4541
4542 for e in tank:
4543 (Type, Channel, Unit, ts, waveform) = e
4544 if (Type == PlexHeaders.Plex.PL_ExtEventType and
4545 Channel == PlexHeaders.Plex.PL_StartExtChannel):
4546 if events is not None:
4547 Logger("pype: double trigger\n")
4548 return None
4549 events = []
4550 zero_ts = ts
4551 elif events is not None:
4552 if (Type == PlexHeaders.Plex.PL_ExtEventType and
4553 Channel == PlexHeaders.Plex.PL_StopExtChannel):
4554 hit_stop = 1
4555
4556 else:
4557
4558 events.append((int(round(float(ts - zero_ts) / fc * 1000.0)),
4559 Channel, Unit))
4560
4561 return events
4562
4564 """
4565 Add directory to the HEAD (or TAIL) of the pytahon search path.
4566
4567 **NOTE:**
4568 This function also lives in pyperun.py.template.
4569
4570 """
4571 if atend:
4572 sys.path = sys.path + [d]
4573 else:
4574 sys.path = [d] + sys.path
4575
4577 try:
4578 return v.tolist()
4579 except AttributeError:
4580 return v
4581
4583 """OBSOLETE
4584
4585 Pype now extends the import module to provide this functionality
4586 automatically as needed.
4587
4588 """
4589 pass
4590
4607
4609 """Toplevel plot window for use with matplotlib.
4610
4611 >>> w = SimplePlotWindow('testplot', app)
4612 >>> w.fig.clf()
4613 >>> a = w.fig.add_subplot(1,1,1)
4614 >>> a.plot(np.random.random(200))
4615 >>> a.set_xlabel('xlabel')
4616 >>> a.set_ylabel('xlabel')
4617 >>> a.set_title('title')
4618 >>> w.drawnow()
4619
4620 Note: Unless userclose is set to True, the user will not
4621 be able to close the plot window and it must be closed
4622 programmatically by calling widget.destroy() method..
4623
4624 """
4625
4626 - def __init__(self, name, app=None, userclose=False, *args, **kw):
4627 from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
4628 from matplotlib.figure import Figure
4629
4630 apply(Toplevel.__init__, (self,), kw)
4631
4632 self.title(name)
4633 self.fig = Figure()
4634 self._canvas = FigureCanvasTkAgg(self.fig, master=self)
4635 self._canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
4636 if not userclose:
4637 self.protocol("WM_DELETE_WINDOW", lambda: 1)
4638
4639 if app:
4640
4641
4642
4643 app.setgeo(self, default='+20+20')
4644
4647
4650 import peyetribe
4651
4652 self.h = peyetribe.EyeTribe(host=host)
4653 self.h.connect()
4654 self.h.pullmode()
4655 self.poll()
4656
4658 self.e = self.h.next()
4659 self.status = self.e.statestr()
4660 self.x = int(round(self.e.raw.x))
4661 self.y = -int(round(self.e.raw.y))
4662
4664 while 1:
4665 self.poll()
4666 if 'G' in self.status:
4667 dacq_set_xtracker(self.x, self.y, 0)
4668
4670 Logger('pype: starting itribe thread\b');
4671 thread.start_new_thread(self.run, ())
4672
4673
4675 """Display a about/splash screen. If transient is True, then return,
4676 but use callback to close it after 10 secs, otherwise user must close.
4677
4678 """
4679
4680 from PIL import Image, ImageTk
4681
4682 im = ImageTk.PhotoImage(Image.open(file))
4683 w = Toplevel(background='white')
4684 if timeout:
4685 w.overrideredirect(1)
4686 w.withdraw()
4687
4688 f = Frame(w, background='white', relief=RIDGE)
4689 f.pack(expand=1, fill=BOTH)
4690 icon = Label(f, relief=FLAT, image=im, background='white')
4691 icon._image = im
4692 icon.pack(expand=1, fill=BOTH, side=TOP)
4693
4694 t = "\n".join(
4695 (
4696 "pype: python physiology environment",
4697 "Version %s" % pypeversion.PypeVersion,
4698 "Copyright (c) 1999-2013 James A. Mazer",
4699 "Build Date: %s" % pypeversion.PypeBuildDate,
4700 ))
4701 text = Label(f, text=t, background='white')
4702 text.pack(expand=1, fill=BOTH, side=BOTTOM)
4703
4704 w.update_idletasks()
4705 screencenter(w)
4706 w.deiconify()
4707 w.update_idletasks()
4708 if timeout:
4709 w.after(timeout, w.destroy)
4710
4711 if __name__ == '__main__':
4712 sys.stderr.write('%s should never be loaded as main.\n' % __file__)
4713 sys.exit(1)
4714