Repository navigation
fix: a figure that says NED is not a rotation, and quat_to_euler at the lock - #130
Merged
Merged
Conversation
…he lock Chapter 2's mathematics is clean -- 400 random attitudes agree across quaternion, matrix and Euler to 1.6e-14. Everything here is a claim printed beside that mathematics, plus one bug that lives on a single point. **`ch2_frame_chain`'s suptitle stated a falsehood.** It read "NED is a handedness-preserving swap-and-flip of ENU, not a rotation." Measured from the shipped matrix: det = +1.0, turn = 180.0 deg, axis = [0.7071, 0.7071, 0] -- the NE bisector. It is a proper rotation, and that is *why* handedness survives; a swap-and-flip that were not one would have det = -1. The caption now derives all three numbers from `C_enu_to_ned` at draw time, so it cannot disagree with the matrix again. **`quat_to_euler` was wrong at exactly roll = +/-90, and only there.** Not degraded -- wrong. `euler_to_quat(pi/2, -30 deg, 0)` came back as [90, 180, 180] deg, whose matrix differs from the input by **0.5**: a different attitude, not a gimbal-equivalent triple. At 89.999 deg it is exact to 6e-12. The mechanism is signed zeros: at the singularity both `arctan2` calls receive (0.0, -2.2e-16), a zero numerator beside a negative denominator, so each returns +pi instead of 0. `rotation_matrix_to_euler` has always carried a lock branch; the quaternion path now mirrors it entry for entry -- same threshold, same `copysign`, same fix-yaw-solve-pitch convention -- so the two share one convention rather than two that happen to agree. `tests/core/coords/test_quat_to_euler_at_the_lock.py` pins it: 19 of its 33 assertions fail against the old code, and its two tolerances are measured against both the noise (1.6e-7, 6.4e-11) and the defect (0.906, 3.14 rad). Note which assertion did the work. "roll comes back as +/-90 with its sign" passes on the *broken* code. So does the reconstruction test at (90, 0, 0), because [90, 180, 180] has the same pitch-minus-yaw. Only several distinct (yaw, pitch) pairs discriminate. **PR #66's equation renumbering never reached the dataset.** `config.json`, the generator that writes it, and ten cites in the dataset README still advertised "2.1 (LLH->ECEF), 2.2 (ECEF->LLH), 2.3 (ECEF->ENU)". Per `docs/equation_index.yml` those are the local body/map *vector definitions* and map <-> body; the geodetic chain is 2.9 and 2.10. Regenerating from the recorded `san_francisco` preset moved the four `equations` strings in `config.json` and **nothing else** -- every coordinate, rotation and accuracy byte identical. Also: `ch2_passive_vs_active` labelled the passive relation Eq. (2.21), which is quaternion-to-matrix; the relation is (2.11) and the matrix drawn is (2.17). Fixed in the panel, the module docstring, the References line and the chapter README. Smaller, each measured: - **The yaw panel's x' axis had collapsed to a stub.** `VIEW_AZIM` was 35.0 and so was the yaw panel's rotation angle, so the one axis that panel exists to show pointed straight at the camera. Over every triad drawn under the shared view the shortest on-screen axis was 0.342 of full; at (35, 45) it is 0.578. - The gimbal figure's top row was labelled by roll alone, hiding that every panel also carries yaw = 30 -- the 30 the bottom row's argument is about. - `ecef_to_llh`'s `tol` was documented "(meters)" and is compared against a latitude difference in **radians**. At 20 200 km, tol=1e-3 leaves 18 mm of height error at 38 deg latitude and 218 m at 89. - `example_coordinate_transforms` printed `Location: Unknown` -- the key is `config['reference_point']['location']` -- and printed "Difference: 5.77e-04 m (should be ~0)" beside "sub-nanometer accuracy". Those two argued; the 0.58 mm is the file's `%.3f` storage quantum, bounded by sqrt(3) * 0.5 mm = 0.87 mm, and the worst of the 20 shipped points is 0.835 mm. The print site says so now. - `docs/CH2_QUICK_REFERENCE.md` claimed 55 passing tests (68), 16 functions (17), and cited `CHAPTER_2_MAPPING_SUMMARY.md`, which exists nowhere. The counts are replaced by their sources, since a count in prose rots silently. - `_C_ENU_NED` read source-first while its own comment, three sibling docstrings and `docs/equation_index.yml` all use `C^to_from`. Renamed to `_C_NED_ENU` -- one outlier moved to match six neighbours. - `euler_to_quat` does not canonicalise the hemisphere and `rotation_matrix_to_quat` does, so a round trip sign-flips on 10 of the 20 shipped attitudes. Behaviour unchanged -- `quaternions.txt` records these signs -- but both docstrings now say so. - The dataset File Structure omitted `rotation_matrices.txt` and `README.md`.
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.
Chapter 2's mathematics is clean. A fresh sweep of 400 random attitudes (seed
42, roll off the lock) agrees across quaternion, matrix and Euler:
quat_to_rotation_matrix(euler_to_quat)vseuler_to_rotation_matrixquat_to_eulerEverything below is a claim printed beside that mathematics, plus one bug
that lives on a single point.
A committed figure stated a falsehood
ch2_frame_chain's suptitle read "NED is a handedness-preservingswap-and-flip of ENU, not a rotation". Measured from the shipped matrix:
det = +1.0, turn = 180.0 deg, axis = [0.7071, 0.7071, 0] -- the NE
bisector. It is a proper rotation, and that is why handedness survives: a
swap-and-flip that were not one would have det = -1 and would hand a
right-handed triad to a left-handed frame. The caption now derives all three
numbers from
C_enu_to_nedat draw time, so it cannot disagree with thematrix again.
quat_to_eulerwas wrong at exactly roll = +/-90Not degraded -- wrong.
euler_to_quat(pi/2, -30 deg, 0)came back as[90, 180, 180]deg, whose matrix differs from the input by 0.5: adifferent attitude, not a gimbal-equivalent triple. At 89.999 deg it is exact
to 6e-12.
The mechanism is signed zeros, which is why the docstring's "degrades near the
lock" was the wrong description. At the singularity both
arctan2callsreceive
(0.0, -2.2e-16)-- a zero numerator beside a negative denominator-- so each returns
+piinstead of0, twice. It is a branch that exists onlyon the boundary, not a precision loss that grows.
rotation_matrix_to_eulerhas always carried an explicit lock branch. Thequaternion path now mirrors it entry for entry -- same
1 - 1e-12threshold,same
copysign, same fix-yaw-solve-pitch convention -- readingsin_roll,C[1,0]andC[1,1]out of Eq. (2.21) so the two share one conventionrather than two that happen to agree.
The red, before the fix: 19 failed / 14 passed (
git stashoncore/coords/rotations.py, new test only). After: 33 passed.Which assertion did the work is worth knowing, because it was not the obvious
one:
was never the wrong part.
(90, 0, 0)and(-90, 0, 0), because[90, 180, 180]has the samepitch - yaw. Onlyseveral distinct
(yaw, pitch)pairs discriminate -- which is why theparametrisation carries five per branch.
Both tolerances are measured against the noise and the defect: 1.6e-7 of
reconstruction against a
2*sqrt(2*LOCK_EPS)= 2.8e-6 gate derived from thethreshold itself, and 6.4e-11 of path disagreement against a 1e-9 gate, versus
0.906 and 3.14 rad for the defect.
PR #66's renumbering never reached the dataset
config.json, the generator that writes it, and ten cites in the datasetREADME still advertised "2.1 (LLH->ECEF), 2.2 (ECEF->LLH), 2.3 (ECEF->ENU)".
Per
docs/equation_index.ymlthose are the local body/map vectordefinitions and map <-> body; the geodetic chain is 2.9 and 2.10.
Regenerating from the recorded
san_franciscopreset moved the fourequationsstrings inconfig.jsonand nothing else -- every coordinate,rotation and accuracy byte identical, including
rotation_roundtrip_deg.tests/test_datasets_reproduce_from_their_recipe.pypasses after.The wrong equation on
ch2_passive_vs_activeThe panel said "Passive: C, Eq. (2.21)". (2.21) is quaternion-to-matrix; the
relation drawn is (2.11) and the matrix is built by
euler_to_rotation_matrix= (2.17). Fixed in the panel, the moduledocstring, the References line and
ch2_coords/README.md.Smaller, each measured
VIEW_AZIMwas 35.0 --and so was the yaw panel's own rotation angle, so the one axis that panel
exists to show pointed straight at the camera. Over every triad drawn under
the shared view (three figures, nine panels, plus the dashed reference in
each) the shortest on-screen axis length was 0.342 of full at (20, 35)
and is 0.578 at (35, 45). The maximiser is (36, 45) at 0.587.
panel also carries yaw = 30 -- the 30 the bottom row's argument is about.
ecef_to_llh'stolwas documented "(meters)" and is compared against alatitude difference in radians. Measured: at 20 200 km,
tol=1e-3leaves18 mm of height error at 38 deg latitude and 218 m at 89 deg; at the
surface it costs nothing at any latitude.
example_coordinate_transformsprintedLocation: Unknown-- the key isconfig['reference_point']['location']-- and printedDifference: 5.77e-04 m (should be ~0)beside "sub-nanometer accuracy".Those two argued. The 0.58 mm is the file's
%.3fstorage quantum, boundedby
sqrt(3) * 0.5 mm= 0.87 mm; the worst of the 20 shipped points is0.835 mm. The print site says so now.
docs/CH2_QUICK_REFERENCE.mdclaimed 55 passing tests (68), 16 functions(17, since
enu_to_llh_offset), and citedCHAPTER_2_MAPPING_SUMMARY.md,which exists nowhere (exhaustive unbounded grep: one hit, the citation
itself). The counts are replaced by their sources; a count in prose rots
silently because there is no failing run to notice. The hand-written "Last
Updated" line went the same way.
_C_ENU_NEDread source-first while its own comment, three siblingdocstrings and
docs/equation_index.ymlall useC^to_from. Renamed_C_NED_ENU-- one outlier moved to match six neighbours, rather thanrewriting the convention.
euler_to_quatdoes not canonicalise the hemisphere androtation_matrix_to_quatdoes, so a round trip sign-flips on 10 of the 20shipped attitudes. Behaviour deliberately unchanged --
quaternions.txtrecords these signs -- but both docstrings now say so, with the double-cover
sentence and how to compare two quaternions without tripping over it.
rotation_matrices.txtandREADME.md.Figures
All four regenerated and every PNG opened. All 12 files re-render
byte-identically on a second run, so the committed diff is the picture change
and nothing else.
Test plan
tests/core/coords tests/ch2_coords-- 118 passedtests/docs/test_readme_example_output.py -k ch2-- 2 passedtests/docs/test_readme_code_blocks.py -k "ch2 or ch6"-- 12 passed,repo root clean afterwards
tests/test_datasets_reproduce_from_their_recipe.py,test_repo_validators.py,test_semantic_api_debt_only_shrinks.py--47 passed
test_repo_conventions.py,test_every_figure_has_a_demo_behind_it.py,test_examples_import_this_checkout.py,test_python_floor_matches_syntax.py-- 723 passed, 8 skipped (10m24s)685 passed
tests/docs/test_notebooks_run.py -k ch2-- 2 passedtests/test_examples_answer_help.py-- 39 passedtests/test_example_console_encoding.py-- 79 passedNot a finding
A cross-agent report placed a bare
plt.savefig('zupt_trajectory_stance.svg')in an executed```pythonblockat
data/sim/ch6_foot_zupt_walk/README.md:204, writing SVGs to the repo rooton every docs-test run. It is already fixed on
mainand was notreproducible at
56f27b9: an unbounded grep forsavefig|save_figurein thatfile returns nothing, line 204 is
# Velocity over time, and commit885b9f6(an ancestor of
main) removed exactly that line. Independently,cf0ccb5made
tests/docs/test_readme_code_blocks.pyneutraliseplt.savefig,plt.showandplt.Figure.savefigfor the duration, so the block could notwrite even if it existed. Verified anyway:
-k "ch2 or ch6"green, root clean.Suspected, not fixed
ch2_coords/README.md's "Expected Output" pins inline mode only(
<!-- example-output: ch2_coords.example_coordinate_transforms -->, no--data). All three of this example's defects --Location: Unknownand thetwo arguing accuracy claims -- lived in the dataset branch, which nothing
pins. Worth a second marked transcript, but that is a new guard rather than a
fix and is left for a follow-up.
🤖 Generated with Claude Code