Skip to content

Move the coastline signed distance into Rust - #11

Open
lispandfound wants to merge 1 commit into
csv-formatfrom
coastline-rust
Open

lispandfound wants to merge 1 commit into
csv-formatfrom
coastline-rust

Conversation

@lispandfound

@lispandfound lispandfound commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

The coastline layer computed its signed distance in three steps: shapely's
STRtree for the unsigned distance, a second STRtree query to collect the
segments a horizontal ray might cross, and a numba kernel to count the
crossings. Per chunk that allocated one shapely Point and one LineString
for every grid column, which is what the layer's own TODO complained about.

src/coastline.rs does the whole thing over one 2-D BVH: nearest_to for the
magnitude, and an AABB traversal along the ray for the sign. The Python layer
drops from 178 lines to 82 and stops building geometry objects per chunk.

Measured against what it replaces

Over the synthetic coastline, comparing directly against the old
implementation checked out of git:

points old new
1,000 7.1 ms 0.2 ms 34x
20,000 145.8 ms 2.0 ms 72x
200,000 1451.4 ms 15.5 ms 94x

Two differences worth naming

Both come from Real being float32 where shapely worked in float64.

The magnitude moves by a few float32 steps. Over 40,000 points against
shapely the median error is 0.48 steps and the largest 3.5, where one step is
0.125 m at NZTM eastings. A coastline distance feeds a taper running over
kilometres, so it does not reach the output. --features high_precision
removes it for anyone who disagrees.

The sign can differ within the resolution of the coordinate. One point in
200,000 flipped. It sat 0.012 m from the coastline, an order of magnitude
inside the 0.125 m float32 resolution at that easting, so there is no fact of
the matter about which side it falls on. Shapely agreed with the old answer;
at that distance both are guesses.

Testing

just synthetic
uv run --dev pytest tests/          # 288 passed, 1 skipped
cargo test && cargo test --features high_precision
cargo clippy --all-targets -- -D warnings
uv run ruff check nzcvm/ tests/ && uv run ty check && uv run deptry .

tests/test_coastline.py checks both halves against shapely as an independent
oracle, with the tolerance derived from that float32 resolution rather than
guessed, and a median assertion that a wrong nearest segment would fail. The
Rust tests use the unit square, whose signed distance is known in closed form,
and cover the tie-break where a ray passes exactly through a shared vertex
(which naive parity counting gets wrong, reporting an interior point as
exterior).

--distributed still works

The Rust index has no Python representation, and --distributed pickles the
layer chain even with thread workers, so the first version of this broke that
path with cannot pickle 'nzcvm.PyCoastline'. CoastlineLayer now hands
dask a registry key through the same __getstate__/__setstate__ pattern
Surface and ModelTree use. Verified: the distributed run on
examples/synthetic.toml matches the pre-#11 distributed output element for
element. Replacing the registry itself is a separate, larger piece of work.

The terminal layer these tests need moved to tests/conftest.py, since
test_layer_coordinates.py had already grown its own copy.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com


Stack created with GitHub Stacks CLIGive Feedback 💬

@lispandfound
lispandfound added this pull request to stack #9 September 11, 2026 11:42
@github-actions

Copy link
Copy Markdown

Benchmark for a2ace4c

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 294.6±2.84ns 294.8±2.45ns +0.07%
Mesh_Point_Query/far_corner/4096000 211.9±2.29ns 210.5±5.65ns -0.66%
Mesh_Point_Query/near_origin/4096000 283.7±3.29ns 286.9±2.24ns +1.13%

@github-actions

Copy link
Copy Markdown

Benchmark for 0af3828

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 244.2±0.49ns 244.1±0.57ns -0.04%
Mesh_Point_Query/far_corner/4096000 231.6±0.46ns 231.6±1.09ns 0.00%
Mesh_Point_Query/near_origin/4096000 284.1±1.70ns 280.1±2.24ns -1.41%

@github-actions

Copy link
Copy Markdown

Benchmark for 11a4434

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 302.4±2.40ns 316.4±7.70ns +4.63%
Mesh_Point_Query/far_corner/4096000 201.2±4.15ns 221.8±6.61ns +10.24%
Mesh_Point_Query/near_origin/4096000 346.4±1.67ns 349.3±6.27ns +0.84%

@github-actions

Copy link
Copy Markdown

Benchmark for ff97bf8

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 210.4±3.17ns 211.9±3.74ns +0.71%
Mesh_Point_Query/far_corner/4096000 154.4±6.22ns 155.0±4.14ns +0.39%
Mesh_Point_Query/near_origin/4096000 205.3±6.26ns 207.4±9.91ns +1.02%

@github-actions

Copy link
Copy Markdown

Benchmark for ff97bf8

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 302.7±1.87ns 303.4±2.50ns +0.23%
Mesh_Point_Query/far_corner/4096000 201.6±3.32ns 202.0±6.16ns +0.20%
Mesh_Point_Query/near_origin/4096000 354.0±2.19ns 345.9±1.79ns -2.29%

@github-actions

Copy link
Copy Markdown

Benchmark for 49ef9a3

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 304.7±10.89ns 314.7±2.61ns +3.28%
Mesh_Point_Query/far_corner/4096000 202.0±5.68ns 223.5±9.92ns +10.64%
Mesh_Point_Query/near_origin/4096000 347.9±1.68ns 350.1±2.41ns +0.63%

@github-actions

Copy link
Copy Markdown

Benchmark for 49ef9a3

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 310.9±2.32ns 315.8±1.48ns +1.58%
Mesh_Point_Query/far_corner/4096000 202.5±6.52ns 202.0±3.98ns -0.25%
Mesh_Point_Query/near_origin/4096000 349.9±6.28ns 347.3±1.83ns -0.74%

@lispandfound
lispandfound removed this pull request from stack #9 September 20, 2026 21:55
@lispandfound
lispandfound added this pull request to stack #15 September 20, 2026 21:56
@github-actions

Copy link
Copy Markdown

Benchmark for 6d3873c

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 315.7±1.92ns 303.4±3.16ns -3.90%
Mesh_Point_Query/far_corner/4096000 216.7±7.40ns 200.8±4.84ns -7.34%
Mesh_Point_Query/near_origin/4096000 348.9±2.08ns 346.9±1.52ns -0.57%

@github-actions

Copy link
Copy Markdown

Benchmark for 6d3873c

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 314.8±2.07ns 302.5±2.50ns -3.91%
Mesh_Point_Query/far_corner/4096000 230.4±7.54ns 200.6±3.71ns -12.93%
Mesh_Point_Query/near_origin/4096000 348.6±1.78ns 347.8±1.77ns -0.23%

The coastline layer computed its signed distance in three steps: shapely's
STRtree for the unsigned distance, a second STRtree query to collect the
segments a horizontal ray might cross, and a numba kernel to count the
crossings. Per chunk that allocated one shapely Point and one LineString for
every grid column, which is what the layer's own TODO complained about.

`src/coastline.rs` does the whole thing over one 2-D BVH: `nearest_to` for
the magnitude, and an AABB traversal along the ray for the sign. The Python
layer drops from 178 lines to 82 and stops building geometry objects
per chunk.

Measured against the implementation it replaces, over the synthetic
coastline:

    n=   1000   old     7.1 ms   new   0.2 ms    34x
    n=  20000   old   145.8 ms   new   2.0 ms    72x
    n= 200000   old  1451.4 ms   new  15.5 ms    94x

Two differences worth naming, both from `Real` being float32 where shapely
worked in float64.

The magnitude moves by a few float32 steps. Over 40,000 points against
shapely the median error is 0.48 steps and the largest 3.5, where one step is
0.125 m at NZTM magnitudes. A coastline distance feeds a taper that runs over
kilometres, so this does not reach the output; `--features high_precision`
removes it for anyone who disagrees.

The sign can differ for a point closer to the coast than float32 can
represent. One point in 200,000 flipped, and it sat 0.012 m from the
coastline, an order of magnitude inside the 0.125 m resolution at that
easting. There is no fact of the matter about which side it falls on.

`tests/test_coastline.py` checks both halves against shapely as an
independent oracle, with the tolerance derived from that resolution and a
median assertion that a wrong nearest segment would fail. The Rust tests use
the unit square, whose signed distance is known in closed form, and cover the
tie-break where a ray passes exactly through a shared vertex.

The terminal layer the tests need moves to `tests/conftest.py`, since
`test_layer_coordinates.py` had already grown its own copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Benchmark for c41290f

Click to view benchmark
Test Base PR %
Mesh_Point_Query/centre/4096000 310.1±6.01ns 313.3±2.36ns +1.03%
Mesh_Point_Query/far_corner/4096000 202.0±3.44ns 200.5±3.00ns -0.74%
Mesh_Point_Query/near_origin/4096000 345.6±1.68ns 348.5±2.48ns +0.84%

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