test_qhull.py 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. import os
  2. import copy
  3. import numpy as np
  4. from numpy.testing import (assert_equal, assert_almost_equal,
  5. assert_, assert_allclose, assert_array_equal)
  6. import pytest
  7. from pytest import raises as assert_raises
  8. import scipy.spatial._qhull as qhull
  9. from scipy.spatial import cKDTree as KDTree # type: ignore[attr-defined]
  10. from scipy.spatial import Voronoi
  11. import itertools
  12. def sorted_tuple(x):
  13. return tuple(sorted(x))
  14. def assert_unordered_tuple_list_equal(a, b, tpl=tuple):
  15. if isinstance(a, np.ndarray):
  16. a = a.tolist()
  17. if isinstance(b, np.ndarray):
  18. b = b.tolist()
  19. a = list(map(tpl, a))
  20. a.sort()
  21. b = list(map(tpl, b))
  22. b.sort()
  23. assert_equal(a, b)
  24. np.random.seed(1234)
  25. points = [(0,0), (0,1), (1,0), (1,1), (0.5, 0.5), (0.5, 1.5)]
  26. pathological_data_1 = np.array([
  27. [-3.14,-3.14], [-3.14,-2.36], [-3.14,-1.57], [-3.14,-0.79],
  28. [-3.14,0.0], [-3.14,0.79], [-3.14,1.57], [-3.14,2.36],
  29. [-3.14,3.14], [-2.36,-3.14], [-2.36,-2.36], [-2.36,-1.57],
  30. [-2.36,-0.79], [-2.36,0.0], [-2.36,0.79], [-2.36,1.57],
  31. [-2.36,2.36], [-2.36,3.14], [-1.57,-0.79], [-1.57,0.79],
  32. [-1.57,-1.57], [-1.57,0.0], [-1.57,1.57], [-1.57,-3.14],
  33. [-1.57,-2.36], [-1.57,2.36], [-1.57,3.14], [-0.79,-1.57],
  34. [-0.79,1.57], [-0.79,-3.14], [-0.79,-2.36], [-0.79,-0.79],
  35. [-0.79,0.0], [-0.79,0.79], [-0.79,2.36], [-0.79,3.14],
  36. [0.0,-3.14], [0.0,-2.36], [0.0,-1.57], [0.0,-0.79], [0.0,0.0],
  37. [0.0,0.79], [0.0,1.57], [0.0,2.36], [0.0,3.14], [0.79,-3.14],
  38. [0.79,-2.36], [0.79,-0.79], [0.79,0.0], [0.79,0.79],
  39. [0.79,2.36], [0.79,3.14], [0.79,-1.57], [0.79,1.57],
  40. [1.57,-3.14], [1.57,-2.36], [1.57,2.36], [1.57,3.14],
  41. [1.57,-1.57], [1.57,0.0], [1.57,1.57], [1.57,-0.79],
  42. [1.57,0.79], [2.36,-3.14], [2.36,-2.36], [2.36,-1.57],
  43. [2.36,-0.79], [2.36,0.0], [2.36,0.79], [2.36,1.57],
  44. [2.36,2.36], [2.36,3.14], [3.14,-3.14], [3.14,-2.36],
  45. [3.14,-1.57], [3.14,-0.79], [3.14,0.0], [3.14,0.79],
  46. [3.14,1.57], [3.14,2.36], [3.14,3.14],
  47. ])
  48. pathological_data_2 = np.array([
  49. [-1, -1], [-1, 0], [-1, 1],
  50. [0, -1], [0, 0], [0, 1],
  51. [1, -1 - np.finfo(np.float64).eps], [1, 0], [1, 1],
  52. ])
  53. bug_2850_chunks = [np.random.rand(10, 2),
  54. np.array([[0,0], [0,1], [1,0], [1,1]]) # add corners
  55. ]
  56. # same with some additional chunks
  57. bug_2850_chunks_2 = (bug_2850_chunks +
  58. [np.random.rand(10, 2),
  59. 0.25 + np.array([[0,0], [0,1], [1,0], [1,1]])])
  60. DATASETS = {
  61. 'some-points': np.asarray(points),
  62. 'random-2d': np.random.rand(30, 2),
  63. 'random-3d': np.random.rand(30, 3),
  64. 'random-4d': np.random.rand(30, 4),
  65. 'random-5d': np.random.rand(30, 5),
  66. 'random-6d': np.random.rand(10, 6),
  67. 'random-7d': np.random.rand(10, 7),
  68. 'random-8d': np.random.rand(10, 8),
  69. 'pathological-1': pathological_data_1,
  70. 'pathological-2': pathological_data_2
  71. }
  72. INCREMENTAL_DATASETS = {
  73. 'bug-2850': (bug_2850_chunks, None),
  74. 'bug-2850-2': (bug_2850_chunks_2, None),
  75. }
  76. def _add_inc_data(name, chunksize):
  77. """
  78. Generate incremental datasets from basic data sets
  79. """
  80. points = DATASETS[name]
  81. ndim = points.shape[1]
  82. opts = None
  83. nmin = ndim + 2
  84. if name == 'some-points':
  85. # since Qz is not allowed, use QJ
  86. opts = 'QJ Pp'
  87. elif name == 'pathological-1':
  88. # include enough points so that we get different x-coordinates
  89. nmin = 12
  90. chunks = [points[:nmin]]
  91. for j in range(nmin, len(points), chunksize):
  92. chunks.append(points[j:j+chunksize])
  93. new_name = f"{name}-chunk-{chunksize}"
  94. assert new_name not in INCREMENTAL_DATASETS
  95. INCREMENTAL_DATASETS[new_name] = (chunks, opts)
  96. for name in DATASETS:
  97. for chunksize in 1, 4, 16:
  98. _add_inc_data(name, chunksize)
  99. class Test_Qhull:
  100. def test_swapping(self):
  101. # Check that Qhull state swapping works
  102. x = qhull._Qhull(b'v',
  103. np.array([[0,0],[0,1],[1,0],[1,1.],[0.5,0.5]]),
  104. b'Qz')
  105. xd = copy.deepcopy(x.get_voronoi_diagram())
  106. y = qhull._Qhull(b'v',
  107. np.array([[0,0],[0,1],[1,0],[1,2.]]),
  108. b'Qz')
  109. yd = copy.deepcopy(y.get_voronoi_diagram())
  110. xd2 = copy.deepcopy(x.get_voronoi_diagram())
  111. x.close()
  112. yd2 = copy.deepcopy(y.get_voronoi_diagram())
  113. y.close()
  114. assert_raises(RuntimeError, x.get_voronoi_diagram)
  115. assert_raises(RuntimeError, y.get_voronoi_diagram)
  116. assert_allclose(xd[0], xd2[0])
  117. assert_unordered_tuple_list_equal(xd[1], xd2[1], tpl=sorted_tuple)
  118. assert_unordered_tuple_list_equal(xd[2], xd2[2], tpl=sorted_tuple)
  119. assert_unordered_tuple_list_equal(xd[3], xd2[3], tpl=sorted_tuple)
  120. assert_array_equal(xd[4], xd2[4])
  121. assert_allclose(yd[0], yd2[0])
  122. assert_unordered_tuple_list_equal(yd[1], yd2[1], tpl=sorted_tuple)
  123. assert_unordered_tuple_list_equal(yd[2], yd2[2], tpl=sorted_tuple)
  124. assert_unordered_tuple_list_equal(yd[3], yd2[3], tpl=sorted_tuple)
  125. assert_array_equal(yd[4], yd2[4])
  126. x.close()
  127. assert_raises(RuntimeError, x.get_voronoi_diagram)
  128. y.close()
  129. assert_raises(RuntimeError, y.get_voronoi_diagram)
  130. def test_issue_8051(self):
  131. points = np.array(
  132. [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2],[2, 0], [2, 1], [2, 2]]
  133. )
  134. Voronoi(points)
  135. class TestUtilities:
  136. """
  137. Check that utility functions work.
  138. """
  139. def test_find_simplex(self):
  140. # Simple check that simplex finding works
  141. points = np.array([(0,0), (0,1), (1,1), (1,0)], dtype=np.float64)
  142. tri = qhull.Delaunay(points)
  143. # +---+
  144. # |\ 0|
  145. # | \ |
  146. # |1 \|
  147. # +---+
  148. assert_equal(tri.simplices, [[1, 3, 2], [3, 1, 0]])
  149. for p in [(0.25, 0.25, 1),
  150. (0.75, 0.75, 0),
  151. (0.3, 0.2, 1)]:
  152. i = tri.find_simplex(p[:2])
  153. assert_equal(i, p[2], err_msg=f'{p!r}')
  154. j = qhull.tsearch(tri, p[:2])
  155. assert_equal(i, j)
  156. def test_plane_distance(self):
  157. # Compare plane distance from hyperplane equations obtained from Qhull
  158. # to manually computed plane equations
  159. x = np.array([(0,0), (1, 1), (1, 0), (0.99189033, 0.37674127),
  160. (0.99440079, 0.45182168)], dtype=np.float64)
  161. p = np.array([0.99966555, 0.15685619], dtype=np.float64)
  162. tri = qhull.Delaunay(x)
  163. z = tri.lift_points(x)
  164. pz = tri.lift_points(p)
  165. dist = tri.plane_distance(p)
  166. for j, v in enumerate(tri.simplices):
  167. x1 = z[v[0]]
  168. x2 = z[v[1]]
  169. x3 = z[v[2]]
  170. n = np.cross(x1 - x3, x2 - x3)
  171. n /= np.sqrt(np.dot(n, n))
  172. n *= -np.sign(n[2])
  173. d = np.dot(n, pz - x3)
  174. assert_almost_equal(dist[j], d)
  175. def test_convex_hull(self):
  176. # Simple check that the convex hull seems to works
  177. points = np.array([(0,0), (0,1), (1,1), (1,0)], dtype=np.float64)
  178. tri = qhull.Delaunay(points)
  179. # +---+
  180. # |\ 0|
  181. # | \ |
  182. # |1 \|
  183. # +---+
  184. assert_equal(tri.convex_hull, [[3, 2], [1, 2], [1, 0], [3, 0]])
  185. def test_volume_area(self):
  186. #Basic check that we get back the correct volume and area for a cube
  187. points = np.array([(0, 0, 0), (0, 1, 0), (1, 0, 0), (1, 1, 0),
  188. (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)])
  189. hull = qhull.ConvexHull(points)
  190. assert_allclose(hull.volume, 1., rtol=1e-14,
  191. err_msg="Volume of cube is incorrect")
  192. assert_allclose(hull.area, 6., rtol=1e-14,
  193. err_msg="Area of cube is incorrect")
  194. def test_random_volume_area(self):
  195. #Test that the results for a random 10-point convex are
  196. #coherent with the output of qconvex Qt s FA
  197. points = np.array([(0.362568364506, 0.472712355305, 0.347003084477),
  198. (0.733731893414, 0.634480295684, 0.950513180209),
  199. (0.511239955611, 0.876839441267, 0.418047827863),
  200. (0.0765906233393, 0.527373281342, 0.6509863541),
  201. (0.146694972056, 0.596725793348, 0.894860986685),
  202. (0.513808585741, 0.069576205858, 0.530890338876),
  203. (0.512343805118, 0.663537132612, 0.037689295973),
  204. (0.47282965018, 0.462176697655, 0.14061843691),
  205. (0.240584597123, 0.778660020591, 0.722913476339),
  206. (0.951271745935, 0.967000673944, 0.890661319684)])
  207. hull = qhull.ConvexHull(points)
  208. assert_allclose(hull.volume, 0.14562013, rtol=1e-07,
  209. err_msg="Volume of random polyhedron is incorrect")
  210. assert_allclose(hull.area, 1.6670425, rtol=1e-07,
  211. err_msg="Area of random polyhedron is incorrect")
  212. def test_incremental_volume_area_random_input(self):
  213. """Test that incremental mode gives the same volume/area as
  214. non-incremental mode and incremental mode with restart"""
  215. nr_points = 20
  216. dim = 3
  217. points = np.random.random((nr_points, dim))
  218. inc_hull = qhull.ConvexHull(points[:dim+1, :], incremental=True)
  219. inc_restart_hull = qhull.ConvexHull(points[:dim+1, :], incremental=True)
  220. for i in range(dim+1, nr_points):
  221. hull = qhull.ConvexHull(points[:i+1, :])
  222. inc_hull.add_points(points[i:i+1, :])
  223. inc_restart_hull.add_points(points[i:i+1, :], restart=True)
  224. assert_allclose(hull.volume, inc_hull.volume, rtol=1e-7)
  225. assert_allclose(hull.volume, inc_restart_hull.volume, rtol=1e-7)
  226. assert_allclose(hull.area, inc_hull.area, rtol=1e-7)
  227. assert_allclose(hull.area, inc_restart_hull.area, rtol=1e-7)
  228. def _check_barycentric_transforms(self, tri, err_msg="",
  229. unit_cube=False,
  230. unit_cube_tol=0):
  231. """Check that a triangulation has reasonable barycentric transforms"""
  232. vertices = tri.points[tri.simplices]
  233. sc = 1/(tri.ndim + 1.0)
  234. centroids = vertices.sum(axis=1) * sc
  235. # Either: (i) the simplex has a `nan` barycentric transform,
  236. # or, (ii) the centroid is in the simplex
  237. def barycentric_transform(tr, x):
  238. r = tr[:,-1,:]
  239. Tinv = tr[:,:-1,:]
  240. return np.einsum('ijk,ik->ij', Tinv, x - r)
  241. eps = np.finfo(float).eps
  242. c = barycentric_transform(tri.transform, centroids)
  243. with np.errstate(invalid="ignore"):
  244. ok = np.isnan(c).all(axis=1) | (abs(c - sc)/sc < 0.1).all(axis=1)
  245. assert_(ok.all(), f"{err_msg} {np.nonzero(~ok)}")
  246. # Invalid simplices must be (nearly) zero volume
  247. q = vertices[:,:-1,:] - vertices[:,-1,None,:]
  248. volume = np.array([np.linalg.det(q[k,:,:])
  249. for k in range(tri.nsimplex)])
  250. ok = np.isfinite(tri.transform[:,0,0]) | (volume < np.sqrt(eps))
  251. assert_(ok.all(), f"{err_msg} {np.nonzero(~ok)}")
  252. # Also, find_simplex for the centroid should end up in some
  253. # simplex for the non-degenerate cases
  254. j = tri.find_simplex(centroids)
  255. ok = (j != -1) | np.isnan(tri.transform[:,0,0])
  256. assert_(ok.all(), f"{err_msg} {np.nonzero(~ok)}")
  257. if unit_cube:
  258. # If in unit cube, no interior point should be marked out of hull
  259. at_boundary = (centroids <= unit_cube_tol).any(axis=1)
  260. at_boundary |= (centroids >= 1 - unit_cube_tol).any(axis=1)
  261. ok = (j != -1) | at_boundary
  262. assert_(ok.all(), f"{err_msg} {np.nonzero(~ok)}")
  263. @pytest.mark.fail_slow(10)
  264. def test_degenerate_barycentric_transforms(self):
  265. # The triangulation should not produce invalid barycentric
  266. # transforms that stump the simplex finding
  267. data = np.load(os.path.join(os.path.dirname(__file__), 'data',
  268. 'degenerate_pointset.npz'))
  269. points = data['c']
  270. data.close()
  271. tri = qhull.Delaunay(points)
  272. # Check that there are not too many invalid simplices
  273. bad_count = np.isnan(tri.transform[:,0,0]).sum()
  274. assert_(bad_count < 23, bad_count)
  275. # Check the transforms
  276. self._check_barycentric_transforms(tri)
  277. @pytest.mark.slow
  278. @pytest.mark.fail_slow(20)
  279. # OK per https://github.com/scipy/scipy/pull/20487#discussion_r1572684869
  280. def test_more_barycentric_transforms(self):
  281. # Triangulate some "nasty" grids
  282. eps = np.finfo(float).eps
  283. npoints = {2: 70, 3: 11, 4: 5, 5: 3}
  284. for ndim in range(2, 6):
  285. # Generate an uniform grid in n-d unit cube
  286. x = np.linspace(0, 1, npoints[ndim])
  287. grid = np.c_[
  288. list(map(np.ravel, np.broadcast_arrays(*np.ix_(*([x]*ndim)))))
  289. ].T
  290. err_msg = f"ndim={ndim}"
  291. # Check using regular grid
  292. tri = qhull.Delaunay(grid)
  293. self._check_barycentric_transforms(tri, err_msg=err_msg,
  294. unit_cube=True)
  295. # Check with eps-perturbations
  296. np.random.seed(1234)
  297. m = (np.random.rand(grid.shape[0]) < 0.2)
  298. grid[m,:] += 2*eps*(np.random.rand(*grid[m,:].shape) - 0.5)
  299. tri = qhull.Delaunay(grid)
  300. self._check_barycentric_transforms(tri, err_msg=err_msg,
  301. unit_cube=True,
  302. unit_cube_tol=2*eps)
  303. # Check with duplicated data
  304. tri = qhull.Delaunay(np.r_[grid, grid])
  305. self._check_barycentric_transforms(tri, err_msg=err_msg,
  306. unit_cube=True,
  307. unit_cube_tol=2*eps)
  308. class TestVertexNeighborVertices:
  309. def _check(self, tri):
  310. expected = [set() for j in range(tri.points.shape[0])]
  311. for s in tri.simplices:
  312. for a in s:
  313. for b in s:
  314. if a != b:
  315. expected[a].add(b)
  316. indptr, indices = tri.vertex_neighbor_vertices
  317. got = [set(map(int, indices[indptr[j]:indptr[j+1]]))
  318. for j in range(tri.points.shape[0])]
  319. assert_equal(got, expected, err_msg=f"{got!r} != {expected!r}")
  320. def test_triangle(self):
  321. points = np.array([(0,0), (0,1), (1,0)], dtype=np.float64)
  322. tri = qhull.Delaunay(points)
  323. self._check(tri)
  324. def test_rectangle(self):
  325. points = np.array([(0,0), (0,1), (1,1), (1,0)], dtype=np.float64)
  326. tri = qhull.Delaunay(points)
  327. self._check(tri)
  328. def test_complicated(self):
  329. points = np.array([(0,0), (0,1), (1,1), (1,0),
  330. (0.5, 0.5), (0.9, 0.5)], dtype=np.float64)
  331. tri = qhull.Delaunay(points)
  332. self._check(tri)
  333. class TestDelaunay:
  334. """
  335. Check that triangulation works.
  336. """
  337. def test_masked_array_fails(self):
  338. masked_array = np.ma.masked_all(1)
  339. assert_raises(ValueError, qhull.Delaunay, masked_array)
  340. # Shouldn't be inherently unsafe; retry with cpython 3.14 once traceback
  341. # thread safety issues are fixed (also goes for other test with same name
  342. # further down)
  343. def test_array_with_nans_fails(self):
  344. points_with_nan = np.array([(0,0), (0,1), (1,1), (1,np.nan)], dtype=np.float64)
  345. assert_raises(ValueError, qhull.Delaunay, points_with_nan)
  346. def test_nd_simplex(self):
  347. # simple smoke test: triangulate a n-dimensional simplex
  348. for nd in range(2, 8):
  349. points = np.zeros((nd+1, nd))
  350. for j in range(nd):
  351. points[j,j] = 1.0
  352. points[-1,:] = 1.0
  353. tri = qhull.Delaunay(points)
  354. tri.simplices.sort()
  355. assert_equal(tri.simplices, np.arange(nd+1, dtype=int)[None, :])
  356. assert_equal(tri.neighbors, -1 + np.zeros((nd+1), dtype=int)[None,:])
  357. def test_2d_square(self):
  358. # simple smoke test: 2d square
  359. points = np.array([(0,0), (0,1), (1,1), (1,0)], dtype=np.float64)
  360. tri = qhull.Delaunay(points)
  361. assert_equal(tri.simplices, [[1, 3, 2], [3, 1, 0]])
  362. assert_equal(tri.neighbors, [[-1, -1, 1], [-1, -1, 0]])
  363. def test_duplicate_points(self):
  364. x = np.array([0, 1, 0, 1], dtype=np.float64)
  365. y = np.array([0, 0, 1, 1], dtype=np.float64)
  366. xp = np.r_[x, x]
  367. yp = np.r_[y, y]
  368. # shouldn't fail on duplicate points
  369. qhull.Delaunay(np.c_[x, y])
  370. qhull.Delaunay(np.c_[xp, yp])
  371. def test_pathological(self):
  372. # both should succeed
  373. points = DATASETS['pathological-1']
  374. tri = qhull.Delaunay(points)
  375. assert_equal(tri.points[tri.simplices].max(), points.max())
  376. assert_equal(tri.points[tri.simplices].min(), points.min())
  377. points = DATASETS['pathological-2']
  378. tri = qhull.Delaunay(points)
  379. assert_equal(tri.points[tri.simplices].max(), points.max())
  380. assert_equal(tri.points[tri.simplices].min(), points.min())
  381. def test_joggle(self):
  382. # Check that the option QJ indeed guarantees that all input points
  383. # occur as vertices of the triangulation
  384. points = np.random.rand(10, 2)
  385. points = np.r_[points, points] # duplicate input data
  386. tri = qhull.Delaunay(points, qhull_options="QJ Qbb Pp")
  387. assert_array_equal(np.unique(tri.simplices.ravel()),
  388. np.arange(len(points)))
  389. def test_coplanar(self):
  390. # Check that the coplanar point output option indeed works
  391. points = np.random.rand(10, 2)
  392. points = np.r_[points, points] # duplicate input data
  393. tri = qhull.Delaunay(points)
  394. assert_(len(np.unique(tri.simplices.ravel())) == len(points)//2)
  395. assert_(len(tri.coplanar) == len(points)//2)
  396. assert_(len(np.unique(tri.coplanar[:,2])) == len(points)//2)
  397. assert_(np.all(tri.vertex_to_simplex >= 0))
  398. def test_furthest_site(self):
  399. points = [(0, 0), (0, 1), (1, 0), (0.5, 0.5), (1.1, 1.1)]
  400. tri = qhull.Delaunay(points, furthest_site=True)
  401. expected = np.array([(1, 4, 0), (4, 2, 0)]) # from Qhull
  402. assert_array_equal(tri.simplices, expected)
  403. @pytest.mark.parametrize("name", sorted(INCREMENTAL_DATASETS))
  404. def test_incremental(self, name):
  405. # Test incremental construction of the triangulation
  406. chunks, opts = INCREMENTAL_DATASETS[name]
  407. points = np.concatenate(chunks, axis=0)
  408. obj = qhull.Delaunay(chunks[0], incremental=True,
  409. qhull_options=opts)
  410. for chunk in chunks[1:]:
  411. obj.add_points(chunk)
  412. obj2 = qhull.Delaunay(points)
  413. obj3 = qhull.Delaunay(chunks[0], incremental=True,
  414. qhull_options=opts)
  415. if len(chunks) > 1:
  416. obj3.add_points(np.concatenate(chunks[1:], axis=0),
  417. restart=True)
  418. # Check that the incremental mode agrees with upfront mode
  419. if name.startswith('pathological'):
  420. # XXX: These produce valid but different triangulations.
  421. # They look OK when plotted, but how to check them?
  422. assert_array_equal(np.unique(obj.simplices.ravel()),
  423. np.arange(points.shape[0]))
  424. assert_array_equal(np.unique(obj2.simplices.ravel()),
  425. np.arange(points.shape[0]))
  426. else:
  427. assert_unordered_tuple_list_equal(obj.simplices, obj2.simplices,
  428. tpl=sorted_tuple)
  429. assert_unordered_tuple_list_equal(obj2.simplices, obj3.simplices,
  430. tpl=sorted_tuple)
  431. def assert_hulls_equal(points, facets_1, facets_2):
  432. # Check that two convex hulls constructed from the same point set
  433. # are equal
  434. facets_1 = set(map(sorted_tuple, facets_1))
  435. facets_2 = set(map(sorted_tuple, facets_2))
  436. if facets_1 != facets_2 and points.shape[1] == 2:
  437. # The direct check fails for the pathological cases
  438. # --- then the convex hull from Delaunay differs (due
  439. # to rounding error etc.) from the hull computed
  440. # otherwise, by the question whether (tricoplanar)
  441. # points that lie almost exactly on the hull are
  442. # included as vertices of the hull or not.
  443. #
  444. # So we check the result, and accept it if the Delaunay
  445. # hull line segments are a subset of the usual hull.
  446. eps = 1000 * np.finfo(float).eps
  447. for a, b in facets_1:
  448. for ap, bp in facets_2:
  449. t = points[bp] - points[ap]
  450. t /= np.linalg.norm(t) # tangent
  451. n = np.array([-t[1], t[0]]) # normal
  452. # check that the two line segments are parallel
  453. # to the same line
  454. c1 = np.dot(n, points[b] - points[ap])
  455. c2 = np.dot(n, points[a] - points[ap])
  456. if not np.allclose(np.dot(c1, n), 0):
  457. continue
  458. if not np.allclose(np.dot(c2, n), 0):
  459. continue
  460. # Check that the segment (a, b) is contained in (ap, bp)
  461. c1 = np.dot(t, points[a] - points[ap])
  462. c2 = np.dot(t, points[b] - points[ap])
  463. c3 = np.dot(t, points[bp] - points[ap])
  464. if c1 < -eps or c1 > c3 + eps:
  465. continue
  466. if c2 < -eps or c2 > c3 + eps:
  467. continue
  468. # OK:
  469. break
  470. else:
  471. raise AssertionError("comparison fails")
  472. # it was OK
  473. return
  474. assert_equal(facets_1, facets_2)
  475. class TestConvexHull:
  476. def test_masked_array_fails(self):
  477. masked_array = np.ma.masked_all(1)
  478. assert_raises(ValueError, qhull.ConvexHull, masked_array)
  479. def test_array_with_nans_fails(self):
  480. points_with_nan = np.array([(0,0), (1,1), (2,np.nan)], dtype=np.float64)
  481. assert_raises(ValueError, qhull.ConvexHull, points_with_nan)
  482. @pytest.mark.parametrize("name", sorted(DATASETS))
  483. def test_hull_consistency_tri(self, name):
  484. # Check that a convex hull returned by qhull in ndim
  485. # and the hull constructed from ndim delaunay agree
  486. points = DATASETS[name]
  487. tri = qhull.Delaunay(points)
  488. hull = qhull.ConvexHull(points)
  489. assert_hulls_equal(points, tri.convex_hull, hull.simplices)
  490. # Check that the hull extremes are as expected
  491. if points.shape[1] == 2:
  492. assert_equal(np.unique(hull.simplices), np.sort(hull.vertices))
  493. else:
  494. assert_equal(np.unique(hull.simplices), hull.vertices)
  495. @pytest.mark.parametrize("name", sorted(INCREMENTAL_DATASETS))
  496. def test_incremental(self, name):
  497. # Test incremental construction of the convex hull
  498. chunks, _ = INCREMENTAL_DATASETS[name]
  499. points = np.concatenate(chunks, axis=0)
  500. obj = qhull.ConvexHull(chunks[0], incremental=True)
  501. for chunk in chunks[1:]:
  502. obj.add_points(chunk)
  503. obj2 = qhull.ConvexHull(points)
  504. obj3 = qhull.ConvexHull(chunks[0], incremental=True)
  505. if len(chunks) > 1:
  506. obj3.add_points(np.concatenate(chunks[1:], axis=0),
  507. restart=True)
  508. # Check that the incremental mode agrees with upfront mode
  509. assert_hulls_equal(points, obj.simplices, obj2.simplices)
  510. assert_hulls_equal(points, obj.simplices, obj3.simplices)
  511. def test_vertices_2d(self):
  512. # The vertices should be in counterclockwise order in 2-D
  513. np.random.seed(1234)
  514. points = np.random.rand(30, 2)
  515. hull = qhull.ConvexHull(points)
  516. assert_equal(np.unique(hull.simplices), np.sort(hull.vertices))
  517. # Check counterclockwiseness
  518. x, y = hull.points[hull.vertices].T
  519. angle = np.arctan2(y - y.mean(), x - x.mean())
  520. assert_(np.all(np.diff(np.unwrap(angle)) > 0))
  521. def test_volume_area(self):
  522. # Basic check that we get back the correct volume and area for a cube
  523. points = np.array([(0, 0, 0), (0, 1, 0), (1, 0, 0), (1, 1, 0),
  524. (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)])
  525. tri = qhull.ConvexHull(points)
  526. assert_allclose(tri.volume, 1., rtol=1e-14)
  527. assert_allclose(tri.area, 6., rtol=1e-14)
  528. @pytest.mark.parametrize("incremental", [False, True])
  529. def test_good2d(self, incremental):
  530. # Make sure the QGn option gives the correct value of "good".
  531. points = np.array([[0.2, 0.2],
  532. [0.2, 0.4],
  533. [0.4, 0.4],
  534. [0.4, 0.2],
  535. [0.3, 0.6]])
  536. hull = qhull.ConvexHull(points=points,
  537. incremental=incremental,
  538. qhull_options='QG4')
  539. expected = np.array([False, True, False, False], dtype=bool)
  540. actual = hull.good
  541. assert_equal(actual, expected)
  542. @pytest.mark.parametrize("visibility", [
  543. "QG4", # visible=True
  544. "QG-4", # visible=False
  545. ])
  546. @pytest.mark.parametrize("new_gen, expected", [
  547. # add generator that places QG4 inside hull
  548. # so all facets are invisible
  549. (np.array([[0.3, 0.7]]),
  550. np.array([False, False, False, False, False], dtype=bool)),
  551. # adding a generator on the opposite side of the square
  552. # should preserve the single visible facet & add one invisible
  553. # facet
  554. (np.array([[0.3, -0.7]]),
  555. np.array([False, True, False, False, False], dtype=bool)),
  556. # split the visible facet on top of the square into two
  557. # visible facets, with visibility at the end of the array
  558. # because add_points concatenates
  559. (np.array([[0.3, 0.41]]),
  560. np.array([False, False, False, True, True], dtype=bool)),
  561. # with our current Qhull options, coplanarity will not count
  562. # for visibility; this case shifts one visible & one invisible
  563. # facet & adds a coplanar facet
  564. # simplex at index position 2 is the shifted visible facet
  565. # the final simplex is the coplanar facet
  566. (np.array([[0.5, 0.6], [0.6, 0.6]]),
  567. np.array([False, False, True, False, False], dtype=bool)),
  568. # place the new generator such that it envelops the query
  569. # point within the convex hull, but only just barely within
  570. # the double precision limit
  571. # NOTE: testing exact degeneracy is less predictable than this
  572. # scenario, perhaps because of the default Qt option we have
  573. # enabled for Qhull to handle precision matters
  574. (np.array([[0.3, 0.6 + 1e-16]]),
  575. np.array([False, False, False, False, False], dtype=bool)),
  576. ])
  577. def test_good2d_incremental_changes(self, new_gen, expected,
  578. visibility):
  579. # use the usual square convex hull
  580. # generators from test_good2d
  581. points = np.array([[0.2, 0.2],
  582. [0.2, 0.4],
  583. [0.4, 0.4],
  584. [0.4, 0.2],
  585. [0.3, 0.6]])
  586. hull = qhull.ConvexHull(points=points,
  587. incremental=True,
  588. qhull_options=visibility)
  589. hull.add_points(new_gen)
  590. actual = hull.good
  591. if '-' in visibility:
  592. expected = np.invert(expected)
  593. assert_equal(actual, expected)
  594. @pytest.mark.parametrize("incremental", [False, True])
  595. def test_good2d_no_option(self, incremental):
  596. # handle case where good attribute doesn't exist
  597. # because Qgn or Qg-n wasn't specified
  598. points = np.array([[0.2, 0.2],
  599. [0.2, 0.4],
  600. [0.4, 0.4],
  601. [0.4, 0.2],
  602. [0.3, 0.6]])
  603. hull = qhull.ConvexHull(points=points,
  604. incremental=incremental)
  605. actual = hull.good
  606. assert actual is None
  607. # preserve None after incremental addition
  608. if incremental:
  609. hull.add_points(np.zeros((1, 2)))
  610. actual = hull.good
  611. assert actual is None
  612. @pytest.mark.parametrize("incremental", [False, True])
  613. def test_good2d_inside(self, incremental):
  614. # Make sure the QGn option gives the correct value of "good".
  615. # When point n is inside the convex hull of the rest, good is
  616. # all False.
  617. points = np.array([[0.2, 0.2],
  618. [0.2, 0.4],
  619. [0.4, 0.4],
  620. [0.4, 0.2],
  621. [0.3, 0.3]])
  622. hull = qhull.ConvexHull(points=points,
  623. incremental=incremental,
  624. qhull_options='QG4')
  625. expected = np.array([False, False, False, False], dtype=bool)
  626. actual = hull.good
  627. assert_equal(actual, expected)
  628. @pytest.mark.parametrize("incremental", [False, True])
  629. def test_good3d(self, incremental):
  630. # Make sure the QGn option gives the correct value of "good"
  631. # for a 3d figure
  632. points = np.array([[0.0, 0.0, 0.0],
  633. [0.90029516, -0.39187448, 0.18948093],
  634. [0.48676420, -0.72627633, 0.48536925],
  635. [0.57651530, -0.81179274, -0.09285832],
  636. [0.67846893, -0.71119562, 0.18406710]])
  637. hull = qhull.ConvexHull(points=points,
  638. incremental=incremental,
  639. qhull_options='QG0')
  640. expected = np.array([True, False, False, False], dtype=bool)
  641. assert_equal(hull.good, expected)
  642. class TestVoronoi:
  643. @pytest.mark.parametrize("qhull_opts, extra_pts", [
  644. # option Qz (default for SciPy) will add
  645. # an extra point at infinity
  646. ("Qbb Qc Qz", 1),
  647. ("Qbb Qc", 0),
  648. ])
  649. @pytest.mark.parametrize("n_pts", [50, 100])
  650. @pytest.mark.parametrize("ndim", [2, 3])
  651. def test_point_region_structure(self,
  652. qhull_opts,
  653. n_pts,
  654. extra_pts,
  655. ndim):
  656. # see gh-16773
  657. rng = np.random.default_rng(7790)
  658. points = rng.random((n_pts, ndim))
  659. vor = Voronoi(points, qhull_options=qhull_opts)
  660. pt_region = vor.point_region
  661. assert pt_region.max() == n_pts - 1 + extra_pts
  662. assert pt_region.size == len(vor.regions) - extra_pts
  663. assert len(vor.regions) == n_pts + extra_pts
  664. assert vor.points.shape[0] == n_pts
  665. # if there is an empty sublist in the Voronoi
  666. # regions data structure, it should never be
  667. # indexed because it corresponds to an internally
  668. # added point at infinity and is not a member of the
  669. # generators (input points)
  670. if extra_pts:
  671. sublens = [len(x) for x in vor.regions]
  672. # only one point at infinity (empty region)
  673. # is allowed
  674. assert sublens.count(0) == 1
  675. assert sublens.index(0) not in pt_region
  676. def test_masked_array_fails(self):
  677. masked_array = np.ma.masked_all(1)
  678. assert_raises(ValueError, qhull.Voronoi, masked_array)
  679. def test_simple(self):
  680. # Simple case with known Voronoi diagram
  681. points = [(0, 0), (0, 1), (0, 2),
  682. (1, 0), (1, 1), (1, 2),
  683. (2, 0), (2, 1), (2, 2)]
  684. # qhull v o Fv Qbb Qc Qz < dat
  685. output = """
  686. 2
  687. 5 10 1
  688. -10.101 -10.101
  689. 0.5 0.5
  690. 0.5 1.5
  691. 1.5 0.5
  692. 1.5 1.5
  693. 2 0 1
  694. 3 2 0 1
  695. 2 0 2
  696. 3 3 0 1
  697. 4 1 2 4 3
  698. 3 4 0 2
  699. 2 0 3
  700. 3 4 0 3
  701. 2 0 4
  702. 0
  703. 12
  704. 4 0 3 0 1
  705. 4 0 1 0 1
  706. 4 1 4 1 2
  707. 4 1 2 0 2
  708. 4 2 5 0 2
  709. 4 3 4 1 3
  710. 4 3 6 0 3
  711. 4 4 5 2 4
  712. 4 4 7 3 4
  713. 4 5 8 0 4
  714. 4 6 7 0 3
  715. 4 7 8 0 4
  716. """
  717. self._compare_qvoronoi(points, output)
  718. def _compare_qvoronoi(self, points, output, **kw):
  719. """Compare to output from 'qvoronoi o Fv < data' to Voronoi()"""
  720. # Parse output
  721. output = [list(map(float, x.split())) for x in output.strip().splitlines()]
  722. nvertex = int(output[1][0])
  723. vertices = list(map(tuple, output[3:2+nvertex])) # exclude inf
  724. nregion = int(output[1][1])
  725. regions = [[int(y)-1 for y in x[1:]]
  726. for x in output[2+nvertex:2+nvertex+nregion]]
  727. ridge_points = [[int(y) for y in x[1:3]]
  728. for x in output[3+nvertex+nregion:]]
  729. ridge_vertices = [[int(y)-1 for y in x[3:]]
  730. for x in output[3+nvertex+nregion:]]
  731. # Compare results
  732. vor = qhull.Voronoi(points, **kw)
  733. def sorttuple(x):
  734. return tuple(sorted(x))
  735. assert_allclose(vor.vertices, vertices)
  736. assert_equal(set(map(tuple, vor.regions)),
  737. set(map(tuple, regions)))
  738. p1 = list(zip(list(map(sorttuple, ridge_points)),
  739. list(map(sorttuple, ridge_vertices))))
  740. p2 = list(zip(list(map(sorttuple, vor.ridge_points.tolist())),
  741. list(map(sorttuple, vor.ridge_vertices))))
  742. p1.sort()
  743. p2.sort()
  744. assert_equal(p1, p2)
  745. @pytest.mark.parametrize("name", sorted(DATASETS))
  746. def test_ridges(self, name):
  747. # Check that the ridges computed by Voronoi indeed separate
  748. # the regions of nearest neighborhood, by comparing the result
  749. # to KDTree.
  750. points = DATASETS[name]
  751. tree = KDTree(points)
  752. vor = qhull.Voronoi(points)
  753. for p, v in vor.ridge_dict.items():
  754. # consider only finite ridges
  755. if not np.all(np.asarray(v) >= 0):
  756. continue
  757. ridge_midpoint = vor.vertices[v].mean(axis=0)
  758. d = 1e-6 * (points[p[0]] - ridge_midpoint)
  759. dist, k = tree.query(ridge_midpoint + d, k=1)
  760. assert_equal(k, p[0])
  761. dist, k = tree.query(ridge_midpoint - d, k=1)
  762. assert_equal(k, p[1])
  763. def test_furthest_site(self):
  764. points = [(0, 0), (0, 1), (1, 0), (0.5, 0.5), (1.1, 1.1)]
  765. # qhull v o Fv Qbb Qc Qu < dat
  766. output = """
  767. 2
  768. 3 5 1
  769. -10.101 -10.101
  770. 0.6000000000000001 0.5
  771. 0.5 0.6000000000000001
  772. 3 0 2 1
  773. 2 0 1
  774. 2 0 2
  775. 0
  776. 3 0 2 1
  777. 5
  778. 4 0 2 0 2
  779. 4 0 4 1 2
  780. 4 0 1 0 1
  781. 4 1 4 0 1
  782. 4 2 4 0 2
  783. """
  784. self._compare_qvoronoi(points, output, furthest_site=True)
  785. def test_furthest_site_flag(self):
  786. points = [(0, 0), (0, 1), (1, 0), (0.5, 0.5), (1.1, 1.1)]
  787. vor = Voronoi(points)
  788. assert_equal(vor.furthest_site,False)
  789. vor = Voronoi(points,furthest_site=True)
  790. assert_equal(vor.furthest_site,True)
  791. @pytest.mark.fail_slow(10)
  792. @pytest.mark.parametrize("name", sorted(INCREMENTAL_DATASETS))
  793. def test_incremental(self, name):
  794. # Test incremental construction of the triangulation
  795. if INCREMENTAL_DATASETS[name][0][0].shape[1] > 3:
  796. # too slow (testing of the result --- qhull is still fast)
  797. return
  798. chunks, opts = INCREMENTAL_DATASETS[name]
  799. points = np.concatenate(chunks, axis=0)
  800. obj = qhull.Voronoi(chunks[0], incremental=True,
  801. qhull_options=opts)
  802. for chunk in chunks[1:]:
  803. obj.add_points(chunk)
  804. obj2 = qhull.Voronoi(points)
  805. obj3 = qhull.Voronoi(chunks[0], incremental=True,
  806. qhull_options=opts)
  807. if len(chunks) > 1:
  808. obj3.add_points(np.concatenate(chunks[1:], axis=0),
  809. restart=True)
  810. # -- Check that the incremental mode agrees with upfront mode
  811. assert_equal(len(obj.point_region), len(obj2.point_region))
  812. assert_equal(len(obj.point_region), len(obj3.point_region))
  813. # The vertices may be in different order or duplicated in
  814. # the incremental map
  815. for objx in obj, obj3:
  816. vertex_map = {-1: -1}
  817. for i, v in enumerate(objx.vertices):
  818. for j, v2 in enumerate(obj2.vertices):
  819. if np.allclose(v, v2):
  820. vertex_map[i] = j
  821. def remap(x):
  822. if hasattr(x, '__len__'):
  823. return tuple({remap(y) for y in x})
  824. try:
  825. return vertex_map[x]
  826. except KeyError as e:
  827. message = (f"incremental result has spurious vertex "
  828. f"at {objx.vertices[x]!r}")
  829. raise AssertionError(message) from e
  830. def simplified(x):
  831. items = set(map(sorted_tuple, x))
  832. if () in items:
  833. items.remove(())
  834. items = [x for x in items if len(x) > 1]
  835. items.sort()
  836. return items
  837. assert_equal(
  838. simplified(remap(objx.regions)),
  839. simplified(obj2.regions)
  840. )
  841. assert_equal(
  842. simplified(remap(objx.ridge_vertices)),
  843. simplified(obj2.ridge_vertices)
  844. )
  845. # XXX: compare ridge_points --- not clear exactly how to do this
  846. class Test_HalfspaceIntersection:
  847. def assert_unordered_allclose(self, arr1, arr2, rtol=1e-7):
  848. """Check that every line in arr1 is only once in arr2"""
  849. assert_equal(arr1.shape, arr2.shape)
  850. truths = np.zeros((arr1.shape[0],), dtype=bool)
  851. for l1 in arr1:
  852. indexes = np.nonzero((abs(arr2 - l1) < rtol).all(axis=1))[0]
  853. assert_equal(indexes.shape, (1,))
  854. truths[indexes[0]] = True
  855. assert_(truths.all())
  856. @pytest.mark.parametrize("dt", [np.float64, int])
  857. def test_cube_halfspace_intersection(self, dt):
  858. halfspaces = np.array([[-1, 0, 0],
  859. [0, -1, 0],
  860. [1, 0, -2],
  861. [0, 1, -2]], dtype=dt)
  862. feasible_point = np.array([1, 1], dtype=dt)
  863. points = np.array([[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0]])
  864. hull = qhull.HalfspaceIntersection(halfspaces, feasible_point)
  865. assert_allclose(hull.intersections, points)
  866. def test_self_dual_polytope_intersection(self):
  867. fname = os.path.join(os.path.dirname(__file__), 'data',
  868. 'selfdual-4d-polytope.txt')
  869. ineqs = np.genfromtxt(fname)
  870. halfspaces = -np.hstack((ineqs[:, 1:], ineqs[:, :1]))
  871. feas_point = np.array([0., 0., 0., 0.])
  872. hs = qhull.HalfspaceIntersection(halfspaces, feas_point)
  873. assert_equal(hs.intersections.shape, (24, 4))
  874. assert_almost_equal(hs.dual_volume, 32.0)
  875. assert_equal(len(hs.dual_facets), 24)
  876. for facet in hs.dual_facets:
  877. assert_equal(len(facet), 6)
  878. dists = halfspaces[:, -1] + halfspaces[:, :-1].dot(feas_point)
  879. self.assert_unordered_allclose((halfspaces[:, :-1].T/dists).T, hs.dual_points)
  880. points = itertools.permutations([0., 0., 0.5, -0.5])
  881. for point in points:
  882. assert_equal(np.sum((hs.intersections == point).all(axis=1)), 1)
  883. def test_wrong_feasible_point(self):
  884. halfspaces = np.array([[-1.0, 0.0, 0.0],
  885. [0.0, -1.0, 0.0],
  886. [1.0, 0.0, -1.0],
  887. [0.0, 1.0, -1.0]])
  888. feasible_point = np.array([0.5, 0.5, 0.5])
  889. #Feasible point is (ndim,) instead of (ndim-1,)
  890. assert_raises(ValueError,
  891. qhull.HalfspaceIntersection, halfspaces, feasible_point)
  892. feasible_point = np.array([[0.5], [0.5]])
  893. #Feasible point is (ndim-1, 1) instead of (ndim-1,)
  894. assert_raises(ValueError,
  895. qhull.HalfspaceIntersection, halfspaces, feasible_point)
  896. feasible_point = np.array([[0.5, 0.5]])
  897. #Feasible point is (1, ndim-1) instead of (ndim-1,)
  898. assert_raises(ValueError,
  899. qhull.HalfspaceIntersection, halfspaces, feasible_point)
  900. feasible_point = np.array([-0.5, -0.5])
  901. #Feasible point is outside feasible region
  902. assert_raises(qhull.QhullError,
  903. qhull.HalfspaceIntersection, halfspaces, feasible_point)
  904. def test_incremental(self):
  905. #Cube
  906. halfspaces = np.array([[0., 0., -1., -0.5],
  907. [0., -1., 0., -0.5],
  908. [-1., 0., 0., -0.5],
  909. [1., 0., 0., -0.5],
  910. [0., 1., 0., -0.5],
  911. [0., 0., 1., -0.5]])
  912. #Cut each summit
  913. extra_normals = np.array([[1., 1., 1.],
  914. [1., 1., -1.],
  915. [1., -1., 1.],
  916. [1, -1., -1.]])
  917. offsets = np.array([[-1.]]*8)
  918. extra_halfspaces = np.hstack((np.vstack((extra_normals, -extra_normals)),
  919. offsets))
  920. feas_point = np.array([0., 0., 0.])
  921. inc_hs = qhull.HalfspaceIntersection(halfspaces, feas_point, incremental=True)
  922. inc_res_hs = qhull.HalfspaceIntersection(halfspaces, feas_point,
  923. incremental=True)
  924. for i, ehs in enumerate(extra_halfspaces):
  925. inc_hs.add_halfspaces(ehs[np.newaxis, :])
  926. inc_res_hs.add_halfspaces(ehs[np.newaxis, :], restart=True)
  927. total = np.vstack((halfspaces, extra_halfspaces[:i+1, :]))
  928. hs = qhull.HalfspaceIntersection(total, feas_point)
  929. assert_allclose(inc_hs.halfspaces, inc_res_hs.halfspaces)
  930. assert_allclose(inc_hs.halfspaces, hs.halfspaces)
  931. #Direct computation and restart should have points in same order
  932. assert_allclose(hs.intersections, inc_res_hs.intersections)
  933. #Incremental will have points in different order than direct computation
  934. self.assert_unordered_allclose(inc_hs.intersections, hs.intersections)
  935. inc_hs.close()
  936. def test_cube(self):
  937. # Halfspaces of the cube:
  938. halfspaces = np.array([[-1., 0., 0., 0.], # x >= 0
  939. [1., 0., 0., -1.], # x <= 1
  940. [0., -1., 0., 0.], # y >= 0
  941. [0., 1., 0., -1.], # y <= 1
  942. [0., 0., -1., 0.], # z >= 0
  943. [0., 0., 1., -1.]]) # z <= 1
  944. point = np.array([0.5, 0.5, 0.5])
  945. hs = qhull.HalfspaceIntersection(halfspaces, point)
  946. # qhalf H0.5,0.5,0.5 o < input.txt
  947. qhalf_points = np.array([
  948. [-2, 0, 0],
  949. [2, 0, 0],
  950. [0, -2, 0],
  951. [0, 2, 0],
  952. [0, 0, -2],
  953. [0, 0, 2]])
  954. qhalf_facets = [
  955. [2, 4, 0],
  956. [4, 2, 1],
  957. [5, 2, 0],
  958. [2, 5, 1],
  959. [3, 4, 1],
  960. [4, 3, 0],
  961. [5, 3, 1],
  962. [3, 5, 0]]
  963. assert len(qhalf_facets) == len(hs.dual_facets)
  964. for a, b in zip(qhalf_facets, hs.dual_facets):
  965. assert set(a) == set(b) # facet orientation can differ
  966. assert_allclose(hs.dual_points, qhalf_points)
  967. @pytest.mark.parametrize("k", range(1,4))
  968. def test_halfspace_batch(self, k):
  969. # Test that we can add halfspaces a few at a time
  970. big_square = np.array([[ 1., 0., -2.],
  971. [-1., 0., -2.],
  972. [ 0., 1., -2.],
  973. [ 0., -1., -2.]])
  974. small_square = np.array([[ 1., 0., -1.],
  975. [-1., 0., -1.],
  976. [ 0., 1., -1.],
  977. [ 0., -1., -1.]])
  978. hs = qhull.HalfspaceIntersection(big_square,
  979. np.array([0.3141, 0.2718]),
  980. incremental=True)
  981. hs.add_halfspaces(small_square[0:k,:])
  982. hs.add_halfspaces(small_square[k:4,:])
  983. hs.close()
  984. # Check the intersections are correct (they are the corners of the small square)
  985. expected_intersections = np.array([[1., 1.],
  986. [1., -1.],
  987. [-1., 1.],
  988. [-1., -1.]])
  989. actual_intersections = hs.intersections
  990. # They may be in any order, so just check that under some permutation
  991. # expected=actual.
  992. ind1 = np.lexsort((actual_intersections[:, 1], actual_intersections[:, 0]))
  993. ind2 = np.lexsort((expected_intersections[:, 1], expected_intersections[:, 0]))
  994. assert_allclose(actual_intersections[ind1], expected_intersections[ind2])
  995. @pytest.mark.parametrize("halfspaces", [
  996. (np.array([-0.70613882, -0.45589431, 0.04178256])),
  997. (np.array([[-0.70613882, -0.45589431, 0.04178256],
  998. [0.70807342, -0.45464871, -0.45969769],
  999. [0., 0.76515026, -0.35614825]])),
  1000. ])
  1001. def test_gh_19865(self, halfspaces):
  1002. # starting off with a feasible interior point and
  1003. # adding halfspaces for which it is no longer feasible
  1004. # should result in an error rather than a problematic
  1005. # intersection polytope
  1006. initial_square = np.array(
  1007. [[1, 0, -1], [0, 1, -1], [-1, 0, -1], [0, -1, -1]]
  1008. )
  1009. incremental_intersector = qhull.HalfspaceIntersection(initial_square,
  1010. np.zeros(2),
  1011. incremental=True)
  1012. with pytest.raises(qhull.QhullError, match="feasible.*-0.706.*"):
  1013. incremental_intersector.add_halfspaces(halfspaces)
  1014. def test_gh_19865_3d(self):
  1015. # 3d case where closed half space is enforced for
  1016. # feasibility
  1017. halfspaces = np.array([[1, 1, 1, -1], # doesn't exclude origin
  1018. [-1, -1, -1, -1], # doesn't exclude origin
  1019. [1, 0, 0, 0]]) # the origin is on the line
  1020. initial_cube = np.array([[1, 0, 0, -1],
  1021. [-1, 0, 0, -1],
  1022. [0, 1, 0, -1],
  1023. [0, -1, 0, -1],
  1024. [0, 0, 1, -1],
  1025. [0, 0, -1, -1]])
  1026. incremental_intersector = qhull.HalfspaceIntersection(initial_cube,
  1027. np.zeros(3),
  1028. incremental=True)
  1029. with pytest.raises(qhull.QhullError, match="feasible.*[1 0 0 0]"):
  1030. incremental_intersector.add_halfspaces(halfspaces)
  1031. def test_2d_add_halfspace_input(self):
  1032. # incrementally added halfspaces should respect the 2D
  1033. # array shape requirement
  1034. initial_square = np.array(
  1035. [[1, 0, -1], [0, 1, -1], [-1, 0, -1], [0, -1, -1]]
  1036. )
  1037. incremental_intersector = qhull.HalfspaceIntersection(initial_square,
  1038. np.zeros(2),
  1039. incremental=True)
  1040. with pytest.raises(ValueError, match="2D array"):
  1041. incremental_intersector.add_halfspaces(np.ones((4, 4, 4)))
  1042. def test_1d_add_halfspace_input(self):
  1043. # we do allow 1D `halfspaces` input to add_halfspaces()
  1044. initial_square = np.array(
  1045. [[1, 0, -1], [0, 1, -1], [-1, 0, -1], [0, -1, -1]]
  1046. )
  1047. incremental_intersector = qhull.HalfspaceIntersection(initial_square,
  1048. np.zeros(2),
  1049. incremental=True)
  1050. assert_allclose(incremental_intersector.dual_vertices, np.arange(4))
  1051. incremental_intersector.add_halfspaces(np.array([2, 2, -1]))
  1052. assert_allclose(incremental_intersector.dual_vertices, np.arange(5))
  1053. @pytest.mark.parametrize("diagram_type", [Voronoi, qhull.Delaunay])
  1054. def test_gh_20623(diagram_type):
  1055. rng = np.random.default_rng(123)
  1056. invalid_data = rng.random((4, 10, 3))
  1057. with pytest.raises(ValueError, match="dimensions"):
  1058. diagram_type(invalid_data)
  1059. def test_gh_21286():
  1060. generators = np.array([[0, 0], [0, 1.1], [1, 0], [1, 1]])
  1061. tri = qhull.Delaunay(generators)
  1062. # verify absence of segfault reported in ticket:
  1063. with pytest.raises(IndexError):
  1064. tri.find_simplex(1)
  1065. with pytest.raises(IndexError):
  1066. # strikingly, Delaunay object has shape
  1067. # () just like np.asanyarray(1) above
  1068. tri.find_simplex(tri)
  1069. def test_find_simplex_ndim_err():
  1070. generators = np.array([[0, 0], [0, 1.1], [1, 0], [1, 1]])
  1071. tri = qhull.Delaunay(generators)
  1072. with pytest.raises(ValueError):
  1073. tri.find_simplex([2, 2, 2])