Skip to main content

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.datetime64 is 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

escapement-0.2.0.tar.gz (47.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

escapement-0.2.0-py3-none-any.whl (9.6 kB view details)

Uploaded Python 3

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

Hashes for escapement-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ea2376fabc0ac1ea200cb08afc3986ecd31da647baecfe293b7c8afcffff8782
MD5 bccb6b3b46f5c0f6d600b876cd727366
BLAKE2b-256 a22536739e539b2ac614715ef2d58ae277ad76b3c5bf57bc9b7e6dfaf33b6562

See more details on using hashes here.

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

Hashes for escapement-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2dc6ea41046e8e1574add1109fc395470bf6c6998dc629160d9b5dd6d571747b
MD5 929c5d035cb97f3eba94949896bdad3e
BLAKE2b-256 fabf91921edecf3eae2631ef9b010f4ec338aefe9f6826cd71b69f95fa8aed3f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page