test_interpnd.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import os
  2. import sys
  3. import warnings
  4. import numpy as np
  5. from pytest import raises as assert_raises
  6. import pytest
  7. from scipy._lib._array_api import xp_assert_close, assert_almost_equal
  8. from scipy._lib._testutils import check_free_memory
  9. import scipy.interpolate._interpnd as interpnd
  10. import scipy.spatial._qhull as qhull
  11. import pickle
  12. import threading
  13. _IS_32BIT = (sys.maxsize < 2**32)
  14. def data_file(basename):
  15. return os.path.join(os.path.abspath(os.path.dirname(__file__)),
  16. 'data', basename)
  17. class TestLinearNDInterpolation:
  18. def test_smoketest(self):
  19. # Test at single points
  20. x = np.array([(0,0), (-0.5,-0.5), (-0.5,0.5), (0.5, 0.5), (0.25, 0.3)],
  21. dtype=np.float64)
  22. y = np.arange(x.shape[0], dtype=np.float64)
  23. yi = interpnd.LinearNDInterpolator(x, y)(x)
  24. assert_almost_equal(y, yi)
  25. def test_smoketest_alternate(self):
  26. # Test at single points, alternate calling convention
  27. x = np.array([(0,0), (-0.5,-0.5), (-0.5,0.5), (0.5, 0.5), (0.25, 0.3)],
  28. dtype=np.float64)
  29. y = np.arange(x.shape[0], dtype=np.float64)
  30. yi = interpnd.LinearNDInterpolator((x[:,0], x[:,1]), y)(x[:,0], x[:,1])
  31. assert_almost_equal(y, yi)
  32. def test_complex_smoketest(self):
  33. # Test at single points
  34. x = np.array([(0,0), (-0.5,-0.5), (-0.5,0.5), (0.5, 0.5), (0.25, 0.3)],
  35. dtype=np.float64)
  36. y = np.arange(x.shape[0], dtype=np.float64)
  37. y = y - 3j*y
  38. yi = interpnd.LinearNDInterpolator(x, y)(x)
  39. assert_almost_equal(y, yi)
  40. def test_tri_input(self):
  41. # Test at single points
  42. x = np.array([(0,0), (-0.5,-0.5), (-0.5,0.5), (0.5, 0.5), (0.25, 0.3)],
  43. dtype=np.float64)
  44. y = np.arange(x.shape[0], dtype=np.float64)
  45. y = y - 3j*y
  46. tri = qhull.Delaunay(x)
  47. interpolator = interpnd.LinearNDInterpolator(tri, y)
  48. yi = interpolator(x)
  49. assert_almost_equal(y, yi)
  50. assert interpolator.tri is tri
  51. def test_square(self):
  52. # Test barycentric interpolation on a square against a manual
  53. # implementation
  54. points = np.array([(0,0), (0,1), (1,1), (1,0)], dtype=np.float64)
  55. values = np.array([1., 2., -3., 5.], dtype=np.float64)
  56. # NB: assume triangles (0, 1, 3) and (1, 2, 3)
  57. #
  58. # 1----2
  59. # | \ |
  60. # | \ |
  61. # 0----3
  62. def ip(x, y):
  63. t1 = (x + y <= 1)
  64. t2 = ~t1
  65. x1 = x[t1]
  66. y1 = y[t1]
  67. x2 = x[t2]
  68. y2 = y[t2]
  69. z = 0*x
  70. z[t1] = (values[0]*(1 - x1 - y1)
  71. + values[1]*y1
  72. + values[3]*x1)
  73. z[t2] = (values[2]*(x2 + y2 - 1)
  74. + values[1]*(1 - x2)
  75. + values[3]*(1 - y2))
  76. return z
  77. xx, yy = np.broadcast_arrays(np.linspace(0, 1, 14)[:,None],
  78. np.linspace(0, 1, 14)[None,:])
  79. xx = xx.ravel()
  80. yy = yy.ravel()
  81. xi = np.array([xx, yy]).T.copy()
  82. zi = interpnd.LinearNDInterpolator(points, values)(xi)
  83. assert_almost_equal(zi, ip(xx, yy))
  84. def test_smoketest_rescale(self):
  85. # Test at single points
  86. x = np.array([(0, 0), (-5, -5), (-5, 5), (5, 5), (2.5, 3)],
  87. dtype=np.float64)
  88. y = np.arange(x.shape[0], dtype=np.float64)
  89. yi = interpnd.LinearNDInterpolator(x, y, rescale=True)(x)
  90. assert_almost_equal(y, yi)
  91. def test_square_rescale(self):
  92. # Test barycentric interpolation on a rectangle with rescaling
  93. # agaings the same implementation without rescaling
  94. points = np.array([(0,0), (0,100), (10,100), (10,0)], dtype=np.float64)
  95. values = np.array([1., 2., -3., 5.], dtype=np.float64)
  96. xx, yy = np.broadcast_arrays(np.linspace(0, 10, 14)[:,None],
  97. np.linspace(0, 100, 14)[None,:])
  98. xx = xx.ravel()
  99. yy = yy.ravel()
  100. xi = np.array([xx, yy]).T.copy()
  101. zi = interpnd.LinearNDInterpolator(points, values)(xi)
  102. zi_rescaled = interpnd.LinearNDInterpolator(points, values,
  103. rescale=True)(xi)
  104. assert_almost_equal(zi, zi_rescaled)
  105. def test_tripoints_input_rescale(self):
  106. # Test at single points
  107. x = np.array([(0,0), (-5,-5), (-5,5), (5, 5), (2.5, 3)],
  108. dtype=np.float64)
  109. y = np.arange(x.shape[0], dtype=np.float64)
  110. y = y - 3j*y
  111. tri = qhull.Delaunay(x)
  112. yi = interpnd.LinearNDInterpolator(tri.points, y)(x)
  113. yi_rescale = interpnd.LinearNDInterpolator(tri.points, y,
  114. rescale=True)(x)
  115. assert_almost_equal(yi, yi_rescale)
  116. def test_tri_input_rescale(self):
  117. # Test at single points
  118. x = np.array([(0,0), (-5,-5), (-5,5), (5, 5), (2.5, 3)],
  119. dtype=np.float64)
  120. y = np.arange(x.shape[0], dtype=np.float64)
  121. y = y - 3j*y
  122. tri = qhull.Delaunay(x)
  123. match = ("Rescaling is not supported when passing a "
  124. "Delaunay triangulation as ``points``.")
  125. with pytest.raises(ValueError, match=match):
  126. interpnd.LinearNDInterpolator(tri, y, rescale=True)(x)
  127. def test_pickle(self):
  128. # Test at single points
  129. np.random.seed(1234)
  130. x = np.random.rand(30, 2)
  131. y = np.random.rand(30) + 1j*np.random.rand(30)
  132. ip = interpnd.LinearNDInterpolator(x, y)
  133. ip2 = pickle.loads(pickle.dumps(ip))
  134. assert_almost_equal(ip(0.5, 0.5), ip2(0.5, 0.5))
  135. @pytest.mark.slow
  136. @pytest.mark.skipif(_IS_32BIT, reason='it fails on 32-bit')
  137. def test_threading(self):
  138. # This test was taken from issue 8856
  139. # https://github.com/scipy/scipy/issues/8856
  140. check_free_memory(10000)
  141. r_ticks = np.arange(0, 4200, 10)
  142. phi_ticks = np.arange(0, 4200, 10)
  143. r_grid, phi_grid = np.meshgrid(r_ticks, phi_ticks)
  144. def do_interp(interpolator, slice_rows, slice_cols):
  145. grid_x, grid_y = np.mgrid[slice_rows, slice_cols]
  146. res = interpolator((grid_x, grid_y))
  147. return res
  148. points = np.vstack((r_grid.ravel(), phi_grid.ravel())).T
  149. values = (r_grid * phi_grid).ravel()
  150. interpolator = interpnd.LinearNDInterpolator(points, values)
  151. worker_thread_1 = threading.Thread(
  152. target=do_interp,
  153. args=(interpolator, slice(0, 2100), slice(0, 2100)))
  154. worker_thread_2 = threading.Thread(
  155. target=do_interp,
  156. args=(interpolator, slice(2100, 4200), slice(0, 2100)))
  157. worker_thread_3 = threading.Thread(
  158. target=do_interp,
  159. args=(interpolator, slice(0, 2100), slice(2100, 4200)))
  160. worker_thread_4 = threading.Thread(
  161. target=do_interp,
  162. args=(interpolator, slice(2100, 4200), slice(2100, 4200)))
  163. worker_thread_1.start()
  164. worker_thread_2.start()
  165. worker_thread_3.start()
  166. worker_thread_4.start()
  167. worker_thread_1.join()
  168. worker_thread_2.join()
  169. worker_thread_3.join()
  170. worker_thread_4.join()
  171. class TestEstimateGradients2DGlobal:
  172. def test_smoketest(self):
  173. x = np.array([(0, 0), (0, 2),
  174. (1, 0), (1, 2), (0.25, 0.75), (0.6, 0.8)], dtype=float)
  175. tri = qhull.Delaunay(x)
  176. # Should be exact for linear functions, independent of triangulation
  177. funcs = [
  178. (lambda x, y: 0*x + 1, (0, 0)),
  179. (lambda x, y: 0 + x, (1, 0)),
  180. (lambda x, y: -2 + y, (0, 1)),
  181. (lambda x, y: 3 + 3*x + 14.15*y, (3, 14.15))
  182. ]
  183. for j, (func, grad) in enumerate(funcs):
  184. z = func(x[:,0], x[:,1])
  185. dz = interpnd.estimate_gradients_2d_global(tri, z, tol=1e-6)
  186. assert dz.shape == (6, 2)
  187. xp_assert_close(
  188. dz, np.array(grad)[None, :] + 0*dz, rtol=1e-5, atol=1e-5,
  189. err_msg=f"item {j}"
  190. )
  191. def test_regression_2359(self):
  192. # Check regression --- for certain point sets, gradient
  193. # estimation could end up in an infinite loop
  194. points = np.load(data_file('estimate_gradients_hang.npy'))
  195. values = np.random.rand(points.shape[0])
  196. tri = qhull.Delaunay(points)
  197. # This should not hang
  198. with warnings.catch_warnings():
  199. warnings.filterwarnings(
  200. "ignore",
  201. "Gradient estimation did not converge",
  202. interpnd.GradientEstimationWarning
  203. )
  204. interpnd.estimate_gradients_2d_global(tri, values, maxiter=1)
  205. class TestCloughTocher2DInterpolator:
  206. def _check_accuracy(self, func, x=None, tol=1e-6, alternate=False,
  207. rescale=False, **kw):
  208. rng = np.random.RandomState(1234)
  209. # np.random.seed(1234)
  210. if x is None:
  211. x = np.array([(0, 0), (0, 1),
  212. (1, 0), (1, 1), (0.25, 0.75), (0.6, 0.8),
  213. (0.5, 0.2)],
  214. dtype=float)
  215. if not alternate:
  216. ip = interpnd.CloughTocher2DInterpolator(x, func(x[:,0], x[:,1]),
  217. tol=1e-6, rescale=rescale)
  218. else:
  219. ip = interpnd.CloughTocher2DInterpolator((x[:,0], x[:,1]),
  220. func(x[:,0], x[:,1]),
  221. tol=1e-6, rescale=rescale)
  222. p = rng.rand(50, 2)
  223. if not alternate:
  224. a = ip(p)
  225. else:
  226. a = ip(p[:,0], p[:,1])
  227. b = func(p[:,0], p[:,1])
  228. try:
  229. xp_assert_close(a, b, **kw)
  230. except AssertionError:
  231. print("_check_accuracy: abs(a-b):", abs(a - b))
  232. print("ip.grad:", ip.grad)
  233. raise
  234. def test_linear_smoketest(self):
  235. # Should be exact for linear functions, independent of triangulation
  236. funcs = [
  237. lambda x, y: 0*x + 1,
  238. lambda x, y: 0 + x,
  239. lambda x, y: -2 + y,
  240. lambda x, y: 3 + 3*x + 14.15*y,
  241. ]
  242. for j, func in enumerate(funcs):
  243. self._check_accuracy(
  244. func, tol=1e-13, atol=1e-7, rtol=1e-7, err_msg=f"Function {j}"
  245. )
  246. self._check_accuracy(
  247. func, tol=1e-13, atol=1e-7, rtol=1e-7, alternate=True,
  248. err_msg=f"Function (alternate) {j}"
  249. )
  250. # check rescaling
  251. self._check_accuracy(
  252. func, tol=1e-13, atol=1e-7, rtol=1e-7,
  253. err_msg=f"Function (rescaled) {j}", rescale=True
  254. )
  255. self._check_accuracy(
  256. func, tol=1e-13, atol=1e-7, rtol=1e-7, alternate=True, rescale=True,
  257. err_msg=f"Function (alternate, rescaled) {j}"
  258. )
  259. def test_quadratic_smoketest(self):
  260. # Should be reasonably accurate for quadratic functions
  261. funcs = [
  262. lambda x, y: x**2,
  263. lambda x, y: y**2,
  264. lambda x, y: x**2 - y**2,
  265. lambda x, y: x*y,
  266. ]
  267. for j, func in enumerate(funcs):
  268. self._check_accuracy(
  269. func, tol=1e-9, atol=0.22, rtol=0, err_msg=f"Function {j}"
  270. )
  271. self._check_accuracy(
  272. func, tol=1e-9, atol=0.22, rtol=0, err_msg=f"Function {j}", rescale=True
  273. )
  274. def test_tri_input(self):
  275. # Test at single points
  276. x = np.array([(0,0), (-0.5,-0.5), (-0.5,0.5), (0.5, 0.5), (0.25, 0.3)],
  277. dtype=np.float64)
  278. y = np.arange(x.shape[0], dtype=np.float64)
  279. y = y - 3j*y
  280. tri = qhull.Delaunay(x)
  281. yi = interpnd.CloughTocher2DInterpolator(tri, y)(x)
  282. assert_almost_equal(y, yi)
  283. def test_tri_input_rescale(self):
  284. # Test at single points
  285. x = np.array([(0,0), (-5,-5), (-5,5), (5, 5), (2.5, 3)],
  286. dtype=np.float64)
  287. y = np.arange(x.shape[0], dtype=np.float64)
  288. y = y - 3j*y
  289. tri = qhull.Delaunay(x)
  290. match = ("Rescaling is not supported when passing a "
  291. "Delaunay triangulation as ``points``.")
  292. with pytest.raises(ValueError, match=match):
  293. interpnd.CloughTocher2DInterpolator(tri, y, rescale=True)(x)
  294. def test_tripoints_input_rescale(self):
  295. # Test at single points
  296. x = np.array([(0,0), (-5,-5), (-5,5), (5, 5), (2.5, 3)],
  297. dtype=np.float64)
  298. y = np.arange(x.shape[0], dtype=np.float64)
  299. y = y - 3j*y
  300. tri = qhull.Delaunay(x)
  301. yi = interpnd.CloughTocher2DInterpolator(tri.points, y)(x)
  302. yi_rescale = interpnd.CloughTocher2DInterpolator(tri.points, y, rescale=True)(x)
  303. assert_almost_equal(yi, yi_rescale)
  304. @pytest.mark.fail_slow(5)
  305. def test_dense(self):
  306. # Should be more accurate for dense meshes
  307. funcs = [
  308. lambda x, y: x**2,
  309. lambda x, y: y**2,
  310. lambda x, y: x**2 - y**2,
  311. lambda x, y: x*y,
  312. lambda x, y: np.cos(2*np.pi*x)*np.sin(2*np.pi*y)
  313. ]
  314. rng = np.random.RandomState(4321) # use a different seed than the check!
  315. grid = np.r_[np.array([(0,0), (0,1), (1,0), (1,1)], dtype=float),
  316. rng.rand(30*30, 2)]
  317. for j, func in enumerate(funcs):
  318. self._check_accuracy(
  319. func, x=grid, tol=1e-9, atol=5e-3, rtol=1e-2, err_msg=f"Function {j}"
  320. )
  321. self._check_accuracy(
  322. func, x=grid, tol=1e-9, atol=5e-3, rtol=1e-2,
  323. err_msg=f"Function {j}", rescale=True
  324. )
  325. def test_wrong_ndim(self):
  326. x = np.random.randn(30, 3)
  327. y = np.random.randn(30)
  328. assert_raises(ValueError, interpnd.CloughTocher2DInterpolator, x, y)
  329. def test_pickle(self):
  330. # Test at single points
  331. rng = np.random.RandomState(1234)
  332. x = rng.rand(30, 2)
  333. y = rng.rand(30) + 1j*rng.rand(30)
  334. ip = interpnd.CloughTocher2DInterpolator(x, y)
  335. ip2 = pickle.loads(pickle.dumps(ip))
  336. assert_almost_equal(ip(0.5, 0.5), ip2(0.5, 0.5))
  337. def test_boundary_tri_symmetry(self):
  338. # Interpolation at neighbourless triangles should retain
  339. # symmetry with mirroring the triangle.
  340. # Equilateral triangle
  341. points = np.array([(0, 0), (1, 0), (0.5, np.sqrt(3)/2)])
  342. values = np.array([1, 0, 0])
  343. ip = interpnd.CloughTocher2DInterpolator(points, values)
  344. # Set gradient to zero at vertices
  345. ip.grad[...] = 0
  346. # Interpolation should be symmetric vs. bisector
  347. alpha = 0.3
  348. p1 = np.array([0.5 * np.cos(alpha), 0.5 * np.sin(alpha)])
  349. p2 = np.array([0.5 * np.cos(np.pi/3 - alpha), 0.5 * np.sin(np.pi/3 - alpha)])
  350. v1 = ip(p1)
  351. v2 = ip(p2)
  352. xp_assert_close(v1, v2)
  353. # ... and affine invariant
  354. rng = np.random.RandomState(1)
  355. A = rng.randn(2, 2)
  356. b = rng.randn(2)
  357. points = A.dot(points.T).T + b[None,:]
  358. p1 = A.dot(p1) + b
  359. p2 = A.dot(p2) + b
  360. ip = interpnd.CloughTocher2DInterpolator(points, values)
  361. ip.grad[...] = 0
  362. w1 = ip(p1)
  363. w2 = ip(p2)
  364. xp_assert_close(w1, v1)
  365. xp_assert_close(w2, v2)