Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Tools for triangular grids. 

3""" 

4 

5import numpy as np 

6 

7from matplotlib import cbook 

8from matplotlib.tri import Triangulation 

9 

10 

11class TriAnalyzer: 

12 """ 

13 Define basic tools for triangular mesh analysis and improvement. 

14 

15 A TriAnalyzer encapsulates a :class:`~matplotlib.tri.Triangulation` 

16 object and provides basic tools for mesh analysis and mesh improvement. 

17 

18 Parameters 

19 ---------- 

20 triangulation : :class:`~matplotlib.tri.Triangulation` object 

21 The encapsulated triangulation to analyze. 

22 

23 Attributes 

24 ---------- 

25 `scale_factors` 

26 

27 """ 

28 def __init__(self, triangulation): 

29 cbook._check_isinstance(Triangulation, triangulation=triangulation) 

30 self._triangulation = triangulation 

31 

32 @property 

33 def scale_factors(self): 

34 """ 

35 Factors to rescale the triangulation into a unit square. 

36 

37 Returns *k*, tuple of 2 scale factors. 

38 

39 Returns 

40 ------- 

41 k : tuple of 2 floats (kx, ky) 

42 Tuple of floats that would rescale the triangulation : 

43 ``[triangulation.x * kx, triangulation.y * ky]`` 

44 fits exactly inside a unit square. 

45 

46 """ 

47 compressed_triangles = self._triangulation.get_masked_triangles() 

48 node_used = (np.bincount(np.ravel(compressed_triangles), 

49 minlength=self._triangulation.x.size) != 0) 

50 return (1 / np.ptp(self._triangulation.x[node_used]), 

51 1 / np.ptp(self._triangulation.y[node_used])) 

52 

53 def circle_ratios(self, rescale=True): 

54 """ 

55 Returns a measure of the triangulation triangles flatness. 

56 

57 The ratio of the incircle radius over the circumcircle radius is a 

58 widely used indicator of a triangle flatness. 

59 It is always ``<= 0.5`` and ``== 0.5`` only for equilateral 

60 triangles. Circle ratios below 0.01 denote very flat triangles. 

61 

62 To avoid unduly low values due to a difference of scale between the 2 

63 axis, the triangular mesh can first be rescaled to fit inside a unit 

64 square with :attr:`scale_factors` (Only if *rescale* is True, which is 

65 its default value). 

66 

67 Parameters 

68 ---------- 

69 rescale : boolean, optional 

70 If True, a rescaling will be internally performed (based on 

71 :attr:`scale_factors`, so that the (unmasked) triangles fit 

72 exactly inside a unit square mesh. Default is True. 

73 

74 Returns 

75 ------- 

76 circle_ratios : masked array 

77 Ratio of the incircle radius over the 

78 circumcircle radius, for each 'rescaled' triangle of the 

79 encapsulated triangulation. 

80 Values corresponding to masked triangles are masked out. 

81 

82 """ 

83 # Coords rescaling 

84 if rescale: 

85 (kx, ky) = self.scale_factors 

86 else: 

87 (kx, ky) = (1.0, 1.0) 

88 pts = np.vstack([self._triangulation.x*kx, 

89 self._triangulation.y*ky]).T 

90 tri_pts = pts[self._triangulation.triangles] 

91 # Computes the 3 side lengths 

92 a = tri_pts[:, 1, :] - tri_pts[:, 0, :] 

93 b = tri_pts[:, 2, :] - tri_pts[:, 1, :] 

94 c = tri_pts[:, 0, :] - tri_pts[:, 2, :] 

95 a = np.hypot(a[:, 0], a[:, 1]) 

96 b = np.hypot(b[:, 0], b[:, 1]) 

97 c = np.hypot(c[:, 0], c[:, 1]) 

98 # circumcircle and incircle radii 

99 s = (a+b+c)*0.5 

100 prod = s*(a+b-s)*(a+c-s)*(b+c-s) 

101 # We have to deal with flat triangles with infinite circum_radius 

102 bool_flat = (prod == 0.) 

103 if np.any(bool_flat): 

104 # Pathologic flow 

105 ntri = tri_pts.shape[0] 

106 circum_radius = np.empty(ntri, dtype=np.float64) 

107 circum_radius[bool_flat] = np.inf 

108 abc = a*b*c 

109 circum_radius[~bool_flat] = abc[~bool_flat] / ( 

110 4.0*np.sqrt(prod[~bool_flat])) 

111 else: 

112 # Normal optimized flow 

113 circum_radius = (a*b*c) / (4.0*np.sqrt(prod)) 

114 in_radius = (a*b*c) / (4.0*circum_radius*s) 

115 circle_ratio = in_radius/circum_radius 

116 mask = self._triangulation.mask 

117 if mask is None: 

118 return circle_ratio 

119 else: 

120 return np.ma.array(circle_ratio, mask=mask) 

121 

122 def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True): 

123 """ 

124 Eliminates excessively flat border triangles from the triangulation. 

125 

126 Returns a mask *new_mask* which allows to clean the encapsulated 

127 triangulation from its border-located flat triangles 

128 (according to their :meth:`circle_ratios`). 

129 This mask is meant to be subsequently applied to the triangulation 

130 using :func:`matplotlib.tri.Triangulation.set_mask`. 

131 *new_mask* is an extension of the initial triangulation mask 

132 in the sense that an initially masked triangle will remain masked. 

133 

134 The *new_mask* array is computed recursively; at each step flat 

135 triangles are removed only if they share a side with the current mesh 

136 border. Thus no new holes in the triangulated domain will be created. 

137 

138 Parameters 

139 ---------- 

140 min_circle_ratio : float, optional 

141 Border triangles with incircle/circumcircle radii ratio r/R will 

142 be removed if r/R < *min_circle_ratio*. Default value: 0.01 

143 rescale : boolean, optional 

144 If True, a rescaling will first be internally performed (based on 

145 :attr:`scale_factors` ), so that the (unmasked) triangles fit 

146 exactly inside a unit square mesh. This rescaling accounts for the 

147 difference of scale which might exist between the 2 axis. Default 

148 (and recommended) value is True. 

149 

150 Returns 

151 ------- 

152 new_mask : array-like of booleans 

153 Mask to apply to encapsulated triangulation. 

154 All the initially masked triangles remain masked in the 

155 *new_mask*. 

156 

157 Notes 

158 ----- 

159 The rationale behind this function is that a Delaunay 

160 triangulation - of an unstructured set of points - sometimes contains 

161 almost flat triangles at its border, leading to artifacts in plots 

162 (especially for high-resolution contouring). 

163 Masked with computed *new_mask*, the encapsulated 

164 triangulation would contain no more unmasked border triangles 

165 with a circle ratio below *min_circle_ratio*, thus improving the 

166 mesh quality for subsequent plots or interpolation. 

167 """ 

168 # Recursively computes the mask_current_borders, true if a triangle is 

169 # at the border of the mesh OR touching the border through a chain of 

170 # invalid aspect ratio masked_triangles. 

171 ntri = self._triangulation.triangles.shape[0] 

172 mask_bad_ratio = self.circle_ratios(rescale) < min_circle_ratio 

173 

174 current_mask = self._triangulation.mask 

175 if current_mask is None: 

176 current_mask = np.zeros(ntri, dtype=bool) 

177 valid_neighbors = np.copy(self._triangulation.neighbors) 

178 renum_neighbors = np.arange(ntri, dtype=np.int32) 

179 nadd = -1 

180 while nadd != 0: 

181 # The active wavefront is the triangles from the border (unmasked 

182 # but with a least 1 neighbor equal to -1 

183 wavefront = (np.min(valid_neighbors, axis=1) == -1) & ~current_mask 

184 # The element from the active wavefront will be masked if their 

185 # circle ratio is bad. 

186 added_mask = wavefront & mask_bad_ratio 

187 current_mask = added_mask | current_mask 

188 nadd = np.sum(added_mask) 

189 

190 # now we have to update the tables valid_neighbors 

191 valid_neighbors[added_mask, :] = -1 

192 renum_neighbors[added_mask] = -1 

193 valid_neighbors = np.where(valid_neighbors == -1, -1, 

194 renum_neighbors[valid_neighbors]) 

195 

196 return np.ma.filled(current_mask, True) 

197 

198 def _get_compressed_triangulation(self, return_tri_renum=False, 

199 return_node_renum=False): 

200 """ 

201 Compress (if masked) the encapsulated triangulation. 

202 

203 Returns minimal-length triangles array (*compressed_triangles*) and 

204 coordinates arrays (*compressed_x*, *compressed_y*) that can still 

205 describe the unmasked triangles of the encapsulated triangulation. 

206 

207 Parameters 

208 ---------- 

209 return_tri_renum : boolean, optional 

210 Indicates whether a renumbering table to translate the triangle 

211 numbers from the encapsulated triangulation numbering into the 

212 new (compressed) renumbering will be returned. 

213 return_node_renum : boolean, optional 

214 Indicates whether a renumbering table to translate the nodes 

215 numbers from the encapsulated triangulation numbering into the 

216 new (compressed) renumbering will be returned. 

217 

218 Returns 

219 ------- 

220 compressed_triangles : array-like 

221 the returned compressed triangulation triangles 

222 compressed_x : array-like 

223 the returned compressed triangulation 1st coordinate 

224 compressed_y : array-like 

225 the returned compressed triangulation 2nd coordinate 

226 tri_renum : array-like of integers 

227 renumbering table to translate the triangle numbers from the 

228 encapsulated triangulation into the new (compressed) renumbering. 

229 -1 for masked triangles (deleted from *compressed_triangles*). 

230 Returned only if *return_tri_renum* is True. 

231 node_renum : array-like of integers 

232 renumbering table to translate the point numbers from the 

233 encapsulated triangulation into the new (compressed) renumbering. 

234 -1 for unused points (i.e. those deleted from *compressed_x* and 

235 *compressed_y*). Returned only if *return_node_renum* is True. 

236 

237 """ 

238 # Valid triangles and renumbering 

239 tri_mask = self._triangulation.mask 

240 compressed_triangles = self._triangulation.get_masked_triangles() 

241 ntri = self._triangulation.triangles.shape[0] 

242 tri_renum = self._total_to_compress_renum(tri_mask, ntri) 

243 

244 # Valid nodes and renumbering 

245 node_mask = (np.bincount(np.ravel(compressed_triangles), 

246 minlength=self._triangulation.x.size) == 0) 

247 compressed_x = self._triangulation.x[~node_mask] 

248 compressed_y = self._triangulation.y[~node_mask] 

249 node_renum = self._total_to_compress_renum(node_mask) 

250 

251 # Now renumbering the valid triangles nodes 

252 compressed_triangles = node_renum[compressed_triangles] 

253 

254 # 4 cases possible for return 

255 if not return_tri_renum: 

256 if not return_node_renum: 

257 return compressed_triangles, compressed_x, compressed_y 

258 else: 

259 return (compressed_triangles, compressed_x, compressed_y, 

260 node_renum) 

261 else: 

262 if not return_node_renum: 

263 return (compressed_triangles, compressed_x, compressed_y, 

264 tri_renum) 

265 else: 

266 return (compressed_triangles, compressed_x, compressed_y, 

267 tri_renum, node_renum) 

268 

269 @staticmethod 

270 def _total_to_compress_renum(mask, n=None): 

271 """ 

272 Parameters 

273 ---------- 

274 mask : 1d boolean array or None 

275 mask 

276 n : integer 

277 length of the mask. Useful only id mask can be None 

278 

279 Returns 

280 ------- 

281 renum : integer array 

282 array so that (`valid_array` being a compressed array 

283 based on a `masked_array` with mask *mask*) : 

284 

285 - For all i such as mask[i] = False: 

286 valid_array[renum[i]] = masked_array[i] 

287 - For all i such as mask[i] = True: 

288 renum[i] = -1 (invalid value) 

289 

290 """ 

291 if n is None: 

292 n = np.size(mask) 

293 if mask is not None: 

294 renum = np.full(n, -1, dtype=np.int32) # Default num is -1 

295 valid = np.arange(n, dtype=np.int32)[~mask] 

296 renum[valid] = np.arange(np.size(valid, 0), dtype=np.int32) 

297 return renum 

298 else: 

299 return np.arange(n, dtype=np.int32)