Skip to content

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.

What does a roll torque pulse do to an uncontrolled quadrotor? 1× real time
World: z up · Body: x forward, y left, z up
Rotors (PX4 Quad X): 0 FR, 1 RL spin CCW · 2 FL, 3 RR spin CW
Thrust arrows: 1 N = 4 cm · Grid: 0.5 m · Body axes: x red, y green, z blue
Rotor speeds [rad/s] 0 FR (= 3 RR) 1 RL (= 2 FL) dashed: command
Roll angle [rad] roll
Roll rate [rad/s] p (body x)
Position [m] y z
t = 0.000 s / 1.500 s

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.

  1. left rotors +40 rad/s, right rotors -40 rad/s: roll torque
  2. reversed: braking torque
  3. 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 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).

What does a trace's scene show around the vehicle? 1× real time
World: z up · Body: x forward, y left, z up
Rotors (PX4 Quad X): 0 FR, 1 RL spin CCW · 2 FL, 3 RR spin CW
Scene: 11 entities · Collision shapes: what a simulation tests
Thrust arrows: 1 N = 4 cm · Grid: 0.5 m · Body axes: x red, y green, z blue
Position [m] x y x setpoint y setpoint
Distance from the square [m] cross-track
t = 0.000 s / 18.000 s

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.

  1. leg 1: the setpoint sets off along +x
  2. corner: the setpoint turns 90°
  3. corner: the setpoint turns 90°
  4. corner: the setpoint turns 90°
  5. 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.
  1. Scenario. Add a source file under tools/trace/scenarios/<block>/. It drives the block’s own C++ and records samples with branes::reflex::trace::Trace, the writer in tools/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 as trace_<block>_<scenario>.
  2. Generate. scripts/gen-traces.sh gcc-release docs-site/src/traces builds and runs every scenario. It stamps the commit as git describe --dirty, so a trace generated from uncommitted changes says so.
  3. Commit the trace with the change that produced it.
  4. Play. Import the JSON file in an .mdx page 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 }]} />
PropMeaning
traceThe imported trace JSON
titleThe one question the animation answers (ANI-7)
descriptionWhat happens, in text (ANI-10); for a V&V animation, also the pass criterion (ANI-4)
plotsThe plot panels, top to bottom (below)
thrustScaleThrust-arrow length in m per N, shown on screen (default 0.04)
viewfalse hides the 3-D view, for a trace where the vehicle barely moves
ghostA second run drawn translucent beside the first: { position, attitude, thrusts?, label } name its channels
liveMakes the player a live widget: the name of a scenario in src/lib/trace/live.js (live widgets)
promptFor a live widget: what to predict before moving a slider (ANI-8)
repo, provenanceNoteFor 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:

FieldMeaning
channel, titleThe channel to plot, and the panel title; the unit is appended
components, labels, colorsWhich components of a vector channel, their legend labels, and their colour slots (1–4)
setpoint, setpointLabelOverlay <channel>_setpoint, dashed in the same colours, named in the legend by setpointLabel (default “command”)
seriesMore lines from other channels in the same unit: { channel, component?, label, color?, dashed? }, for a limit, a reference, or a second run
yRangeA 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.

FieldMeaning
channel, title, labels, labelThe 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.

FieldMeaning
channel, titleThe spectrum channel, the amplitude in bin b at b·binHz, and the panel title
binHzA scalar channel in Hz holding the bin spacing, the same at every sample
rangeThe 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.

The Docs traces job in ci.yml runs on every pull request:

CheckToolFails when
Schema and semanticsnpm 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 --regenA 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 buildnpm run buildA page passes an invalid trace to TracePlayer
Wasm agreement (Tier 2)scripts/build-wasm.sh with the pinned emsdk, then npm run wasm-agreeA 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-wasmA 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.

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:

  1. Parameterize it. Move the scenario’s body into tools/trace/scenarios/<block>/<scenario>.hpp, as a function of a Params struct that rejects values outside the slider ranges (ANI-9). The .cpp calls it with the defaults, so the committed trace is the default run.
  2. Export it. Add a case to reflex_run() in tools/wasm/reflex_wasm.cpp. The C ABI takes up to three parameters.
  3. Register it. Add an entry to LIVE in src/lib/trace/live.js: the committed trace, and for each parameter a label, display unit, scale to SI, range, step, and default. CI’s npm run wasm-agree then runs it at the defaults and compares it with the committed trace.
  4. Use it. Pass live="<scenario>" and a prompt to the TracePlayer.

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.

  • 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 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 takes live as 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/.npmrc sets install-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 run scripts/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.

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 as entities. 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 tree on a cylinder is its trunk, the cylinder itself, and a canopy over its upper half, canopy_radius wide (by default three times the trunk’s radius, at least 0.6 m). A model (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 in writer.hpp) are the validator’s.