Plan: B0 as a graph (anatomy ADR 0004, step 3)
Issue: #98. Implemented in #99. Anatomy’s side: the graph builder, the CPU stack and log_compare, released in anatomy v0.8.0 and v0.9.0 (anatomy’s plan). scout-drone did the same for s0-gt-pid in step 2 (scout-drone#25).
Step 3 of anatomy ADR 0004: racing’s stack described as a graph of operators, which anatomy checks as it is built and runs on its CPU stack. Each baseline gets a second build, the graph’s, beside its slot build. The graph build exists so that step 4 can lower the graph to other hardware. For now it must fly exactly as the slot build does.
What stays the same:
- The slot builds: their targets, their logs and their outputs are byte-identical to main’s.
- Formats: the log format, the YAML files’ existing keys, and the command lines.
Acceptance (from #98, stated before any code)
Section titled “Acceptance (from #98, stated before any code)”- The graph: racing’s graph has seven composites, one per slot, and describes the slot
Stackexactly: the same ports, one region per input, and the firings inStack’s order.null,scripted-hopandb0-gt-piddeclareclass: scoreboardand agraph:. Each getssitl_<name>_graph,replay_<name>_graphandsitl_batch_<name>_graph. - Equal logs: for each of the three baselines, on every scenario in
scenarios/, the graph build’s SITL log equals the slot build’s exceptcomposition_hash, according to anatomy’slog_compare --except composition_hash. Compared whole, the two logs must differ in that field, which shows the graph’s hash is the one recorded. - Equal batches: on
b0-nominal, with 10 seeds, the two builds’ batches write byte-identical JSON lines and identical reports. - Determinism:
gate.b0.determinism.graph(a rerun and two replays, byte-identical) passes on the graph build ofb0-gt-pid. - The slot builds are unchanged:
tools/compare_outputs.shwithgcc-releasegives all 39 files identical to main’s, and logs written by main’ssitl_null,sitl_scripted-hopandsitl_b0-gt-pidreplay with--verify. - Every test passes on the four Linux presets and on MSVC Debug and Release, with clang-format and commitlint clean.
Design
Section titled “Design”The graph is C++, in <branes/racing/flow/...>, namespace branes::racing::graph. It follows scout-drone’s, which follows anatomy’s plan.
| Header | What it holds |
|---|---|
domains.hpp | The domain of each racing message: Gate, CourseMap, GateObservation, GateObservations |
bindings.hpp | The bindings of racing’s three task composites: the estimator, the gate detector and the planner |
graph.hpp | The graph’s ports, the scoreboard graph, and graph::Stack, the CPU stack that runs it |
The ports
Section titled “The ports”| Port | Kind | Message |
|---|---|---|
imu | input | ImuSample |
camera | input, timed by the frame’s mid-exposure | CameraFrame |
truth | input, SITL only | TruthState |
tick | the control tick | Timestamp |
course | constant | CourseMap |
detections | constant, empty: nothing produces intruder detections yet | IntruderDetections |
estimate, reference, command | outputs, at the tick | StateEstimate, Reference, ThrustRates |
The composites
Section titled “The composites”Seven composites, one per slot. A composite’s binding is the CPU calling convention of its operators: one handler per method of the slot’s concept. So every slot implementation is an operator of its composite, unchanged, registered under its slot registry name (cmake/flow_registry.cmake).
| Composite | Binding | Handlers |
|---|---|---|
estimator | graph::EstimatorBinding | imu, frame (camera, gates), truth, estimate (at a time) |
gate_detector | graph::GateDetectorBinding | detect (camera, course, prior) |
planner | graph::PlannerBinding | plan (course, estimate) |
controller | anatomy’s ControllerBinding | step |
safety_filter | anatomy’s SafetyFilterBinding | filter |
daa_tracker | anatomy’s DaaTrackerBinding | update, conflicts |
fc_bridge | anatomy’s FcBridgeBinding | send |
The slot Stack has two branches, and a graph has none, so each moves into a handler:
- Truth. The estimator’s
truthhandler hands the true state to an estimator that reads it (TruthConsumer), and does nothing otherwise. So every graph build reads the SITL-onlytruthport, and is SITL-only (G9). - Corrupted frames.
Stack::on_frame()doesn’t call the detector on a corrupted frame, and hands the estimator no gates. The detector’sdetecthandler does the same.
Stack::on_truth() hands the truth to every slot that reads it. The graph hands it to the estimator only. That is the one slot that reads it today (GroundTruthEstimator), so the two builds agree. A non-estimator slot that reads the truth would need a truth handler in its binding.
The regions
Section titled “The regions”Each input fires one region. A region’s firings run in the order the graph writes them, which is Stack’s order:
- truth: the estimator takes the true state.
- imu: the estimator takes the sample.
- frame: the estimate at the frame’s mid-exposure is read. The gate detector finds the gates with that prior, and none in a corrupted frame. The estimator takes the frame and the gates, and the DAA tracker is updated with the prior and the detections.
- tick: at the tick’s time, the estimate is read; the planner plans from the course and the estimate; the DAA tracker gives its conflicts; the controller steps; the safety filter filters the command, with the course’s geofence; and the FC bridge sends it. The estimate, the reference and the filtered command are the tick’s outputs.
The builds
Section titled “The builds”- CMake.
racing_add_baseline(<name> GRAPH)reads the file’sclass:andgraph:with anatomy’sflow_add_baseline(), and generatesgenerated/<name>_graph/racing_baseline.hppfromcmake/racing_graph.hpp.in. It addsreplay_<name>_graph,sitl_<name>_graphandsitl_batch_<name>_graph, from the sameapps/sources as the slot build. There is noflight_stack_<name>_graph, since every graph build is SITL-only. The precision sweep stays on the slot build. - The generated headers. Both builds’ headers define
baseline::name,baseline::Composition<T>,baseline::check_baseline(dir)andbaseline::hash(stack). So the apps don’t know which build they are. The slot build checks the file’s composition hash. The graph build checks only the name, and its stack checksclass:andgraph:against the graph it builds (G8). - The hash. The slot build’s log records the composition hash, as before. The graph build’s records the graph’s hash.
replay --verifychecks the input log’s hash against the stack’s once the stack is built, since a graph’s hash is known only then.
The gates
Section titled “The gates”gate.<name>.graph, fornull,scripted-hopandb0-gt-pid(tests/gate/graph_gate.cmake): acceptance 2 on every scenario, and acceptance 3 onb0-nominal.gate.b0.determinism.graph: acceptance 4, the determinism gate on the graph build ofb0-gt-pid.
Progress
Section titled “Progress”| Part | What | PR | State |
|---|---|---|---|
| 3a | The graph, its builds and its gates, for null, scripted-hop and b0-gt-pid | #99 | complete |
After this plan, anatomy’s flow-testing plan (T5, #104) added b0-shaped-pid and b0-shaped-research, the first body of several operators and the first research graph in racing, and --trace on the graph builds.