# Offsets and Time

## Time Constants

The `Time` enum provides nanosecond-based constants for consistent unit
handling:

```python
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.

```python
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](../background/offsets.md).

## 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:

```python
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:

```{.python notest}
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:

```python
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:

```python
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`:

```python
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:

```python
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:

```python
from sgnts.base import Offset

event1 = Offset.fromsec(10.25)
event2 = Offset.fromsec(12.75)

diff = event2 - event1
print(Offset.tosec(diff))                 # 2.5 seconds
print(Offset.tosamples(diff, 8192))  # 20480 samples
```
