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