_dual_annealing.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. # Dual Annealing implementation.
  2. # Copyright (c) 2018 Sylvain Gubian <sylvain.gubian@pmi.com>,
  3. # Yang Xiang <yang.xiang@pmi.com>
  4. # Author: Sylvain Gubian, Yang Xiang, PMP S.A.
  5. """
  6. A Dual Annealing global optimization algorithm
  7. """
  8. from __future__ import division, print_function, absolute_import
  9. import numpy as np
  10. from scipy.optimize import OptimizeResult
  11. from scipy.optimize import minimize
  12. from scipy.special import gammaln
  13. from scipy._lib._util import check_random_state
  14. __all__ = ['dual_annealing']
  15. class VisitingDistribution(object):
  16. """
  17. Class used to generate new coordinates based on the distorted
  18. Cauchy-Lorentz distribution. Depending on the steps within the strategy
  19. chain, the class implements the strategy for generating new location
  20. changes.
  21. Parameters
  22. ----------
  23. lb : array_like
  24. A 1-D numpy ndarray containing lower bounds of the generated
  25. components. Neither NaN or inf are allowed.
  26. ub : array_like
  27. A 1-D numpy ndarray containing upper bounds for the generated
  28. components. Neither NaN or inf are allowed.
  29. visiting_param : float
  30. Parameter for visiting distribution. Default value is 2.62.
  31. Higher values give the visiting distribution a heavier tail, this
  32. makes the algorithm jump to a more distant region.
  33. The value range is (0, 3].
  34. rand_state : RandomState object
  35. A numpy.random.RandomState object for using the current state of the
  36. created random generator container.
  37. """
  38. TAIL_LIMIT = 1.e8
  39. MIN_VISIT_BOUND = 1.e-10
  40. def __init__(self, lb, ub, visiting_param, rand_state):
  41. self.visiting_param = visiting_param
  42. self.rand_state = rand_state
  43. self.lower = lb
  44. self.upper = ub
  45. self.bound_range = ub - lb
  46. def visiting(self, x, step, temperature):
  47. """ Based on the step in the strategy chain, new coordinated are
  48. generated by changing all components is the same time or only
  49. one of them, the new values are computed with visit_fn method
  50. """
  51. dim = x.size
  52. if step < dim:
  53. # Changing all coordinates with a new visting value
  54. visits = np.array([self.visit_fn(
  55. temperature) for _ in range(dim)])
  56. upper_sample = self.rand_state.random_sample()
  57. lower_sample = self.rand_state.random_sample()
  58. visits[visits > self.TAIL_LIMIT] = self.TAIL_LIMIT * upper_sample
  59. visits[visits < -self.TAIL_LIMIT] = -self.TAIL_LIMIT * lower_sample
  60. x_visit = visits + x
  61. a = x_visit - self.lower
  62. b = np.fmod(a, self.bound_range) + self.bound_range
  63. x_visit = np.fmod(b, self.bound_range) + self.lower
  64. x_visit[np.fabs(
  65. x_visit - self.lower) < self.MIN_VISIT_BOUND] += 1.e-10
  66. else:
  67. # Changing only one coordinate at a time based on strategy
  68. # chain step
  69. x_visit = np.copy(x)
  70. visit = self.visit_fn(temperature)
  71. if visit > self.TAIL_LIMIT:
  72. visit = self.TAIL_LIMIT * self.rand_state.random_sample()
  73. elif visit < -self.TAIL_LIMIT:
  74. visit = -self.TAIL_LIMIT * self.rand_state.random_sample()
  75. index = step - dim
  76. x_visit[index] = visit + x[index]
  77. a = x_visit[index] - self.lower[index]
  78. b = np.fmod(a, self.bound_range[index]) + self.bound_range[index]
  79. x_visit[index] = np.fmod(b, self.bound_range[
  80. index]) + self.lower[index]
  81. if np.fabs(x_visit[index] - self.lower[
  82. index]) < self.MIN_VISIT_BOUND:
  83. x_visit[index] += self.MIN_VISIT_BOUND
  84. return x_visit
  85. def visit_fn(self, temperature):
  86. """ Formula Visita from p. 405 of reference [2] """
  87. factor1 = np.exp(np.log(temperature) / (self.visiting_param - 1.0))
  88. factor2 = np.exp((4.0 - self.visiting_param) * np.log(
  89. self.visiting_param - 1.0))
  90. factor3 = np.exp((2.0 - self.visiting_param) * np.log(2.0) / (
  91. self.visiting_param - 1.0))
  92. factor4 = np.sqrt(np.pi) * factor1 * factor2 / (factor3 * (
  93. 3.0 - self.visiting_param))
  94. factor5 = 1.0 / (self.visiting_param - 1.0) - 0.5
  95. d1 = 2.0 - factor5
  96. factor6 = np.pi * (1.0 - factor5) / np.sin(
  97. np.pi * (1.0 - factor5)) / np.exp(gammaln(d1))
  98. sigmax = np.exp(-(self.visiting_param - 1.0) * np.log(
  99. factor6 / factor4) / (3.0 - self.visiting_param))
  100. x = sigmax * self.rand_state.normal()
  101. y = self.rand_state.normal()
  102. den = np.exp(
  103. (self.visiting_param - 1.0) * np.log((np.fabs(y))) / (
  104. 3.0 - self.visiting_param))
  105. return x / den
  106. class EnergyState(object):
  107. """
  108. Class used to record the energy state. At any time, it knows what is the
  109. currently used coordinates and the most recent best location.
  110. Parameters
  111. ----------
  112. lower : array_like
  113. A 1-D numpy ndarray containing lower bounds for generating an initial
  114. random components in the `reset` method.
  115. upper : array_like
  116. A 1-D numpy ndarray containing upper bounds for generating an initial
  117. random components in the `reset` method
  118. components. Neither NaN or inf are allowed.
  119. callback : callable, ``callback(x, f, context)``, optional
  120. A callback function which will be called for all minima found.
  121. ``x`` and ``f`` are the coordinates and function value of the
  122. latest minimum found, and `context` has value in [0, 1, 2]
  123. """
  124. # Maximimum number of trials for generating a valid starting point
  125. MAX_REINIT_COUNT = 1000
  126. def __init__(self, lower, upper, callback=None):
  127. self.ebest = None
  128. self.current_energy = None
  129. self.current_location = None
  130. self.xbest = None
  131. self.lower = lower
  132. self.upper = upper
  133. self.callback = callback
  134. def reset(self, func_wrapper, rand_state, x0=None):
  135. """
  136. Initialize current location is the search domain. If `x0` is not
  137. provided, a random location within the bounds is generated.
  138. """
  139. if x0 is None:
  140. self.current_location = self.lower + rand_state.random_sample(
  141. len(self.lower)) * (self.upper - self.lower)
  142. else:
  143. self.current_location = np.copy(x0)
  144. init_error = True
  145. reinit_counter = 0
  146. while init_error:
  147. self.current_energy = func_wrapper.fun(self.current_location)
  148. if self.current_energy is None:
  149. raise ValueError('Objective function is returning None')
  150. if (not np.isfinite(self.current_energy) or np.isnan(
  151. self.current_energy)):
  152. if reinit_counter >= EnergyState.MAX_REINIT_COUNT:
  153. init_error = False
  154. message = (
  155. 'Stopping algorithm because function '
  156. 'create NaN or (+/-) infinity values even with '
  157. 'trying new random parameters'
  158. )
  159. raise ValueError(message)
  160. self.current_location = self.lower + rand_state.random_sample(
  161. self.lower.size) * (self.upper - self.lower)
  162. reinit_counter += 1
  163. else:
  164. init_error = False
  165. # If first time reset, initialize ebest and xbest
  166. if self.ebest is None and self.xbest is None:
  167. self.ebest = self.current_energy
  168. self.xbest = np.copy(self.current_location)
  169. # Otherwise, we keep them in case of reannealing reset
  170. def update_best(self, e, x, context):
  171. self.ebest = e
  172. self.xbest = np.copy(x)
  173. if self.callback is not None:
  174. val = self.callback(x, e, context)
  175. if val is not None:
  176. if val:
  177. return('Callback function requested to stop early by '
  178. 'returning True')
  179. def update_current(self, e, x):
  180. self.current_energy = e
  181. self.current_location = np.copy(x)
  182. class StrategyChain(object):
  183. """
  184. Class that implements within a Markov chain the strategy for location
  185. acceptance and local search decision making.
  186. Parameters
  187. ----------
  188. acceptance_param : float
  189. Parameter for acceptance distribution. It is used to control the
  190. probability of acceptance. The lower the acceptance parameter, the
  191. smaller the probability of acceptance. Default value is -5.0 with
  192. a range (-1e4, -5].
  193. visit_dist : VisitingDistribution
  194. Instance of `VisitingDistribution` class.
  195. func_wrapper : ObjectiveFunWrapper
  196. Instance of `ObjectiveFunWrapper` class.
  197. minimizer_wrapper: LocalSearchWrapper
  198. Instance of `LocalSearchWrapper` class.
  199. rand_state : RandomState object
  200. A numpy.random.RandomState object for using the current state of the
  201. created random generator container.
  202. energy_state: EnergyState
  203. Instance of `EnergyState` class.
  204. """
  205. def __init__(self, acceptance_param, visit_dist, func_wrapper,
  206. minimizer_wrapper, rand_state, energy_state):
  207. # Local strategy chain minimum energy and location
  208. self.emin = energy_state.current_energy
  209. self.xmin = np.array(energy_state.current_location)
  210. # Global optimizer state
  211. self.energy_state = energy_state
  212. # Acceptance parameter
  213. self.acceptance_param = acceptance_param
  214. # Visiting distribution instance
  215. self.visit_dist = visit_dist
  216. # Wrapper to objective function
  217. self.func_wrapper = func_wrapper
  218. # Wrapper to the local minimizer
  219. self.minimizer_wrapper = minimizer_wrapper
  220. self.not_improved_idx = 0
  221. self.not_improved_max_idx = 1000
  222. self._rand_state = rand_state
  223. self.temperature_step = 0
  224. self.K = 100 * len(energy_state.current_location)
  225. def accept_reject(self, j, e, x_visit):
  226. r = self._rand_state.random_sample()
  227. pqv_temp = (self.acceptance_param - 1.0) * (
  228. e - self.energy_state.current_energy) / (
  229. self.temperature_step + 1.)
  230. if pqv_temp <= 0.:
  231. pqv = 0.
  232. else:
  233. pqv = np.exp(np.log(pqv_temp) / (
  234. 1. - self.acceptance_param))
  235. if r <= pqv:
  236. # We accept the new location and update state
  237. self.energy_state.update_current(e, x_visit)
  238. self.xmin = np.copy(self.energy_state.current_location)
  239. # No improvement for a long time
  240. if self.not_improved_idx >= self.not_improved_max_idx:
  241. if j == 0 or self.energy_state.current_energy < self.emin:
  242. self.emin = self.energy_state.current_energy
  243. self.xmin = np.copy(self.energy_state.current_location)
  244. def run(self, step, temperature):
  245. self.temperature_step = temperature / float(step + 1)
  246. self.not_improved_idx += 1
  247. for j in range(self.energy_state.current_location.size * 2):
  248. if j == 0:
  249. if step == 0:
  250. self.energy_state_improved = True
  251. else:
  252. self.energy_state_improved = False
  253. x_visit = self.visit_dist.visiting(
  254. self.energy_state.current_location, j, temperature)
  255. # Calling the objective function
  256. e = self.func_wrapper.fun(x_visit)
  257. if e < self.energy_state.current_energy:
  258. # We have got a better energy value
  259. self.energy_state.update_current(e, x_visit)
  260. if e < self.energy_state.ebest:
  261. val = self.energy_state.update_best(e, x_visit, 0)
  262. if val is not None:
  263. if val:
  264. return val
  265. self.energy_state_improved = True
  266. self.not_improved_idx = 0
  267. else:
  268. # We have not improved but do we accept the new location?
  269. self.accept_reject(j, e, x_visit)
  270. if self.func_wrapper.nfev >= self.func_wrapper.maxfun:
  271. return ('Maximum number of function call reached '
  272. 'during annealing')
  273. # End of StrategyChain loop
  274. def local_search(self):
  275. # Decision making for performing a local search
  276. # based on strategy chain results
  277. # If energy has been improved or no improvement since too long,
  278. # performing a local search with the best strategy chain location
  279. if self.energy_state_improved:
  280. # Global energy has improved, let's see if LS improves further
  281. e, x = self.minimizer_wrapper.local_search(self.energy_state.xbest,
  282. self.energy_state.ebest)
  283. if e < self.energy_state.ebest:
  284. self.not_improved_idx = 0
  285. val = self.energy_state.update_best(e, x, 1)
  286. if val is not None:
  287. if val:
  288. return val
  289. self.energy_state.update_current(e, x)
  290. if self.func_wrapper.nfev >= self.func_wrapper.maxfun:
  291. return ('Maximum number of function call reached '
  292. 'during local search')
  293. # Check probability of a need to perform a LS even if no improvement
  294. do_ls = False
  295. if self.K < 90 * len(self.energy_state.current_location):
  296. pls = np.exp(self.K * (
  297. self.energy_state.ebest - self.energy_state.current_energy
  298. ) / self.temperature_step)
  299. if pls >= self._rand_state.random_sample():
  300. do_ls = True
  301. # Global energy not improved, let's see what LS gives
  302. # on the best strategy chain location
  303. if self.not_improved_idx >= self.not_improved_max_idx:
  304. do_ls = True
  305. if do_ls:
  306. e, x = self.minimizer_wrapper.local_search(self.xmin, self.emin)
  307. self.xmin = np.copy(x)
  308. self.emin = e
  309. self.not_improved_idx = 0
  310. self.not_improved_max_idx = self.energy_state.current_location.size
  311. if e < self.energy_state.ebest:
  312. val = self.energy_state.update_best(
  313. self.emin, self.xmin, 2)
  314. if val is not None:
  315. if val:
  316. return val
  317. self.energy_state.update_current(e, x)
  318. if self.func_wrapper.nfev >= self.func_wrapper.maxfun:
  319. return ('Maximum number of function call reached '
  320. 'during dual annealing')
  321. class ObjectiveFunWrapper(object):
  322. def __init__(self, func, maxfun=1e7, *args):
  323. self.func = func
  324. self.args = args
  325. # Number of objective function evaluations
  326. self.nfev = 0
  327. # Number of gradient function evaluation if used
  328. self.ngev = 0
  329. # Number of hessian of the objective function if used
  330. self.nhev = 0
  331. self.maxfun = maxfun
  332. def fun(self, x):
  333. self.nfev += 1
  334. return self.func(x, *self.args)
  335. class LocalSearchWrapper(object):
  336. """
  337. Class used to wrap around the minimizer used for local search
  338. Default local minimizer is SciPy minimizer L-BFGS-B
  339. """
  340. LS_MAXITER_RATIO = 6
  341. LS_MAXITER_MIN = 100
  342. LS_MAXITER_MAX = 1000
  343. def __init__(self, bounds, func_wrapper, **kwargs):
  344. self.func_wrapper = func_wrapper
  345. self.kwargs = kwargs
  346. self.minimizer = minimize
  347. bounds_list = list(zip(*bounds))
  348. self.lower = np.array(bounds_list[0])
  349. self.upper = np.array(bounds_list[1])
  350. # If no minimizer specified, use SciPy minimize with 'L-BFGS-B' method
  351. if not self.kwargs:
  352. n = len(self.lower)
  353. ls_max_iter = min(max(n * self.LS_MAXITER_RATIO,
  354. self.LS_MAXITER_MIN),
  355. self.LS_MAXITER_MAX)
  356. self.kwargs['method'] = 'L-BFGS-B'
  357. self.kwargs['options'] = {
  358. 'maxiter': ls_max_iter,
  359. }
  360. self.kwargs['bounds'] = list(zip(self.lower, self.upper))
  361. def local_search(self, x, e):
  362. # Run local search from the given x location where energy value is e
  363. x_tmp = np.copy(x)
  364. mres = self.minimizer(self.func_wrapper.fun, x, **self.kwargs)
  365. if 'njev' in mres.keys():
  366. self.func_wrapper.ngev += mres.njev
  367. if 'nhev' in mres.keys():
  368. self.func_wrapper.nhev += mres.nhev
  369. # Check if is valid value
  370. is_finite = np.all(np.isfinite(mres.x)) and np.isfinite(mres.fun)
  371. in_bounds = np.all(mres.x >= self.lower) and np.all(
  372. mres.x <= self.upper)
  373. is_valid = is_finite and in_bounds
  374. # Use the new point only if it is valid and return a better results
  375. if is_valid and mres.fun < e:
  376. return mres.fun, mres.x
  377. else:
  378. return e, x_tmp
  379. def dual_annealing(func, bounds, args=(), maxiter=1000,
  380. local_search_options={}, initial_temp=5230.,
  381. restart_temp_ratio=2.e-5, visit=2.62, accept=-5.0,
  382. maxfun=1e7, seed=None, no_local_search=False,
  383. callback=None, x0=None):
  384. """
  385. Find the global minimum of a function using Dual Annealing.
  386. Parameters
  387. ----------
  388. func : callable
  389. The objective function to be minimized. Must be in the form
  390. ``f(x, *args)``, where ``x`` is the argument in the form of a 1-D array
  391. and ``args`` is a tuple of any additional fixed parameters needed to
  392. completely specify the function.
  393. bounds : sequence, shape (n, 2)
  394. Bounds for variables. ``(min, max)`` pairs for each element in ``x``,
  395. defining bounds for the objective function parameter.
  396. args : tuple, optional
  397. Any additional fixed parameters needed to completely specify the
  398. objective function.
  399. maxiter : int, optional
  400. The maximum number of global search iterations. Default value is 1000.
  401. local_search_options : dict, optional
  402. Extra keyword arguments to be passed to the local minimizer
  403. (`minimize`). Some important options could be:
  404. ``method`` for the minimizer method to use and ``args`` for
  405. objective function additional arguments.
  406. initial_temp : float, optional
  407. The initial temperature, use higher values to facilitates a wider
  408. search of the energy landscape, allowing dual_annealing to escape
  409. local minima that it is trapped in. Default value is 5230. Range is
  410. (0.01, 5.e4].
  411. restart_temp_ratio : float, optional
  412. During the annealing process, temperature is decreasing, when it
  413. reaches ``initial_temp * restart_temp_ratio``, the reannealing process
  414. is triggered. Default value of the ratio is 2e-5. Range is (0, 1).
  415. visit : float, optional
  416. Parameter for visiting distribution. Default value is 2.62. Higher
  417. values give the visiting distribution a heavier tail, this makes
  418. the algorithm jump to a more distant region. The value range is (0, 3].
  419. accept : float, optional
  420. Parameter for acceptance distribution. It is used to control the
  421. probability of acceptance. The lower the acceptance parameter, the
  422. smaller the probability of acceptance. Default value is -5.0 with
  423. a range (-1e4, -5].
  424. maxfun : int, optional
  425. Soft limit for the number of objective function calls. If the
  426. algorithm is in the middle of a local search, this number will be
  427. exceeded, the algorithm will stop just after the local search is
  428. done. Default value is 1e7.
  429. seed : {int or `numpy.random.RandomState` instance}, optional
  430. If `seed` is not specified the `numpy.random.RandomState` singleton is
  431. used.
  432. If `seed` is an int, a new ``RandomState`` instance is used,
  433. seeded with `seed`.
  434. If `seed` is already a ``RandomState`` instance, then that
  435. instance is used.
  436. Specify `seed` for repeatable minimizations. The random numbers
  437. generated with this seed only affect the visiting distribution
  438. function and new coordinates generation.
  439. no_local_search : bool, optional
  440. If `no_local_search` is set to True, a traditional Generalized
  441. Simulated Annealing will be performed with no local search
  442. strategy applied.
  443. callback : callable, optional
  444. A callback function with signature ``callback(x, f, context)``,
  445. which will be called for all minima found.
  446. ``x`` and ``f`` are the coordinates and function value of the
  447. latest minimum found, and ``context`` has value in [0, 1, 2], with the
  448. following meaning:
  449. - 0: minimum detected in the annealing process.
  450. - 1: detection occured in the local search process.
  451. - 2: detection done in the dual annealing process.
  452. If the callback implementation returns True, the algorithm will stop.
  453. x0 : ndarray, shape(n,), optional
  454. Coordinates of a single n-dimensional starting point.
  455. Returns
  456. -------
  457. res : OptimizeResult
  458. The optimization result represented as a `OptimizeResult` object.
  459. Important attributes are: ``x`` the solution array, ``fun`` the value
  460. of the function at the solution, and ``message`` which describes the
  461. cause of the termination.
  462. See `OptimizeResult` for a description of other attributes.
  463. Notes
  464. -----
  465. This function implements the Dual Annealing optimization. This stochastic
  466. approach derived from [3]_ combines the generalization of CSA (Classical
  467. Simulated Annealing) and FSA (Fast Simulated Annealing) [1]_ [2]_ coupled
  468. to a strategy for applying a local search on accepted locations [4]_.
  469. An alternative implementation of this same algorithm is described in [5]_
  470. and benchmarks are presented in [6]_. This approach introduces an advanced
  471. method to refine the solution found by the generalized annealing
  472. process. This algorithm uses a distorted Cauchy-Lorentz visiting
  473. distribution, with its shape controlled by the parameter :math:`q_{v}`
  474. .. math::
  475. g_{q_{v}}(\\Delta x(t)) \\propto \\frac{ \\
  476. \\left[T_{q_{v}}(t) \\right]^{-\\frac{D}{3-q_{v}}}}{ \\
  477. \\left[{1+(q_{v}-1)\\frac{(\\Delta x(t))^{2}} { \\
  478. \\left[T_{q_{v}}(t)\\right]^{\\frac{2}{3-q_{v}}}}}\\right]^{ \\
  479. \\frac{1}{q_{v}-1}+\\frac{D-1}{2}}}
  480. Where :math:`t` is the artificial time. This visiting distribution is used
  481. to generate a trial jump distance :math:`\\Delta x(t)` of variable
  482. :math:`x(t)` under artificial temperature :math:`T_{q_{v}}(t)`.
  483. From the starting point, after calling the visiting distribution
  484. function, the acceptance probability is computed as follows:
  485. .. math::
  486. p_{q_{a}} = \\min{\\{1,\\left[1-(1-q_{a}) \\beta \\Delta E \\right]^{ \\
  487. \\frac{1}{1-q_{a}}}\\}}
  488. Where :math:`q_{a}` is a acceptance parameter. For :math:`q_{a}<1`, zero
  489. acceptance probability is assigned to the cases where
  490. .. math::
  491. [1-(1-q_{a}) \\beta \\Delta E] < 0
  492. The artificial temperature :math:`T_{q_{v}}(t)` is decreased according to
  493. .. math::
  494. T_{q_{v}}(t) = T_{q_{v}}(1) \\frac{2^{q_{v}-1}-1}{\\left( \\
  495. 1 + t\\right)^{q_{v}-1}-1}
  496. Where :math:`q_{v}` is the visiting parameter.
  497. .. versionadded:: 1.2.0
  498. References
  499. ----------
  500. .. [1] Tsallis C. Possible generalization of Boltzmann-Gibbs
  501. statistics. Journal of Statistical Physics, 52, 479-487 (1998).
  502. .. [2] Tsallis C, Stariolo DA. Generalized Simulated Annealing.
  503. Physica A, 233, 395-406 (1996).
  504. .. [3] Xiang Y, Sun DY, Fan W, Gong XG. Generalized Simulated
  505. Annealing Algorithm and Its Application to the Thomson Model.
  506. Physics Letters A, 233, 216-220 (1997).
  507. .. [4] Xiang Y, Gong XG. Efficiency of Generalized Simulated
  508. Annealing. Physical Review E, 62, 4473 (2000).
  509. .. [5] Xiang Y, Gubian S, Suomela B, Hoeng J. Generalized
  510. Simulated Annealing for Efficient Global Optimization: the GenSA
  511. Package for R. The R Journal, Volume 5/1 (2013).
  512. .. [6] Mullen, K. Continuous Global Optimization in R. Journal of
  513. Statistical Software, 60(6), 1 - 45, (2014). DOI:10.18637/jss.v060.i06
  514. Examples
  515. --------
  516. The following example is a 10-dimensional problem, with many local minima.
  517. The function involved is called Rastrigin
  518. (https://en.wikipedia.org/wiki/Rastrigin_function)
  519. >>> from scipy.optimize import dual_annealing
  520. >>> func = lambda x: np.sum(x*x - 10*np.cos(2*np.pi*x)) + 10*np.size(x)
  521. >>> lw = [-5.12] * 10
  522. >>> up = [5.12] * 10
  523. >>> ret = dual_annealing(func, bounds=list(zip(lw, up)), seed=1234)
  524. >>> print("global minimum: xmin = {0}, f(xmin) = {1:.6f}".format(
  525. ... ret.x, ret.fun))
  526. global minimum: xmin = [-4.26437714e-09 -3.91699361e-09 -1.86149218e-09 -3.97165720e-09
  527. -6.29151648e-09 -6.53145322e-09 -3.93616815e-09 -6.55623025e-09
  528. -6.05775280e-09 -5.00668935e-09], f(xmin) = 0.000000
  529. """
  530. if x0 is not None and not len(x0) == len(bounds):
  531. raise ValueError('Bounds size does not match x0')
  532. lu = list(zip(*bounds))
  533. lower = np.array(lu[0])
  534. upper = np.array(lu[1])
  535. # Check that restart temperature ratio is correct
  536. if restart_temp_ratio <= 0. or restart_temp_ratio >= 1.:
  537. raise ValueError('Restart temperature ratio has to be in range (0, 1)')
  538. # Checking bounds are valid
  539. if (np.any(np.isinf(lower)) or np.any(np.isinf(upper)) or np.any(
  540. np.isnan(lower)) or np.any(np.isnan(upper))):
  541. raise ValueError('Some bounds values are inf values or nan values')
  542. # Checking that bounds are consistent
  543. if not np.all(lower < upper):
  544. raise ValueError('Bounds are not consistent min < max')
  545. # Checking that bounds are the same length
  546. if not len(lower) == len(upper):
  547. raise ValueError('Bounds do not have the same dimensions')
  548. # Wrapper for the objective function
  549. func_wrapper = ObjectiveFunWrapper(func, maxfun, *args)
  550. # Wrapper fot the minimizer
  551. minimizer_wrapper = LocalSearchWrapper(
  552. bounds, func_wrapper, **local_search_options)
  553. # Initialization of RandomState for reproducible runs if seed provided
  554. rand_state = check_random_state(seed)
  555. # Initialization of the energy state
  556. energy_state = EnergyState(lower, upper, callback)
  557. energy_state.reset(func_wrapper, rand_state, x0)
  558. # Minimum value of annealing temperature reached to perform
  559. # re-annealing
  560. temperature_restart = initial_temp * restart_temp_ratio
  561. # VisitingDistribution instance
  562. visit_dist = VisitingDistribution(lower, upper, visit, rand_state)
  563. # Strategy chain instance
  564. strategy_chain = StrategyChain(accept, visit_dist, func_wrapper,
  565. minimizer_wrapper, rand_state, energy_state)
  566. # Run the search loop
  567. need_to_stop = False
  568. iteration = 0
  569. message = []
  570. t1 = np.exp((visit - 1) * np.log(2.0)) - 1.0
  571. while(not need_to_stop):
  572. for i in range(maxiter):
  573. # Compute temperature for this step
  574. s = float(i) + 2.0
  575. t2 = np.exp((visit - 1) * np.log(s)) - 1.0
  576. temperature = initial_temp * t1 / t2
  577. if iteration >= maxiter:
  578. message.append("Maximum number of iteration reached")
  579. need_to_stop = True
  580. break
  581. # Need a re-annealing process?
  582. if temperature < temperature_restart:
  583. energy_state.reset(func_wrapper, rand_state)
  584. break
  585. # starting strategy chain
  586. val = strategy_chain.run(i, temperature)
  587. if val is not None:
  588. message.append(val)
  589. need_to_stop = True
  590. break
  591. # Possible local search at the end of the strategy chain
  592. if not no_local_search:
  593. val = strategy_chain.local_search()
  594. if val is not None:
  595. message.append(val)
  596. need_to_stop = True
  597. break
  598. iteration += 1
  599. # Return the OptimizeResult
  600. res = OptimizeResult()
  601. res.x = energy_state.xbest
  602. res.fun = energy_state.ebest
  603. res.nit = iteration
  604. res.nfev = func_wrapper.nfev
  605. res.njev = func_wrapper.ngev
  606. res.nhev = func_wrapper.nhev
  607. res.message = message
  608. return res