Trace Player
This page documents the trace playback tier of the block pages and animation standard. It covers the infrastructure built in phase P0 and plays its test trace. Block pages use the same player for their own animations.
Test trace: an open-loop roll pulse
Section titled “Test trace: an open-loop roll pulse”The 3-D view needs WebGL, which this browser does not provide. The plots and the description below show the same trace.
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
The vehicle hovers at 1 m with every rotor at the hover speed of about 1660 rad/s. At 0.1 s the left rotors (1 and 2) are commanded 40 rad/s faster and the right rotors (0 and 3) 40 rad/s slower. The rotors follow with a 20 ms lag, and the vehicle starts to roll right side down. At 0.22 s the commands reverse, which brakes the roll. At 0.34 s every rotor is commanded back to hover speed. The vehicle is left rolled by about 0.18 rad (10°). Nothing levels it, because there is no controller. The tilted thrust pushes the vehicle to the right (−y) and it slowly sinks, about 1.4 m sideways and 0.11 m down by 1.5 s.
- left rotors +40 rad/s, right rotors -40 rad/s: roll torque
- reversed: braking torque
- hover command: the vehicle stays rolled about 10°
- ✓ passBB-V3 attitude_norm_error_max = 4.44e-16 (<= 1.78e-15), computed by the scenario harness
Look for three things:
- Motor lag. The rotor speeds follow the dashed commands with a delay of about τ = 20 ms, so the torque arrives late.
- Coupling. Roll does not move the vehicle by itself. Translation begins only once the thrust vector tilts.
- Conventions. Positive roll lowers the right side (rotation about body x, with z up). The vehicle then slides toward −y, to its right.
The check below the player is computed by the scenario harness, in C++. The player only displays it (ANI-4). Its badge reads pass or fail, or fails, as designed for a check the trace expects to fail: a scenario that shows the edge of the block’s envelope.
A scene: the waypoint square among trees
Section titled “A scene: the waypoint square among trees”A trace can carry a scene: the world the vehicle flies in, drawn around it
(trace format additions). This one is the
cascade’s waypoint square,
unchanged, with a scene added by its scenario
(tools/trace/scenarios/player/scene_square.cpp).
The 3-D view needs WebGL, which this browser does not provide. The plots and the description below show the same trace.
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
The flight is the cascade's waypoint square: RP-1 flies a 10 m square at 5 m and ends where it started. Around it, the scene: a boundary box, six trees (cylinders drawn as trees, one in the middle of the square), a rock (a box drawn in the obstacle colour), a gate on the third leg that the path runs through, the goal at the start corner, and the position setpoint as a marker that moves, its pose read from the trace's setpoint_position and target_attitude channels. The Collision shapes button draws each entity's shape, which is what a simulation tests, over its visual: the trees' canopies are drawn, but only their trunks are obstacles.
- leg 1: the setpoint sets off along +x
- corner: the setpoint turns 90°
- corner: the setpoint turns 90°
- corner: the setpoint turns 90°
- back at the start: hover
- ✓ passBB-I2 worst_criterion_fraction = 0.1981 (<= 1), computed by the scenario harness
Look for three things:
- Fixed and moving entities. The trees, the rock, the gate and the goal stay where the scene puts them. The setpoint marker moves with its channels, ahead of the vehicle on each leg.
- Shape and visual. With Collision shapes on, each tree shows the cylinder under it: the trunk. A simulation tests that cylinder; the canopy is only drawn.
- The whole field. The Overview camera and the ground grid take in the scene as well as the path.
How a trace gets onto a page
Section titled “How a trace gets onto a page”- Scenario. Add a source file under
tools/trace/scenarios/<block>/. It drives the block’s own C++ and records samples withbranes::reflex::trace::Trace, the writer intools/trace/include/branes/reflex/trace/writer.hpp. The writer enforces the schema’s invariants as the scenario runs: monotone uniform time, one entry per sample, fixed widths, finite values, and units on non-reserved channels. It writes$REFLEX_TRACE_DIR/<block>/<scenario>.json. CMake builds each scenario astrace_<block>_<scenario>. - Generate.
scripts/gen-traces.sh gcc-release docs-site/src/tracesbuilds and runs every scenario. It stamps the commit asgit describe --dirty, so a trace generated from uncommitted changes says so. - Commit the trace with the change that produced it.
- Play. Import the JSON file in an
.mdxpage and pass it to<TracePlayer>, together with a title that asks one question, a text description, and the plots to show. The component validates the trace at build time, so an invalid trace fails the docs build.
import TracePlayer from '@site/components/trace/TracePlayer.astro';import rollPulse from '@site/traces/quadrotor/roll_pulse.json';
<TracePlayer trace={rollPulse} title="…?" description="…" plots={[{ channel: 'rotor_speeds', title: 'Rotor speeds', setpoint: true }]} />Component options
Section titled “Component options”| Prop | Meaning |
|---|---|
trace | The imported trace JSON |
title | The one question the animation answers (ANI-7) |
description | What happens, in text (ANI-10); for a V&V animation, also the pass criterion (ANI-4) |
plots | The plot panels, top to bottom (below) |
thrustScale | Thrust-arrow length in m per N, shown on screen (default 0.04) |
view | false hides the 3-D view, for a trace where the vehicle barely moves |
ghost | A second run drawn translucent beside the first: { position, attitude, thrusts?, label } name its channels |
live | Makes the player a live widget: the name of a scenario in src/lib/trace/live.js (live widgets) |
prompt | For a live widget: what to predict before moving a slider (ANI-8) |
repo, provenanceNote | For a trace from another repo: the GitHub repository the footer links the scenario and commit to, and how the trace is kept (defaults: reflex, and “checked by CI against a fresh run”) |
Each entry of plots takes:
| Field | Meaning |
|---|---|
channel, title | The channel to plot, and the panel title; the unit is appended |
components, labels, colors | Which components of a vector channel, their legend labels, and their colour slots (1–4) |
setpoint, setpointLabel | Overlay <channel>_setpoint, dashed in the same colours, named in the legend by setpointLabel (default “command”) |
series | More lines from other channels in the same unit: { channel, component?, label, color?, dashed? }, for a limit, a reference, or a second run |
yRange | A fixed y-axis range [lo, hi] to zoom in on part of a signal; lines are clipped to it, and the axis ticks show the range |
The quadrotor model pages use each of these.
An entry with kind: 'bars' is a bar panel instead: one bar per component
of a vector channel, at the cursor’s sample, for quantities read side by
side rather than over time. The bars move with the cursor, and their values
are printed under them. It has no time axis, and its y range is fitted to
the whole trace so the axis holds still.
| Field | Meaning |
|---|---|
channel, title, labels, label | The vector channel, the panel title (the unit is appended), a label under each bar, and the legend label |
compare | { channel, label }: a second run’s channel of the same width, its bars beside the first |
marker | { channel, label }: a channel of the same width drawn as a dashed outline over each bar, such as the demand before limits |
limits | { channel, label }: a two-component channel [lo, hi], drawn as dashed lines across the panel |
The mixer explainer uses one for the rotor thrusts.
An entry with kind: 'spectrogram' is a spectrogram: frequency against
time, one column per sample and one row per frequency bin, coloured by
level on a sequential ramp in each theme. The trace records the spectrum,
computed by the scenario’s C++, as linear amplitudes in the signal’s unit,
one vector per sample; the player converts them to dB for the colour and
paints nothing it computes (ANI-1). Recording amplitudes rather than dB
keeps the regen and wasm comparisons absolute where the spectrum is
near zero. The columns after the cursor are dimmed, so the spectrogram
fills in as the trace plays, and a screen reader can read the strongest
peaks at the cursor.
| Field | Meaning |
|---|---|
channel, title | The spectrum channel, the amplitude in bin b at b·binHz, and the panel title |
binHz | A scalar channel in Hz holding the bin spacing, the same at every sample |
range | The colour range in dB relative to one unit of the channel (default [-60, 0]) |
overlay | { channel, weights?, label }: lines over the image, one per component of a channel in Hz, drawn only where the weights channel (same width) is above 0 |
The gyro filters explainer uses them for a throttle sweep, with the notch centres as the overlay.
What CI checks
Section titled “What CI checks”The Docs traces job in ci.yml runs on every pull request:
| Check | Tool | Fails when |
|---|---|---|
| Schema and semantics | npm run traces:check (the package’s JSON Schema, schemas/reflex-trace-1.schema.json, plus its validate) | A committed trace is malformed |
| Failing scenario (BB-I2) | npm run traces:check (the package’s status) | A check in a committed trace fails, or passes when the trace marks it "expect": "fail" |
| Staleness (ANI-3) | scripts/gen-traces.sh into build/traces, then check.mjs --regen | A regenerated trace is not equivalent to the committed one under the regen profile, or a scenario’s trace is missing from either side |
| Trace diff (BB-D3) | npm run trace-diff -- <candidate> <reference> [--profile regen|wasm] | (used by the staleness check; also a local tool) It reports the first channel and sample that differ |
| Docs build | npm run build | A page passes an invalid trace to TracePlayer |
| Wasm agreement (Tier 2) | scripts/build-wasm.sh with the pinned emsdk, then npm run wasm-agree | A live scenario fails in the WebAssembly module, or its trace at the default parameters is not equivalent to the committed native trace under the wasm profile |
| Size budget (ANI-12) | npm run size-budget -- --require-wasm | A trace is over 250 KB gzipped, the Wasm module with its loader is over 400 KB or missing, or a page without a 3-D view loads three.js |
A model change that alters a trace therefore fails CI until the trace is regenerated and committed in the same pull request.
Live widgets
Section titled “Live widgets”A live widget is a player with sliders. Each slider move reruns the scenario’s C++, compiled to WebAssembly, in the reader’s browser, and the player then plays the new trace: plots, 3-D view, events, and checks. The footer names the Emscripten build, and the status line labels the run “exploration, not evidence”, because only the recorded run is checked by CI.
To make a scenario live:
- Parameterize it. Move the scenario’s body into
tools/trace/scenarios/<block>/<scenario>.hpp, as a function of aParamsstruct that rejects values outside the slider ranges (ANI-9). The.cppcalls it with the defaults, so the committed trace is the default run. - Export it. Add a case to
reflex_run()intools/wasm/reflex_wasm.cpp. The C ABI takes up to three parameters. - Register it. Add an entry to
LIVEinsrc/lib/trace/live.js: the committed trace, and for each parameter a label, display unit, scale to SI, range, step, and default. CI’snpm run wasm-agreethen runs it at the defaults and compares it with the committed trace. - Use it. Pass
live="<scenario>"and apromptto theTracePlayer.
The module loads only when a reader first moves a slider. A site built
without it, as in a local build with no emsdk, still plays the recorded
traces, and the sliders report that live runs are unavailable.
tools/README.md has the build commands.
Player behaviour
Section titled “Player behaviour”- Playback. The player starts paused. It has play and pause, a scrubber, speeds of 0.1× to 1×, and seek buttons for each event. Dragging on any plot also scrubs. The current rate is always shown in the header.
- Keyboard. With focus anywhere in the player: Space or K plays and
pauses, ← and → step one sample (ten with Shift), Home and End jump to the
ends, and
[and]change the speed. - Reduced motion. With
prefers-reduced-motion, the camera starts in the fixed overview instead of the chase view, and still only plays on request. - Sample and hold. The cursor always sits on a recorded sample. The player neither interpolates nor computes dynamics, so everything on screen comes from the trace.
- 3-D view. The view shows world axes and a ground grid at z = 0 with 0.5 m squares, and the PX4 Quad X glyph. Rotors carry their model indices, spin arrows, and colours that match the plots. Each rotor has a thrust arrow, drawn to the scale stated on screen, and the view also shows body axes, a nose cone on +x, and the trail so far. three.js loads only when a player scrolls near the viewport, and only on pages that have one. Without WebGL, the plots and the description still carry the trace.
- Theme. Colours come from the same tokens as the SVG figures, for light and dark. The layout stacks to one column at phone width.
The shared package
Section titled “The shared package”The player, the schema, and the validator are the package
@branes-ai/trace-player, in reflex’s packages/trace-player/, so that a
repo that builds on reflex plays its own traces in the same player (the
animation standard’s boundary
with mission SITL). It ships with each reflex release as an npm tarball,
branes-ai-trace-player-<version>.tgz, at the version of the release, and
a consuming docs site pins that tarball’s URL. The package’s README is the
consumer’s guide: the install, the props, and the CI helpers.
reflex’s own site uses it in two ways:
- The pages import
@site/components/trace/TracePlayer.astro, a thin wrapper that takesliveas the name of one of reflex’s live scenarios and passes everything else to the package’s component. - The site installs the package as a packed copy (
docs-site/.npmrcsetsinstall-links), which holds the same files as the release tarball. A file the tarball leaves out therefore fails reflex’s own build first.npm run build,dev, and the trace scripts first runscripts/player-sync.mjs, which refreshes the copy when the package’s source has changed.
A C++ consumer writes its traces with the same writer, branes::reflex_trace,
which reflex’s CMake defines for consumers too.
Trace format additions
Section titled “Trace format additions”reflex-trace/1 gained two optional fields, as the versioning rule allows.
-
vehicle.arm_length(in m), in P0, lets the 3-D view draw the airframe to scale. Traces without it are drawn with the RP-1 default of 0.11 m. -
scene, for mission replays (the animation standard’s boundary with mission SITL), holds the world around the vehicle asentities. The trace schema defines each field; in the view:- Shapes. Each entity has a shape, a box, a cylinder (along its z), a sphere, or a compound of these at offsets, drawn in its role’s colour: obstacles in grey, a gate orange, the goal translucent green, an agent amber, and a boundary or a marker as edges only. The scene’s solids are lit, so their form reads in 3-D.
- Visuals. An entity’s visual, if it has one, is drawn in place of
the shape: a
treeon a cylinder is its trunk, the cylinder itself, and a canopy over its upper half,canopy_radiuswide (by default three times the trunk’s radius, at least 0.6 m). Amodel(a glTF file) is for a later player. Until then it is drawn as its shape. When any entity has a visual, the view adds a Collision shapes button, which draws the shapes of those entities over their visuals, as translucent volumes that show through (an entity without a visual is drawn as its shape already). - Poses. An entity’s position and attitude are fixed, or name trace channels of width 3 and 4. Those are read at the cursor’s sample, so an entity moves with the trace: another agent, or a setpoint.
- Framing. The ground grid and the Overview camera take in the scene as well as the path.
The example above uses each role but the agent. A C++ scenario adds entities with
Trace::entity(), whose rules (W8 and W9 inwriter.hpp) are the validator’s.