-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
/
_triangulation.py
247 lines (218 loc) · 9.55 KB
/
_triangulation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import sys
import numpy as np
from matplotlib import _api
class Triangulation:
"""
An unstructured triangular grid consisting of npoints points and
ntri triangles. The triangles can either be specified by the user
or automatically generated using a Delaunay triangulation.
Parameters
----------
x, y : (npoints,) array-like
Coordinates of grid points.
triangles : (ntri, 3) array-like of int, optional
For each triangle, the indices of the three points that make
up the triangle, ordered in an anticlockwise manner. If not
specified, the Delaunay triangulation is calculated.
mask : (ntri,) array-like of bool, optional
Which triangles are masked out.
Attributes
----------
triangles : (ntri, 3) array of int
For each triangle, the indices of the three points that make
up the triangle, ordered in an anticlockwise manner. If you want to
take the *mask* into account, use `get_masked_triangles` instead.
mask : (ntri, 3) array of bool or None
Masked out triangles.
is_delaunay : bool
Whether the Triangulation is a calculated Delaunay
triangulation (where *triangles* was not specified) or not.
Notes
-----
For a Triangulation to be valid it must not have duplicate points,
triangles formed from colinear points, or overlapping triangles.
"""
def __init__(self, x, y, triangles=None, mask=None):
from matplotlib import _qhull
self.x = np.asarray(x, dtype=np.float64)
self.y = np.asarray(y, dtype=np.float64)
if self.x.shape != self.y.shape or self.x.ndim != 1:
raise ValueError("x and y must be equal-length 1D arrays, but "
f"found shapes {self.x.shape!r} and "
f"{self.y.shape!r}")
self.mask = None
self._edges = None
self._neighbors = None
self.is_delaunay = False
if triangles is None:
# No triangulation specified, so use matplotlib._qhull to obtain
# Delaunay triangulation.
self.triangles, self._neighbors = _qhull.delaunay(x, y, sys.flags.verbose)
self.is_delaunay = True
else:
# Triangulation specified. Copy, since we may correct triangle
# orientation.
try:
self.triangles = np.array(triangles, dtype=np.int32, order='C')
except ValueError as e:
raise ValueError('triangles must be a (N, 3) int array, not '
f'{triangles!r}') from e
if self.triangles.ndim != 2 or self.triangles.shape[1] != 3:
raise ValueError(
'triangles must be a (N, 3) int array, but found shape '
f'{self.triangles.shape!r}')
if self.triangles.max() >= len(self.x):
raise ValueError(
'triangles are indices into the points and must be in the '
f'range 0 <= i < {len(self.x)} but found value '
f'{self.triangles.max()}')
if self.triangles.min() < 0:
raise ValueError(
'triangles are indices into the points and must be in the '
f'range 0 <= i < {len(self.x)} but found value '
f'{self.triangles.min()}')
# Underlying C++ object is not created until first needed.
self._cpp_triangulation = None
# Default TriFinder not created until needed.
self._trifinder = None
self.set_mask(mask)
def calculate_plane_coefficients(self, z):
"""
Calculate plane equation coefficients for all unmasked triangles from
the point (x, y) coordinates and specified z-array of shape (npoints).
The returned array has shape (npoints, 3) and allows z-value at (x, y)
position in triangle tri to be calculated using
``z = array[tri, 0] * x + array[tri, 1] * y + array[tri, 2]``.
"""
return self.get_cpp_triangulation().calculate_plane_coefficients(z)
@property
def edges(self):
"""
Return integer array of shape (nedges, 2) containing all edges of
non-masked triangles.
Each row defines an edge by its start point index and end point
index. Each edge appears only once, i.e. for an edge between points
*i* and *j*, there will only be either *(i, j)* or *(j, i)*.
"""
if self._edges is None:
self._edges = self.get_cpp_triangulation().get_edges()
return self._edges
def get_cpp_triangulation(self):
"""
Return the underlying C++ Triangulation object, creating it
if necessary.
"""
from matplotlib import _tri
if self._cpp_triangulation is None:
self._cpp_triangulation = _tri.Triangulation(
# For unset arrays use empty tuple which has size of zero.
self.x, self.y, self.triangles,
self.mask if self.mask is not None else (),
self._edges if self._edges is not None else (),
self._neighbors if self._neighbors is not None else (),
not self.is_delaunay)
return self._cpp_triangulation
def get_masked_triangles(self):
"""
Return an array of triangles taking the mask into account.
"""
if self.mask is not None:
return self.triangles[~self.mask]
else:
return self.triangles
@staticmethod
def get_from_args_and_kwargs(*args, **kwargs):
"""
Return a Triangulation object from the args and kwargs, and
the remaining args and kwargs with the consumed values removed.
There are two alternatives: either the first argument is a
Triangulation object, in which case it is returned, or the args
and kwargs are sufficient to create a new Triangulation to
return. In the latter case, see Triangulation.__init__ for
the possible args and kwargs.
"""
if isinstance(args[0], Triangulation):
triangulation, *args = args
if 'triangles' in kwargs:
_api.warn_external(
"Passing the keyword 'triangles' has no effect when also "
"passing a Triangulation")
if 'mask' in kwargs:
_api.warn_external(
"Passing the keyword 'mask' has no effect when also "
"passing a Triangulation")
else:
x, y, triangles, mask, args, kwargs = \
Triangulation._extract_triangulation_params(args, kwargs)
triangulation = Triangulation(x, y, triangles, mask)
return triangulation, args, kwargs
@staticmethod
def _extract_triangulation_params(args, kwargs):
x, y, *args = args
# Check triangles in kwargs then args.
triangles = kwargs.pop('triangles', None)
from_args = False
if triangles is None and args:
triangles = args[0]
from_args = True
if triangles is not None:
try:
triangles = np.asarray(triangles, dtype=np.int32)
except ValueError:
triangles = None
if triangles is not None and (triangles.ndim != 2 or
triangles.shape[1] != 3):
triangles = None
if triangles is not None and from_args:
args = args[1:] # Consumed first item in args.
# Check for mask in kwargs.
mask = kwargs.pop('mask', None)
return x, y, triangles, mask, args, kwargs
def get_trifinder(self):
"""
Return the default `matplotlib.tri.TriFinder` of this
triangulation, creating it if necessary. This allows the same
TriFinder object to be easily shared.
"""
if self._trifinder is None:
# Default TriFinder class.
from matplotlib.tri._trifinder import TrapezoidMapTriFinder
self._trifinder = TrapezoidMapTriFinder(self)
return self._trifinder
@property
def neighbors(self):
"""
Return integer array of shape (ntri, 3) containing neighbor triangles.
For each triangle, the indices of the three triangles that
share the same edges, or -1 if there is no such neighboring
triangle. ``neighbors[i, j]`` is the triangle that is the neighbor
to the edge from point index ``triangles[i, j]`` to point index
``triangles[i, (j+1)%3]``.
"""
if self._neighbors is None:
self._neighbors = self.get_cpp_triangulation().get_neighbors()
return self._neighbors
def set_mask(self, mask):
"""
Set or clear the mask array.
Parameters
----------
mask : None or bool array of length ntri
"""
if mask is None:
self.mask = None
else:
self.mask = np.asarray(mask, dtype=bool)
if self.mask.shape != (self.triangles.shape[0],):
raise ValueError('mask array must have same length as '
'triangles array')
# Set mask in C++ Triangulation.
if self._cpp_triangulation is not None:
self._cpp_triangulation.set_mask(
self.mask if self.mask is not None else ())
# Clear derived fields so they are recalculated when needed.
self._edges = None
self._neighbors = None
# Recalculate TriFinder if it exists.
if self._trifinder is not None:
self._trifinder._initialize()