tritools.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. """
  2. Tools for triangular grids.
  3. """
  4. import numpy as np
  5. from matplotlib import cbook
  6. from matplotlib.tri import Triangulation
  7. class TriAnalyzer:
  8. """
  9. Define basic tools for triangular mesh analysis and improvement.
  10. A TriAnalyzer encapsulates a :class:`~matplotlib.tri.Triangulation`
  11. object and provides basic tools for mesh analysis and mesh improvement.
  12. Parameters
  13. ----------
  14. triangulation : :class:`~matplotlib.tri.Triangulation` object
  15. The encapsulated triangulation to analyze.
  16. Attributes
  17. ----------
  18. `scale_factors`
  19. """
  20. def __init__(self, triangulation):
  21. cbook._check_isinstance(Triangulation, triangulation=triangulation)
  22. self._triangulation = triangulation
  23. @property
  24. def scale_factors(self):
  25. """
  26. Factors to rescale the triangulation into a unit square.
  27. Returns *k*, tuple of 2 scale factors.
  28. Returns
  29. -------
  30. k : tuple of 2 floats (kx, ky)
  31. Tuple of floats that would rescale the triangulation :
  32. ``[triangulation.x * kx, triangulation.y * ky]``
  33. fits exactly inside a unit square.
  34. """
  35. compressed_triangles = self._triangulation.get_masked_triangles()
  36. node_used = (np.bincount(np.ravel(compressed_triangles),
  37. minlength=self._triangulation.x.size) != 0)
  38. return (1 / np.ptp(self._triangulation.x[node_used]),
  39. 1 / np.ptp(self._triangulation.y[node_used]))
  40. def circle_ratios(self, rescale=True):
  41. """
  42. Returns a measure of the triangulation triangles flatness.
  43. The ratio of the incircle radius over the circumcircle radius is a
  44. widely used indicator of a triangle flatness.
  45. It is always ``<= 0.5`` and ``== 0.5`` only for equilateral
  46. triangles. Circle ratios below 0.01 denote very flat triangles.
  47. To avoid unduly low values due to a difference of scale between the 2
  48. axis, the triangular mesh can first be rescaled to fit inside a unit
  49. square with :attr:`scale_factors` (Only if *rescale* is True, which is
  50. its default value).
  51. Parameters
  52. ----------
  53. rescale : boolean, optional
  54. If True, a rescaling will be internally performed (based on
  55. :attr:`scale_factors`, so that the (unmasked) triangles fit
  56. exactly inside a unit square mesh. Default is True.
  57. Returns
  58. -------
  59. circle_ratios : masked array
  60. Ratio of the incircle radius over the
  61. circumcircle radius, for each 'rescaled' triangle of the
  62. encapsulated triangulation.
  63. Values corresponding to masked triangles are masked out.
  64. """
  65. # Coords rescaling
  66. if rescale:
  67. (kx, ky) = self.scale_factors
  68. else:
  69. (kx, ky) = (1.0, 1.0)
  70. pts = np.vstack([self._triangulation.x*kx,
  71. self._triangulation.y*ky]).T
  72. tri_pts = pts[self._triangulation.triangles]
  73. # Computes the 3 side lengths
  74. a = tri_pts[:, 1, :] - tri_pts[:, 0, :]
  75. b = tri_pts[:, 2, :] - tri_pts[:, 1, :]
  76. c = tri_pts[:, 0, :] - tri_pts[:, 2, :]
  77. a = np.hypot(a[:, 0], a[:, 1])
  78. b = np.hypot(b[:, 0], b[:, 1])
  79. c = np.hypot(c[:, 0], c[:, 1])
  80. # circumcircle and incircle radii
  81. s = (a+b+c)*0.5
  82. prod = s*(a+b-s)*(a+c-s)*(b+c-s)
  83. # We have to deal with flat triangles with infinite circum_radius
  84. bool_flat = (prod == 0.)
  85. if np.any(bool_flat):
  86. # Pathologic flow
  87. ntri = tri_pts.shape[0]
  88. circum_radius = np.empty(ntri, dtype=np.float64)
  89. circum_radius[bool_flat] = np.inf
  90. abc = a*b*c
  91. circum_radius[~bool_flat] = abc[~bool_flat] / (
  92. 4.0*np.sqrt(prod[~bool_flat]))
  93. else:
  94. # Normal optimized flow
  95. circum_radius = (a*b*c) / (4.0*np.sqrt(prod))
  96. in_radius = (a*b*c) / (4.0*circum_radius*s)
  97. circle_ratio = in_radius/circum_radius
  98. mask = self._triangulation.mask
  99. if mask is None:
  100. return circle_ratio
  101. else:
  102. return np.ma.array(circle_ratio, mask=mask)
  103. def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True):
  104. """
  105. Eliminates excessively flat border triangles from the triangulation.
  106. Returns a mask *new_mask* which allows to clean the encapsulated
  107. triangulation from its border-located flat triangles
  108. (according to their :meth:`circle_ratios`).
  109. This mask is meant to be subsequently applied to the triangulation
  110. using :func:`matplotlib.tri.Triangulation.set_mask`.
  111. *new_mask* is an extension of the initial triangulation mask
  112. in the sense that an initially masked triangle will remain masked.
  113. The *new_mask* array is computed recursively; at each step flat
  114. triangles are removed only if they share a side with the current mesh
  115. border. Thus no new holes in the triangulated domain will be created.
  116. Parameters
  117. ----------
  118. min_circle_ratio : float, optional
  119. Border triangles with incircle/circumcircle radii ratio r/R will
  120. be removed if r/R < *min_circle_ratio*. Default value: 0.01
  121. rescale : boolean, optional
  122. If True, a rescaling will first be internally performed (based on
  123. :attr:`scale_factors` ), so that the (unmasked) triangles fit
  124. exactly inside a unit square mesh. This rescaling accounts for the
  125. difference of scale which might exist between the 2 axis. Default
  126. (and recommended) value is True.
  127. Returns
  128. -------
  129. new_mask : array-like of booleans
  130. Mask to apply to encapsulated triangulation.
  131. All the initially masked triangles remain masked in the
  132. *new_mask*.
  133. Notes
  134. -----
  135. The rationale behind this function is that a Delaunay
  136. triangulation - of an unstructured set of points - sometimes contains
  137. almost flat triangles at its border, leading to artifacts in plots
  138. (especially for high-resolution contouring).
  139. Masked with computed *new_mask*, the encapsulated
  140. triangulation would contain no more unmasked border triangles
  141. with a circle ratio below *min_circle_ratio*, thus improving the
  142. mesh quality for subsequent plots or interpolation.
  143. """
  144. # Recursively computes the mask_current_borders, true if a triangle is
  145. # at the border of the mesh OR touching the border through a chain of
  146. # invalid aspect ratio masked_triangles.
  147. ntri = self._triangulation.triangles.shape[0]
  148. mask_bad_ratio = self.circle_ratios(rescale) < min_circle_ratio
  149. current_mask = self._triangulation.mask
  150. if current_mask is None:
  151. current_mask = np.zeros(ntri, dtype=bool)
  152. valid_neighbors = np.copy(self._triangulation.neighbors)
  153. renum_neighbors = np.arange(ntri, dtype=np.int32)
  154. nadd = -1
  155. while nadd != 0:
  156. # The active wavefront is the triangles from the border (unmasked
  157. # but with a least 1 neighbor equal to -1
  158. wavefront = (np.min(valid_neighbors, axis=1) == -1) & ~current_mask
  159. # The element from the active wavefront will be masked if their
  160. # circle ratio is bad.
  161. added_mask = wavefront & mask_bad_ratio
  162. current_mask = added_mask | current_mask
  163. nadd = np.sum(added_mask)
  164. # now we have to update the tables valid_neighbors
  165. valid_neighbors[added_mask, :] = -1
  166. renum_neighbors[added_mask] = -1
  167. valid_neighbors = np.where(valid_neighbors == -1, -1,
  168. renum_neighbors[valid_neighbors])
  169. return np.ma.filled(current_mask, True)
  170. def _get_compressed_triangulation(self, return_tri_renum=False,
  171. return_node_renum=False):
  172. """
  173. Compress (if masked) the encapsulated triangulation.
  174. Returns minimal-length triangles array (*compressed_triangles*) and
  175. coordinates arrays (*compressed_x*, *compressed_y*) that can still
  176. describe the unmasked triangles of the encapsulated triangulation.
  177. Parameters
  178. ----------
  179. return_tri_renum : boolean, optional
  180. Indicates whether a renumbering table to translate the triangle
  181. numbers from the encapsulated triangulation numbering into the
  182. new (compressed) renumbering will be returned.
  183. return_node_renum : boolean, optional
  184. Indicates whether a renumbering table to translate the nodes
  185. numbers from the encapsulated triangulation numbering into the
  186. new (compressed) renumbering will be returned.
  187. Returns
  188. -------
  189. compressed_triangles : array-like
  190. the returned compressed triangulation triangles
  191. compressed_x : array-like
  192. the returned compressed triangulation 1st coordinate
  193. compressed_y : array-like
  194. the returned compressed triangulation 2nd coordinate
  195. tri_renum : array-like of integers
  196. renumbering table to translate the triangle numbers from the
  197. encapsulated triangulation into the new (compressed) renumbering.
  198. -1 for masked triangles (deleted from *compressed_triangles*).
  199. Returned only if *return_tri_renum* is True.
  200. node_renum : array-like of integers
  201. renumbering table to translate the point numbers from the
  202. encapsulated triangulation into the new (compressed) renumbering.
  203. -1 for unused points (i.e. those deleted from *compressed_x* and
  204. *compressed_y*). Returned only if *return_node_renum* is True.
  205. """
  206. # Valid triangles and renumbering
  207. tri_mask = self._triangulation.mask
  208. compressed_triangles = self._triangulation.get_masked_triangles()
  209. ntri = self._triangulation.triangles.shape[0]
  210. tri_renum = self._total_to_compress_renum(tri_mask, ntri)
  211. # Valid nodes and renumbering
  212. node_mask = (np.bincount(np.ravel(compressed_triangles),
  213. minlength=self._triangulation.x.size) == 0)
  214. compressed_x = self._triangulation.x[~node_mask]
  215. compressed_y = self._triangulation.y[~node_mask]
  216. node_renum = self._total_to_compress_renum(node_mask)
  217. # Now renumbering the valid triangles nodes
  218. compressed_triangles = node_renum[compressed_triangles]
  219. # 4 cases possible for return
  220. if not return_tri_renum:
  221. if not return_node_renum:
  222. return compressed_triangles, compressed_x, compressed_y
  223. else:
  224. return (compressed_triangles, compressed_x, compressed_y,
  225. node_renum)
  226. else:
  227. if not return_node_renum:
  228. return (compressed_triangles, compressed_x, compressed_y,
  229. tri_renum)
  230. else:
  231. return (compressed_triangles, compressed_x, compressed_y,
  232. tri_renum, node_renum)
  233. @staticmethod
  234. def _total_to_compress_renum(mask, n=None):
  235. """
  236. Parameters
  237. ----------
  238. mask : 1d boolean array or None
  239. mask
  240. n : integer
  241. length of the mask. Useful only id mask can be None
  242. Returns
  243. -------
  244. renum : integer array
  245. array so that (`valid_array` being a compressed array
  246. based on a `masked_array` with mask *mask*) :
  247. - For all i such as mask[i] = False:
  248. valid_array[renum[i]] = masked_array[i]
  249. - For all i such as mask[i] = True:
  250. renum[i] = -1 (invalid value)
  251. """
  252. if n is None:
  253. n = np.size(mask)
  254. if mask is not None:
  255. renum = np.full(n, -1, dtype=np.int32) # Default num is -1
  256. valid = np.arange(n, dtype=np.int32)[~mask]
  257. renum[valid] = np.arange(np.size(valid, 0), dtype=np.int32)
  258. return renum
  259. else:
  260. return np.arange(n, dtype=np.int32)