Skip to content

ros2-poc: the FicTrac record in the bag — one /arena/frame message per frame with 10 columns (LAB-303) - #264

Open
mbreiser wants to merge 1 commit into
claude/ros-studio-plan-b-c44942from
claude/fictrac-columns-record
Open

mbreiser wants to merge 1 commit into
claude/ros-studio-plan-b-c44942from
claude/fictrac-columns-record

Conversation

@mbreiser

@mbreiser mbreiser commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements the 2026-10-18 decision in docs/development/fictrac-columns-in-the-bag.md (PR #263, merged): the bag keeps 10 of FicTrac's 25 columns — the behavior row's 5 plus the error score (col 5), the lab-frame delta rotation (cols 6–8) and the sequence counter (col 23) — on the per-frame /arena/frame record, float64, one MCAP message per frame. The adapter's raw topics (/fictrac/frame / /fictrac/raw: 25 columns + the verbatim line, 68–77 % of a bag) are recorded only with the new --record-fictrac-raw. Stacked on #254's branch (claude/ros-studio-plan-b-c44942); not part of candidate 2026-10-20.

  • ArenaFrame.msg: err, dr_lab_x/y/z, ft_seq, ft_ns (col 22 as received), flags (bit 0 = untracked FT_BADFR frame). One type for the live topic and the bag. The recorded copy carries NaN in turning_dps / forward_mms: they are live-only, exactly recomputable from x/y/hd/ft, and measured at 16 B per frame of incompressible doubles (a fifth of the record) — the doc's "≈ 1 %" came from a bench bag whose derived fields were the 2026-10-14 mixed-build garbage; a docs follow-up corrects it.
  • bridge_node.py: the consumer parses each line once (hub.fields) so the record carries the columns of the same line as x/y/hd (the harness proves it: wrap(Δhd) == dr_lab_z frame to frame); an FT_BADFR line becomes a flagged record with no run-log row, connector_status counts bad_frames; raw FicTrac topics gated by --record-fictrac-raw. The WebSocket hub message to the Studio is unchanged.
  • _rows.py: parse_fictrac_line (one parser for the adapter, the node and the converter), fictrac_extras, arena_frame_to_row (the one record→row mapping; None for a flagged frame).
  • mcap_convert.py: rows through _rows; flagged frames skipped; --level full refused with a clear error for a bag without the raw topics (bags from before 2026-10-18 still convert, full included).
  • mcap_index.reindex: chunk-only index + zstd 19 (the final layout a cleanly closed run gets); the recorder's default profile passes Zstd / Slowest / noMessageIndex through the plugin's storage options (verified: zstd chunks, 0 message-index entries, both profiles).
  • sizing/columns.py + pixi run size-columns: the bag-vs-jsonl.gz study, reproducible on any bag (--all adds the declined alternatives).
  • README + telemetry-logging-reference.md pointer paragraph. fictrac-bridge/bridge.py, BEHAVIOR_V1_COLS and the JSONL row are untouched.

Measured (pixi run size-columns -- <bag> <jsonl> --all, chunk-only index, zstd 19)

bag 1 (194449-517, 9 056 frames) bag 2 (204300-169, 8 199 frames)
jsonl.gz 228 764 B 162 356 B
as recorded before this PR 10.2× 30.4×
adopted: one record per frame, 10 cols, float64, per message 550 738 B = 2.41× (60.8 B/frame) 435 721 B = 2.68× (53.1 B/frame)
same, batched 1 s (declined) 2.10× 2.34×
same, int32 fixed-point (declined) 1.96× per record / 1.70× batched 2.09× / 1.79×

On the harness run (typed, 1 084 frames): frame records 63.7 B/frame compressed (3.37× the gzip of the frame rows; 4.17× before the derived channels went NaN). For a 30 min run at 50 Hz: ≈ 5–5.5 MB against ≈ 2.3 MB of jsonl.gz. The bench bags cannot show the derived-channel saving (bag 1's derived values were constant garbage, bag 2 had none), which is why their adopted numbers match the doc's layout rows; the reproducible script's packing differs slightly from the doc's scratch matrix (2.41×/2.68× vs 2.24×/2.48×) — the docs follow-up carries the script's numbers.

Tier: next candidate (after 2026-10-20)

Do not merge into claude/ros-studio-plan-b-c44942: that branch is a pinned constituent of the live Next candidate (release PR #262); moving its head invalidates the candidate. This PR stays open and stacked until #262 has landed, then is retargeted to main (gh pr edit 264 --base main) and rebased onto main to become a constituent of the next candidate (2026-10-22). No footer/version bump.

Release notes

none (bridge-node / ROS 2 record format; no Studio change)

Test plan

  • pixi run test (default env, String topics): 60/60
  • pixi run -e typed test: 63/63 — merged-record alignment, bad-frame injection, full refused, pre-2026-10-18 bag still converts (with full), FrameDeriver math, 72 B/frame guard (measured 63.7)
  • both recorder profiles write zstd chunks with no message index (3 000-frame synthetic bag, both profiles)
  • a rig run (ww18 or Bergamo) with a real FicTrac: ft_ns is the camera clock, err non-zero, dr_lab_z == −Δhd (FicTrac's sign; the simulator's is +)

Related: LAB-303. Linear not touched (proposed comment text is in the decision doc §5).

🤖 Generated with Claude Code

…r frame with 10 columns (LAB-303)

Decided 2026-10-18 (docs/development/fictrac-columns-in-the-bag.md): the bag keeps 10 of
FicTrac's 25 columns — the behavior row's 5 plus the error score (col 5), the lab-frame delta
rotation (cols 6-8) and the sequence counter (col 23) — on the per-frame /arena/frame record,
float64, one MCAP message per frame; the adapter's raw topics (/fictrac/frame, /fictrac/raw:
25 columns + the verbatim line, 68-77 % of a bag) are recorded only with --record-fictrac-raw.

- ArenaFrame.msg: err, dr_lab_x/y/z, ft_seq, ft_ns (col 22 as received), flags (bit 0 =
  untracked FT_BADFR frame). The recorded copy carries NaN in turning_dps/forward_mms: they
  are live-only, exactly recomputable, and measured at 16 B/frame of incompressible doubles.
- bridge_node: the consumer parses each line once (hub.fields) so the record carries the
  columns of the SAME line as x/y/hd; an FT_BADFR line becomes a flagged record (no run-log
  row) and connector_status counts bad_frames; raw FicTrac topics gated by --record-fictrac-raw.
- _rows.py: parse_fictrac_line (one parser for adapter, node, converter), fictrac_extras,
  arena_frame_to_row (the ONE record→row mapping; None for a flagged frame).
- mcap_convert: rows via _rows; flagged frames skipped; --level full refused without the raw
  topics (old bags that have them still convert, full included).
- mcap_index.reindex: chunk-only index + zstd 19 (the final layout); recorder `default`
  profile: Zstd / Slowest / noMessageIndex through the plugin's storage options.
- sizing/columns.py + `pixi run size-columns`: the bag-vs-jsonl.gz study, reproducible.
- tests: merged-record alignment (wrap(dhd) == dr_lab_z), bad-frame injection, full-level
  rule, pre-2026-10-18 bag still converts, FrameDeriver math, 72 B/frame size guard.
  pixi run test 60/60 (default env), pixi run -e typed test 63/63.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mbreiser added a commit that referenced this pull request Oct 19, 2026
… derived-channel correction (LAB-303) (#265)

Follow-up to #263: the reproducible ros2-poc/sizing/columns.py (PR #264) measures the adopted
record at 2.41x / 2.68x the jsonl.gz (the first version quoted 2.24x / 2.48x from a scratch
script); the live turning_dps / forward_mms cost 16 B/frame of incompressible doubles, not
"about 1 %" (that came from a bench bag whose derived fields were the 2026-10-14 mixed-build
garbage), so the implementation records them as NaN. Links the implementation PR.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant