[WIP] Prepared geometry, for real this time#428
Draft
asinghvi17 wants to merge 36 commits into
Draft
Conversation
asinghvi17
commented
Jul 4, 2026
asinghvi17
added a commit
that referenced
this pull request
Jul 4, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asinghvi17
added a commit
that referenced
this pull request
Jul 4, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asinghvi17
added a commit
that referenced
this pull request
Jul 4, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asinghvi17
added a commit
that referenced
this pull request
Jul 4, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `Prepared` wrapper carries a tuple of preparations and a cached extent alongside a geometry, forwarding GeoInterface so it can be passed anywhere a geometry can. `getprep` retrieves preparations by type (abstract or concrete, with a get-or-else form for get-or-create use), and `prepare` builds them. `RingEdgeTrees` is the first preparation: one spatial index over each ring's edge extents, backed by `NaturalIndex`, `STRtree`, or any callable via the `build_edge_tree` hook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the per-edge kernel of the Hao–Sun algorithm from `_point_filled_curve_orientation` and add an indexed variant that visits only edges whose extent touches the rightward ray strip, via a `SpatialTreeInterface.depth_first_search` with that strip as the predicate. `_point_polygon_process` looks up `AbstractRingEdgeTrees` once per call, so point-vs-polygon predicates accelerate whenever the polygon is `Prepared`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each per-child operation (`nchild`, `getchild`, `child_indices_extents`) re-chased `parent_index -> levels -> level -> extents` for every child, and profiles showed that pointer chase dominating tree-query time. Hoisting the child-extent vector once per node makes the generic SpatialTreeInterface queries ~1.7x faster (prepared point-in-polygon: 300 ns -> 178 ns per query on a 65k-vertex polygon) with no interface or layout change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le bulk loading
Vendored and restructured from SortTileRecursiveTree.jl: storage is a flat
vector of per-level extent vectors plus a leaf permutation (like
`NaturalIndex` and JTS `HPRtree`), so `RTree{A, E}` is concrete regardless of
depth, works for extents of any dimensionality, and is fully inferrable. The
bulk-load algorithm is a value — `RTree(STR() | HPR() | Unsorted(), data)` —
where `HPR` sorts by Skilling N-dimensional Hilbert codes and adding an
algorithm is one singleton type plus one `loadorder` method. Implements
SpatialTreeInterface, and a `build_edge_tree` method lets
`RingEdgeTrees(poly; tree = HPR())` select it for prepared geometry.
Benchmarks (recorded in the design doc): on spatially random collections HPR
beats STR by ~20% and unsorted packing by ~1000x; on ring edges natural
order remains fastest, confirming `NaturalIndex` as the prepared-ring default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `benchmarks/prepared_natural_earth.jl` (one scale per run) and records the results: whole-dataset speedups of 5.2x / 10.9x / 19.1x at 110m / 50m / 10m with a flat ~40-58 ns prepared per-query cost, rising to 481x on >10k vertex coastlines; preparation breaks even at ~2-6 queries per polygon Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ared` nodes `prepare` now converts the input into GeometryOps' tuple layout (coordinate number types preserved, `m` dropped) and builds an eager tree of `Prepared` nodes: rings store coordinate-tuple vectors, and every level — ring, polygon, multi-geometry member — is its own `Prepared` node with its own preparations and cached extent. Because the stored children are themselves prepared, preparedness survives GeoInterface decomposition: a prepared MultiPolygon accelerates through the existing polygon seam with no extra code, and any consumer holding a ring can discover its index. The edge index becomes a property of the ring: `EdgeTree`/`AbstractEdgeTree`/ `edge_tree` replace the polygon-level `RingEdgeTrees`, eliminating the parallel-array pairing of holes with hole trees, and the point-in-polygon seam simplifies to a per-ring `getprep`. `EdgeTrees(backend)` selects the tree backend across the whole recursion. `Base.parent` returns the converted geometry; there is no opt-out from materialization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he PIP seam An explicit inner constructor stops the geometry-taking `EdgeTree` outer constructor from overwriting the default one, which made precompilation fail. The point-in-polygon seam now strips the `Prepared` shell before handing rings to the orientation kernels, so the hot loop reads tuple storage without an extra layer of accessor forwarding Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rait GeoJSON types polygon rings as line strings, so the recursion dispatched them to the curve method and they lost their ring defaults — no `EdgeTree` was built and point-in-polygon silently fell back to the sequential loop (correct but up to ~40x slower on large coastline polygons). Polygon children are rings by construction: materialize them with `LinearRingTrait` explicitly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The natural-tree accelerators receive rings, and a ring of a `Prepared` geometry already carries the index they build per call: its `EdgeTree` preparation, whose leaf indices match `eachedge` one-to-one for closed rings. `_edge_tree_and_coords` reuses that tree as-is — any SpatialTreeInterface-compatible backend, with no assumption about traversal order, since candidate indices are now collected and sorted into nested-loop order (as the `SingleSTRtree` path always did) — and reads edge coordinates straight from ring storage through the same `_tuple_point` conversion as `eachedge`. Unclosed rings fall back to the ephemeral build. `AutoAccelerator` picks a tree path whenever an input ring is prepared, resolving its "respect existing spatial indices" TODO. Also normalize input holes through `tuples`/`_linearring` in the union hole assembly, matching `_add_holes_to_polys!` — pushing raw input rings into output polygons broke for any non-plain wrapper type, `Prepared` included Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`eachedge` numbers a geometry's edges as the concatenation of per-curve `eachedge` numberings, so the natural-tree accelerators can decompose any geometry into per-curve (tree, coords, offset) parts (`_edge_parts`) and merge candidate queries across curves through the same sort that already handles arbitrary traversal orders. Whole polygons and multipolygons — as `intersection_points` passes — now reuse prepared edge trees and prune curve pairs by extent, resolving that function's acceleration TODO; curve inputs keep the existing static fast path. `prepare` now builds `EdgeTree`s for line strings too. Their trees index exactly the `eachedge` pairs (no implicit closing edge), so unlike ring trees they are reusable even when unclosed; the point-in-polygon seam correspondingly only uses a tree for filled-curve orientation when the curve is ring-trait or explicitly closed. Add `hasprep`, the node-level boolean companion to `getprep`, and use it for the `AutoAccelerator` prep check — whole geometries fall through to the size heuristic, whose tree paths reuse prepared trees regardless Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unify the tree accelerators over per-curve edge parts: `_edge_parts` returns a concretely-typed 1-tuple for curve inputs, so the parts loops serve curves and whole geometries alike and `_single_tree_loop` / `_dual_tree_loop` are deleted. Inline the single-use `_prep_matches_eachedge`, merge `_ring_edge_extents` and `_line_edge_extents` into `_edge_extents`, collapse `_dual_pairs_loop`'s duplicate tail branches, drop dead locals and stale TODOs, and condense comments and docstrings throughout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore the comments the condense pass cut from the clipping processor (pre-allocation, sorted-query rationale, LoopStateMachine control flow, the nested-loop equivalence block in `_build_a_list`, the SingleSTRtree middle-ground note) and from `_intersection_points`. Rewrite the story-register comments to describe behavior: the FlexibleRTrees module header, `RTreeNode` (now a docstring), and the NaturalIndexing child-extents note. Apply the suggested accelerator docstring bullets, iteration-order wording, and `where A` in the `RTree` constructor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Materialization now closes linear rings (the first point is repeated at the end when the input ring was open) and preserves points already in a native representation — a curve of `UnitSphericalPoint`s keeps them instead of destructuring to coordinate tuples. `prepare` takes the manifold as its first argument like other GeometryOps functions, and it flows through `default_preparations`, `buildprep`, and `build_edge_tree`; edge trees are a `Planar()` default, creating the seam spherical preparations plug into. Consumers stop re-checking what materialization now guarantees: `_point_ring_orientation`'s per-call ring-closure check is gone (the call sites pick the indexed or sequential walk from `_ring_edge_tree` alone), since any preparation retrieved from a `Prepared` node was built against closed materialized storage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the implicit `hasprep` knowledge in the accelerator layer with an explicit vocabulary: a `TreeAccelerator(a, b)` states, per side, whether to reuse a prepared edge tree (`ReuseTree`), build an ephemeral one (`BuildTree(backend)`), or iterate without one (`IterateEdges`, side `a` only). `SingleNaturalTree()` and `DoubleNaturalTree()` remain as constructors for the common combinations, so existing call sites and tests keep working. `AutoAccelerator` becomes a documented decision table over the same vocabulary: every tree side uses `ReuseTree`, so prepared trees are reused whichever branch is taken — including the previously undefined both-prepared case, which now resolves to a dual traversal that reuses both trees and builds nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answer the review's structural questions in the code: one coordinate accessor instead of two (the ephemeral path now materializes the curve into tuple storage and reads coordinates in place like the reuse path, so `_EdgeListCoords` and the `to_edgelist` detour are gone), domain names (`_EdgePart` → `_CurveTree`, `_PartsCoords` → `_ConcatCoords`, `_single_parts_loop`/`_dual_pairs_loop` → `_single_tree_loop`/ `_dual_tree_loop`), and named capture-free callables (`_OffsetPush`, `_OffsetPairPush`) instead of offset-capturing closures in the traversal callbacks. The loop functions stay separate from their accelerator methods on purpose — each is the function barrier where the per-curve-tree container types become concrete — and now say so. The multi-curve decomposition builds a typed vector directly instead of `Any[]` + `map(identity)`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the value-level `_first_prep` tuple recursion with a `@generated` lookup over the preps type-tuple: the generated body is a constant field access (or `nothing`), so there is provably no runtime search, and hits and misses both infer concretely — asserted with `@inferred` tests for concrete, abstract-kind, and root-abstract queries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… selector One name instead of two: applying the `EdgeTree` constructor to a backend (instead of a geometry) curries it into a spec for `prepare`'s `preps` tuple. Specs now declare where they apply through the overloadable `appliesto(spec, trait, istop)` — the top node only by default, every curve for `EdgeTree` bare or curried — and nodes where no given spec applies still get `default_preparations`, so `preps = (MyPrep,)` keeps meaning "custom prep on the geometry I called `prepare` on". The `EdgeTrees` selector struct is deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…spec Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`IterateEdges`, `BuildTree`, and `ReuseTree` now subtype `TreePolicy`, `TreeAccelerator`'s sides and `ReuseTree`'s fallback are constrained to it, so a non-policy argument fails at construction with a method error instead of propagating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tree loops no longer adapt to arbitrary input geometries — they ingest at the traversal entry instead: `Prepared` inputs pass straight through and their trees are reused, anything else is prepared ephemerally with the side's backend. `BuildTree(backend)` becomes the one tree policy (reuse when prepared, build otherwise), so `ReuseTree`, `_CurveCoords`, `_ConcatCoords`, `_edge_tree_and_coords`, and `_extents_tree` are deleted and `_CurveTree` flattens to (tree, storage, offset, n, extent) read by a plain `_edge_coords`. What remains is the irreducible part of tree traversal: random access to edge coordinates by index, and the offset routing that lets per-ring trees serve geometry-global `eachedge` numbering. Unprepared inputs on tree paths now pay a full `prepare` per call (setup roughly doubles on the donut×blob micro-benchmark; prepared paths unchanged) — the answer to "make this fast" is `prepare`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tree sides no longer silently prepare unprepared geometry. By default `BuildTree` indexes a raw input as it is: an ephemeral tree over its `eachedge` extents (not `_edge_extents`, whose closing-edge wrap would desync the shared numbering on unclosed rings), with coordinates read from the input's own storage in place — geometries with expensive point access pay per read, and the answer is still `prepare`. Passing `prepare = true` (on `BuildTree`, `AutoAccelerator`, `SingleNaturalTree`, or `DoubleNaturalTree`) instead runs a full ephemeral `prepare` on such inputs at the traversal entry. `Prepared` inputs pass through and reuse their trees either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`ARCHITECTURE.md` now holds package structure, core abstractions, design principles, and implementation patterns; `AGENTS.md` focuses on how to approach design and implementation, referencing `ARCHITECTURE.md` Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design guidance now reads as defaults with reasons rather than commandments: new machinery is a flagged last resort instead of a blocked one, single-use helpers can earn their place, and the numeric comment limits are replaced by pointing exposition to the literate header, docstrings, and commit messages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
756af56 to
e594917
Compare
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.
Prepared geometry wrappers, recursive, with type based keys to extract preparations. Only planar for now but will also try to wire spherical in.
The main preparation right now is an edge list spatial tree, can be either a natural tree or some form of R-tree, which we have an internal, flattened implementation for.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com