Coverage for /usr/lib/python3/dist-packages/scipy/optimize/_differentialevolution.py: 11%

448 statements  

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

1""" 

2differential_evolution: The differential evolution global optimization algorithm 

3Added by Andrew Nelson 2014 

4""" 

5import warnings 

6 

7import numpy as np 

8from scipy.optimize import OptimizeResult, minimize 

9from scipy.optimize._optimize import _status_message 

10from scipy._lib._util import check_random_state, MapWrapper, _FunctionWrapper 

11 

12from scipy.optimize._constraints import (Bounds, new_bounds_to_old, 

13 NonlinearConstraint, LinearConstraint) 

14from scipy.sparse import issparse 

15 

16__all__ = ['differential_evolution'] 

17 

18 

19_MACHEPS = np.finfo(np.float64).eps 

20 

21 

22def differential_evolution(func, bounds, args=(), strategy='best1bin', 

23 maxiter=1000, popsize=15, tol=0.01, 

24 mutation=(0.5, 1), recombination=0.7, seed=None, 

25 callback=None, disp=False, polish=True, 

26 init='latinhypercube', atol=0, updating='immediate', 

27 workers=1, constraints=(), x0=None, *, 

28 integrality=None, vectorized=False): 

29 """Finds the global minimum of a multivariate function. 

30 

31 The differential evolution method [1]_ is stochastic in nature. It does 

32 not use gradient methods to find the minimum, and can search large areas 

33 of candidate space, but often requires larger numbers of function 

34 evaluations than conventional gradient-based techniques. 

35 

36 The algorithm is due to Storn and Price [2]_. 

37 

38 Parameters 

39 ---------- 

40 func : callable 

41 The objective function to be minimized. Must be in the form 

42 ``f(x, *args)``, where ``x`` is the argument in the form of a 1-D array 

43 and ``args`` is a tuple of any additional fixed parameters needed to 

44 completely specify the function. The number of parameters, N, is equal 

45 to ``len(x)``. 

46 bounds : sequence or `Bounds` 

47 Bounds for variables. There are two ways to specify the bounds: 

48 

49 1. Instance of `Bounds` class. 

50 2. ``(min, max)`` pairs for each element in ``x``, defining the 

51 finite lower and upper bounds for the optimizing argument of 

52 `func`. 

53 

54 The total number of bounds is used to determine the number of 

55 parameters, N. If there are parameters whose bounds are equal the total 

56 number of free parameters is ``N - N_equal``. 

57 

58 args : tuple, optional 

59 Any additional fixed parameters needed to 

60 completely specify the objective function. 

61 strategy : str, optional 

62 The differential evolution strategy to use. Should be one of: 

63 

64 - 'best1bin' 

65 - 'best1exp' 

66 - 'rand1exp' 

67 - 'randtobest1exp' 

68 - 'currenttobest1exp' 

69 - 'best2exp' 

70 - 'rand2exp' 

71 - 'randtobest1bin' 

72 - 'currenttobest1bin' 

73 - 'best2bin' 

74 - 'rand2bin' 

75 - 'rand1bin' 

76 

77 The default is 'best1bin'. 

78 maxiter : int, optional 

79 The maximum number of generations over which the entire population is 

80 evolved. The maximum number of function evaluations (with no polishing) 

81 is: ``(maxiter + 1) * popsize * (N - N_equal)`` 

82 popsize : int, optional 

83 A multiplier for setting the total population size. The population has 

84 ``popsize * (N - N_equal)`` individuals. This keyword is overridden if 

85 an initial population is supplied via the `init` keyword. When using 

86 ``init='sobol'`` the population size is calculated as the next power 

87 of 2 after ``popsize * (N - N_equal)``. 

88 tol : float, optional 

89 Relative tolerance for convergence, the solving stops when 

90 ``np.std(pop) <= atol + tol * np.abs(np.mean(population_energies))``, 

91 where and `atol` and `tol` are the absolute and relative tolerance 

92 respectively. 

93 mutation : float or tuple(float, float), optional 

94 The mutation constant. In the literature this is also known as 

95 differential weight, being denoted by F. 

96 If specified as a float it should be in the range [0, 2]. 

97 If specified as a tuple ``(min, max)`` dithering is employed. Dithering 

98 randomly changes the mutation constant on a generation by generation 

99 basis. The mutation constant for that generation is taken from 

100 ``U[min, max)``. Dithering can help speed convergence significantly. 

101 Increasing the mutation constant increases the search radius, but will 

102 slow down convergence. 

103 recombination : float, optional 

104 The recombination constant, should be in the range [0, 1]. In the 

105 literature this is also known as the crossover probability, being 

106 denoted by CR. Increasing this value allows a larger number of mutants 

107 to progress into the next generation, but at the risk of population 

108 stability. 

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

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

111 singleton is used. 

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

113 seeded with `seed`. 

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

115 that instance is used. 

116 Specify `seed` for repeatable minimizations. 

117 disp : bool, optional 

118 Prints the evaluated `func` at every iteration. 

119 callback : callable, `callback(xk, convergence=val)`, optional 

120 A function to follow the progress of the minimization. ``xk`` is 

121 the best solution found so far. ``val`` represents the fractional 

122 value of the population convergence. When ``val`` is greater than one 

123 the function halts. If callback returns `True`, then the minimization 

124 is halted (any polishing is still carried out). 

125 polish : bool, optional 

126 If True (default), then `scipy.optimize.minimize` with the `L-BFGS-B` 

127 method is used to polish the best population member at the end, which 

128 can improve the minimization slightly. If a constrained problem is 

129 being studied then the `trust-constr` method is used instead. For large 

130 problems with many constraints, polishing can take a long time due to 

131 the Jacobian computations. 

132 init : str or array-like, optional 

133 Specify which type of population initialization is performed. Should be 

134 one of: 

135 

136 - 'latinhypercube' 

137 - 'sobol' 

138 - 'halton' 

139 - 'random' 

140 - array specifying the initial population. The array should have 

141 shape ``(S, N)``, where S is the total population size and N is 

142 the number of parameters. 

143 `init` is clipped to `bounds` before use. 

144 

145 The default is 'latinhypercube'. Latin Hypercube sampling tries to 

146 maximize coverage of the available parameter space. 

147 

148 'sobol' and 'halton' are superior alternatives and maximize even more 

149 the parameter space. 'sobol' will enforce an initial population 

150 size which is calculated as the next power of 2 after 

151 ``popsize * (N - N_equal)``. 'halton' has no requirements but is a bit 

152 less efficient. See `scipy.stats.qmc` for more details. 

153 

154 'random' initializes the population randomly - this has the drawback 

155 that clustering can occur, preventing the whole of parameter space 

156 being covered. Use of an array to specify a population could be used, 

157 for example, to create a tight bunch of initial guesses in an location 

158 where the solution is known to exist, thereby reducing time for 

159 convergence. 

160 atol : float, optional 

161 Absolute tolerance for convergence, the solving stops when 

162 ``np.std(pop) <= atol + tol * np.abs(np.mean(population_energies))``, 

163 where and `atol` and `tol` are the absolute and relative tolerance 

164 respectively. 

165 updating : {'immediate', 'deferred'}, optional 

166 If ``'immediate'``, the best solution vector is continuously updated 

167 within a single generation [4]_. This can lead to faster convergence as 

168 trial vectors can take advantage of continuous improvements in the best 

169 solution. 

170 With ``'deferred'``, the best solution vector is updated once per 

171 generation. Only ``'deferred'`` is compatible with parallelization or 

172 vectorization, and the `workers` and `vectorized` keywords can 

173 over-ride this option. 

174 

175 .. versionadded:: 1.2.0 

176 

177 workers : int or map-like callable, optional 

178 If `workers` is an int the population is subdivided into `workers` 

179 sections and evaluated in parallel 

180 (uses `multiprocessing.Pool <multiprocessing>`). 

181 Supply -1 to use all available CPU cores. 

182 Alternatively supply a map-like callable, such as 

183 `multiprocessing.Pool.map` for evaluating the population in parallel. 

184 This evaluation is carried out as ``workers(func, iterable)``. 

185 This option will override the `updating` keyword to 

186 ``updating='deferred'`` if ``workers != 1``. 

187 This option overrides the `vectorized` keyword if ``workers != 1``. 

188 Requires that `func` be pickleable. 

189 

190 .. versionadded:: 1.2.0 

191 

192 constraints : {NonLinearConstraint, LinearConstraint, Bounds} 

193 Constraints on the solver, over and above those applied by the `bounds` 

194 kwd. Uses the approach by Lampinen [5]_. 

195 

196 .. versionadded:: 1.4.0 

197 

198 x0 : None or array-like, optional 

199 Provides an initial guess to the minimization. Once the population has 

200 been initialized this vector replaces the first (best) member. This 

201 replacement is done even if `init` is given an initial population. 

202 ``x0.shape == (N,)``. 

203 

204 .. versionadded:: 1.7.0 

205 

206 integrality : 1-D array, optional 

207 For each decision variable, a boolean value indicating whether the 

208 decision variable is constrained to integer values. The array is 

209 broadcast to ``(N,)``. 

210 If any decision variables are constrained to be integral, they will not 

211 be changed during polishing. 

212 Only integer values lying between the lower and upper bounds are used. 

213 If there are no integer values lying between the bounds then a 

214 `ValueError` is raised. 

215 

216 .. versionadded:: 1.9.0 

217 

218 vectorized : bool, optional 

219 If ``vectorized is True``, `func` is sent an `x` array with 

220 ``x.shape == (N, S)``, and is expected to return an array of shape 

221 ``(S,)``, where `S` is the number of solution vectors to be calculated. 

222 If constraints are applied, each of the functions used to construct 

223 a `Constraint` object should accept an `x` array with 

224 ``x.shape == (N, S)``, and return an array of shape ``(M, S)``, where 

225 `M` is the number of constraint components. 

226 This option is an alternative to the parallelization offered by 

227 `workers`, and may help in optimization speed by reducing interpreter 

228 overhead from multiple function calls. This keyword is ignored if 

229 ``workers != 1``. 

230 This option will override the `updating` keyword to 

231 ``updating='deferred'``. 

232 See the notes section for further discussion on when to use 

233 ``'vectorized'``, and when to use ``'workers'``. 

234 

235 .. versionadded:: 1.9.0 

236 

237 Returns 

238 ------- 

239 res : OptimizeResult 

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

241 Important attributes are: ``x`` the solution array, ``success`` a 

242 Boolean flag indicating if the optimizer exited successfully and 

243 ``message`` which describes the cause of the termination. See 

244 `OptimizeResult` for a description of other attributes. If `polish` 

245 was employed, and a lower minimum was obtained by the polishing, then 

246 OptimizeResult also contains the ``jac`` attribute. 

247 If the eventual solution does not satisfy the applied constraints 

248 ``success`` will be `False`. 

249 

250 Notes 

251 ----- 

252 Differential evolution is a stochastic population based method that is 

253 useful for global optimization problems. At each pass through the 

254 population the algorithm mutates each candidate solution by mixing with 

255 other candidate solutions to create a trial candidate. There are several 

256 strategies [3]_ for creating trial candidates, which suit some problems 

257 more than others. The 'best1bin' strategy is a good starting point for 

258 many systems. In this strategy two members of the population are randomly 

259 chosen. Their difference is used to mutate the best member (the 'best' in 

260 'best1bin'), :math:`b_0`, so far: 

261 

262 .. math:: 

263 

264 b' = b_0 + mutation * (population[rand0] - population[rand1]) 

265 

266 A trial vector is then constructed. Starting with a randomly chosen ith 

267 parameter the trial is sequentially filled (in modulo) with parameters 

268 from ``b'`` or the original candidate. The choice of whether to use ``b'`` 

269 or the original candidate is made with a binomial distribution (the 'bin' 

270 in 'best1bin') - a random number in [0, 1) is generated. If this number is 

271 less than the `recombination` constant then the parameter is loaded from 

272 ``b'``, otherwise it is loaded from the original candidate. The final 

273 parameter is always loaded from ``b'``. Once the trial candidate is built 

274 its fitness is assessed. If the trial is better than the original candidate 

275 then it takes its place. If it is also better than the best overall 

276 candidate it also replaces that. 

277 To improve your chances of finding a global minimum use higher `popsize` 

278 values, with higher `mutation` and (dithering), but lower `recombination` 

279 values. This has the effect of widening the search radius, but slowing 

280 convergence. 

281 By default the best solution vector is updated continuously within a single 

282 iteration (``updating='immediate'``). This is a modification [4]_ of the 

283 original differential evolution algorithm which can lead to faster 

284 convergence as trial vectors can immediately benefit from improved 

285 solutions. To use the original Storn and Price behaviour, updating the best 

286 solution once per iteration, set ``updating='deferred'``. 

287 The ``'deferred'`` approach is compatible with both parallelization and 

288 vectorization (``'workers'`` and ``'vectorized'`` keywords). These may 

289 improve minimization speed by using computer resources more efficiently. 

290 The ``'workers'`` distribute calculations over multiple processors. By 

291 default the Python `multiprocessing` module is used, but other approaches 

292 are also possible, such as the Message Passing Interface (MPI) used on 

293 clusters [6]_ [7]_. The overhead from these approaches (creating new 

294 Processes, etc) may be significant, meaning that computational speed 

295 doesn't necessarily scale with the number of processors used. 

296 Parallelization is best suited to computationally expensive objective 

297 functions. If the objective function is less expensive, then 

298 ``'vectorized'`` may aid by only calling the objective function once per 

299 iteration, rather than multiple times for all the population members; the 

300 interpreter overhead is reduced. 

301 

302 .. versionadded:: 0.15.0 

303 

304 References 

305 ---------- 

306 .. [1] Differential evolution, Wikipedia, 

307 http://en.wikipedia.org/wiki/Differential_evolution 

308 .. [2] Storn, R and Price, K, Differential Evolution - a Simple and 

309 Efficient Heuristic for Global Optimization over Continuous Spaces, 

310 Journal of Global Optimization, 1997, 11, 341 - 359. 

311 .. [3] http://www1.icsi.berkeley.edu/~storn/code.html 

312 .. [4] Wormington, M., Panaccione, C., Matney, K. M., Bowen, D. K., - 

313 Characterization of structures from X-ray scattering data using 

314 genetic algorithms, Phil. Trans. R. Soc. Lond. A, 1999, 357, 

315 2827-2848 

316 .. [5] Lampinen, J., A constraint handling approach for the differential 

317 evolution algorithm. Proceedings of the 2002 Congress on 

318 Evolutionary Computation. CEC'02 (Cat. No. 02TH8600). Vol. 2. IEEE, 

319 2002. 

320 .. [6] https://mpi4py.readthedocs.io/en/stable/ 

321 .. [7] https://schwimmbad.readthedocs.io/en/latest/ 

322 

323 Examples 

324 -------- 

325 Let us consider the problem of minimizing the Rosenbrock function. This 

326 function is implemented in `rosen` in `scipy.optimize`. 

327 

328 >>> import numpy as np 

329 >>> from scipy.optimize import rosen, differential_evolution 

330 >>> bounds = [(0,2), (0, 2), (0, 2), (0, 2), (0, 2)] 

331 >>> result = differential_evolution(rosen, bounds) 

332 >>> result.x, result.fun 

333 (array([1., 1., 1., 1., 1.]), 1.9216496320061384e-19) 

334 

335 Now repeat, but with parallelization. 

336 

337 >>> result = differential_evolution(rosen, bounds, updating='deferred', 

338 ... workers=2) 

339 >>> result.x, result.fun 

340 (array([1., 1., 1., 1., 1.]), 1.9216496320061384e-19) 

341 

342 Let's do a constrained minimization. 

343 

344 >>> from scipy.optimize import LinearConstraint, Bounds 

345 

346 We add the constraint that the sum of ``x[0]`` and ``x[1]`` must be less 

347 than or equal to 1.9. This is a linear constraint, which may be written 

348 ``A @ x <= 1.9``, where ``A = array([[1, 1]])``. This can be encoded as 

349 a `LinearConstraint` instance: 

350 

351 >>> lc = LinearConstraint([[1, 1]], -np.inf, 1.9) 

352 

353 Specify limits using a `Bounds` object. 

354 

355 >>> bounds = Bounds([0., 0.], [2., 2.]) 

356 >>> result = differential_evolution(rosen, bounds, constraints=lc, 

357 ... seed=1) 

358 >>> result.x, result.fun 

359 (array([0.96632622, 0.93367155]), 0.0011352416852625719) 

360 

361 Next find the minimum of the Ackley function 

362 (https://en.wikipedia.org/wiki/Test_functions_for_optimization). 

363 

364 >>> def ackley(x): 

365 ... arg1 = -0.2 * np.sqrt(0.5 * (x[0] ** 2 + x[1] ** 2)) 

366 ... arg2 = 0.5 * (np.cos(2. * np.pi * x[0]) + np.cos(2. * np.pi * x[1])) 

367 ... return -20. * np.exp(arg1) - np.exp(arg2) + 20. + np.e 

368 >>> bounds = [(-5, 5), (-5, 5)] 

369 >>> result = differential_evolution(ackley, bounds, seed=1) 

370 >>> result.x, result.fun 

371 (array([0., 0.]), 4.440892098500626e-16) 

372 

373 The Ackley function is written in a vectorized manner, so the 

374 ``'vectorized'`` keyword can be employed. Note the reduced number of 

375 function evaluations. 

376 

377 >>> result = differential_evolution( 

378 ... ackley, bounds, vectorized=True, updating='deferred', seed=1 

379 ... ) 

380 >>> result.x, result.fun 

381 (array([0., 0.]), 4.440892098500626e-16) 

382 

383 """ 

384 

385 # using a context manager means that any created Pool objects are 

386 # cleared up. 

387 with DifferentialEvolutionSolver(func, bounds, args=args, 

388 strategy=strategy, 

389 maxiter=maxiter, 

390 popsize=popsize, tol=tol, 

391 mutation=mutation, 

392 recombination=recombination, 

393 seed=seed, polish=polish, 

394 callback=callback, 

395 disp=disp, init=init, atol=atol, 

396 updating=updating, 

397 workers=workers, 

398 constraints=constraints, 

399 x0=x0, 

400 integrality=integrality, 

401 vectorized=vectorized) as solver: 

402 ret = solver.solve() 

403 

404 return ret 

405 

406 

407class DifferentialEvolutionSolver: 

408 

409 """This class implements the differential evolution solver 

410 

411 Parameters 

412 ---------- 

413 func : callable 

414 The objective function to be minimized. Must be in the form 

415 ``f(x, *args)``, where ``x`` is the argument in the form of a 1-D array 

416 and ``args`` is a tuple of any additional fixed parameters needed to 

417 completely specify the function. The number of parameters, N, is equal 

418 to ``len(x)``. 

419 bounds : sequence or `Bounds` 

420 Bounds for variables. There are two ways to specify the bounds: 

421 

422 1. Instance of `Bounds` class. 

423 2. ``(min, max)`` pairs for each element in ``x``, defining the 

424 finite lower and upper bounds for the optimizing argument of 

425 `func`. 

426 

427 The total number of bounds is used to determine the number of 

428 parameters, N. If there are parameters whose bounds are equal the total 

429 number of free parameters is ``N - N_equal``. 

430 args : tuple, optional 

431 Any additional fixed parameters needed to 

432 completely specify the objective function. 

433 strategy : str, optional 

434 The differential evolution strategy to use. Should be one of: 

435 

436 - 'best1bin' 

437 - 'best1exp' 

438 - 'rand1exp' 

439 - 'randtobest1exp' 

440 - 'currenttobest1exp' 

441 - 'best2exp' 

442 - 'rand2exp' 

443 - 'randtobest1bin' 

444 - 'currenttobest1bin' 

445 - 'best2bin' 

446 - 'rand2bin' 

447 - 'rand1bin' 

448 

449 The default is 'best1bin' 

450 

451 maxiter : int, optional 

452 The maximum number of generations over which the entire population is 

453 evolved. The maximum number of function evaluations (with no polishing) 

454 is: ``(maxiter + 1) * popsize * (N - N_equal)`` 

455 popsize : int, optional 

456 A multiplier for setting the total population size. The population has 

457 ``popsize * (N - N_equal)`` individuals. This keyword is overridden if 

458 an initial population is supplied via the `init` keyword. When using 

459 ``init='sobol'`` the population size is calculated as the next power 

460 of 2 after ``popsize * (N - N_equal)``. 

461 tol : float, optional 

462 Relative tolerance for convergence, the solving stops when 

463 ``np.std(pop) <= atol + tol * np.abs(np.mean(population_energies))``, 

464 where and `atol` and `tol` are the absolute and relative tolerance 

465 respectively. 

466 mutation : float or tuple(float, float), optional 

467 The mutation constant. In the literature this is also known as 

468 differential weight, being denoted by F. 

469 If specified as a float it should be in the range [0, 2]. 

470 If specified as a tuple ``(min, max)`` dithering is employed. Dithering 

471 randomly changes the mutation constant on a generation by generation 

472 basis. The mutation constant for that generation is taken from 

473 U[min, max). Dithering can help speed convergence significantly. 

474 Increasing the mutation constant increases the search radius, but will 

475 slow down convergence. 

476 recombination : float, optional 

477 The recombination constant, should be in the range [0, 1]. In the 

478 literature this is also known as the crossover probability, being 

479 denoted by CR. Increasing this value allows a larger number of mutants 

480 to progress into the next generation, but at the risk of population 

481 stability. 

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

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

484 singleton is used. 

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

486 seeded with `seed`. 

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

488 that instance is used. 

489 Specify `seed` for repeatable minimizations. 

490 disp : bool, optional 

491 Prints the evaluated `func` at every iteration. 

492 callback : callable, `callback(xk, convergence=val)`, optional 

493 A function to follow the progress of the minimization. ``xk`` is 

494 the current value of ``x0``. ``val`` represents the fractional 

495 value of the population convergence. When ``val`` is greater than one 

496 the function halts. If callback returns `True`, then the minimization 

497 is halted (any polishing is still carried out). 

498 polish : bool, optional 

499 If True (default), then `scipy.optimize.minimize` with the `L-BFGS-B` 

500 method is used to polish the best population member at the end, which 

501 can improve the minimization slightly. If a constrained problem is 

502 being studied then the `trust-constr` method is used instead. For large 

503 problems with many constraints, polishing can take a long time due to 

504 the Jacobian computations. 

505 maxfun : int, optional 

506 Set the maximum number of function evaluations. However, it probably 

507 makes more sense to set `maxiter` instead. 

508 init : str or array-like, optional 

509 Specify which type of population initialization is performed. Should be 

510 one of: 

511 

512 - 'latinhypercube' 

513 - 'sobol' 

514 - 'halton' 

515 - 'random' 

516 - array specifying the initial population. The array should have 

517 shape ``(S, N)``, where S is the total population size and 

518 N is the number of parameters. 

519 `init` is clipped to `bounds` before use. 

520 

521 The default is 'latinhypercube'. Latin Hypercube sampling tries to 

522 maximize coverage of the available parameter space. 

523 

524 'sobol' and 'halton' are superior alternatives and maximize even more 

525 the parameter space. 'sobol' will enforce an initial population 

526 size which is calculated as the next power of 2 after 

527 ``popsize * (N - N_equal)``. 'halton' has no requirements but is a bit 

528 less efficient. See `scipy.stats.qmc` for more details. 

529 

530 'random' initializes the population randomly - this has the drawback 

531 that clustering can occur, preventing the whole of parameter space 

532 being covered. Use of an array to specify a population could be used, 

533 for example, to create a tight bunch of initial guesses in an location 

534 where the solution is known to exist, thereby reducing time for 

535 convergence. 

536 atol : float, optional 

537 Absolute tolerance for convergence, the solving stops when 

538 ``np.std(pop) <= atol + tol * np.abs(np.mean(population_energies))``, 

539 where and `atol` and `tol` are the absolute and relative tolerance 

540 respectively. 

541 updating : {'immediate', 'deferred'}, optional 

542 If ``'immediate'``, the best solution vector is continuously updated 

543 within a single generation [4]_. This can lead to faster convergence as 

544 trial vectors can take advantage of continuous improvements in the best 

545 solution. 

546 With ``'deferred'``, the best solution vector is updated once per 

547 generation. Only ``'deferred'`` is compatible with parallelization or 

548 vectorization, and the `workers` and `vectorized` keywords can 

549 over-ride this option. 

550 workers : int or map-like callable, optional 

551 If `workers` is an int the population is subdivided into `workers` 

552 sections and evaluated in parallel 

553 (uses `multiprocessing.Pool <multiprocessing>`). 

554 Supply `-1` to use all cores available to the Process. 

555 Alternatively supply a map-like callable, such as 

556 `multiprocessing.Pool.map` for evaluating the population in parallel. 

557 This evaluation is carried out as ``workers(func, iterable)``. 

558 This option will override the `updating` keyword to 

559 `updating='deferred'` if `workers != 1`. 

560 Requires that `func` be pickleable. 

561 constraints : {NonLinearConstraint, LinearConstraint, Bounds} 

562 Constraints on the solver, over and above those applied by the `bounds` 

563 kwd. Uses the approach by Lampinen. 

564 x0 : None or array-like, optional 

565 Provides an initial guess to the minimization. Once the population has 

566 been initialized this vector replaces the first (best) member. This 

567 replacement is done even if `init` is given an initial population. 

568 ``x0.shape == (N,)``. 

569 integrality : 1-D array, optional 

570 For each decision variable, a boolean value indicating whether the 

571 decision variable is constrained to integer values. The array is 

572 broadcast to ``(N,)``. 

573 If any decision variables are constrained to be integral, they will not 

574 be changed during polishing. 

575 Only integer values lying between the lower and upper bounds are used. 

576 If there are no integer values lying between the bounds then a 

577 `ValueError` is raised. 

578 vectorized : bool, optional 

579 If ``vectorized is True``, `func` is sent an `x` array with 

580 ``x.shape == (N, S)``, and is expected to return an array of shape 

581 ``(S,)``, where `S` is the number of solution vectors to be calculated. 

582 If constraints are applied, each of the functions used to construct 

583 a `Constraint` object should accept an `x` array with 

584 ``x.shape == (N, S)``, and return an array of shape ``(M, S)``, where 

585 `M` is the number of constraint components. 

586 This option is an alternative to the parallelization offered by 

587 `workers`, and may help in optimization speed. This keyword is 

588 ignored if ``workers != 1``. 

589 This option will override the `updating` keyword to 

590 ``updating='deferred'``. 

591 """ 

592 

593 # Dispatch of mutation strategy method (binomial or exponential). 

594 _binomial = {'best1bin': '_best1', 

595 'randtobest1bin': '_randtobest1', 

596 'currenttobest1bin': '_currenttobest1', 

597 'best2bin': '_best2', 

598 'rand2bin': '_rand2', 

599 'rand1bin': '_rand1'} 

600 _exponential = {'best1exp': '_best1', 

601 'rand1exp': '_rand1', 

602 'randtobest1exp': '_randtobest1', 

603 'currenttobest1exp': '_currenttobest1', 

604 'best2exp': '_best2', 

605 'rand2exp': '_rand2'} 

606 

607 __init_error_msg = ("The population initialization method must be one of " 

608 "'latinhypercube' or 'random', or an array of shape " 

609 "(S, N) where N is the number of parameters and S>5") 

610 

611 def __init__(self, func, bounds, args=(), 

612 strategy='best1bin', maxiter=1000, popsize=15, 

613 tol=0.01, mutation=(0.5, 1), recombination=0.7, seed=None, 

614 maxfun=np.inf, callback=None, disp=False, polish=True, 

615 init='latinhypercube', atol=0, updating='immediate', 

616 workers=1, constraints=(), x0=None, *, integrality=None, 

617 vectorized=False): 

618 

619 if strategy in self._binomial: 

620 self.mutation_func = getattr(self, self._binomial[strategy]) 

621 elif strategy in self._exponential: 

622 self.mutation_func = getattr(self, self._exponential[strategy]) 

623 else: 

624 raise ValueError("Please select a valid mutation strategy") 

625 self.strategy = strategy 

626 

627 self.callback = callback 

628 self.polish = polish 

629 

630 # set the updating / parallelisation options 

631 if updating in ['immediate', 'deferred']: 

632 self._updating = updating 

633 

634 self.vectorized = vectorized 

635 

636 # want to use parallelisation, but updating is immediate 

637 if workers != 1 and updating == 'immediate': 

638 warnings.warn("differential_evolution: the 'workers' keyword has" 

639 " overridden updating='immediate' to" 

640 " updating='deferred'", UserWarning, stacklevel=2) 

641 self._updating = 'deferred' 

642 

643 if vectorized and workers != 1: 

644 warnings.warn("differential_evolution: the 'workers' keyword" 

645 " overrides the 'vectorized' keyword", stacklevel=2) 

646 self.vectorized = vectorized = False 

647 

648 if vectorized and updating == 'immediate': 

649 warnings.warn("differential_evolution: the 'vectorized' keyword" 

650 " has overridden updating='immediate' to updating" 

651 "='deferred'", UserWarning, stacklevel=2) 

652 self._updating = 'deferred' 

653 

654 # an object with a map method. 

655 if vectorized: 

656 def maplike_for_vectorized_func(func, x): 

657 # send an array (N, S) to the user func, 

658 # expect to receive (S,). Transposition is required because 

659 # internally the population is held as (S, N) 

660 return np.atleast_1d(func(x.T)) 

661 workers = maplike_for_vectorized_func 

662 

663 self._mapwrapper = MapWrapper(workers) 

664 

665 # relative and absolute tolerances for convergence 

666 self.tol, self.atol = tol, atol 

667 

668 # Mutation constant should be in [0, 2). If specified as a sequence 

669 # then dithering is performed. 

670 self.scale = mutation 

671 if (not np.all(np.isfinite(mutation)) or 

672 np.any(np.array(mutation) >= 2) or 

673 np.any(np.array(mutation) < 0)): 

674 raise ValueError('The mutation constant must be a float in ' 

675 'U[0, 2), or specified as a tuple(min, max)' 

676 ' where min < max and min, max are in U[0, 2).') 

677 

678 self.dither = None 

679 if hasattr(mutation, '__iter__') and len(mutation) > 1: 

680 self.dither = [mutation[0], mutation[1]] 

681 self.dither.sort() 

682 

683 self.cross_over_probability = recombination 

684 

685 # we create a wrapped function to allow the use of map (and Pool.map 

686 # in the future) 

687 self.func = _FunctionWrapper(func, args) 

688 self.args = args 

689 

690 # convert tuple of lower and upper bounds to limits 

691 # [(low_0, high_0), ..., (low_n, high_n] 

692 # -> [[low_0, ..., low_n], [high_0, ..., high_n]] 

693 if isinstance(bounds, Bounds): 

694 self.limits = np.array(new_bounds_to_old(bounds.lb, 

695 bounds.ub, 

696 len(bounds.lb)), 

697 dtype=float).T 

698 else: 

699 self.limits = np.array(bounds, dtype='float').T 

700 

701 if (np.size(self.limits, 0) != 2 or not 

702 np.all(np.isfinite(self.limits))): 

703 raise ValueError('bounds should be a sequence containing ' 

704 'real valued (min, max) pairs for each value' 

705 ' in x') 

706 

707 if maxiter is None: # the default used to be None 

708 maxiter = 1000 

709 self.maxiter = maxiter 

710 if maxfun is None: # the default used to be None 

711 maxfun = np.inf 

712 self.maxfun = maxfun 

713 

714 # population is scaled to between [0, 1]. 

715 # We have to scale between parameter <-> population 

716 # save these arguments for _scale_parameter and 

717 # _unscale_parameter. This is an optimization 

718 self.__scale_arg1 = 0.5 * (self.limits[0] + self.limits[1]) 

719 self.__scale_arg2 = np.fabs(self.limits[0] - self.limits[1]) 

720 with np.errstate(divide='ignore'): 

721 # if lb == ub then the following line will be 1/0, which is why 

722 # we ignore the divide by zero warning. The result from 1/0 is 

723 # inf, so replace those values by 0. 

724 self.__recip_scale_arg2 = 1 / self.__scale_arg2 

725 self.__recip_scale_arg2[~np.isfinite(self.__recip_scale_arg2)] = 0 

726 

727 self.parameter_count = np.size(self.limits, 1) 

728 

729 self.random_number_generator = check_random_state(seed) 

730 

731 # Which parameters are going to be integers? 

732 if np.any(integrality): 

733 # # user has provided a truth value for integer constraints 

734 integrality = np.broadcast_to( 

735 integrality, 

736 self.parameter_count 

737 ) 

738 integrality = np.asarray(integrality, bool) 

739 # For integrality parameters change the limits to only allow 

740 # integer values lying between the limits. 

741 lb, ub = np.copy(self.limits) 

742 

743 lb = np.ceil(lb) 

744 ub = np.floor(ub) 

745 if not (lb[integrality] <= ub[integrality]).all(): 

746 # there's a parameter that doesn't have an integer value 

747 # lying between the limits 

748 raise ValueError("One of the integrality constraints does not" 

749 " have any possible integer values between" 

750 " the lower/upper bounds.") 

751 nlb = np.nextafter(lb[integrality] - 0.5, np.inf) 

752 nub = np.nextafter(ub[integrality] + 0.5, -np.inf) 

753 

754 self.integrality = integrality 

755 self.limits[0, self.integrality] = nlb 

756 self.limits[1, self.integrality] = nub 

757 else: 

758 self.integrality = False 

759 

760 # check for equal bounds 

761 eb = self.limits[0] == self.limits[1] 

762 eb_count = np.count_nonzero(eb) 

763 

764 # default population initialization is a latin hypercube design, but 

765 # there are other population initializations possible. 

766 # the minimum is 5 because 'best2bin' requires a population that's at 

767 # least 5 long 

768 # 202301 - reduced population size to account for parameters with 

769 # equal bounds. If there are no varying parameters set N to at least 1 

770 self.num_population_members = max( 

771 5, 

772 popsize * max(1, self.parameter_count - eb_count) 

773 ) 

774 self.population_shape = (self.num_population_members, 

775 self.parameter_count) 

776 

777 self._nfev = 0 

778 # check first str otherwise will fail to compare str with array 

779 if isinstance(init, str): 

780 if init == 'latinhypercube': 

781 self.init_population_lhs() 

782 elif init == 'sobol': 

783 # must be Ns = 2**m for Sobol' 

784 n_s = int(2 ** np.ceil(np.log2(self.num_population_members))) 

785 self.num_population_members = n_s 

786 self.population_shape = (self.num_population_members, 

787 self.parameter_count) 

788 self.init_population_qmc(qmc_engine='sobol') 

789 elif init == 'halton': 

790 self.init_population_qmc(qmc_engine='halton') 

791 elif init == 'random': 

792 self.init_population_random() 

793 else: 

794 raise ValueError(self.__init_error_msg) 

795 else: 

796 self.init_population_array(init) 

797 

798 if x0 is not None: 

799 # scale to within unit interval and 

800 # ensure parameters are within bounds. 

801 x0_scaled = self._unscale_parameters(np.asarray(x0)) 

802 if ((x0_scaled > 1.0) | (x0_scaled < 0.0)).any(): 

803 raise ValueError( 

804 "Some entries in x0 lay outside the specified bounds" 

805 ) 

806 self.population[0] = x0_scaled 

807 

808 # infrastructure for constraints 

809 self.constraints = constraints 

810 self._wrapped_constraints = [] 

811 

812 if hasattr(constraints, '__len__'): 

813 # sequence of constraints, this will also deal with default 

814 # keyword parameter 

815 for c in constraints: 

816 self._wrapped_constraints.append( 

817 _ConstraintWrapper(c, self.x) 

818 ) 

819 else: 

820 self._wrapped_constraints = [ 

821 _ConstraintWrapper(constraints, self.x) 

822 ] 

823 self.total_constraints = np.sum( 

824 [c.num_constr for c in self._wrapped_constraints] 

825 ) 

826 self.constraint_violation = np.zeros((self.num_population_members, 1)) 

827 self.feasible = np.ones(self.num_population_members, bool) 

828 

829 self.disp = disp 

830 

831 def init_population_lhs(self): 

832 """ 

833 Initializes the population with Latin Hypercube Sampling. 

834 Latin Hypercube Sampling ensures that each parameter is uniformly 

835 sampled over its range. 

836 """ 

837 rng = self.random_number_generator 

838 

839 # Each parameter range needs to be sampled uniformly. The scaled 

840 # parameter range ([0, 1)) needs to be split into 

841 # `self.num_population_members` segments, each of which has the following 

842 # size: 

843 segsize = 1.0 / self.num_population_members 

844 

845 # Within each segment we sample from a uniform random distribution. 

846 # We need to do this sampling for each parameter. 

847 samples = (segsize * rng.uniform(size=self.population_shape) 

848 

849 # Offset each segment to cover the entire parameter range [0, 1) 

850 + np.linspace(0., 1., self.num_population_members, 

851 endpoint=False)[:, np.newaxis]) 

852 

853 # Create an array for population of candidate solutions. 

854 self.population = np.zeros_like(samples) 

855 

856 # Initialize population of candidate solutions by permutation of the 

857 # random samples. 

858 for j in range(self.parameter_count): 

859 order = rng.permutation(range(self.num_population_members)) 

860 self.population[:, j] = samples[order, j] 

861 

862 # reset population energies 

863 self.population_energies = np.full(self.num_population_members, 

864 np.inf) 

865 

866 # reset number of function evaluations counter 

867 self._nfev = 0 

868 

869 def init_population_qmc(self, qmc_engine): 

870 """Initializes the population with a QMC method. 

871 

872 QMC methods ensures that each parameter is uniformly 

873 sampled over its range. 

874 

875 Parameters 

876 ---------- 

877 qmc_engine : str 

878 The QMC method to use for initialization. Can be one of 

879 ``latinhypercube``, ``sobol`` or ``halton``. 

880 

881 """ 

882 from scipy.stats import qmc 

883 

884 rng = self.random_number_generator 

885 

886 # Create an array for population of candidate solutions. 

887 if qmc_engine == 'latinhypercube': 

888 sampler = qmc.LatinHypercube(d=self.parameter_count, seed=rng) 

889 elif qmc_engine == 'sobol': 

890 sampler = qmc.Sobol(d=self.parameter_count, seed=rng) 

891 elif qmc_engine == 'halton': 

892 sampler = qmc.Halton(d=self.parameter_count, seed=rng) 

893 else: 

894 raise ValueError(self.__init_error_msg) 

895 

896 self.population = sampler.random(n=self.num_population_members) 

897 

898 # reset population energies 

899 self.population_energies = np.full(self.num_population_members, 

900 np.inf) 

901 

902 # reset number of function evaluations counter 

903 self._nfev = 0 

904 

905 def init_population_random(self): 

906 """ 

907 Initializes the population at random. This type of initialization 

908 can possess clustering, Latin Hypercube sampling is generally better. 

909 """ 

910 rng = self.random_number_generator 

911 self.population = rng.uniform(size=self.population_shape) 

912 

913 # reset population energies 

914 self.population_energies = np.full(self.num_population_members, 

915 np.inf) 

916 

917 # reset number of function evaluations counter 

918 self._nfev = 0 

919 

920 def init_population_array(self, init): 

921 """ 

922 Initializes the population with a user specified population. 

923 

924 Parameters 

925 ---------- 

926 init : np.ndarray 

927 Array specifying subset of the initial population. The array should 

928 have shape (S, N), where N is the number of parameters. 

929 The population is clipped to the lower and upper bounds. 

930 """ 

931 # make sure you're using a float array 

932 popn = np.asfarray(init) 

933 

934 if (np.size(popn, 0) < 5 or 

935 popn.shape[1] != self.parameter_count or 

936 len(popn.shape) != 2): 

937 raise ValueError("The population supplied needs to have shape" 

938 " (S, len(x)), where S > 4.") 

939 

940 # scale values and clip to bounds, assigning to population 

941 self.population = np.clip(self._unscale_parameters(popn), 0, 1) 

942 

943 self.num_population_members = np.size(self.population, 0) 

944 

945 self.population_shape = (self.num_population_members, 

946 self.parameter_count) 

947 

948 # reset population energies 

949 self.population_energies = np.full(self.num_population_members, 

950 np.inf) 

951 

952 # reset number of function evaluations counter 

953 self._nfev = 0 

954 

955 @property 

956 def x(self): 

957 """ 

958 The best solution from the solver 

959 """ 

960 return self._scale_parameters(self.population[0]) 

961 

962 @property 

963 def convergence(self): 

964 """ 

965 The standard deviation of the population energies divided by their 

966 mean. 

967 """ 

968 if np.any(np.isinf(self.population_energies)): 

969 return np.inf 

970 return (np.std(self.population_energies) / 

971 (np.abs(np.mean(self.population_energies)) + _MACHEPS)) 

972 

973 def converged(self): 

974 """ 

975 Return True if the solver has converged. 

976 """ 

977 if np.any(np.isinf(self.population_energies)): 

978 return False 

979 

980 return (np.std(self.population_energies) <= 

981 self.atol + 

982 self.tol * np.abs(np.mean(self.population_energies))) 

983 

984 def solve(self): 

985 """ 

986 Runs the DifferentialEvolutionSolver. 

987 

988 Returns 

989 ------- 

990 res : OptimizeResult 

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

992 Important attributes are: ``x`` the solution array, ``success`` a 

993 Boolean flag indicating if the optimizer exited successfully and 

994 ``message`` which describes the cause of the termination. See 

995 `OptimizeResult` for a description of other attributes. If `polish` 

996 was employed, and a lower minimum was obtained by the polishing, 

997 then OptimizeResult also contains the ``jac`` attribute. 

998 """ 

999 nit, warning_flag = 0, False 

1000 status_message = _status_message['success'] 

1001 

1002 # The population may have just been initialized (all entries are 

1003 # np.inf). If it has you have to calculate the initial energies. 

1004 # Although this is also done in the evolve generator it's possible 

1005 # that someone can set maxiter=0, at which point we still want the 

1006 # initial energies to be calculated (the following loop isn't run). 

1007 if np.all(np.isinf(self.population_energies)): 

1008 self.feasible, self.constraint_violation = ( 

1009 self._calculate_population_feasibilities(self.population)) 

1010 

1011 # only work out population energies for feasible solutions 

1012 self.population_energies[self.feasible] = ( 

1013 self._calculate_population_energies( 

1014 self.population[self.feasible])) 

1015 

1016 self._promote_lowest_energy() 

1017 

1018 # do the optimization. 

1019 for nit in range(1, self.maxiter + 1): 

1020 # evolve the population by a generation 

1021 try: 

1022 next(self) 

1023 except StopIteration: 

1024 warning_flag = True 

1025 if self._nfev > self.maxfun: 

1026 status_message = _status_message['maxfev'] 

1027 elif self._nfev == self.maxfun: 

1028 status_message = ('Maximum number of function evaluations' 

1029 ' has been reached.') 

1030 break 

1031 

1032 if self.disp: 

1033 print("differential_evolution step %d: f(x)= %g" 

1034 % (nit, 

1035 self.population_energies[0])) 

1036 

1037 if self.callback: 

1038 c = self.tol / (self.convergence + _MACHEPS) 

1039 warning_flag = bool(self.callback(self.x, convergence=c)) 

1040 if warning_flag: 

1041 status_message = ('callback function requested stop early' 

1042 ' by returning True') 

1043 

1044 # should the solver terminate? 

1045 if warning_flag or self.converged(): 

1046 break 

1047 

1048 else: 

1049 status_message = _status_message['maxiter'] 

1050 warning_flag = True 

1051 

1052 DE_result = OptimizeResult( 

1053 x=self.x, 

1054 fun=self.population_energies[0], 

1055 nfev=self._nfev, 

1056 nit=nit, 

1057 message=status_message, 

1058 success=(warning_flag is not True)) 

1059 

1060 if self.polish and not np.all(self.integrality): 

1061 # can't polish if all the parameters are integers 

1062 if np.any(self.integrality): 

1063 # set the lower/upper bounds equal so that any integrality 

1064 # constraints work. 

1065 limits, integrality = self.limits, self.integrality 

1066 limits[0, integrality] = DE_result.x[integrality] 

1067 limits[1, integrality] = DE_result.x[integrality] 

1068 

1069 polish_method = 'L-BFGS-B' 

1070 

1071 if self._wrapped_constraints: 

1072 polish_method = 'trust-constr' 

1073 

1074 constr_violation = self._constraint_violation_fn(DE_result.x) 

1075 if np.any(constr_violation > 0.): 

1076 warnings.warn("differential evolution didn't find a" 

1077 " solution satisfying the constraints," 

1078 " attempting to polish from the least" 

1079 " infeasible solution", UserWarning) 

1080 if self.disp: 

1081 print(f"Polishing solution with '{polish_method}'") 

1082 result = minimize(self.func, 

1083 np.copy(DE_result.x), 

1084 method=polish_method, 

1085 bounds=self.limits.T, 

1086 constraints=self.constraints) 

1087 

1088 self._nfev += result.nfev 

1089 DE_result.nfev = self._nfev 

1090 

1091 # Polishing solution is only accepted if there is an improvement in 

1092 # cost function, the polishing was successful and the solution lies 

1093 # within the bounds. 

1094 if (result.fun < DE_result.fun and 

1095 result.success and 

1096 np.all(result.x <= self.limits[1]) and 

1097 np.all(self.limits[0] <= result.x)): 

1098 DE_result.fun = result.fun 

1099 DE_result.x = result.x 

1100 DE_result.jac = result.jac 

1101 # to keep internal state consistent 

1102 self.population_energies[0] = result.fun 

1103 self.population[0] = self._unscale_parameters(result.x) 

1104 

1105 if self._wrapped_constraints: 

1106 DE_result.constr = [c.violation(DE_result.x) for 

1107 c in self._wrapped_constraints] 

1108 DE_result.constr_violation = np.max( 

1109 np.concatenate(DE_result.constr)) 

1110 DE_result.maxcv = DE_result.constr_violation 

1111 if DE_result.maxcv > 0: 

1112 # if the result is infeasible then success must be False 

1113 DE_result.success = False 

1114 DE_result.message = ("The solution does not satisfy the " 

1115 f"constraints, MAXCV = {DE_result.maxcv}") 

1116 

1117 return DE_result 

1118 

1119 def _calculate_population_energies(self, population): 

1120 """ 

1121 Calculate the energies of a population. 

1122 

1123 Parameters 

1124 ---------- 

1125 population : ndarray 

1126 An array of parameter vectors normalised to [0, 1] using lower 

1127 and upper limits. Has shape ``(np.size(population, 0), N)``. 

1128 

1129 Returns 

1130 ------- 

1131 energies : ndarray 

1132 An array of energies corresponding to each population member. If 

1133 maxfun will be exceeded during this call, then the number of 

1134 function evaluations will be reduced and energies will be 

1135 right-padded with np.inf. Has shape ``(np.size(population, 0),)`` 

1136 """ 

1137 num_members = np.size(population, 0) 

1138 # S is the number of function evals left to stay under the 

1139 # maxfun budget 

1140 S = min(num_members, self.maxfun - self._nfev) 

1141 

1142 energies = np.full(num_members, np.inf) 

1143 

1144 parameters_pop = self._scale_parameters(population) 

1145 try: 

1146 calc_energies = list( 

1147 self._mapwrapper(self.func, parameters_pop[0:S]) 

1148 ) 

1149 calc_energies = np.squeeze(calc_energies) 

1150 except (TypeError, ValueError) as e: 

1151 # wrong number of arguments for _mapwrapper 

1152 # or wrong length returned from the mapper 

1153 raise RuntimeError( 

1154 "The map-like callable must be of the form f(func, iterable), " 

1155 "returning a sequence of numbers the same length as 'iterable'" 

1156 ) from e 

1157 

1158 if calc_energies.size != S: 

1159 if self.vectorized: 

1160 raise RuntimeError("The vectorized function must return an" 

1161 " array of shape (S,) when given an array" 

1162 " of shape (len(x), S)") 

1163 raise RuntimeError("func(x, *args) must return a scalar value") 

1164 

1165 energies[0:S] = calc_energies 

1166 

1167 if self.vectorized: 

1168 self._nfev += 1 

1169 else: 

1170 self._nfev += S 

1171 

1172 return energies 

1173 

1174 def _promote_lowest_energy(self): 

1175 # swaps 'best solution' into first population entry 

1176 

1177 idx = np.arange(self.num_population_members) 

1178 feasible_solutions = idx[self.feasible] 

1179 if feasible_solutions.size: 

1180 # find the best feasible solution 

1181 idx_t = np.argmin(self.population_energies[feasible_solutions]) 

1182 l = feasible_solutions[idx_t] 

1183 else: 

1184 # no solution was feasible, use 'best' infeasible solution, which 

1185 # will violate constraints the least 

1186 l = np.argmin(np.sum(self.constraint_violation, axis=1)) 

1187 

1188 self.population_energies[[0, l]] = self.population_energies[[l, 0]] 

1189 self.population[[0, l], :] = self.population[[l, 0], :] 

1190 self.feasible[[0, l]] = self.feasible[[l, 0]] 

1191 self.constraint_violation[[0, l], :] = ( 

1192 self.constraint_violation[[l, 0], :]) 

1193 

1194 def _constraint_violation_fn(self, x): 

1195 """ 

1196 Calculates total constraint violation for all the constraints, for a 

1197 set of solutions. 

1198 

1199 Parameters 

1200 ---------- 

1201 x : ndarray 

1202 Solution vector(s). Has shape (S, N), or (N,), where S is the 

1203 number of solutions to investigate and N is the number of 

1204 parameters. 

1205 

1206 Returns 

1207 ------- 

1208 cv : ndarray 

1209 Total violation of constraints. Has shape ``(S, M)``, where M is 

1210 the total number of constraint components (which is not necessarily 

1211 equal to len(self._wrapped_constraints)). 

1212 """ 

1213 # how many solution vectors you're calculating constraint violations 

1214 # for 

1215 S = np.size(x) // self.parameter_count 

1216 _out = np.zeros((S, self.total_constraints)) 

1217 offset = 0 

1218 for con in self._wrapped_constraints: 

1219 # the input/output of the (vectorized) constraint function is 

1220 # {(N, S), (N,)} --> (M, S) 

1221 # The input to _constraint_violation_fn is (S, N) or (N,), so 

1222 # transpose to pass it to the constraint. The output is transposed 

1223 # from (M, S) to (S, M) for further use. 

1224 c = con.violation(x.T).T 

1225 

1226 # The shape of c should be (M,), (1, M), or (S, M). Check for 

1227 # those shapes, as an incorrect shape indicates that the 

1228 # user constraint function didn't return the right thing, and 

1229 # the reshape operation will fail. Intercept the wrong shape 

1230 # to give a reasonable error message. I'm not sure what failure 

1231 # modes an inventive user will come up with. 

1232 if c.shape[-1] != con.num_constr or (S > 1 and c.shape[0] != S): 

1233 raise RuntimeError("An array returned from a Constraint has" 

1234 " the wrong shape. If `vectorized is False`" 

1235 " the Constraint should return an array of" 

1236 " shape (M,). If `vectorized is True` then" 

1237 " the Constraint must return an array of" 

1238 " shape (M, S), where S is the number of" 

1239 " solution vectors and M is the number of" 

1240 " constraint components in a given" 

1241 " Constraint object.") 

1242 

1243 # the violation function may return a 1D array, but is it a 

1244 # sequence of constraints for one solution (S=1, M>=1), or the 

1245 # value of a single constraint for a sequence of solutions 

1246 # (S>=1, M=1) 

1247 c = np.reshape(c, (S, con.num_constr)) 

1248 _out[:, offset:offset + con.num_constr] = c 

1249 offset += con.num_constr 

1250 

1251 return _out 

1252 

1253 def _calculate_population_feasibilities(self, population): 

1254 """ 

1255 Calculate the feasibilities of a population. 

1256 

1257 Parameters 

1258 ---------- 

1259 population : ndarray 

1260 An array of parameter vectors normalised to [0, 1] using lower 

1261 and upper limits. Has shape ``(np.size(population, 0), N)``. 

1262 

1263 Returns 

1264 ------- 

1265 feasible, constraint_violation : ndarray, ndarray 

1266 Boolean array of feasibility for each population member, and an 

1267 array of the constraint violation for each population member. 

1268 constraint_violation has shape ``(np.size(population, 0), M)``, 

1269 where M is the number of constraints. 

1270 """ 

1271 num_members = np.size(population, 0) 

1272 if not self._wrapped_constraints: 

1273 # shortcut for no constraints 

1274 return np.ones(num_members, bool), np.zeros((num_members, 1)) 

1275 

1276 # (S, N) 

1277 parameters_pop = self._scale_parameters(population) 

1278 

1279 if self.vectorized: 

1280 # (S, M) 

1281 constraint_violation = np.array( 

1282 self._constraint_violation_fn(parameters_pop) 

1283 ) 

1284 else: 

1285 # (S, 1, M) 

1286 constraint_violation = np.array([self._constraint_violation_fn(x) 

1287 for x in parameters_pop]) 

1288 # if you use the list comprehension in the line above it will 

1289 # create an array of shape (S, 1, M), because each iteration 

1290 # generates an array of (1, M). In comparison the vectorized 

1291 # version returns (S, M). It's therefore necessary to remove axis 1 

1292 constraint_violation = constraint_violation[:, 0] 

1293 

1294 feasible = ~(np.sum(constraint_violation, axis=1) > 0) 

1295 

1296 return feasible, constraint_violation 

1297 

1298 def __iter__(self): 

1299 return self 

1300 

1301 def __enter__(self): 

1302 return self 

1303 

1304 def __exit__(self, *args): 

1305 return self._mapwrapper.__exit__(*args) 

1306 

1307 def _accept_trial(self, energy_trial, feasible_trial, cv_trial, 

1308 energy_orig, feasible_orig, cv_orig): 

1309 """ 

1310 Trial is accepted if: 

1311 * it satisfies all constraints and provides a lower or equal objective 

1312 function value, while both the compared solutions are feasible 

1313 - or - 

1314 * it is feasible while the original solution is infeasible, 

1315 - or - 

1316 * it is infeasible, but provides a lower or equal constraint violation 

1317 for all constraint functions. 

1318 

1319 This test corresponds to section III of Lampinen [1]_. 

1320 

1321 Parameters 

1322 ---------- 

1323 energy_trial : float 

1324 Energy of the trial solution 

1325 feasible_trial : float 

1326 Feasibility of trial solution 

1327 cv_trial : array-like 

1328 Excess constraint violation for the trial solution 

1329 energy_orig : float 

1330 Energy of the original solution 

1331 feasible_orig : float 

1332 Feasibility of original solution 

1333 cv_orig : array-like 

1334 Excess constraint violation for the original solution 

1335 

1336 Returns 

1337 ------- 

1338 accepted : bool 

1339 

1340 """ 

1341 if feasible_orig and feasible_trial: 

1342 return energy_trial <= energy_orig 

1343 elif feasible_trial and not feasible_orig: 

1344 return True 

1345 elif not feasible_trial and (cv_trial <= cv_orig).all(): 

1346 # cv_trial < cv_orig would imply that both trial and orig are not 

1347 # feasible 

1348 return True 

1349 

1350 return False 

1351 

1352 def __next__(self): 

1353 """ 

1354 Evolve the population by a single generation 

1355 

1356 Returns 

1357 ------- 

1358 x : ndarray 

1359 The best solution from the solver. 

1360 fun : float 

1361 Value of objective function obtained from the best solution. 

1362 """ 

1363 # the population may have just been initialized (all entries are 

1364 # np.inf). If it has you have to calculate the initial energies 

1365 if np.all(np.isinf(self.population_energies)): 

1366 self.feasible, self.constraint_violation = ( 

1367 self._calculate_population_feasibilities(self.population)) 

1368 

1369 # only need to work out population energies for those that are 

1370 # feasible 

1371 self.population_energies[self.feasible] = ( 

1372 self._calculate_population_energies( 

1373 self.population[self.feasible])) 

1374 

1375 self._promote_lowest_energy() 

1376 

1377 if self.dither is not None: 

1378 self.scale = self.random_number_generator.uniform(self.dither[0], 

1379 self.dither[1]) 

1380 

1381 if self._updating == 'immediate': 

1382 # update best solution immediately 

1383 for candidate in range(self.num_population_members): 

1384 if self._nfev > self.maxfun: 

1385 raise StopIteration 

1386 

1387 # create a trial solution 

1388 trial = self._mutate(candidate) 

1389 

1390 # ensuring that it's in the range [0, 1) 

1391 self._ensure_constraint(trial) 

1392 

1393 # scale from [0, 1) to the actual parameter value 

1394 parameters = self._scale_parameters(trial) 

1395 

1396 # determine the energy of the objective function 

1397 if self._wrapped_constraints: 

1398 cv = self._constraint_violation_fn(parameters) 

1399 feasible = False 

1400 energy = np.inf 

1401 if not np.sum(cv) > 0: 

1402 # solution is feasible 

1403 feasible = True 

1404 energy = self.func(parameters) 

1405 self._nfev += 1 

1406 else: 

1407 feasible = True 

1408 cv = np.atleast_2d([0.]) 

1409 energy = self.func(parameters) 

1410 self._nfev += 1 

1411 

1412 # compare trial and population member 

1413 if self._accept_trial(energy, feasible, cv, 

1414 self.population_energies[candidate], 

1415 self.feasible[candidate], 

1416 self.constraint_violation[candidate]): 

1417 self.population[candidate] = trial 

1418 self.population_energies[candidate] = np.squeeze(energy) 

1419 self.feasible[candidate] = feasible 

1420 self.constraint_violation[candidate] = cv 

1421 

1422 # if the trial candidate is also better than the best 

1423 # solution then promote it. 

1424 if self._accept_trial(energy, feasible, cv, 

1425 self.population_energies[0], 

1426 self.feasible[0], 

1427 self.constraint_violation[0]): 

1428 self._promote_lowest_energy() 

1429 

1430 elif self._updating == 'deferred': 

1431 # update best solution once per generation 

1432 if self._nfev >= self.maxfun: 

1433 raise StopIteration 

1434 

1435 # 'deferred' approach, vectorised form. 

1436 # create trial solutions 

1437 trial_pop = np.array( 

1438 [self._mutate(i) for i in range(self.num_population_members)]) 

1439 

1440 # enforce bounds 

1441 self._ensure_constraint(trial_pop) 

1442 

1443 # determine the energies of the objective function, but only for 

1444 # feasible trials 

1445 feasible, cv = self._calculate_population_feasibilities(trial_pop) 

1446 trial_energies = np.full(self.num_population_members, np.inf) 

1447 

1448 # only calculate for feasible entries 

1449 trial_energies[feasible] = self._calculate_population_energies( 

1450 trial_pop[feasible]) 

1451 

1452 # which solutions are 'improved'? 

1453 loc = [self._accept_trial(*val) for val in 

1454 zip(trial_energies, feasible, cv, self.population_energies, 

1455 self.feasible, self.constraint_violation)] 

1456 loc = np.array(loc) 

1457 self.population = np.where(loc[:, np.newaxis], 

1458 trial_pop, 

1459 self.population) 

1460 self.population_energies = np.where(loc, 

1461 trial_energies, 

1462 self.population_energies) 

1463 self.feasible = np.where(loc, 

1464 feasible, 

1465 self.feasible) 

1466 self.constraint_violation = np.where(loc[:, np.newaxis], 

1467 cv, 

1468 self.constraint_violation) 

1469 

1470 # make sure the best solution is updated if updating='deferred'. 

1471 # put the lowest energy into the best solution position. 

1472 self._promote_lowest_energy() 

1473 

1474 return self.x, self.population_energies[0] 

1475 

1476 def _scale_parameters(self, trial): 

1477 """Scale from a number between 0 and 1 to parameters.""" 

1478 # trial either has shape (N, ) or (L, N), where L is the number of 

1479 # solutions being scaled 

1480 scaled = self.__scale_arg1 + (trial - 0.5) * self.__scale_arg2 

1481 if np.any(self.integrality): 

1482 i = np.broadcast_to(self.integrality, scaled.shape) 

1483 scaled[i] = np.round(scaled[i]) 

1484 return scaled 

1485 

1486 def _unscale_parameters(self, parameters): 

1487 """Scale from parameters to a number between 0 and 1.""" 

1488 return (parameters - self.__scale_arg1) * self.__recip_scale_arg2 + 0.5 

1489 

1490 def _ensure_constraint(self, trial): 

1491 """Make sure the parameters lie between the limits.""" 

1492 mask = np.where((trial > 1) | (trial < 0)) 

1493 trial[mask] = self.random_number_generator.uniform(size=mask[0].shape) 

1494 

1495 def _mutate(self, candidate): 

1496 """Create a trial vector based on a mutation strategy.""" 

1497 trial = np.copy(self.population[candidate]) 

1498 

1499 rng = self.random_number_generator 

1500 

1501 fill_point = rng.choice(self.parameter_count) 

1502 

1503 if self.strategy in ['currenttobest1exp', 'currenttobest1bin']: 

1504 bprime = self.mutation_func(candidate, 

1505 self._select_samples(candidate, 5)) 

1506 else: 

1507 bprime = self.mutation_func(self._select_samples(candidate, 5)) 

1508 

1509 if self.strategy in self._binomial: 

1510 crossovers = rng.uniform(size=self.parameter_count) 

1511 crossovers = crossovers < self.cross_over_probability 

1512 # the last one is always from the bprime vector for binomial 

1513 # If you fill in modulo with a loop you have to set the last one to 

1514 # true. If you don't use a loop then you can have any random entry 

1515 # be True. 

1516 crossovers[fill_point] = True 

1517 trial = np.where(crossovers, bprime, trial) 

1518 return trial 

1519 

1520 elif self.strategy in self._exponential: 

1521 i = 0 

1522 crossovers = rng.uniform(size=self.parameter_count) 

1523 crossovers = crossovers < self.cross_over_probability 

1524 crossovers[0] = True 

1525 while (i < self.parameter_count and crossovers[i]): 

1526 trial[fill_point] = bprime[fill_point] 

1527 fill_point = (fill_point + 1) % self.parameter_count 

1528 i += 1 

1529 

1530 return trial 

1531 

1532 def _best1(self, samples): 

1533 """best1bin, best1exp""" 

1534 r0, r1 = samples[:2] 

1535 return (self.population[0] + self.scale * 

1536 (self.population[r0] - self.population[r1])) 

1537 

1538 def _rand1(self, samples): 

1539 """rand1bin, rand1exp""" 

1540 r0, r1, r2 = samples[:3] 

1541 return (self.population[r0] + self.scale * 

1542 (self.population[r1] - self.population[r2])) 

1543 

1544 def _randtobest1(self, samples): 

1545 """randtobest1bin, randtobest1exp""" 

1546 r0, r1, r2 = samples[:3] 

1547 bprime = np.copy(self.population[r0]) 

1548 bprime += self.scale * (self.population[0] - bprime) 

1549 bprime += self.scale * (self.population[r1] - 

1550 self.population[r2]) 

1551 return bprime 

1552 

1553 def _currenttobest1(self, candidate, samples): 

1554 """currenttobest1bin, currenttobest1exp""" 

1555 r0, r1 = samples[:2] 

1556 bprime = (self.population[candidate] + self.scale * 

1557 (self.population[0] - self.population[candidate] + 

1558 self.population[r0] - self.population[r1])) 

1559 return bprime 

1560 

1561 def _best2(self, samples): 

1562 """best2bin, best2exp""" 

1563 r0, r1, r2, r3 = samples[:4] 

1564 bprime = (self.population[0] + self.scale * 

1565 (self.population[r0] + self.population[r1] - 

1566 self.population[r2] - self.population[r3])) 

1567 

1568 return bprime 

1569 

1570 def _rand2(self, samples): 

1571 """rand2bin, rand2exp""" 

1572 r0, r1, r2, r3, r4 = samples 

1573 bprime = (self.population[r0] + self.scale * 

1574 (self.population[r1] + self.population[r2] - 

1575 self.population[r3] - self.population[r4])) 

1576 

1577 return bprime 

1578 

1579 def _select_samples(self, candidate, number_samples): 

1580 """ 

1581 obtain random integers from range(self.num_population_members), 

1582 without replacement. You can't have the original candidate either. 

1583 """ 

1584 idxs = list(range(self.num_population_members)) 

1585 idxs.remove(candidate) 

1586 self.random_number_generator.shuffle(idxs) 

1587 idxs = idxs[:number_samples] 

1588 return idxs 

1589 

1590 

1591class _ConstraintWrapper: 

1592 """Object to wrap/evaluate user defined constraints. 

1593 

1594 Very similar in practice to `PreparedConstraint`, except that no evaluation 

1595 of jac/hess is performed (explicit or implicit). 

1596 

1597 If created successfully, it will contain the attributes listed below. 

1598 

1599 Parameters 

1600 ---------- 

1601 constraint : {`NonlinearConstraint`, `LinearConstraint`, `Bounds`} 

1602 Constraint to check and prepare. 

1603 x0 : array_like 

1604 Initial vector of independent variables, shape (N,) 

1605 

1606 Attributes 

1607 ---------- 

1608 fun : callable 

1609 Function defining the constraint wrapped by one of the convenience 

1610 classes. 

1611 bounds : 2-tuple 

1612 Contains lower and upper bounds for the constraints --- lb and ub. 

1613 These are converted to ndarray and have a size equal to the number of 

1614 the constraints. 

1615 """ 

1616 def __init__(self, constraint, x0): 

1617 self.constraint = constraint 

1618 

1619 if isinstance(constraint, NonlinearConstraint): 

1620 def fun(x): 

1621 x = np.asarray(x) 

1622 return np.atleast_1d(constraint.fun(x)) 

1623 elif isinstance(constraint, LinearConstraint): 

1624 def fun(x): 

1625 if issparse(constraint.A): 

1626 A = constraint.A 

1627 else: 

1628 A = np.atleast_2d(constraint.A) 

1629 return A.dot(x) 

1630 elif isinstance(constraint, Bounds): 

1631 def fun(x): 

1632 return np.asarray(x) 

1633 else: 

1634 raise ValueError("`constraint` of an unknown type is passed.") 

1635 

1636 self.fun = fun 

1637 

1638 lb = np.asarray(constraint.lb, dtype=float) 

1639 ub = np.asarray(constraint.ub, dtype=float) 

1640 

1641 x0 = np.asarray(x0) 

1642 

1643 # find out the number of constraints 

1644 f0 = fun(x0) 

1645 self.num_constr = m = f0.size 

1646 self.parameter_count = x0.size 

1647 

1648 if lb.ndim == 0: 

1649 lb = np.resize(lb, m) 

1650 if ub.ndim == 0: 

1651 ub = np.resize(ub, m) 

1652 

1653 self.bounds = (lb, ub) 

1654 

1655 def __call__(self, x): 

1656 return np.atleast_1d(self.fun(x)) 

1657 

1658 def violation(self, x): 

1659 """How much the constraint is exceeded by. 

1660 

1661 Parameters 

1662 ---------- 

1663 x : array-like 

1664 Vector of independent variables, (N, S), where N is number of 

1665 parameters and S is the number of solutions to be investigated. 

1666 

1667 Returns 

1668 ------- 

1669 excess : array-like 

1670 How much the constraint is exceeded by, for each of the 

1671 constraints specified by `_ConstraintWrapper.fun`. 

1672 Has shape (M, S) where M is the number of constraint components. 

1673 """ 

1674 # expect ev to have shape (num_constr, S) or (num_constr,) 

1675 ev = self.fun(np.asarray(x)) 

1676 

1677 try: 

1678 excess_lb = np.maximum(self.bounds[0] - ev.T, 0) 

1679 excess_ub = np.maximum(ev.T - self.bounds[1], 0) 

1680 except ValueError as e: 

1681 raise RuntimeError("An array returned from a Constraint has" 

1682 " the wrong shape. If `vectorized is False`" 

1683 " the Constraint should return an array of" 

1684 " shape (M,). If `vectorized is True` then" 

1685 " the Constraint must return an array of" 

1686 " shape (M, S), where S is the number of" 

1687 " solution vectors and M is the number of" 

1688 " constraint components in a given" 

1689 " Constraint object.") from e 

1690 

1691 v = (excess_lb + excess_ub).T 

1692 return v