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
... | ... |
@@ -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} |