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.1.0.tar.gz (42.0 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.1.0-py3-none-any.whl (8.6 kB view details)

Uploaded Python 3

File details

Details for the file escapement-0.1.0.tar.gz.

File metadata

  • Download URL: escapement-0.1.0.tar.gz
  • Upload date:
  • Size: 42.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","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.1.0.tar.gz
Algorithm Hash digest
SHA256 13e0af241cbef910df458c829f0fa8cbffbdf6b6c3b69e091d2370d617448015
MD5 05265feb5a5d6744f437c604e58793b8
BLAKE2b-256 ee84a4e0705131e001223b7f3b2d93e51cf1f08575388dec3d5ec1b390b44816

See more details on using hashes here.

File details

Details for the file escapement-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: escapement-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 8.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3b98369f684dd3f44b9f48796026c8564f5659cc85f6782c212d613fdac20fe8
MD5 68a7cc0f14dd6c5138dc690486e5f5c2
BLAKE2b-256 3c1ab8ec818b86835d7a501a5a2338ce16aee749e2d3eeb8ea4f296514e1dbe5

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