/
clock.py
612 lines (492 loc) · 20.7 KB
/
clock.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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on Tue Apr 23 11:28:32 2013
Provides the high resolution timebase used by psychopy, and defines some time
related utility Classes.
Moved functionality from core.py so a common code
base could be used in core.py and logging.py; vs. duplicating the getTime and
Clock logic.
@author: Sol
@author: Jon
"""
# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
import logging
import time
import sys
from datetime import datetime
from pkg_resources import parse_version
try:
import pyglet
except ImportError:
pass # pyglet is not installed
from psychopy.constants import STARTED, NOT_STARTED, FINISHED
import psychopy.logging # Absolute import to work around circularity
# set the default timing mechanism
getTime = None
# Select the timer to use as the psychopy high resolution time base. Selection
# is based on OS and Python version.
#
# Three requirements exist for the psychopy time base implementation:
# A) The Python interpreter does not apply an offset to the times returned
# based on when the timer module being used was loaded or when the
# timer function first called was first called.
# B) The timer implementation used must be monotonic and report elapsed
# time between calls, 'not' system or CPU usage time.
# C) The timer implementation must provide a resolution of 50 usec or
# better.
#
# Given the above requirements, psychopy selects a timer implementation as
# follows:
# 1) On Windows, the Windows Query Performance Counter API is used using
# ctypes access.
# 2) On other OS's, if the Python version being used is 2.6 or lower,
# time.time is used. For Python 2.7 and above, the timeit.default_timer
# function is used.
try:
import psychtoolbox
havePTB = True
except ImportError:
havePTB = False
if havePTB:
# def getTime():
# secs, wallTime, error = psychtoolbox.GetSecs('allclocks')
# return wallTime
getTime = psychtoolbox.GetSecs
elif sys.platform == 'win32':
from ctypes import byref, c_int64, windll
_fcounter = c_int64()
_qpfreq = c_int64()
windll.Kernel32.QueryPerformanceFrequency(byref(_qpfreq))
_qpfreq = float(_qpfreq.value)
_winQPC = windll.Kernel32.QueryPerformanceCounter
def getTime():
_winQPC(byref(_fcounter))
return _fcounter.value / _qpfreq
elif sys.platform == "darwin":
# Monotonic getTime with absolute origin. Suggested by @aforren1, and
# copied from github.com/aforren1/toon/blob/master/toon/input/mac_clock.py
import ctypes
_libc = ctypes.CDLL('/usr/lib/libc.dylib', use_errno=True)
# create helper class to store data
class mach_timebase_info_data_t(ctypes.Structure):
_fields_ = (('numer', ctypes.c_uint32),
('denom', ctypes.c_uint32))
# get function and set response type
_mach_absolute_time = _libc.mach_absolute_time
_mach_absolute_time.restype = ctypes.c_uint64
# calculate timebase
_timebase = mach_timebase_info_data_t()
_libc.mach_timebase_info(ctypes.byref(_timebase))
_ticks_per_second = _timebase.numer / _timebase.denom * 1.0e9
# scaling factor so that timing works correctly on Intal and Apple Silicon
_scaling_factor = _timebase.numer / _timebase.denom
# then define getTime func
def getTime():
return (_mach_absolute_time() * _scaling_factor) / 1.0e9
else:
import timeit
getTime = timeit.default_timer
class Timestamp(float):
"""
Object to represent a timestamp, which can return itself in a variety of formats.
Parameters
----------
value : float or str
Current time, as either:
- float : Seconds since arbitrary start time (if only using as a duration)
- float : Seconds since epoch (for an absolute time)
- str : Time string in the format specified by the parameter `format`
format : str or class
Time format string (as in time.strftime) indicated how to convert this timestamp to a string, and how to
interpret its value if given as a string. Use `float` (default) to always print timestamp as a float, or `str`
as
lastReset : float
Epoch time at last clock reset. Will be added to raw value if printing to string.
"""
def __new__(cls, value, format=float, lastReset=0.0):
# if given a string, attempt to parse it using the given format
if isinstance(value, str):
# substitute nonspecified str format for ISO 8601
if format in (str, "str"):
format = "%Y-%m-%d_%H:%M:%S.%f%z"
# try to parse
try:
value = datetime.strptime(value, format)
except ValueError as err:
# if parsing fails, try again without %z (as this is excluded in GMT)
if format.endswith("%z"):
value = datetime.strptime(value, format[:-2])
# convert to timestamp
value = datetime.timestamp(value) - lastReset
return float.__new__(cls, value)
def __init__(self, value, format=float, lastReset=0.0):
self.lastReset = lastReset
self.format = format
# create self as float representing the time
float.__init__(value)
def __str__(self):
# use strftime to return with own format
return self.strftime(format=self.format)
def __format__(self, format_spec):
if self.format in (float, "float"):
# format as normal if float is requested
return float.__format__(self, format_spec)
else:
# otherwise just stringify
return str(self)
def resolve(self, format=None):
"""
Get the value of this timestamp as a simple value, either str or float.
Parameters
----------
format : str, class or None
Time format string, as in time.strftime, or `float` to return as a float. Defaults (None) to using the
format given when this timestamp was initialised.
Returns
-------
str, float
The value of this timestamp in the requested format.
"""
# if format is unspecified, use own default
if format is None:
format = self.format
# if format is float, return as simple (non-timestamp) float
if format in (float, "float"):
return float(self)
# otherwise, format to string in requested format
return self.strftime(format=format)
def strftime(self, format="%Y-%m-%d_%H:%M:%S.%f%z"):
"""
Format this timestamp into a string with the given format.
Parameters
----------
format : str, class or None
Time format string, as in time.strftime, or `float` to print as a float. Defaults (None) to using the
format given when this timestamp was initialised.
Returns
-------
str
This timestamp as a string
"""
# if format is unspecified, use own default
if format in (None, "None"):
format = self.format
# if format is float, print using base method
if format in (float, "float"):
return float.__str__(self)
# substitute nonspecified str format for ISO 8601
if format in (str, "str"):
format = "%Y-%m-%d_%H:%M:%S.%f%z"
# convert to datetime
now = datetime.fromtimestamp(self + self.lastReset)
# format
return now.strftime(format)
class MonotonicClock:
"""A convenient class to keep track of time in your experiments using a
sub-millisecond timer.
Unlike the :class:`~psychopy.core.Clock` this cannot be reset to
arbitrary times. For this clock t=0 always represents the time that
the clock was created.
Don't confuse this `class` with `core.monotonicClock` which is an
`instance` of it that got created when PsychoPy.core was imported.
That clock instance is deliberately designed always to return the
time since the start of the study.
Version Notes: This class was added in PsychoPy 1.77.00
"""
def __init__(self, start_time=None, format=float):
super(MonotonicClock, self).__init__()
if start_time is None:
# this is sub-millisecond timer in python
self._timeAtLastReset = getTime()
else:
self._timeAtLastReset = start_time
self._epochTimeAtLastReset = time.time()
# store default format
self.format = format
def getTime(self, applyZero=True, format=None):
"""
Returns the current time on this clock in secs (sub-ms precision).
Parameters
----------
applyZero : bool
If applying zero then this will be the time since the clock was created (typically the beginning of the
script). If not applying zero then it is whatever the underlying clock uses as its base time but that is
system dependent. e.g. can be time since reboot, time since Unix Epoch etc.
Only applies when format is `float`.
format : type, str or None
Format in which to show timestamp when converting to a string. Can be either:
- time format codes: Time will return as a string in that format, as in time.strftime
- `str`: Time will return as a string in ISO 8601 (YYYY-MM-DD_HH:MM:SS.mmmmmmZZZZ)
- `None`: Will use this clock's `format` attribute
Returns
-------
Timestamp
Time with format requested.
"""
# substitute no format for default
if format in (None, "None"):
format = self.format
# substitute nonspecified str format for ISO 8601
if format in (str, "str"):
format = "%Y-%m-%d_%H:%M:%S.%f%z"
# get time since last reset
t = getTime() - self._timeAtLastReset
# get last reset time from epoch
lastReset = self._epochTimeAtLastReset
if not applyZero:
# if not applying zero, add epoch start time to t rather than supplying it
t += self._epochTimeAtLastReset
lastReset = 0
return Timestamp(t, format, lastReset=lastReset)
def getLastResetTime(self):
"""
Returns the current offset being applied to the high resolution
timebase used by Clock.
"""
return self._timeAtLastReset
monotonicClock = MonotonicClock()
class Clock(MonotonicClock):
"""A convenient class to keep track of time in your experiments.
You can have as many independent clocks as you like (e.g. one
to time responses, one to keep track of stimuli ...)
This clock is identical to the :class:`~psychopy.core.MonotonicClock`
except that it can also be reset to 0 or another value at any point.
"""
def __init__(self, format=float):
super(Clock, self).__init__(format=format)
def reset(self, newT=0.0):
"""Reset the time on the clock. With no args time will be
set to zero. If a float is received this will be the new
time on the clock
"""
self._timeAtLastReset = getTime() + newT
self._epochTimeAtLastReset = time.time()
def addTime(self, t):
"""Add more time to the Clock/Timer
e.g.::
timer = core.Clock()
timer.addTime(5)
while timer.getTime() > 0:
# do something
"""
self._timeAtLastReset -= t
self._epochTimeAtLastReset -= t
def add(self, t):
"""DEPRECATED: use .addTime() instead
This function adds time TO THE BASE (t0) which, counterintuitively,
reduces the apparent time on the clock
"""
logging.warning("DEPRECATED: Clock.add() is deprecated in favor of .addTime() due to "
"the counterintuitive design (it added time to the baseline, which "
"reduced the values returned from getTime()")
self._timeAtLastReset += t
self._epochTimeAtLastReset += t
class CountdownTimer(Clock):
"""Similar to a :class:`~psychopy.core.Clock` except that time counts down
from the time of last reset.
Parameters
----------
start : float or int
Starting time in seconds to countdown on.
Examples
--------
Create a countdown clock with a 5 second duration::
timer = core.CountdownTimer(5)
while timer.getTime() > 0: # after 5s will become negative
# do stuff
"""
def __init__(self, start=0):
super(CountdownTimer, self).__init__()
self._countdown_duration = start
if start:
self.reset()
def getTime(self):
"""Returns the current time left on this timer in seconds with sub-ms
precision (`float`).
"""
return self._timeAtLastReset - getTime()
def addTime(self, t):
"""Add more time to the CountdownTimer
e.g.:
countdownTimer = core.CountdownTimer()
countdownTimer.addTime(1)
while countdownTimer.getTime() > 0:
# do something
"""
self._timeAtLastReset += t
self._epochTimeAtLastReset += t
def reset(self, t=None):
"""Reset the time on the clock.
Parameters
----------
t : float, int or None
With no args (`None`), time will be set to the time used for last
reset (or start time if no previous resets). If a number is
received, this will be the new time on the clock.
"""
if t is not None:
self._countdown_duration = t
Clock.reset(self, self._countdown_duration)
class StaticPeriod:
"""A class to help insert a timing period that includes code to be run.
Parameters
----------
screenHz : int or None
the frame rate of the monitor (leave as None if you
don't want this accounted for)
win : :class:`~psychopy.visual.Window`
If a :class:`~psychopy.visual.Window` is given then
:class:`StaticPeriod` will also pause/restart frame interval recording.
name : str
Give this StaticPeriod a name for more informative logging messages.
Examples
--------
Typical usage for the static period::
fixation.draw()
win.flip()
ISI = StaticPeriod(screenHz=60)
ISI.start(0.5) # start a period of 0.5s
stim.image = 'largeFile.bmp' # could take some time
ISI.complete() # finish the 0.5s, taking into account one 60Hz frame
stim.draw()
win.flip() # the period takes into account the next frame flip
# time should now be at exactly 0.5s later than when ISI.start()
# was called
"""
def __init__(self, screenHz=None, win=None, name='StaticPeriod'):
self.status = NOT_STARTED
self.countdown = CountdownTimer()
self.name = name
self.win = win
if screenHz is None:
self.frameTime = 0
else:
self.frameTime = 1.0 / screenHz
self._winWasRecordingIntervals = False
def start(self, duration):
"""Start the period. If this is called a second time, the timer will
be reset and starts again
Parameters
----------
duration : float or int
The duration of the period, in seconds.
"""
self.status = STARTED
self.countdown.reset(duration - self.frameTime)
# turn off recording of frame intervals throughout static period
if self.win:
self._winWasRecordingIntervals = self.win.recordFrameIntervals
self.win.recordFrameIntervals = False
def complete(self):
"""Completes the period, using up whatever time is remaining with a
call to `wait()`.
Returns
-------
float
`1` for success, `0` for fail (the period overran).
"""
self.status = FINISHED
timeRemaining = self.countdown.getTime()
if self.win:
self.win.recordFrameIntervals = self._winWasRecordingIntervals
if timeRemaining < 0:
msg = ('We overshot the intended duration of %s by %.4fs. The '
'intervening code took too long to execute.')
vals = self.name, abs(timeRemaining)
psychopy.logging.warn(msg % vals)
return 0
wait(timeRemaining)
return 1
def _dispatchWindowEvents():
"""Helper function for :func:`~.psychopy.core.wait`. Handles window event if
needed or returns otherwise.
"""
from . import core
if not (core.havePyglet and core.checkPygletDuringWait):
return # nop
# let's see if pyglet collected any event in meantime
try:
# this takes focus away from command line terminal window:
if parse_version(pyglet.version) < parse_version('1.2'):
# events for sounds/video should run independently of wait()
pyglet.media.dispatch_events()
except AttributeError:
# see http://www.pyglet.org/doc/api/pyglet.media-module.html#dispatch_events
# Deprecated: Since pyglet 1.1, Player objects schedule themselves
# on the default clock automatically. Applications should not call
# pyglet.media.dispatch_events().
pass
for winWeakRef in core.openWindows:
win = winWeakRef()
if (win.winType == "pyglet" and
hasattr(win.winHandle, "dispatch_events")):
win.winHandle.dispatch_events() # pump events
def wait(secs, hogCPUperiod=0.2):
"""Wait for a given time period.
This function halts execution of the program for the specified duration.
Precision of this function is usually within 1 millisecond of the specified
time, this may vary depending on factors such as system load and the Python
version in use. Window events are periodically dispatched during the wait
to keep the application responsive, to avoid the OS complaining that the
process is unresponsive.
If `secs=10` and `hogCPU=0.2` then for 9.8s Python's `time.sleep` function
will be used, which is not especially precise, but allows the cpu to
perform housekeeping. In the final `hogCPUperiod` the more precise
method of constantly polling the clock is used for greater precision.
If you want to obtain key-presses during the wait, be sure to use
pyglet and then call :func:`psychopy.event.getKeys()` after calling
:func:`~.psychopy.core.wait()`
If you want to suppress checking for pyglet events during the wait, do this
once::
core.checkPygletDuringWait = False
and from then on you can do::
core.wait(sec)
This will preserve terminal-window focus during command line usage.
Parameters
----------
secs : float or int
Number of seconds to wait before continuing the program.
hogCPUperiod : float or int
Number of seconds to hog the CPU. This causes the thread to enter a
'tight' loop when the remaining wait time is less than the specified
interval. This is set to 200ms (0.2s) by default. It is recommended that
this interval is kept short to avoid stalling the processor for too
long which may result in poorer timing.
"""
# Calculate the relaxed period which we periodically suspend the thread,
# this puts less load on the CPU during long wait intervals.
relaxedPeriod = secs - hogCPUperiod
# wait loop, suspends the thread periodically and consumes CPU resources
t0 = getTime()
while True:
elapsed = getTime() - t0
if elapsed > secs: # no more time left, break the loop
break
if elapsed > relaxedPeriod: # hog period
sleepDur = 0.00001 # 0.1ms
else:
relaxedTimeLeft = relaxedPeriod - elapsed
sleepDur = 0.01 if relaxedTimeLeft > 0.01 else relaxedTimeLeft
time.sleep(sleepDur)
_dispatchWindowEvents()
def getAbsTime():
"""Get the absolute time.
This uses the same clock-base as the other timing features, like
`getTime()`. The time (in seconds) ignores the time-zone (like `time.time()`
on linux). To take the timezone into account, use
`int(time.mktime(time.gmtime()))`.
Absolute times in seconds are especially useful to add to generated file
names for being unique, informative (= a meaningful time stamp), and because
the resulting files will always sort as expected when sorted in
chronological, alphabetical, or numerical order, regardless of locale and so
on.
Version Notes: This method was added in PsychoPy 1.77.00
Returns
-------
float
Absolute Unix time (i.e., whole seconds elapsed since Jan 1, 1970).
"""
return int(time.mktime(time.localtime()))