Skip to content

Block Pages and Animation

Each reflex block captures dynamics that are hard to see in equations and tables: motor lag feeding into attitude, saturation priority in the mixer, windup in a PID, a notch tracking a rotor harmonic. Animations show those dynamics directly. They are also a way to mislead: a smooth animation of a wrong trace builds false confidence faster than any table. This page defines how reflex uses animation so that it explains without misleading. It covers:

  • the four pages each block publishes, and what animation does on each;
  • the rules every animation follows (generated from the block’s own code, with provenance, paired with the check it illustrates);
  • the boundary with mission SITL in consuming repos such as racing-drone.

Why reflex animates its blocks at all is the assessment. The work that builds this, its phases, status and decisions, is the animation plan.

PageReader and questionContentRole of animation
ExplainerA newcomer: how does this work, and why?The physics or algorithm, built up one idea at a timeCentral. Interactive widgets running the real block in the browser (Wasm), with parameters to vary
Developer guideA contributor: how do I change, test, or debug it?API, conventions, contracts, harnesses, debugging workflowSupporting. Replays of the debug tools, such as an invariant violation found by the monitor and replayed
V&V and datasheetA reviewer: what is shown, and how well?The V&V plan, evidence ledger, generated datasheetIllustrative only. Each animation sits next to the metric it illustrates and states the pass criterion
Deployment guideAn integrator: how do I use it on my vehicle, and where does it break?Parameter identification, rates and timing budget, envelope, failure behaviour, consumer contractShows the envelope edges and failure modes, such as saturation, rotor loss, and battery sag

The current usage page becomes the developer guide. The V&V plan and the datasheet together become the V&V page. The explainer and deployment pages are new, and can be short for small blocks. The quadrotor control guide is already an explainer for the control layer as a whole.

Pages are grouped per block in the sidebar:

Model
└── Quadrotor
├── Explainer
├── Developer guide (today's model/quadrotor)
├── V&V and datasheet (today's model/quadrotor-vv + datasheet)
└── Deployment guide
  • ANI-1. Generated from the block’s code. An animation renders a trace produced by the block’s own C++: a characterize/, sim/, or test executable, or the same headers compiled to WebAssembly. Dynamics are never re-implemented in JavaScript, and motion is never keyframed by hand.
  • ANI-2. Provenance on screen. Each trace records the commit, block, scenario, seed, compiler, and the simulation and trace step sizes. The player shows them in a footer, with a link to the scenario source.
  • ANI-3. Stale traces fail CI. CI regenerates each committed trace and compares it with the committed copy under the regen equivalence profile. The profile defines how t, channels, events, and checks are compared. The fields that say what the trace is must match exactly: schema, frames, units, vehicle, and, in provenance, block, scenario, seed, dt_sim, and dt_trace. provenance.commit and provenance.compiler are excluded, because they change with every rebuild. A model change that alters a trace fails the docs build until the trace is regenerated, in the same PR.
  • ANI-4. Paired with a check. A V&V animation names the requirement it illustrates (BB-V4, BB-I3, …) and the quantitative check behind it, and its caption states what to look for and the pass criterion. An animation is never the evidence itself.
  • ANI-5. Honest time and scale. Time runs at 1× unless the player shows the rate (for example “0.1×”). Exaggerated geometry, such as attitude ×5 or a scaled thrust vector, is labelled on screen. Nothing is exaggerated silently.
  • ANI-6. Conventions visible. World and body axes (z up), rotor numbers, and spin directions are drawn in the same PX4 Quad X convention as the model, and a legend states the frames.
  • ANI-7. One idea per animation. Each animation answers one question, stated as its title (“Why does the roll step overshoot?”). Explainers build up in sequence: rotor, then thrust, torque, attitude, translation.
  • ANI-8. Predict, then show. Explainer widgets ask the reader what will happen before playing, where that helps, for example “Increase τ: does the overshoot grow or shrink?”
  • ANI-9. Only parameters that teach. Interactive widgets expose the two or three parameters that drive the idea, with ranges inside the block’s validity envelope (BB-S2), and say so when the reader reaches an edge.
  • ANI-10. Readers control playback. Animations start paused and respect prefers-reduced-motion. They have play, pause, scrub, and speed controls, keyboard operation, and a text description of what happens.

  • ANI-11. Theme and layout. They follow the docs’ light and dark theme, as the SVG figures already do, and work at phone width.

  • ANI-12. Size budget. Sizes are gzip-compressed bytes (gzip -9) of the files as committed or as built:

    • each trace file: ≤ 250 KB, after downsampling to the display rate;
    • the WebAssembly module plus its JavaScript loader: ≤ 400 KB together;
    • three.js is outside the budget, but appears only in the built bundles of pages that use the 3-D view.

    A CI script lists each counted file with its size and fails over budget.

TierMechanismUsed forEvidence?
Trace playbackA committed trace (JSON) played by a TracePlayer component: a 3-D vehicle view synchronized with SVG plots and a shared time cursorV&V, deployment, and developer pagesIt shows evidence, as a view of a CI-checked trace
Live in the browserThe block’s headers compiled to WebAssembly with Emscripten; the page reruns the scenario’s C++ as the reader moves sliders, and plays the new traceExplainersNo: interactive, labelled “exploration, not evidence”. CI checks that Wasm and native traces agree at the default parameters of every live scenario under the wasm equivalence profile
Development viewerMCAP traces from the scenario runner (BB-D1), opened in Foxglove or RerunLocal debugging of any scenarioNo: a tool, not a page

Why not video: video can’t follow the site theme, can’t be scrubbed against plots, can’t carry machine-checkable provenance, and can’t be diffed. It is also large in git. The trace is the artifact, and the player is code.

One schema, reflex-trace/1, serves reflex pages and mission replays:

{
"schema": "reflex-trace/1",
"provenance": {
"commit": "732b7ef", "block": "model/quadrotor",
"scenario": "sim/scenarios/quadrotor/roll_step.json", "seed": 7,
"compiler": "gcc 13.2 -O2", "dt_sim": 0.0005, "dt_trace": 0.01
},
"frames": { "world": "z-up", "body": "x-forward y-left z-up", "rotors": "px4-quad-x" },
"t": [0.0, 0.01],
"channels": {
"position": [[0, 0, 1], [0, 0, 1.0001]],
"attitude": [[1, 0, 0, 0], [0.9999978, 0.0021, 0, 0]],
"rotor_speeds": [[1660, 1660, 1660, 1660], [1632, 1688, 1688, 1632]],
"roll_setpoint": [0.0, 0.349],
"roll_integrator": [0.0, 0.0001]
},
"units": { "roll_integrator": "N·m" },
"events": [{ "t": 0.0, "label": "roll step 20°" }],
"checks": [{ "id": "BB-I2", "metric": "overshoot", "unit": "1",
"value": 0.071, "limit": 0.10, "comparison": "<=", "pass": true }]
}

The schema is normative. A JSON Schema file, packages/trace-player/schemas/reflex-trace-1.schema.json, checks structure and types. A validator, packages/trace-player/src/lib/validate.js, checks the semantic rules a JSON Schema cannot express: lengths, monotonicity, and unit norm. Both ship in the shared player package, @branes-ai/trace-player. CI validates every committed trace, and TracePlayer refuses a trace that fails, which fails the docs build. The C++ writer (tools/trace/include/branes/reflex/trace/writer.hpp) enforces the same rules as a scenario records samples. The Trace Player page documents the tooling.

  • Units. SI throughout, and every angle in radians, including setpoints and checks. Degrees appear only in human-readable label text.

  • t is time in seconds since the start of the scenario, starting at 0, strictly increasing, and uniformly spaced at dt_trace when dt_trace is given.

  • Cardinality. Each channel has exactly one entry per element of t. An entry is a number, or an array of the channel’s fixed length.

  • Reserved channels. These have fixed shapes and units, and the 3-D view reads them:

    ChannelShapeUnit and frame
    position[x, y, z]m, world
    velocity[x, y, z]m/s, world
    attitude[w, x, y, z]Hamilton unit quaternion, body to world, scalar first; norm within 1e-6
    body_rates[x, y, z]rad/s, body
    rotor_speeds[4]rad/s, PX4 Quad X order
    rotor_thrusts[4]N, PX4 Quad X order
    wrench[T, τx, τy, τz]N and N·m, body
    <quantity>_setpointas <quantity>as <quantity>; roll_setpoint, pitch_setpoint, yaw_setpoint in rad
  • Other channels must declare a unit in units. "1" means dimensionless. The player plots them but doesn’t interpret them.

  • events have t within the range of t, and a display label.

  • checks have id (a requirement ID), metric, unit, value, limit, comparison (<= or >=), and pass. pass is computed by the harness and never by the player. Check ids are unique within a trace, because the equivalence profiles match checks by id. A check may add "expect": "fail" (the default is "pass") when its scenario shows the edge of the block’s envelope, where the criterion is meant to fail. A scenario fails when a check fails, or passes against an "expect": "fail". CI fails on a failing scenario, and the scenario gallery shows it.

  • vehicle (optional) holds geometry for the 3-D view: arm_length in m. Without it, the view draws the RP-1 default airframe.

  • scene (optional) holds the world around the vehicle, for a mission replay: entities, at least one. Each has:

    • id, matching [a-z0-9][a-z0-9_-]*, unique in the trace;
    • role: obstacle, goal, gate, boundary, agent, or marker;
    • shape, in the entity’s frame and centred on its origin: { "type": "box", "size": [x, y, z] }, { "type": "cylinder", "radius", "height" } along z, { "type": "sphere", "radius" }, or { "type": "compound", "parts": [{ "shape", "offset": { "position", "attitude"? } }] }, whose parts are primitives. Lengths in m, all > 0. The shape is what a simulation tests, such as an obstacle’s collision geometry.
    • pose: a position in m and an optional attitude, body to world, each either fixed (an array) or the name of a channel of width 3 or 4, read at each sample, for an entity that moves. Attitudes, fixed or recorded, are unit quaternions.
    • visual (optional), drawn in place of the shape and never tested: { "kind": "tree", "canopy_radius"? } on a cylinder, or { "kind": "model", "uri", "scale"? }, a glTF model, which a player that can’t load models draws as the shape.
    • label (optional), display text.

    A scene is part of what the trace is: the regen and wasm profiles require it to be identical.

  • provenance.equivalence (optional) declares an override for a chaotic scenario: a justification, and optionally a comparison horizon in s and per-channel tolerances (rtol, atol).

  • Versioning. Adding optional fields or reserved channels keeps reflex-trace/1. Any change to an existing field’s meaning, shape, or unit makes it reflex-trace/2.

Two traces are equivalent under a profile when all of these hold:

  1. Their t arrays are identical.
  2. The compared channels have the same names, and each channel has the same shape in both traces.
  3. Every component of every sample of every compared channel satisfies ∣a−b∣≤atol+rtol ∣b∣|a - b| \le a_{\text{tol}} + r_{\text{tol}}\,|b|. Here bb is the reference. Tolerances apply per component, not to a vector norm, so position is checked on x, y, and z separately. Attitude is the one exception, compared as a single angle (below).
  4. events match exactly.
  5. Each check matches the reference check with the same id. Its metric, unit, limit, comparison, pass, and expect match exactly, and its value satisfies the component rule with rtolr_{\text{tol}} = 1e-9 and atola_{\text{tol}} = 1e-9 in the check’s unit. Both traces have the same set of check ids. The trace_diff tool (BB-D3), npm run trace-diff in docs-site/, implements these profiles and reports the first sample and channel that fail.
ProfileUsed byComparedrtolr_{\text{tol}}atola_{\text{tol}}
regenANI-3: regenerated against committed, possibly with a different compilerAll channels, events, and checks1e-9position 1e-9 m; velocity 1e-9 m/s; attitude 1e-9 rad; body rates 1e-9 rad/s; rotor speeds and their setpoints 1e-6 rad/s; other channels 1e-9 in their unit
wasmTier 2: Wasm against native, every live scenario at its default parametersAs regen: all channels, events, and checks1e-9as regen
  • Why wasm compares everything. It first compared only the reserved channels, which are the ones the 3-D view reads. The control blocks’ live scenarios (P3) have none: a PID trace is all block-specific channels. So wasm now compares what regen does. The module and the native build run the same scenario function, so they agree to roundoff on every channel, and on the events and checks computed from them.
  • Attitude is compared as the rotation angle 2 asin(∥vec(qa−1⊗qb)∥)2\,\mathrm{asin}(\lVert \mathrm{vec}(q_a^{-1} \otimes q_b) \rVert), so q and −q are equal.
  • No resampling. Both traces are written at the same dt_trace from the same scenario, so their t arrays must be identical. A trace that needs resampling fails the comparison.
  • Why 1e-9. Compiler differences (FMA contraction, operation order) and libm differences produce errors of order ε ≈ 2e-16 per operation, which stay below 1e-9 over the 10⁴–10⁵ steps of a docs scenario when the dynamics are not chaotic.
  • Chaotic scenarios, such as the intermediate-axis tumble, amplify roundoff exponentially. Such a trace declares an override in provenance.equivalence: a shorter comparison horizon or looser tolerances per channel, with a one-line justification. Overrides are reviewed like golden-file changes (BB-R4).
ConcernReflexMission repo (racing-drone)
ScopeOne block, or the reference closed loop (BB-I1)The full stack in a mission
SceneMinimal: axes, ground grid, vehicle glyph, force vectorsCourse, gates, environment, other agents, in the trace’s scene
Question answeredHow the block behaves, and whyWhether the mission succeeds, and how fast
ScenariosBlock characterization, envelope edges, faultsMission profiles, races, operator workflows
RenderingDocs-site player (Tier 1) and Wasm widgets (Tier 2)Its SITL renderer (Gazebo or another engine), plus the same player for docs
SharedTrace schema reflex-trace/1 (its scene included) and the TracePlayer component, published from reflexConsumes both, and exports mission replays in the schema, with its world as the scene

The rule: if an animation needs mission assets or more than the reference closed loop, it belongs in the mission repo. If it explains a block, it belongs in reflex, even when the scenario was found in mission SITL. Such a scenario is reduced to a block-level reproduction and added to reflex’s scenario suite, which is how mission findings reach the block’s V&V.

Block work is driven by the agent working through the racing-drone issues. To keep that agent’s PRs small, the standard splits the work:

  • Block PRs keep adding what they already add: tests, a usage page, and closed-loop scenarios. Each scenario writes a reflex-trace/1 file when REFLEX_TRACE_DIR is set, which is the same opt-in pattern the gyro example uses. That is the only animation-related requirement on a block PR.
  • Documentation PRs, separate and in this workstream, add the explainer, deployment page, and animations from those traces.

Animation is not part of a block’s grade. Grades measure evidence, and an animation is a view of evidence. One addition to the building block standard is proposed: BB-R5, deployment guide, required for grade A, because integrators need the envelope and failure behaviour in a form they can act on.