Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
A very simple clock to replace the Psychopy core clock, written in Rust with Python bindings. Yields ~20x speedup for basic operations like `clock.getTime()`, going from ~1.5 microseconds to ~70 nanoseconds. Not very useful unless you're doing very frequent polling of the clock, but I made this primarily for debugging a badly-drifting system clock.
# psyquartz

Also provides a high-accuracy sleep function, `psyquartz.sleepers`, which offers better precision than the default python implementation `time.sleep`.
A high-performance clock and sleep library for Python, written in Rust with
[PyO3](https://pyo3.rs) bindings. Drop-in replacement for
[PsychoPy](https://www.psychopy.org/) core clocks with ~20x speedup on
`clock.getTime()` (~70 ns vs ~1.5 μs).

Also provides a high-accuracy sleep function that substantially outperforms
Python's `time.sleep`.

## Installation

```bash
pip install psyquartz
```

## Usage

### Clocks

`MonotonicClock` records the current time at creation. Calling `getTime()`
returns the elapsed seconds since then.

```python
import psyquartz

clock = psyquartz.MonotonicClock()
elapsed = clock.getTime() # seconds since creation
raw = clock.getTime(applyZero=False) # raw UNIX epoch time
```

`Clock` extends `MonotonicClock` with the ability to reset and shift the
baseline.

```python
clock = psyquartz.Clock()
print(clock.getTime()) # seconds since creation

clock.reset() # restart from 0
print(clock.getTime()) # close to 0

clock.addTime(1.0) # shift baseline forward by 1 s
print(clock.getTime()) # close to -1.0
```

### High-accuracy sleep

`sleep` (and its alias `sleepers`) uses a hybrid strategy — sleeping for half
the remaining duration while more than 200 μs remain, then spin-waiting for the
final stretch.

```python
import psyquartz

psyquartz.sleep(0.01) # accurate 10 ms sleep
```
193 changes: 163 additions & 30 deletions psyquartz.pyi
Original file line number Diff line number Diff line change
@@ -1,68 +1,201 @@
from __future__ import annotations

class MonotonicClock:
"""A PsychoPy-compatible monotonic clock using the system time.

On creation, the current UNIX epoch time is recorded as the baseline. All subsequent
calls to :meth:`getTime` return the elapsed time since that baseline.

Attributes
----------
_timeAtLastReset : float
UNIX epoch time (in seconds) recorded at clock creation or last reset.
_epochTimeAtLastReset : float
UNIX epoch time (in seconds) recorded at clock creation or last reset.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.MonotonicClock()
>>> elapsed = clock.getTime() # seconds since creation
"""
A psychopy-compatible monotonic clock using accurate system time.
"""

_timeAtLastReset: float
_epochTimeAtLastReset: float

def __init__(self) -> None:
"""
Initialize a new MonotonicClock instance. Time will begin at 0.
"""
pass
"""Create a new :class:`MonotonicClock`.

The current UNIX epoch time is recorded as the baseline for all future
:meth:`getTime` calls.

def getTime(self) -> float:
Raises
------
RuntimeError
If the system clock cannot be read.
"""
Get the current time in seconds since the clock was started or last reset (only Clock,
not MonotonicClock).

def getTime(self, applyZero: bool = True) -> float:
"""Get the current time from the clock.

Parameters
----------
applyZero : bool
If ``True`` (default), return the elapsed time in seconds since the
clock was created (or last reset for :class:`Clock`). If ``False``,
return the raw UNIX epoch time in seconds.

Returns
-------
float
Time in seconds.

Raises
------
RuntimeError
If the system clock cannot be read.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.MonotonicClock()
>>> elapsed = clock.getTime()
>>> raw_epoch = clock.getTime(applyZero=False)
"""
pass

def getLastResetTime(self) -> float:
"""Get the UNIX epoch time recorded at clock creation or last reset.

Returns
-------
float
UNIX epoch time in seconds.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.MonotonicClock()
>>> clock.getLastResetTime() # doctest: +SKIP
1741193045.123456
"""
Get the time of the last reset in seconds.
"""
pass

class Clock(MonotonicClock):
"""A PsychoPy-compatible resettable clock.

Extends :class:`MonotonicClock` with :meth:`reset`, :meth:`addTime`, and
:meth:`add` methods to manipulate the clock's baseline.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.Clock()
>>> elapsed = clock.getTime()
>>> clock.reset() # restart from 0
>>> clock.getTime() # close to 0
"""
A psychopy-compatible clock that can be used to measure time intervals.
"""

def __init__(self) -> None:
"""
Initialize a new Clock instance. Will use accurate system time.
"""
super().__init__()
"""Create a new :class:`Clock`.

def reset(self, newT: float) -> None:
Raises
------
RuntimeError
If the system clock cannot be read.
"""
Reset the clock to the specified time value.

def reset(self, newT: float = 0.0) -> None:
"""Reset the clock by re-capturing the current system time as baseline.

Parameters
----------
newT : float
The time to reset the clock to, in seconds.
Currently unused. Reserved for PsychoPy API compatibility.

Raises
------
RuntimeError
If the system clock cannot be read.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.Clock()
>>> psyquartz.sleep(0.1)
>>> clock.reset()
>>> clock.getTime() # close to 0
"""
pass

def addTime(self, t: float) -> None:
"""Shift the clock's baseline forward by ``t`` seconds.

Adding time to the baseline makes future :meth:`getTime` calls return a smaller
value, effectively subtracting ``t`` from the elapsed time.

Parameters
----------
t : float
The amount of time to add to the baseline, in seconds.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.Clock()
>>> clock.addTime(1.0)
>>> clock.getTime() # close to -1.0
"""
Add a specified amount of time to the clock.

def add(self, t: float) -> None:
"""Shift the clock's baseline forward by ``t`` seconds.

Alias for :meth:`addTime`.

Parameters
----------
t : float
The amount of time to add, in seconds.
The amount of time to add to the baseline, in seconds.

Examples
--------
>>> import psyquartz
>>> clock = psyquartz.Clock()
>>> clock.add(1.0)
>>> clock.getTime() # close to -1.0
"""
pass

def sleepers(t: float) -> None:
"""Sleep for ``t`` seconds with high accuracy.

Uses a hybrid strategy for precise timing: sleeps for half the remaining duration
while more than 200 microseconds remain, then spin-waits for the final stretch.
Substantially more accurate than :func:`time.sleep`.

If ``t`` is zero or negative, returns immediately.

Parameters
----------
t : float
The duration to sleep, in seconds.

Examples
--------
>>> import psyquartz
>>> psyquartz.sleepers(0.01) # accurate 10 ms sleep
"""
Sleep for a specified amount of time. Substantially more accurate than
time.sleep().

def sleep(t: float) -> None:
"""Sleep for ``t`` seconds with high accuracy.

Alias for :func:`sleepers`.

If ``t`` is zero or negative, returns immediately.

Parameters
----------
t : float
The amount of time to sleep, in seconds.
The duration to sleep, in seconds.

Examples
--------
>>> import psyquartz
>>> psyquartz.sleep(0.01) # accurate 10 ms sleep
"""
pass
16 changes: 14 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,30 @@ build-backend = "maturin"

[project]
name = "psyquartz"
description = "A high-performance clock and sleep library for Python, written in Rust."
readme = "README.md"
requires-python = ">=3.8"
keywords = ["clock", "timing", "sleep", "rust"]
classifiers = [
"Programming Language :: Rust",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Rust",
"Topic :: Scientific/Engineering",
]
dynamic = ["version"]

[project.urls]
source = "https://github.com/berkgercek/psyquartz/"

[tool.maturin]
features = ["pyo3/extension-module"]


[tool.uv]
cache-keys = [{file = "**/*.rs"}, {file = "Cargo.toml"}, {file = "pyproject.toml"}]
default-groups = 'all'
Expand Down