test__basinhopping.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. """
  2. Unit tests for the basin hopping global minimization algorithm.
  3. """
  4. from __future__ import division, print_function, absolute_import
  5. import copy
  6. from numpy.testing import assert_almost_equal, assert_equal, assert_
  7. from pytest import raises as assert_raises
  8. import numpy as np
  9. from numpy import cos, sin
  10. from scipy.optimize import basinhopping, OptimizeResult
  11. from scipy.optimize._basinhopping import (
  12. Storage, RandomDisplacement, Metropolis, AdaptiveStepsize)
  13. def func1d(x):
  14. f = cos(14.5 * x - 0.3) + (x + 0.2) * x
  15. df = np.array(-14.5 * sin(14.5 * x - 0.3) + 2. * x + 0.2)
  16. return f, df
  17. def func2d_nograd(x):
  18. f = cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0]
  19. return f
  20. def func2d(x):
  21. f = cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0]
  22. df = np.zeros(2)
  23. df[0] = -14.5 * sin(14.5 * x[0] - 0.3) + 2. * x[0] + 0.2
  24. df[1] = 2. * x[1] + 0.2
  25. return f, df
  26. def func2d_easyderiv(x):
  27. f = 2.0*x[0]**2 + 2.0*x[0]*x[1] + 2.0*x[1]**2 - 6.0*x[0]
  28. df = np.zeros(2)
  29. df[0] = 4.0*x[0] + 2.0*x[1] - 6.0
  30. df[1] = 2.0*x[0] + 4.0*x[1]
  31. return f, df
  32. class MyTakeStep1(RandomDisplacement):
  33. """use a copy of displace, but have it set a special parameter to
  34. make sure it's actually being used."""
  35. def __init__(self):
  36. self.been_called = False
  37. super(MyTakeStep1, self).__init__()
  38. def __call__(self, x):
  39. self.been_called = True
  40. return super(MyTakeStep1, self).__call__(x)
  41. def myTakeStep2(x):
  42. """redo RandomDisplacement in function form without the attribute stepsize
  43. to make sure everything still works ok
  44. """
  45. s = 0.5
  46. x += np.random.uniform(-s, s, np.shape(x))
  47. return x
  48. class MyAcceptTest(object):
  49. """pass a custom accept test
  50. This does nothing but make sure it's being used and ensure all the
  51. possible return values are accepted
  52. """
  53. def __init__(self):
  54. self.been_called = False
  55. self.ncalls = 0
  56. self.testres = [False, 'force accept', True, np.bool_(True),
  57. np.bool_(False), [], {}, 0, 1]
  58. def __call__(self, **kwargs):
  59. self.been_called = True
  60. self.ncalls += 1
  61. if self.ncalls - 1 < len(self.testres):
  62. return self.testres[self.ncalls - 1]
  63. else:
  64. return True
  65. class MyCallBack(object):
  66. """pass a custom callback function
  67. This makes sure it's being used. It also returns True after 10
  68. steps to ensure that it's stopping early.
  69. """
  70. def __init__(self):
  71. self.been_called = False
  72. self.ncalls = 0
  73. def __call__(self, x, f, accepted):
  74. self.been_called = True
  75. self.ncalls += 1
  76. if self.ncalls == 10:
  77. return True
  78. class TestBasinHopping(object):
  79. def setup_method(self):
  80. """ Tests setup.
  81. Run tests based on the 1-D and 2-D functions described above.
  82. """
  83. self.x0 = (1.0, [1.0, 1.0])
  84. self.sol = (-0.195, np.array([-0.195, -0.1]))
  85. self.tol = 3 # number of decimal places
  86. self.niter = 100
  87. self.disp = False
  88. # fix random seed
  89. np.random.seed(1234)
  90. self.kwargs = {"method": "L-BFGS-B", "jac": True}
  91. self.kwargs_nograd = {"method": "L-BFGS-B"}
  92. def test_TypeError(self):
  93. # test the TypeErrors are raised on bad input
  94. i = 1
  95. # if take_step is passed, it must be callable
  96. assert_raises(TypeError, basinhopping, func2d, self.x0[i],
  97. take_step=1)
  98. # if accept_test is passed, it must be callable
  99. assert_raises(TypeError, basinhopping, func2d, self.x0[i],
  100. accept_test=1)
  101. def test_1d_grad(self):
  102. # test 1d minimizations with gradient
  103. i = 0
  104. res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
  105. niter=self.niter, disp=self.disp)
  106. assert_almost_equal(res.x, self.sol[i], self.tol)
  107. def test_2d(self):
  108. # test 2d minimizations with gradient
  109. i = 1
  110. res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
  111. niter=self.niter, disp=self.disp)
  112. assert_almost_equal(res.x, self.sol[i], self.tol)
  113. assert_(res.nfev > 0)
  114. def test_njev(self):
  115. # test njev is returned correctly
  116. i = 1
  117. minimizer_kwargs = self.kwargs.copy()
  118. # L-BFGS-B doesn't use njev, but BFGS does
  119. minimizer_kwargs["method"] = "BFGS"
  120. res = basinhopping(func2d, self.x0[i],
  121. minimizer_kwargs=minimizer_kwargs, niter=self.niter,
  122. disp=self.disp)
  123. assert_(res.nfev > 0)
  124. assert_equal(res.nfev, res.njev)
  125. def test_jac(self):
  126. # test jacobian returned
  127. minimizer_kwargs = self.kwargs.copy()
  128. # BFGS returns a Jacobian
  129. minimizer_kwargs["method"] = "BFGS"
  130. res = basinhopping(func2d_easyderiv, [0.0, 0.0],
  131. minimizer_kwargs=minimizer_kwargs, niter=self.niter,
  132. disp=self.disp)
  133. assert_(hasattr(res.lowest_optimization_result, "jac"))
  134. # in this case, the jacobian is just [df/dx, df/dy]
  135. _, jacobian = func2d_easyderiv(res.x)
  136. assert_almost_equal(res.lowest_optimization_result.jac, jacobian,
  137. self.tol)
  138. def test_2d_nograd(self):
  139. # test 2d minimizations without gradient
  140. i = 1
  141. res = basinhopping(func2d_nograd, self.x0[i],
  142. minimizer_kwargs=self.kwargs_nograd,
  143. niter=self.niter, disp=self.disp)
  144. assert_almost_equal(res.x, self.sol[i], self.tol)
  145. def test_all_minimizers(self):
  146. # test 2d minimizations with gradient. Nelder-Mead, Powell and COBYLA
  147. # don't accept jac=True, so aren't included here.
  148. i = 1
  149. methods = ['CG', 'BFGS', 'Newton-CG', 'L-BFGS-B', 'TNC', 'SLSQP']
  150. minimizer_kwargs = copy.copy(self.kwargs)
  151. for method in methods:
  152. minimizer_kwargs["method"] = method
  153. res = basinhopping(func2d, self.x0[i],
  154. minimizer_kwargs=minimizer_kwargs,
  155. niter=self.niter, disp=self.disp)
  156. assert_almost_equal(res.x, self.sol[i], self.tol)
  157. def test_all_nograd_minimizers(self):
  158. # test 2d minimizations without gradient. Newton-CG requires jac=True,
  159. # so not included here.
  160. i = 1
  161. methods = ['CG', 'BFGS', 'L-BFGS-B', 'TNC', 'SLSQP',
  162. 'Nelder-Mead', 'Powell', 'COBYLA']
  163. minimizer_kwargs = copy.copy(self.kwargs_nograd)
  164. for method in methods:
  165. minimizer_kwargs["method"] = method
  166. res = basinhopping(func2d_nograd, self.x0[i],
  167. minimizer_kwargs=minimizer_kwargs,
  168. niter=self.niter, disp=self.disp)
  169. tol = self.tol
  170. if method == 'COBYLA':
  171. tol = 2
  172. assert_almost_equal(res.x, self.sol[i], decimal=tol)
  173. def test_pass_takestep(self):
  174. # test that passing a custom takestep works
  175. # also test that the stepsize is being adjusted
  176. takestep = MyTakeStep1()
  177. initial_step_size = takestep.stepsize
  178. i = 1
  179. res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
  180. niter=self.niter, disp=self.disp,
  181. take_step=takestep)
  182. assert_almost_equal(res.x, self.sol[i], self.tol)
  183. assert_(takestep.been_called)
  184. # make sure that the built in adaptive step size has been used
  185. assert_(initial_step_size != takestep.stepsize)
  186. def test_pass_simple_takestep(self):
  187. # test that passing a custom takestep without attribute stepsize
  188. takestep = myTakeStep2
  189. i = 1
  190. res = basinhopping(func2d_nograd, self.x0[i],
  191. minimizer_kwargs=self.kwargs_nograd,
  192. niter=self.niter, disp=self.disp,
  193. take_step=takestep)
  194. assert_almost_equal(res.x, self.sol[i], self.tol)
  195. def test_pass_accept_test(self):
  196. # test passing a custom accept test
  197. # makes sure it's being used and ensures all the possible return values
  198. # are accepted.
  199. accept_test = MyAcceptTest()
  200. i = 1
  201. # there's no point in running it more than a few steps.
  202. basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
  203. niter=10, disp=self.disp, accept_test=accept_test)
  204. assert_(accept_test.been_called)
  205. def test_pass_callback(self):
  206. # test passing a custom callback function
  207. # This makes sure it's being used. It also returns True after 10 steps
  208. # to ensure that it's stopping early.
  209. callback = MyCallBack()
  210. i = 1
  211. # there's no point in running it more than a few steps.
  212. res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
  213. niter=30, disp=self.disp, callback=callback)
  214. assert_(callback.been_called)
  215. assert_("callback" in res.message[0])
  216. assert_equal(res.nit, 10)
  217. def test_minimizer_fail(self):
  218. # test if a minimizer fails
  219. i = 1
  220. self.kwargs["options"] = dict(maxiter=0)
  221. self.niter = 10
  222. res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
  223. niter=self.niter, disp=self.disp)
  224. # the number of failed minimizations should be the number of
  225. # iterations + 1
  226. assert_equal(res.nit + 1, res.minimization_failures)
  227. def test_niter_zero(self):
  228. # gh5915, what happens if you call basinhopping with niter=0
  229. i = 0
  230. basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
  231. niter=0, disp=self.disp)
  232. def test_seed_reproducibility(self):
  233. # seed should ensure reproducibility between runs
  234. minimizer_kwargs = {"method": "L-BFGS-B", "jac": True}
  235. f_1 = []
  236. def callback(x, f, accepted):
  237. f_1.append(f)
  238. basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs,
  239. niter=10, callback=callback, seed=10)
  240. f_2 = []
  241. def callback2(x, f, accepted):
  242. f_2.append(f)
  243. basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs,
  244. niter=10, callback=callback2, seed=10)
  245. assert_equal(np.array(f_1), np.array(f_2))
  246. def test_monotonic_basin_hopping(self):
  247. # test 1d minimizations with gradient and T=0
  248. i = 0
  249. res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
  250. niter=self.niter, disp=self.disp, T=0)
  251. assert_almost_equal(res.x, self.sol[i], self.tol)
  252. class Test_Storage(object):
  253. def setup_method(self):
  254. self.x0 = np.array(1)
  255. self.f0 = 0
  256. minres = OptimizeResult()
  257. minres.x = self.x0
  258. minres.fun = self.f0
  259. self.storage = Storage(minres)
  260. def test_higher_f_rejected(self):
  261. new_minres = OptimizeResult()
  262. new_minres.x = self.x0 + 1
  263. new_minres.fun = self.f0 + 1
  264. ret = self.storage.update(new_minres)
  265. minres = self.storage.get_lowest()
  266. assert_equal(self.x0, minres.x)
  267. assert_equal(self.f0, minres.fun)
  268. assert_(not ret)
  269. def test_lower_f_accepted(self):
  270. new_minres = OptimizeResult()
  271. new_minres.x = self.x0 + 1
  272. new_minres.fun = self.f0 - 1
  273. ret = self.storage.update(new_minres)
  274. minres = self.storage.get_lowest()
  275. assert_(self.x0 != minres.x)
  276. assert_(self.f0 != minres.fun)
  277. assert_(ret)
  278. class Test_RandomDisplacement(object):
  279. def setup_method(self):
  280. self.stepsize = 1.0
  281. self.displace = RandomDisplacement(stepsize=self.stepsize)
  282. self.N = 300000
  283. self.x0 = np.zeros([self.N])
  284. def test_random(self):
  285. # the mean should be 0
  286. # the variance should be (2*stepsize)**2 / 12
  287. # note these tests are random, they will fail from time to time
  288. x = self.displace(self.x0)
  289. v = (2. * self.stepsize) ** 2 / 12
  290. assert_almost_equal(np.mean(x), 0., 1)
  291. assert_almost_equal(np.var(x), v, 1)
  292. class Test_Metropolis(object):
  293. def setup_method(self):
  294. self.T = 2.
  295. self.met = Metropolis(self.T)
  296. def test_boolean_return(self):
  297. # the return must be a bool. else an error will be raised in
  298. # basinhopping
  299. ret = self.met(f_new=0., f_old=1.)
  300. assert isinstance(ret, bool)
  301. def test_lower_f_accepted(self):
  302. assert_(self.met(f_new=0., f_old=1.))
  303. def test_KeyError(self):
  304. # should raise KeyError if kwargs f_old or f_new is not passed
  305. assert_raises(KeyError, self.met, f_old=1.)
  306. assert_raises(KeyError, self.met, f_new=1.)
  307. def test_accept(self):
  308. # test that steps are randomly accepted for f_new > f_old
  309. one_accept = False
  310. one_reject = False
  311. for i in range(1000):
  312. if one_accept and one_reject:
  313. break
  314. ret = self.met(f_new=1., f_old=0.5)
  315. if ret:
  316. one_accept = True
  317. else:
  318. one_reject = True
  319. assert_(one_accept)
  320. assert_(one_reject)
  321. def test_GH7495(self):
  322. # an overflow in exp was producing a RuntimeWarning
  323. # create own object here in case someone changes self.T
  324. met = Metropolis(2)
  325. with np.errstate(over='raise'):
  326. met.accept_reject(0, 2000)
  327. class Test_AdaptiveStepsize(object):
  328. def setup_method(self):
  329. self.stepsize = 1.
  330. self.ts = RandomDisplacement(stepsize=self.stepsize)
  331. self.target_accept_rate = 0.5
  332. self.takestep = AdaptiveStepsize(takestep=self.ts, verbose=False,
  333. accept_rate=self.target_accept_rate)
  334. def test_adaptive_increase(self):
  335. # if few steps are rejected, the stepsize should increase
  336. x = 0.
  337. self.takestep(x)
  338. self.takestep.report(False)
  339. for i in range(self.takestep.interval):
  340. self.takestep(x)
  341. self.takestep.report(True)
  342. assert_(self.ts.stepsize > self.stepsize)
  343. def test_adaptive_decrease(self):
  344. # if few steps are rejected, the stepsize should increase
  345. x = 0.
  346. self.takestep(x)
  347. self.takestep.report(True)
  348. for i in range(self.takestep.interval):
  349. self.takestep(x)
  350. self.takestep.report(False)
  351. assert_(self.ts.stepsize < self.stepsize)
  352. def test_all_accepted(self):
  353. # test that everything works OK if all steps were accepted
  354. x = 0.
  355. for i in range(self.takestep.interval + 1):
  356. self.takestep(x)
  357. self.takestep.report(True)
  358. assert_(self.ts.stepsize > self.stepsize)
  359. def test_all_rejected(self):
  360. # test that everything works OK if all steps were rejected
  361. x = 0.
  362. for i in range(self.takestep.interval + 1):
  363. self.takestep(x)
  364. self.takestep.report(False)
  365. assert_(self.ts.stepsize < self.stepsize)