# Track API

Tracks are timestamp-indexed, multi-modal streams: robot poses, sensor readings, per-step state. Each entry carries a float timestamp and an arbitrary dict payload, and entries that share a timestamp on the same topic merge into one row.

## Tracks vs. Metrics

Use a **track** when entries are timestamp-indexed and the schema may vary across topics (poses, cameras, lidar, RL transitions). Use a **metric** when you have step-indexed scalars for plotting (loss, accuracy). See [/metrics](/metrics.md).

## Basic Usage

```python
from ml_dash import Experiment

with Experiment("robotics/training").run as exp:
    for step in range(1000):
        t = step / 30.0  # 30 Hz simulator clock
        exp.tracks("robot/pose").append(
            q=[0.1, -0.22, 0.45],
            e=[0.5, 0.0, 0.6],
            _ts=t,
        )
```

`exp.tracks(topic)` returns a `TrackBuilder` bound to a topic path (e.g. `"robot/pose"`). `append(**fields, _ts=...)` writes one entry.

## Timestamps

`_ts` is **required** on every `append` call and must be numeric (cast to `float` internally). There is no auto-generated or inherited timestamp — omitting `_ts` raises `ValueError`. Pick a consistent clock per experiment (simulator time, wall clock, sensor timestamp).

Two `append` calls to the same topic at the same `_ts` merge: later fields overwrite earlier ones at the same keys. This lets you split a sample across calls without duplicating rows:

```python
exp.tracks("camera/rgb").append(frame_id=0, _ts=0.0)
exp.tracks("camera/rgb").append(path="frame_0.png", _ts=0.0)
# -> one row at _ts=0.0 with {frame_id: 0, path: "frame_0.png"}
```

Different topics keep independent timestamp tables, so log multi-modal samples at the same `_ts` across topics to align them later.

## Flexible Schema

The data dict is free-form per call — different fields per entry are allowed. The backend reconciles columns at read time.

## Reading

```python
TrackBuilder.read(
    start_timestamp: float | None = None,
    end_timestamp: float | None = None,
    columns: list[str] | None = None,
    format: str = "json",  # "json" | "jsonl" | "parquet" | "mocap"
)
```

`read()` returns the topic's entries (optionally filtered by timestamp range and projected to selected columns). Flush before reading in the same process.

```python
exp.tracks.flush()

data    = exp.tracks("robot/pose").read()
window  = exp.tracks("robot/pose").read(start_timestamp=0.0, end_timestamp=10.0)
parquet = exp.tracks("robot/pose").read(format="parquet")
```

## Flushing

```python
exp.tracks.flush()                  # flush all topics
exp.tracks("robot/pose").flush()    # flush one topic
```

Appends are non-blocking and batched by the background uploader. See [/buffering](/buffering.md) for batch size and flush interval configuration.

## Aligning with Frames

To pair a track entry with an image, log the filename alongside the data and use a consistent zero-padded index. See [/images](/images.md).

```python
for step in range(1000):
    t = step / 30.0
    fname = f"frame_{step:05d}.jpg"
    exp.files("frames").save_image(frame, to=fname)
    exp.tracks("robot/pose").append(frame=fname, q=q_step, _ts=t)
```

In MDX prose, wrap path templates like `{step}` or `{i:05d}` in backticks so the renderer doesn't treat them as expressions.
