Skip to content

Warn that enu_to_body/body_to_enu take origins in different frames - #97

Open
qmohsu wants to merge 2 commits into
mainfrom
claude/ch2-enu-body-origin-frames
Open

Warn that enu_to_body/body_to_enu take origins in different frames#97
qmohsu wants to merge 2 commits into
mainfrom
claude/ch2-enu-body-origin-frames

Conversation

@qmohsu

@qmohsu qmohsu commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

core/coords/transforms.py's enu_to_body (line 454) and body_to_enu
(line 485) are a true inverse pair only when the two origin arguments are
converted into each other, not when the same array is passed to both:

  • enu_to_body(..., body_origin_enu=...) wants the body origin expressed
    in ENU
    .
  • body_to_enu(..., enu_origin_body=...) wants the ENU origin expressed
    in the body frame
    .

They're related by enu_origin_body = -C @ body_origin_enu (C = the
rotation matrix for the same roll/pitch/yaw), not equality. Passing one
array to both — the obvious thing a reader would try — silently returns a
wrong point.

Measured before the change, with x=[10,20,5], origin=[1,2,3],
roll,pitch,yaw=(0.1,0.2,0.3), same array to both origin arguments:

  • Full 3D round-trip error: 7.4727 m
  • Up-axis component alone: 6.2210 m (this is what the task brief's
    "6.22 m" referred to — a per-axis figure, not the vector norm; both
    numbers are reported here since they differ).
  • With the correct conversion instead: round-trip error is 3.55e-15 m
    (float noise).

Nothing in the repo previously exercised this: TestEnuBody::test_round_trip
only ever called with the default origin=None, and no caller in the repo
passes a non-default origin to either function.

Scope decision

Signatures are unchanged — they mirror the book's Eq. (2.6)/(2.7), and this
repo's convention is code follows the book unless the book is wrong (see
docs/book_errata.md). This is a documentation + test fix only:

  1. core/coords/transforms.py:454 and :485 — added a Notes: section
    with a CONVENTION WARNING to both docstrings, naming the frame of each
    origin argument and giving the conversion. Style matches
    core/sensors/strapdown.py's quat_to_rotmat CONVENTION WARNING (the
    existing house style for this kind of cross-function frame mismatch note).
  2. tests/core/coords/test_transforms.py — two new tests in TestEnuBody:
    • test_round_trip_with_offset_origin: round-trips through a non-None
      origin using the correct -C @ conversion.
    • test_same_array_as_both_origins_does_not_round_trip: an inequality
      assertion (with an explanatory comment) pinning that passing the same
      array to both is a trap — if a future change unifies the two arguments,
      this goes green and should read as the convention having changed.
  3. Checked docs/ch2_equation_mapping.md and docs/CH2_QUICK_REFERENCE.md:
    both describe the rotation/attitude convention but are silent on the frame
    of the origin arguments specifically — not wrong, just silent — so neither
    needed a change.

Blast radius

git status --short after the change:

 M core/coords/transforms.py
 M tests/core/coords/test_transforms.py

Pure additions (2 files changed, 80 insertions(+), 0 deletions(-)). No other
.py file, no data file, and no figure byte touched.

Test plan

  • Reproduced the pre-fix round-trip error via a scratch probe against
    this worktree's core (PYTHONPATH=$(pwd), confirmed the resolved
    transforms.py path was inside the worktree, not the main checkout):
    7.4727 m full 3D error / 6.2210 m on the Up axis alone.
  • python -m pytest tests/core/coords -q64 passed.
  • Mutation check: temporarily replaced
    enu_origin_body = -c_body_enu @ body_origin_enu with
    enu_origin_body = body_origin_enu in the new
    test_round_trip_with_offset_origin — it failed
    (Max absolute difference among violations: 7.17...) — then reverted,
    and the suite is green again (64 passed).
  • python -m pytest tests/test_repo_conventions.py -q479 passed
    (pyflakes + convention ratchets).

🤖 Generated with Claude Code

qmohsu added 2 commits August 26, 2026 07:17
body_origin_enu (enu_to_body) is the body origin expressed in ENU;
enu_origin_body (body_to_enu) is the ENU origin expressed in the body
frame. They are related by enu_origin_body = -C @ body_origin_enu, not
equal, so passing the same array to both -- the obvious thing to try --
silently returns a wrong point (measured: full 3D round-trip error 7.47 m,
with a 6.22 m error on the Up axis alone, for x=[10,20,5], origin=[1,2,3],
roll/pitch/yaw=(0.1,0.2,0.3)).

The signatures stay as-is per Eqs. (2.6)/(2.7); this adds a CONVENTION
WARNING to both docstrings (matching the house style in
core/sensors/strapdown.py's quat_to_rotmat) and two tests to
TestEnuBody: a round trip through a non-None origin using the correct
conversion (the existing test_round_trip only ever exercised the
default None), and an inequality assertion pinning that the naive
same-array usage does not round-trip, so a future change that unifies
the two arguments turns it red instead of silently changing meaning.

docs/ch2_equation_mapping.md and docs/CH2_QUICK_REFERENCE.md are silent
on the frame of these origin arguments -- neither states it, correctly
or otherwise -- so neither needed a change.
Two follow-ups from review of the previous commit, both confirmed by
rerunning the tests the review named:

- from core.coords.rotations import euler_to_rotation_matrix sorted
  after the core.coords.transforms block, which ruff's I001 flags;
  moved above it (matches ruff --fix's own reordering, diffed to
  confirm nothing else moved). Was growing the lint ratchet from 84 to
  85; test_lint_debt_only_shrinks.py now passes and a direct
  `ruff check --statistics` over the ratchet's SOURCE_DIRS confirms 84.
- The new same-array trap test used a bare `assert`, the only one in a
  file that otherwise uses self.assert* 34 times; changed to
  self.assertGreater to match. Also folded the closed form
  recovered - x_enu == -(I + C.T) @ origin into the comment above it,
  so the ~7.47 m figure is checkable by inspection instead of only by
  rerunning.
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