Browse code

add triangulation

Anton Akhmerov authored on 10/12/2017 15:13:32 • Bas Nijholt committed on 11/07/2018 05:27:20
Showing 1 changed files
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