Browse code

Merge branch 'initial-simplex' into 'master'

Move some logic from the learner to the triangulation.
Triangulation now accepts any number of vertices >= dim + 1,
instead of only num_vertices == dim + 1.

Closes #71

See merge request qt/adaptive!89

Joseph Weston authored on 25/07/2018 17:25:34
Showing 3 changed files
... ...
@@ -14,17 +14,6 @@ from .triangulation import Triangulation
14 14
 import random
15 15
 
16 16
 
17
-def find_initial_simplex(pts, ndim):
18
-    origin = pts[0]
19
-    vecs = pts[1:] - origin
20
-    if np.linalg.matrix_rank(vecs) < ndim:
21
-        return None  # all points are coplanar
22
-
23
-    tri = scipy.spatial.Delaunay(pts)
24
-    simplex = tri.simplices[0]  # take a random simplex from tri
25
-    return simplex
26
-
27
-
28 17
 def volume(simplex, ys=None):
29 18
     # Notice the parameter ys is there so you can use this volume method as
30 19
     # as loss function
... ...
@@ -198,19 +187,15 @@ class LearnerND(BaseLearner):
198 187
         if self._tri is not None:
199 188
             return self._tri
200 189
 
201
-        if len(self.data) < 2:
202
-            return None
203
-
204
-        initial_simplex = find_initial_simplex(self.points, self.ndim)
205
-        if initial_simplex is None:
190
+        try:
191
+            self._tri = Triangulation(self.points)
192
+            return self._tri
193
+        except ValueError:
194
+            # A ValueError is raised if we do not have enough points or
195
+            # the provided points are coplanar, so we need more points to create
196
+            # a valid triangulation
206 197
             return None
207
-        pts = self.points[initial_simplex]
208
-        self._tri = Triangulation([tuple(p) for p in pts])
209
-        to_add = [p for i, p in enumerate(self.points)
210
-                  if i not in initial_simplex]
211 198
 
212
-        for p in to_add:
213
-            self._tri.add_point(p, transform=self._transform)
214 199
         # XXX: also compute losses of initial simplex
215 200
 
216 201
     @property
... ...
@@ -225,19 +210,20 @@ class LearnerND(BaseLearner):
225 210
         point = tuple(point)
226 211
 
227 212
         if point in self.data:
228
-            return
213
+            return  # we already know about the point
229 214
 
230 215
         if value is None:
231 216
             return self._tell_pending(point)
232 217
 
233 218
         self._pending.discard(point)
219
+        tri = self.tri
234 220
         self.data[point] = value
235 221
 
236
-        if self.tri is not None:
222
+        if tri is not None:
237 223
             simplex = self._pending_to_simplex.get(point)
238 224
             if simplex is not None and not self._simplex_exists(simplex):
239 225
                 simplex = None
240
-            to_delete, to_add = self._tri.add_point(
226
+            to_delete, to_add = tri.add_point(
241 227
                 point, simplex, transform=self._transform)
242 228
             self.update_losses(to_delete, to_add)
243 229
 
... ...
@@ -313,14 +299,16 @@ class LearnerND(BaseLearner):
313 299
             if len(losses):
314 300
                 loss, simplex = heapq.heappop(losses)
315 301
 
316
-                assert self._simplex_exists(simplex), "all simplices in the heap should exist"
302
+                assert self._simplex_exists(
303
+                    simplex), "all simplices in the heap should exist"
317 304
 
318 305
                 if simplex in self._subtriangulations:
319 306
                     subtri = self._subtriangulations[simplex]
320 307
                     loss_density = loss / self.tri.volume(simplex)
321 308
                     for pend_simplex in subtri.simplices:
322 309
                         pend_loss = subtri.volume(pend_simplex) * loss_density
323
-                        heapq.heappush(pending_losses, (pend_loss, simplex, pend_simplex))
310
+                        heapq.heappush(
311
+                            pending_losses, (pend_loss, simplex, pend_simplex))
324 312
                     continue
325 313
             else:
326 314
                 loss = 0
... ...
@@ -350,7 +338,8 @@ class LearnerND(BaseLearner):
350 338
         return new_points[0], new_loss_improvements[0]
351 339
 
352 340
     def update_losses(self, to_delete: set, to_add: set):
353
-        pending_points_unbound = set()  # XXX: add the points outside the triangulation to this as well
341
+        # XXX: add the points outside the triangulation to this as well
342
+        pending_points_unbound = set()
354 343
 
355 344
         for simplex in to_delete:
356 345
             self._losses.pop(simplex, None)
... ...
@@ -415,8 +404,8 @@ class LearnerND(BaseLearner):
415 404
             raise NotImplementedError('holoviews currently does not support',
416 405
                                       '3D surface plots in bokeh.')
417 406
         if len(self.bounds) != 2:
418
-           raise NotImplementedError("Only 2D plots are implemented: You can "
419
-                                     "plot a 2D slice with 'plot_slice'.")
407
+            raise NotImplementedError("Only 2D plots are implemented: You can "
408
+                                      "plot a 2D slice with 'plot_slice'.")
420 409
         x, y = self.bounds
421 410
         lbrt = x[0], y[0], x[1], y[1]
422 411
 
... ...
@@ -3,6 +3,7 @@ from itertools import combinations, chain
3 3
 
4 4
 import numpy as np
5 5
 import math
6
+import scipy.spatial
6 7
 
7 8
 
8 9
 def fast_norm(v):
... ...
@@ -142,7 +143,7 @@ class Triangulation:
142 143
     Parameters
143 144
     ----------
144 145
     coords : 2d array-like of floats
145
-        Coordinates of vertices of the first simplex.
146
+        Coordinates of vertices.
146 147
 
147 148
     Attributes
148 149
     ----------
... ...
@@ -155,14 +156,23 @@ class Triangulation:
155 156
         index of the list.
156 157
     hull : set of int
157 158
         Exterior vertices
159
+
160
+    Raises
161
+    ------
162
+    ValueError
163
+        if the list of coordinates is incorrect or the points do not form one 
164
+        or more simplices in the 
158 165
     """
159 166
 
160 167
     def __init__(self, coords):
161 168
         if not is_iterable_and_sized(coords):
162
-            raise ValueError("Please provide a 2-dimensional list of points")
169
+            raise TypeError("Please provide a 2-dimensional list of points")
163 170
         coords = list(coords)
164 171
         if not all(is_iterable_and_sized(coord) for coord in coords):
165
-            raise ValueError("Please provide a 2-dimensional list of points")
172
+            raise TypeError("Please provide a 2-dimensional list of points")
173
+        if len(coords) == 0:
174
+            raise ValueError("Please provide at least one simplex") 
175
+            # raise now because otherwise the next line will raise a less
166 176
 
167 177
         dim = len(coords[0])
168 178
         if any(len(coord) != dim for coord in coords):
... ...
@@ -171,12 +181,12 @@ class Triangulation:
171 181
         if dim == 1:
172 182
             raise ValueError("Triangulation class only supports dim >= 2")
173 183
 
174
-        if len(coords) != dim + 1:
175
-            raise ValueError("Can only add one simplex on initialization")
184
+        if len(coords) < dim + 1:
185
+            raise ValueError("Please provide at least one simplex")
176 186
 
177 187
         coords = list(map(tuple, coords))
178 188
         vectors = np.subtract(coords[1:], coords[0])
179
-        if np.linalg.det(vectors) == 0:
189
+        if np.linalg.matrix_rank(vectors) < dim:
180 190
             raise ValueError("Initial simplex has zero volumes "
181 191
                              "(the points are linearly dependent)")
182 192
 
... ...
@@ -184,7 +194,12 @@ class Triangulation:
184 194
         self.simplices = set()
185 195
         # initialise empty set for each vertex
186 196
         self.vertex_to_simplices = [set() for _ in coords]
187
-        self.add_simplex(range(len(self.vertices)))
197
+
198
+        # find a Delaunay triangulation to start with, then we will throw it 
199
+        # away and continue with our own algorithm
200
+        initial_tri = scipy.spatial.Delaunay(coords)
201
+        for simplex in initial_tri.simplices:
202
+            self.add_simplex(simplex)
188 203
 
189 204
     def delete_simplex(self, simplex):
190 205
         simplex = tuple(sorted(simplex))
... ...
@@ -75,7 +75,7 @@ def test_triangulation_raises_exception_for_1d_list():
75 75
     # We could support 1d, but we don't for now, because it is not relevant
76 76
     # so a user has to be aware
77 77
     pts = [0, 1]
78
-    with pytest.raises(ValueError):
78
+    with pytest.raises(TypeError):
79 79
         Triangulation(pts)
80 80
 
81 81
 
... ...
@@ -292,3 +292,40 @@ def test_triangulation_is_deterministic(dim):
292 292
     t1 = _make_triangulation(points)
293 293
     t2 = _make_triangulation(points)
294 294
     assert t1.simplices == t2.simplices
295
+
296
+
297
+@with_dimension
298
+def test_initialisation_raises_when_not_enough_points(dim):
299
+    deficient_simplex = _make_standard_simplex(dim)[:-1]
300
+    
301
+    with pytest.raises(ValueError):
302
+        Triangulation(deficient_simplex)
303
+
304
+
305
+@with_dimension
306
+def test_initialisation_raises_when_points_coplanar(dim):
307
+    zero_volume_simplex = _make_standard_simplex(dim)[:-1]
308
+    
309
+    new_point1 = np.average(zero_volume_simplex, axis=0)
310
+    new_point2 = np.sum(zero_volume_simplex, axis=0)
311
+    zero_volume_simplex = np.vstack((zero_volume_simplex, 
312
+                                     new_point1, new_point2))
313
+    
314
+    with pytest.raises(ValueError):
315
+        Triangulation(zero_volume_simplex)
316
+
317
+
318
+@with_dimension
319
+def test_initialisation_accepts_more_than_one_simplex(dim):
320
+    points = _make_standard_simplex(dim)
321
+    new_point = [1.1] * dim  # Point oposing the origin but outside circumsphere
322
+    points = np.vstack((points, new_point))
323
+
324
+    tri = Triangulation(points)
325
+
326
+    simplex1 = tuple(range(dim+1))
327
+    simplex2 = tuple(range(1, dim+2))
328
+
329
+    _check_triangulation_is_valid(tri)    
330
+    
331
+    assert tri.simplices == {simplex1, simplex2}