test__dual_annealing.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. # Dual annealing unit tests implementation.
  2. # Copyright (c) 2018 Sylvain Gubian <sylvain.gubian@pmi.com>,
  3. # Yang Xiang <yang.xiang@pmi.com>
  4. # Author: Sylvain Gubian, PMP S.A.
  5. """
  6. Unit tests for the dual annealing global optimizer
  7. """
  8. from scipy.optimize import dual_annealing, Bounds
  9. from scipy.optimize._dual_annealing import EnergyState
  10. from scipy.optimize._dual_annealing import LocalSearchWrapper
  11. from scipy.optimize._dual_annealing import ObjectiveFunWrapper
  12. from scipy.optimize._dual_annealing import StrategyChain
  13. from scipy.optimize._dual_annealing import VisitingDistribution
  14. from scipy.optimize import rosen, rosen_der
  15. import pytest
  16. import numpy as np
  17. from numpy.testing import assert_equal, assert_allclose, assert_array_less
  18. from pytest import raises as assert_raises
  19. from scipy._lib._util import check_random_state
  20. import threading
  21. class TestDualAnnealing:
  22. def setup_method(self):
  23. # A function that returns always infinity for initialization tests
  24. self.weirdfunc = lambda x: np.inf
  25. # 2-D bounds for testing function
  26. self.ld_bounds = [(-5.12, 5.12)] * 2
  27. # 4-D bounds for testing function
  28. self.hd_bounds = self.ld_bounds * 4
  29. # Number of values to be generated for testing visit function
  30. self.nbtestvalues = 5000
  31. self.high_temperature = 5230
  32. self.low_temperature = 0.1
  33. self.qv = 2.62
  34. self.seed = 1234
  35. self.nb_fun_call = threading.local()
  36. self.ngev = threading.local()
  37. def callback(self, x, f, context):
  38. # For testing callback mechanism. Should stop for e <= 1 as
  39. # the callback function returns True
  40. if f <= 1.0:
  41. return True
  42. def func(self, x, args=()):
  43. # Using Rastrigin function for performing tests
  44. if args:
  45. shift = args
  46. else:
  47. shift = 0
  48. y = np.sum((x - shift) ** 2 - 10 * np.cos(2 * np.pi * (
  49. x - shift))) + 10 * np.size(x) + shift
  50. if not hasattr(self.nb_fun_call, 'c'):
  51. self.nb_fun_call.c = 0
  52. self.nb_fun_call.c += 1
  53. return y
  54. def rosen_der_wrapper(self, x, args=()):
  55. if not hasattr(self.ngev, 'c'):
  56. self.ngev.c = 0
  57. self.ngev.c += 1
  58. return rosen_der(x, *args)
  59. # FIXME: there are some discontinuities in behaviour as a function of `qv`,
  60. # this needs investigating - see gh-12384
  61. @pytest.mark.parametrize('qv', [1.1, 1.41, 2, 2.62, 2.9])
  62. def test_visiting_stepping(self, qv):
  63. rng = np.random.default_rng(1234)
  64. lu = list(zip(*self.ld_bounds))
  65. lower = np.array(lu[0])
  66. upper = np.array(lu[1])
  67. dim = lower.size
  68. vd = VisitingDistribution(lower, upper, qv, rng)
  69. values = np.zeros(dim)
  70. x_step_low = vd.visiting(values, 0, self.high_temperature)
  71. # Make sure that only the first component is changed
  72. assert_equal(np.not_equal(x_step_low, 0), True)
  73. values = np.zeros(dim)
  74. x_step_high = vd.visiting(values, dim, self.high_temperature)
  75. # Make sure that component other than at dim has changed
  76. assert_equal(np.not_equal(x_step_high[0], 0), True)
  77. @pytest.mark.parametrize('qv', [2.25, 2.62, 2.9])
  78. def test_visiting_dist_high_temperature(self, qv):
  79. rng = np.random.default_rng(1234)
  80. lu = list(zip(*self.ld_bounds))
  81. lower = np.array(lu[0])
  82. upper = np.array(lu[1])
  83. vd = VisitingDistribution(lower, upper, qv, rng)
  84. # values = np.zeros(self.nbtestvalues)
  85. # for i in np.arange(self.nbtestvalues):
  86. # values[i] = vd.visit_fn(self.high_temperature)
  87. values = vd.visit_fn(self.high_temperature, self.nbtestvalues)
  88. # Visiting distribution is a distorted version of Cauchy-Lorentz
  89. # distribution, and as no 1st and higher moments (no mean defined,
  90. # no variance defined).
  91. # Check that big tails values are generated
  92. assert_array_less(np.min(values), 1e-10)
  93. assert_array_less(1e+10, np.max(values))
  94. def test_reset(self):
  95. owf = ObjectiveFunWrapper(self.weirdfunc)
  96. lu = list(zip(*self.ld_bounds))
  97. lower = np.array(lu[0])
  98. upper = np.array(lu[1])
  99. es = EnergyState(lower, upper)
  100. assert_raises(ValueError, es.reset, owf, check_random_state(None))
  101. def test_low_dim(self):
  102. ret = dual_annealing(
  103. self.func, self.ld_bounds, rng=self.seed)
  104. assert_allclose(ret.fun, 0., atol=1e-12)
  105. assert ret.success
  106. @pytest.mark.fail_slow(10)
  107. def test_high_dim(self):
  108. ret = dual_annealing(self.func, self.hd_bounds, rng=self.seed)
  109. assert_allclose(ret.fun, 0., atol=1e-12)
  110. assert ret.success
  111. def test_low_dim_no_ls(self):
  112. ret = dual_annealing(self.func, self.ld_bounds,
  113. no_local_search=True, seed=self.seed)
  114. assert_allclose(ret.fun, 0., atol=1e-4)
  115. @pytest.mark.fail_slow(10)
  116. def test_high_dim_no_ls(self):
  117. ret = dual_annealing(self.func, self.hd_bounds,
  118. no_local_search=True, rng=self.seed)
  119. assert_allclose(ret.fun, 0., atol=1.2e-4)
  120. def test_nb_fun_call(self):
  121. self.nb_fun_call.c = 0
  122. ret = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
  123. assert_equal(self.nb_fun_call.c, ret.nfev)
  124. def test_nb_fun_call_no_ls(self):
  125. self.nb_fun_call.c = 0
  126. ret = dual_annealing(self.func, self.ld_bounds,
  127. no_local_search=True, rng=self.seed)
  128. assert_equal(self.nb_fun_call.c, ret.nfev)
  129. def test_max_reinit(self):
  130. assert_raises(ValueError, dual_annealing, self.weirdfunc,
  131. self.ld_bounds)
  132. @pytest.mark.fail_slow(10)
  133. def test_reproduce(self):
  134. res1 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
  135. res2 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
  136. res3 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
  137. # If we have reproducible results, x components found has to
  138. # be exactly the same, which is not the case with no seeding
  139. assert_equal(res1.x, res2.x)
  140. assert_equal(res1.x, res3.x)
  141. def test_rand_gen(self):
  142. # check that np.random.Generator can be used (numpy >= 1.17)
  143. # obtain a np.random.Generator object
  144. rng = np.random.default_rng(1)
  145. res1 = dual_annealing(self.func, self.ld_bounds, rng=rng)
  146. # seed again
  147. rng = np.random.default_rng(1)
  148. res2 = dual_annealing(self.func, self.ld_bounds, rng=rng)
  149. # If we have reproducible results, x components found has to
  150. # be exactly the same, which is not the case with no seeding
  151. assert_equal(res1.x, res2.x)
  152. def test_bounds_integrity(self):
  153. wrong_bounds = [(-5.12, 5.12), (1, 0), (5.12, 5.12)]
  154. assert_raises(ValueError, dual_annealing, self.func,
  155. wrong_bounds)
  156. def test_bound_validity(self):
  157. invalid_bounds = [(-5, 5), (-np.inf, 0), (-5, 5)]
  158. assert_raises(ValueError, dual_annealing, self.func,
  159. invalid_bounds)
  160. invalid_bounds = [(-5, 5), (0, np.inf), (-5, 5)]
  161. assert_raises(ValueError, dual_annealing, self.func,
  162. invalid_bounds)
  163. invalid_bounds = [(-5, 5), (0, np.nan), (-5, 5)]
  164. assert_raises(ValueError, dual_annealing, self.func,
  165. invalid_bounds)
  166. def test_deprecated_local_search_options_bounds(self):
  167. def func(x):
  168. return np.sum((x - 5) * (x - 1))
  169. bounds = list(zip([-6, -5], [6, 5]))
  170. # Test bounds can be passed (see gh-10831)
  171. with pytest.warns(RuntimeWarning, match=r"Method CG cannot handle "):
  172. dual_annealing(
  173. func,
  174. bounds=bounds,
  175. minimizer_kwargs={"method": "CG", "bounds": bounds})
  176. def test_minimizer_kwargs_bounds(self):
  177. def func(x):
  178. return np.sum((x - 5) * (x - 1))
  179. bounds = list(zip([-6, -5], [6, 5]))
  180. # Test bounds can be passed (see gh-10831)
  181. dual_annealing(
  182. func,
  183. bounds=bounds,
  184. minimizer_kwargs={"method": "SLSQP", "bounds": bounds})
  185. with pytest.warns(RuntimeWarning, match=r"Method CG cannot handle "):
  186. dual_annealing(
  187. func,
  188. bounds=bounds,
  189. minimizer_kwargs={"method": "CG", "bounds": bounds})
  190. def test_max_fun_ls(self):
  191. ret = dual_annealing(self.func, self.ld_bounds, maxfun=100,
  192. rng=self.seed)
  193. ls_max_iter = min(max(
  194. len(self.ld_bounds) * LocalSearchWrapper.LS_MAXITER_RATIO,
  195. LocalSearchWrapper.LS_MAXITER_MIN),
  196. LocalSearchWrapper.LS_MAXITER_MAX)
  197. assert ret.nfev <= 100 + ls_max_iter
  198. assert not ret.success
  199. def test_max_fun_no_ls(self):
  200. ret = dual_annealing(self.func, self.ld_bounds,
  201. no_local_search=True, maxfun=500, rng=self.seed)
  202. assert ret.nfev <= 500
  203. assert not ret.success
  204. def test_maxiter(self):
  205. ret = dual_annealing(self.func, self.ld_bounds, maxiter=700,
  206. rng=self.seed)
  207. assert ret.nit <= 700
  208. # Testing that args are passed correctly for dual_annealing
  209. def test_fun_args_ls(self):
  210. ret = dual_annealing(self.func, self.ld_bounds,
  211. args=((3.14159,)), rng=self.seed)
  212. assert_allclose(ret.fun, 3.14159, atol=1e-6)
  213. # Testing that args are passed correctly for pure simulated annealing
  214. def test_fun_args_no_ls(self):
  215. ret = dual_annealing(self.func, self.ld_bounds,
  216. args=((3.14159, )), no_local_search=True,
  217. rng=self.seed)
  218. assert_allclose(ret.fun, 3.14159, atol=1e-4)
  219. def test_callback_stop(self):
  220. # Testing that callback make the algorithm stop for
  221. # fun value <= 1.0 (see callback method)
  222. ret = dual_annealing(self.func, self.ld_bounds,
  223. callback=self.callback, rng=self.seed)
  224. assert ret.fun <= 1.0
  225. assert 'stop early' in ret.message[0]
  226. assert not ret.success
  227. @pytest.mark.parametrize('method, atol', [
  228. ('Nelder-Mead', 2e-5),
  229. ('COBYLA', 1e-5),
  230. ('COBYQA', 1e-8),
  231. ('Powell', 1e-8),
  232. ('CG', 1e-8),
  233. ('BFGS', 1e-8),
  234. ('TNC', 1e-8),
  235. ('SLSQP', 2e-7),
  236. ])
  237. def test_multi_ls_minimizer(self, method, atol):
  238. ret = dual_annealing(self.func, self.ld_bounds,
  239. minimizer_kwargs=dict(method=method),
  240. rng=self.seed)
  241. assert_allclose(ret.fun, 0., atol=atol)
  242. def test_wrong_restart_temp(self):
  243. assert_raises(ValueError, dual_annealing, self.func,
  244. self.ld_bounds, restart_temp_ratio=1)
  245. assert_raises(ValueError, dual_annealing, self.func,
  246. self.ld_bounds, restart_temp_ratio=0)
  247. def test_gradient_gnev(self):
  248. minimizer_opts = {
  249. 'jac': self.rosen_der_wrapper,
  250. }
  251. ret = dual_annealing(rosen, self.ld_bounds,
  252. minimizer_kwargs=minimizer_opts,
  253. rng=self.seed)
  254. assert ret.njev == self.ngev.c
  255. @pytest.mark.fail_slow(10)
  256. def test_from_docstring(self):
  257. def func(x):
  258. return np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x)
  259. lw = [-5.12] * 10
  260. up = [5.12] * 10
  261. ret = dual_annealing(func, bounds=list(zip(lw, up)), rng=1234)
  262. assert_allclose(ret.x,
  263. [-4.26437714e-09, -3.91699361e-09, -1.86149218e-09,
  264. -3.97165720e-09, -6.29151648e-09, -6.53145322e-09,
  265. -3.93616815e-09, -6.55623025e-09, -6.05775280e-09,
  266. -5.00668935e-09], atol=4e-8)
  267. assert_allclose(ret.fun, 0.000000, atol=5e-13)
  268. @pytest.mark.parametrize('new_e, temp_step, accepted, accept_rate', [
  269. (0, 100, 1000, 1.0097587941791923),
  270. (0, 2, 1000, 1.2599210498948732),
  271. (10, 100, 878, 0.8786035869128718),
  272. (10, 60, 695, 0.6812920690579612),
  273. (2, 100, 990, 0.9897404249173424),
  274. ])
  275. def test_accept_reject_probabilistic(
  276. self, new_e, temp_step, accepted, accept_rate):
  277. # Test accepts unconditionally with e < current_energy and
  278. # probabilistically with e > current_energy
  279. rs = check_random_state(123)
  280. count_accepted = 0
  281. iterations = 1000
  282. accept_param = -5
  283. current_energy = 1
  284. for _ in range(iterations):
  285. energy_state = EnergyState(lower=None, upper=None)
  286. # Set energy state with current_energy, any location.
  287. energy_state.update_current(current_energy, [0])
  288. chain = StrategyChain(
  289. accept_param, None, None, None, rs, energy_state)
  290. # Normally this is set in run()
  291. chain.temperature_step = temp_step
  292. # Check if update is accepted.
  293. chain.accept_reject(j=1, e=new_e, x_visit=[2])
  294. if energy_state.current_energy == new_e:
  295. count_accepted += 1
  296. assert count_accepted == accepted
  297. # Check accept rate
  298. pqv = 1 - (1 - accept_param) * (new_e - current_energy) / temp_step
  299. rate = 0 if pqv <= 0 else np.exp(np.log(pqv) / (1 - accept_param))
  300. assert_allclose(rate, accept_rate)
  301. @pytest.mark.fail_slow(10)
  302. def test_bounds_class(self):
  303. # test that result does not depend on the bounds type
  304. def func(x):
  305. f = np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x)
  306. return f
  307. lw = [-5.12] * 5
  308. up = [5.12] * 5
  309. # Unbounded global minimum is all zeros. Most bounds below will force
  310. # a DV away from unbounded minimum and be active at solution.
  311. up[0] = -2.0
  312. up[1] = -1.0
  313. lw[3] = 1.0
  314. lw[4] = 2.0
  315. # run optimizations
  316. bounds = Bounds(lw, up)
  317. ret_bounds_class = dual_annealing(func, bounds=bounds, rng=1234)
  318. bounds_old = list(zip(lw, up))
  319. ret_bounds_list = dual_annealing(func, bounds=bounds_old, rng=1234)
  320. # test that found minima, function evaluations and iterations match
  321. assert_allclose(ret_bounds_class.x, ret_bounds_list.x, atol=1e-8)
  322. assert_allclose(ret_bounds_class.x, np.arange(-2, 3), atol=1e-7)
  323. assert_allclose(ret_bounds_list.fun, ret_bounds_class.fun, atol=1e-9)
  324. assert ret_bounds_list.nfev == ret_bounds_class.nfev
  325. @pytest.mark.fail_slow(10)
  326. def test_callable_jac_hess_with_args_gh11052(self):
  327. # dual_annealing used to fail when `jac` was callable and `args` were
  328. # used; check that this is resolved. Example is from gh-11052.
  329. # extended to hess as part of closing gh20614
  330. rng = np.random.default_rng(94253637693657847462)
  331. def f(x, power):
  332. return np.sum(np.exp(x ** power))
  333. def jac(x, power):
  334. return np.exp(x ** power) * power * x ** (power - 1)
  335. def hess(x, power):
  336. # calculated using WolframAlpha as d^2/dx^2 e^(x^p)
  337. return np.diag(
  338. power * np.exp(x ** power) * x ** (power - 2) *
  339. (power * x ** power + power - 1)
  340. )
  341. def hessp(x, p, power):
  342. return hess(x, power) @ p
  343. res1 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
  344. minimizer_kwargs=dict(method='L-BFGS-B'))
  345. res2 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
  346. minimizer_kwargs=dict(method='L-BFGS-B',
  347. jac=jac))
  348. res3 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
  349. minimizer_kwargs=dict(method='newton-cg',
  350. jac=jac, hess=hess))
  351. res4 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
  352. minimizer_kwargs=dict(method='newton-cg',
  353. jac=jac, hessp=hessp))
  354. assert_allclose(res1.fun, res2.fun, rtol=1e-6)
  355. assert_allclose(res3.fun, res2.fun, rtol=1e-6)
  356. assert_allclose(res4.fun, res2.fun, rtol=1e-6)