diff --git a/README.md b/README.md index 701ab05..adc926d 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/psyquartz.pyi b/psyquartz.pyi index e89080d..d4e43d6 100644 --- a/psyquartz.pyi +++ b/psyquartz.pyi @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 06c6b8e..2666f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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'