ListScheduler.py

The following is code for module listScheduler.py as it was on June 22nd 2020. This frozen copy is here for reference for the other pages explaining how the software works.

  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on August 6th 2018
  4. @author: wschnupp
  5. the "list scheduler" simply works through the list of stimuli
  6. defined by stimVarParams and stimVarValues
  7. for the number of times specified in stimListRepeats, 
  8. shuffling the presentation order of the stimVarValues on each run
  9. """
  10. import numpy as np
  11. import time, sys
  12. import psyPhysConfig as config
  13. if not hasattr(config,'maxResponseTime'):
  14.     config.maxResponseTime=15 # max interval between start spout lick and response in seconds
  15. verbose=False
  16. #%%
  17. def myvstack(Y,newRow):
  18.     if len(Y)==0:
  19.         Y=[newRow]
  20.     else:
  21.         if np.size(newRow)==0:
  22.             return Y
  23.         else:
  24.             Y=np.vstack((Y,[newRow]))  
  25.     return Y
  26. def permute(A,B):
  27.     Y=np.array([])
  28.     A=np.array(A)
  29.     B=np.array(B)
  30.     if A.size==0:
  31.         return []
  32.     for a in A:
  33.         a=np.array(a)
  34.         if B.size==0:
  35.             Y=myvstack(Y,a)
  36.         else:
  37.             for b in B:
  38.                 b=np.array(b)
  39.                 newRow=np.append(a,b)
  40.                 Y=myvstack(Y,newRow)
  41.     return Y
  42.     
  43. #%%
  44. class schedule: # the schedule object creates and maintains a list of stim parameters to work through.
  45.                 #  by permuting the params given.
  46.                 # Alternatively the schedule can be read from a CSV file.
  47.                 
  48.     def __init__(self, stimVarParams=[]):  
  49.         self.stimIndex=-1
  50.         self.stimVarParams=stimVarParams
  51.         # stimVarParams could be a file name for a CSV file instead of a list
  52.         #  of variable paramaters.
  53.         self.stimListRepeats=0
  54.         if type(stimVarParams) == str:
  55.             self.importFromCSV(stimVarParams)            
  56.         
  57.     def permute(self, valuesToPermute, stimListRepeats):       
  58.         # build the stimVarValues by permuting the valuesToPermute lists
  59.         self.stimListRepeats=stimListRepeats
  60.         ndims=np.shape(valuesToPermute)[0]
  61.         values=permute(valuesToPermute[0],[])
  62.         for ii in range(ndims-1):
  63.             values=permute(values,valuesToPermute[ii+1])
  64.         np.random.shuffle(values)
  65.         self.stimVarValues=values
  66.         for ii in range(stimListRepeats-1):
  67.             np.random.shuffle(values)
  68.             self.stimVarValues= np.concatenate((self.stimVarValues,values))
  69.     def importFromCSV(self, csvFileName):
  70.         import pandas as pd
  71.         tbl=pd.read_csv(csvFileName)
  72.         self.stimVarParams=np.array(tbl.columns)
  73.         self.stimVarValues=np.array(tbl)
  74.         
  75.     def repeatStimListNtimes(self, stimListRepeats):
  76.         values=self.stimVarValues.copy()
  77.         for ii in range(stimListRepeats-1):
  78.             self.stimVarValues= np.concatenate((self.stimVarValues,values))        
  79.             
  80.     def shuffleStimList(self):
  81.         #seedvalue = np.random.choice(10000,1)
  82.         np.random.seed(round(time.time()))
  83.         np.random.shuffle(self.stimVarValues)
  84.     def currentParams(self):
  85.         if not self.finished():
  86.             return self.stimVarValues[max(self.stimIndex,0)]
  87.         else:
  88.             return []
  89.     def nextParams(self):
  90.         self.stimIndex+=1
  91.         return self.currentParams()
  92.         
  93.     def paramNames(self):
  94.         return self.stimVarParams
  95.         
  96.     def finished(self):
  97.         return self.stimIndex >= len(self.stimVarValues)
  98.     
  99.     def readScheduleFromFile(self,defaultName=''):
  100.         #from tkinter import messagebox
  101.         from tkinter import filedialog
  102.         #import tkinter as tk
  103.         filename = filedialog.askopenfilename(title = "Open Stim Params Table:",initialfile=defaultName,filetypes = (("CSV","*.csv"),("all files","*.*")))
  104.         if filename=='':
  105.             # opening canceled. 
  106.             print('No valid file chosen.')
  107.             return ''
  108.         try:
  109.             self.importFromCSV(filename)
  110.             return filename
  111.         except:
  112.             print('Warning: Could not open file: '+filename)
  113.             return ''
  114. #%%
  115. class scheduler:
  116.     def __init__(self, detectors,stimulator,dataHandler,schedule):
  117.         self.detectors=detectors
  118.         self.stimulator=stimulator
  119.         self.dataHandler=dataHandler
  120.         self.schedule=schedule
  121.         self.lastAction=time.time()
  122.         self.oldStatus=""
  123.         
  124.     def start(self):
  125.         config.status=""
  126.         self.broadCastStatus('start')
  127.         self.nextJob=self.chooseNextTrial
  128.         self.stimStarted=time.time()
  129.         self.timeZero=time.time()
  130.         self.processJobs()
  131.         
  132.     def processJobs(self):
  133.         # now keep working through jobs
  134.         while self.nextJob != None:
  135.             if config.status=="abort":
  136.                 print('Schedule aborted.')
  137.                 self.nextJob=None
  138.             else:
  139.                 self.nextJob=self.nextJob()
  140.             
  141.     def didSucceed(self,returnCode):
  142.         # status change functions of modules may return nothing or a boolean
  143.         if returnCode is None:
  144.             return True
  145.         return returnCode
  146.                
  147.     def broadCastStatus(self,aStatus):
  148.         # only broadcast new status if it has changed
  149.         if aStatus==self.oldStatus:
  150.             return
  151.         self.oldStatus=aStatus
  152.         if verbose:
  153.             print()
  154.             print('#### New status: '+aStatus)   
  155.         sys.stdout.flush()
  156.         if hasattr(self.stimulator,'statusChange'):
  157.             success=self.didSucceed(self.stimulator.statusChange(aStatus))
  158.             while not success:
  159.                 time.sleep(0.1)
  160.                 success=self.didSucceed(self.stimulator.statusChange(aStatus))
  161.         if not self.detectors is None:
  162.             if hasattr(self.detectors,'statusChange'):
  163.                 success=self.didSucceed(self.detectors.statusChange(aStatus))
  164.                 while not success:
  165.                     time.sleep(0.1)
  166.                     success=self.didSucceed(self.detectors.statusChange(aStatus))
  167.         if hasattr(self.dataHandler,'statusChange'):
  168.             success=self.didSucceed(self.dataHandler.statusChange(aStatus))
  169.             while not success:
  170.                 time.sleep(0.1)
  171.                 success=self.didSucceed(self.dataHandler.statusChange(aStatus))
  172.     def chooseNextTrial(self):
  173.         self.broadCastStatus('chooseNextTrial')
  174.         self.nextParams=self.schedule.nextParams()
  175.         if (self.nextParams == []):
  176.             return None # we are done, no more jobs to do
  177.         else:
  178.             # get the stimulator ready for the chosen stimulus
  179.             self.stimulator.setParams(list(self.schedule.paramNames()), self.nextParams)
  180.             # for debugging, print info about the chosen stimulus
  181.             if verbose:
  182.                 print('Next stimulus has parameters ',self.schedule.paramNames(),self.nextParams)
  183.                 if self.stimulator.correctResponse("RIGHT"):
  184.                     side = 'RIGHT'
  185.                 else:
  186.                     side='LEFT'           
  187.                 if verbose:
  188.                     print()
  189.                     print('Correct response would be on the '+side)   
  190.                     print()     
  191.             sys.stdout.flush()
  192.             return self.waitForStart
  193.     
  194.     def waitForStart(self):
  195.         # listen to detectors and wait for a START signal
  196.         self.broadCastStatus('waitForStart')
  197.         if self.detectors is None:
  198.             # if there are no detectors to poll for start signal we just go
  199.             return self.presentTrial
  200.         response=self.detectors.responseDetected() 
  201.         if response[0]=='NONE':
  202.             # no start signal yet
  203.             time.sleep(0.05)
  204.             if (time.time()-self.lastAction) > 10:
  205.                 self.lastAction=time.time()
  206.                 print('Still waiting for centre lick at '+time.asctime(time.localtime()))
  207.                 sys.stdout.flush()
  208.             return self.waitForStart
  209.         if response[0]=='QUIT':
  210.             # exit signal sent: return None as next job to do
  211.             return None
  212.         if response[0]=='START':
  213.             return self.presentTrial
  214.         else:
  215.             # if we got here an inappropriate response was received.
  216.             # Reset sensors and continue
  217.             print('Received unexpected signal '+response[0])
  218.             sys.stdout.flush()
  219.             return self.waitForStart
  220.         
  221.     def presentTrial(self):
  222.         # present next stimulus
  223.         print()
  224.         print('### presenting trial {} of {}'.format(self.schedule.stimIndex+1, len(self.schedule.stimVarValues)))
  225.         self.stimStarted=time.time()
  226.         config.currentStimTimestamp=self.stimStarted-self.timeZero
  227.         config.currentStimParams=self.nextParams
  228.         self.broadCastStatus('presentTrial')
  229.         self.lastAction=time.time()
  230.         return self.getResponse
  231.         
  232.                     
  233.     def getResponse(self):
  234.         self.broadCastStatus('getResponse')
  235.         # listen to detectors and wait for a response to a recently presented stimulus
  236.         # if there are no detectors we just save the timestamp of when the stimulus started
  237.         if self.detectors is None:
  238.             par=self.stimulator.stimParams.copy()
  239.             par['timeStamp']=config.currentStimTimestamp
  240.             self.dataHandler.saveTrial(par)
  241.             return self.chooseNextTrial
  242.         
  243.         response=self.detectors.responseDetected() 
  244.         if response[0] in ['NONE','START']:
  245.             # no response signal yet
  246.             time.sleep(0.05)
  247.             if (time.time()-self.lastAction) > config.maxResponseTime:
  248.                 print('Subject took too long to respond. Resetting trial.')
  249.                 sys.stdout.flush()
  250.                 self.lastAction=time.time()
  251.                 return self.waitForStart
  252.             return self.getResponse
  253.         
  254.         print('Registered response '+response[0]+' at:',response[1])
  255.         sys.stdout.flush()
  256.         if response[0]=='QUIT':
  257.             # exit signal sent: return None as next job to do
  258.             return None
  259.         # at this point my response was neither none nor start nor quit, so it must be a judgment.
  260.         # Save the trial. Build a dictionary with all the trial info
  261.         par=self.stimulator.stimParams.copy()
  262.         # Ask the stimulator whether the response was correct
  263.         par['correct']=self.stimulator.correctResponse(response[0])
  264.         par['response']=response[0]
  265.         par['reactionTime']=response[1]-self.stimStarted
  266.         par['timeStamp']=response[1]-self.timeZero
  267.         
  268.         self.dataHandler.saveTrial(par)
  269.         # Give feedback
  270.         if par['correct']:
  271.             return self.reward
  272.         else:
  273.             return self.punish            
  274.         
  275.     def reward(self):
  276.         # reward, then move on to next trial
  277.         print('Correct response. Giving reward then choosing next trial')
  278.         sys.stdout.flush()
  279.         self.broadCastStatus('reward')
  280.         return self.chooseNextTrial
  281.         
  282.     def punish(self):
  283.         # give timeout signals, then repeat current stimulus as correction trial
  284.         print('Wrong response. Starting timeout then repeating trial')
  285.         sys.stdout.flush()
  286.         self.broadCastStatus('punish')
  287.         return self.waitForStart    
  288.     
  289.     def done(self):    
  290.         pass