Can I replace draw()?

Main Issue

I’m coding a BCI that should simultaneously generate P300 and SSVEP stimuli on an interface, and collect/process/classify EEG signal. When the interface runs separately from the EEG collection, both run smoothly, but when both run together it takes around twice as much time.

Is there a way to make the stimuli faster? Maybe remove some draw()'s somehow? Se the snippet of my interface below (full code):

TrialLabels = [0,1,1,2,1,0,2,0,2,1,0,2]
def start(self):
	T0 = genclock.getTime()
	p =[[-0.5, -0.5],[0, 0.5],[0.5, -0.5]] #from the centre; 1 is for 100%, 0 is for 50% and -1 is for 0% of screen

	self.stSq0 = visual.Rect(self.win, fillColor = 'black', lineColor = 'black', width = self.pS, height = self.pS, pos = p[0])
	self.stSq1 = visual.Rect(self.win, fillColor = 'black', lineColor = 'black', width = self.pS, height = self.pS, pos = p[1])
	self.stSq2 = visual.Rect(self.win, fillColor = 'black', lineColor = 'black', width = self.pS, height = self.pS, pos = p[2])
		
	for TL in TrialLabels:
		ct = 0
		mx_ct = 1799
		op_off = 0
		op_on = 1
		endflag = False
		
		sequenceP300 = []
		for m in range (0,3):
			for k in range(0,7): #number of blinks per square
				sequenceP300.append(m)
		random.shuffle(sequenceP300)
		blinkLabels = sequenceP300 #copied because sequenceP300 gets destroyed

		t0 = p300clock.getTime()
		current_P300 = sequenceP300.pop(0)
		while not endflag:#True:
			try:
				##::SSVEP part
				if ct%2 >= 1:
					self.Sq0 = visual.Rect(self.win, fillColor = '#00388A', lineColor = '#00388A',  width = self.sS, height = self.sS, pos = p[0], opacity = op_on) #blue
				else:
					self.Sq0 = visual.Rect(self.win, fillColor = '#59FF00', lineColor = '#59FF00', width = self.sS, height = self.sS, pos = p[0], opacity = op_on) #green

				if ct%4 >= 2:
					self.Sq1 = visual.Rect(self.win, fillColor = '#59FF00', lineColor = '#59FF00', width = self.sS, height = self.sS, pos = p[1], opacity = op_on) #green
				else:
					self.Sq1 = visual.Rect(self.win, fillColor = '#B80117', lineColor = '#B80117', width = self.sS, height = self.sS, pos = p[1], opacity = op_on) #red

				if ct%6 >= 3:
					self.Sq2 = visual.Rect(self.win, fillColor = '#59FF00', lineColor = '#59FF00', width = self.sS, height = self.sS, pos = p[2], opacity = op_on) #green
				else:
					self.Sq2 = visual.Rect(self.win, fillColor = '#B80117', lineColor = '#B80117', width = self.sS, height = self.sS, pos = p[2], opacity = op_on) #red

				##::P300 part
				t1 = p300clock.getTime()
				if t1-t0 < 0.2:
					if current_P300 == 0:
						self.p300Sq0 = visual.Rect(self.win, fillColor = 'white', lineColor = 'white', width = self.p3S, height = self.p3S, pos = p[0], opacity = op_on)
					if current_P300 == 1:
						self.p300Sq0 = visual.Rect(self.win, fillColor = 'white', lineColor = 'white', width = self.p3S, height = self.p3S, pos = p[1], opacity = op_on)
					if current_P300 == 2:
						self.p300Sq0 = visual.Rect(self.win, fillColor = 'white', lineColor = 'white', width = self.p3S, height = self.p3S, pos = p[2], opacity = op_on)
				elif t1-t0 >= 0.2 and t1-t0 < 0.45:
					self.p300Sq0 = visual.Rect(self.win, fillColor = 'black', lineColor = 'black', width = self.p3S, height = self.p3S, pos = p[0], opacity = op_off)
				elif t1-t0 >= 0.5:
					t0 = p300clock.getTime()
					try:
						current_P300 = sequenceP300.pop(0)
					except IndexError as e:
					    endflag = True

				self.p300Sq0.draw()
				self.stSq0.draw()
				self.stSq1.draw()
				self.stSq2.draw()
				self.Sq0.draw()
				self.Sq1.draw()
				self.Sq2.draw()

				self.win.flip()
				ct += 1
				if ct>= mx_ct:
					ct = 0

			except KeyboardInterrupt:
				self.stop()

Here is what the profiler yields (and that’s why I was wondering if I cant take some draw()'s off):

   213360 function calls (208353 primitive calls) in 44.338 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.004    0.004   44.338   44.338 fb_01_threads.py:70(start)
      363   18.329    0.050   21.593    0.059 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\visual\shape.py:320(draw)
2776/2772    0.004    0.000   13.424    0.005 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\tools\attributetools.py:31(__set__)
        4    0.006    0.001   13.399    3.350 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\visual\text.py:80(__init__)
  387/361    0.000    0.000   13.389    0.037 {built-in method builtins.setattr}
    24/20    0.000    0.000   13.389    0.669 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\tools\attributetools.py:55(setAttribute)
        4    0.000    0.000   12.723    3.181 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\visual\text.py:369(setText)
        4    0.000    0.000   12.723    3.181 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\visual\text.py:334(text)
        4    0.000    0.000   12.723    3.181 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\psychopy\visual\text.py:375(_setTextShaders)
        4    0.000    0.000    9.789    2.447 C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\site-packages\pyglet\text\__init__.py:399(__init__)
...

And this is what PsychoPy outputs as well:

2.1699  WARNING         Monitor specification not found. Creating a temporary one...
4.5121  WARNING         Couldn't measure a consistent frame rate.
  - Is your graphics card set to sync to vertical blank?
  - Are you running other processes on your computer?

16.8873         WARNING         t of last frame was 12358.70ms (=1/0)
17.5809         WARNING         t of last frame was 693.56ms (=1/1)
18.3954         WARNING         t of last frame was 814.52ms (=1/1)
20.0773         WARNING         t of last frame was 1681.95ms (=1/0)
21.5205         WARNING         Multiple dropped frames have occurred - I'll stop bothering you about them!

Hardware Specs

Item Description
Processor Intel® Core™ i5-2400 CPU @ 3.10GHz 3.10 GHz //4 cores
WinOS Windows 10 Enterprise
Monitor Refresh Rate 60Hz
Installed RAM 8.00 GB
System type 64-bit operating system, x64-based processor
Graphics Card AMD Radeon HD 6450
Python 3.6.10
Psychopy 2020.1.2

It looks as though you’re creating a new Rect object each frame, is that right? That would explain why it’s running slowly - it would be better to create the Rects at the start of the routine and then simply change their attributes (e.g. self.Sq0.fillColor = '#00388A') rather than making a new Rect each time.

Another way to speed it up that I can think of is to use Textbox rather than Text for the text stimuli you’re getting messages about in the profiler output - as Textbox renders the characters directly rather than using pyglet as a gobetween.

I’m not sure about removing draw() commands - to do so you would also have to prevent the screen from flipping, as without a draw() command the stimulus would disappear on the next flip, and this is likely to mess up timing.

2 Likes

Thanks @TParsons. That actually speeds up the code.

But my main problem was being the use of threading Events in loop.
I was testing if the experiment was running while testing if any flag for my paradigm was raised:

while not Experiment.is_set():
    if Paradigm.is_set():
        ###processing

But the amount of times is_set was being tested was messing with Python’s synchronization primitives, which was slowing my code lots. So the solution was to use the wait() method instead:

while not Experiment.is_set():
   Paradigm.wait(timeout = 10):
        ###processing

Hope this helps if anyone is going through similar situations.