Package pype :: Module pype
[frames] | no frames]

Source Code for Module pype.pype

   1  # -*- Mode: Python; tab-width: 4; py-indent-offset: 4; -*- 
   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  #  external python dependencies 
  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  # THIS IS UGLY! There are somethings you can't/shouldn't do suid-root: 
  36  # 
  37  #  1. matplotlib-0.9.9 uses some sort of cache file that's not compatible 
  38  #     with later versions. 
  39  #  2. you don't want to initialize matplotlib as root -- doesn't work, so 
  40  #     we need to drop root access and restore after init 
  41  #  3. FIXME With Anaconda2 (python2.7), you can't import numpy as root. You 
  42  #     get an error about "_remove_dead_weakref" not found. This turns 
  43  #     out to be be an issue only if the app is suid-root. But giving up 
  44  #     root permissions doesn't fix the problem. Still broken! 
  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      # force matplotlib to use a version-specific config file, otherwise 
  55      # incompatiblities between cache files (0.9.9 vs 1.1.1) will cause 
  56      # problems (ie, different ubuntu versions) 
  57      os.environ['MPLCONFIGDIR'] = '%s-%s' % (matplotlib.get_configdir(), 
  58                                              matplotlib.__version__) 
  59      # make sure this dir exists (really only for 0.9.9..) 
  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  #  pype internal modules 
  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)               # validate the mers-sieve.. 
  95   
96 -def MYNAME(level=0):
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
106 -def _base_ptables():
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'), # global, but handled by task 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'), # global, but handled by pype itself 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 #pyesno('save_ain2', 0, 'save raw AIN 2 (photodiode) -- no effect'), 218 #pyesno('save_ain3', 0, 'save raw AIN 3 (spk detection) -- no effect'), 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
274 -def addicon(app, button, file):
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
280 -class PypeApp(object): # !SINGLETON CLASS!
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):
294 """This ensure a single instantation. 295 """ 296 if cls._instance is None: 297 cls._instance = super(PypeApp, cls).__new__(cls) 298 cls._init = 1 299 else: 300 cls._init = 0 301 return cls._instance
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 # from python stdlib: 324 import signal 325 326 # from pype libraries: 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 # no console window to start with.. 339 self.conwin = None 340 341 # get uid for current EFFECTIVE user id -- at this point it 342 # should be the user, not root.. 343 self.uname = pwd.getpwuid(os.getuid())[0] 344 345 # Load user/host-specific config data and set appropriate 346 # defaults for missing values. 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 # FULLSCREEN's really shouldn't be used for anything except 369 # dedicated xserver's for physiology. The following tried 370 # to prevent you from locking up a single-headed machine 371 # by starting in fullscreen mode: 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 # you can set debug mode by: 378 # - running with --debug argument 379 # - setenv PYPEDEBUG=1 380 # - setting DEBUG: 1 in the Config.$HOST file 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 # these MUST be set from now on.. 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 # setup global log/console window (see guitools.py) 439 # all queued (ie, old) Logger messages will get pushed 440 # into the console window are this point.. 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 # make top-level menubar for task loaders that can be 515 # disabled when it's not safe to load new tasks... 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 # record state is the TTL line used to sync pype to 540 # and external recording system (plexon, etc) 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) # entire lower section.. not visible! 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 # reaction time plot window 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 # psth plot window -- only for recording sessions 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 # ELOG should specify path to the elog module 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 #Logger("pype: no ELOG setup -- using old-style cell counter.\n") 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 # quit should be last entry 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 # 'cell' (exper in elog database) not changable in elog mode: 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 # JAM 07-feb-2003: Compute ppd values based on current setup. 680 # Then place these in the rig menu automatically. 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 = [] # list of recently used tasks 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 # make sure Notebook is big enough for buttons above to show up 893 #pw.setnaturalsize() 894 book.setnaturalsize(pageNames=None) 895 896 # extract some startup params.. these are read only ONCE! 897 self.pix_per_dva = float(self.rig_common.queryv('mon_ppd')) 898 899 # keyboard/event input que 900 self.tkkeyque = EventQueue(self.tk, '<Key>') 901 902 # history info 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 # make sure we're root, if possible 908 root_take() 909 910 self.dacq_going = 1 911 self.eyeset() 912 if self.eyemouse: 913 # this sets the gain/offset exactly for the current 914 # display parameters.. no f8 should be required! 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 # stash info on port states 920 # this is a hack -- some buttons are down/true others are 921 # up/true .. lets user sort it all out.. 922 self.flip_bar = self.config.iget('FLIP_BAR') 923 924 # beep with each reward? 925 self.reward_beep = self.config.iget('REWARD_BEEP') 926 927 # NOTE: 928 # Framebuffer initialization will give up root access 929 # automatically.. so make sure you start the dacq process 930 # first (see above). 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 # added automatic detection of framerate (13-jan-2004 JAM): 944 # Wed Apr 27 17:37:26 2011 mazer moved it into init_framebuffer 945 #fps = self.init_framebuffer() 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 # userdisplay: shadow of framebuffer window 956 # xscale=1./self.config.fget('XSCALE'), 957 # yscale=1./self.config.fget('YSCALE'), 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 # this must be defered until sync spot info is computed.. 981 self.udpy.drawaxis() 982 983 # drop root access 984 # Tue Jul 12 10:01:53 2005 mazer -- 985 # dropping root access causes the ALSA libs to bitch. I'm 986 # not sure what the problem is, but we're generally having 987 # also of sound problems recently.. 988 root_drop() 989 Logger('pype: dropped root access\n') 990 991 # Sat Sep 3 16:56:50 2005 mazer 992 # as of now, fixation break and bartransitions received via 993 # the interupt handler (SIGUSR1) are handled the first time 994 # idlefn() gets called after the interupt. The interupt handler 995 # just sets these flags and exceptions are raised at the next 996 # possible time. Therefore, you must continue to call idlefn 997 # (although you can now use fast=1 to minimize CPU usage) if 998 # you can FixBreak and BarTransition to work.. 999 self.clear_pending_ints() 1000 self.lastint_ts = None # exact time of last interupt 1001 self.idle_queue = [] 1002 1003 # catch interupts from the das_server process indicating 1004 # bar state transitions and fixation breaks 1005 1006 # make sure bar touches don't cause interupts 1007 dacq_bar_genint(0) 1008 dacq_joy_genint(0) 1009 1010 # disable interupts and default to interupt queuing 1011 self.interupts(enable=0, queue=1) 1012 1013 # setup interupt handler 1014 signal.signal(signal.SIGUSR1, self._int_handler) 1015 1016 # we're now ready to receive interupts, but they won't come 1017 # through until you do in your task: 1018 # (app/self).interupts(enable=1, queued=0|1) 1019 1020 # 1021 # External DACQ interface -- 1022 # this is for multichannel recording systems: 1023 # - Plexon MAP box (via PlexNet API) 1024 # - Tucker-Davis (via pype's tdt.py client-server module) 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 # dir must have trailing backslash 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
1117 - def open_elog(self):
1118 # open elog for this animal, today w/o asking for confirmation 1119 animal = self.sub_common.queryv('full_subject') 1120 os.system('elog -y -animal=%s -today &' % (animal,))
1121
1122 - def elrestart(self):
1123 dacq_elrestart()
1124
1125 - def about(self, timeout=None):
1126 show_about(os.path.join(self.pypedir,'lib', 'logo.gif'), timeout)
1127
1128 - def queue_action(self, inms=None, action=None):
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
1142 - def _whereami(self):
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
1152 - def migrate_pypestate(self):
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 # ppd's here are because previously this was set in the 1162 # wrong (but still functional) units 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 # if you're migrating, then assume that you 1169 # don't want any affine transform (yet) 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
1226 - def _candyplay(self, fn):
1227 try: 1228 fn(self) 1229 finally: 1230 self.showtestpat()
1231
1232 - def _getparams_frompypefile(self):
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
1253 - def _findparam(self):
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 # skip title slots.. 1269 pass
1270
1271 - def keyboard(self):
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
1290 - def set_state(self, running=-1):
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
1311 - def _show_stateinfo(self):
1312 barstate = self.bardown() # this will handle BAR_FLIP setting 1313 if self.itribe: 1314 t = self.itribe.status + ' ' 1315 else: 1316 t = '' 1317 1318 if not self.udpy.isvisible(): 1319 # only show bar state if udpy not visible 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
1350 - def isrunning(self):
1351 """Query to see if a task is running. 1352 1353 :return: (bool) 1354 1355 """ 1356 return self.running
1357
1358 - def set_result(self, type=None):
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 # combination of self.tally() and self.history() 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
1379 - def get_result(self, nback=1):
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 # clear everything 1394 self.tallycount = {} 1395 elif not cleartask is None: 1396 # just clear current task data 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 # add new data to current task stats 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
1475 - def getcommon(self):
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 # for backward compatibility: 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 # add flag for current notion of where spikes are really 1494 # coming from (currently: 'None', 'plexon' or 'tdt') 1495 if self.xdacq: 1496 d['datasrc'] = self.xdacq 1497 else: 1498 d['datasrc'] = 'None' 1499 1500 return d
1501
1502 - def make_taskmenu(self, menubar):
1503 # add ~/.pyperc/Tasks/*.py (in any) 1504 self.tasklist = {} 1505 1506 self.add_tasks(menubar, '~pyperc', pyperc('Tasks')) 1507 1508 # add each subdir in ~/.pyperc/Tasks (unless prefixed with _) 1509 files = glob.glob(pyperc('Tasks/*')) 1510 for d in files: 1511 m = posixpath.basename(d) 1512 # Tue Jan 4 11:50:19 2005 mazer -- skip _* dirs (disabled) 1513 if os.path.isdir(d) and not (m[0] == '_'): 1514 self.add_tasks(menubar, "~"+m, d) 1515 1516 # add tasks in current working dir (in any) 1517 self.add_tasks(menubar, 'cwd', '.') 1518 1519 # TASKPATH can be set in the Config file and/or as env var.. 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
1533 - def _get_taskheader(self, filename):
1534 # read up to ~50 lines of text from specified file looking 1535 # for a task header line (starts with '#pypeinfo;') 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
1552 - def make_recent(self):
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, '') # ensure trailing delim 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 # this is needed for pype to find non-task files in the dir 1591 _addpath(dirname, atend=1) 1592 1593 tasks.sort() 1594 # right prevents long menus from generating incorrect task selection! 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 #sys.stderr.write('Warning: duplicate task name -- %s\n' % t) 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
1618 - def make_toolbar(self, parent):
1619 import imp 1620 1621 if self.config.get('TOOLS', None) is None: 1622 return 1623 1624 toolbar = None 1625 for tool in self.config.get('TOOLS').split(':'): 1626 try: 1627 t = os.path.basename(tool).replace('.py','') 1628 file, fullpath, descr = imp.find_module(t) 1629 d = string.join(fullpath.split('/')[:-1],'/') 1630 if toolbar is None: 1631 toolbar = Frame(parent, borderwidth=2) 1632 toolbar.pack(anchor=CENTER, padx=10, pady=10) 1633 b = Button(toolbar, text=t[:3], 1634 background='white', 1635 font=('Andale Mono', 8), 1636 command=lambda s=self, t=t, d=d: s.loadtask(t, d)) 1637 b.pack(side=LEFT) 1638 self.balloon.bind(b, fullpath) 1639 except ImportError: 1640 pass
1641
1642 - def prevtask(self):
1643 if len(self.recent) > 1: 1644 (name, dir) = self.recent[1] 1645 self.loadtask(name, dir)
1646
1647 - def unloadtask(self):
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 # don't allow loading of task while running! 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() # unload current, if it exists.. 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 # in case loading throws an exception: 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 # module must provide a 'main' function that is: 1739 # 1. a function that takes one arg (app; PypeApp handle, 1740 # ie, self). 1741 # 2. When called, binds something to the start function 1742 # by calling app.set_startfn, eg: 1743 # t = make_some_task_object_with_state() 1744 # app.set_startfn(lambda app,task=t: task.start_run(app)) 1745 # module MAY optionally provide a cleanup function that 1746 # also takes 'app', that will be called when the task is 1747 # unloaded. 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
1760 - def set_canvashook(self, fn=None, data=None):
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
1783 - def udpy_note(self, t=''):
1784 self.udpy.note(t)
1785
1786 - def cleargains(self):
1787 """Clear eyecal gains. 1788 1789 :return: nothing 1790 1791 """ 1792 self.eyeset(xgain=1.0, ygain=1.0)
1793
1794 - def clearaffine(self):
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 # convert 3x3 matrix into CSV string for param table storage 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 # it is possible to merge xgain/xoff/ygain/yoff into a single 1840 # matrix, by: 1841 # A[0,0] *= xgain; A[2,0] += xoff; 1842 # A[1,1] *= ygain; A[2,1] += yoff; 1843 # but there is a problem in that the offset's are in pixels 1844 # and need to be in ETUs.. so forget it for now.. 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
1855 - def init_dacq(self):
1856 1857 # EYELINK_OPTS can be defined in the pyperc config file and 1858 # should be a colon-delimited list of commands to be sent to 1859 # the eyelink one at a time. Most users should NEVER use this 1860 # feature, it's really just for testing and debugging. 1861 # 1862 # NOTE: No attempt is made to make sure the built in commands 1863 # don't conflict with user commands. 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 # <0: fatal error -- exit regardless of force.. 1890 # =0: overridable error, ok to retry.. 1891 # =1: success 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 # Tue Jan 8 15:08:12 2013 mazer 1901 # - This basically works -- the lag between the comedi 1902 # datastream and the labjack is ~0.7ms as far as I can 1903 # tell (using cross correlation against FM chirps). 1904 # - However, integration is not complete... not sure how 1905 # I want to do this -- could use it to get rid of 1906 # comedi_server completely.. 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 # trip labjack once to force init and load 1929 self.u3.start() 1930 time.sleep(0.01) 1931 self.u3.stop(wait=1)
1932
1933 - def init_framebuffer(self):
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 # turn off the xserver's audible bell and screensaver! 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
1979 - def _gethostname(self, fqdn=False):
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
1994 - def _tallyfile(self, save=1):
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
2021 - def _savestate(self):
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
2028 - def _loadstate(self):
2029 # rig,sub,ical etc are automatically loaded by the ptable object.. 2030 self._tallyfile(save=0)
2031
2032 - def _runstats_update(self, clear=None, resultcode=None):
2033 if clear is not None: 2034 self._runstats = {} 2035 self._runstats['ncorrect'] = 0 2036 self._runstats['nerror'] = 0 2037 self._runstats['nui'] = 0 # really sequential UI's 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
2086 - def _start(self):
2087 self._start_helper(temp=None)
2088
2089 - def _starttmp(self):
2090 self._start_helper(temp=1)
2091
2092 - def _stopabort(self):
2093 self._doabort = 1 2094 self.running = 0
2095
2096 - def _start_helper(self, temp=None):
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 # abort if validatefn returns False 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 # If elog is in use, then at the end of each run insert 2136 # (or update, since force=1) data on this file in the 2137 # sql database. 2138 # Do this at the START of the run so it can be seen right 2139 # away.. 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 #del self._exper 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 # start new block in current tank, this includes resting the 2160 # trial counter.. 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 # clear/reset result stack at the start of the run.. 2166 self.set_result() 2167 2168 # reset histogram/stats at start of run 2169 self.update_rt() # RT histogram 2170 self.update_psth() # PSTH (spike data) 2171 self._runstats_update(clear=1) # clear block stats 2172 2173 # make sure graphic display is visible 2174 if self.psych: 2175 self.fb.screen_open() 2176 2177 # call task-specific start function. 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 # either there was an error OR the task 2191 # completed normally, ensure proper cleanup 2192 # gets done: 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 # recording's done -- direct output back to TempBlk 2209 self.tdt.newblock(record=0)
2210
2211 - def query_ncorrect(self):
2212 """Get number of correct (C) trials since run start 2213 2214 :return: (int) C count 2215 2216 """ 2217 return self._runstats['ncorrect']
2218
2219 - def query_nerror(self):
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
2227 - def query_nui(self):
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
2236 - def query_ntrials(self):
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
2248 - def _new_cell(self):
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 # called before start to validate params
2303
2304 - def _shutdown(self):
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() # Do this NOW in case of lockup on exit 2320 Logger('pype: user shutdown/close -- terminating.\n') 2321 if self.server: 2322 self.server.shutdown() 2323 self.terminate = 1
2324
2325 - def ts(self):
2326 """Get current timestamp. 2327 2328 :return: (ms) current 'time', as determined by the DACQ module. 2329 2330 """ 2331 return dacq_ts()
2332
2333 - def dotrialtag(self, reset=None):
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
2348 - def eye_txy(self):
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
2362 - def eyepos(self):
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
2371 - def looking_at(self, x=None, y=None):
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 # assume x is a fixwin object -- sz is ignored.. 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 # assume eye is looking at specified point -- for mouse clicks! 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 #FIXME: this is some sort of race condition -- interupt 2434 # can be generated after task stops running.. just ignore 2435 # it for now.. 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 # note: work with copy, to avoid problems with 'remove' below.. 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 # drawin the plexon buffer to prevent overflow when 2488 # pype is idle... 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 # If gamepad/joystick attached -- use as follows: 2500 # 0,1,2,3 --> SIMULATED DIGITAL I/O LINES (comedi_server handles) 2501 # 4,5,6,7 --> special functions, see below 2502 # NOTE: 0,1,2.. refers to "labels" 1,2,3... 2503 # 2504 # 0 ("1"): Response Bar 2505 # XX 1 ("2"): Juice squirter (aka SW1) 2506 # XX 2 ("3"): userdefined switch 2 (aka SW2) 2507 # 3 ("4"): not used 2508 # 4 ("5"): alternate stop button 2509 # 5 ("6"): alternate F8 (zero eye tracker) 2510 # 6+7 ("7"+"8"): emergency quit hotkey 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 # user hit abort/stop button -- this will abort next 2523 # time task calls idlefn... 2524 self._doabort = 0 2525 raise UserAbort 2526 2527 # process keys from the TkInter GUI (user display etc) 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 # process keys from the framebuffer window 2546 if 'f8' in self.fb.checkkeys(): 2547 self.eyeshift(zero=1) 2548 self.con('[f8]', color='red') 2549 # debounce: wait for keys to be released.. 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 # debounce: wait for keys to be released.. 2557 while not self.fb.checkkeys() == []: 2558 pass 2559 raise UserAbort 2560 2561 if self.eyemouse: 2562 # Use framebuffer mouse position as eye position 2563 # and button1 for bar state. Note that if you hold 2564 # down both left and right shift keys, this aborts 2565 # current trial and stops run immediately. 2566 # 2567 # mx,my are in physical coords (0-DPYW, 0-DPYH) 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 # inject mouse position into 2575 dacq_set_xtracker(mx, my, 0) 2576 try: 2577 # check to see if bar state has changed.. 2578 if not b1 == self._lastb1: 2579 self.eyebar = b1 # stash for bardown() method 2580 self._lastb1 = b1 # maybe generate interupt 2581 self._int_handler(None, None, iclass=1, iarg=b1) 2582 except AttributeError: 2583 # first time through -- initialize 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
2621 - def _drain(self):
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
2648 - def warn_run_start(self):
2649 if self.sub_common.queryv('warningbeeps'): 2650 beep(500, 50, wait=0) 2651 beep(1000, 50, wait=0)
2652
2653 - def warn_run_stop(self):
2654 if self.sub_common.queryv('warningbeeps'): 2655 beep(1000, 50, wait=0) 2656 beep(500, 50, wait=0)
2657
2658 - def warn_trial_correct(self, flash=None):
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
2673 - def warn_trial_incorrect(self, flash=None):
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
2687 - def dropsize(self):
2688 """Query dropsize. 2689 2690 :return: (ms) mean dropsize 2691 2692 """ 2693 return self.sub_common.queryv('dropsize')
2694
2695 - def dropvar(self):
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 # if var==0, then ms is the exact time of the drop, if var>0 2714 # then drop time is selected randomly from a normal distribution 2715 # with mean=ms, std=var 2716 2717 # becasue the distribution is normal, very small and very 2718 # large numbers can (rarely) come up, so you MUST clip the 2719 # distribution to avoid pype locking up in app._reward_finisher()... 2720 2721 if ms is None: 2722 ms = int(round(multiplier * float(self.dropsize()))) 2723 sigma = self.dropvar()**0.5 2724 else: 2725 # user specified ms, no variance 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 # Fri Dec 8 14:02:49 2006 mazer 2756 # automatically encode the actual reward size (ms open) in 2757 # the data file 2758 self.encode('ACT_' + REWARD + '%d' % actual_reward_size) 2759 2760 return actual_reward_size
2761
2762 - def _reward_finisher(self, t):
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
2774 - def _juice_on(self):
2775 """Open juice solenoid. 2776 2777 """ 2778 dacq_juice(1)
2779
2780 - def _juice_off(self):
2781 """Close juice solenoid. 2782 2783 """ 2784 dacq_juice(0)
2785
2786 - def _juice_drip(self, ms):
2787 dacq_juice_drip(int(round(ms)))
2788
2789 - def barchanges(self, reset=None):
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
2806 - def bar_genint(self, enable=1):
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 # also: clear saved flag -- no more interupts 2826 # in theory you could loose a pending interupt here, but it 2827 # should be unlikely. 2828 self._post_bartransition = 0
2829
2830 - def joy_genint_getbutton(self):
2831 return self._joypad_intbut
2832
2833 - def joy_genint(self, enable=1):
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 # also: clear saved flag -- no more interupts 2863 # in theory you could loose a pending interupt here, but it 2864 # should be unlikely. 2865 self._post_joytransition = 0
2866
2867 - def set_alarm(self, ms=None, clear=None):
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
2891 - def clear_pending_ints(self):
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 #print 'INT iclass=%d arg=%d' % (iclass, iarg,) 2937 2938 if iclass == 666: 2939 self.running = 0 2940 warn(MYNAME(), 'Lost eyelink connection!', wait=0) 2941 return 2942 2943 # for INT_FATAL interupts -- handle regardless of allow_ints 2944 # flag -- this probably means eyelink connection died.. 2945 if not self.allow_ints: 2946 # interupts are disabled -- do nothing 2947 return 2948 2949 # latch interupts off -- must explictly enable interupts to use again 2950 self.allow_ints = 0 2951 2952 # class/arg is: 2953 # 1: DIN transition (arg is input # that changed; bar==0) 2954 # --> this is also joypad/stick button #1 presses 2955 # 2: fixwin break (arg is meaningless -- always 0) 2956 # 3: alarm expired (arg is meaningless -- always 0) 2957 # 4: joypad/stick transition (button # > 1) 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
2992 - def bardown(self):
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
3007 - def barup(self):
3008 """Query to see if touchbar is touched (aka 'down'). 3009 3010 """ 3011 return not self.bardown()
3012
3013 - def joybut(self, n):
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
3028 - def joyaxis(self):
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
3097 - def close(self):
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 # turn bell and screensaver back on -- this isn't really 3111 # quite right -- it always gets turned back on, even if they 3112 # weren't on to start with.. 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 # only do the state thing if in GUI mode 3135 self._savestate() 3136 Logger('pype: saved state.\n') 3137 Logger('pype: bye bye.\n')
3138
3139 - def repinfo(self, msg=None):
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
3150 - def taskname(self):
3151 """Query name of task. 3152 3153 :return: (string) current task name (no extension) or none. 3154 3155 """ 3156 return self._taskname()
3157
3158 - def _taskname(self, taskname=[], path=[]):
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
3185 - def _set_recfile(self):
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
3199 - def set_userbutton(self, n, text=None, check=None, command=None):
3200 """Set callback and label for user-defined buttons. 3201 3202 """ 3203 if check: 3204 c = Checkbutton(self._userbuttonframe, 3205 text=text, relief=RAISED) 3206 else: 3207 c = Button(self._userbuttonframe, 3208 text=text, relief=RAISED, command=command) 3209 c.pack(expand=0, fill=X, side=TOP, pady=2) 3210 return c
3211
3212 - def taskbutton(self, text=None, check=None, command=None, size=8):
3213 """Create a task-button in the GUI and link to command. 3214 3215 """ 3216 if check: 3217 c = Checkbutton(self._userbuttonframe, 3218 text=text, anchor=W, relief=RAISED) 3219 else: 3220 c = Button(self._userbuttonframe, 3221 text=text, anchor=W, relief=RAISED, command=command) 3222 c.pack(expand=0, fill=X, pady=2) 3223 3224 return c
3225
3226 - def record_start(self):
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 # bump up the data collect process priorities 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 # if rt_sched is set in the rig menu, switch scheduler mode 3242 # to real time for the duration of the trial 3243 dacq_set_rt(1) 3244 3245 self.record_buffer = [] 3246 3247 # Thu Oct 23 15:51:34 2008 mazer 3248 # 3249 # - The record_state() function can now block for 250ms to 3250 # ensure that the plexon has time to register onset of new 3251 # trial, therefore, the timestamp should be recorded when 3252 # record_state() returns, not before it's called. 3253 # 3254 # - So the t=self.encode(START) has been moved from right 3255 # before the self.record_state(1) to right after 3256 3257 # tell plexon trial is BEGINNING 3258 self.record_state(1) 3259 if self.u3test: 3260 self.u3.start() 3261 t = self.encode(START) 3262 3263 # Mon Oct 27 12:56:47 2008 mazer 3264 # - log full 'time of day' for start event 3265 self.encode('TOD_START %f' % time.time()) 3266 3267 self.recording = 1
3268 3269 # save recording start time -- this is used to ensure 3270 # 250ms between start/stop events for proper plexon sync 3271
3272 - def record_stop(self):
3273 """Stop recording data. 3274 3275 End of trial clean up. 3276 3277 """ 3278 3279 # Thu Oct 23 15:53:43 2008 mazer 3280 # see note about re self.encode(START) -- this has been moved 3281 # to right AFTER the self.record_state(0) call to avoid 3282 # problems with plexon sync.. 3283 3284 # tell plexon trial is OVER 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 # Mon Oct 27 12:56:47 2008 mazer 3292 # - log full 'time of day' for start event 3293 self.encode('TOD_STOP %f' % time.time()) 3294 3295 # bump back down the data collect process priorities 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
3304 - def status_plex(self):
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
3313 - def record_state(self, state):
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 # this is (was?) causing a wedge!!! 3340 dacq_dig_out(2, state) 3341 self._last_recstate = dacq_ts()
3342
3343 - def eyetrace(self, on):
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 # only allow turn off once.. 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 # Mon Aug 19 12:27:30 2013 mazer 3402 # this worked only for tuples, should also work for lists now. 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
3411 - def get_encodes(self):
3412 return self.record_buffer
3413
3414 - def get_spikes_now(self):
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 #ppd = self.rig_common.queryv("mon_h_ppd") # float 3467 #thresh = thresh * ppd 3468 #if maxthresh: maxthresh = maxthresh * ppd 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
3482 - def get_eyetrace_now(self, raw=0):
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
3516 - def get_phototrace_now(self):
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
3533 - def get_events_now(self):
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 # force taskinfo to be a tuple.. 3575 if type(taskinfo) != TupleType: 3576 taskinfo = (taskinfo,) 3577 3578 # stop eye recording, just in case user forgot. 3579 self.eyetrace(0) 3580 3581 # clear the idlefn queue, just in case.. 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 # be careful here -- if you're trying to look at the photodiode 3621 # signals, you'd better not set fast_tmp=1... 3622 ndups = 0 3623 if not fast_tmp or self._show_eyetrace.get(): 3624 for i in range(0,n): 3625 # convert dacq_adbuf_t() in 'us' to 'ms' for saving 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) # photo diode 3636 s0[i] = dacq_adbuf_c3(i) # spike detect 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 ###############################################################3 3645 # (starting) Thu Oct 21 14:38:49 2010 mazer 3646 # look for duplicates in the time stream -- this means 3647 # something's wrong with comedi_server or passing doubles 3648 # around... 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 ###############################################################3 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 # ..only do this if there's real data to write.. 3671 3672 # dacq_ts0 is the time at which comedi_server was initialized, 3673 # which is time zero as far as pype is concerned, since labjack 3674 # data is timestamped from clock_monotonic, we need to 3675 # subtract of ts0 from incoming timestamps 3676 # 3677 # Mon Jan 7 15:12:15 2013 mazer 3678 # this looks like the two are synced to within 0.7ms (LJ 3679 # seems to lag the comedi stream by about 0.7 ms). 3680 # Tue Mar 27 10:56:09 2018 mazer 3681 # confirmed: <1ms lag between u3 and das1602 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. # convert s->ms 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 # Completely wipe the buffers -- don't let them accidently 3717 # get read TWICE!! They're saved as self/app.eyebuf_[xyt] 3718 # in case you wawnt them for something.. 3719 dacq_adbuf_clear() 3720 3721 # insert these into the param dictionary for later retrieval 3722 params['PypeBuildDate'] = pypeversion.PypeBuildDate 3723 params['PypeBuildHost'] = pypeversion.PypeBuildHost 3724 3725 # these are git/svn info (if available) 3726 params['PypeVersionInfo'] = pypeversion.PypeVersionInfo 3727 params['PypeVersionID'] = pypeversion.PypeVersionID 3728 3729 # starting with pype3 (string set in mkpypeversion.sh) 3730 params['PypeVersion'] = pypeversion.PypeVersion 3731 3732 # pype internal ('pi') -- user tagged this trial (f7)? 3733 params['piTrialTag'] = tag 3734 3735 # save record_id in parameter table for easy access 3736 params['record_id'] = tag 3737 3738 rec = None 3739 3740 if not fast_tmp and self.record_file: 3741 # dump the event stream 3742 info = (resultcode, rt, params) + taskinfo 3743 3744 # Wed Sep 11 14:36:42 2002 mazer 3745 # 3746 # Structure of the saved record (mirrored in pypdata.py): 3747 # 3748 # rec[0] record type STRING: ('ENCODE' usually) 3749 # rec[1] info TUPLE: (resultcode, rt, paramdict, taskinfo) 3750 # taskinfo can be ANYTHING user wants to 3751 # save in the datafile 3752 # rec[2] event LIST: [(time, event), (time, event) ...] 3753 # rec[3] time VECTOR (list) 3754 # rec[4] eye x-pos VECTOR (list) 3755 # rec[5] eye y-pos VECTOR (list) 3756 # rec[6] LIST of photodiode time stamps 3757 # rec[7] LIST of spike time stamps 3758 # rec[8] record_id (SCALAR; auto incr'd after each write) 3759 # rec[9] raw photodiode response VECTOR 3760 # rec[10] raw spike response VECTOR 3761 # rec[11] TUPLE of raw analog channel data 3762 # (chns: 0,1,2,3,4,5,6), but 2 & 3 are same as 3763 # rec[9] and rec[10], so they're just None's in 3764 # this vector to save space. c5 and c6 aren't 3765 # currently implemented 3766 # rec[12] pupil area data (if available) in same format 3767 # as the eye [xy]-position data above 3768 # (added: 08-feb-2003 JAM) 3769 # rec[13] on-line plexon data via PlexNet. This should be 3770 # a list of (timestamp, unit) pairs, with timestamps 3771 # in ms and unit's following the standard plexon 3772 # naming scheme (01a, 02a, 02b etc..) 3773 # (added: rec[13] 31-oct-2005 JAM) 3774 # rec[14] eyenew data (added: Fri Apr 8 15:27:34 2011 mazer ) 3775 3776 if self.xdacq == 'tdt': 3777 # insert tdt tank info into the parameter table for this 3778 # trial so we can recover the data later. 3779 (server, tank, block, tnum) = self.tdt.getblock() 3780 3781 # str() here convert UTF8 strings back to plain old ascii, 3782 # which is the only thing the p2m can currently handle. small 3783 # risk here -- don't use international chars for datafile names! 3784 # 3785 # NOTE: first trial has tdt_tnum == 1, not zero!!!! 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), # photo diode trace (analog) 3803 _tolist(s0), # spike detect trace (TTL) 3804 ( 3805 _tolist(ain0), # analog input channel 0 3806 _tolist(ain1), # analog input channel 1 3807 None, # photo diode trace (dup!) 3808 None, # spike detect trace (dup!) 3809 _tolist(ain5), # analog input channel 5 3810 _tolist(ain6), # analog input channel 6 3811 _tolist(ain7), # analog input channel 7 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
3834 - def record_split(self, rec):
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
3853 - def record_note(self, tag, note):
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
3865 - def _guess_fallback(self):
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 # generate list of files (including zipped files) 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
3900 - def _guess_elog(self):
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 # search database based on full_animal (animal=full_subject) 3915 exper = elogapi.GetExper(full_animal) 3916 if exper is None: 3917 # create first experiment based on 'subject', ie, file prefix.. 3918 exper = "%s%04d" % (animal, 1) 3919 # update cell slot in worksheets with exper 3920 self.sub_common.set('cell', exper) 3921 3922 if self.training: 3923 exper = exper[:-4] + '0000' 3924 self._exper = exper 3925 3926 # now find next avilable number in the sequence 3927 pat = "%s.*.[0-9][0-9][0-9]" % (exper, ) 3928 # generate list of files (including zipped files) 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
3942 - def _guess(self):
3943 if self.use_elog: 3944 return self._guess_elog() 3945 else: 3946 return self._guess_fallback()
3947
3948 - def record_done(self):
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
3963 - def _record_selectfile(self, fname=None):
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 # stash the userparms dict in the file.. this contains 3996 # the monitor parameters, just in case they get lost 3997 # or forgotten.. 3998 self.write_userparams() 3999 #self.record_note('userparams', self.config.dict) 4000 4001 return 1
4002
4003 - def write_userparams(self):
4004 self.record_note('userparams', self.config.dict)
4005
4006 - def showtestpat(self):
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
4030 - def plotEyetracesRange(self, start=None, stop=None):
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
4044 - def _plotEyetraces(self, t, x, y, p0, s0, raster):
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 #pylab.xlim(start - t0, stop - t0) 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
4103 - def update_rt(self, infotuple=None):
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
4147 - def update_psth(self, data=None, trigger=PSTH_TRIG):
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 # no trigger event.. don't update.. 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 # how histogram first 2s of data in 50 ms bins; this really 4175 # should be settable by the task.. 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
4188 - def makeFixWin(self, x, y, tweak=0):
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 # base fixwin radius in pixels + task-specific teak 4204 r = self.sub_common.queryv('win_size') + tweak 4205 4206 # add eccentricity dependent adjustment factor: 4207 r = r + (self.sub_common.queryv('win_scale') * ((x**2 + y**2)**0.5)) 4208 4209 # vertical elongation factor (i.e., major/minor axis ratio) 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
4214 -def getapp():
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
4222 -def _hostconfigfile():
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
4269 -def _safeLookup(dict, key, default):
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
4280 -def subject():
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
4293 -def subjectrc(file=""):
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
4305 -def pyperc(file=""):
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
4321 -class FixWin(object):
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 # ellipsoid: multiplicate scale of ydim 4337 self.fwnum = 0 # hard coded..
4338
4339 - def __del__(self):
4340 self.interupts(enable=0) # disable interupt generation 4341 self.draw(clear=1) # clear from user display
4342
4343 - def interupts(self, enable=0):
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 # clear any pending INT and then enable 4356 self._post_fixbreak = 0 4357 dacq_fixwin_genint(self.fwnum, enable) 4358 else: 4359 # disable, then clear any pending INTs 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
4370 - def get(self):
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
4409 - def reset(self):
4410 dacq_fixwin_reset(self.fwnum)
4411
4412 - def on(self):
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
4420 - def off(self):
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
4428 - def inside(self):
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
4436 - def broke(self):
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
4444 - def break_time(self):
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
4468 -class Timer(object):
4469 - def __init__(self, on=True):
4470 if on: 4471 self.reset() 4472 else: 4473 self.disable()
4474
4475 - def disable(self):
4476 self._start_at = None
4477
4478 - def reset(self):
4479 """Reset timer. 4480 4481 :return: nothing 4482 4483 """ 4484 self._start_at = dacq_ts()
4485
4486 - def ms(self):
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
4497 -class Holder(object):
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
4523 -def _get_plexon_events(plex, fc=40000):
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 # drain rest of tank, then return 4556 else: 4557 # use fc (sample freq in hz) to convert timestmaps to ms 4558 events.append((int(round(float(ts - zero_ts) / fc * 1000.0)), 4559 Channel, Unit)) 4560 4561 return events
4562
4563 -def _addpath(d, atend=None):
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
4576 -def _tolist(v):
4577 try: 4578 return v.tolist() 4579 except AttributeError: 4580 return v # v is None (or some other non-numpy array object)
4581
4582 -def loadwarn(*args):
4583 """OBSOLETE 4584 4585 Pype now extends the import module to provide this functionality 4586 automatically as needed. 4587 4588 """ 4589 pass
4590
4591 -class EmbeddedFigure:
4592 """Create matplotlib figure embedded in tkinter parent window. 4593 4594 """ 4595
4596 - def __init__(self, parent, *args, **kwargs):
4597 from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 4598 from matplotlib.figure import Figure 4599 4600 self.fig = Figure(*args, **kwargs) 4601 self._canvas = FigureCanvasTkAgg(self.fig, master=parent) 4602 self._canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
4603
4604 - def drawnow(self):
4605 if matplotlib: 4606 self._canvas.show()
4607
4608 -class SimplePlotWindow(Toplevel):
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 # note: only position is remembered, not size -- this 4641 # is becaue teh FigureCanvasTkAgg has a size param we're 4642 # not using here.. 4643 app.setgeo(self, default='+20+20')
4644
4645 - def drawnow(self):
4646 self._canvas.show()
4647
4648 -class EyeTribeThreadRunner(object):
4649 - def __init__(self, host='127.0.0.1'):
4650 import peyetribe 4651 4652 self.h = peyetribe.EyeTribe(host=host) 4653 self.h.connect() 4654 self.h.pullmode() 4655 self.poll()
4656
4657 - def poll(self):
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
4663 - def run(self):
4664 while 1: 4665 self.poll() 4666 if 'G' in self.status: 4667 dacq_set_xtracker(self.x, self.y, 0)
4668
4669 - def start(self):
4670 Logger('pype: starting itribe thread\b'); 4671 thread.start_new_thread(self.run, ())
4672 4673
4674 -def show_about(file, timeout=None):
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