-
Notifications
You must be signed in to change notification settings - Fork 21
/
ppc.py
225 lines (174 loc) · 8.36 KB
/
ppc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# -*- coding: utf-8 -*-
"""
Version: 2019.02.0
The ppc module (ppc is short for "PsychoPy Course) contain some useful
methods to help you build and verify your experiment. Put the ppc.py
in the same folder as your script or in your PYTHONPATH. See these
functions in use in the ppc3_template.py and in ppc2_timing.py.
Jonas Lindeløv
TO DO:
* add UTC times in csvWriter?
* Remove sound or make it a dummy-one which drops back to psychopy
* Use PEP8 names instead of camelCase
"""
# Check python version
import sys
python3 = sys.version_info[0] == 3
class Sound(object):
"""
A windows-only low-latency replacement for psychopy.sound.
It can only play wav files. Timing is unreliable if sound.play() is called
before previous sound ends. Usage::
beep = ppc.Sound('beep.wav')
beep.play()
# or generated beep:
beep = ppc.Sound()
beep.beep(1000, 0.2) # 1000 Hz for 0.2 seconds
"""
def __init__(self, filename=''):
""" :filename: a .wav file"""
self.sound = filename
self._winsound = __import__('winsound')
def play(self):
""" plays the sound file with low latency"""
self._winsound.PlaySound(self.sound, self._winsound.SND_FILENAME | self._winsound.SND_ASYNC)
def beep(self, frequency, duration):
""" plays a beep with low latency"""
self._winsound.Beep(frequency, duration / float(1000))
def timer(script, setup='', timeScale=False, runs=False):
"""
Times code snippets and returns average duration in seconds.
:script: a string to be timed
:setup: a comma-separated string specifying methods and variables to be imported from __main__
:timeScale: the unit for seconds. 10**-9 = nanoseconds. If False, the scale is automagically determined as s, ms, us or ns
:runs: how many times to run the script. If False, the number of runs is automagically determine from 3 testruns, trying to keep the total test duration around a second but at least 10 runs and at most 10**6 runs.
"""
if setup:
setup = 'from __main__ import ' + setup
import timeit
timeit.timeit(number=10**7) # get the computer's attention/ressources. First run is slower.
# optional: determine appropriate number of runs from 3 test runs
if not runs:
result = timeit.timeit(script, setup=setup, number=3)
runs = int(3 / result) if result > 0 else 10 ** 6
if runs > 10 ** 6:
runs = 10 ** 6 # a million at most
if runs < 10:
runs = 10 # ten at least
# Actually do the timing
baseline = timeit.timeit(setup=setup, number=runs) # the time it takes to run an empty script
result = timeit.timeit(script, setup=setup, number=runs) # Run the test!
mean = (result - baseline) / runs # in seconds
# Optional: determine appropriate timeScale for reporting
if not timeScale:
timeScale = 1 if mean > 1 else 10**-3 if mean > 10**-3 else 10**-6 if mean > 10**-6 else 10**-9
unit = 's' if timeScale == 1 else 'ms' if timeScale == 10**-3 else 'us' if timeScale == 10**-6 else 'ns' if timeScale == 10**-9 else '*' + str(timeScale)
# Print results
print('\n\'', script, '\'')
print('AVERAGE:', round(mean / timeScale, 3), unit, 'from', runs, 'runs')
def deg2cm(angle, distance):
"""
Returns the size of a stimulus in cm given:
:distance: ... to monitor in cm
:angle: ... that stimulus extends as seen from the eye
Use this function to verify whether your stimuli are the expected size.
(there's an equivalent in psychopy.tools.monitorunittools.deg2cm)
"""
import math
return math.tan(math.radians(angle)) * distance # trigonometry
class csv_writer(object):
def __init__(self, filename_prefix='', folder='', column_order=[]):
"""
Take a dictionary and write it to a csv file as a row.
Writing is very fast - less than a microsecond.
:filename_prefix: (str) would usually be the id of the participant
:folder: (str) optionally use/create a folder.
:column_order: (list) The columns to put first in the csv. Some or all.
Use like:
# Once towards the beginning of the script
writer = csv_writer('participant1', folder='data', column_order=['id', 'condition'])
# After each trial is completed
trial = {'id': 'participant1', 'rt': 0.2323, 'condition': 'practice'}
writer.write(trial)
# Optional: forces save of hitherto collected data to disk.
# writer.flush()
"""
import os
import time
self.column_order = column_order
self._header_written = False
# Create folder if it doesn't exist
if folder:
folder += '/'
if not os.path.isdir(folder):
os.makedirs(folder)
# Generate self.save_file and self.writer
self.save_file = '%s%s (%s).csv' % (folder, filename_prefix, time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())) # Filename for csv. E.g. "myFolder/subj1_cond2 (2013-12-28 09-53-04).csv"
self._setup_file()
def _setup_file(self):
"""Setting up the self.writer depends on python version."""
import csv
if python3:
self._file = open(self.save_file, 'a', newline='')
else:
self._file = open(self.save_file, 'wb')
self.writer = csv.DictWriter(self._file, fieldnames=self.column_order) # The writer function to csv. It appends a single row to file
def write(self, trial):
"""Saves a trial to buffer. :trial: a dictionary"""
# Write header and add fieldnames on first trial
if self.writer.fieldnames is None:
self.writer.fieldnames = list(trial.keys())
# Check that all column_order are present in the trial
if len(set(self.column_order) - set(list(trial.keys()))) != 0:
raise(ValueError('A column in column_order was not present in the trial dictionary'))
# Enforce order on first columns. Then add the last in "random" order.
if len(trial) > len(self.column_order):
self.writer.fieldnames = self.column_order + list(set(list(trial.keys())) - set(self.column_order))
# Write header if it hasn't been
if not self._header_written:
self.writer.writeheader()
self._header_written = True
# Now write data
self.writer.writerow(trial) # Works both in python2 and python3
def flush(self):
"""Saves current content to file.
This will happen automatically when the script terminates.
Only do this if you fear a hard crash. It's mostly fast (< 1 ms) but can be slow (up to 30 ms)
"""
self._file.close()
self._setup_file()
def getActualFrameRate(frames=1000):
"""
Measures the actual framerate of your monitor. It's not always as clean as
you'd think. Prints various useful information.
:frames: number of frames to do test on.
"""
from psychopy import visual, core
# Set stimuli up
durations = []
clock = core.Clock()
win = visual.Window(color='pink')
# Show a brief instruction / warning
visual.TextStim(win, text='Now wait and \ndon\'t do anything', color='black').draw()
win.flip()
core.wait(1.5)
# blank screen and synchronize clock to vertical blanks
win.flip()
clock.reset()
# Run the test!
for i in range(frames):
win.flip()
durations += [clock.getTime()]
clock.reset()
win.close()
# Print summary
import numpy as np
print('average frame duration was', round(np.average(durations) * 1000, 3), 'ms (SD', round(np.std(durations), 5), ') ms')
print('corresponding to a framerate of', round(1 / np.average(durations), 3), 'Hz')
print('60 frames on your monitor takes', round(np.average(durations) * 60 * 1000, 3), 'ms')
print('shortest duration was ', round(min(durations) * 1000, 3), 'ms and longest duration was ', round(max(durations) * 1000, 3), 'ms')
def dkl2rgb(dkl):
""" takes a DKL color as input and returns the corresponding RGB color """
from numpy import array
from psychopy.misc import dkl2rgb
return dkl2rgb(array(dkl))