Skip to content

Group A small-fix bundle from issue #108#111

Draft
espg wants to merge 4 commits into
mainfrom
claude/small-fixes-2026-07-10
Draft

Group A small-fix bundle from issue #108#111
espg wants to merge 4 commits into
mainfrom
claude/small-fixes-2026-07-10

Conversation

@espg

@espg espg commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Refs #108 (Group A only — Group B items remain open on that issue, so no Closes).

One bundled small-fix PR per §5, one commit per item, plus one follow-up commit addressing the adversarial self-review (review):

  • (1) from_geometry(..., normalize=False) linestring guard — the linear branch of from_geometry now raises ValueError("normalize applies only to polygonal geometry") instead of silently ignoring normalize=False, symmetric with the existing moc/tolerance/max_cells rejection two lines above (mortie/geometry.py). Docstring updated; test test_ingest_linear_rejects_normalize_false covers LineString and MultiLineString via from_wkt. (Consolidated follow-ups from the June/July sweeps (#73, #88, #97) #108 Group A item 1, from Follow up items for the week of July 4, 2026 #97 item 7)
  • (2) crate-root clippy allow for useless_conversion#[allow(clippy::useless_conversion)] at the top of src_rust/src/lib.rs with a one-line comment that the pyo3/numpy ? conversions are load-bearing. cargo clippy --all-targets warning count: 43 → 6 (exactly the 37 useless_conversion instances suppressed). The 6 remaining warnings (3 cloned_ref_to_slice_refs, 2 useless_vec, 1 needless_range_loop — all pre-existing, mostly in test code) stay visible, confirming nothing else is masked. (Consolidated follow-ups from the June/July sweeps (#73, #88, #97) #108 Group A item 2, from Follow up items for the week of June 23, 2026 #73 item 8)
  • (3) dissolve hemisphere winding guarddissolve (Rust src_rust/src/dissolve.rs, surfaced as a Python ValueError from rust_dissolve, mirrored in the Python oracle _dissolved_rings_py via _reject_hemisphere_cover) now fails loud on covers spanning near or over a hemisphere instead of silently swapping exteriors and holes. Tests: Rust hemisphere_cover_fails_loud / sub_hemisphere_cover_still_dissolves, Python test_dissolve_hemisphere_cover_fails_loud (exact-hemisphere and over-hemisphere polar cap, both engines, plus the dissolve=False fallback). (Consolidated follow-ups from the June/July sweeps (#73, #88, #97) #108 Group A item 3, from Follow up items for the week of July 4, 2026 #97 item 5)
  • (review follow-up) commit 4f43894 addresses the self-review findings:
    • Σ-vs-exact-area wrap cross-check (finding): the covered-area guard alone did not make the winding sign trustworthy — the mod-4π fan wrap is driven by the area each ring encloses, so the reviewer's ~1.3 sr equatorial band (320 order-4 cells, verified reproducing on both engines) wrapped both rings to −5.63, fired the global flip, and died in the antimeridian stitcher's cryptic assert. classify_and_split now takes the exact covered area and rejects with a curated ValueError when ||Σ ring areas| − area| > π (an honest fan matches the exact area within chord discretization ≲0.1 sr; any wrap is off by ~4π, so π has no false-positive surface). Mirrored in the Python oracle (same failure mode there, RuntimeError in _next_segment); the "sign is trustworthy" comments are now backed by the cross-check. Tests: Rust equatorial_band_fails_loud_not_cryptic, Python test_dissolve_hemisphere_enclosing_ring_fails_loud (both engines).
    • Truncated dangling comment above classify_and_split (finding): removed.
    • cover_area disjoint-cells precondition (finding): documented in both mirrors (cover_area doc comment and _reject_hemisphere_cover docstring) rather than deduping — duplicate words already break the edge-cancellation dissolve itself, so disjoint input is the function's existing contract.

Note on the guard key for (3)

The issue's standing recommendation was to raise when |Σ ring areas| ≈ 2π. While implementing it I found the literal check does not fire on the flagship ambiguous input: for a cover of exactly half the sphere (24 order-1 cells, base cells 0–5), the anchor-fan signed-area formula wraps mod 4π per-ring and returns Σ = −1.85, not ±2π — and because ring chaining starts from a HashMap iteration, the wrapped value's sign is not even deterministic at that scale. Probing polar caps: Σ tracks the true area faithfully below ~2π (e.g. 6.02 for a 6.02 sr cap) and only breaks at/past the hemisphere.

So the guard is keyed on the exact covered area instead — cells are equal-area, so Σ π/(3·4^depth) over the input words is exact — raising when area > 2π − margin with margin = 2% of 2π (HEMISPHERE_MARGIN). This honors the item's intent (fail loud when the winding sign becomes untrustworthy), fires on the exact-hemisphere case the literal check misses, and additionally catches all over-hemisphere covers (which today silently emit the complement). The review follow-up adds the complementary per-ring wrap detection (Σ cross-checked against the exact area), so hemisphere-enclosing rings on small covers now also fail loud instead of panicking. The error messages tell the user to split the cover or pass dissolve=False.

How tested

  • cargo test: 200 passed, 0 failed (3 new dissolve tests); cargo fmt clean; cargo clippy --all-targets: 6 pre-existing warnings, 0 useless_conversion.
  • maturin develop --release && pytest -v: 673 passed, 11 skipped (3 new Python tests).
  • flake8 mortie --select=E9,F63,F7,F82: clean.

Questions for review

  1. Threshold for (3): the 2% margin (2π·0.02 ≈ 0.126 sr) is a judgment call — it keeps the guard clear of the fan-formula breakdown while still admitting a 96%-of-hemisphere polar cap (which dissolves correctly today). The wrap cross-check tolerance (π) is separately justified above. Happy to tighten/loosen either.
  2. Guard key for (3): keyed on exact covered area rather than the literal |Σ ring areas| for the reasons above — flagging the deviation from the issue's wording explicitly in case the literal form was wanted anyway.
  3. Over-hemisphere covers (area > 2π) now raise where they previously returned a silently inverted outline, and hemisphere-enclosing-ring covers (e.g. equatorial bands) now raise a curated ValueError where they previously surfaced a cryptic stitcher panic — behavior changes beyond "near 2π", but the old outputs were wrong or unactionable. The full winding-free classifier from Follow up items for the week of July 4, 2026 #97 item 5 remains a possible follow-up if polar-scale dissolves become a real workload.
  4. Uniformity call (from the self-review, finding): moc=True, normalize=False on polygonal geometry is still a silent no-op (morton_coverage_moc has no orientation auto-correct at all), while the linear branch now fails loud — the docstring documents the asymmetry. Issue Consolidated follow-ups from the June/July sweeps (#73, #88, #97) #108 scoped item 1 to the linear branch, so this PR leaves the polygonal branch unchanged; say the word if fail-loud should be uniform and it can ride this PR or a follow-up.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.33%. Comparing base (2f0439d) to head (4f43894).

Files with missing lines Patch % Lines
mortie/geometry.py 94.11% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #111      +/-   ##
==========================================
- Coverage   94.33%   94.33%   -0.01%     
==========================================
  Files           9        9              
  Lines        1307     1323      +16     
==========================================
+ Hits         1233     1248      +15     
- Misses         74       75       +1     
Flag Coverage Δ
unittests 94.33% <94.11%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
mortie/geometry.py 95.12% <94.11%> (-0.07%) ⬇️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2f0439d...4f43894. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing claude/small-fixes-2026-07-10 (4f43894) with main (2f0439d)

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)

Solid bundle overall: fix (1) matches the issue and is properly tested; fix (2)'s inventory claim verified empirically (clippy on main: exactly 37 useless_conversion; on this branch: 0 suppressed lints, precisely the 6 claimed pre-existing warnings remain); and fix (3)'s deviation from the issue's literal |Σ ring areas| trigger is well-reasoned — the fan sum does wrap mod 4π exactly where the literal check would need it, and keying on exact covered area is the right call for that failure mode. Findings, ranked: (should-fix) the guard does not make the winding sign trustworthy as the new comments claim — a ~1.3 sr equatorial band (verified end-to-end on this branch) wraps both ring areas to −5.63, fires the global flip, and dies in a cryptic stitcher panic instead of the new actionable ValueError; a cheap cross-check of Σ fan areas against the exact covered area would close the hole (details inline on dissolve). (should-fix, trivial) a truncated leftover comment dangles above classify_and_split (dissolve.rs 420–421). (nit) cover_area's disjoint-cells precondition is undocumented (duplicates double-count toward the threshold); (nit) moc=True, normalize=False on polygonal geometry remains a silent no-op while the linear branch now fails loud — worth an explicit espg call on uniformity.


Generated by Claude Code

Comment thread src_rust/src/dissolve.rs
Comment on lines +66 to +75
pub fn dissolve(morton: &[u64], step: u32) -> Result<ClassifiedRings, String> {
let area = cover_area(morton);
if area > std::f64::consts::TAU - HEMISPHERE_MARGIN {
return Err(format!(
"dissolved cover spans {area:.6} sr — within 2% of a hemisphere \
(2π sr) or beyond — so its exterior/hole winding is ambiguous; \
split the cover into sub-hemisphere parts or pass dissolve=False \
for per-cell polygons"
));
}

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)

The exact-area key is sound for what it checks, but it does not deliver what the new comments claim. The mod-4π wrap is a function of the area each ring encloses, not of the covered area — so a small cover whose rings individually enclose more than a hemisphere still wraps, sails through this guard, and still breaks classification.

Verified end-to-end on this branch (throwaway clone, scratch test in this module): an equatorial band of 320 order-4 cells covering |lat| ≲ 3.7°:

band cells=320 area=1.3089969389957445 (guard threshold=6.157521601035994)
boundary rings=2 ring_areas=[-5.628269441174673, -5.628269441174666] sum=-11.25653888234934
thread ... panicked at src_rust/src/dissolve.rs:372:5:
unbalanced antimeridian segments but no pole enclosed

Each boundary ring encloses a ~2π+0.65 sr cap, so the fan returns the wrapped complement (−5.628 instead of +6.94), Σ = A − 4π < 0, the global flip fires on a correctly-wound cover, and the stitcher dies on its assert — surfaced to Python as the cryptic dissolve panicked: unbalanced antimeridian segments but no pole enclosed instead of this item's curated, actionable ValueError. (This family is pre-existing on mainclassify_and_split is unchanged — so it's not a regression; but the PR's whole point per issue #108 was "fail loud instead of silently inverting", and this polar-adjacent shape still fails cryptic.)

Cheap hardening that would close it: dissolve already has the exact area in hand — pass it into classify_and_split and, after computing the fan areas, reject when (total.abs() - area).abs() is large (any wrapped ring is off by ~4π; a tolerance of, say, π has no false-positive surface). That converts every wrap — near-2π covers and hemisphere-enclosing-ring covers — into the same actionable error, and makes the "sign is trustworthy" comments below actually true. At minimum, the comments at lines 432–433 (and the geometry.py mirror) should be softened back to an assumption, because as written they overclaim.


Generated by Claude Code

Comment thread src_rust/src/dissolve.rs Outdated
Comment on lines +420 to +421
// Winding-guard margin (issue #108): spherical signed area is defined mod 4π,
// so a cover whose |Σ ring areas| lands near 2π reads the same as its

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)

Leftover edit artifact: this comment is truncated mid-sentence ("…reads the same as its") and duplicates the HEMISPHERE_MARGIN block at the top of the file — it also dangles over classify_and_split, which defines no "winding-guard margin". Delete these two lines.


Generated by Claude Code

Comment thread src_rust/src/dissolve.rs
Comment on lines +53 to +59
/// Exact covered area (steradians) of a morton cover: Σ π/(3·4^depth).
fn cover_area(morton: &[u64]) -> f64 {
morton
.iter()
.map(|&w| std::f64::consts::PI / (3.0 * 4f64.powi(mort2nested(w).1 as i32)))
.sum()
}

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: "exact" holds only for disjoint, non-duplicated words — duplicates and parent/child overlaps double-count, so a legitimate sub-threshold cover containing duplicates can spuriously raise. That precondition is real in practice: raw morton_coverage output can carry duplicates (the test helper _polar_cap that feeds the new over test needs its np.unique). Worth one line in this doc comment (mirrored in _reject_hemisphere_cover) stating the disjoint-cells precondition, or a dedup in both engines — whichever, keep the two mirrors in lockstep.


Generated by Claude Code

Comment thread mortie/geometry.py Outdated
Comment on lines +693 to +694
# (Spherical signed area is defined mod 4π; the hemisphere guard above
# keeps the cover well under 2π, so the sign is trustworthy.)

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)

Same overclaim as the Rust twin (see the main comment on dissolve in src_rust/src/dissolve.rs): the guard bounds the covered area, but the wrap is driven by the area each ring encloses — an equatorial band of ~1.3 sr (well under 2π) still wraps both rings to −5.63, flips the winding here, and dies in _next_segment's RuntimeError instead of the new curated ValueError. "keeps the cover well under 2π, so the sign is trustworthy" should either become true (cross-check sum(areas) against the exact covered area the guard just computed) or stay phrased as the assumption it was before this change.


Generated by Claude Code

Comment thread mortie/geometry.py
Comment on lines +264 to +265
if not normalize:
raise ValueError("normalize applies only to polygonal geometry")

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 / question for espg: the fix matches issue #108 Group A item 1 exactly and the test pins both LineString and MultiLineString. But the same "silent no-op" rationale from #97 item 7 now applies one branch up: moc=True, normalize=False on polygonal geometry is still silently ignored (morton_coverage_moc has no orientation auto-correct at all), while the linear branch fails loud. The updated docstring enshrines the asymmetry ("Ignored when moc=True"). Defensible as-is since the issue scoped this to the linear branch — flagging in case fail-loud was meant to be the uniform policy.


Generated by Claude Code

@espg espg mentioned this pull request Jul 10, 2026
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.

2 participants