Documentation · v0.29.1

Dalaran docs

Everything from the first install to the shape of the wire protocol. If you're new, start at the quickstart and come back for the concepts once something surprises you.

Note. This documentation covers the 0.29.x line. The data model changed in 0.26; recordings written before that need dalaran migrate.

Installation

The SDK and the viewer ship together. Installing the Python package gives you both.

pip install dalaran-sdk

# verify — prints the version of both the SDK and the bundled viewer
dalaran --version

Supported platforms: macOS 12+ (arm64, x86-64), Linux glibc 2.28+ (x86-64, aarch64), Windows 10+. Jetson and Raspberry Pi wheels are published for aarch64.

Quickstart

Three things happen in every Dalaran program: you initialize a recording stream, you set a time, and you log an entity. Everything else is elaboration.

import dalaran as dl
import numpy as np

# 1. Open a stream. spawn=True launches the viewer and connects to it.
dl.init("quickstart", spawn=True)

for i in range(200):
    # 2. Position everything that follows on a timeline.
    dl.set_time("frame", sequence=i)

    theta = i * 0.05
    pts = np.stack([
        np.cos(np.linspace(0, 6.28, 2000) + theta),
        np.sin(np.linspace(0, 6.28, 2000) * 2 + theta),
        np.linspace(-1, 1, 2000),
    ], axis=1)

    # 3. Log an entity. The archetype decides how it renders.
    dl.log("world/helix", dl.Points3D(pts, colors=[110, 231, 183], radii=0.008))
    dl.log("metrics/theta", dl.Scalar(theta))

Run it. The viewer opens, the helix twists, and the scalar plot fills in beside it. Drag the timeline scrubber and both views move together — that synchronization is the whole point.

Opening the viewer

There are four ways to get pixels on screen:

  • dl.init(..., spawn=True) — launch and connect to the native viewer
  • dl.connect("127.0.0.1:9876") — attach to a viewer that's already running
  • dl.save("run.dlr") — write a file, open it later with dalaran run.dlr
  • dl.notebook_show() — embed the viewer in a Jupyter output cell
Tip. In long-running services, prefer dl.save() or a Hub connection over spawn. A viewer process that dies shouldn't take your logging with it — but if it does, the SDK buffers and drops rather than blocking your main loop.

Entities & paths

An entity path is a slash-separated address in your world: world/robot/arm/gripper. It is the unit that almost every feature operates on.

  • Transforms logged at a path apply to everything beneath it
  • Visibility, color overrides and blueprint queries select by glob: world/**/cam_*
  • Paths are created implicitly — there's no registration step

Choose paths that describe physical containment, not data provenance. world/robot/cam_front composes correctly; logs/camera_node/output does not.

Components

A component is one typed, columnar array stored against an entity at a point in time — Position3D, Color, Radius, Text. Components are the physical storage; archetypes are the ergonomic API on top.

You can log components directly when you need to update just one:

# Log geometry once...
dl.log("world/cloud", dl.Points3D(pts), static=True)

# ...then recolor it every frame without resending positions
for i, colors in enumerate(color_stream):
    dl.set_time("frame", sequence=i)
    dl.log("world/cloud", [dl.components.Color(colors)])

Timelines

Every log call is stamped on whatever timelines are currently set. Three kinds exist:

KindCallUse for
Sequenceset_time(name, sequence=i)Frame or step indices
Timestampset_time(name, timestamp=t)Wall clock, sensor clocks, epochs
Durationset_time(name, duration=s)Time since start, simulation time

Queries use latest-at semantics: at time t, each component takes the most recent value logged at or before t. This is why you only need to log what changed.

Static data

Data marked static=True has no timestamp and is visible at every point on every timeline. Use it for camera intrinsics, meshes, coordinate-system declarations and annotation contexts.

Careful. Static data overwrites rather than accumulates. Logging a static component twice keeps only the second value, for the whole recording — including the frames before it.

Archetypes

The full set, grouped by what they draw:

GroupArchetypes
3DPoints3D · Boxes3D · LineStrips3D · Mesh3D · Arrows3D · Asset3D · Ellipsoids3D · Capsules3D
2DImage · DepthImage · SegmentationImage · EncodedImage · Boxes2D · Points2D · LineStrips2D
SpatialTransform3D · Pinhole · ViewCoordinates · InstancePoses3D
PlotsScalar · SeriesLine · SeriesPoint · BarChart
OtherTensor · TextDocument · TextLog · AnnotationContext · Clear

Transforms

Log each frame's transform relative to its parent, never to the world. The viewer composes the chain.

dl.log("world", dl.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)

dl.log("world/base", dl.Transform3D(translation=[x, y, 0.0],
                                    rotation=dl.Quaternion(xyzw=q)))
dl.log("world/base/lidar", dl.Transform3D(translation=[0.0, 0.0, 0.42]))

# Logged in the lidar's own frame — no manual matrix math required
dl.log("world/base/lidar/points", dl.Points3D(raw_returns))

Images & cameras

Log a Pinhole at a path to declare it a camera. Any 3D entity beneath that path is projected into the image plane; any 2D entity is drawn in pixel coordinates.

dl.log("world/cam", dl.Pinhole(focal=[725.0, 725.0],
                               principal_point=[960.0, 540.0],
                               width=1920, height=1080), static=True)

dl.log("world/cam", dl.EncodedImage(path="frame_0412.jpg"))   # no decode cost
dl.log("world/cam/depth", dl.DepthImage(depth_m, meter=1.0))
dl.log("world/cam/mask", dl.SegmentationImage(labels))

Prefer EncodedImage for video-rate streams: it stores the compressed bytes and decodes lazily on the GPU, which typically cuts recording size by an order of magnitude.

Scalars & tensors

Scalar is a single number per timestamp; the viewer accumulates them into a series. Style the series once with SeriesLine, logged statically.

dl.log("metrics/loss", dl.SeriesLine(color=[110, 231, 183], name="train loss"), static=True)

for step, loss in enumerate(history):
    dl.set_time("step", sequence=step)
    dl.log("metrics/loss", dl.Scalar(loss))

# N-dimensional data goes to the tensor inspector
dl.log("debug/attn", dl.Tensor(attn, dim_names=["head", "query", "key"]))

Blueprints

A blueprint describes the viewer layout as data: which views exist, what each one contains, and how they're arranged. Pass one to dl.init or send it to a running viewer.

import dalaran.blueprint as dlb

bp = dlb.Blueprint(
    dlb.Tabs(
        dlb.Spatial3DView(origin="world", name="Scene",
                          contents=["world/**", "-world/debug/**"]),
        dlb.Grid(
            dlb.Spatial2DView(origin="world/cam_front"),
            dlb.Spatial2DView(origin="world/cam_rear"),
            dlb.TensorView(origin="debug/attn"),
            dlb.TimeSeriesView(origin="metrics"),
        ),
    ),
    collapse_panels=True,
)

dl.send_blueprint(bp)      # apply to the running viewer

Recordings & files

A .dlr file is a self-describing columnar archive: schema, data and metadata in one place, with no external dependency on the code that wrote it. Files are append-friendly and memory-mapped on open.

  • Recording ID — a UUID per stream; two processes can write to the same viewer without colliding
  • Application ID — groups recordings that belong to the same program
  • Chunks — data is batched before flush; tune with DALARAN_FLUSH_NUM_BYTES

CLI

dalaran run.dlr                        # open in the native viewer
dalaran --serve-web --port 8080        # serve the wasm viewer
dalaran ls run.dlr                     # entities, components, time ranges
dalaran filter run.dlr --entity 'world/cam/**' -o cam_only.dlr
dalaran merge left.dlr right.dlr -o combined.dlr
dalaran compare base.dlr candidate.dlr # synced side-by-side
dalaran migrate old.dlr -o new.dlr     # upgrade a pre-0.26 recording
dalaran analytics disable              # opt out of telemetry, permanently

API reference

dl.init(application_id, *, recording_id=None, spawn=False, default_blueprint=None)

Creates the global recording stream. Call once per process, before any log. Returns the stream so you can hold it explicitly in library code instead of relying on the global.

dl.log(entity_path, *archetypes_or_components, static=False)

Writes data to an entity at the current time. Accepts one or more archetypes, or a list of raw components. Non-blocking: data is batched and flushed on a background thread.

dl.set_time(timeline, *, sequence=None, timestamp=None, duration=None)

Sets the current position on a timeline for subsequent log calls. Exactly one of the three keyword arguments must be given. Call dl.disable_timeline(name) to stop stamping on it.

dl.save(path) · dl.connect(addr) · dl.serve_web(port)

Sinks. A stream has exactly one active sink; calling a second replaces the first and flushes the buffer.

dl.notebook_show(*, width=960, height=540, blueprint=None)

Renders the WebAssembly viewer inline in a notebook cell, bound to the current recording.

Troubleshooting

Check that a timeline is set — data logged before any set_time lands on an implicit "log time" timeline that the current view may not be showing. Also confirm the entity isn't filtered out by the active blueprint's contents query.
Almost always a transform logged in world coordinates under a path that already has a parent transform. Log local transforms only. Second most common cause: a missing ViewCoordinates declaration, so Z-up data is drawn Y-up.
Look for raw Image where EncodedImage would do, and for static geometry being re-logged every frame. dalaran ls --sizes run.dlr ranks entities by bytes on disk.
The SDK batches on a background thread, but very large per-frame payloads can still saturate the channel. Raise DALARAN_FLUSH_NUM_BYTES, or log at a lower rate for the heaviest entity. Setting DALARAN_STRICT=1 in development surfaces the warnings that are otherwise silent.

Something wrong or missing on this page? Tell us