test_qmc.py 50 KB

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