Skip to content

Commit 7038f78

Browse files
committed
Merge pull request matplotlib#936 from ianthomas23/838_delaunay_duplicate_points
Correct handling of duplicate points in delaunay triangulation
2 parents 3c6a107 + f07af9a commit 7038f78

File tree

3 files changed

+156
-15
lines changed

3 files changed

+156
-15
lines changed

lib/matplotlib/delaunay/triangulate.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,29 @@ class Triangulation(object):
6161
6262
hull -- list of point_id's giving the nodes which form the convex hull
6363
of the point set. This list is sorted in counter-clockwise order.
64+
65+
Duplicate points.
66+
If there are no duplicate points, Triangulation stores the specified
67+
x and y arrays and there is no difference between the client's and
68+
Triangulation's understanding of point indices used in edge_db,
69+
triangle_nodes and hull.
70+
71+
If there are duplicate points, they are removed from the stored
72+
self.x and self.y as the underlying delaunay code cannot deal with
73+
duplicates. len(self.x) is therefore equal to len(x) minus the
74+
number of duplicate points. Triangulation's edge_db, triangle_nodes
75+
and hull refer to point indices in self.x and self.y, for internal
76+
consistency within Triangulation and the corresponding Interpolator
77+
classes. Client code must take care to deal with this in one of
78+
two ways:
79+
80+
1. Ignore the x,y it specified in Triangulation's constructor and
81+
use triangulation.x and triangulation.y instead, as these are
82+
consistent with edge_db, triangle_nodes and hull.
83+
84+
2. If using the x,y the client specified then edge_db,
85+
triangle_nodes and hull should be passed through the function
86+
to_client_point_indices() first.
6487
"""
6588
def __init__(self, x, y):
6689
self.x = np.asarray(x, dtype=np.float64)
@@ -70,38 +93,48 @@ def __init__(self, x, y):
7093
raise ValueError("x,y must be equal-length 1-D arrays")
7194

7295
self.old_shape = self.x.shape
73-
j_unique = self._collapse_duplicate_points()
96+
duplicates = self._get_duplicate_point_indices()
7497

75-
if j_unique.shape != self.x.shape:
98+
if len(duplicates) > 0:
7699
warnings.warn(
77100
"Input data contains duplicate x,y points; some values are ignored.",
78101
DuplicatePointWarning,
79102
)
80-
self.j_unique = j_unique
103+
104+
# self.j_unique is the array of non-duplicate indices, in
105+
# increasing order.
106+
self.j_unique = np.delete(np.arange(len(self.x)), duplicates)
81107
self.x = self.x[self.j_unique]
82108
self.y = self.y[self.j_unique]
83109
else:
84110
self.j_unique = None
85111

112+
# If there are duplicate points, need a map of point indices used
113+
# by delaunay to those used by client. If there are no duplicate
114+
# points then the map is not needed. Either way, the map is
115+
# conveniently the same as j_unique, so share it.
116+
self._client_point_index_map = self.j_unique
86117

87118
self.circumcenters, self.edge_db, self.triangle_nodes, \
88119
self.triangle_neighbors = delaunay(self.x, self.y)
89120

90121
self.hull = self._compute_convex_hull()
91122

92-
def _collapse_duplicate_points(self):
93-
"""Generate index array that picks out unique x,y points.
94-
95-
This appears to be required by the underlying delaunay triangulation
96-
code.
123+
def _get_duplicate_point_indices(self):
124+
"""Return array of indices of x,y points that are duplicates of
125+
previous points. Indices are in no particular order.
97126
"""
98-
# Find the indices of the unique entries
127+
# Indices of sorted x,y points.
99128
j_sorted = np.lexsort(keys=(self.x, self.y))
100-
mask_unique = np.hstack([
101-
True,
102-
(np.diff(self.x[j_sorted]) != 0) | (np.diff(self.y[j_sorted]) != 0),
129+
130+
# Mask, in j_sorted order, which is True for duplicate points.
131+
mask_duplicates = np.hstack([
132+
False,
133+
(np.diff(self.x[j_sorted]) == 0) & (np.diff(self.y[j_sorted]) == 0),
103134
])
104-
return j_sorted[mask_unique]
135+
136+
# Array of duplicate point indices, in no particular order.
137+
return j_sorted[mask_duplicates]
105138

106139
def _compute_convex_hull(self):
107140
"""Extract the convex hull from the triangulation information.
@@ -129,6 +162,16 @@ def _compute_convex_hull(self):
129162

130163
return hull
131164

165+
def to_client_point_indices(self, array):
166+
"""Converts any array of point indices used within this class to
167+
refer to point indices within the (x,y) arrays specified in the
168+
constructor before duplicates were removed.
169+
"""
170+
if self._client_point_index_map is not None:
171+
return self._client_point_index_map[array]
172+
else:
173+
return array
174+
132175
def linear_interpolator(self, z, default_value=np.nan):
133176
"""Get an object which can interpolate within the convex hull by
134177
assigning a plane to each triangle.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import numpy as np
2+
import matplotlib.tri as mtri
3+
import matplotlib.delaunay as mdel
4+
from nose.tools import assert_equal
5+
from numpy.testing import assert_array_equal, assert_array_almost_equal
6+
7+
def test_delaunay():
8+
# No duplicate points.
9+
x = [0,1,1,0]
10+
y = [0,0,1,1]
11+
npoints = 4
12+
ntriangles = 2
13+
nedges = 5
14+
15+
# Without duplicate points, mpl calls delaunay triangulation and
16+
# does not modify it.
17+
mpl_triang = mtri.Triangulation(x,y)
18+
del_triang = mdel.Triangulation(x,y)
19+
20+
# Points - floating point.
21+
assert_array_almost_equal(mpl_triang.x, x)
22+
assert_array_almost_equal(mpl_triang.x, del_triang.x)
23+
assert_array_almost_equal(mpl_triang.y, y)
24+
assert_array_almost_equal(mpl_triang.y, del_triang.y)
25+
26+
# Triangles - integers.
27+
assert_equal(len(mpl_triang.triangles), ntriangles)
28+
assert_equal(np.min(mpl_triang.triangles), 0)
29+
assert_equal(np.max(mpl_triang.triangles), npoints-1)
30+
assert_array_equal(mpl_triang.triangles, del_triang.triangle_nodes)
31+
32+
# Edges - integers.
33+
assert_equal(len(mpl_triang.edges), nedges)
34+
assert_equal(np.min(mpl_triang.edges), 0)
35+
assert_equal(np.max(mpl_triang.edges), npoints-1)
36+
assert_array_equal(mpl_triang.edges, del_triang.edge_db)
37+
38+
def test_delaunay_duplicate_points():
39+
# Issue 838.
40+
import warnings
41+
42+
# Index 2 is the same as index 0.
43+
x = [0,1,0,1,0]
44+
y = [0,0,0,1,1]
45+
duplicate_index = 2
46+
npoints = 4 # Number of non-duplicate points.
47+
nduplicates = 1
48+
ntriangles = 2
49+
nedges = 5
50+
51+
# With duplicate points, mpl calls delaunay triangulation but
52+
# modified returned arrays.
53+
warnings.simplefilter("ignore") # Ignore DuplicatePointWarning.
54+
mpl_triang = mtri.Triangulation(x,y)
55+
del_triang = mdel.Triangulation(x,y)
56+
warnings.resetwarnings()
57+
58+
# Points - floating point.
59+
assert_equal(len(mpl_triang.x), npoints + nduplicates)
60+
assert_equal(len(del_triang.x), npoints)
61+
assert_array_almost_equal(mpl_triang.x, x)
62+
assert_array_almost_equal(del_triang.x[:duplicate_index], x[:duplicate_index])
63+
assert_array_almost_equal(del_triang.x[duplicate_index:], x[duplicate_index+1:])
64+
65+
assert_equal(len(mpl_triang.y), npoints + nduplicates)
66+
assert_equal(len(del_triang.y), npoints)
67+
assert_array_almost_equal(mpl_triang.y, y)
68+
assert_array_almost_equal(del_triang.y[:duplicate_index], y[:duplicate_index])
69+
assert_array_almost_equal(del_triang.y[duplicate_index:], y[duplicate_index+1:])
70+
71+
# Triangles - integers.
72+
assert_equal(len(mpl_triang.triangles), ntriangles)
73+
assert_equal(np.min(mpl_triang.triangles), 0)
74+
assert_equal(np.max(mpl_triang.triangles), npoints-1 + nduplicates)
75+
assert_equal(len(del_triang.triangle_nodes), ntriangles)
76+
assert_equal(np.min(del_triang.triangle_nodes), 0)
77+
assert_equal(np.max(del_triang.triangle_nodes), npoints-1)
78+
# Convert mpl triangle point indices to delaunay's.
79+
converted_indices = np.where(mpl_triang.triangles > duplicate_index,
80+
mpl_triang.triangles - nduplicates,
81+
mpl_triang.triangles)
82+
assert_array_equal(del_triang.triangle_nodes, converted_indices)
83+
84+
# Edges - integers.
85+
assert_equal(len(mpl_triang.edges), nedges)
86+
assert_equal(np.min(mpl_triang.edges), 0)
87+
assert_equal(np.max(mpl_triang.edges), npoints-1 + nduplicates)
88+
assert_equal(len(del_triang.edge_db), nedges)
89+
assert_equal(np.min(del_triang.edge_db), 0)
90+
assert_equal(np.max(del_triang.edge_db), npoints-1)
91+
# Convert mpl edge point indices to delaunay's.
92+
converted_indices = np.where(mpl_triang.edges > duplicate_index,
93+
mpl_triang.edges - nduplicates,
94+
mpl_triang.edges)
95+
assert_array_equal(del_triang.edge_db, converted_indices)
96+

lib/matplotlib/tri/triangulation.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ def __init__(self, x, y, triangles=None, mask=None):
6969
if triangles is None:
7070
# No triangulation specified, so use matplotlib.delaunay.
7171
dt = delaunay.Triangulation(self.x, self.y)
72-
self.triangles = np.asarray(dt.triangle_nodes, dtype=np.int32)
72+
self.triangles = np.asarray(dt.to_client_point_indices(dt.triangle_nodes),
73+
dtype=np.int32)
7374
if mask is None:
74-
self._edges = np.asarray(dt.edge_db, dtype=np.int32)
75+
self._edges = np.asarray(dt.to_client_point_indices(dt.edge_db),
76+
dtype=np.int32)
7577
# Delaunay triangle_neighbors uses different edge indexing,
7678
# so convert.
7779
neighbors = np.asarray(dt.triangle_neighbors, dtype=np.int32)

0 commit comments

Comments
 (0)