A scattered coordinate set does not show which samples share a local face. Delaunay triangulation turns those coordinates into a non-overlapping simplex mesh for spatial lookup, interpolation, and mesh preparation.
SciPy's scipy.spatial.Delaunay accepts a floating-point NumPy array and delegates tessellation to Qhull. Its simplices rows contain indexes into the original point array; each row has three counterclockwise vertex indexes for two-dimensional input.
The five input points include a rectangular boundary and one interior sample, so the triangle areas can be checked against the boundary area. Two inside queries and one outside query also expose the -1 result that must be guarded before simplices is indexed.
Steps to create a Delaunay triangulation with SciPy:
- Create delaunay_demo.py with the SciPy import and two-dimensional point array.
$ cat > delaunay_demo.py <<'PY' import numpy as np from scipy.spatial import Delaunay points = np.array([ [0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0], [0.9, 0.7], ]) PYThe first four rows form the boundary, and the last row adds an interior vertex.
- Append the triangulation and stable vertex-index report to delaunay_demo.py.
$ cat >> delaunay_demo.py <<'PY' triangulation = Delaunay(points) triangles = points[triangulation.simplices] vertex_sets = sorted( sorted(int(index) for index in simplex) for simplex in triangulation.simplices ) print("triangle vertex indexes:") for vertices in vertex_sets: print(f" {vertices}") PYSciPy may return simplex rows in a different order when floating-point rounding differs. Sorting the report keeps its presentation stable without changing triangulation.simplices or assuming a fixed simplex number.
- Append the triangle-area coverage check to delaunay_demo.py.
$ cat >> delaunay_demo.py <<'PY' edge_a = triangles[:, 1] - triangles[:, 0] edge_b = triangles[:, 2] - triangles[:, 0] triangle_areas = 0.5 * np.abs( edge_a[:, 0] * edge_b[:, 1] - edge_a[:, 1] * edge_b[:, 0] ) boundary = points[:4] next_boundary = np.roll(boundary, -1, axis=0) boundary_area = 0.5 * abs( np.sum(boundary[:, 0] * next_boundary[:, 1]) - np.sum(boundary[:, 1] * next_boundary[:, 0]) ) np.testing.assert_allclose(triangle_areas.sum(), boundary_area) print(f"triangles: {len(triangles)}") print(f"triangulated area: {triangle_areas.sum():.3f}") print(f"boundary area: {boundary_area:.3f}") PYDuplicate, collinear, or nearly coplanar input can omit vertices or produce degenerate simplices. The triangulation.coplanar attribute records omitted points, while a Qhull option such as QJ perturbs input and may create zero-area triangles.
- Append the guarded point-location check to delaunay_demo.py.
$ cat >> delaunay_demo.py <<'PY' query_points = np.array([ [0.2, 0.2], [1.7, 0.4], [2.3, 0.5], ]) query_simplexes = triangulation.find_simplex(query_points) assert np.all(query_simplexes[:2] >= 0) assert query_simplexes[2] == -1 print("point lookup:") for point, simplex_index in zip(query_points, query_simplexes): label = ( "outside triangulation" if simplex_index == -1 else f"inside vertices {sorted(triangulation.simplices[simplex_index].tolist())}" ) print(f" ({point[0]:.1f}, {point[1]:.1f}) -> {label}") PYAn outside lookup returns -1, which Python would otherwise interpret as the last row of triangulation.simplices.
- Run delaunay_demo.py to print the complete geometric report.
$ python3 delaunay_demo.py triangle vertex indexes: [0, 1, 4] [0, 3, 4] [1, 2, 4] [2, 3, 4] triangles: 4 triangulated area: 4.000 boundary area: 4.000 point lookup: (0.2, 0.2) -> inside vertices [0, 3, 4] (1.7, 0.4) -> inside vertices [1, 2, 4] (2.3, 0.5) -> outside triangulation
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.