Skip to content

Spherical RelateNG kernel#422

Open
asinghvi17 wants to merge 18 commits into
relatengfrom
relateng-spherical
Open

Spherical RelateNG kernel#422
asinghvi17 wants to merge 18 commits into
relatengfrom
relateng-spherical

Conversation

@asinghvi17

Copy link
Copy Markdown
Member

Summary

Implements the Spherical() manifold for the RelateNG engine, so
relate(RelateNG(; manifold = Spherical()), a, b) produces a DE-9IM on the
unit sphere. Stacked on top of the relateng port branch; this PR is just the
spherical work (17 commits) for easier review.

  • Spherical kernel (kernel_spherical.jl): all 13 rk_* predicates over
    UnitSphericalPoint{Float64}, each a sign of det(u,v,w) = (u×v)·w, with an
    exact path (ExactPredicates.orient / Rational{BigInt} composites) and a
    float path. Bulge-aware great-circle arc_extent; S2 interior-on-left
    point-in-ring via meridian-crossing parity; ring/axis containment via a
    robust winding-number test (no atan2 branch ambiguity).
  • Manifold-derived point type: threaded _kernel_point_type /
    _to_kernel_point through the engine. Planar stays byte-identical
    (signed-zero Tuple{Float64,Float64}); Spherical ingests unit xyz.
  • Acceleration: the per-segment spatial index and the prepared-mode index
    build over 3D arc_extents, so the tree and prepared paths match the nested
    loop (a 2D endpoint box misses a long arc's bulge and prunes real crossings).
  • Antipodal edges: the kernel throws informatively on exactly-antipodal
    vertices; AntipodalEdgeSplit is the opt-in geometry correction that inserts
    the lon/lat midpoint as the remedy.

Engine generality (manifold-generic apply/topology) was already in place from
the port; this adds the spherical kernel and the point-type plumbing.

Test Plan

  • Spherical kernel conformance suite: 114,624 assertions, exact ∈ {True(), False()}
  • Planar kernel conformance: 76,908 — no regression
  • JTS XML conformance corpus: 6,537 — no planar regression
  • LibGEOS differential fuzz: 500 — no planar regression
  • End-to-end spherical_end_to_end.jl: planar agreement on a mid-latitude
    patch, pole-containing ring, tree/NestedLoop + prepared/unprepared
    agreement, antipodal throw
  • AntipodalEdgeSplit correction
  • Allocations + type stability (the P type parameter does not regress the
    planar path)
  • Full test/methods/relateng/runtests.jl green (exit 0)

One follow-up deferred (documented in the design doc): the lon-interval
indexed spherical point locator — a pure optimization over the already-correct
rk_point_in_ring fall-through.

🤖 Generated with Claude Code

asinghvi17 and others added 18 commits June 15, 2026 15:14
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrade rk_point_on_segment to a scale-invariant cross-product span test
(exact for unit and non-unit inputs alike), which the classify symmetry
tests over integer-grid points require.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generalize the PolygonNodeTopology port (_compare_angle, _is_angle_greater,
_is_between, _compare_between, rk_crossing_dirs_ccw, rk_is_crossing,
rk_is_interior_segment) to m::Manifold; they depend only on rk_quadrant and
rk_orient. Add a tangent-plane rk_quadrant and a crossing-apex comparator for
Spherical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An area element's boundary-arc box does not cover its interior (a ring
encircling a pole has a thin boundary band but its interior reaches the
pole). Widen each axis whose ±eᵢ is interior to the exterior ring out to
±1 — conservative, since over-covering can only under-prune.

Decide ±eᵢ membership by the ring's winding about the axis rather than a
per-triangle signed-solid-angle sum: each projected edge contributes a
turn < π, so there is no atan2 branch ambiguity. The solid-angle sum
reported spurious interiors for axes far from the loop's winding axis
(e.g. equatorial axes against a polar-cap ring). `_ring_contains_pole`
now shares this winding test. Documented limitation: super-hemisphere
rings under-report a non-encircled interior axis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thread the manifold's kernel point type `P` through the engine so a
non-planar manifold ingests its own coordinate representation. Planar is
byte-identical: `_kernel_point_type(::Planar)` is the signed-zero
`Tuple{Float64, Float64}` and `_to_kernel_point(::Planar, p)` is
`_node_point(p)`, while `_kernel_point_type(::Spherical)` is
`UnitSphericalPoint{Float64}` converted via `_spherical_kernel_point`.

- `kernel.jl`: add `_kernel_point_type`, `_to_kernel_point`,
  `_to_kernel_points` (manifold-aware `_node_points`), and
  `_kernel_point_box` (dimension-matched point extent).
- `RelateGeometry` and `RelatePointLocator` gain a `P` type parameter;
  `unique_points`, the locator `points`/`line_boundary`/`adj_edge_locator`
  fields, the segment-string eltype and `TopologyComputer`'s `NodeKey`
  dict are all typed from it.
- Route every coordinate ingest (`_node_points`/`_node_point`/`_tuple_point`
  on raw vertices) through `_to_kernel_point(m, …)`; the two remaining
  `_tuple_point`/`_node_point` reads are pure equality checks where
  conversion preserves the result.
- Spherical `rk_point_in_ring` reads a ring via `_ring_kernel_pts`: a 3D
  ring is already kernel coordinates (read verbatim to preserve the exact
  boundary orient), a 2D lon/lat ring is converted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Smoke tests for `relate(RelateNG(; manifold = Spherical()), …)` on the
unindexed NestedLoop path: a mid-latitude overlapping-box patch whose
DE-9IM matrix matches the planar result exactly, and a pole-containing
cap ring that `contains` a near-pole point — a case only the spherical
kernel classifies correctly. Both pass without further engine changes,
confirming the Phase 3 plumbing (ring orientation, node topology,
point-in-ring) is correct end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the tree-accelerated edge-intersection paths (unprepared and
prepared) valid on the sphere by building the per-segment spatial index
over bulge-aware 3D great-circle `arc_extent`s instead of a 2D endpoint
box. A 2D box misses a long arc's bulge — e.g. a near-equatorial arc
from lon 0 to 170 bulges to y ≈ 1 at lon 90 while its endpoint box has
y ∈ [0, 0.17] — and prunes away real crossings, producing a wrong DE-9IM.

- `_segment_extent` / `_segment_extent_table` / `_push_segment_extents!`
  dispatch on the manifold; Spherical uses `arc_extent`.
- `_select_edge_set_accelerator(::Spherical)` enables the tree above the
  segment-count threshold; `_segment_envs_disjoint(::Spherical)` prunes
  the nested loop with 3D arc extents.
- `_build_prepared_edge_index(::Spherical, ::AutoAccelerator)` indexes A
  in 3D, so prepared spherical relate matches the unprepared result (the
  `NaturalIndex` is dimension-generic — no separate STR ordering needed).

Covered by `spherical_end_to_end.jl`: the arc-bulge equivalence case,
AutoAccelerator above threshold, and prepared/unprepared agreement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An edge between exactly-antipodal vertices has no unique great-circle arc
(its endpoint cross product vanishes while the dot product is negative),
so the bulge math in `arc_extent` is undefined. Detect that case at the
ingest chokepoint — every edge's extent is computed through `arc_extent`
during `RelateGeometry` construction — and throw an `ArgumentError` that
names the `AntipodalEdgeSplit` remedy. A vanishing normal with a positive
dot product is a zero-length/repeated vertex and is left alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The opt-in remedy for the antipodal-edge error: a `GeometryCorrection`
that inserts the lon/lat midpoint of every edge whose endpoints map to
exactly-antipodal unit vectors, replacing one ambiguous edge with two
well-defined arcs. Antipodal detection reuses the spherical kernel's
`_spherical_kernel_point`, so it splits exactly the edges `arc_extent`
rejects. Applicable through `fix` or directly on a geometry.

Also record in the design doc that the lon-interval indexed spherical
point locator (Task 16) is skipped — a pure optimization over the
already-correct `rk_point_in_ring` fall-through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `test/methods/relateng/s2_crossings_conformance.jl`, ported from
s2geometry `s2edge_crosser_test.cc` (`TEST(S2, Crossings)` and
`CollinearEdgesThatDontTouch`). Maps S2 `CrossingSign` +1/-1/0 onto
`rk_classify_intersection` `SS_PROPER`/`SS_DISJOINT`/`SS_TOUCH`, asserting the
full mapping on the authoritative exact path and the conservative
never-invent/never-deny contract on the float accelerator. Covers S2's
numerically extreme cases (floating-point underflow, >2000-bit determinants).

`CoincidentZeroLengthEdgesThatDontTouch` and `GetIntersection` are not ported
(documented in the file): the former asserts S2's symbolic-perturbation
convention, which this kernel's exact-on-actual-coordinates model legitimately
diverges from; the latter has no analog since proper-crossing nodes are symbolic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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