Skip to content

Unify order2res Earth model with the spec-page sphere (R=6371.0088)#126

Merged
espg merged 2 commits into
mainfrom
claude/119-unify-order2res
Jul 21, 2026
Merged

Unify order2res Earth model with the spec-page sphere (R=6371.0088)#126
espg merged 2 commits into
mainfrom
claude/119-unify-order2res

Conversation

@espg

@espg espg commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Closes #119

What & approach

mortie.tools.order2res no longer uses the historical flat constant
(111 km/deg × 58.6323 × 0.5^order, implied sphere R ≈ 6366 km). It now derives the resolution from the exact HEALPix sphere at mean radius R = 6371.0088 km, the same Earth model the spec page (docs/specification.md §3, PR #118) uses for both its cell-scale and cell-area columns:

EARTH_RADIUS_KM = 6371.0088  # single module-level constant

def order2res(order):
    area = 4 * np.pi * EARTH_RADIUS_KM**2 / (12 * 4**order)  # km2
    return float(np.sqrt(area))  # RMS cell spacing (km)

Because HEALPix is equal-area, every order-k cell has area 4πR²/(12·4^order); the cell scale is sqrt(area), the RMS cell spacing. Code and page now share one Earth model.

Behavioral change

This is a behavioral change: order2res is ~0.2% larger at every order (order 0: 6508.19 km6519.62 km; order 29: 1.212 cm1.214 cm). Its consumers shift accordingly:

  • res2display — per-order printout (order 12 now 1.592 km, order 13 795.852 m);
  • tests/test_coverage_boundary.py — coverage-buffer pad pad_cells * order2res(order) / KM_PER_DEG, now ~0.2% larger (more conservative — see Questions).

Phases

  • Phase 1 — add EARTH_RADIUS_KM constant + sphere-formula order2res in mortie/tools.py; update the res2display docstring examples; rewrite the docs/specification.md §3 "code vs. page" note to reflect that code is now unified onto the page sphere.
  • Phase 2 — re-pin test_tools.py (test_order2res_basic/_range now compare against the sphere formula; test_order2res_decreasing unchanged — still valid); refresh res2display expected strings; switch test_spec_page.py to import EARTH_RADIUS_KM from mortie.tools instead of redefining it locally, and update its module docstring.

How tested

  • maturin develop --release (no Rust change here — order2res is pure Python — but the extension was rebuilt for the suite).
  • flake8 mortie --select=E9,F63,F7,F82 — clean.
  • pytest -v683 passed, 11 skipped, 1 failed. The single failure is the pre-existing, unrelated test_spec_page.py::TestDecimalParseTieBreak::test_order29_string_parses_to_area_word (issue main is red: test_spec_page tie-break test predates #120's p-suffix rendering #123, owned by a separate PR — not touched here). Every re-pinned test passes, including the full test_coverage_boundary.py matrix.

Coordination note

This branch edits mortie/tests/test_spec_page.py (the EARTH_RADIUS_KM import + a module-docstring refresh). PRs for #123 and #124 also touch that file — flagging the potential merge-order/rebase conflict. This PR does not touch the TestDecimalParseTieBreak class that #123 owns; the overlap is confined to the imports block and the module docstring.

Questions for review

  • Boundary pad tolerance. The pad in test_coverage_boundary.py grew ~0.2% with order2res. A larger pad only loosens the "no-escape" bounds (lo_bound/hi_bound/lon_slack are additive tolerances), so it is strictly more conservative and the whole matrix still passes — no assertion was weakened. Flagging in case you'd prefer the pad decoupled from order2res and pinned to a fixed km value instead.
  • _EARTH_RADIUS_M (buffer path). morton_buffer_meters keeps its own _EARTH_RADIUS_M = 6_371_008.7714 (IUGG 2015 mean, m) — that is 6371.0087714 km, vs the new EARTH_RADIUS_KM = 6371.0088. They agree to ~0.03 m and Unify order2res Earth model with the spec page sphere (R=6371.0088) #119 scopes only order2res, so I left the buffer constant untouched. Say the word if you'd like them collapsed to one constant.

🤖 Generated with Claude Code

@espg espg added the implement label Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] <I001> reported by reviewdog 🐶
Import block is un-sorted or un-formatted

import math
import pytest
import numpy as np
from numpy.testing import assert_array_equal, assert_allclose
from mortie import tools

@espg espg left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Fresh-context adversarial review of the #119 unification (order2res → exact HEALPix sphere sqrt(4πR²/(12·4^order)), R = EARTH_RADIUS_KM = 6371.0088 km). The change is correct and meets the issue's acceptance criteria. Two comments below, both non-blocking (one test suggestion, one out-of-scope observation).

Verified correct

  • Formula & spec-page parity. order2res now returns sqrt(4πR²/(12·4^order)) and matches the spec-page "cell scale" column exactly: order 0 → 6,519.623 km (table row 0), order 12 → 1.592 km (table row 12). Recomputed res0 = 6519.6235 km; ratio to the old 111·58.6323 = 6508.185 is 1.00176 (~0.18%), consistent with the "~0.2% larger" claim in issue #119 and §3.
  • Single-sourcing. EARTH_RADIUS_KM is defined once in mortie/tools.py; test_spec_page.py now imports it (removing its local redefinition), so code and page share one constant. No stray 6371.0088 literal elsewhere.
  • Re-pinned test values recomputed and correct. TestRes2Display: order 12 = 1.592 km, order 13 = 795.852 m, order 25 = 19.43 cm, order 29 = 1.214 cm — all match the sphere formula. res2display docstring examples updated to match. These are genuine literal golden pins (not weakened) and flow through order2res, so they anchor the constant.
  • Coverage-boundary pad direction is right. pad = pad_cells * order2res(order) / KM_PER_DEG feeds only the "no-escape" (b) bounds — lo_bound = lat_lo - pad, hi_bound = lat_hi + pad, lon_slack = half + pad/cos_floor — so a larger pad strictly loosens those assertions (the ~0.2% increase cannot cause a spurious failure). The superset (a) check doesn't use pad. No re-pin needed there; correct to leave it.
  • No stale mentions missed. 58.6323 / R ≈ 6366 survive only in docs/specification.md as the historical value the note explicitly says was replaced — intentional. Notebook resolution outputs render via order2res dynamically and their .1f displays (12.1 mm@order29) are unchanged (old 12.122 → new 12.144, both 12.1). USAGE.md §order2res is generic ("approximate resolution in km") and stays accurate.
  • CLAUDE.md compliance. Commits are title-only (phase 1 of issue #119, phase 2 of issue #119) with no attribution, matching repo style; the behavioral change carries tests in the same PR; no assertions weakened, no lint-ignore added; import math in test_tools.py is used.

Comments (both non-blocking): (1) test_order2res_range/_basic now pin against a helper that recomputes the identical formula/constant — self-referential, so add one literal anchor there (coverage is fine via res2display/spec_page). (2) Two near-identical Earth-radius constants now coexist in tools.py (EARTH_RADIUS_KM vs pre-existing _EARTH_RADIUS_M) — future cleanup, out of scope here.

Net: sound, ship-ready pending the optional test tweak.


Generated by Claude Code

res = tools.order2res(order)
expected = 111 * 58.6323 * (0.5 ** order)
assert_allclose(res, expected, rtol=1e-10)
assert_allclose(res, _sphere_res(order), rtol=1e-10)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Self-referential pin (acceptable, but worth one literal anchor). Both test_order2res_range and test_order2res_basic now assert order2res(order) == _sphere_res(order), where _sphere_res recomputes the same formula sqrt(4πR²/(12·4^order)) with the same tools.EARTH_RADIUS_KM. This checks that order2res follows the formula, but it can no longer catch a wrong constant/formula: flip EARTH_RADIUS_KM and both sides move together, so the test stays green. Contrast the old pin (111 * 58.6323), which was an independent literal.

Coverage isn't actually lost overall — TestRes2Display pins hard literal strings that flow through order2res (1.592 km@12, 795.852 m@13, 19.43 cm@25, 1.214 cm@29 — all recomputed and correct), and test_spec_page pins the committed table literals (6,519.623 km@0), so a bad R would fail those. So this is a suggestion, not a defect: consider adding one literal assertion here (e.g. assert_allclose(tools.order2res(0), 6519.623, atol=1e-3)) so the direct order2res test is self-anchoring rather than relying on downstream tests.


Generated by Claude Code

Comment thread mortie/tools.py
# Mean Earth radius (km), the exact HEALPix sphere the spec page's resolution
# table (docs/specification.md §3) derives from. order2res is the RMS cell
# spacing on this sphere; unified with the page in issue #119.
EARTH_RADIUS_KM = 6371.0088

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Non-blocking observation (out of scope for this diff). The module now carries two Earth-radius constants: EARTH_RADIUS_KM = 6371.0088 (here) and the pre-existing _EARTH_RADIUS_M = 6_371_008.7714 at line 752 (used by the buffer cell_width_m). These are the same physical radius to ~0.03 m (6371.0088 km = 6,371,008.8 m vs 6,371,008.7714 m). Not a defect and not introduced by #119, but a future cleanup could single-source them so the buffer path and order2res provably share one Earth model. Flagging only; no change needed in this PR.


Generated by Claude Code

@codspeed-hq

codspeed-hq Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing claude/119-unify-order2res (dbba806) with main (4b57a1f)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@espg espg left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Adversarial re-review of the order2res unification (issue #119). I tried to refute the diff and could not find a correctness or test defect — the change is sound. Verification notes:

  • Formula & constant correct. order2res(0) = sqrt(4π·6371.0088²/12) = R·sqrt(π/3) ≈ 6519.62 km, vs the old 111·58.6323 = 6508.19 km → +0.176%, matching the "~0.2% larger at every order" claim. Each order halves cleanly (area ÷4).
  • Re-pinned test_tools.py values check out against the sphere reference: _sphere_res(order) mirrors the implementation, and orders 12/13/25/29 reproduce the res2display pins exactly.
  • res2display pins are consistent with its round(value, 3) + str() rendering (trailing zeros dropped): order 12 → 1.592 km, 13 → 795.852 m, 25 → 19.43 cm, 29 → 1.214 cm. Docstring examples refreshed to match.
  • test_spec_page.py now imports EARTH_RADIUS_KM from mortie.tools instead of redefining it — code and page provably share one Earth model; regenerated rows are byte-identical since the constant value is unchanged.
  • docs/specification.md §3 note updated from "code vs. page" divergence to "code and page unified", correctly stating consumers read the cell-scale column directly.
  • test_coverage_boundary.py correctly left untouched. The pad pad_cells * order2res(order) / KM_PER_DEG grows ~0.2%, which only loosens the invariant-(b) no-escape bounds (lo_bound/hi_bound/lon_slack) — no assertion is tightened or weakened in any material way (0.2% of a 3-cell pad), and the invariants still hold. Worth a one-line mention in the PR body under testing that the pad re-check was done and required no change.

Two non-blocking nits inline (docstring "Approximate" wording; float() dropping array-broadcast that the old form had, though no current caller relies on it). No changes required for correctness.


Generated by Claude Code

Comment thread mortie/tools.py
def order2res(order):
res = 111 * 58.6323 * .5**order
return res
"""Approximate cell scale (km) at a HEALPix tessellation ``order``.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Nit (wording): the summary line calls this "Approximate cell scale", but the body — correctly — says it is the exact RMS cell spacing on the equal-area HEALPix sphere (sqrt(area)). There is no approximation in the closed form; the only "approx" is that RMS spacing stands in for a per-cell shape. Consider dropping "Approximate" (or say "RMS cell scale") so the one-liner doesn't undersell the exactness the rest of the docstring establishes. Not blocking.


Generated by Claude Code

Comment thread mortie/tools.py
model (issue #119).
"""
area = 4 * np.pi * EARTH_RADIUS_KM**2 / (12 * 4**order) # km2
return float(np.sqrt(area))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Minor behavioral narrowing: float(np.sqrt(area)) forces a scalar return, whereas the old 111 * 58.6323 * .5**order broadcast over an array order. I checked every consumer (res2display, test_coverage_boundary.py:78, USAGE.md, the notebooks) and all pass a scalar, so nothing breaks today — but if any caller ever passes an array of orders, float() on a non-scalar array raises TypeError. If you want to keep the previous vectorization, np.sqrt(area) (unwrapped) would return a numpy scalar for scalar input and an array for array input. Purely a note; the scalar form is fine for current usage.


Generated by Claude Code

@espg espg added the waiting label Jul 21, 2026
@espg espg mentioned this pull request Jul 21, 2026
@espg
espg marked this pull request as ready for review July 21, 2026 16:05
@espg
espg merged commit e78f1d9 into main Jul 21, 2026
16 of 19 checks passed
@espg
espg deleted the claude/119-unify-order2res branch July 21, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify order2res Earth model with the spec page sphere (R=6371.0088)

2 participants