Skip to main content

Decide the shape first

Series identity is (run, kind, key, dimensions). Every distinct dimension combination is a separate series — and a series holding one point has nothing to plot, so it renders as a scalar tile rather than a graph. Every metric is one of three shapes:

Curve

Loss, learning rate, reward over time. One series, many points, distinguished by step.Only step makes a curve. Spreading values across a dimension does not.

Headline scalar

Final accuracy. One series, one point, zero to two low-cardinality dimensions.

Breakdown

Accuracy by category. One series per category value.
Budget series at the write. The number of series is roughly the product of your dimension cardinalities. Past about 50 you have designed a wall of tiles.That failure is silent: every call succeeds, the values are correct, the data stays fully queryable, and the only symptom is an unreadable run page. Assert the count after the first run instead of eyeballing the dashboard.

Dimensions versus labels

This is the distinction that decides whether a run page is readable.
A dimension is never an identifier. 500 examples logged with example_id as a dimension is 500 series, 500 tiles and zero graphs. Per-sample identity goes in labels; per-item detail belongs in an artifact, which is where analysis code reads it from anyway.Metrics are for what a human should see. Artifacts are for what code reads.
Both maps merge over any ambient unit() context, with the explicit call site winning per key. A key present in both maps raises ValueError. On the CLI, dimensions are --dim:

Steps

A resumed run continues its curve, so a step at or below the resume point is treated as a retry rather than appended — that would splice two executions into one series. The hardware kind is exempt in both directions, because its steps come off a different clock.

Timestamps

wall_clock is optional and, when omitted, the point is stamped at ingest time. That is right for a live training loop and wrong for an importer replaying history — pass wall_clock explicitly when the value’s real time is not now.

Value types

Numbers — including bools, numpy scalars and 0-dimensional tensors — become metric points and plot. Strings, dicts, lists and None go into that step’s record and read back through the trajectory view. You do not need to filter before logging.

Kinds

kind separates rails that should not share a page. model is the default; hardware is what log_hw writes.
Use a separate kind to keep a high-cardinality cloud off the run page while leaving it queryable:
Never log a headline scalar and a per-item cloud under the same key. A computed view resolves a key and refuses one carrying several dimension variants. Use two keys.

Declaring the reduction

agg (mean, sum, min, max, count) declares how the key reduces, so a later grouped read can omit its own. The producer knows whether a count sums or a loss averages; declaring it at the write saves every reader from guessing. Conflicting declarations are rejected.

Derived series

Values computed after the fact are marked as such, so post-hoc numbers never masquerade as training-loop capture.
Derived series carry a derived chip in the dashboard, with producer and computed-at on hover.

Expression views

A view is a read-time formula over logged series. The stored object is the formula, never data — so a view costs nothing and can be created on a completed run.
Views are agent-authored by design: the dashboard renders, renames and deletes one but never composes one. A panel card marks itself with an ƒ chip and shows its rendered formula as a subtitle, and its detail page carries the formula, its inputs, the raw spec, and who generated it.

Reading metrics back

grouped reduces server-side. export is lossless and cursor-paginated — use it when you need the actual rows, not a chart’s downsampled view.

Curves in the terminal

Bare, it prints the board — every key with its last, min and max — which is the overview to read before choosing what to look at. Several --keys get a panel each, because two metrics on different scales do not share an axis. --overlay puts them on one canvas and says in the footer when the scales are far enough apart that the smaller curve has flattened. Anything the picture cannot show is printed to stderr first: a read the window cut short, a --key that matched nothing, a series dropped from an overlay, a non-finite point no scale can hold. On the canvas, % marks a cell more than one curve reached. Colour is off when stdout is not a TTY and whenever NO_COLOR is set; braille and box-drawing degrade to ASCII when the stream’s encoding cannot carry them. Unlike its siblings, plot accepts a petname short_id as well as a UUID.