Composable time code encoder/decoder
Project description
escapement
Composable time code encoder/decoder for Python.
escapement converts between numpy.datetime64 timestamps and the binary
time code formats used in software or protocols. Define a clock as an epoch
plus an ordered tuple of fields, then encode and decode — scalars or arrays.
from escapement import Clock
cuc = Clock.cuc(coarse_bytes=4, fine_bytes=2)
encoded = cuc.encode(np.datetime64("2025-06-15T12:00:00.5"))
decoded = cuc.decode(encoded) # round-trips within one finest tick
Why "escapement"?
In a mechanical clock the escapement is the mechanism that converts continuous energy into discrete, evenly spaced ticks. It is the part that makes a clock tick — literally parcelling smooth motion into counted intervals.
That is exactly what this library does: it takes a continuous time value and divides it into a cascade of discrete, counted fields — coarse ticks flowing into finer ticks, each field consuming a portion of the remainder before passing it on. The analogy runs deeper than the name suggests: a real escapement couples a fast oscillator (the balance wheel) to a slow gear train, just as a CUC time code couples a fine sub-second counter to a coarse whole-second counter.
Standard-specific factories
| Factory | Standard | Fields |
|---|---|---|
Clock.unix() |
Unix / POSIX | seconds (or ms/us/ns) |
Clock.met() |
Mission Elapsed Time | seconds (or ms/us/ns) |
Clock.cuc() |
CCSDS Unsegmented | coarse seconds + fine fraction |
Clock.cds() |
CCSDS Day Segmented | days + ms of day (+ sub-ms) |
Clock.gps() |
GPS | week number + seconds of week |
Custom segmented clocks (D/H/M/S, or any mixed-radix layout) are built
directly from Clock and ClockField — see
Custom clocks below.
Features
- Pure codec:
datetime64 <-> uint8 array <-> datetime64 - Vectorized: scalar and array inputs, zero-copy where possible
- Rational tick arithmetic (GCD-reduced) — no floating-point error
- Built-in epoch registry (Unix, J2000, GPS, GLONASS, Galileo, BeiDou, CCSDS)
- CUC P-field generation per CCSDS 301.0-B-4
Installation
pip install escapement
Requires Python 3.9+ and NumPy 1.20+.
Quick start
import numpy as np
from escapement import Clock, ClockField, epoch
# Unix timestamp, millisecond resolution
unix_ms = Clock.unix(resolution="ms", num_bytes=8)
encoded = unix_ms.encode(np.datetime64("2025-01-01T00:00:00.123"))
decoded = unix_ms.decode(encoded)
# CUC 4.2 (CCSDS epoch, 4 coarse + 2 fine bytes)
cuc = Clock.cuc(coarse_bytes=4, fine_bytes=2)
print(cuc.resolution_ns) # 15258 ns per finest tick
# GPS week + seconds
gps = Clock.gps()
t = gps.epoch + np.timedelta64(2 * 604800 + 100000, "s")
assert gps.decode(gps.encode(t)) == t
# Batch encode 1000 timestamps
times = np.array([cuc.epoch + np.timedelta64(i, "s") for i in range(1000)])
encoded = cuc.encode(times) # shape (1000, 6)
decoded = cuc.decode(encoded) # shape (1000,)
# Register a mission-specific epoch
epoch.register("LAUNCH", np.datetime64("2027-03-15T09:30:00"))
met = Clock.met(epoch="LAUNCH", resolution="ms", num_bytes=8)
Custom clocks
Any time code layout can be expressed by composing ClockField tuples.
Each ClockField(ticks, seconds, width) reads as "ticks per seconds
seconds, stored in width bytes". Fields are ordered coarsest to finest —
during encoding, each field consumes what it can from the remainder and
passes the rest down, exactly like the gear train in a mechanical clock.
from escapement import Clock, ClockField
# Days / hours / minutes / seconds — 5 bytes total
dhms = Clock(
epoch="UNIX",
fields=(
ClockField(1, 86400, 2), # days (1 tick per 86_400 s, 2 bytes)
ClockField(1, 3600, 1), # hours (1 tick per 3_600 s, 1 byte)
ClockField(1, 60, 1), # minutes (1 tick per 60 s, 1 byte)
ClockField(1, 1, 1), # seconds (1 tick per 1 s, 1 byte)
),
)
t = dhms.epoch + np.timedelta64(3, "D") + np.timedelta64(14, "h") \
+ np.timedelta64(30, "m") + np.timedelta64(45, "s")
raw = dhms.encode(t) # 5 bytes: [0, 3, 14, 30, 45]
assert dhms.decode(raw) == t
Leap seconds and timescales
escapement is a pure codec — it computes the arithmetic delta between an
epoch and a timestamp, then divides that delta into fields. It has no
notion of UTC, TAI, or leap seconds.
This is by design. Time code standards like CUC, CDS, and GPS define byte layouts, not timescales. A CUC packet from one spacecraft might count TAI seconds; from another it might count UTC seconds. The byte-level encoding is identical in both cases — only the interpretation differs. Mixing timescale logic into the codec would force one interpretation on everyone.
In practice this means:
numpy.datetime64is leap-second-unaware (it counts SI seconds on a proleptic Gregorian calendar), so it behaves like a continuous timescale.- GPS time is also continuous (no leap seconds since its 1980 epoch), so
Clock.gps()round-trips correctly without any correction. - If your input timestamps are UTC and you need TAI or GPS time, apply the leap second offset before encoding (or after decoding).
The authoritative source for leap seconds is IERS Bulletin C, published every six months at https://www.iers.org/IERS/EN/Publications/Bulletins/Bulletins.html.
The current TAI-UTC offset and the full history of leap second insertions
are also available in the leap-seconds.list file maintained by IETF/NIST
at https://www.ietf.org/timezones/data/leap-seconds.list.
License
escapement is licensed under the MIT License - see the LICENSE file for details
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file escapement-0.2.0.tar.gz.
File metadata
- Download URL: escapement-0.2.0.tar.gz
- Upload date:
- Size: 47.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea2376fabc0ac1ea200cb08afc3986ecd31da647baecfe293b7c8afcffff8782
|
|
| MD5 |
bccb6b3b46f5c0f6d600b876cd727366
|
|
| BLAKE2b-256 |
a22536739e539b2ac614715ef2d58ae277ad76b3c5bf57bc9b7e6dfaf33b6562
|
File details
Details for the file escapement-0.2.0-py3-none-any.whl.
File metadata
- Download URL: escapement-0.2.0-py3-none-any.whl
- Upload date:
- Size: 9.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2dc6ea41046e8e1574add1109fc395470bf6c6998dc629160d9b5dd6d571747b
|
|
| MD5 |
929c5d035cb97f3eba94949896bdad3e
|
|
| BLAKE2b-256 |
fabf91921edecf3eae2631ef9b010f4ec338aefe9f6826cd71b69f95fa8aed3f
|