Coverage for /usr/lib/python3/dist-packages/scipy/optimize/_basinhopping.py: 17%

210 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2basinhopping: The basinhopping global optimization algorithm 

3""" 

4import numpy as np 

5import math 

6import inspect 

7import scipy.optimize 

8from scipy._lib._util import check_random_state 

9 

10__all__ = ['basinhopping'] 

11 

12 

13_params = (inspect.Parameter('res_new', kind=inspect.Parameter.KEYWORD_ONLY), 

14 inspect.Parameter('res_old', kind=inspect.Parameter.KEYWORD_ONLY)) 

15_new_accept_test_signature = inspect.Signature(parameters=_params) 

16 

17 

18class Storage: 

19 """ 

20 Class used to store the lowest energy structure 

21 """ 

22 def __init__(self, minres): 

23 self._add(minres) 

24 

25 def _add(self, minres): 

26 self.minres = minres 

27 self.minres.x = np.copy(minres.x) 

28 

29 def update(self, minres): 

30 if minres.success and (minres.fun < self.minres.fun 

31 or not self.minres.success): 

32 self._add(minres) 

33 return True 

34 else: 

35 return False 

36 

37 def get_lowest(self): 

38 return self.minres 

39 

40 

41class BasinHoppingRunner: 

42 """This class implements the core of the basinhopping algorithm. 

43 

44 x0 : ndarray 

45 The starting coordinates. 

46 minimizer : callable 

47 The local minimizer, with signature ``result = minimizer(x)``. 

48 The return value is an `optimize.OptimizeResult` object. 

49 step_taking : callable 

50 This function displaces the coordinates randomly. Signature should 

51 be ``x_new = step_taking(x)``. Note that `x` may be modified in-place. 

52 accept_tests : list of callables 

53 Each test is passed the kwargs `f_new`, `x_new`, `f_old` and 

54 `x_old`. These tests will be used to judge whether or not to accept 

55 the step. The acceptable return values are True, False, or ``"force 

56 accept"``. If any of the tests return False then the step is rejected. 

57 If ``"force accept"``, then this will override any other tests in 

58 order to accept the step. This can be used, for example, to forcefully 

59 escape from a local minimum that ``basinhopping`` is trapped in. 

60 disp : bool, optional 

61 Display status messages. 

62 

63 """ 

64 def __init__(self, x0, minimizer, step_taking, accept_tests, disp=False): 

65 self.x = np.copy(x0) 

66 self.minimizer = minimizer 

67 self.step_taking = step_taking 

68 self.accept_tests = accept_tests 

69 self.disp = disp 

70 

71 self.nstep = 0 

72 

73 # initialize return object 

74 self.res = scipy.optimize.OptimizeResult() 

75 self.res.minimization_failures = 0 

76 

77 # do initial minimization 

78 minres = minimizer(self.x) 

79 if not minres.success: 

80 self.res.minimization_failures += 1 

81 if self.disp: 

82 print("warning: basinhopping: local minimization failure") 

83 self.x = np.copy(minres.x) 

84 self.energy = minres.fun 

85 self.incumbent_minres = minres # best minimize result found so far 

86 if self.disp: 

87 print("basinhopping step %d: f %g" % (self.nstep, self.energy)) 

88 

89 # initialize storage class 

90 self.storage = Storage(minres) 

91 

92 if hasattr(minres, "nfev"): 

93 self.res.nfev = minres.nfev 

94 if hasattr(minres, "njev"): 

95 self.res.njev = minres.njev 

96 if hasattr(minres, "nhev"): 

97 self.res.nhev = minres.nhev 

98 

99 def _monte_carlo_step(self): 

100 """Do one Monte Carlo iteration 

101 

102 Randomly displace the coordinates, minimize, and decide whether 

103 or not to accept the new coordinates. 

104 """ 

105 # Take a random step. Make a copy of x because the step_taking 

106 # algorithm might change x in place 

107 x_after_step = np.copy(self.x) 

108 x_after_step = self.step_taking(x_after_step) 

109 

110 # do a local minimization 

111 minres = self.minimizer(x_after_step) 

112 x_after_quench = minres.x 

113 energy_after_quench = minres.fun 

114 if not minres.success: 

115 self.res.minimization_failures += 1 

116 if self.disp: 

117 print("warning: basinhopping: local minimization failure") 

118 if hasattr(minres, "nfev"): 

119 self.res.nfev += minres.nfev 

120 if hasattr(minres, "njev"): 

121 self.res.njev += minres.njev 

122 if hasattr(minres, "nhev"): 

123 self.res.nhev += minres.nhev 

124 

125 # accept the move based on self.accept_tests. If any test is False, 

126 # then reject the step. If any test returns the special string 

127 # 'force accept', then accept the step regardless. This can be used 

128 # to forcefully escape from a local minimum if normal basin hopping 

129 # steps are not sufficient. 

130 accept = True 

131 for test in self.accept_tests: 

132 if inspect.signature(test) == _new_accept_test_signature: 

133 testres = test(res_new=minres, res_old=self.incumbent_minres) 

134 else: 

135 testres = test(f_new=energy_after_quench, x_new=x_after_quench, 

136 f_old=self.energy, x_old=self.x) 

137 

138 if testres == 'force accept': 

139 accept = True 

140 break 

141 elif testres is None: 

142 raise ValueError("accept_tests must return True, False, or " 

143 "'force accept'") 

144 elif not testres: 

145 accept = False 

146 

147 # Report the result of the acceptance test to the take step class. 

148 # This is for adaptive step taking 

149 if hasattr(self.step_taking, "report"): 

150 self.step_taking.report(accept, f_new=energy_after_quench, 

151 x_new=x_after_quench, f_old=self.energy, 

152 x_old=self.x) 

153 

154 return accept, minres 

155 

156 def one_cycle(self): 

157 """Do one cycle of the basinhopping algorithm 

158 """ 

159 self.nstep += 1 

160 new_global_min = False 

161 

162 accept, minres = self._monte_carlo_step() 

163 

164 if accept: 

165 self.energy = minres.fun 

166 self.x = np.copy(minres.x) 

167 self.incumbent_minres = minres # best minimize result found so far 

168 new_global_min = self.storage.update(minres) 

169 

170 # print some information 

171 if self.disp: 

172 self.print_report(minres.fun, accept) 

173 if new_global_min: 

174 print("found new global minimum on step %d with function" 

175 " value %g" % (self.nstep, self.energy)) 

176 

177 # save some variables as BasinHoppingRunner attributes 

178 self.xtrial = minres.x 

179 self.energy_trial = minres.fun 

180 self.accept = accept 

181 

182 return new_global_min 

183 

184 def print_report(self, energy_trial, accept): 

185 """print a status update""" 

186 minres = self.storage.get_lowest() 

187 print("basinhopping step %d: f %g trial_f %g accepted %d " 

188 " lowest_f %g" % (self.nstep, self.energy, energy_trial, 

189 accept, minres.fun)) 

190 

191 

192class AdaptiveStepsize: 

193 """ 

194 Class to implement adaptive stepsize. 

195 

196 This class wraps the step taking class and modifies the stepsize to 

197 ensure the true acceptance rate is as close as possible to the target. 

198 

199 Parameters 

200 ---------- 

201 takestep : callable 

202 The step taking routine. Must contain modifiable attribute 

203 takestep.stepsize 

204 accept_rate : float, optional 

205 The target step acceptance rate 

206 interval : int, optional 

207 Interval for how often to update the stepsize 

208 factor : float, optional 

209 The step size is multiplied or divided by this factor upon each 

210 update. 

211 verbose : bool, optional 

212 Print information about each update 

213 

214 """ 

215 def __init__(self, takestep, accept_rate=0.5, interval=50, factor=0.9, 

216 verbose=True): 

217 self.takestep = takestep 

218 self.target_accept_rate = accept_rate 

219 self.interval = interval 

220 self.factor = factor 

221 self.verbose = verbose 

222 

223 self.nstep = 0 

224 self.nstep_tot = 0 

225 self.naccept = 0 

226 

227 def __call__(self, x): 

228 return self.take_step(x) 

229 

230 def _adjust_step_size(self): 

231 old_stepsize = self.takestep.stepsize 

232 accept_rate = float(self.naccept) / self.nstep 

233 if accept_rate > self.target_accept_rate: 

234 # We're accepting too many steps. This generally means we're 

235 # trapped in a basin. Take bigger steps. 

236 self.takestep.stepsize /= self.factor 

237 else: 

238 # We're not accepting enough steps. Take smaller steps. 

239 self.takestep.stepsize *= self.factor 

240 if self.verbose: 

241 print("adaptive stepsize: acceptance rate {:f} target {:f} new " 

242 "stepsize {:g} old stepsize {:g}".format(accept_rate, 

243 self.target_accept_rate, self.takestep.stepsize, 

244 old_stepsize)) 

245 

246 def take_step(self, x): 

247 self.nstep += 1 

248 self.nstep_tot += 1 

249 if self.nstep % self.interval == 0: 

250 self._adjust_step_size() 

251 return self.takestep(x) 

252 

253 def report(self, accept, **kwargs): 

254 "called by basinhopping to report the result of the step" 

255 if accept: 

256 self.naccept += 1 

257 

258 

259class RandomDisplacement: 

260 """Add a random displacement of maximum size `stepsize` to each coordinate. 

261 

262 Calling this updates `x` in-place. 

263 

264 Parameters 

265 ---------- 

266 stepsize : float, optional 

267 Maximum stepsize in any dimension 

268 random_gen : {None, int, `numpy.random.Generator`, 

269 `numpy.random.RandomState`}, optional 

270 

271 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

272 singleton is used. 

273 If `seed` is an int, a new ``RandomState`` instance is used, 

274 seeded with `seed`. 

275 If `seed` is already a ``Generator`` or ``RandomState`` instance then 

276 that instance is used. 

277 

278 """ 

279 

280 def __init__(self, stepsize=0.5, random_gen=None): 

281 self.stepsize = stepsize 

282 self.random_gen = check_random_state(random_gen) 

283 

284 def __call__(self, x): 

285 x += self.random_gen.uniform(-self.stepsize, self.stepsize, 

286 np.shape(x)) 

287 return x 

288 

289 

290class MinimizerWrapper: 

291 """ 

292 wrap a minimizer function as a minimizer class 

293 """ 

294 def __init__(self, minimizer, func=None, **kwargs): 

295 self.minimizer = minimizer 

296 self.func = func 

297 self.kwargs = kwargs 

298 

299 def __call__(self, x0): 

300 if self.func is None: 

301 return self.minimizer(x0, **self.kwargs) 

302 else: 

303 return self.minimizer(self.func, x0, **self.kwargs) 

304 

305 

306class Metropolis: 

307 """Metropolis acceptance criterion. 

308 

309 Parameters 

310 ---------- 

311 T : float 

312 The "temperature" parameter for the accept or reject criterion. 

313 random_gen : {None, int, `numpy.random.Generator`, 

314 `numpy.random.RandomState`}, optional 

315 

316 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

317 singleton is used. 

318 If `seed` is an int, a new ``RandomState`` instance is used, 

319 seeded with `seed`. 

320 If `seed` is already a ``Generator`` or ``RandomState`` instance then 

321 that instance is used. 

322 Random number generator used for acceptance test. 

323 

324 """ 

325 

326 def __init__(self, T, random_gen=None): 

327 # Avoid ZeroDivisionError since "MBH can be regarded as a special case 

328 # of the BH framework with the Metropolis criterion, where temperature 

329 # T = 0." (Reject all steps that increase energy.) 

330 self.beta = 1.0 / T if T != 0 else float('inf') 

331 self.random_gen = check_random_state(random_gen) 

332 

333 def accept_reject(self, res_new, res_old): 

334 """ 

335 Assuming the local search underlying res_new was successful: 

336 If new energy is lower than old, it will always be accepted. 

337 If new is higher than old, there is a chance it will be accepted, 

338 less likely for larger differences. 

339 """ 

340 with np.errstate(invalid='ignore'): 

341 # The energy values being fed to Metropolis are 1-length arrays, and if 

342 # they are equal, their difference is 0, which gets multiplied by beta, 

343 # which is inf, and array([0]) * float('inf') causes 

344 # 

345 # RuntimeWarning: invalid value encountered in multiply 

346 # 

347 # Ignore this warning so when the algorithm is on a flat plane, it always 

348 # accepts the step, to try to move off the plane. 

349 prod = -(res_new.fun - res_old.fun) * self.beta 

350 w = math.exp(min(0, prod)) 

351 

352 rand = self.random_gen.uniform() 

353 return w >= rand and (res_new.success or not res_old.success) 

354 

355 def __call__(self, *, res_new, res_old): 

356 """ 

357 f_new and f_old are mandatory in kwargs 

358 """ 

359 return bool(self.accept_reject(res_new, res_old)) 

360 

361 

362def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5, 

363 minimizer_kwargs=None, take_step=None, accept_test=None, 

364 callback=None, interval=50, disp=False, niter_success=None, 

365 seed=None, *, target_accept_rate=0.5, stepwise_factor=0.9): 

366 """Find the global minimum of a function using the basin-hopping algorithm. 

367 

368 Basin-hopping is a two-phase method that combines a global stepping 

369 algorithm with local minimization at each step. Designed to mimic 

370 the natural process of energy minimization of clusters of atoms, it works 

371 well for similar problems with "funnel-like, but rugged" energy landscapes 

372 [5]_. 

373 

374 As the step-taking, step acceptance, and minimization methods are all 

375 customizable, this function can also be used to implement other two-phase 

376 methods. 

377 

378 Parameters 

379 ---------- 

380 func : callable ``f(x, *args)`` 

381 Function to be optimized. ``args`` can be passed as an optional item 

382 in the dict `minimizer_kwargs` 

383 x0 : array_like 

384 Initial guess. 

385 niter : integer, optional 

386 The number of basin-hopping iterations. There will be a total of 

387 ``niter + 1`` runs of the local minimizer. 

388 T : float, optional 

389 The "temperature" parameter for the acceptance or rejection criterion. 

390 Higher "temperatures" mean that larger jumps in function value will be 

391 accepted. For best results `T` should be comparable to the 

392 separation (in function value) between local minima. 

393 stepsize : float, optional 

394 Maximum step size for use in the random displacement. 

395 minimizer_kwargs : dict, optional 

396 Extra keyword arguments to be passed to the local minimizer 

397 `scipy.optimize.minimize` Some important options could be: 

398 

399 method : str 

400 The minimization method (e.g. ``"L-BFGS-B"``) 

401 args : tuple 

402 Extra arguments passed to the objective function (`func`) and 

403 its derivatives (Jacobian, Hessian). 

404 

405 take_step : callable ``take_step(x)``, optional 

406 Replace the default step-taking routine with this routine. The default 

407 step-taking routine is a random displacement of the coordinates, but 

408 other step-taking algorithms may be better for some systems. 

409 `take_step` can optionally have the attribute ``take_step.stepsize``. 

410 If this attribute exists, then `basinhopping` will adjust 

411 ``take_step.stepsize`` in order to try to optimize the global minimum 

412 search. 

413 accept_test : callable, ``accept_test(f_new=f_new, x_new=x_new, f_old=fold, x_old=x_old)``, optional 

414 Define a test which will be used to judge whether to accept the 

415 step. This will be used in addition to the Metropolis test based on 

416 "temperature" `T`. The acceptable return values are True, 

417 False, or ``"force accept"``. If any of the tests return False 

418 then the step is rejected. If the latter, then this will override any 

419 other tests in order to accept the step. This can be used, for example, 

420 to forcefully escape from a local minimum that `basinhopping` is 

421 trapped in. 

422 callback : callable, ``callback(x, f, accept)``, optional 

423 A callback function which will be called for all minima found. ``x`` 

424 and ``f`` are the coordinates and function value of the trial minimum, 

425 and ``accept`` is whether that minimum was accepted. This can 

426 be used, for example, to save the lowest N minima found. Also, 

427 `callback` can be used to specify a user defined stop criterion by 

428 optionally returning True to stop the `basinhopping` routine. 

429 interval : integer, optional 

430 interval for how often to update the `stepsize` 

431 disp : bool, optional 

432 Set to True to print status messages 

433 niter_success : integer, optional 

434 Stop the run if the global minimum candidate remains the same for this 

435 number of iterations. 

436 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional 

437 

438 If `seed` is None (or `np.random`), the `numpy.random.RandomState` 

439 singleton is used. 

440 If `seed` is an int, a new ``RandomState`` instance is used, 

441 seeded with `seed`. 

442 If `seed` is already a ``Generator`` or ``RandomState`` instance then 

443 that instance is used. 

444 Specify `seed` for repeatable minimizations. The random numbers 

445 generated with this seed only affect the default Metropolis 

446 `accept_test` and the default `take_step`. If you supply your own 

447 `take_step` and `accept_test`, and these functions use random 

448 number generation, then those functions are responsible for the state 

449 of their random number generator. 

450 target_accept_rate : float, optional 

451 The target acceptance rate that is used to adjust the `stepsize`. 

452 If the current acceptance rate is greater than the target, 

453 then the `stepsize` is increased. Otherwise, it is decreased. 

454 Range is (0, 1). Default is 0.5. 

455 

456 .. versionadded:: 1.8.0 

457 

458 stepwise_factor : float, optional 

459 The `stepsize` is multiplied or divided by this stepwise factor upon 

460 each update. Range is (0, 1). Default is 0.9. 

461 

462 .. versionadded:: 1.8.0 

463 

464 Returns 

465 ------- 

466 res : OptimizeResult 

467 The optimization result represented as a `OptimizeResult` object. 

468 Important attributes are: ``x`` the solution array, ``fun`` the value 

469 of the function at the solution, and ``message`` which describes the 

470 cause of the termination. The ``OptimizeResult`` object returned by the 

471 selected minimizer at the lowest minimum is also contained within this 

472 object and can be accessed through the ``lowest_optimization_result`` 

473 attribute. See `OptimizeResult` for a description of other attributes. 

474 

475 See Also 

476 -------- 

477 minimize : 

478 The local minimization function called once for each basinhopping step. 

479 `minimizer_kwargs` is passed to this routine. 

480 

481 Notes 

482 ----- 

483 Basin-hopping is a stochastic algorithm which attempts to find the global 

484 minimum of a smooth scalar function of one or more variables [1]_ [2]_ [3]_ 

485 [4]_. The algorithm in its current form was described by David Wales and 

486 Jonathan Doye [2]_ http://www-wales.ch.cam.ac.uk/. 

487 

488 The algorithm is iterative with each cycle composed of the following 

489 features 

490 

491 1) random perturbation of the coordinates 

492 

493 2) local minimization 

494 

495 3) accept or reject the new coordinates based on the minimized function 

496 value 

497 

498 The acceptance test used here is the Metropolis criterion of standard Monte 

499 Carlo algorithms, although there are many other possibilities [3]_. 

500 

501 This global minimization method has been shown to be extremely efficient 

502 for a wide variety of problems in physics and chemistry. It is 

503 particularly useful when the function has many minima separated by large 

504 barriers. See the `Cambridge Cluster Database 

505 <https://www-wales.ch.cam.ac.uk/CCD.html>`_ for databases of molecular 

506 systems that have been optimized primarily using basin-hopping. This 

507 database includes minimization problems exceeding 300 degrees of freedom. 

508 

509 See the free software program `GMIN <https://www-wales.ch.cam.ac.uk/GMIN>`_ 

510 for a Fortran implementation of basin-hopping. This implementation has many 

511 variations of the procedure described above, including more 

512 advanced step taking algorithms and alternate acceptance criterion. 

513 

514 For stochastic global optimization there is no way to determine if the true 

515 global minimum has actually been found. Instead, as a consistency check, 

516 the algorithm can be run from a number of different random starting points 

517 to ensure the lowest minimum found in each example has converged to the 

518 global minimum. For this reason, `basinhopping` will by default simply 

519 run for the number of iterations `niter` and return the lowest minimum 

520 found. It is left to the user to ensure that this is in fact the global 

521 minimum. 

522 

523 Choosing `stepsize`: This is a crucial parameter in `basinhopping` and 

524 depends on the problem being solved. The step is chosen uniformly in the 

525 region from x0-stepsize to x0+stepsize, in each dimension. Ideally, it 

526 should be comparable to the typical separation (in argument values) between 

527 local minima of the function being optimized. `basinhopping` will, by 

528 default, adjust `stepsize` to find an optimal value, but this may take 

529 many iterations. You will get quicker results if you set a sensible 

530 initial value for ``stepsize``. 

531 

532 Choosing `T`: The parameter `T` is the "temperature" used in the 

533 Metropolis criterion. Basinhopping steps are always accepted if 

534 ``func(xnew) < func(xold)``. Otherwise, they are accepted with 

535 probability:: 

536 

537 exp( -(func(xnew) - func(xold)) / T ) 

538 

539 So, for best results, `T` should to be comparable to the typical 

540 difference (in function values) between local minima. (The height of 

541 "walls" between local minima is irrelevant.) 

542 

543 If `T` is 0, the algorithm becomes Monotonic Basin-Hopping, in which all 

544 steps that increase energy are rejected. 

545 

546 .. versionadded:: 0.12.0 

547 

548 References 

549 ---------- 

550 .. [1] Wales, David J. 2003, Energy Landscapes, Cambridge University Press, 

551 Cambridge, UK. 

552 .. [2] Wales, D J, and Doye J P K, Global Optimization by Basin-Hopping and 

553 the Lowest Energy Structures of Lennard-Jones Clusters Containing up to 

554 110 Atoms. Journal of Physical Chemistry A, 1997, 101, 5111. 

555 .. [3] Li, Z. and Scheraga, H. A., Monte Carlo-minimization approach to the 

556 multiple-minima problem in protein folding, Proc. Natl. Acad. Sci. USA, 

557 1987, 84, 6611. 

558 .. [4] Wales, D. J. and Scheraga, H. A., Global optimization of clusters, 

559 crystals, and biomolecules, Science, 1999, 285, 1368. 

560 .. [5] Olson, B., Hashmi, I., Molloy, K., and Shehu1, A., Basin Hopping as 

561 a General and Versatile Optimization Framework for the Characterization 

562 of Biological Macromolecules, Advances in Artificial Intelligence, 

563 Volume 2012 (2012), Article ID 674832, :doi:`10.1155/2012/674832` 

564 

565 Examples 

566 -------- 

567 The following example is a 1-D minimization problem, with many 

568 local minima superimposed on a parabola. 

569 

570 >>> import numpy as np 

571 >>> from scipy.optimize import basinhopping 

572 >>> func = lambda x: np.cos(14.5 * x - 0.3) + (x + 0.2) * x 

573 >>> x0 = [1.] 

574 

575 Basinhopping, internally, uses a local minimization algorithm. We will use 

576 the parameter `minimizer_kwargs` to tell basinhopping which algorithm to 

577 use and how to set up that minimizer. This parameter will be passed to 

578 `scipy.optimize.minimize`. 

579 

580 >>> minimizer_kwargs = {"method": "BFGS"} 

581 >>> ret = basinhopping(func, x0, minimizer_kwargs=minimizer_kwargs, 

582 ... niter=200) 

583 >>> print("global minimum: x = %.4f, f(x) = %.4f" % (ret.x, ret.fun)) 

584 global minimum: x = -0.1951, f(x) = -1.0009 

585 

586 Next consider a 2-D minimization problem. Also, this time, we 

587 will use gradient information to significantly speed up the search. 

588 

589 >>> def func2d(x): 

590 ... f = np.cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 

591 ... 0.2) * x[0] 

592 ... df = np.zeros(2) 

593 ... df[0] = -14.5 * np.sin(14.5 * x[0] - 0.3) + 2. * x[0] + 0.2 

594 ... df[1] = 2. * x[1] + 0.2 

595 ... return f, df 

596 

597 We'll also use a different local minimization algorithm. Also, we must tell 

598 the minimizer that our function returns both energy and gradient (Jacobian). 

599 

600 >>> minimizer_kwargs = {"method":"L-BFGS-B", "jac":True} 

601 >>> x0 = [1.0, 1.0] 

602 >>> ret = basinhopping(func2d, x0, minimizer_kwargs=minimizer_kwargs, 

603 ... niter=200) 

604 >>> print("global minimum: x = [%.4f, %.4f], f(x) = %.4f" % (ret.x[0], 

605 ... ret.x[1], 

606 ... ret.fun)) 

607 global minimum: x = [-0.1951, -0.1000], f(x) = -1.0109 

608 

609 Here is an example using a custom step-taking routine. Imagine you want 

610 the first coordinate to take larger steps than the rest of the coordinates. 

611 This can be implemented like so: 

612 

613 >>> class MyTakeStep: 

614 ... def __init__(self, stepsize=0.5): 

615 ... self.stepsize = stepsize 

616 ... self.rng = np.random.default_rng() 

617 ... def __call__(self, x): 

618 ... s = self.stepsize 

619 ... x[0] += self.rng.uniform(-2.*s, 2.*s) 

620 ... x[1:] += self.rng.uniform(-s, s, x[1:].shape) 

621 ... return x 

622 

623 Since ``MyTakeStep.stepsize`` exists basinhopping will adjust the magnitude 

624 of `stepsize` to optimize the search. We'll use the same 2-D function as 

625 before 

626 

627 >>> mytakestep = MyTakeStep() 

628 >>> ret = basinhopping(func2d, x0, minimizer_kwargs=minimizer_kwargs, 

629 ... niter=200, take_step=mytakestep) 

630 >>> print("global minimum: x = [%.4f, %.4f], f(x) = %.4f" % (ret.x[0], 

631 ... ret.x[1], 

632 ... ret.fun)) 

633 global minimum: x = [-0.1951, -0.1000], f(x) = -1.0109 

634 

635 Now, let's do an example using a custom callback function which prints the 

636 value of every minimum found 

637 

638 >>> def print_fun(x, f, accepted): 

639 ... print("at minimum %.4f accepted %d" % (f, int(accepted))) 

640 

641 We'll run it for only 10 basinhopping steps this time. 

642 

643 >>> rng = np.random.default_rng() 

644 >>> ret = basinhopping(func2d, x0, minimizer_kwargs=minimizer_kwargs, 

645 ... niter=10, callback=print_fun, seed=rng) 

646 at minimum 0.4159 accepted 1 

647 at minimum -0.4317 accepted 1 

648 at minimum -1.0109 accepted 1 

649 at minimum -0.9073 accepted 1 

650 at minimum -0.4317 accepted 0 

651 at minimum -0.1021 accepted 1 

652 at minimum -0.7425 accepted 1 

653 at minimum -0.9073 accepted 1 

654 at minimum -0.4317 accepted 0 

655 at minimum -0.7425 accepted 1 

656 at minimum -0.9073 accepted 1 

657 

658 The minimum at -1.0109 is actually the global minimum, found already on the 

659 8th iteration. 

660 

661 """ 

662 if target_accept_rate <= 0. or target_accept_rate >= 1.: 

663 raise ValueError('target_accept_rate has to be in range (0, 1)') 

664 if stepwise_factor <= 0. or stepwise_factor >= 1.: 

665 raise ValueError('stepwise_factor has to be in range (0, 1)') 

666 

667 x0 = np.array(x0) 

668 

669 # set up the np.random generator 

670 rng = check_random_state(seed) 

671 

672 # set up minimizer 

673 if minimizer_kwargs is None: 

674 minimizer_kwargs = dict() 

675 wrapped_minimizer = MinimizerWrapper(scipy.optimize.minimize, func, 

676 **minimizer_kwargs) 

677 

678 # set up step-taking algorithm 

679 if take_step is not None: 

680 if not callable(take_step): 

681 raise TypeError("take_step must be callable") 

682 # if take_step.stepsize exists then use AdaptiveStepsize to control 

683 # take_step.stepsize 

684 if hasattr(take_step, "stepsize"): 

685 take_step_wrapped = AdaptiveStepsize( 

686 take_step, interval=interval, 

687 accept_rate=target_accept_rate, 

688 factor=stepwise_factor, 

689 verbose=disp) 

690 else: 

691 take_step_wrapped = take_step 

692 else: 

693 # use default 

694 displace = RandomDisplacement(stepsize=stepsize, random_gen=rng) 

695 take_step_wrapped = AdaptiveStepsize(displace, interval=interval, 

696 accept_rate=target_accept_rate, 

697 factor=stepwise_factor, 

698 verbose=disp) 

699 

700 # set up accept tests 

701 accept_tests = [] 

702 if accept_test is not None: 

703 if not callable(accept_test): 

704 raise TypeError("accept_test must be callable") 

705 accept_tests = [accept_test] 

706 

707 # use default 

708 metropolis = Metropolis(T, random_gen=rng) 

709 accept_tests.append(metropolis) 

710 

711 if niter_success is None: 

712 niter_success = niter + 2 

713 

714 bh = BasinHoppingRunner(x0, wrapped_minimizer, take_step_wrapped, 

715 accept_tests, disp=disp) 

716 

717 # The wrapped minimizer is called once during construction of 

718 # BasinHoppingRunner, so run the callback 

719 if callable(callback): 

720 callback(bh.storage.minres.x, bh.storage.minres.fun, True) 

721 

722 # start main iteration loop 

723 count, i = 0, 0 

724 message = ["requested number of basinhopping iterations completed" 

725 " successfully"] 

726 for i in range(niter): 

727 new_global_min = bh.one_cycle() 

728 

729 if callable(callback): 

730 # should we pass a copy of x? 

731 val = callback(bh.xtrial, bh.energy_trial, bh.accept) 

732 if val is not None: 

733 if val: 

734 message = ["callback function requested stop early by" 

735 "returning True"] 

736 break 

737 

738 count += 1 

739 if new_global_min: 

740 count = 0 

741 elif count > niter_success: 

742 message = ["success condition satisfied"] 

743 break 

744 

745 # prepare return object 

746 res = bh.res 

747 res.lowest_optimization_result = bh.storage.get_lowest() 

748 res.x = np.copy(res.lowest_optimization_result.x) 

749 res.fun = res.lowest_optimization_result.fun 

750 res.message = message 

751 res.nit = i + 1 

752 res.success = res.lowest_optimization_result.success 

753 return res