Skip to content

Complete the drop-in API for neighbor-aware losses; numpy fast paths in the PyO3 layer#7

Merged
basnijholt merged 3 commits into
mainfrom
pyo3-surface
Jun 10, 2026
Merged

Complete the drop-in API for neighbor-aware losses; numpy fast paths in the PyO3 layer#7
basnijholt merged 3 commits into
mainfrom
pyo3-surface

Conversation

@basnijholt

Copy link
Copy Markdown
Member

Summary

Two independent improvements to the Python-facing layer.

1. Drop-in API completeness (found by integration testing, not review)

LearnerND with a neighbor-aware loss (triangle_loss / curvature_loss_function(), i.e. nth_neighbors >= 1) crashes with the current release: it calls five Triangulation methods we never implemented, and relies on two behaviors only that code path exercises. This PR adds:

  • get_vertex, get_neighbors_from_vertices, get_simplices_attached_to_points, get_opposing_vertices, get_face_sharing_neighbors — same semantics and exception types as the reference; get_opposing_vertices and get_simplices_attached_to_points are answered natively from the facet/vertex indexes (single lookups) instead of the reference's Python set scans
  • get_vertices passes None entries through as None (adaptive feeds the result of get_opposing_vertices, which contains None on hull facets, straight back into it)
  • SimplicesProxy supports binary set operators in both operand orders (adaptive computes ... - tri.simplices), delegating to a snapshot set

Pinned by cross-validation tests against the reference on random 2D/3D triangulations, exception-type parity, and an end-to-end LearnerND run with curvature_loss_function() driving 100 points through the previously uncovered path.

2. numpy fast paths

  • parse_point / parse_points bulk-copy f64 numpy arrays instead of iterating them as Python objects (other dtypes and nested sequences keep the generic path)
  • VerticesProxy.__array__ builds a PyArray2 directly instead of round-tripping through a list of Python tuples and numpy.array

Output types are unchanged everywhere (tuples/lists/sets), so the drop-in contract is untouched.

Benchmarks (A/B vs main, best of 5)

operation main this PR
np.asarray(tri.vertices) ×2000, 500-vertex mesh 0.189 s 0.004 s (50×)
constructor from numpy array, 2000×2D 0.0089 s 0.0077 s
add_point with numpy rows (4D, 600 pts) 0.410 s 0.400 s
macro insertion benchmarks (2D/3D/4D, hull-heavy) unchanged

examples/adaptive_learnernd.py: 3.6× end-to-end (unchanged).

Verification

  • pytest: 112 passed (incl. new cross-validation + LearnerND integration tests)
  • cargo test --lib --tests: 18 passed; pre-commit clean

LearnerND with a neighbor-aware loss (triangle_loss / curvature_loss,
nth_neighbors >= 1) calls five Triangulation methods that were missing,
plus two behaviors only that path exercises. Add them:

- get_vertex, get_neighbors_from_vertices,
  get_simplices_attached_to_points, get_opposing_vertices,
  get_face_sharing_neighbors - same semantics and exception types as
  the reference; get_opposing_vertices and
  get_simplices_attached_to_points are answered natively from the
  facet/vertex indexes instead of the reference's set scans
- get_vertices passes None entries through as None (adaptive feeds the
  result of get_opposing_vertices, which contains None on the hull,
  straight back in)
- SimplicesProxy supports the binary set operators in both operand
  orders (adaptive computes `... - tri.simplices`), delegating to a
  snapshot set

Pinned by cross-validation tests against the reference on random 2D/3D
triangulations, exception-type parity for unknown simplices, and an
end-to-end LearnerND run with curvature_loss_function() driving 100
points through the previously uncovered code path.
- parse_point / parse_points bulk-copy f64 numpy arrays instead of
  iterating them as Python objects (other dtypes and nested sequences
  keep the generic path)
- VerticesProxy.__array__ builds a PyArray2 directly instead of
  round-tripping through a list of Python tuples and numpy.array;
  dtype is honored via astype, and copy needs no handling because the
  snapshot is always freshly allocated

Output types are unchanged everywhere (tuples/lists/sets), so the
drop-in contract is untouched; all 112 tests pass. A/B microbenchmark
vs main (best of 5): np.asarray(tri.vertices) snapshots 50x faster
(0.189s -> 0.004s per 2000 calls on a 500-vertex mesh), constructor
from a numpy array ~14% faster, add_point unchanged.
@basnijholt basnijholt merged commit 4b44e56 into main Jun 10, 2026
17 checks passed
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