1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,309 @@ |
1 |
+from collections import defaultdict, Counter |
|
2 |
+from itertools import combinations, chain |
|
3 |
+ |
|
4 |
+import numpy as np |
|
5 |
+from scipy import linalg |
|
6 |
+ |
|
7 |
+ |
|
8 |
+class Triangulation: |
|
9 |
+ def __init__(self, coords): |
|
10 |
+ """A triangulation object. |
|
11 |
+ |
|
12 |
+ Parameters |
|
13 |
+ ---------- |
|
14 |
+ coords : 2d array-like of floats |
|
15 |
+ Coordinates of vertices of the first simplex. |
|
16 |
+ |
|
17 |
+ Attributes |
|
18 |
+ ---------- |
|
19 |
+ vertices : list of float tuples |
|
20 |
+ Coordinates of the triangulation vertices. |
|
21 |
+ simplices : set of integer tuples |
|
22 |
+ List with indices of vertices forming individual simplices |
|
23 |
+ vertex_to_simplices : dict int → set |
|
24 |
+ Mapping from vertex index to the set of simplices containing that |
|
25 |
+ vertex. |
|
26 |
+ hull : set of int |
|
27 |
+ Exterior vertices |
|
28 |
+ """ |
|
29 |
+ dim = len(coords[0]) |
|
30 |
+ if any(len(coord) != dim for coord in coords): |
|
31 |
+ raise ValueError("Coordinates dimension mismatch") |
|
32 |
+ |
|
33 |
+ if len(coords) != dim + 1: |
|
34 |
+ raise ValueError("Can only add one simplex on initialization") |
|
35 |
+ self.vertices = list(coords) |
|
36 |
+ self.simplices = set() |
|
37 |
+ self.vertex_to_simplices = defaultdict(set) |
|
38 |
+ self.add_simplex(range(len(self.vertices))) |
|
39 |
+ self.hull = set(range(len(coords))) |
|
40 |
+ |
|
41 |
+ def delete_simplex(self, simplex): |
|
42 |
+ simplex = tuple(sorted(simplex)) |
|
43 |
+ self.simplices.remove(simplex) |
|
44 |
+ for vertex in simplex: |
|
45 |
+ self.vertex_to_simplices[vertex].remove(simplex) |
|
46 |
+ |
|
47 |
+ def add_simplex(self, simplex): |
|
48 |
+ simplex = tuple(sorted(simplex)) |
|
49 |
+ self.simplices.add(simplex) |
|
50 |
+ for vertex in simplex: |
|
51 |
+ self.vertex_to_simplices[vertex].add(simplex) |
|
52 |
+ |
|
53 |
+ def point_in_simplex(self, point, simplex, eps=1e-8): |
|
54 |
+ """Check whether vertex lies within a simplex. |
|
55 |
+ |
|
56 |
+ Returns |
|
57 |
+ ------- |
|
58 |
+ vertices : list of ints |
|
59 |
+ Indices of vertices of the simplex to which the vertex belongs. None |
|
60 |
+ indicates that the vertex is outside the simplex |
|
61 |
+ """ |
|
62 |
+ if len(simplex) != self.dim + 1: |
|
63 |
+ # We are checking whether point belongs to a face. |
|
64 |
+ simplex = self.containing(simplex).pop() |
|
65 |
+ x0 = np.array(self.vertices[simplex[0]]) |
|
66 |
+ vectors = np.array([self.vertices[i] for i in simplex[1:]]) - x0 |
|
67 |
+ alpha = np.linalg.solve(vectors.T, point - x0) |
|
68 |
+ if any(alpha < -eps) or sum(alpha) > 1 + eps: |
|
69 |
+ return [] |
|
70 |
+ result = ( |
|
71 |
+ ([0] if sum(alpha) < 1 - eps else []) |
|
72 |
+ + [i + 1 for i, a in enumerate(alpha) if a > eps] |
|
73 |
+ ) |
|
74 |
+ return [simplex[i] for i in result] |
|
75 |
+ |
|
76 |
+ def locate_point(self, point): |
|
77 |
+ """Find to which simplex the point belongs. |
|
78 |
+ |
|
79 |
+ Return indices of the simplex containing the point. |
|
80 |
+ Empty tuple means the point is outside the triangulation |
|
81 |
+ """ |
|
82 |
+ for simplex in self.simplices: |
|
83 |
+ face = self.point_in_simplex(point, simplex) |
|
84 |
+ if face: |
|
85 |
+ return face |
|
86 |
+ |
|
87 |
+ return () |
|
88 |
+ |
|
89 |
+ @property |
|
90 |
+ def dim(self): |
|
91 |
+ return len(self.vertices[0]) |
|
92 |
+ |
|
93 |
+ def faces(self, dim=None, simplices=None, vertices=None): |
|
94 |
+ """Iterator over faces of a simplex or vertex sequence.""" |
|
95 |
+ if dim is None: |
|
96 |
+ dim = self.dim |
|
97 |
+ |
|
98 |
+ if simplices is not None and vertices is not None: |
|
99 |
+ raise ValueError("Only one of simplices and vertices is allowed.") |
|
100 |
+ if vertices is not None: |
|
101 |
+ vertices = set(vertices) |
|
102 |
+ simplices = chain(*(self.vertex_to_simplices[i] for i in vertices)) |
|
103 |
+ simplices = set(simplices) |
|
104 |
+ elif simplices is None: |
|
105 |
+ simplices = self.simplices |
|
106 |
+ |
|
107 |
+ faces = (face for tri in simplices |
|
108 |
+ for face in combinations(tri, dim)) |
|
109 |
+ |
|
110 |
+ if vertices is not None: |
|
111 |
+ return (face for face in faces if all(i in vertices for i in face)) |
|
112 |
+ else: |
|
113 |
+ return faces |
|
114 |
+ |
|
115 |
+ def containing(self, face): |
|
116 |
+ """Simplices containing a face.""" |
|
117 |
+ return set.intersection(*(self.vertex_to_simplices[i] for i in face)) |
|
118 |
+ |
|
119 |
+ def _extend_hull(self, new_vertex, eps=1e-8): |
|
120 |
+ hull_faces = list(self.faces(vertices=self.hull)) |
|
121 |
+ decomp = [] |
|
122 |
+ for face in hull_faces: |
|
123 |
+ coords = np.array([self.vertices[i] for i in face]) - new_vertex |
|
124 |
+ decomp.append(linalg.lu_factor(coords.T)) |
|
125 |
+ shifted = [self.vertices[vertex] - np.array(new_vertex) |
|
126 |
+ for vertex in self.hull] |
|
127 |
+ new_vertices = set() |
|
128 |
+ for coord, index in zip(shifted, self.hull): |
|
129 |
+ good = True |
|
130 |
+ for face, factored in zip(hull_faces, decomp): |
|
131 |
+ if index in face: |
|
132 |
+ continue |
|
133 |
+ alpha = linalg.lu_solve(factored, coord) |
|
134 |
+ if all(alpha > eps): |
|
135 |
+ good = False |
|
136 |
+ break |
|
137 |
+ if good: |
|
138 |
+ new_vertices.add(index) |
|
139 |
+ |
|
140 |
+ pt_index = len(self.vertices) |
|
141 |
+ self.vertices.append(new_vertex) |
|
142 |
+ for face in hull_faces: |
|
143 |
+ if all(i in new_vertices for i in face): |
|
144 |
+ self.add_simplex(face + (pt_index,)) |
|
145 |
+ |
|
146 |
+ multiplicities = Counter(face for face in |
|
147 |
+ self.faces(vertices=new_vertices | {pt_index}) |
|
148 |
+ if pt_index in face) |
|
149 |
+ |
|
150 |
+ if all(i == 2 for i in multiplicities.values()): |
|
151 |
+ # We tried to add an internal point, revert and raise. |
|
152 |
+ for tri in self.vertex_to_simplices[pt_index]: |
|
153 |
+ self.simplices.remove(tri) |
|
154 |
+ del self.vertex_to_simplices[pt_index] |
|
155 |
+ del self.vertices[pt_index] |
|
156 |
+ raise ValueError("Candidate vertex is inside the hull.") |
|
157 |
+ |
|
158 |
+ self.hull.add(pt_index) |
|
159 |
+ self.hull = self.compute_hull(vertices=self.hull, check=False) |
|
160 |
+ |
|
161 |
+ def add_point(self, point, simplex=None): |
|
162 |
+ """Add a new vertex and create simplices as appropriate. |
|
163 |
+ |
|
164 |
+ Parameters |
|
165 |
+ ---------- |
|
166 |
+ point : float vector |
|
167 |
+ Coordinates of the point to be added. |
|
168 |
+ simplex : tuple of ints, optional |
|
169 |
+ Simplex containing the point. Empty tuple indicates points outside |
|
170 |
+ the hull. If not provided, the algorithm costs O(N), so this should |
|
171 |
+ be used whenever possible. |
|
172 |
+ """ |
|
173 |
+ if simplex is None: |
|
174 |
+ simplex = self.locate_point(point) |
|
175 |
+ |
|
176 |
+ if not simplex: |
|
177 |
+ self._extend_hull(point) |
|
178 |
+ return |
|
179 |
+ else: |
|
180 |
+ reduced_simplex = self.point_in_simplex(point, simplex) |
|
181 |
+ if not reduced_simplex: |
|
182 |
+ raise ValueError( |
|
183 |
+ 'Point lies outside of the specified simplex.' |
|
184 |
+ ) |
|
185 |
+ else: |
|
186 |
+ simplex = reduced_simplex |
|
187 |
+ |
|
188 |
+ if len(simplex) == 1: |
|
189 |
+ raise ValueError("Point already in triangulation.") |
|
190 |
+ elif len(simplex) == self.dim + 1: |
|
191 |
+ self.add_point_inside_simplex(point, simplex) |
|
192 |
+ else: |
|
193 |
+ self.add_point_on_face(point, simplex) |
|
194 |
+ |
|
195 |
+ def add_point_inside_simplex(self, point, simplex): |
|
196 |
+ if len(self.point_in_simplex(point, simplex)) != self.dim + 1: |
|
197 |
+ raise ValueError("Vertex is not inside simplex") |
|
198 |
+ pt_index = len(self.vertices) |
|
199 |
+ self.vertices.append(point) |
|
200 |
+ self.delete_simplex(simplex) |
|
201 |
+ |
|
202 |
+ new = [] |
|
203 |
+ for others in combinations(simplex, len(simplex) - 1): |
|
204 |
+ tri = others + (pt_index,) |
|
205 |
+ self.add_simplex(tri) |
|
206 |
+ new.append(tri) |
|
207 |
+ |
|
208 |
+ return(new) |
|
209 |
+ |
|
210 |
+ def add_point_on_face(self, point, face): |
|
211 |
+ pt_index = len(self.vertices) |
|
212 |
+ self.vertices.append(point) |
|
213 |
+ |
|
214 |
+ simplices = self.containing(face) |
|
215 |
+ if (set(self.point_in_simplex(point, next(iter(simplices)))) |
|
216 |
+ != set(face)): |
|
217 |
+ |
|
218 |
+ raise ValueError("Vertex does not lie on the face.") |
|
219 |
+ |
|
220 |
+ if all(pt in self.hull for pt in face): |
|
221 |
+ self.hull.add(pt_index) |
|
222 |
+ for simplex in simplices: |
|
223 |
+ self.delete_simplex(simplex) |
|
224 |
+ opposing = tuple(pt for pt in simplex if pt not in face) |
|
225 |
+ |
|
226 |
+ for others in combinations(face, len(face) - 1): |
|
227 |
+ self.add_simplex(others + opposing + (pt_index,)) |
|
228 |
+ |
|
229 |
+ def flip(self, face): |
|
230 |
+ """Flip the face shared between several simplices.""" |
|
231 |
+ simplices = self.containing(face) |
|
232 |
+ |
|
233 |
+ new_face = tuple(set.union(*(set(tri) for tri in simplices)) |
|
234 |
+ - set(face)) |
|
235 |
+ if len(new_face) + len(face) != self.dim + 2: |
|
236 |
+ # TODO: is this condition correct for arbitraty face dimension in |
|
237 |
+ # d>2? |
|
238 |
+ raise RuntimeError("face has too few or too many neighbors.") |
|
239 |
+ |
|
240 |
+ new_simplices = [others + new_face for others in |
|
241 |
+ combinations(face, len(face) - 1)] |
|
242 |
+ |
|
243 |
+ volume_new = sum(self.volume(tri) for tri in new_simplices) |
|
244 |
+ volume_was = sum(self.volume(tri) for tri in simplices) |
|
245 |
+ |
|
246 |
+ if not np.allclose(volume_was, volume_new): |
|
247 |
+ raise RuntimeError( |
|
248 |
+ "face cannot be flipped without breaking the triangulation." |
|
249 |
+ ) |
|
250 |
+ |
|
251 |
+ for simplex in new_simplices: |
|
252 |
+ self.add_simplex(simplex) |
|
253 |
+ |
|
254 |
+ for simplex in simplices: |
|
255 |
+ self.delete_simplex(simplex) |
|
256 |
+ |
|
257 |
+ def volume(self, simplex): |
|
258 |
+ prefactor = np.math.factorial(self.dim) |
|
259 |
+ vectors = np.array([self.vertices[i] for i in simplex[1:]]) |
|
260 |
+ return abs(np.linalg.det(vectors |
|
261 |
+ - self.vertices[simplex[0]])) / prefactor |
|
262 |
+ |
|
263 |
+ def reference_invariant(self): |
|
264 |
+ """vertex_to_simplices and simplices are compatible.""" |
|
265 |
+ for vertex in range(len(self.vertices)): |
|
266 |
+ if any(vertex not in tri |
|
267 |
+ for tri in self.vertex_to_simplices[vertex]): |
|
268 |
+ return False |
|
269 |
+ for simplex in self.simplices: |
|
270 |
+ if any(simplex not in self.vertex_to_simplices[pt] |
|
271 |
+ for pt in simplex): |
|
272 |
+ return False |
|
273 |
+ return True |
|
274 |
+ |
|
275 |
+ def vertex_invariant(self, vertex): |
|
276 |
+ """Simplices originating from a vertex don't overlap.""" |
|
277 |
+ raise NotImplementedError |
|
278 |
+ |
|
279 |
+ def compute_hull(self, vertices=None, check=True): |
|
280 |
+ """Recompute hull from triangulation. |
|
281 |
+ |
|
282 |
+ Parameters |
|
283 |
+ ---------- |
|
284 |
+ vertices : set of int |
|
285 |
+ check : bool, default True |
|
286 |
+ Whether to raise an error if the computed hull is different from |
|
287 |
+ stored. |
|
288 |
+ |
|
289 |
+ Returns |
|
290 |
+ ------- |
|
291 |
+ hull : set of int |
|
292 |
+ Vertices in the hull. |
|
293 |
+ """ |
|
294 |
+ counts = Counter(self.faces()) |
|
295 |
+ if any(i > 2 for i in counts.values()): |
|
296 |
+ raise RuntimeError("Broken triangulation, a d-1 dimensional face " |
|
297 |
+ "appears in more than 2 simplices.") |
|
298 |
+ |
|
299 |
+ hull = set(point for face, count in counts.items() if count == 1 |
|
300 |
+ for point in face) |
|
301 |
+ |
|
302 |
+ if check and self.hull != hull: |
|
303 |
+ raise RuntimeError("Incorrect hull value.") |
|
304 |
+ |
|
305 |
+ return hull |
|
306 |
+ |
|
307 |
+ def convex_invariant(self, vertex): |
|
308 |
+ """Hull is convex.""" |
|
309 |
+ raise NotImplementedError |