Warn that enu_to_body/body_to_enu take origins in different frames - #97
Open
qmohsu wants to merge 2 commits into
Open
Warn that enu_to_body/body_to_enu take origins in different frames#97qmohsu wants to merge 2 commits into
qmohsu wants to merge 2 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
core/coords/transforms.py'senu_to_body(line 454) andbody_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 expressedin ENU.
body_to_enu(..., enu_origin_body=...)wants the ENU origin expressedin the body frame.
They're related by
enu_origin_body = -C @ body_origin_enu(C = therotation 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:"6.22 m" referred to — a per-axis figure, not the vector norm; both
numbers are reported here since they differ).
3.55e-15m(float noise).
Nothing in the repo previously exercised this:
TestEnuBody::test_round_triponly ever called with the default
origin=None, and no caller in the repopasses 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:core/coords/transforms.py:454and:485— added aNotes:sectionwith a CONVENTION WARNING to both docstrings, naming the frame of each
origin argument and giving the conversion. Style matches
core/sensors/strapdown.py'squat_to_rotmatCONVENTION WARNING (theexisting house style for this kind of cross-function frame mismatch note).
tests/core/coords/test_transforms.py— two new tests inTestEnuBody:test_round_trip_with_offset_origin: round-trips through a non-Noneorigin using the correct
-C @conversion.test_same_array_as_both_origins_does_not_round_trip: an inequalityassertion (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.
docs/ch2_equation_mapping.mdanddocs/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 --shortafter the change:Pure additions (2 files changed, 80 insertions(+), 0 deletions(-)). No other
.pyfile, no data file, and no figure byte touched.Test plan
this worktree's
core(PYTHONPATH=$(pwd), confirmed the resolvedtransforms.pypath 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 -q→ 64 passed.enu_origin_body = -c_body_enu @ body_origin_enuwithenu_origin_body = body_origin_enuin the newtest_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 -q→ 479 passed(pyflakes + convention ratchets).
🤖 Generated with Claude Code