Skip to content

Commit 4e49e89

Browse files
committed
docs: proper README with LearnerND integration guide and example scripts
1 parent 7c7a8b7 commit 4e49e89

File tree

4 files changed

+280
-96
lines changed

4 files changed

+280
-96
lines changed

README.md

Lines changed: 102 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,130 @@
11
# adaptive-triangulation
22

3-
Fast N-dimensional Delaunay triangulation in Rust with Python bindings.
3+
[![CI](https://github.com/python-adaptive/adaptive-triangulation/actions/workflows/ci.yml/badge.svg)](https://github.com/python-adaptive/adaptive-triangulation/actions)
4+
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
45

5-
[![PyPI](https://img.shields.io/pypi/v/adaptive-triangulation)](https://pypi.org/project/adaptive-triangulation/)
6-
[![Python](https://img.shields.io/pypi/pyversions/adaptive-triangulation)](https://pypi.org/project/adaptive-triangulation/)
7-
[![License](https://img.shields.io/github/license/python-adaptive/adaptive-triangulation)](LICENSE)
8-
[![CI](https://img.shields.io/github/actions/workflow/status/python-adaptive/adaptive-triangulation/ci.yml)](https://github.com/python-adaptive/adaptive-triangulation/actions)
9-
[![Downloads](https://img.shields.io/pypi/dm/adaptive-triangulation)](https://pypi.org/project/adaptive-triangulation/)
6+
Fast N-dimensional Delaunay triangulation in Rust with Python bindings (PyO3).
7+
Drop-in replacement for [adaptive](https://github.com/python-adaptive/adaptive)'s `Triangulation` class — **5-99× faster**.
108

11-
`adaptive-triangulation` is a Rust/PyO3 implementation of the incremental Bowyer-Watson
12-
algorithm for Delaunay triangulation. It is designed as a fast drop-in triangulation backend for
13-
`adaptive`, while remaining useful as a standalone computational geometry package for 2D, 3D, and
14-
higher-dimensional point sets.
9+
## Performance
10+
11+
### Standalone triangulation (incremental insertion)
12+
| Case | Rust | Python | Speedup |
13+
|---|---:|---:|---:|
14+
| 2D, 1K pts | 38.5 ms | 668 ms | **17×** |
15+
| 2D, 5K pts | 260 ms | 8,547 ms | **33×** |
16+
| 3D, 500 pts | 133 ms | 5,571 ms | **42×** |
17+
18+
### LearnerND integration (end-to-end, `ring_of_fire` 2D)
19+
| N pts | Learner2D (scipy) | LearnerND (Python) | LearnerND (Rust) |
20+
|---|---:|---:|---:|
21+
| 1,000 | 0.34 s | 0.91 s | **0.23 s** |
22+
| 2,000 | 1.17 s | 1.80 s | **0.38 s** |
23+
| 5,000 | 6.99 s | 4.57 s | **0.99 s** |
24+
25+
LearnerND + Rust is **5× faster** than LearnerND + Python, and **7× faster** than Learner2D at 5K points.
1526

1627
## Installation
1728

1829
```bash
1930
pip install adaptive-triangulation
2031
```
2132

22-
## Quick Start
33+
Requires a Rust toolchain for building from source. Pre-built wheels are available for common platforms via CI.
34+
35+
## Quick start
2336

2437
```python
25-
import adaptive_triangulation as at
26-
import numpy as np
27-
28-
# 2D triangulation
29-
points = [[0, 0], [1, 0], [0, 1], [1, 1], [0.5, 0.5]]
30-
tri = at.Triangulation(points)
31-
print(f"Simplices: {tri.simplices}")
32-
33-
# Incremental point insertion
34-
deleted, added = tri.add_point((0.25, 0.75))
35-
print(f"Deleted simplices: {deleted}")
36-
print(f"Added simplices: {added}")
37-
38-
# Circumsphere queries
39-
for simplex in tri.simplices:
40-
center, radius = tri.circumscribed_circle(simplex)
41-
print(simplex, center, radius)
42-
43-
# Works in any dimension
44-
points_3d = np.random.rand(10, 3).tolist()
45-
tri3d = at.Triangulation(points_3d)
46-
print(f"3D simplices: {len(tri3d.simplices)}")
38+
from adaptive_triangulation import Triangulation
39+
40+
# Build a 2D triangulation
41+
tri = Triangulation([(0, 0), (1, 0), (0, 1), (1, 1)])
42+
43+
# Insert points incrementally (Bowyer-Watson)
44+
deleted, added = tri.add_point((0.5, 0.5))
45+
46+
# Query properties
47+
print(len(tri.simplices)) # number of triangles
48+
print(tri.dim) # 2
49+
print(tri.reference_invariant()) # True
4750
```
4851

49-
Standalone geometry helpers are also available:
52+
## Usage with adaptive's LearnerND
53+
54+
This is a drop-in replacement for `adaptive`'s built-in triangulation.
55+
Monkey-patch the module to use Rust triangulation everywhere:
5056

5157
```python
5258
import adaptive_triangulation as at
59+
from adaptive.learner import learnerND as lnd_mod
60+
from adaptive.learner.learnerND import LearnerND
61+
62+
# Replace both the class and standalone functions
63+
lnd_mod.Triangulation = at.Triangulation
64+
lnd_mod.circumsphere = at.circumsphere
65+
lnd_mod.simplex_volume_in_embedding = at.simplex_volume_in_embedding
66+
lnd_mod.point_in_simplex = at.point_in_simplex
5367

54-
triangle = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]
55-
center, radius = at.circumsphere(triangle)
56-
inside = at.point_in_simplex([0.25, 0.25], triangle)
57-
area = at.volume(triangle)
68+
# Now use LearnerND as normal — it's 5× faster
69+
learner = LearnerND(my_function, bounds=[(-1, 1), (-1, 1)])
5870
```
5971

60-
## Performance
72+
See [`examples/adaptive_learnernd.py`](examples/adaptive_learnernd.py) for a full working example with timing comparison.
73+
74+
## API
6175

62-
The table below compares release-mode `adaptive-triangulation` against the Python reference
63-
implementation used in the test suite on the same machine. Each case builds a triangulation from a
64-
minimal seed simplex and inserts the remaining points incrementally.
65-
66-
| Case | Rust (`maturin develop --release`) | Python reference | Speedup |
67-
| --- | ---: | ---: | ---: |
68-
| 2D, 1,000 points | 38.5 ms | 668.1 ms | 17.4x |
69-
| 2D, 5,000 points | 259.8 ms | 8547.2 ms | 32.9x |
70-
| 3D, 500 points | 133.4 ms | 5570.9 ms | 41.8x |
71-
72-
Absolute timings will vary by machine, but the release build consistently outperforms the pure
73-
Python reference by a wide margin on construction and incremental insertion workloads.
74-
75-
## API Reference
76-
77-
### Module attributes
78-
79-
- `__version__`: Package version sourced from `Cargo.toml`.
80-
81-
### `Triangulation`
82-
83-
- `Triangulation(coords)`: Build an N-dimensional Delaunay triangulation from points in general position.
84-
- `add_point(point, simplex=None, transform=None)`: Insert one point and return `(deleted_simplices, added_simplices)`.
85-
- `add_simplex(simplex)`: Insert a simplex directly into the triangulation state.
86-
- `delete_simplex(simplex)`: Remove a simplex from the triangulation state.
87-
- `locate_point(point)`: Return the simplex containing a query point, or an empty tuple when none is found.
88-
- `get_reduced_simplex(point, simplex, eps=1e-8)`: Reduce a simplex to the smallest face containing the point.
89-
- `circumscribed_circle(simplex, transform=None)`: Compute the circumsphere center and radius for a simplex.
90-
- `point_in_circumcircle(pt_index, simplex, transform=None)`: Check whether a stored vertex lies inside a simplex circumsphere.
91-
- `point_in_cicumcircle(pt_index, simplex, transform=None)`: Legacy alias for `point_in_circumcircle`.
92-
- `point_in_simplex(point, simplex, eps=1e-8)`: Test whether a point lies inside a simplex.
93-
- `volume(simplex)`: Compute the volume of one simplex by vertex index.
94-
- `volumes()`: Return the volumes of all simplices in the triangulation.
95-
- `faces(dim=None, simplices=None, vertices=None)`: Iterate over faces filtered by dimension, simplices, or vertices.
96-
- `containing(face)`: Return the simplices that contain a face.
97-
- `get_vertices(indices)`: Fetch vertex coordinates for a sequence of vertex indices.
98-
- `bowyer_watson(pt_index, containing_simplex=None, transform=None)`: Run one Bowyer-Watson insertion step for an existing vertex.
99-
- `reference_invariant()`: Check internal consistency against the reference invariants used by the tests.
100-
- `vertex_invariant(vertex)`: Compatibility placeholder that currently raises `NotImplementedError`.
101-
- `convex_invariant(vertex)`: Compatibility placeholder that currently raises `NotImplementedError`.
102-
- `vertices`: Vertex coordinates stored by the triangulation.
103-
- `simplices`: Set of simplices represented as tuples of vertex indices.
104-
- `vertex_to_simplices`: Reverse map from vertex index to incident simplices.
105-
- `hull`: Set of vertex indices on the convex hull.
106-
- `dim`: Spatial dimension of the triangulation.
107-
- `default_transform`: Identity metric transform for the current dimension.
76+
### `Triangulation` class
77+
78+
```python
79+
tri = Triangulation(coords) # Build from initial points
80+
tri.add_point(point) # Incremental insertion → (deleted, added)
81+
tri.locate_point(point) # Find containing simplex
82+
tri.circumscribed_circle(simplex) # → (center, radius)
83+
tri.volume(simplex) # Simplex volume
84+
tri.volumes() # All simplex volumes
85+
tri.point_in_simplex(point, simplex) # Containment test
86+
tri.point_in_circumcircle(pt, simplex) # Circumcircle test
87+
tri.bowyer_watson(pt_index) # Direct Bowyer-Watson
88+
tri.reference_invariant() # Consistency check
89+
```
90+
91+
**Properties:** `vertices`, `simplices`, `vertex_to_simplices`, `hull`, `dim`, `default_transform`
10892

10993
### Standalone functions
11094

111-
- `circumsphere(points)`: Compute the circumsphere of a simplex given explicit coordinates.
112-
- `fast_2d_circumcircle(points)`: Fast circumcircle routine specialized for three 2D points.
113-
- `fast_3d_circumsphere(points)`: Fast circumsphere routine specialized for four 3D points.
114-
- `fast_3d_circumcircle(points)`: Alias for the 3D circumsphere helper.
115-
- `point_in_simplex(point, simplex, eps=1e-8)`: Generic point-in-simplex predicate.
116-
- `fast_2d_point_in_simplex(point, simplex, eps=1e-8)`: Fast 2D point-in-triangle predicate.
117-
- `volume(simplex)`: Compute the volume of a simplex from coordinates.
118-
- `simplex_volume_in_embedding(vertices)`: Compute simplex volume in a higher-dimensional embedding space.
119-
- `orientation(face, origin)`: Return the orientation of a face relative to an origin point.
120-
- `fast_norm(point)`: Compute the Euclidean norm of a point.
95+
```python
96+
from adaptive_triangulation import (
97+
circumsphere, # General circumsphere
98+
fast_2d_circumcircle, # Optimized 2D
99+
fast_3d_circumsphere, # Optimized 3D
100+
point_in_simplex, # Containment test
101+
volume, # Simplex volume
102+
simplex_volume_in_embedding, # Volume in embedding space
103+
orientation, # Face orientation
104+
)
105+
```
106+
107+
## Examples
108+
109+
- [`examples/basic_usage.py`](examples/basic_usage.py) — Core API walkthrough
110+
- [`examples/adaptive_learnernd.py`](examples/adaptive_learnernd.py) — LearnerND integration with timing
111+
- [`examples/benchmark_vs_python.py`](examples/benchmark_vs_python.py) — Standalone benchmarks across dimensions
112+
113+
## Development
114+
115+
```bash
116+
# Build (requires Rust toolchain)
117+
pip install maturin
118+
maturin develop --release
119+
120+
# Tests
121+
cargo test # Rust tests
122+
python -m pytest tests/ -v # Python tests
123+
124+
# Linting
125+
pre-commit run --all-files # ruff, mypy, cargo fmt, cargo clippy
126+
```
121127

122128
## License
123129

124-
BSD-3-Clause, matching the `adaptive` project. See [LICENSE](LICENSE).
130+
BSD-3-Clause

examples/adaptive_learnernd.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Using adaptive-triangulation with adaptive's LearnerND.
2+
3+
Drop-in replacement for adaptive's built-in Triangulation class,
4+
providing 5× speedup for LearnerND and 7× vs Learner2D at 5K points.
5+
6+
Requirements:
7+
pip install adaptive adaptive-triangulation
8+
"""
9+
10+
import time
11+
12+
import numpy as np
13+
14+
import adaptive_triangulation as at
15+
from adaptive.learner import learnerND as lnd_mod
16+
from adaptive.learner.learnerND import LearnerND
17+
18+
19+
def ring_of_fire(xy: tuple[float, float]) -> float:
20+
"""A 2D function with a ring-shaped feature — good test for adaptive sampling."""
21+
x, y = xy
22+
a, d = 0.2, 0.5
23+
return x + np.exp(-((x**2 + y**2 - d**2) ** 2) / a**4)
24+
25+
26+
bounds = [(-1, 1), (-1, 1)]
27+
n_points = 2000
28+
29+
# --- Baseline: pure Python triangulation ---
30+
t0 = time.perf_counter()
31+
learner_py = LearnerND(ring_of_fire, bounds=bounds)
32+
for _ in range(n_points):
33+
points, _ = learner_py.ask(1)
34+
for p in points:
35+
learner_py.tell(p, ring_of_fire(p))
36+
t_python = time.perf_counter() - t0
37+
print(f"Python triangulation: {t_python:.2f}s ({n_points} points)")
38+
39+
# --- Rust triangulation: monkey-patch the module ---
40+
# Replace both the Triangulation class AND the standalone geometry functions
41+
lnd_mod.Triangulation = at.Triangulation
42+
lnd_mod.circumsphere = at.circumsphere
43+
lnd_mod.simplex_volume_in_embedding = at.simplex_volume_in_embedding
44+
lnd_mod.point_in_simplex = at.point_in_simplex
45+
46+
t0 = time.perf_counter()
47+
learner_rust = LearnerND(ring_of_fire, bounds=bounds)
48+
for _ in range(n_points):
49+
points, _ = learner_rust.ask(1)
50+
for p in points:
51+
learner_rust.tell(p, ring_of_fire(p))
52+
t_rust = time.perf_counter() - t0
53+
print(f"Rust triangulation: {t_rust:.2f}s ({n_points} points)")
54+
print(f"Speedup: {t_python / t_rust:.1f}×")

examples/basic_usage.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Basic usage of adaptive-triangulation.
2+
3+
Demonstrates core Triangulation API: construction, point insertion,
4+
simplex queries, and geometry computations.
5+
"""
6+
7+
import numpy as np
8+
9+
from adaptive_triangulation import Triangulation, circumsphere, volume
10+
11+
# Create a 2D triangulation from initial points
12+
points = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)]
13+
tri = Triangulation(points)
14+
15+
print(f"Vertices: {len(tri.vertices)}")
16+
print(f"Simplices: {len(tri.simplices)}")
17+
print(f"Dimension: {tri.dim}")
18+
19+
# Add a point incrementally (Bowyer-Watson insertion)
20+
deleted, added = tri.add_point((0.5, 0.5))
21+
print(f"\nAfter adding (0.5, 0.5):")
22+
print(f" Deleted simplices: {len(deleted)}")
23+
print(f" Added simplices: {len(added)}")
24+
print(f" Total simplices: {len(tri.simplices)}")
25+
26+
# Query geometry
27+
for simplex in tri.simplices:
28+
verts = tri.get_vertices(simplex)
29+
center, radius = tri.circumscribed_circle(simplex)
30+
vol = tri.volume(simplex)
31+
print(f" Simplex {simplex}: volume={vol:.4f}, circumradius={radius:.4f}")
32+
33+
# Standalone functions work on raw point arrays
34+
pts = np.array([[0.0, 0.0], [1.0, 0.0], [0.5, 0.866]])
35+
center, radius = circumsphere(pts)
36+
vol = volume(pts)
37+
print(f"\nStandalone: circumcenter={center}, radius={radius:.4f}, volume={vol:.4f}")
38+
39+
# Locate which simplex contains a query point
40+
containing = tri.locate_point((0.3, 0.3))
41+
print(f"\nPoint (0.3, 0.3) is in simplex: {containing}")
42+
43+
# Check invariants
44+
assert tri.reference_invariant(), "Triangulation invariant violated!"
45+
print("\nAll invariants passed ✓")

0 commit comments

Comments
 (0)