Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/tri/tritools.py : 13%

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"""
5import numpy as np
7from matplotlib import cbook
8from matplotlib.tri import Triangulation
11class TriAnalyzer:
12 """
13 Define basic tools for triangular mesh analysis and improvement.
15 A TriAnalyzer encapsulates a :class:`~matplotlib.tri.Triangulation`
16 object and provides basic tools for mesh analysis and mesh improvement.
18 Parameters
19 ----------
20 triangulation : :class:`~matplotlib.tri.Triangulation` object
21 The encapsulated triangulation to analyze.
23 Attributes
24 ----------
25 `scale_factors`
27 """
28 def __init__(self, triangulation):
29 cbook._check_isinstance(Triangulation, triangulation=triangulation)
30 self._triangulation = triangulation
32 @property
33 def scale_factors(self):
34 """
35 Factors to rescale the triangulation into a unit square.
37 Returns *k*, tuple of 2 scale factors.
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.
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]))
53 def circle_ratios(self, rescale=True):
54 """
55 Returns a measure of the triangulation triangles flatness.
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.
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).
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.
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.
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)
122 def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True):
123 """
124 Eliminates excessively flat border triangles from the triangulation.
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.
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.
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.
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*.
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
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)
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])
196 return np.ma.filled(current_mask, True)
198 def _get_compressed_triangulation(self, return_tri_renum=False,
199 return_node_renum=False):
200 """
201 Compress (if masked) the encapsulated triangulation.
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.
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.
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.
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)
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)
251 # Now renumbering the valid triangles nodes
252 compressed_triangles = node_renum[compressed_triangles]
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)
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
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*) :
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)
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)