Offsets and Time¶
Time Constants¶
The Time enum provides nanosecond-based constants for consistent unit
handling:
from sgnts.base import Time
print(Time.SECONDS) # 1_000_000_000
print(Time.MILLISECONDS) # 1_000_000
print(Time.MICROSECONDS) # 1_000
print(Time.NANOSECONDS) # 1
# Convert 2.5 seconds to nanoseconds
ns = int(2.5 * Time.SECONDS)
The Offset System¶
Offsets are integer sample counts at the maximum sample rate (16384 Hz by default). They provide a precise, rounding-error-free alternative to floating-point timestamps.
from sgnts.base import Offset
# Convert between seconds and offsets
offset = Offset.fromsec(1.0) # 16384
seconds = Offset.tosec(16384) # 1.0
# Convert between nanoseconds and offsets
offset = Offset.fromns(1_000_000_000) # 16384
ns = Offset.tons(16384) # 1_000_000_000
# Convert between samples at a given rate and offsets
offset = Offset.fromsamples(2048, 2048) # 16384
samples = Offset.tosamples(16384, 2048) # 2048
One second of data always spans 16384 offset units, regardless of sample rate. This is what makes cross-rate alignment possible. For a deeper discussion of why offsets exist and the problems they solve, see The Offset System.
Allowed Sample Rates¶
Only power-of-2 rates are allowed (1, 2, 4, ... up to the max rate). This ensures offset conversions are always exact integers:
from sgnts.base import Offset
print(Offset.ALLOWED_RATES) # {1, 2, 4, 8, 16, ..., 16384}
print(Offset.MAX_RATE) # 16384
Changing the Maximum Rate¶
If your application needs rates above 16384 Hz, increase the maximum globally before creating any buffers:
from sgnts.base import Offset
Offset.set_max_rate(262144)
print(Offset.MAX_RATE) # 262144
print(Offset.ALLOWED_RATES) # {1, 2, 4, ..., 262144}
This must be called once at application startup, before any buffers or elements are created.
Converting Between Sample Rates¶
Offsets act as a common currency between sample rates. Convert samples at one rate to offsets, then from offsets to samples at another rate:
from sgnts.base import Offset
# 1000 samples at 8192 Hz → how many samples at 4096 Hz?
offset = Offset.fromsamples(1000, 8192)
samples_4k = Offset.tosamples(offset, 4096) # 500
This also means two buffers at different rates that cover the same time span
share identical offset and offset_end values:
import numpy as np
from sgnts.base import Offset, SeriesBuffer
start = Offset.fromsec(10.0)
buf_8k = SeriesBuffer(offset=start, sample_rate=8192, data=np.zeros(8192))
buf_4k = SeriesBuffer(offset=start, sample_rate=4096, data=np.zeros(4096))
assert buf_8k.offset == buf_4k.offset # same start
assert buf_8k.offset_end == buf_4k.offset_end # same end — both 1 second
Valid Conversions¶
tosamples requires the offset to correspond to a whole number of samples at
the target rate. In practice, an offset must be a multiple of
MAX_RATE / sample_rate:
from sgnts.base import Offset
# 16384 offsets = exactly 4096 samples at 4096 Hz ✓
Offset.tosamples(16384, 4096) # 4096
# 10000 offsets is not a whole number of samples at 4096 Hz ✗
# Offset.tosamples(10000, 4096) → raises AssertionError
If you're constructing offsets from external timestamps, use fromsamples or
the rounding support in fromsec/fromns (below) to stay on valid boundaries.
Rounding to Sample Boundaries¶
When converting from seconds or nanoseconds, pass a sample_rate to round the
result to the nearest sample boundary at that rate. This is useful when
working with external timestamps that don't fall exactly on sample points:
from sgnts.base import Offset
# Without sample_rate — exact conversion, may not align with any rate
offset = Offset.fromns(1_000_000_123)
# With sample_rate — rounds to nearest sample at 2048 Hz
offset = Offset.fromns(1_000_000_123, sample_rate=2048)
# Now guaranteed to convert cleanly
samples = Offset.tosamples(offset, 2048) # integer result
This is particularly useful when defining segment boundaries from external data (GPS times, file timestamps) that need to map to exact sample points.
Calculating Time Differences¶
Since offsets are plain integers, arithmetic works naturally: