Lost in time: core.getTime() vs clock.getTime()

I have been working to understand the Psychopy API, and there are many ways to handle time. One of the most fundamental confusions for me is that getTime() is defined differently in core and clock. But regardless of whether a clock is constructed using core.Clock or clock.Clock, they both seem to operate based on clock.getTime()

# clocks-example.py
from psychopy import (core,clock)

t0_monoclock = clock.monotonicClock.getTime()
t0_core_T = core.getTime(True)
t0_core_F = core.getTime(False)
t0_clock = clock.getTime()

x = core.Clock()
y = clock.Clock()

m_lastResetTime = clock.monotonicClock.getLastResetTime()
x_lastResetTime = x.getLastResetTime()
y_lastResetTime = y.getLastResetTime()

print(f"t0 clock.monotonicClock            : {t0_monoclock:f}")
print(f"t0 core.getTime(True)              : {t0_core_T:f}")
print(f"t0 core.getTime(False)             : {t0_core_F:f}")
print(f"t0 clock.getTime()                 : {t0_clock:f}")
print(f"lastResetTime clock.monotonicClock : {m_lastResetTime:f}")
print(f"lastResetTime core.Clock           : {x_lastResetTime:f}")
print(f"lastResetTime clock.Clock          : {y_lastResetTime:f}")


# EXAMPLE EXECUTION OF clocks-example.py SCRIPT
> & "C:\Program Files\PsychoPy\python.exe" .\clocks-example.py
t0 clock.monotonicClock            : 0.075200
t0 core.getTime(True)              : 0.075208
t0 core.getTime(False)             : 1758804816.684122
t0 clock.getTime()                 : 977521.716676
lastResetTime clock.monotonicClock : 977521.641462
lastResetTime core.Clock           : 977521.716681
lastResetTime clock.Clock          : 977521.716684

My questions are:

  1. What is core.getTime() used for, if anything?
  2. Since Clock is imported from the clock module into the core module, and the general idiom seems to be to initialize clocks with core.Clock rather than importing and using clock.Clock directly, is it best to avoid interacting with the clock module directly?
  3. There are some functions, like visual.window.timeOnFlip() that do not take a clock as an argument and so reference some “default” clock. I think this is the instance of monotonicClock that gets initialized when importing the clock module. Do I have this right?

Especially when trying to get my head around the helper functions that hide some of the complexity, I have found it confusing to keep things straight. Thanks for help and insight!

Edit: digging into things some more, it seems that visual.window.timeOnFlip() does NOT reference the default monotonicClock. It actually references whatever clock is set as the default for logging, which will might be that default monotonicClock if you don’t set it to something else. But visual.window.timeOnFlip() is simply reading out a time that was already recorded: win.flip() stores a timestamp in win._frameTime (which, again, is determined by whatever the logging.defaultClock is).

So, for this particular helper function, for order of operations is something like:

  1. You tell Psychopy that you want to store the time of the next flip into a specific field of a specified object immediately after that flip happens.
  2. This event is scheduled using win.callOnFlip()
  3. When the flip happens, a time stamp is stored in win._frameTime.
  4. After the flip, the scheduled event happens, and the value of win._frameTime is returned.

I think I understand the difference between clock.getTime() and core.getTime(). clock.getTime() is the lowest level interface to whatever is actually providing the timing information. core.getTime() references the default monotonicClock instance, which internally references clock.getTime() when returning times, but monotonicClock is instantiated such that its own getTime() method applies an offset to the value returned by clock.getTime() such that 0 corresponds to the beginning of the experiment (i.e., the moment monotonicClock was instantiated).

I think this untangles the soup of localized getTime methods and functions to reveal the truth: at the bottom of it all, there is just one clock. Every instantiation of a clock is just a way of managing different offsets from that true clock.

This also means there is no conflict between clock and core. core builds on top of clock and provides some abstraction, but they don’t interfere with each other. And now I think I understand the default clock and how to change it, if desired.

Would still love to hear insights from people who really understand timing in Psychopy… but I think I understand well enough to answer my three questions from the first post.