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