Skip to content

[WIP] Prepared geometry, for real this time#428

Draft
asinghvi17 wants to merge 36 commits into
mainfrom
prepared-minimal
Draft

[WIP] Prepared geometry, for real this time#428
asinghvi17 wants to merge 36 commits into
mainfrom
prepared-minimal

Conversation

@asinghvi17

Copy link
Copy Markdown
Member

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

Comment thread src/methods/clipping/clipping_processor.jl Outdated
Comment thread src/methods/clipping/clipping_processor.jl Outdated
Comment thread src/methods/clipping/clipping_processor.jl
Comment thread src/methods/clipping/clipping_processor.jl
Comment thread src/methods/clipping/clipping_processor.jl
Comment thread src/prepared.jl Outdated
Comment thread src/prepared.jl Outdated
Comment thread src/prepared.jl Outdated
Comment thread src/prepared.jl Outdated
Comment thread src/prepared.jl
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>
asinghvi17 and others added 24 commits July 6, 2026 10:22
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>
asinghvi17 and others added 12 commits July 6, 2026 10:23
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>
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