ML-Dash

Track API

The Track API provides efficient time-series data tracking for robotics, reinforcement learning, and any sequential experiments.

Overview

Tracks are perfect for:

  • Robotics: Robot positions, joint angles, sensor readings
  • Reinforcement Learning: Agent trajectories, rewards, states
  • Simulations: Physics parameters over time
  • Sequential Data: Any data that changes over time

Basic Usage

python
from ml_dash import Experiment

with Experiment("robotics/training").run as experiment:
    for step in range(1000):
        # Append data to a track
        experiment.tracks("robot/position").append(
            step=step,
            x=position[0],
            y=position[1],
            z=position[2]
        )

Timestamp-Based Tracking

Tracks support three timestamp modes:

When _ts is not provided, timestamps are automatically generated:

python
with Experiment("my-project/exp").run as experiment:
    for i in range(100):
        # Auto-generate unique timestamps
        experiment.tracks("sensors/temperature").append(value=temperature)
        experiment.tracks("sensors/pressure").append(value=pressure)

2. Explicit Timestamps

Provide explicit timestamps when you need precise control:

python
import time

with Experiment("my-project/exp").run as experiment:
    for i in range(100):
        timestamp = time.time()

        # Track with explicit timestamp
        experiment.tracks("sensors/temperature").append(value=temperature, _ts=timestamp)

        # Track with same timestamp (entries are merged)
        experiment.tracks("sensors/pressure").append(value=pressure, _ts=timestamp)

3. Timestamp Inheritance with _ts=-1

Use _ts=-1 to inherit the last timestamp from the previous tracks.append() or metrics.log() call in the same thread. This is perfect for synchronizing multi-modal data:

python
with Experiment("robotics/multi-modal").run as experiment:
    for step in range(1000):
        # First append - auto-generates timestamp
        experiment.tracks("robot/pose").append(position=[1.0, 2.0, 3.0])

        # Following appends - inherit the same timestamp
        experiment.tracks("camera/left").append(width=640, height=480, _ts=-1)
        experiment.tracks("camera/right").append(width=640, height=480, _ts=-1)
        experiment.tracks("robot/velocity").append(linear=[0.1, 0.0, 0.0], _ts=-1)
        experiment.tracks("sensors/lidar").append(ranges=[1.5, 2.0, 2.5], _ts=-1)

        # All 5 tracks now share the exact same timestamp!

_ts=-1 also inherits across metrics and tracks — they share the same last timestamp per thread:

python
# Set timestamp via track, inherit in metric
experiment.tracks("robot/pose").append(position=[1.0, 2.0, 3.0], _ts=100.0)
experiment.metrics("train").log(loss=0.5, _ts=-1)   # inherits 100.0

# Set timestamp via metric, inherit in track
experiment.metrics("train").log(loss=0.5)            # auto _ts, sets _last_timestamp
experiment.tracks("robot/pose").append(x=1.0, _ts=-1)  # inherits it

Benefits of _ts=-1:

  • Cleaner code - no need to manually pass timestamps around
  • Less error-prone - can't accidentally use wrong timestamp
  • Perfect for robotics/ML multi-modal data (poses, images, sensors at same instant)
  • Works across ALL tracks and metrics globally (per thread)

Multiple Tracks

Track different aspects of your experiment:

python
with Experiment("robotics/walker").run as experiment:
    for step in range(1000):
        # Robot position
        experiment.tracks("robot/position").append(
            step=step,
            x=pos[0],
            y=pos[1],
            z=pos[2]
        )

        # Joint angles
        experiment.tracks("robot/joints").append(
            step=step,
            joint1=angles[0],
            joint2=angles[1],
            joint3=angles[2]
        )

        # Control signals
        experiment.tracks("robot/control").append(
            step=step,
            motor1=ctrl[0],
            motor2=ctrl[1]
        )

        # Sensor readings
        experiment.tracks("robot/sensors").append(
            step=step,
            imu_x=imu[0],
            imu_y=imu[1],
            imu_z=imu[2],
            force=force_sensor
        )

Numpy Array Serialization

Tracks automatically serialize numpy arrays:

python
import numpy as np

with Experiment("rl/training").run as experiment:
    for episode in range(100):
        state = env.reset()  # numpy array

        # Numpy arrays are automatically converted to lists
        experiment.tracks("agent/states").append(
            episode=episode,
            state=state,  # numpy array - auto-serialized!
            reward=0.0
        )

MuJoCo Example

Perfect for tracking robot simulations:

python
import mujoco
import numpy as np

with Experiment("robotics/mujoco-sim").run as experiment:
    model = mujoco.MjModel.from_xml_string(xml_content)
    data = mujoco.MjData(model)
    renderer = mujoco.Renderer(model, height=480, width=640)

    for i in range(1000):
        # Apply control
        data.ctrl[0] = 4.0 * np.sin(i * 0.03)
        data.ctrl[1] = 2.5 * np.sin(i * 0.05)

        # Step simulation
        mujoco.mj_step(model, data)

        if i % 4 == 0:
            # Get end effector position
            ee_pos = data.site_xpos[model.site('end_effector').id].copy()

            # Track position
            experiment.tracks("robot/position").append(
                step=i,
                x=ee_pos[0],
                y=ee_pos[1],
                z=ee_pos[2]
            )

            # Track control
            experiment.tracks("robot/control").append(
                step=i,
                ctrl_1=float(data.ctrl[0]),
                ctrl_2=float(data.ctrl[1])
            )

            # Save frame aligned with track
            renderer.update_scene(data)
            pixels = renderer.render()
            experiment.files("frames").save_image(
                pixels,
                to=f"frame_{i:05d}.jpg"
            )

Aligning Frames with Tracks

Use consistent step indices to align frames with track data:

python
with Experiment("vision/tracking").run as experiment:
    for step in range(1000):
        # Track data
        experiment.tracks("agent/position").append(
            step=step,
            x=x,
            y=y
        )

        # Save frame with same step index
        experiment.files("frames").save_image(
            frame,
            to=f"frame_{step:05d}.jpg"
        )

        # Now frames and tracks are aligned by step index!

Reinforcement Learning Example

python
import gymnasium as gym

with Experiment("rl/cartpole").run as experiment:
    env = gym.make("CartPole-v1")

    for episode in range(100):
        state, info = env.reset()
        done = False
        total_reward = 0
        step = 0

        while not done:
            action = agent.act(state)
            next_state, reward, terminated, truncated, info = env.step(action)
            done = terminated or truncated

            # Track episode trajectory
            experiment.tracks("agent/trajectory").append(
                episode=episode,
                step=step,
                state=state.tolist(),  # numpy to list
                action=int(action),
                reward=float(reward),
                next_state=next_state.tolist()
            )

            state = next_state
            total_reward += reward
            step += 1

        # Track episode summary
        experiment.tracks("agent/episodes").append(
            episode=episode,
            total_reward=total_reward,
            steps=step
        )

Buffering and Performance

Tracks use the background buffering system:

python
with Experiment("high-freq/tracking").run as experiment:
    # All track appends are non-blocking
    for i in range(100000):
        experiment.tracks("sensor/data").append(
            step=i,
            value=sensor.read()
        )
        # Returns immediately! No blocking I/O

    # Automatically batched and uploaded in background

Configure track buffering:

bash
export ML_DASH_TRACK_BATCH_SIZE=100  # Batch 100 track entries
export ML_DASH_FLUSH_INTERVAL=5.0    # Flush every 5 seconds

Timestamp Merging

Entries with the same timestamp are automatically merged:

python
# Using explicit timestamps
experiment.tracks("multi-sensor").append(temperature=25.0, _ts=12345.67)
experiment.tracks("multi-sensor").append(pressure=1013.0, _ts=12345.67)

# Result: Single entry with both fields
# {"timestamp": 12345.67, "temperature": 25.0, "pressure": 1013.0}

You can also use _ts=-1 for merging:

python
# First append sets the timestamp
experiment.tracks("multi-sensor").append(temperature=25.0, _ts=12345.67)

# Following appends inherit and merge
experiment.tracks("multi-sensor").append(pressure=1013.0, _ts=-1)
experiment.tracks("multi-sensor").append(humidity=60.0, _ts=-1)

# Result: Single entry with all three fields at timestamp 12345.67

Best Practices

1. Use Consistent Indexing

python
# Good: Use step index consistently
for step in range(1000):
    experiment.tracks("data").append(step=step, value=v
    )
    experiment.files("frames").save_image(frame, to=f"frame_{step:05d}.jpg")

# Bad: Inconsistent indexing makes alignment hard
for i in range(1000):
    experiment.tracks("data").append(index=i * 2, value=v
    )
    experiment.files("frames").save_image(frame, to=f"frame_{i}.jpg")

2. Include Step/Episode in Data

python
# Good: Always include step/episode for easy querying
experiment.tracks("agent/rewards").append(
    episode=episode,
    step=step,
    reward=reward
)

# Less useful: Missing context
experiment.tracks("agent/rewards").append(
    reward=reward
)

3. Organize by Topic

python
# Good: Clear topic hierarchy
experiment.tracks("robot/joints/arm").append(**data)
experiment.tracks("robot/joints/leg").append(**data)
experiment.tracks("robot/sensors/imu").append(**data)

# Less clear: Flat structure
experiment.tracks("arm_data").append(**data)
experiment.tracks("leg_data").append(**data)

4. Zero-Pad Filenames for Alignment

python
# Good: Files sort correctly
experiment.files("frames").save_image(frame, to=f"frame_{i:05d}.jpg")
# frame_00000.jpg, frame_00001.jpg, frame_00002.jpg

# Bad: Sorting breaks
experiment.files("frames").save_image(frame, to=f"frame_{i}.jpg")
# frame_1.jpg, frame_10.jpg, frame_2.jpg  (wrong order!)

5. Store Frame References in Tracks

python
for step in range(1000):
    frame_filename = f"frame_{step:05d}.jpg"

    # Save frame
    experiment.files("frames").save_image(frame, to=frame_filename)

    # Reference frame in track
    experiment.tracks("robot/position").append(
        step=step,
        frame=frame_filename,  # Link to frame
        x=x,
        y=y,
        z=z
    )

Track Slicing and Iteration

The slice() method returns an iterable view of track data with timestamp-based indexing using floor matching.

Basic Iteration

python
with Experiment("robotics/analysis").run as experiment:
    # ... append data to tracks ...
    experiment.tracks.flush()

    # Create slice
    track_slice = experiment.tracks("robot/pose").slice()

    # Iterate through all entries
    for entry in track_slice:
        timestamp = entry["timestamp"]
        position = entry["position"]
        print(f"t={timestamp}: pos={position}")

Timestamp Range Filtering

python
# Slice specific time range
track_slice = experiment.tracks("robot/pose").slice(
    start_timestamp=10.0,
    end_timestamp=20.0
)

# Iterate through entries in range [10.0, 20.0]
for entry in track_slice:
    process(entry)

Floor-Match Timestamp Queries

The slice object supports findByTime() with floor matching: returns the entry with the largest timestamp ≤ queried timestamp.

python
# Create slice
track_slice = experiment.tracks("robot/pose").slice()

# If timestamps are [1.0, 3.0, 5.0, 7.0, 9.0]:

# Single timestamp queries
entry = track_slice.findByTime(5.5)  # Returns entry at timestamp 5.0 (floor match)
entry = track_slice.findByTime(5.0)  # Returns entry at timestamp 5.0 (exact match)
entry = track_slice.findByTime(6.9)  # Returns entry at timestamp 5.0 (floor match)
entry = track_slice.findByTime(7.0)  # Returns entry at timestamp 7.0 (exact match)

# Batch queries with list of timestamps
entries = track_slice.findByTime([1.0, 3.5, 7.0])  # Returns list of 3 entries
# entries[0] -> timestamp 1.0
# entries[1] -> timestamp 3.0 (floor match for 3.5)
# entries[2] -> timestamp 7.0

# Query before first timestamp raises ml_dash.StorageError
# track_slice.findByTime(0.5)  # StorageError: No entry found with timestamp <= 0.5

# Query after last timestamp returns last entry
entry = track_slice.findByTime(100.0)  # Returns entry at timestamp 9.0

Practical Example: Robot Trajectory Analysis

python
with Experiment("robotics/analysis").run as experiment:
    # ... data already logged ...

    # Analyze trajectory in specific time window
    trajectory = experiment.tracks("robot/pose").slice(0.0, 30.0)

    # Iterate through trajectory
    positions = []
    for entry in trajectory:
        positions.append(entry["position"])

    # Get robot state at specific times (single queries)
    state_at_5s = trajectory.findByTime(5.0)
    state_at_10s = trajectory.findByTime(10.0)
    state_at_15_5s = trajectory.findByTime(15.5)  # Floor match to closest ≤ 15.5

    # Or batch query multiple times at once
    states = trajectory.findByTime([5.0, 10.0, 15.5, 20.0, 25.0])
    for state in states:
        print(f"t={state['timestamp']}: pos={state['position']}")

    # Calculate metrics
    total_distance = calculate_distance(positions)
    print(f"Total distance: {total_distance:.2f}m")
    print(f"Number of waypoints: {len(trajectory)}")

Synchronizing Multi-Modal Data

Combine slicing with timestamp inheritance for synchronized queries:

python
with Experiment("robotics/sync").run as experiment:
    # Read multiple synchronized tracks
    pose_slice = experiment.tracks("robot/pose").slice(0.0, 10.0)
    camera_slice = experiment.tracks("camera/left").slice(0.0, 10.0)
    lidar_slice = experiment.tracks("sensors/lidar").slice(0.0, 10.0)

    # Query all tracks at same timestamp (floor match ensures sync)
    for pose_entry in pose_slice:
        t = pose_entry["timestamp"]
        camera_entry = camera_slice.findByTime(t)
        lidar_entry = lidar_slice.findByTime(t)

        # All entries at same timestamp (or floor-matched)
        process_multimodal(pose_entry, camera_entry, lidar_entry)

Slice Features

Iterator Protocol:

  • for entry in slice: ... - Iterate through entries
  • iter(slice) - Get iterator
  • Can iterate multiple times (data is cached)

Timestamp Queries:

  • slice.findByTime(timestamp) - Get entry by timestamp (floor match)
  • Optimized for sequential queries using internal index
  • Raises StorageError (from ml_dash) if query timestamp is before first entry

Length:

  • len(slice) - Get number of entries in slice

Representation:

  • repr(slice) - Shows topic, start, and end timestamps

API Reference

experiment.tracks(topic: str) -> TrackBuilder

Returns a TrackBuilder for the specified topic.

Parameters:

  • topic (str): Topic name (e.g., "robot/position")

Returns:

  • TrackBuilder instance

TrackBuilder.append(**fields, _ts=None) -> TrackBuilder

Append a data entry to the track. Keyword-only: each keyword becomes a field.

Parameters:

  • **fields: Data fields to track
  • _ts (float, optional): Timestamp. Omitted: generated from the current time. -1: reuse the previous timestamp. A number: use it as given.

Examples:

python
# With automatic timestamp
experiment.tracks("sensor/temp").append(value=25.0)

# With explicit timestamp
experiment.tracks("sensor/temp").append(value=25.0, _ts=time.time())

# Multiple fields
experiment.tracks("sensor/temp").append(
    step=100,
    value=25.0
)

# From a dict
experiment.tracks("sensor/temp").append(**{"value": 25.0})

TrackBuilder.slice(start_timestamp: float = None, end_timestamp: float = None) -> TrackSlice

Create an iterable slice of track data with timestamp-based indexing.

Parameters:

  • start_timestamp (float, optional): Start timestamp (inclusive)
  • end_timestamp (float, optional): End timestamp (inclusive)

Returns:

  • TrackSlice object supporting iteration and timestamp indexing

Examples:

python
# Full track slice
track_slice = experiment.tracks("robot/pose").slice()

# Time range slice
track_slice = experiment.tracks("robot/pose").slice(
    start_timestamp=0.0,
    end_timestamp=10.0
)

# Iterate
for entry in track_slice:
    print(entry["timestamp"], entry["position"])

# Floor-match timestamp query
entry = track_slice.findByTime(5.5)  # Returns entry with largest timestamp <= 5.5

# Get length
count = len(track_slice)

Comparison with Metrics

FeatureTracksMetrics
Use CaseTime-series data, trajectoriesTraining metrics, losses
StructureFlexible dict per entryFlat key-value pairs
Timestamp_ts field, explicit or auto_ts field, explicit or auto
_ts=-1 inheritanceYes, shared with metricsYes, shared with tracks
MergingTimestamp-based mergingNo merging
Best ForRobotics, RL, sensorsTraining loss, accuracy

Example: Complete Robot Tracking

python
import mujoco
import numpy as np
from ml_dash import Experiment

with Experiment(
    prefix="robotics/complete-tracking",
    tags=["mujoco", "tracking"],
    dash_url="http://localhost:3000"
).run as experiment:
    experiment.log("Starting robot tracking experiment")

    # Initialize MuJoCo
    model = mujoco.MjModel.from_xml_string(xml_content)
    data = mujoco.MjData(model)
    renderer = mujoco.Renderer(model, height=480, width=640)

    steps = 1000
    record_interval = 4

    for i in range(steps):
        # Control
        ctrl_1 = 4.0 * np.sin(i * 0.03)
        ctrl_2 = 2.5 * np.sin(i * 0.05)
        data.ctrl[0] = ctrl_1
        data.ctrl[1] = ctrl_2

        # Simulate
        mujoco.mj_step(model, data)

        # Record
        if i % record_interval == 0:
            # Render
            renderer.update_scene(data)
            pixels = renderer.render()

            # Get state
            ee_pos = data.site_xpos[model.site('end_effector').id].copy()

            # Save frame
            experiment.files("robot/frames").save_image(
                pixels,
                to=f"frame_{i:05d}.jpg",
                quality=85
            )

            # Track position
            experiment.tracks("robot/position").append(
                step=i,
                x=float(ee_pos[0]),
                y=float(ee_pos[1]),
                z=float(ee_pos[2])
            )

            # Track control
            experiment.tracks("robot/control").append(
                step=i,
                ctrl_1=float(ctrl_1),
                ctrl_2=float(ctrl_2)
            )

            experiment.log(
                f"Step {i:3d}: pos=({ee_pos[0]:.4f}, "
                f"{ee_pos[1]:.4f}, {ee_pos[2]:.4f})"
            )

    experiment.log("Tracking complete!")

This creates a complete tracking dataset with aligned frames, positions, and control signals!