test_qmc.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534
  1. import os
  2. from collections import Counter
  3. from itertools import combinations, product
  4. import pytest
  5. import numpy as np
  6. from numpy.testing import (assert_allclose, assert_equal, assert_array_equal,
  7. assert_array_less)
  8. from scipy.spatial import distance
  9. from scipy.stats import shapiro
  10. from scipy.stats._sobol import _test_find_index, _test_low_0_bit
  11. from scipy.stats import qmc
  12. from scipy.stats._qmc import (
  13. van_der_corput, n_primes, primes_from_2_to,
  14. update_discrepancy, QMCEngine, _l1_norm,
  15. _perturb_discrepancy, _lloyd_centroidal_voronoi_tessellation
  16. )
  17. class TestUtils:
  18. def test_scale(self):
  19. # 1d scalar
  20. space = [[0], [1], [0.5]]
  21. out = [[-2], [6], [2]]
  22. scaled_space = qmc.scale(space, l_bounds=-2, u_bounds=6)
  23. assert_allclose(scaled_space, out)
  24. # 2d space
  25. space = [[0, 0], [1, 1], [0.5, 0.5]]
  26. bounds = np.array([[-2, 0], [6, 5]])
  27. out = [[-2, 0], [6, 5], [2, 2.5]]
  28. scaled_space = qmc.scale(space, l_bounds=bounds[0], u_bounds=bounds[1])
  29. assert_allclose(scaled_space, out)
  30. scaled_back_space = qmc.scale(scaled_space, l_bounds=bounds[0],
  31. u_bounds=bounds[1], reverse=True)
  32. assert_allclose(scaled_back_space, space)
  33. # broadcast
  34. space = [[0, 0, 0], [1, 1, 1], [0.5, 0.5, 0.5]]
  35. l_bounds, u_bounds = 0, [6, 5, 3]
  36. out = [[0, 0, 0], [6, 5, 3], [3, 2.5, 1.5]]
  37. scaled_space = qmc.scale(space, l_bounds=l_bounds, u_bounds=u_bounds)
  38. assert_allclose(scaled_space, out)
  39. def test_scale_random(self):
  40. rng = np.random.default_rng(317589836511269190194010915937762468165)
  41. sample = rng.random((30, 10))
  42. a = -rng.random(10) * 10
  43. b = rng.random(10) * 10
  44. scaled = qmc.scale(sample, a, b, reverse=False)
  45. unscaled = qmc.scale(scaled, a, b, reverse=True)
  46. assert_allclose(unscaled, sample)
  47. def test_scale_errors(self):
  48. with pytest.raises(ValueError, match=r"Sample is not a 2D array"):
  49. space = [0, 1, 0.5]
  50. qmc.scale(space, l_bounds=-2, u_bounds=6)
  51. with pytest.raises(ValueError, match=r"Bounds are not consistent"):
  52. space = [[0, 0], [1, 1], [0.5, 0.5]]
  53. bounds = np.array([[-2, 6], [6, 5]])
  54. qmc.scale(space, l_bounds=bounds[0], u_bounds=bounds[1])
  55. with pytest.raises(ValueError, match=r"'l_bounds' and 'u_bounds'"
  56. r" must be broadcastable"):
  57. space = [[0, 0], [1, 1], [0.5, 0.5]]
  58. l_bounds, u_bounds = [-2, 0, 2], [6, 5]
  59. qmc.scale(space, l_bounds=l_bounds, u_bounds=u_bounds)
  60. with pytest.raises(ValueError, match=r"'l_bounds' and 'u_bounds'"
  61. r" must be broadcastable"):
  62. space = [[0, 0], [1, 1], [0.5, 0.5]]
  63. bounds = np.array([[-2, 0, 2], [6, 5, 5]])
  64. qmc.scale(space, l_bounds=bounds[0], u_bounds=bounds[1])
  65. with pytest.raises(ValueError, match=r"Sample is not in unit "
  66. r"hypercube"):
  67. space = [[0, 0], [1, 1.5], [0.5, 0.5]]
  68. bounds = np.array([[-2, 0], [6, 5]])
  69. qmc.scale(space, l_bounds=bounds[0], u_bounds=bounds[1])
  70. with pytest.raises(ValueError, match=r"Sample is out of bounds"):
  71. out = [[-2, 0], [6, 5], [8, 2.5]]
  72. bounds = np.array([[-2, 0], [6, 5]])
  73. qmc.scale(out, l_bounds=bounds[0], u_bounds=bounds[1],
  74. reverse=True)
  75. def test_discrepancy(self):
  76. space_1 = np.array([[1, 3], [2, 6], [3, 2], [4, 5], [5, 1], [6, 4]])
  77. space_1 = (2.0 * space_1 - 1.0) / (2.0 * 6.0)
  78. space_2 = np.array([[1, 5], [2, 4], [3, 3], [4, 2], [5, 1], [6, 6]])
  79. space_2 = (2.0 * space_2 - 1.0) / (2.0 * 6.0)
  80. # From Fang et al. Design and modeling for computer experiments, 2006
  81. assert_allclose(qmc.discrepancy(space_1), 0.0081, atol=1e-4)
  82. assert_allclose(qmc.discrepancy(space_2), 0.0105, atol=1e-4)
  83. # From Zhou Y.-D. et al. Mixture discrepancy for quasi-random point
  84. # sets. Journal of Complexity, 29 (3-4), pp. 283-301, 2013.
  85. # Example 4 on Page 298
  86. sample = np.array([[2, 1, 1, 2, 2, 2],
  87. [1, 2, 2, 2, 2, 2],
  88. [2, 1, 1, 1, 1, 1],
  89. [1, 1, 1, 1, 2, 2],
  90. [1, 2, 2, 2, 1, 1],
  91. [2, 2, 2, 2, 1, 1],
  92. [2, 2, 2, 1, 2, 2]])
  93. sample = (2.0 * sample - 1.0) / (2.0 * 2.0)
  94. assert_allclose(qmc.discrepancy(sample, method='MD'), 2.5000,
  95. atol=1e-4)
  96. assert_allclose(qmc.discrepancy(sample, method='WD'), 1.3680,
  97. atol=1e-4)
  98. assert_allclose(qmc.discrepancy(sample, method='CD'), 0.3172,
  99. atol=1e-4)
  100. # From Tim P. et al. Minimizing the L2 and Linf star discrepancies
  101. # of a single point in the unit hypercube. JCAM, 2005
  102. # Table 1 on Page 283
  103. for dim in [2, 4, 8, 16, 32, 64]:
  104. ref = np.sqrt(3**(-dim))
  105. assert_allclose(qmc.discrepancy(np.array([[1]*dim]),
  106. method='L2-star'), ref)
  107. def test_discrepancy_errors(self):
  108. sample = np.array([[1, 3], [2, 6], [3, 2], [4, 5], [5, 1], [6, 4]])
  109. with pytest.raises(
  110. ValueError, match=r"Sample is not in unit hypercube"
  111. ):
  112. qmc.discrepancy(sample)
  113. with pytest.raises(ValueError, match=r"Sample is not a 2D array"):
  114. qmc.discrepancy([1, 3])
  115. sample = [[0, 0], [1, 1], [0.5, 0.5]]
  116. with pytest.raises(ValueError, match=r"'toto' is not a valid ..."):
  117. qmc.discrepancy(sample, method="toto")
  118. def test_discrepancy_parallel(self, monkeypatch):
  119. sample = np.array([[2, 1, 1, 2, 2, 2],
  120. [1, 2, 2, 2, 2, 2],
  121. [2, 1, 1, 1, 1, 1],
  122. [1, 1, 1, 1, 2, 2],
  123. [1, 2, 2, 2, 1, 1],
  124. [2, 2, 2, 2, 1, 1],
  125. [2, 2, 2, 1, 2, 2]])
  126. sample = (2.0 * sample - 1.0) / (2.0 * 2.0)
  127. assert_allclose(qmc.discrepancy(sample, method='MD', workers=8),
  128. 2.5000,
  129. atol=1e-4)
  130. assert_allclose(qmc.discrepancy(sample, method='WD', workers=8),
  131. 1.3680,
  132. atol=1e-4)
  133. assert_allclose(qmc.discrepancy(sample, method='CD', workers=8),
  134. 0.3172,
  135. atol=1e-4)
  136. # From Tim P. et al. Minimizing the L2 and Linf star discrepancies
  137. # of a single point in the unit hypercube. JCAM, 2005
  138. # Table 1 on Page 283
  139. for dim in [2, 4, 8, 16, 32, 64]:
  140. ref = np.sqrt(3 ** (-dim))
  141. assert_allclose(qmc.discrepancy(np.array([[1] * dim]),
  142. method='L2-star', workers=-1), ref)
  143. monkeypatch.setattr(os, 'cpu_count', lambda: None)
  144. with pytest.raises(NotImplementedError, match="Cannot determine the"):
  145. qmc.discrepancy(sample, workers=-1)
  146. with pytest.raises(ValueError, match="Invalid number of workers..."):
  147. qmc.discrepancy(sample, workers=-2)
  148. def test_geometric_discrepancy_errors(self):
  149. sample = np.array([[1, 3], [2, 6], [3, 2], [4, 5], [5, 1], [6, 4]])
  150. with pytest.raises(ValueError, match=r"Sample is not in unit hypercube"):
  151. qmc.geometric_discrepancy(sample)
  152. with pytest.raises(ValueError, match=r"Sample is not a 2D array"):
  153. qmc.geometric_discrepancy([1, 3])
  154. sample = [[0, 0], [1, 1], [0.5, 0.5]]
  155. with pytest.raises(ValueError, match=r"'toto' is not a valid ..."):
  156. qmc.geometric_discrepancy(sample, method="toto")
  157. sample = np.array([[0, 0], [0, 0], [0, 1]])
  158. with pytest.warns(UserWarning, match="Sample contains duplicate points."):
  159. qmc.geometric_discrepancy(sample)
  160. sample = np.array([[0.5, 0.5]])
  161. with pytest.raises(ValueError, match="Sample must contain at least two points"):
  162. qmc.geometric_discrepancy(sample)
  163. def test_geometric_discrepancy(self):
  164. sample = np.array([[0, 0], [1, 1]])
  165. assert_allclose(qmc.geometric_discrepancy(sample), np.sqrt(2))
  166. assert_allclose(qmc.geometric_discrepancy(sample, method="mst"), np.sqrt(2))
  167. sample = np.array([[0, 0], [0, 1], [0.5, 1]])
  168. assert_allclose(qmc.geometric_discrepancy(sample), 0.5)
  169. assert_allclose(qmc.geometric_discrepancy(sample, method="mst"), 0.75)
  170. sample = np.array([[0, 0], [0.25, 0.25], [1, 1]])
  171. assert_allclose(qmc.geometric_discrepancy(sample), np.sqrt(2) / 4)
  172. assert_allclose(qmc.geometric_discrepancy(sample, method="mst"), np.sqrt(2) / 2)
  173. assert_allclose(qmc.geometric_discrepancy(sample, metric="chebyshev"), 0.25)
  174. assert_allclose(
  175. qmc.geometric_discrepancy(sample, method="mst", metric="chebyshev"), 0.5
  176. )
  177. rng = np.random.default_rng(191468432622931918890291693003068437394)
  178. sample = qmc.LatinHypercube(d=3, rng=rng).random(50)
  179. assert_allclose(qmc.geometric_discrepancy(sample), 0.05106012076093356)
  180. assert_allclose(
  181. qmc.geometric_discrepancy(sample, method='mst'), 0.19704396643366182
  182. )
  183. @pytest.mark.xfail(
  184. reason="minimum_spanning_tree ignores zero distances (#18892)",
  185. strict=True,
  186. )
  187. @pytest.mark.thread_unsafe
  188. def test_geometric_discrepancy_mst_with_zero_distances(self):
  189. sample = np.array([[0, 0], [0, 0], [0, 1]])
  190. assert_allclose(qmc.geometric_discrepancy(sample, method='mst'), 0.5)
  191. def test_update_discrepancy(self):
  192. # From Fang et al. Design and modeling for computer experiments, 2006
  193. space_1 = np.array([[1, 3], [2, 6], [3, 2], [4, 5], [5, 1], [6, 4]])
  194. space_1 = (2.0 * space_1 - 1.0) / (2.0 * 6.0)
  195. disc_init = qmc.discrepancy(space_1[:-1], iterative=True)
  196. disc_iter = update_discrepancy(space_1[-1], space_1[:-1], disc_init)
  197. assert_allclose(disc_iter, 0.0081, atol=1e-4)
  198. # n<d
  199. rng = np.random.default_rng(241557431858162136881731220526394276199)
  200. space_1 = rng.random((4, 10))
  201. disc_ref = qmc.discrepancy(space_1)
  202. disc_init = qmc.discrepancy(space_1[:-1], iterative=True)
  203. disc_iter = update_discrepancy(space_1[-1], space_1[:-1], disc_init)
  204. assert_allclose(disc_iter, disc_ref, atol=1e-4)
  205. # errors
  206. with pytest.raises(ValueError, match=r"Sample is not in unit "
  207. r"hypercube"):
  208. update_discrepancy(space_1[-1], space_1[:-1] + 1, disc_init)
  209. with pytest.raises(ValueError, match=r"Sample is not a 2D array"):
  210. update_discrepancy(space_1[-1], space_1[0], disc_init)
  211. x_new = [1, 3]
  212. with pytest.raises(ValueError, match=r"x_new is not in unit "
  213. r"hypercube"):
  214. update_discrepancy(x_new, space_1[:-1], disc_init)
  215. x_new = [[0.5, 0.5]]
  216. with pytest.raises(ValueError, match=r"x_new is not a 1D array"):
  217. update_discrepancy(x_new, space_1[:-1], disc_init)
  218. x_new = [0.3, 0.1, 0]
  219. with pytest.raises(ValueError, match=r"x_new and sample must be "
  220. r"broadcastable"):
  221. update_discrepancy(x_new, space_1[:-1], disc_init)
  222. def test_perm_discrepancy(self):
  223. rng = np.random.default_rng(46449423132557934943847369749645759997)
  224. qmc_gen = qmc.LatinHypercube(5, rng=rng)
  225. sample = qmc_gen.random(10)
  226. disc = qmc.discrepancy(sample)
  227. for i in range(100):
  228. row_1 = rng.integers(10)
  229. row_2 = rng.integers(10)
  230. col = rng.integers(5)
  231. disc = _perturb_discrepancy(sample, row_1, row_2, col, disc)
  232. sample[row_1, col], sample[row_2, col] = (
  233. sample[row_2, col], sample[row_1, col])
  234. disc_reference = qmc.discrepancy(sample)
  235. assert_allclose(disc, disc_reference)
  236. def test_discrepancy_alternative_implementation(self):
  237. """Alternative definitions from Matt Haberland."""
  238. def disc_c2(x):
  239. n, s = x.shape
  240. xij = x
  241. disc1 = np.sum(np.prod((1
  242. + 1/2*np.abs(xij-0.5)
  243. - 1/2*np.abs(xij-0.5)**2), axis=1))
  244. xij = x[None, :, :]
  245. xkj = x[:, None, :]
  246. disc2 = np.sum(np.sum(np.prod(1
  247. + 1/2*np.abs(xij - 0.5)
  248. + 1/2*np.abs(xkj - 0.5)
  249. - 1/2*np.abs(xij - xkj), axis=2),
  250. axis=0))
  251. return (13/12)**s - 2/n * disc1 + 1/n**2*disc2
  252. def disc_wd(x):
  253. n, s = x.shape
  254. xij = x[None, :, :]
  255. xkj = x[:, None, :]
  256. disc = np.sum(np.sum(np.prod(3/2
  257. - np.abs(xij - xkj)
  258. + np.abs(xij - xkj)**2, axis=2),
  259. axis=0))
  260. return -(4/3)**s + 1/n**2 * disc
  261. def disc_md(x):
  262. n, s = x.shape
  263. xij = x
  264. disc1 = np.sum(np.prod((5/3
  265. - 1/4*np.abs(xij-0.5)
  266. - 1/4*np.abs(xij-0.5)**2), axis=1))
  267. xij = x[None, :, :]
  268. xkj = x[:, None, :]
  269. disc2 = np.sum(np.sum(np.prod(15/8
  270. - 1/4*np.abs(xij - 0.5)
  271. - 1/4*np.abs(xkj - 0.5)
  272. - 3/4*np.abs(xij - xkj)
  273. + 1/2*np.abs(xij - xkj)**2,
  274. axis=2), axis=0))
  275. return (19/12)**s - 2/n * disc1 + 1/n**2*disc2
  276. def disc_star_l2(x):
  277. n, s = x.shape
  278. return np.sqrt(
  279. 3 ** (-s) - 2 ** (1 - s) / n
  280. * np.sum(np.prod(1 - x ** 2, axis=1))
  281. + np.sum([
  282. np.prod(1 - np.maximum(x[k, :], x[j, :]))
  283. for k in range(n) for j in range(n)
  284. ]) / n ** 2
  285. )
  286. rng = np.random.default_rng(117065081482921065782761407107747179201)
  287. sample = rng.random((30, 10))
  288. disc_curr = qmc.discrepancy(sample, method='CD')
  289. disc_alt = disc_c2(sample)
  290. assert_allclose(disc_curr, disc_alt)
  291. disc_curr = qmc.discrepancy(sample, method='WD')
  292. disc_alt = disc_wd(sample)
  293. assert_allclose(disc_curr, disc_alt)
  294. disc_curr = qmc.discrepancy(sample, method='MD')
  295. disc_alt = disc_md(sample)
  296. assert_allclose(disc_curr, disc_alt)
  297. disc_curr = qmc.discrepancy(sample, method='L2-star')
  298. disc_alt = disc_star_l2(sample)
  299. assert_allclose(disc_curr, disc_alt)
  300. def test_n_primes(self):
  301. primes = n_primes(10)
  302. assert primes[-1] == 29
  303. primes = n_primes(168)
  304. assert primes[-1] == 997
  305. primes = n_primes(350)
  306. assert primes[-1] == 2357
  307. def test_primes(self):
  308. primes = primes_from_2_to(50)
  309. out = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
  310. assert_allclose(primes, out)
  311. class TestVDC:
  312. def test_van_der_corput(self):
  313. sample = van_der_corput(10)
  314. out = [0.0, 0.5, 0.25, 0.75, 0.125, 0.625,
  315. 0.375, 0.875, 0.0625, 0.5625]
  316. assert_allclose(sample, out)
  317. sample = van_der_corput(10, workers=4)
  318. assert_allclose(sample, out)
  319. sample = van_der_corput(10, workers=8)
  320. assert_allclose(sample, out)
  321. sample = van_der_corput(7, start_index=3)
  322. assert_allclose(sample, out[3:])
  323. def test_van_der_corput_scramble(self):
  324. rng = 338213789010180879520345496831675783177
  325. out = van_der_corput(10, scramble=True, rng=rng)
  326. sample = van_der_corput(7, start_index=3, scramble=True, rng=rng)
  327. assert_allclose(sample, out[3:])
  328. sample = van_der_corput(
  329. 7, start_index=3, scramble=True, rng=rng, workers=4
  330. )
  331. assert_allclose(sample, out[3:])
  332. sample = van_der_corput(
  333. 7, start_index=3, scramble=True, rng=rng, workers=8
  334. )
  335. assert_allclose(sample, out[3:])
  336. def test_invalid_base_error(self):
  337. with pytest.raises(ValueError, match=r"'base' must be at least 2"):
  338. van_der_corput(10, base=1)
  339. class RandomEngine(qmc.QMCEngine):
  340. def __init__(self, d, optimization=None, rng=None):
  341. super().__init__(d=d, optimization=optimization, rng=rng)
  342. def _random(self, n=1, *, workers=1):
  343. sample = self.rng.random((n, self.d))
  344. return sample
  345. def test_subclassing_QMCEngine():
  346. engine = RandomEngine(2, rng=175180605424926556207367152557812293274)
  347. sample_1 = engine.random(n=5)
  348. sample_2 = engine.random(n=7)
  349. assert engine.num_generated == 12
  350. # reset and re-sample
  351. engine.reset()
  352. assert engine.num_generated == 0
  353. sample_1_test = engine.random(n=5)
  354. assert_equal(sample_1, sample_1_test)
  355. # repeat reset and fast forward
  356. engine.reset()
  357. engine.fast_forward(n=5)
  358. sample_2_test = engine.random(n=7)
  359. assert_equal(sample_2, sample_2_test)
  360. assert engine.num_generated == 12
  361. def test_raises():
  362. # input validation
  363. with pytest.raises(ValueError, match=r"d must be a non-negative integer"):
  364. RandomEngine((2,))
  365. with pytest.raises(ValueError, match=r"d must be a non-negative integer"):
  366. RandomEngine(-1)
  367. msg = r"'u_bounds' and 'l_bounds' must be integers"
  368. with pytest.raises(ValueError, match=msg):
  369. engine = RandomEngine(1)
  370. engine.integers(l_bounds=1, u_bounds=1.1)
  371. def test_integers():
  372. engine = RandomEngine(1, rng=231195739755290648063853336582377368684)
  373. # basic tests
  374. sample = engine.integers(1, n=10)
  375. assert_equal(np.unique(sample), [0])
  376. assert sample.dtype == np.dtype('int64')
  377. sample = engine.integers(1, n=10, endpoint=True)
  378. assert_equal(np.unique(sample), [0, 1])
  379. low = -5
  380. high = 7
  381. # scaling logic
  382. engine.reset()
  383. ref_sample = engine.random(20)
  384. ref_sample = ref_sample * (high - low) + low
  385. ref_sample = np.floor(ref_sample).astype(np.int64)
  386. engine.reset()
  387. sample = engine.integers(low, u_bounds=high, n=20, endpoint=False)
  388. assert_equal(sample, ref_sample)
  389. # up to bounds, no less, no more
  390. sample = engine.integers(low, u_bounds=high, n=100, endpoint=False)
  391. assert_equal((sample.min(), sample.max()), (low, high-1))
  392. sample = engine.integers(low, u_bounds=high, n=100, endpoint=True)
  393. assert_equal((sample.min(), sample.max()), (low, high))
  394. def test_integers_nd():
  395. d = 10
  396. rng = np.random.default_rng(3716505122102428560615700415287450951)
  397. low = rng.integers(low=-5, high=-1, size=d)
  398. high = rng.integers(low=1, high=5, size=d, endpoint=True)
  399. engine = RandomEngine(d, rng=rng)
  400. sample = engine.integers(low, u_bounds=high, n=100, endpoint=False)
  401. assert_equal(sample.min(axis=0), low)
  402. assert_equal(sample.max(axis=0), high-1)
  403. sample = engine.integers(low, u_bounds=high, n=100, endpoint=True)
  404. assert_equal(sample.min(axis=0), low)
  405. assert_equal(sample.max(axis=0), high)
  406. class QMCEngineTests:
  407. """Generic tests for QMC engines."""
  408. qmce = NotImplemented
  409. can_scramble = NotImplemented
  410. unscramble_nd = NotImplemented
  411. scramble_nd = NotImplemented
  412. scramble = [True, False]
  413. ids = ["Scrambled", "Unscrambled"]
  414. def engine(
  415. self, scramble: bool,
  416. rng=170382760648021597650530316304495310428,
  417. **kwargs
  418. ) -> QMCEngine:
  419. # preserve use of `seed` during SPEC 7 transition because
  420. # some tests rely on behavior with integer `seed` (which is
  421. # different from behavior with integer `rng`)
  422. if self.can_scramble:
  423. return self.qmce(scramble=scramble, seed=rng, **kwargs)
  424. else:
  425. if scramble:
  426. pytest.skip()
  427. else:
  428. return self.qmce(seed=rng, **kwargs)
  429. def reference(self, scramble: bool) -> np.ndarray:
  430. return self.scramble_nd if scramble else self.unscramble_nd
  431. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  432. def test_0dim(self, scramble):
  433. engine = self.engine(d=0, scramble=scramble)
  434. sample = engine.random(4)
  435. assert_array_equal(np.empty((4, 0)), sample)
  436. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  437. def test_0sample(self, scramble):
  438. engine = self.engine(d=2, scramble=scramble)
  439. sample = engine.random(0)
  440. assert_array_equal(np.empty((0, 2)), sample)
  441. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  442. def test_1sample(self, scramble):
  443. engine = self.engine(d=2, scramble=scramble)
  444. sample = engine.random(1)
  445. assert (1, 2) == sample.shape
  446. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  447. def test_bounds(self, scramble):
  448. engine = self.engine(d=100, scramble=scramble)
  449. sample = engine.random(512)
  450. assert np.all(sample >= 0)
  451. assert np.all(sample <= 1)
  452. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  453. def test_sample(self, scramble):
  454. ref_sample = self.reference(scramble=scramble)
  455. engine = self.engine(d=2, scramble=scramble)
  456. sample = engine.random(n=len(ref_sample))
  457. assert_allclose(sample, ref_sample, atol=1e-1)
  458. assert engine.num_generated == len(ref_sample)
  459. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  460. def test_continuing(self, scramble):
  461. engine = self.engine(d=2, scramble=scramble)
  462. ref_sample = engine.random(n=8)
  463. engine = self.engine(d=2, scramble=scramble)
  464. n_half = len(ref_sample) // 2
  465. _ = engine.random(n=n_half)
  466. sample = engine.random(n=n_half)
  467. assert_allclose(sample, ref_sample[n_half:], atol=1e-1)
  468. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  469. @pytest.mark.parametrize(
  470. "rng",
  471. (
  472. 170382760648021597650530316304495310428,
  473. lambda: np.random.default_rng(170382760648021597650530316304495310428),
  474. pytest.param(None, marks=pytest.mark.thread_unsafe),
  475. ),
  476. )
  477. def test_reset(self, scramble, rng):
  478. if callable(rng):
  479. # Initialize local RNG here to make it thread-local in pytest-run-parallel
  480. rng = rng()
  481. engine = self.engine(d=2, scramble=scramble, rng=rng)
  482. ref_sample = engine.random(n=8)
  483. engine.reset()
  484. assert engine.num_generated == 0
  485. sample = engine.random(n=8)
  486. assert_allclose(sample, ref_sample)
  487. @pytest.mark.parametrize("scramble", scramble, ids=ids)
  488. def test_fast_forward(self, scramble):
  489. engine = self.engine(d=2, scramble=scramble)
  490. ref_sample = engine.random(n=8)
  491. engine = self.engine(d=2, scramble=scramble)
  492. engine.fast_forward(4)
  493. sample = engine.random(n=4)
  494. assert_allclose(sample, ref_sample[4:], atol=1e-1)
  495. # alternate fast forwarding with sampling
  496. engine.reset()
  497. even_draws = []
  498. for i in range(8):
  499. if i % 2 == 0:
  500. even_draws.append(engine.random())
  501. else:
  502. engine.fast_forward(1)
  503. assert_allclose(
  504. ref_sample[[i for i in range(8) if i % 2 == 0]],
  505. np.concatenate(even_draws),
  506. atol=1e-5
  507. )
  508. @pytest.mark.parametrize("scramble", [True])
  509. def test_distribution(self, scramble):
  510. d = 50
  511. engine = self.engine(d=d, scramble=scramble)
  512. sample = engine.random(1024)
  513. assert_allclose(
  514. np.mean(sample, axis=0), np.repeat(0.5, d), atol=1e-2
  515. )
  516. assert_allclose(
  517. np.percentile(sample, 25, axis=0), np.repeat(0.25, d), atol=1e-2
  518. )
  519. assert_allclose(
  520. np.percentile(sample, 75, axis=0), np.repeat(0.75, d), atol=1e-2
  521. )
  522. def test_raises_optimizer(self):
  523. message = r"'toto' is not a valid optimization method"
  524. with pytest.raises(ValueError, match=message):
  525. self.engine(d=1, scramble=False, optimization="toto")
  526. @pytest.mark.parametrize(
  527. "optimization,metric",
  528. [
  529. ("random-CD", qmc.discrepancy),
  530. ("lloyd", lambda sample: -_l1_norm(sample))]
  531. )
  532. def test_optimizers(self, optimization, metric):
  533. engine = self.engine(d=2, scramble=False)
  534. sample_ref = engine.random(n=64)
  535. metric_ref = metric(sample_ref)
  536. optimal_ = self.engine(d=2, scramble=False, optimization=optimization)
  537. sample_ = optimal_.random(n=64)
  538. metric_ = metric(sample_)
  539. assert metric_ < metric_ref
  540. def test_consume_prng_state(self):
  541. rng = np.random.default_rng(0xa29cabb11cfdf44ff6cac8bec254c2a0)
  542. sample = []
  543. for i in range(3):
  544. engine = self.engine(d=2, scramble=True, rng=rng)
  545. sample.append(engine.random(4))
  546. with pytest.raises(AssertionError, match="Arrays are not equal"):
  547. assert_equal(sample[0], sample[1])
  548. with pytest.raises(AssertionError, match="Arrays are not equal"):
  549. assert_equal(sample[0], sample[2])
  550. class TestHalton(QMCEngineTests):
  551. qmce = qmc.Halton
  552. can_scramble = True
  553. # theoretical values known from Van der Corput
  554. unscramble_nd = np.array([[0, 0], [1 / 2, 1 / 3],
  555. [1 / 4, 2 / 3], [3 / 4, 1 / 9],
  556. [1 / 8, 4 / 9], [5 / 8, 7 / 9],
  557. [3 / 8, 2 / 9], [7 / 8, 5 / 9]])
  558. # theoretical values unknown: convergence properties checked
  559. scramble_nd = np.array([[0.50246036, 0.93382481],
  560. [0.00246036, 0.26715815],
  561. [0.75246036, 0.60049148],
  562. [0.25246036, 0.8227137 ],
  563. [0.62746036, 0.15604704],
  564. [0.12746036, 0.48938037],
  565. [0.87746036, 0.71160259],
  566. [0.37746036, 0.04493592]])
  567. def test_workers(self):
  568. ref_sample = self.reference(scramble=True)
  569. engine = self.engine(d=2, scramble=True)
  570. sample = engine.random(n=len(ref_sample), workers=8)
  571. assert_allclose(sample, ref_sample, atol=1e-3)
  572. # worker + integers
  573. engine.reset()
  574. ref_sample = engine.integers(10)
  575. engine.reset()
  576. sample = engine.integers(10, workers=8)
  577. assert_equal(sample, ref_sample)
  578. class TestLHS(QMCEngineTests):
  579. qmce = qmc.LatinHypercube
  580. can_scramble = True
  581. def test_continuing(self, *args):
  582. pytest.skip("Not applicable: not a sequence.")
  583. def test_fast_forward(self, *args):
  584. pytest.skip("Not applicable: not a sequence.")
  585. def test_sample(self, *args):
  586. pytest.skip("Not applicable: the value of reference sample is"
  587. " implementation dependent.")
  588. @pytest.mark.parametrize("strength", [1, 2])
  589. @pytest.mark.parametrize("scramble", [False, True])
  590. @pytest.mark.parametrize("optimization", [None, "random-CD"])
  591. def test_sample_stratified(self, optimization, scramble, strength):
  592. rng = np.random.default_rng(37511836202578819870665127532742111260)
  593. p = 5
  594. n = p**2
  595. d = 6
  596. engine = qmc.LatinHypercube(d=d, scramble=scramble,
  597. strength=strength,
  598. optimization=optimization,
  599. rng=rng)
  600. sample = engine.random(n=n)
  601. assert sample.shape == (n, d)
  602. assert engine.num_generated == n
  603. # centering stratifies samples in the middle of equal segments:
  604. # * inter-sample distance is constant in 1D sub-projections
  605. # * after ordering, columns are equal
  606. expected1d = (np.arange(n) + 0.5) / n
  607. expected = np.broadcast_to(expected1d, (d, n)).T
  608. assert np.any(sample != expected)
  609. sorted_sample = np.sort(sample, axis=0)
  610. tol = 0.5 / n if scramble else 0
  611. assert_allclose(sorted_sample, expected, atol=tol)
  612. assert np.any(sample - expected > tol)
  613. if strength == 2 and optimization is None:
  614. unique_elements = np.arange(p)
  615. desired = set(product(unique_elements, unique_elements))
  616. for i, j in combinations(range(engine.d), 2):
  617. samples_2d = sample[:, [i, j]]
  618. res = (samples_2d * p).astype(int)
  619. res_set = {tuple(row) for row in res}
  620. assert_equal(res_set, desired)
  621. def test_optimizer_1d(self):
  622. # discrepancy measures are invariant under permuting factors and runs
  623. engine = self.engine(d=1, scramble=False)
  624. sample_ref = engine.random(n=64)
  625. optimal_ = self.engine(d=1, scramble=False, optimization="random-CD")
  626. sample_ = optimal_.random(n=64)
  627. assert_array_equal(sample_ref, sample_)
  628. def test_raises(self):
  629. message = r"not a valid strength"
  630. with pytest.raises(ValueError, match=message):
  631. qmc.LatinHypercube(1, strength=3)
  632. message = r"n is not the square of a prime number"
  633. with pytest.raises(ValueError, match=message):
  634. engine = qmc.LatinHypercube(d=2, strength=2)
  635. engine.random(16)
  636. message = r"n is not the square of a prime number"
  637. with pytest.raises(ValueError, match=message):
  638. engine = qmc.LatinHypercube(d=2, strength=2)
  639. engine.random(5) # because int(sqrt(5)) would result in 2
  640. message = r"n is too small for d"
  641. with pytest.raises(ValueError, match=message):
  642. engine = qmc.LatinHypercube(d=5, strength=2)
  643. engine.random(9)
  644. class TestSobol(QMCEngineTests):
  645. qmce = qmc.Sobol
  646. can_scramble = True
  647. # theoretical values from Joe Kuo2010
  648. unscramble_nd = np.array([[0., 0.],
  649. [0.5, 0.5],
  650. [0.75, 0.25],
  651. [0.25, 0.75],
  652. [0.375, 0.375],
  653. [0.875, 0.875],
  654. [0.625, 0.125],
  655. [0.125, 0.625]])
  656. # theoretical values unknown: convergence properties checked
  657. scramble_nd = np.array([[0.25331921, 0.41371179],
  658. [0.8654213, 0.9821167],
  659. [0.70097554, 0.03664616],
  660. [0.18027647, 0.60895735],
  661. [0.10521339, 0.21897069],
  662. [0.53019685, 0.66619033],
  663. [0.91122276, 0.34580743],
  664. [0.45337471, 0.78912079]])
  665. def test_warning(self):
  666. with pytest.warns(UserWarning, match=r"The balance properties of "
  667. r"Sobol' points"):
  668. engine = qmc.Sobol(1)
  669. engine.random(10)
  670. def test_random_base2(self):
  671. engine = qmc.Sobol(2, scramble=False)
  672. sample = engine.random_base2(2)
  673. assert_array_equal(self.unscramble_nd[:4], sample)
  674. # resampling still having N=2**n
  675. sample = engine.random_base2(2)
  676. assert_array_equal(self.unscramble_nd[4:8], sample)
  677. # resampling again but leading to N!=2**n
  678. with pytest.raises(ValueError, match=r"The balance properties of "
  679. r"Sobol' points"):
  680. engine.random_base2(2)
  681. def test_raise(self):
  682. with pytest.raises(ValueError, match=r"Maximum supported "
  683. r"dimensionality"):
  684. qmc.Sobol(qmc.Sobol.MAXDIM + 1)
  685. with pytest.raises(ValueError, match=r"Maximum supported "
  686. r"'bits' is 64"):
  687. qmc.Sobol(1, bits=65)
  688. def test_high_dim(self):
  689. engine = qmc.Sobol(1111, scramble=False)
  690. count1 = Counter(engine.random().flatten().tolist())
  691. count2 = Counter(engine.random().flatten().tolist())
  692. assert_equal(count1, Counter({0.0: 1111}))
  693. assert_equal(count2, Counter({0.5: 1111}))
  694. @pytest.mark.parametrize("bits", [2, 3])
  695. def test_bits(self, bits):
  696. engine = qmc.Sobol(2, scramble=False, bits=bits)
  697. ns = 2**bits
  698. sample = engine.random(ns)
  699. assert_array_equal(self.unscramble_nd[:ns], sample)
  700. with pytest.raises(ValueError, match="increasing `bits`"):
  701. engine.random()
  702. def test_64bits(self):
  703. engine = qmc.Sobol(2, scramble=False, bits=64)
  704. sample = engine.random(8)
  705. assert_array_equal(self.unscramble_nd, sample)
  706. class TestLow0Bit:
  707. def test_examples(self):
  708. test_vector = [
  709. # from low_0_bit's docstring
  710. (0b0000, 1),
  711. (0b0001, 2),
  712. (0b0010, 1),
  713. (0b0101, 2),
  714. (0b0111, 4),
  715. # gh-23409
  716. (2 ** 32 - 1, 33),
  717. (2 ** 32, 1),
  718. (2 ** 33 - 1, 34),
  719. (2 ** 64 - 1, 65),
  720. ]
  721. for in_, out in test_vector:
  722. assert_equal(_test_low_0_bit(in_), out)
  723. class TestPoisson(QMCEngineTests):
  724. qmce = qmc.PoissonDisk
  725. can_scramble = False
  726. def test_bounds(self, *args):
  727. pytest.skip("Too costly in memory.")
  728. def test_fast_forward(self, *args):
  729. pytest.skip("Not applicable: recursive process.")
  730. def test_sample(self, *args):
  731. pytest.skip("Not applicable: the value of reference sample is"
  732. " implementation dependent.")
  733. def test_continuing(self, *args):
  734. # can continue a sampling, but will not preserve the same order
  735. # because candidates are lost, so we will not select the same center
  736. radius = 0.05
  737. ns = 6
  738. engine = self.engine(d=2, radius=radius, scramble=False)
  739. sample_init = engine.random(n=ns)
  740. assert len(sample_init) <= ns
  741. assert l2_norm(sample_init) >= radius
  742. sample_continued = engine.random(n=ns)
  743. assert len(sample_continued) <= ns
  744. assert l2_norm(sample_continued) >= radius
  745. sample = np.concatenate([sample_init, sample_continued], axis=0)
  746. assert len(sample) <= ns * 2
  747. assert l2_norm(sample) >= radius
  748. def test_mindist(self):
  749. rng = np.random.default_rng(132074951149370773672162394161442690287)
  750. ns = 50
  751. low, high = 0.08, 0.2
  752. radii = (high - low) * rng.random(5) + low
  753. dimensions = [1, 3, 4]
  754. hypersphere_methods = ["volume", "surface"]
  755. gen = product(dimensions, radii, hypersphere_methods)
  756. for d, radius, hypersphere in gen:
  757. engine = self.qmce(
  758. d=d, radius=radius, hypersphere=hypersphere, rng=rng
  759. )
  760. sample = engine.random(ns)
  761. assert len(sample) <= ns
  762. assert l2_norm(sample) >= radius
  763. @pytest.mark.parametrize("l_bounds, u_bounds", # add bounds to test gh-22819
  764. [(None, None), ([-5, -5], [5, 5]), ([1.1, 1.1], [5.1, 5.1])])
  765. def test_fill_space(self, l_bounds, u_bounds):
  766. radius = 0.2
  767. engine = self.qmce(d=2, radius=radius, l_bounds=l_bounds, u_bounds=u_bounds)
  768. sample = engine.fill_space()
  769. # circle packing problem is np complex
  770. assert l2_norm(sample) >= radius
  771. def test_bounds_shift_scale(self):
  772. # test a reasonable property: as long as shape of region is a hypercube,
  773. # bounds just shift and scale the sample.
  774. radius = 0.2 # arbitrary parameters
  775. shift = np.asarray([-2.1, 3.4])
  776. scale = 2.6
  777. rng = np.random.default_rng(8504196751)
  778. engine = self.qmce(d=2, radius=radius*scale, rng=rng,
  779. l_bounds=shift, u_bounds=shift + scale)
  780. res = engine.fill_space()
  781. rng = np.random.default_rng(8504196751)
  782. engine = self.qmce(d=2, radius=radius, rng=rng)
  783. ref = engine.fill_space()
  784. # circle packing problem is np complex
  785. assert l2_norm(res) >= radius
  786. assert_allclose(res, ref*scale + shift)
  787. @pytest.mark.parametrize("l_bounds", [[-1, -2, -1], [1, 2, 1]])
  788. def test_sample_inside_lower_bounds(self, l_bounds):
  789. radius = 0.2
  790. u_bounds=[3, 3, 2]
  791. engine = self.qmce(
  792. d=3, radius=radius, l_bounds=l_bounds, u_bounds=u_bounds
  793. )
  794. sample = engine.random(30)
  795. for point in sample:
  796. assert_array_less(point, u_bounds)
  797. assert_array_less(l_bounds, point)
  798. @pytest.mark.parametrize("u_bounds", [[-1, -2, -1], [1, 2, 1]])
  799. def test_sample_inside_upper_bounds(self, u_bounds):
  800. radius = 0.2
  801. l_bounds=[-3, -3, -2]
  802. engine = self.qmce(
  803. d=3, radius=radius, l_bounds=l_bounds, u_bounds=u_bounds
  804. )
  805. sample = engine.random(30)
  806. for point in sample:
  807. assert_array_less(point, u_bounds)
  808. assert_array_less(l_bounds, point)
  809. def test_inconsistent_bound_value(self):
  810. radius = 0.2
  811. l_bounds=[3, 2, 1]
  812. u_bounds=[-1, -2, -1]
  813. with pytest.raises(
  814. ValueError,
  815. match="Bounds are not consistent 'l_bounds' < 'u_bounds'"):
  816. self.qmce(d=3, radius=radius, l_bounds=l_bounds, u_bounds=u_bounds)
  817. @pytest.mark.parametrize("u_bounds", [[-1, -2, -1], [-1, -2]])
  818. @pytest.mark.parametrize("l_bounds", [[3, 2]])
  819. def test_inconsistent_bounds(self, u_bounds, l_bounds):
  820. radius = 0.2
  821. with pytest.raises(
  822. ValueError,
  823. match="'l_bounds' and 'u_bounds' must be broadcastable and respect"
  824. " the sample dimension"):
  825. self.qmce(
  826. d=3, radius=radius,
  827. l_bounds=l_bounds, u_bounds=u_bounds
  828. )
  829. def test_raises(self):
  830. message = r"'toto' is not a valid hypersphere sampling"
  831. with pytest.raises(ValueError, match=message):
  832. qmc.PoissonDisk(1, hypersphere="toto")
  833. class TestMultinomialQMC:
  834. def test_validations(self):
  835. # negative Ps
  836. p = np.array([0.12, 0.26, -0.05, 0.35, 0.22])
  837. with pytest.raises(ValueError, match=r"Elements of pvals must "
  838. r"be non-negative."):
  839. qmc.MultinomialQMC(p, n_trials=10)
  840. # sum of P too large
  841. p = np.array([0.12, 0.26, 0.1, 0.35, 0.22])
  842. message = r"Elements of pvals must sum to 1."
  843. with pytest.raises(ValueError, match=message):
  844. qmc.MultinomialQMC(p, n_trials=10)
  845. p = np.array([0.12, 0.26, 0.05, 0.35, 0.22])
  846. message = r"Dimension of `engine` must be 1."
  847. with pytest.raises(ValueError, match=message):
  848. qmc.MultinomialQMC(p, n_trials=10, engine=qmc.Sobol(d=2))
  849. message = r"`engine` must be an instance of..."
  850. with pytest.raises(ValueError, match=message):
  851. qmc.MultinomialQMC(p, n_trials=10, engine=np.random.default_rng())
  852. @pytest.mark.filterwarnings('ignore::UserWarning')
  853. def test_MultinomialBasicDraw(self):
  854. rng = np.random.default_rng(6955663962957011631562466584467607969)
  855. p = np.array([0.12, 0.26, 0.05, 0.35, 0.22])
  856. n_trials = 100
  857. expected = np.atleast_2d(n_trials * p).astype(int)
  858. # preserve use of legacy keyword during SPEC 7 transition
  859. engine = qmc.MultinomialQMC(p, n_trials=n_trials, seed=rng)
  860. assert_allclose(engine.random(1), expected, atol=1)
  861. def test_MultinomialDistribution(self):
  862. rng = np.random.default_rng(77797854505813727292048130876699859000)
  863. p = np.array([0.12, 0.26, 0.05, 0.35, 0.22])
  864. engine = qmc.MultinomialQMC(p, n_trials=8192, rng=rng)
  865. draws = engine.random(1)
  866. assert_allclose(draws / np.sum(draws), np.atleast_2d(p), atol=1e-4)
  867. def test_FindIndex(self):
  868. p_cumulative = np.array([0.1, 0.4, 0.45, 0.6, 0.75, 0.9, 0.99, 1.0])
  869. size = len(p_cumulative)
  870. assert_equal(_test_find_index(p_cumulative, size, 0.0), 0)
  871. assert_equal(_test_find_index(p_cumulative, size, 0.4), 2)
  872. assert_equal(_test_find_index(p_cumulative, size, 0.44999), 2)
  873. assert_equal(_test_find_index(p_cumulative, size, 0.45001), 3)
  874. assert_equal(_test_find_index(p_cumulative, size, 1.0), size - 1)
  875. @pytest.mark.filterwarnings('ignore::UserWarning')
  876. def test_other_engine(self):
  877. # same as test_MultinomialBasicDraw with different engine
  878. rng = np.random.default_rng(283753519042773243071753037669078065412)
  879. p = np.array([0.12, 0.26, 0.05, 0.35, 0.22])
  880. n_trials = 100
  881. expected = np.atleast_2d(n_trials * p).astype(int)
  882. base_engine = qmc.Sobol(1, scramble=True, rng=rng)
  883. engine = qmc.MultinomialQMC(p, n_trials=n_trials, engine=base_engine,
  884. rng=rng)
  885. assert_allclose(engine.random(1), expected, atol=1)
  886. class TestNormalQMC:
  887. def test_NormalQMC(self):
  888. # d = 1
  889. engine = qmc.MultivariateNormalQMC(mean=np.zeros(1))
  890. samples = engine.random()
  891. assert_equal(samples.shape, (1, 1))
  892. samples = engine.random(n=5)
  893. assert_equal(samples.shape, (5, 1))
  894. # d = 2
  895. engine = qmc.MultivariateNormalQMC(mean=np.zeros(2))
  896. samples = engine.random()
  897. assert_equal(samples.shape, (1, 2))
  898. samples = engine.random(n=5)
  899. assert_equal(samples.shape, (5, 2))
  900. def test_NormalQMCInvTransform(self):
  901. # d = 1
  902. engine = qmc.MultivariateNormalQMC(
  903. mean=np.zeros(1), inv_transform=True)
  904. samples = engine.random()
  905. assert_equal(samples.shape, (1, 1))
  906. samples = engine.random(n=5)
  907. assert_equal(samples.shape, (5, 1))
  908. # d = 2
  909. engine = qmc.MultivariateNormalQMC(
  910. mean=np.zeros(2), inv_transform=True)
  911. samples = engine.random()
  912. assert_equal(samples.shape, (1, 2))
  913. samples = engine.random(n=5)
  914. assert_equal(samples.shape, (5, 2))
  915. def test_NormalQMCSeeded(self):
  916. # test even dimension
  917. rng = np.random.default_rng(274600237797326520096085022671371676017)
  918. # preserve use of legacy keyword during SPEC 7 transition
  919. engine = qmc.MultivariateNormalQMC(
  920. mean=np.zeros(2), inv_transform=False, seed=rng)
  921. samples = engine.random(n=2)
  922. samples_expected = np.array([[-0.932001, -0.522923],
  923. [-1.477655, 0.846851]])
  924. assert_allclose(samples, samples_expected, atol=1e-4)
  925. # test odd dimension
  926. rng = np.random.default_rng(274600237797326520096085022671371676017)
  927. engine = qmc.MultivariateNormalQMC(
  928. mean=np.zeros(3), inv_transform=False, rng=rng)
  929. samples = engine.random(n=2)
  930. samples_expected = np.array([[-0.932001, -0.522923, 0.036578],
  931. [-1.778011, 0.912428, -0.065421]])
  932. assert_allclose(samples, samples_expected, atol=1e-4)
  933. # same test with another engine
  934. rng = np.random.default_rng(274600237797326520096085022671371676017)
  935. base_engine = qmc.Sobol(4, scramble=True, rng=rng)
  936. engine = qmc.MultivariateNormalQMC(
  937. mean=np.zeros(3), inv_transform=False,
  938. engine=base_engine, rng=rng
  939. )
  940. samples = engine.random(n=2)
  941. samples_expected = np.array([[-0.932001, -0.522923, 0.036578],
  942. [-1.778011, 0.912428, -0.065421]])
  943. assert_allclose(samples, samples_expected, atol=1e-4)
  944. def test_NormalQMCSeededInvTransform(self):
  945. # test even dimension
  946. rng = np.random.default_rng(288527772707286126646493545351112463929)
  947. engine = qmc.MultivariateNormalQMC(
  948. mean=np.zeros(2), rng=rng, inv_transform=True)
  949. samples = engine.random(n=2)
  950. samples_expected = np.array([[-0.913237, -0.964026],
  951. [0.255904, 0.003068]])
  952. assert_allclose(samples, samples_expected, atol=1e-4)
  953. # test odd dimension
  954. rng = np.random.default_rng(288527772707286126646493545351112463929)
  955. engine = qmc.MultivariateNormalQMC(
  956. mean=np.zeros(3), rng=rng, inv_transform=True)
  957. samples = engine.random(n=2)
  958. samples_expected = np.array([[-0.913237, -0.964026, 0.355501],
  959. [0.699261, 2.90213 , -0.6418]])
  960. assert_allclose(samples, samples_expected, atol=1e-4)
  961. def test_other_engine(self):
  962. for d in (0, 1, 2):
  963. base_engine = qmc.Sobol(d=d, scramble=False)
  964. engine = qmc.MultivariateNormalQMC(mean=np.zeros(d),
  965. engine=base_engine,
  966. inv_transform=True)
  967. samples = engine.random()
  968. assert_equal(samples.shape, (1, d))
  969. def test_NormalQMCShapiro(self):
  970. rng = np.random.default_rng(13242)
  971. engine = qmc.MultivariateNormalQMC(mean=np.zeros(2), rng=rng)
  972. samples = engine.random(n=256)
  973. assert all(np.abs(samples.mean(axis=0)) < 1e-2)
  974. assert all(np.abs(samples.std(axis=0) - 1) < 1e-2)
  975. # perform Shapiro-Wilk test for normality
  976. for i in (0, 1):
  977. _, pval = shapiro(samples[:, i])
  978. assert pval > 0.9
  979. # make sure samples are uncorrelated
  980. cov = np.cov(samples.transpose())
  981. assert np.abs(cov[0, 1]) < 1e-2
  982. def test_NormalQMCShapiroInvTransform(self):
  983. rng = np.random.default_rng(32344554)
  984. engine = qmc.MultivariateNormalQMC(
  985. mean=np.zeros(2), inv_transform=True, rng=rng)
  986. samples = engine.random(n=256)
  987. assert all(np.abs(samples.mean(axis=0)) < 1e-2)
  988. assert all(np.abs(samples.std(axis=0) - 1) < 1e-2)
  989. # perform Shapiro-Wilk test for normality
  990. for i in (0, 1):
  991. _, pval = shapiro(samples[:, i])
  992. assert pval > 0.9
  993. # make sure samples are uncorrelated
  994. cov = np.cov(samples.transpose())
  995. assert np.abs(cov[0, 1]) < 1e-2
  996. class TestMultivariateNormalQMC:
  997. def test_validations(self):
  998. message = r"Dimension of `engine` must be consistent"
  999. with pytest.raises(ValueError, match=message):
  1000. qmc.MultivariateNormalQMC([0], engine=qmc.Sobol(d=2))
  1001. message = r"Dimension of `engine` must be consistent"
  1002. with pytest.raises(ValueError, match=message):
  1003. qmc.MultivariateNormalQMC([0, 0, 0], engine=qmc.Sobol(d=4))
  1004. message = r"`engine` must be an instance of..."
  1005. with pytest.raises(ValueError, match=message):
  1006. qmc.MultivariateNormalQMC([0, 0], engine=np.random.default_rng())
  1007. message = r"Covariance matrix not PSD."
  1008. with pytest.raises(ValueError, match=message):
  1009. qmc.MultivariateNormalQMC([0, 0], [[1, 2], [2, 1]])
  1010. message = r"Covariance matrix is not symmetric."
  1011. with pytest.raises(ValueError, match=message):
  1012. qmc.MultivariateNormalQMC([0, 0], [[1, 0], [2, 1]])
  1013. message = r"Dimension mismatch between mean and covariance."
  1014. with pytest.raises(ValueError, match=message):
  1015. qmc.MultivariateNormalQMC([0], [[1, 0], [0, 1]])
  1016. def test_MultivariateNormalQMCNonPD(self):
  1017. # try with non-pd but psd cov; should work
  1018. engine = qmc.MultivariateNormalQMC(
  1019. [0, 0, 0], [[1, 0, 1], [0, 1, 1], [1, 1, 2]],
  1020. )
  1021. assert engine._corr_matrix is not None
  1022. def test_MultivariateNormalQMC(self):
  1023. # d = 1 scalar
  1024. engine = qmc.MultivariateNormalQMC(mean=0, cov=5)
  1025. samples = engine.random()
  1026. assert_equal(samples.shape, (1, 1))
  1027. samples = engine.random(n=5)
  1028. assert_equal(samples.shape, (5, 1))
  1029. # d = 2 list
  1030. engine = qmc.MultivariateNormalQMC(mean=[0, 1], cov=[[1, 0], [0, 1]])
  1031. samples = engine.random()
  1032. assert_equal(samples.shape, (1, 2))
  1033. samples = engine.random(n=5)
  1034. assert_equal(samples.shape, (5, 2))
  1035. # d = 3 np.array
  1036. mean = np.array([0, 1, 2])
  1037. cov = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
  1038. engine = qmc.MultivariateNormalQMC(mean, cov)
  1039. samples = engine.random()
  1040. assert_equal(samples.shape, (1, 3))
  1041. samples = engine.random(n=5)
  1042. assert_equal(samples.shape, (5, 3))
  1043. def test_MultivariateNormalQMCInvTransform(self):
  1044. # d = 1 scalar
  1045. engine = qmc.MultivariateNormalQMC(mean=0, cov=5, inv_transform=True)
  1046. samples = engine.random()
  1047. assert_equal(samples.shape, (1, 1))
  1048. samples = engine.random(n=5)
  1049. assert_equal(samples.shape, (5, 1))
  1050. # d = 2 list
  1051. engine = qmc.MultivariateNormalQMC(
  1052. mean=[0, 1], cov=[[1, 0], [0, 1]], inv_transform=True,
  1053. )
  1054. samples = engine.random()
  1055. assert_equal(samples.shape, (1, 2))
  1056. samples = engine.random(n=5)
  1057. assert_equal(samples.shape, (5, 2))
  1058. # d = 3 np.array
  1059. mean = np.array([0, 1, 2])
  1060. cov = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
  1061. engine = qmc.MultivariateNormalQMC(mean, cov, inv_transform=True)
  1062. samples = engine.random()
  1063. assert_equal(samples.shape, (1, 3))
  1064. samples = engine.random(n=5)
  1065. assert_equal(samples.shape, (5, 3))
  1066. def test_MultivariateNormalQMCSeeded(self):
  1067. # test even dimension
  1068. rng = np.random.default_rng(180182791534511062935571481899241825000)
  1069. a = rng.standard_normal((2, 2))
  1070. A = a @ a.transpose() + np.diag(rng.random(2))
  1071. engine = qmc.MultivariateNormalQMC(np.array([0, 0]), A,
  1072. inv_transform=False, rng=rng)
  1073. samples = engine.random(n=2)
  1074. samples_expected = np.array([[-0.64419, -0.882413],
  1075. [0.837199, 2.045301]])
  1076. assert_allclose(samples, samples_expected, atol=1e-4)
  1077. # test odd dimension
  1078. rng = np.random.default_rng(180182791534511062935571481899241825000)
  1079. a = rng.standard_normal((3, 3))
  1080. A = a @ a.transpose() + np.diag(rng.random(3))
  1081. engine = qmc.MultivariateNormalQMC(np.array([0, 0, 0]), A,
  1082. inv_transform=False, rng=rng)
  1083. samples = engine.random(n=2)
  1084. samples_expected = np.array([[-0.693853, -1.265338, -0.088024],
  1085. [1.620193, 2.679222, 0.457343]])
  1086. assert_allclose(samples, samples_expected, atol=1e-4)
  1087. def test_MultivariateNormalQMCSeededInvTransform(self):
  1088. # test even dimension
  1089. rng = np.random.default_rng(224125808928297329711992996940871155974)
  1090. a = rng.standard_normal((2, 2))
  1091. A = a @ a.transpose() + np.diag(rng.random(2))
  1092. engine = qmc.MultivariateNormalQMC(
  1093. np.array([0, 0]), A, rng=rng, inv_transform=True
  1094. )
  1095. samples = engine.random(n=2)
  1096. samples_expected = np.array([[0.682171, -3.114233],
  1097. [-0.098463, 0.668069]])
  1098. assert_allclose(samples, samples_expected, atol=1e-4)
  1099. # test odd dimension
  1100. rng = np.random.default_rng(224125808928297329711992996940871155974)
  1101. a = rng.standard_normal((3, 3))
  1102. A = a @ a.transpose() + np.diag(rng.random(3))
  1103. engine = qmc.MultivariateNormalQMC(
  1104. np.array([0, 0, 0]), A, rng=rng, inv_transform=True
  1105. )
  1106. samples = engine.random(n=2)
  1107. samples_expected = np.array([[0.988061, -1.644089, -0.877035],
  1108. [-1.771731, 1.096988, 2.024744]])
  1109. assert_allclose(samples, samples_expected, atol=1e-4)
  1110. def test_MultivariateNormalQMCShapiro(self):
  1111. # test the standard case
  1112. rng = np.random.default_rng(188960007281846377164494575845971640)
  1113. engine = qmc.MultivariateNormalQMC(
  1114. mean=[0, 0], cov=[[1, 0], [0, 1]], rng=rng
  1115. )
  1116. samples = engine.random(n=256)
  1117. assert all(np.abs(samples.mean(axis=0)) < 1e-2)
  1118. assert all(np.abs(samples.std(axis=0) - 1) < 1e-2)
  1119. # perform Shapiro-Wilk test for normality
  1120. for i in (0, 1):
  1121. _, pval = shapiro(samples[:, i])
  1122. assert pval > 0.9
  1123. # make sure samples are uncorrelated
  1124. cov = np.cov(samples.transpose())
  1125. assert np.abs(cov[0, 1]) < 1e-2
  1126. # test the correlated, non-zero mean case
  1127. engine = qmc.MultivariateNormalQMC(
  1128. mean=[1.0, 2.0], cov=[[1.5, 0.5], [0.5, 1.5]], rng=rng
  1129. )
  1130. samples = engine.random(n=256)
  1131. assert all(np.abs(samples.mean(axis=0) - [1, 2]) < 1e-2)
  1132. assert all(np.abs(samples.std(axis=0) - np.sqrt(1.5)) < 1e-2)
  1133. # perform Shapiro-Wilk test for normality
  1134. for i in (0, 1):
  1135. _, pval = shapiro(samples[:, i])
  1136. assert pval > 0.9
  1137. # check covariance
  1138. cov = np.cov(samples.transpose())
  1139. assert np.abs(cov[0, 1] - 0.5) < 1e-2
  1140. def test_MultivariateNormalQMCShapiroInvTransform(self):
  1141. # test the standard case
  1142. rng = np.random.default_rng(200089821034563288698994840831440331329)
  1143. engine = qmc.MultivariateNormalQMC(
  1144. mean=[0, 0], cov=[[1, 0], [0, 1]], rng=rng, inv_transform=True
  1145. )
  1146. samples = engine.random(n=256)
  1147. assert all(np.abs(samples.mean(axis=0)) < 1e-2)
  1148. assert all(np.abs(samples.std(axis=0) - 1) < 1e-2)
  1149. # perform Shapiro-Wilk test for normality
  1150. for i in (0, 1):
  1151. _, pval = shapiro(samples[:, i])
  1152. assert pval > 0.9
  1153. # make sure samples are uncorrelated
  1154. cov = np.cov(samples.transpose())
  1155. assert np.abs(cov[0, 1]) < 1e-2
  1156. # test the correlated, non-zero mean case
  1157. engine = qmc.MultivariateNormalQMC(
  1158. mean=[1.0, 2.0],
  1159. cov=[[1.5, 0.5], [0.5, 1.5]],
  1160. rng=rng,
  1161. inv_transform=True,
  1162. )
  1163. samples = engine.random(n=256)
  1164. assert all(np.abs(samples.mean(axis=0) - [1, 2]) < 1e-2)
  1165. assert all(np.abs(samples.std(axis=0) - np.sqrt(1.5)) < 1e-2)
  1166. # perform Shapiro-Wilk test for normality
  1167. for i in (0, 1):
  1168. _, pval = shapiro(samples[:, i])
  1169. assert pval > 0.9
  1170. # check covariance
  1171. cov = np.cov(samples.transpose())
  1172. assert np.abs(cov[0, 1] - 0.5) < 1e-2
  1173. def test_MultivariateNormalQMCDegenerate(self):
  1174. # X, Y iid standard Normal and Z = X + Y, random vector (X, Y, Z)
  1175. rng = np.random.default_rng(16320637417581448357869821654290448620)
  1176. engine = qmc.MultivariateNormalQMC(
  1177. mean=[0.0, 0.0, 0.0],
  1178. cov=[[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [1.0, 1.0, 2.0]],
  1179. rng=rng,
  1180. )
  1181. samples = engine.random(n=512)
  1182. assert all(np.abs(samples.mean(axis=0)) < 1e-2)
  1183. assert np.abs(np.std(samples[:, 0]) - 1) < 1e-2
  1184. assert np.abs(np.std(samples[:, 1]) - 1) < 1e-2
  1185. assert np.abs(np.std(samples[:, 2]) - np.sqrt(2)) < 1e-2
  1186. for i in (0, 1, 2):
  1187. _, pval = shapiro(samples[:, i])
  1188. assert pval > 0.8
  1189. cov = np.cov(samples.transpose())
  1190. assert np.abs(cov[0, 1]) < 1e-2
  1191. assert np.abs(cov[0, 2] - 1) < 1e-2
  1192. # check to see if X + Y = Z almost exactly
  1193. assert all(np.abs(samples[:, 0] + samples[:, 1] - samples[:, 2])
  1194. < 1e-5)
  1195. class TestLloyd:
  1196. def test_lloyd(self):
  1197. # quite sensible seed as it can go up before going further down
  1198. rng = np.random.RandomState(1809831)
  1199. sample = rng.uniform(0, 1, size=(128, 2))
  1200. base_l1 = _l1_norm(sample)
  1201. base_l2 = l2_norm(sample)
  1202. for _ in range(4):
  1203. sample_lloyd = _lloyd_centroidal_voronoi_tessellation(
  1204. sample, maxiter=1,
  1205. )
  1206. curr_l1 = _l1_norm(sample_lloyd)
  1207. curr_l2 = l2_norm(sample_lloyd)
  1208. # higher is better for the distance measures
  1209. assert base_l1 < curr_l1
  1210. assert base_l2 < curr_l2
  1211. base_l1 = curr_l1
  1212. base_l2 = curr_l2
  1213. sample = sample_lloyd
  1214. def test_lloyd_non_mutating(self):
  1215. """
  1216. Verify that the input samples are not mutated in place and that they do
  1217. not share memory with the output.
  1218. """
  1219. sample_orig = np.array([[0.1, 0.1],
  1220. [0.1, 0.2],
  1221. [0.2, 0.1],
  1222. [0.2, 0.2]])
  1223. sample_copy = sample_orig.copy()
  1224. new_sample = _lloyd_centroidal_voronoi_tessellation(
  1225. sample=sample_orig
  1226. )
  1227. assert_allclose(sample_orig, sample_copy)
  1228. assert not np.may_share_memory(sample_orig, new_sample)
  1229. def test_lloyd_errors(self):
  1230. with pytest.raises(ValueError, match=r"`sample` is not a 2D array"):
  1231. sample = [0, 1, 0.5]
  1232. _lloyd_centroidal_voronoi_tessellation(sample)
  1233. msg = r"`sample` dimension is not >= 2"
  1234. with pytest.raises(ValueError, match=msg):
  1235. sample = [[0], [0.4], [1]]
  1236. _lloyd_centroidal_voronoi_tessellation(sample)
  1237. msg = r"`sample` is not in unit hypercube"
  1238. with pytest.raises(ValueError, match=msg):
  1239. sample = [[-1.1, 0], [0.1, 0.4], [1, 2]]
  1240. _lloyd_centroidal_voronoi_tessellation(sample)
  1241. # mindist
  1242. def l2_norm(sample):
  1243. return distance.pdist(sample).min()
  1244. @pytest.mark.parametrize('engine', [qmc.Halton, qmc.Sobol,
  1245. qmc.LatinHypercube, qmc.PoissonDisk])
  1246. def test_deterministic(engine):
  1247. seed_number = 2359834584
  1248. rng = np.random.RandomState(seed_number)
  1249. res1 = engine(d=1, seed=rng).random(4)
  1250. rng = np.random.RandomState(seed_number)
  1251. res2 = engine(d=1, seed=rng).random(4)
  1252. assert_equal(res1, res2)
  1253. rng = np.random.default_rng(seed_number)
  1254. res1 = engine(d=1, seed=rng).random(4)
  1255. res2 = engine(d=1, rng=seed_number).random(4)
  1256. assert_equal(res1, res2)
  1257. rng = np.random.default_rng(seed_number)
  1258. res3 = engine(d=1, rng=rng).random(4)
  1259. assert_equal(res2, res1)
  1260. assert_equal(res3, res1)
  1261. message = "got multiple values for argument now known as `rng`"
  1262. with pytest.raises(TypeError, match=message):
  1263. engine(d=1, rng=seed_number, seed=seed_number)