_shgo.py 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604
  1. """
  2. shgo: The simplicial homology global optimisation algorithm
  3. """
  4. import numpy as np
  5. import time
  6. import logging
  7. import warnings
  8. from scipy import spatial
  9. from scipy.optimize import OptimizeResult, minimize, Bounds
  10. from scipy.optimize._constraints import new_bounds_to_old
  11. from ._optimize import _wrap_scalar_function
  12. from scipy.optimize._shgo_lib.triangulation import Complex
  13. __all__ = ['shgo']
  14. def shgo(func, bounds, args=(), constraints=None, n=None, iters=1,
  15. callback=None,
  16. minimizer_kwargs=None, options=None, sampling_method='simplicial'):
  17. """
  18. Finds the global minimum of a function using SHG optimization.
  19. SHGO stands for "simplicial homology global optimization".
  20. Parameters
  21. ----------
  22. func : callable
  23. The objective function to be minimized. Must be in the form
  24. ``f(x, *args)``, where ``x`` is the argument in the form of a 1-D array
  25. and ``args`` is a tuple of any additional fixed parameters needed to
  26. completely specify the function.
  27. bounds : sequence or `Bounds`
  28. Bounds for variables. There are two ways to specify the bounds:
  29. 1. Instance of `Bounds` class.
  30. 2. Sequence of ``(min, max)`` pairs for each element in `x`.
  31. args : tuple, optional
  32. Any additional fixed parameters needed to completely specify the
  33. objective function.
  34. constraints : dict or sequence of dict, optional
  35. Constraints definition.
  36. Function(s) ``R**n`` in the form::
  37. g(x) >= 0 applied as g : R^n -> R^m
  38. h(x) == 0 applied as h : R^n -> R^p
  39. Each constraint is defined in a dictionary with fields:
  40. type : str
  41. Constraint type: 'eq' for equality, 'ineq' for inequality.
  42. fun : callable
  43. The function defining the constraint.
  44. jac : callable, optional
  45. The Jacobian of `fun` (only for SLSQP).
  46. args : sequence, optional
  47. Extra arguments to be passed to the function and Jacobian.
  48. Equality constraint means that the constraint function result is to
  49. be zero whereas inequality means that it is to be non-negative.
  50. Note that COBYLA only supports inequality constraints.
  51. .. note::
  52. Only the COBYLA and SLSQP local minimize methods currently
  53. support constraint arguments. If the ``constraints`` sequence
  54. used in the local optimization problem is not defined in
  55. ``minimizer_kwargs`` and a constrained method is used then the
  56. global ``constraints`` will be used.
  57. (Defining a ``constraints`` sequence in ``minimizer_kwargs``
  58. means that ``constraints`` will not be added so if equality
  59. constraints and so forth need to be added then the inequality
  60. functions in ``constraints`` need to be added to
  61. ``minimizer_kwargs`` too).
  62. n : int, optional
  63. Number of sampling points used in the construction of the simplicial
  64. complex. Note that this argument is only used for ``sobol`` and other
  65. arbitrary `sampling_methods`. In case of ``sobol``, it must be a
  66. power of 2: ``n=2**m``, and the argument will automatically be
  67. converted to the next higher power of 2. Default is 100 for
  68. ``sampling_method='simplicial'`` and 128 for
  69. ``sampling_method='sobol'``.
  70. iters : int, optional
  71. Number of iterations used in the construction of the simplicial
  72. complex. Default is 1.
  73. callback : callable, optional
  74. Called after each iteration, as ``callback(xk)``, where ``xk`` is the
  75. current parameter vector.
  76. minimizer_kwargs : dict, optional
  77. Extra keyword arguments to be passed to the minimizer
  78. ``scipy.optimize.minimize`` Some important options could be:
  79. * method : str
  80. The minimization method, the default is ``SLSQP``.
  81. * args : tuple
  82. Extra arguments passed to the objective function (``func``) and
  83. its derivatives (Jacobian, Hessian).
  84. * options : dict, optional
  85. Note that by default the tolerance is specified as
  86. ``{ftol: 1e-12}``
  87. options : dict, optional
  88. A dictionary of solver options. Many of the options specified for the
  89. global routine are also passed to the scipy.optimize.minimize routine.
  90. The options that are also passed to the local routine are marked with
  91. "(L)".
  92. Stopping criteria, the algorithm will terminate if any of the specified
  93. criteria are met. However, the default algorithm does not require any to
  94. be specified:
  95. * maxfev : int (L)
  96. Maximum number of function evaluations in the feasible domain.
  97. (Note only methods that support this option will terminate
  98. the routine at precisely exact specified value. Otherwise the
  99. criterion will only terminate during a global iteration)
  100. * f_min
  101. Specify the minimum objective function value, if it is known.
  102. * f_tol : float
  103. Precision goal for the value of f in the stopping
  104. criterion. Note that the global routine will also
  105. terminate if a sampling point in the global routine is
  106. within this tolerance.
  107. * maxiter : int
  108. Maximum number of iterations to perform.
  109. * maxev : int
  110. Maximum number of sampling evaluations to perform (includes
  111. searching in infeasible points).
  112. * maxtime : float
  113. Maximum processing runtime allowed
  114. * minhgrd : int
  115. Minimum homology group rank differential. The homology group of the
  116. objective function is calculated (approximately) during every
  117. iteration. The rank of this group has a one-to-one correspondence
  118. with the number of locally convex subdomains in the objective
  119. function (after adequate sampling points each of these subdomains
  120. contain a unique global minimum). If the difference in the hgr is 0
  121. between iterations for ``maxhgrd`` specified iterations the
  122. algorithm will terminate.
  123. Objective function knowledge:
  124. * symmetry : bool
  125. Specify True if the objective function contains symmetric variables.
  126. The search space (and therefore performance) is decreased by O(n!).
  127. * jac : bool or callable, optional
  128. Jacobian (gradient) of objective function. Only for CG, BFGS,
  129. Newton-CG, L-BFGS-B, TNC, SLSQP, dogleg, trust-ncg. If ``jac`` is a
  130. boolean and is True, ``fun`` is assumed to return the gradient along
  131. with the objective function. If False, the gradient will be
  132. estimated numerically. ``jac`` can also be a callable returning the
  133. gradient of the objective. In this case, it must accept the same
  134. arguments as ``fun``. (Passed to `scipy.optimize.minmize` automatically)
  135. * hess, hessp : callable, optional
  136. Hessian (matrix of second-order derivatives) of objective function
  137. or Hessian of objective function times an arbitrary vector p.
  138. Only for Newton-CG, dogleg, trust-ncg. Only one of ``hessp`` or
  139. ``hess`` needs to be given. If ``hess`` is provided, then
  140. ``hessp`` will be ignored. If neither ``hess`` nor ``hessp`` is
  141. provided, then the Hessian product will be approximated using
  142. finite differences on ``jac``. ``hessp`` must compute the Hessian
  143. times an arbitrary vector. (Passed to `scipy.optimize.minmize`
  144. automatically)
  145. Algorithm settings:
  146. * minimize_every_iter : bool
  147. If True then promising global sampling points will be passed to a
  148. local minimization routine every iteration. If False then only the
  149. final minimizer pool will be run. Defaults to False.
  150. * local_iter : int
  151. Only evaluate a few of the best minimizer pool candidates every
  152. iteration. If False all potential points are passed to the local
  153. minimization routine.
  154. * infty_constraints: bool
  155. If True then any sampling points generated which are outside will
  156. the feasible domain will be saved and given an objective function
  157. value of ``inf``. If False then these points will be discarded.
  158. Using this functionality could lead to higher performance with
  159. respect to function evaluations before the global minimum is found,
  160. specifying False will use less memory at the cost of a slight
  161. decrease in performance. Defaults to True.
  162. Feedback:
  163. * disp : bool (L)
  164. Set to True to print convergence messages.
  165. sampling_method : str or function, optional
  166. Current built in sampling method options are ``halton``, ``sobol`` and
  167. ``simplicial``. The default ``simplicial`` provides
  168. the theoretical guarantee of convergence to the global minimum in finite
  169. time. ``halton`` and ``sobol`` method are faster in terms of sampling
  170. point generation at the cost of the loss of
  171. guaranteed convergence. It is more appropriate for most "easier"
  172. problems where the convergence is relatively fast.
  173. User defined sampling functions must accept two arguments of ``n``
  174. sampling points of dimension ``dim`` per call and output an array of
  175. sampling points with shape `n x dim`.
  176. Returns
  177. -------
  178. res : OptimizeResult
  179. The optimization result represented as a `OptimizeResult` object.
  180. Important attributes are:
  181. ``x`` the solution array corresponding to the global minimum,
  182. ``fun`` the function output at the global solution,
  183. ``xl`` an ordered list of local minima solutions,
  184. ``funl`` the function output at the corresponding local solutions,
  185. ``success`` a Boolean flag indicating if the optimizer exited
  186. successfully,
  187. ``message`` which describes the cause of the termination,
  188. ``nfev`` the total number of objective function evaluations including
  189. the sampling calls,
  190. ``nlfev`` the total number of objective function evaluations
  191. culminating from all local search optimizations,
  192. ``nit`` number of iterations performed by the global routine.
  193. Notes
  194. -----
  195. Global optimization using simplicial homology global optimization [1]_.
  196. Appropriate for solving general purpose NLP and blackbox optimization
  197. problems to global optimality (low-dimensional problems).
  198. In general, the optimization problems are of the form::
  199. minimize f(x) subject to
  200. g_i(x) >= 0, i = 1,...,m
  201. h_j(x) = 0, j = 1,...,p
  202. where x is a vector of one or more variables. ``f(x)`` is the objective
  203. function ``R^n -> R``, ``g_i(x)`` are the inequality constraints, and
  204. ``h_j(x)`` are the equality constraints.
  205. Optionally, the lower and upper bounds for each element in x can also be
  206. specified using the `bounds` argument.
  207. While most of the theoretical advantages of SHGO are only proven for when
  208. ``f(x)`` is a Lipschitz smooth function, the algorithm is also proven to
  209. converge to the global optimum for the more general case where ``f(x)`` is
  210. non-continuous, non-convex and non-smooth, if the default sampling method
  211. is used [1]_.
  212. The local search method may be specified using the ``minimizer_kwargs``
  213. parameter which is passed on to ``scipy.optimize.minimize``. By default,
  214. the ``SLSQP`` method is used. In general, it is recommended to use the
  215. ``SLSQP`` or ``COBYLA`` local minimization if inequality constraints
  216. are defined for the problem since the other methods do not use constraints.
  217. The ``halton`` and ``sobol`` method points are generated using
  218. `scipy.stats.qmc`. Any other QMC method could be used.
  219. References
  220. ----------
  221. .. [1] Endres, SC, Sandrock, C, Focke, WW (2018) "A simplicial homology
  222. algorithm for lipschitz optimisation", Journal of Global Optimization.
  223. .. [2] Joe, SW and Kuo, FY (2008) "Constructing Sobol' sequences with
  224. better two-dimensional projections", SIAM J. Sci. Comput. 30,
  225. 2635-2654.
  226. .. [3] Hoch, W and Schittkowski, K (1981) "Test examples for nonlinear
  227. programming codes", Lecture Notes in Economics and Mathematical
  228. Systems, 187. Springer-Verlag, New York.
  229. http://www.ai7.uni-bayreuth.de/test_problem_coll.pdf
  230. .. [4] Wales, DJ (2015) "Perspective: Insight into reaction coordinates and
  231. dynamics from the potential energy landscape",
  232. Journal of Chemical Physics, 142(13), 2015.
  233. Examples
  234. --------
  235. First consider the problem of minimizing the Rosenbrock function, `rosen`:
  236. >>> import numpy as np
  237. >>> from scipy.optimize import rosen, shgo
  238. >>> bounds = [(0,2), (0, 2), (0, 2), (0, 2), (0, 2)]
  239. >>> result = shgo(rosen, bounds)
  240. >>> result.x, result.fun
  241. (array([1., 1., 1., 1., 1.]), 2.920392374190081e-18)
  242. Note that bounds determine the dimensionality of the objective
  243. function and is therefore a required input, however you can specify
  244. empty bounds using ``None`` or objects like ``np.inf`` which will be
  245. converted to large float numbers.
  246. >>> bounds = [(None, None), ]*4
  247. >>> result = shgo(rosen, bounds)
  248. >>> result.x
  249. array([0.99999851, 0.99999704, 0.99999411, 0.9999882 ])
  250. Next, we consider the Eggholder function, a problem with several local
  251. minima and one global minimum. We will demonstrate the use of arguments and
  252. the capabilities of `shgo`.
  253. (https://en.wikipedia.org/wiki/Test_functions_for_optimization)
  254. >>> def eggholder(x):
  255. ... return (-(x[1] + 47.0)
  256. ... * np.sin(np.sqrt(abs(x[0]/2.0 + (x[1] + 47.0))))
  257. ... - x[0] * np.sin(np.sqrt(abs(x[0] - (x[1] + 47.0))))
  258. ... )
  259. ...
  260. >>> bounds = [(-512, 512), (-512, 512)]
  261. `shgo` has built-in low discrepancy sampling sequences. First, we will
  262. input 64 initial sampling points of the *Sobol'* sequence:
  263. >>> result = shgo(eggholder, bounds, n=64, sampling_method='sobol')
  264. >>> result.x, result.fun
  265. (array([512. , 404.23180824]), -959.6406627208397)
  266. `shgo` also has a return for any other local minima that was found, these
  267. can be called using:
  268. >>> result.xl
  269. array([[ 512. , 404.23180824],
  270. [ 283.0759062 , -487.12565635],
  271. [-294.66820039, -462.01964031],
  272. [-105.87688911, 423.15323845],
  273. [-242.97926 , 274.38030925],
  274. [-506.25823477, 6.3131022 ],
  275. [-408.71980731, -156.10116949],
  276. [ 150.23207937, 301.31376595],
  277. [ 91.00920901, -391.283763 ],
  278. [ 202.89662724, -269.38043241],
  279. [ 361.66623976, -106.96493868],
  280. [-219.40612786, -244.06020508]])
  281. >>> result.funl
  282. array([-959.64066272, -718.16745962, -704.80659592, -565.99778097,
  283. -559.78685655, -557.36868733, -507.87385942, -493.9605115 ,
  284. -426.48799655, -421.15571437, -419.31194957, -410.98477763])
  285. These results are useful in applications where there are many global minima
  286. and the values of other global minima are desired or where the local minima
  287. can provide insight into the system (for example morphologies
  288. in physical chemistry [4]_).
  289. If we want to find a larger number of local minima, we can increase the
  290. number of sampling points or the number of iterations. We'll increase the
  291. number of sampling points to 64 and the number of iterations from the
  292. default of 1 to 3. Using ``simplicial`` this would have given us
  293. 64 x 3 = 192 initial sampling points.
  294. >>> result_2 = shgo(eggholder, bounds, n=64, iters=3, sampling_method='sobol')
  295. >>> len(result.xl), len(result_2.xl)
  296. (12, 20)
  297. Note the difference between, e.g., ``n=192, iters=1`` and ``n=64,
  298. iters=3``.
  299. In the first case the promising points contained in the minimiser pool
  300. are processed only once. In the latter case it is processed every 64
  301. sampling points for a total of 3 times.
  302. To demonstrate solving problems with non-linear constraints consider the
  303. following example from Hock and Schittkowski problem 73 (cattle-feed) [3]_::
  304. minimize: f = 24.55 * x_1 + 26.75 * x_2 + 39 * x_3 + 40.50 * x_4
  305. subject to: 2.3 * x_1 + 5.6 * x_2 + 11.1 * x_3 + 1.3 * x_4 - 5 >= 0,
  306. 12 * x_1 + 11.9 * x_2 + 41.8 * x_3 + 52.1 * x_4 - 21
  307. -1.645 * sqrt(0.28 * x_1**2 + 0.19 * x_2**2 +
  308. 20.5 * x_3**2 + 0.62 * x_4**2) >= 0,
  309. x_1 + x_2 + x_3 + x_4 - 1 == 0,
  310. 1 >= x_i >= 0 for all i
  311. The approximate answer given in [3]_ is::
  312. f([0.6355216, -0.12e-11, 0.3127019, 0.05177655]) = 29.894378
  313. >>> def f(x): # (cattle-feed)
  314. ... return 24.55*x[0] + 26.75*x[1] + 39*x[2] + 40.50*x[3]
  315. ...
  316. >>> def g1(x):
  317. ... return 2.3*x[0] + 5.6*x[1] + 11.1*x[2] + 1.3*x[3] - 5 # >=0
  318. ...
  319. >>> def g2(x):
  320. ... return (12*x[0] + 11.9*x[1] +41.8*x[2] + 52.1*x[3] - 21
  321. ... - 1.645 * np.sqrt(0.28*x[0]**2 + 0.19*x[1]**2
  322. ... + 20.5*x[2]**2 + 0.62*x[3]**2)
  323. ... ) # >=0
  324. ...
  325. >>> def h1(x):
  326. ... return x[0] + x[1] + x[2] + x[3] - 1 # == 0
  327. ...
  328. >>> cons = ({'type': 'ineq', 'fun': g1},
  329. ... {'type': 'ineq', 'fun': g2},
  330. ... {'type': 'eq', 'fun': h1})
  331. >>> bounds = [(0, 1.0),]*4
  332. >>> res = shgo(f, bounds, iters=3, constraints=cons)
  333. >>> res
  334. message: Optimization terminated successfully.
  335. success: True
  336. fun: 29.894378159142136
  337. funl: [ 2.989e+01]
  338. x: [ 6.355e-01 1.137e-13 3.127e-01 5.178e-02]
  339. xl: [[ 6.355e-01 1.137e-13 3.127e-01 5.178e-02]]
  340. nit: 3
  341. nfev: 114
  342. nlfev: 35
  343. nljev: 5
  344. nlhev: 0
  345. >>> g1(res.x), g2(res.x), h1(res.x)
  346. (-5.062616992290714e-14, -2.9594104944408173e-12, 0.0)
  347. """
  348. # if necessary, convert bounds class to old bounds
  349. if isinstance(bounds, Bounds):
  350. bounds = new_bounds_to_old(bounds.lb, bounds.ub, len(bounds.lb))
  351. # Initiate SHGO class
  352. shc = SHGO(func, bounds, args=args, constraints=constraints, n=n,
  353. iters=iters, callback=callback,
  354. minimizer_kwargs=minimizer_kwargs,
  355. options=options, sampling_method=sampling_method)
  356. # Run the algorithm, process results and test success
  357. shc.construct_complex()
  358. if not shc.break_routine:
  359. if shc.disp:
  360. print("Successfully completed construction of complex.")
  361. # Test post iterations success
  362. if len(shc.LMC.xl_maps) == 0:
  363. # If sampling failed to find pool, return lowest sampled point
  364. # with a warning
  365. shc.find_lowest_vertex()
  366. shc.break_routine = True
  367. shc.fail_routine(mes="Failed to find a feasible minimizer point. "
  368. "Lowest sampling point = {}".format(shc.f_lowest))
  369. shc.res.fun = shc.f_lowest
  370. shc.res.x = shc.x_lowest
  371. shc.res.nfev = shc.fn
  372. # Confirm the routine ran successfully
  373. if not shc.break_routine:
  374. shc.res.message = 'Optimization terminated successfully.'
  375. shc.res.success = True
  376. # Return the final results
  377. return shc.res
  378. class SHGO:
  379. def __init__(self, func, bounds, args=(), constraints=None, n=None,
  380. iters=None, callback=None, minimizer_kwargs=None,
  381. options=None, sampling_method='sobol'):
  382. from scipy.stats import qmc
  383. # Input checks
  384. methods = ['halton', 'sobol', 'simplicial']
  385. if isinstance(sampling_method, str) and sampling_method not in methods:
  386. raise ValueError(("Unknown sampling_method specified."
  387. " Valid methods: {}").format(', '.join(methods)))
  388. # Initiate class
  389. self._raw_func = func # some methods pass args in (e.g. Complex)
  390. _, self.func = _wrap_scalar_function(func, args)
  391. self.bounds = bounds
  392. self.args = args
  393. self.callback = callback
  394. # Bounds
  395. abound = np.array(bounds, float)
  396. self.dim = np.shape(abound)[0] # Dimensionality of problem
  397. # Set none finite values to large floats
  398. infind = ~np.isfinite(abound)
  399. abound[infind[:, 0], 0] = -1e50
  400. abound[infind[:, 1], 1] = 1e50
  401. # Check if bounds are correctly specified
  402. bnderr = abound[:, 0] > abound[:, 1]
  403. if bnderr.any():
  404. raise ValueError('Error: lb > ub in bounds {}.'
  405. .format(', '.join(str(b) for b in bnderr)))
  406. self.bounds = abound
  407. # Constraints
  408. # Process constraint dict sequence:
  409. if constraints is not None:
  410. self.min_cons = constraints
  411. self.g_cons = []
  412. self.g_args = []
  413. if (type(constraints) is not tuple) and (type(constraints)
  414. is not list):
  415. constraints = (constraints,)
  416. for cons in constraints:
  417. if cons['type'] == 'ineq':
  418. self.g_cons.append(cons['fun'])
  419. try:
  420. self.g_args.append(cons['args'])
  421. except KeyError:
  422. self.g_args.append(())
  423. self.g_cons = tuple(self.g_cons)
  424. self.g_args = tuple(self.g_args)
  425. else:
  426. self.g_cons = None
  427. self.g_args = None
  428. # Define local minimization keyword arguments
  429. # Start with defaults
  430. self.minimizer_kwargs = {'method': 'SLSQP',
  431. 'bounds': self.bounds,
  432. 'options': {},
  433. 'callback': self.callback
  434. }
  435. if minimizer_kwargs is not None:
  436. # Overwrite with supplied values
  437. self.minimizer_kwargs.update(minimizer_kwargs)
  438. else:
  439. self.minimizer_kwargs['options'] = {'ftol': 1e-12}
  440. if (self.minimizer_kwargs['method'] in ('SLSQP', 'COBYLA') and
  441. (minimizer_kwargs is not None and
  442. 'constraints' not in minimizer_kwargs and
  443. constraints is not None) or
  444. (self.g_cons is not None)):
  445. self.minimizer_kwargs['constraints'] = self.min_cons
  446. # Process options dict
  447. if options is not None:
  448. self.init_options(options)
  449. else: # Default settings:
  450. self.f_min_true = None
  451. self.minimize_every_iter = False
  452. # Algorithm limits
  453. self.maxiter = None
  454. self.maxfev = None
  455. self.maxev = None
  456. self.maxtime = None
  457. self.f_min_true = None
  458. self.minhgrd = None
  459. # Objective function knowledge
  460. self.symmetry = False
  461. # Algorithm functionality
  462. self.local_iter = False
  463. self.infty_cons_sampl = True
  464. # Feedback
  465. self.disp = False
  466. # Remove unknown arguments in self.minimizer_kwargs
  467. # Start with arguments all the solvers have in common
  468. self.min_solver_args = ['fun', 'x0', 'args',
  469. 'callback', 'options', 'method']
  470. # then add the ones unique to specific solvers
  471. solver_args = {
  472. '_custom': ['jac', 'hess', 'hessp', 'bounds', 'constraints'],
  473. 'nelder-mead': [],
  474. 'powell': [],
  475. 'cg': ['jac'],
  476. 'bfgs': ['jac'],
  477. 'newton-cg': ['jac', 'hess', 'hessp'],
  478. 'l-bfgs-b': ['jac', 'bounds'],
  479. 'tnc': ['jac', 'bounds'],
  480. 'cobyla': ['constraints'],
  481. 'slsqp': ['jac', 'bounds', 'constraints'],
  482. 'dogleg': ['jac', 'hess'],
  483. 'trust-ncg': ['jac', 'hess', 'hessp'],
  484. 'trust-krylov': ['jac', 'hess', 'hessp'],
  485. 'trust-exact': ['jac', 'hess'],
  486. 'trust-constr': ['jac', 'hess', 'hessp'],
  487. }
  488. method = self.minimizer_kwargs['method']
  489. self.min_solver_args += solver_args[method.lower()]
  490. # Only retain the known arguments
  491. def _restrict_to_keys(dictionary, goodkeys):
  492. """Remove keys from dictionary if not in goodkeys - inplace"""
  493. existingkeys = set(dictionary)
  494. for key in existingkeys - set(goodkeys):
  495. dictionary.pop(key, None)
  496. _restrict_to_keys(self.minimizer_kwargs, self.min_solver_args)
  497. _restrict_to_keys(self.minimizer_kwargs['options'],
  498. self.min_solver_args + ['ftol'])
  499. # Algorithm controls
  500. # Global controls
  501. self.stop_global = False # Used in the stopping_criteria method
  502. self.break_routine = False # Break the algorithm globally
  503. self.iters = iters # Iterations to be ran
  504. self.iters_done = 0 # Iterations to be ran
  505. self.n = n # Sampling points per iteration
  506. self.nc = n # Sampling points to sample in current iteration
  507. self.n_prc = 0 # Processed points (used to track Delaunay iters)
  508. self.n_sampled = 0 # To track number of sampling points already generated
  509. self.fn = 0 # Number of feasible sampling points evaluations performed
  510. self.hgr = 0 # Homology group rank
  511. # Default settings if no sampling criteria.
  512. if self.iters is None:
  513. self.iters = 1
  514. if self.n is None:
  515. self.n = 100
  516. if sampling_method == 'sobol':
  517. self.n = 128
  518. self.nc = self.n
  519. if not ((self.maxiter is None) and (self.maxfev is None) and (
  520. self.maxev is None)
  521. and (self.minhgrd is None) and (self.f_min_true is None)):
  522. self.iters = None
  523. # Set complex construction mode based on a provided stopping criteria:
  524. # Choose complex constructor
  525. if sampling_method == 'simplicial':
  526. self.iterate_complex = self.iterate_hypercube
  527. self.minimizers = self.simplex_minimizers
  528. self.sampling_method = sampling_method
  529. elif sampling_method in ['halton', 'sobol'] or \
  530. not isinstance(sampling_method, str):
  531. self.iterate_complex = self.iterate_delaunay
  532. self.minimizers = self.delaunay_complex_minimisers
  533. # Sampling method used
  534. if sampling_method in ['halton', 'sobol']:
  535. if sampling_method == 'sobol':
  536. self.n = int(2 ** np.ceil(np.log2(self.n)))
  537. self.nc = self.n
  538. self.sampling_method = 'sobol'
  539. self.qmc_engine = qmc.Sobol(d=self.dim, scramble=False,
  540. seed=np.random.RandomState())
  541. else:
  542. self.sampling_method = 'halton'
  543. self.qmc_engine = qmc.Halton(d=self.dim, scramble=True,
  544. seed=np.random.RandomState())
  545. sampling_method = lambda n, d: self.qmc_engine.random(n)
  546. else:
  547. # A user defined sampling method:
  548. self.sampling_method = 'custom'
  549. self.sampling = self.sampling_custom
  550. self.sampling_function = sampling_method # F(n, d)
  551. # Local controls
  552. self.stop_l_iter = False # Local minimisation iterations
  553. self.stop_complex_iter = False # Sampling iterations
  554. # Initiate storage objects used in algorithm classes
  555. self.minimizer_pool = []
  556. # Cache of local minimizers mapped
  557. self.LMC = LMapCache()
  558. # Initialize return object
  559. self.res = OptimizeResult() # scipy.optimize.OptimizeResult object
  560. self.res.nfev = 0 # Includes each sampling point as func evaluation
  561. self.res.nlfev = 0 # Local function evals for all minimisers
  562. self.res.nljev = 0 # Local Jacobian evals for all minimisers
  563. self.res.nlhev = 0 # Local Hessian evals for all minimisers
  564. # Initiation aids
  565. def init_options(self, options):
  566. """
  567. Initiates the options.
  568. Can also be useful to change parameters after class initiation.
  569. Parameters
  570. ----------
  571. options : dict
  572. Returns
  573. -------
  574. None
  575. """
  576. # Update 'options' dict passed to optimize.minimize
  577. # Do this first so we don't mutate `options` below.
  578. self.minimizer_kwargs['options'].update(options)
  579. # Ensure that 'jac', 'hess', and 'hessp' are passed directly to
  580. # `minimize` as keywords, not as part of its 'options' dictionary.
  581. for opt in ['jac', 'hess', 'hessp']:
  582. if opt in self.minimizer_kwargs['options']:
  583. self.minimizer_kwargs[opt] = (
  584. self.minimizer_kwargs['options'].pop(opt))
  585. # Default settings:
  586. self.minimize_every_iter = options.get('minimize_every_iter', False)
  587. # Algorithm limits
  588. # Maximum number of iterations to perform.
  589. self.maxiter = options.get('maxiter', None)
  590. # Maximum number of function evaluations in the feasible domain
  591. self.maxfev = options.get('maxfev', None)
  592. # Maximum number of sampling evaluations (includes searching in
  593. # infeasible points
  594. self.maxev = options.get('maxev', None)
  595. # Maximum processing runtime allowed
  596. self.init = time.time()
  597. self.maxtime = options.get('maxtime', None)
  598. if 'f_min' in options:
  599. # Specify the minimum objective function value, if it is known.
  600. self.f_min_true = options['f_min']
  601. self.f_tol = options.get('f_tol', 1e-4)
  602. else:
  603. self.f_min_true = None
  604. self.minhgrd = options.get('minhgrd', None)
  605. # Objective function knowledge
  606. self.symmetry = 'symmetry' in options
  607. # Algorithm functionality
  608. # Only evaluate a few of the best candiates
  609. self.local_iter = options.get('local_iter', False)
  610. self.infty_cons_sampl = options.get('infty_constraints', True)
  611. # Feedback
  612. self.disp = options.get('disp', False)
  613. # Iteration properties
  614. # Main construction loop:
  615. def construct_complex(self):
  616. """
  617. Construct for `iters` iterations.
  618. If uniform sampling is used, every iteration adds 'n' sampling points.
  619. Iterations if a stopping criteria (e.g., sampling points or
  620. processing time) has been met.
  621. """
  622. if self.disp:
  623. print('Splitting first generation')
  624. while not self.stop_global:
  625. if self.break_routine:
  626. break
  627. # Iterate complex, process minimisers
  628. self.iterate()
  629. self.stopping_criteria()
  630. # Build minimiser pool
  631. # Final iteration only needed if pools weren't minimised every iteration
  632. if not self.minimize_every_iter:
  633. if not self.break_routine:
  634. self.find_minima()
  635. self.res.nit = self.iters_done + 1
  636. def find_minima(self):
  637. """
  638. Construct the minimizer pool, map the minimizers to local minima
  639. and sort the results into a global return object.
  640. """
  641. self.minimizers()
  642. if len(self.X_min) != 0:
  643. # Minimize the pool of minimizers with local minimization methods
  644. # Note that if Options['local_iter'] is an `int` instead of default
  645. # value False then only that number of candidates will be minimized
  646. self.minimise_pool(self.local_iter)
  647. # Sort results and build the global return object
  648. self.sort_result()
  649. # Lowest values used to report in case of failures
  650. self.f_lowest = self.res.fun
  651. self.x_lowest = self.res.x
  652. else:
  653. self.find_lowest_vertex()
  654. def find_lowest_vertex(self):
  655. # Find the lowest objective function value on one of
  656. # the vertices of the simplicial complex
  657. if self.sampling_method == 'simplicial':
  658. self.f_lowest = np.inf
  659. for x in self.HC.V.cache:
  660. if self.HC.V[x].f < self.f_lowest:
  661. self.f_lowest = self.HC.V[x].f
  662. self.x_lowest = self.HC.V[x].x_a
  663. if self.f_lowest == np.inf: # no feasible point
  664. self.f_lowest = None
  665. self.x_lowest = None
  666. else:
  667. if self.fn == 0:
  668. self.f_lowest = None
  669. self.x_lowest = None
  670. else:
  671. self.f_I = np.argsort(self.F, axis=-1)
  672. self.f_lowest = self.F[self.f_I[0]]
  673. self.x_lowest = self.C[self.f_I[0]]
  674. # Stopping criteria functions:
  675. def finite_iterations(self):
  676. if self.iters is not None:
  677. if self.iters_done >= (self.iters - 1):
  678. self.stop_global = True
  679. if self.maxiter is not None: # Stop for infeasible sampling
  680. if self.iters_done >= (self.maxiter - 1):
  681. self.stop_global = True
  682. return self.stop_global
  683. def finite_fev(self):
  684. # Finite function evals in the feasible domain
  685. if self.fn >= self.maxfev:
  686. self.stop_global = True
  687. return self.stop_global
  688. def finite_ev(self):
  689. # Finite evaluations including infeasible sampling points
  690. if self.n_sampled >= self.maxev:
  691. self.stop_global = True
  692. def finite_time(self):
  693. if (time.time() - self.init) >= self.maxtime:
  694. self.stop_global = True
  695. def finite_precision(self):
  696. """
  697. Stop the algorithm if the final function value is known
  698. Specify in options (with ``self.f_min_true = options['f_min']``)
  699. and the tolerance with ``f_tol = options['f_tol']``
  700. """
  701. # If no minimizer has been found use the lowest sampling value
  702. if len(self.LMC.xl_maps) == 0:
  703. self.find_lowest_vertex()
  704. # Function to stop algorithm at specified percentage error:
  705. if self.f_lowest == 0.0:
  706. if self.f_min_true == 0.0:
  707. if self.f_lowest <= self.f_tol:
  708. self.stop_global = True
  709. else:
  710. pe = (self.f_lowest - self.f_min_true) / abs(self.f_min_true)
  711. if self.f_lowest <= self.f_min_true:
  712. self.stop_global = True
  713. # 2if (pe - self.f_tol) <= abs(1.0 / abs(self.f_min_true)):
  714. if abs(pe) >= 2 * self.f_tol:
  715. warnings.warn("A much lower value than expected f* =" +
  716. " {} than".format(self.f_min_true) +
  717. " the was found f_lowest =" +
  718. "{} ".format(self.f_lowest))
  719. if pe <= self.f_tol:
  720. self.stop_global = True
  721. return self.stop_global
  722. def finite_homology_growth(self):
  723. if self.LMC.size == 0:
  724. return # pass on no reason to stop yet.
  725. self.hgrd = self.LMC.size - self.hgr
  726. self.hgr = self.LMC.size
  727. if self.hgrd <= self.minhgrd:
  728. self.stop_global = True
  729. return self.stop_global
  730. def stopping_criteria(self):
  731. """
  732. Various stopping criteria ran every iteration
  733. Returns
  734. -------
  735. stop : bool
  736. """
  737. if self.maxiter is not None:
  738. self.finite_iterations()
  739. if self.iters is not None:
  740. self.finite_iterations()
  741. if self.maxfev is not None:
  742. self.finite_fev()
  743. if self.maxev is not None:
  744. self.finite_ev()
  745. if self.maxtime is not None:
  746. self.finite_time()
  747. if self.f_min_true is not None:
  748. self.finite_precision()
  749. if self.minhgrd is not None:
  750. self.finite_homology_growth()
  751. def iterate(self):
  752. self.iterate_complex()
  753. # Build minimizer pool
  754. if self.minimize_every_iter:
  755. if not self.break_routine:
  756. self.find_minima() # Process minimizer pool
  757. # Algorithm updates
  758. self.iters_done += 1
  759. def iterate_hypercube(self):
  760. """
  761. Iterate a subdivision of the complex
  762. Note: called with ``self.iterate_complex()`` after class initiation
  763. """
  764. # Iterate the complex
  765. if self.n_sampled == 0:
  766. # Initial triangulation of the hyper-rectangle. Note that
  767. # we use `self.raw_func` as `self.func` is a *wrapped* function
  768. # that already takes the original function arguments into
  769. # account.
  770. self.HC = Complex(self.dim, self._raw_func, self.args,
  771. self.symmetry, self.bounds, self.g_cons,
  772. self.g_args)
  773. else:
  774. self.HC.split_generation()
  775. # feasible sampling points counted by the triangulation.py routines
  776. self.fn = self.HC.V.nfev
  777. self.n_sampled = self.HC.V.size # nevs counted in triangulation.py
  778. return
  779. def iterate_delaunay(self):
  780. """
  781. Build a complex of Delaunay triangulated points
  782. Note: called with ``self.iterate_complex()`` after class initiation
  783. """
  784. self.sampled_surface(infty_cons_sampl=self.infty_cons_sampl)
  785. self.nc += self.n
  786. self.n_sampled = self.nc
  787. # Hypercube minimizers
  788. def simplex_minimizers(self):
  789. """
  790. Returns the indexes of all minimizers
  791. """
  792. self.minimizer_pool = []
  793. # Note: Can implement parallelization here
  794. for x in self.HC.V.cache:
  795. if self.HC.V[x].minimiser():
  796. if self.disp:
  797. logging.info('=' * 60)
  798. logging.info(
  799. 'v.x = {} is minimizer'.format(self.HC.V[x].x_a))
  800. logging.info('v.f = {} is minimizer'.format(self.HC.V[x].f))
  801. logging.info('=' * 30)
  802. if self.HC.V[x] not in self.minimizer_pool:
  803. self.minimizer_pool.append(self.HC.V[x])
  804. if self.disp:
  805. logging.info('Neighbors:')
  806. logging.info('=' * 30)
  807. for vn in self.HC.V[x].nn:
  808. logging.info('x = {} || f = {}'.format(vn.x, vn.f))
  809. logging.info('=' * 60)
  810. self.minimizer_pool_F = []
  811. self.X_min = []
  812. # normalized tuple in the Vertex cache
  813. self.X_min_cache = {} # Cache used in hypercube sampling
  814. for v in self.minimizer_pool:
  815. self.X_min.append(v.x_a)
  816. self.minimizer_pool_F.append(v.f)
  817. self.X_min_cache[tuple(v.x_a)] = v.x
  818. self.minimizer_pool_F = np.array(self.minimizer_pool_F)
  819. self.X_min = np.array(self.X_min)
  820. # TODO: Only do this if global mode
  821. self.sort_min_pool()
  822. return self.X_min
  823. # Local minimization
  824. # Minimizer pool processing
  825. def minimise_pool(self, force_iter=False):
  826. """
  827. This processing method can optionally minimise only the best candidate
  828. solutions in the minimizer pool
  829. Parameters
  830. ----------
  831. force_iter : int
  832. Number of starting minimizers to process (can be sepcified
  833. globally or locally)
  834. """
  835. # Find first local minimum
  836. # NOTE: Since we always minimize this value regardless it is a waste to
  837. # build the topograph first before minimizing
  838. lres_f_min = self.minimize(self.X_min[0], ind=self.minimizer_pool[0])
  839. # Trim minimized point from current minimizer set
  840. self.trim_min_pool(0)
  841. # Force processing to only
  842. if force_iter:
  843. self.local_iter = force_iter
  844. while not self.stop_l_iter:
  845. # Global stopping criteria:
  846. if self.f_min_true is not None:
  847. if (lres_f_min.fun - self.f_min_true) / abs(
  848. self.f_min_true) <= self.f_tol:
  849. self.stop_l_iter = True
  850. break
  851. # Note first iteration is outside loop:
  852. if self.local_iter is not None:
  853. if self.disp:
  854. logging.info(
  855. 'SHGO.iters in function minimise_pool = {}'.format(
  856. self.local_iter))
  857. self.local_iter -= 1
  858. if self.local_iter == 0:
  859. self.stop_l_iter = True
  860. break
  861. if np.shape(self.X_min)[0] == 0:
  862. self.stop_l_iter = True
  863. break
  864. # Construct topograph from current minimizer set
  865. # (NOTE: This is a very small topograph using only the minizer pool
  866. # , it might be worth using some graph theory tools instead.
  867. self.g_topograph(lres_f_min.x, self.X_min)
  868. # Find local minimum at the miniser with the greatest Euclidean
  869. # distance from the current solution
  870. ind_xmin_l = self.Z[:, -1]
  871. lres_f_min = self.minimize(self.Ss[-1, :], self.minimizer_pool[-1])
  872. # Trim minimised point from current minimizer set
  873. self.trim_min_pool(ind_xmin_l)
  874. # Reset controls
  875. self.stop_l_iter = False
  876. return
  877. def sort_min_pool(self):
  878. # Sort to find minimum func value in min_pool
  879. self.ind_f_min = np.argsort(self.minimizer_pool_F)
  880. self.minimizer_pool = np.array(self.minimizer_pool)[self.ind_f_min]
  881. self.minimizer_pool_F = np.array(self.minimizer_pool_F)[
  882. self.ind_f_min]
  883. return
  884. def trim_min_pool(self, trim_ind):
  885. self.X_min = np.delete(self.X_min, trim_ind, axis=0)
  886. self.minimizer_pool_F = np.delete(self.minimizer_pool_F, trim_ind)
  887. self.minimizer_pool = np.delete(self.minimizer_pool, trim_ind)
  888. return
  889. def g_topograph(self, x_min, X_min):
  890. """
  891. Returns the topographical vector stemming from the specified value
  892. ``x_min`` for the current feasible set ``X_min`` with True boolean
  893. values indicating positive entries and False values indicating
  894. negative entries.
  895. """
  896. x_min = np.array([x_min])
  897. self.Y = spatial.distance.cdist(x_min, X_min, 'euclidean')
  898. # Find sorted indexes of spatial distances:
  899. self.Z = np.argsort(self.Y, axis=-1)
  900. self.Ss = X_min[self.Z][0]
  901. self.minimizer_pool = self.minimizer_pool[self.Z]
  902. self.minimizer_pool = self.minimizer_pool[0]
  903. return self.Ss
  904. # Local bound functions
  905. def construct_lcb_simplicial(self, v_min):
  906. """
  907. Construct locally (approximately) convex bounds
  908. Parameters
  909. ----------
  910. v_min : Vertex object
  911. The minimizer vertex
  912. Returns
  913. -------
  914. cbounds : list of lists
  915. List of size dimension with length-2 list of bounds for each dimension
  916. """
  917. cbounds = [[x_b_i[0], x_b_i[1]] for x_b_i in self.bounds]
  918. # Loop over all bounds
  919. for vn in v_min.nn:
  920. for i, x_i in enumerate(vn.x_a):
  921. # Lower bound
  922. if (x_i < v_min.x_a[i]) and (x_i > cbounds[i][0]):
  923. cbounds[i][0] = x_i
  924. # Upper bound
  925. if (x_i > v_min.x_a[i]) and (x_i < cbounds[i][1]):
  926. cbounds[i][1] = x_i
  927. if self.disp:
  928. logging.info('cbounds found for v_min.x_a = {}'.format(v_min.x_a))
  929. logging.info('cbounds = {}'.format(cbounds))
  930. return cbounds
  931. def construct_lcb_delaunay(self, v_min, ind=None):
  932. """
  933. Construct locally (approximately) convex bounds
  934. Parameters
  935. ----------
  936. v_min : Vertex object
  937. The minimizer vertex
  938. Returns
  939. -------
  940. cbounds : list of lists
  941. List of size dimension with length-2 list of bounds for each dimension
  942. """
  943. cbounds = [[x_b_i[0], x_b_i[1]] for x_b_i in self.bounds]
  944. return cbounds
  945. # Minimize a starting point locally
  946. def minimize(self, x_min, ind=None):
  947. """
  948. This function is used to calculate the local minima using the specified
  949. sampling point as a starting value.
  950. Parameters
  951. ----------
  952. x_min : vector of floats
  953. Current starting point to minimize.
  954. Returns
  955. -------
  956. lres : OptimizeResult
  957. The local optimization result represented as a `OptimizeResult`
  958. object.
  959. """
  960. # Use minima maps if vertex was already run
  961. if self.disp:
  962. logging.info('Vertex minimiser maps = {}'.format(self.LMC.v_maps))
  963. if self.LMC[x_min].lres is not None:
  964. return self.LMC[x_min].lres
  965. # TODO: Check discarded bound rules
  966. if self.callback is not None:
  967. print('Callback for '
  968. 'minimizer starting at {}:'.format(x_min))
  969. if self.disp:
  970. print('Starting '
  971. 'minimization at {}...'.format(x_min))
  972. if self.sampling_method == 'simplicial':
  973. x_min_t = tuple(x_min)
  974. # Find the normalized tuple in the Vertex cache:
  975. x_min_t_norm = self.X_min_cache[tuple(x_min_t)]
  976. x_min_t_norm = tuple(x_min_t_norm)
  977. g_bounds = self.construct_lcb_simplicial(self.HC.V[x_min_t_norm])
  978. if 'bounds' in self.min_solver_args:
  979. self.minimizer_kwargs['bounds'] = g_bounds
  980. else:
  981. g_bounds = self.construct_lcb_delaunay(x_min, ind=ind)
  982. if 'bounds' in self.min_solver_args:
  983. self.minimizer_kwargs['bounds'] = g_bounds
  984. if self.disp and 'bounds' in self.minimizer_kwargs:
  985. print('bounds in kwarg:')
  986. print(self.minimizer_kwargs['bounds'])
  987. # Local minimization using scipy.optimize.minimize:
  988. lres = minimize(self.func, x_min, **self.minimizer_kwargs)
  989. if self.disp:
  990. print('lres = {}'.format(lres))
  991. # Local function evals for all minimizers
  992. self.res.nlfev += lres.nfev
  993. if 'njev' in lres:
  994. self.res.nljev += lres.njev
  995. if 'nhev' in lres:
  996. self.res.nlhev += lres.nhev
  997. try: # Needed because of the brain dead 1x1 NumPy arrays
  998. lres.fun = lres.fun[0]
  999. except (IndexError, TypeError):
  1000. lres.fun
  1001. # Append minima maps
  1002. self.LMC[x_min]
  1003. self.LMC.add_res(x_min, lres, bounds=g_bounds)
  1004. return lres
  1005. # Post local minimization processing
  1006. def sort_result(self):
  1007. """
  1008. Sort results and build the global return object
  1009. """
  1010. # Sort results in local minima cache
  1011. results = self.LMC.sort_cache_result()
  1012. self.res.xl = results['xl']
  1013. self.res.funl = results['funl']
  1014. self.res.x = results['x']
  1015. self.res.fun = results['fun']
  1016. # Add local func evals to sampling func evals
  1017. # Count the number of feasible vertices and add to local func evals:
  1018. self.res.nfev = self.fn + self.res.nlfev
  1019. return self.res
  1020. # Algorithm controls
  1021. def fail_routine(self, mes=("Failed to converge")):
  1022. self.break_routine = True
  1023. self.res.success = False
  1024. self.X_min = [None]
  1025. self.res.message = mes
  1026. def sampled_surface(self, infty_cons_sampl=False):
  1027. """
  1028. Sample the function surface.
  1029. There are 2 modes, if ``infty_cons_sampl`` is True then the sampled
  1030. points that are generated outside the feasible domain will be
  1031. assigned an ``inf`` value in accordance with SHGO rules.
  1032. This guarantees convergence and usually requires less objective function
  1033. evaluations at the computational costs of more Delaunay triangulation
  1034. points.
  1035. If ``infty_cons_sampl`` is False, then the infeasible points are discarded
  1036. and only a subspace of the sampled points are used. This comes at the
  1037. cost of the loss of guaranteed convergence and usually requires more
  1038. objective function evaluations.
  1039. """
  1040. # Generate sampling points
  1041. if self.disp:
  1042. print('Generating sampling points')
  1043. self.sampling(self.nc, self.dim)
  1044. self.n = self.nc
  1045. if not infty_cons_sampl:
  1046. # Find subspace of feasible points
  1047. if self.g_cons is not None:
  1048. self.sampling_subspace()
  1049. # Sort remaining samples
  1050. self.sorted_samples()
  1051. # Find objective function references
  1052. self.fun_ref()
  1053. self.n_sampled = self.nc
  1054. def delaunay_complex_minimisers(self):
  1055. # Construct complex minimizers on the current sampling set.
  1056. # if self.fn >= (self.dim + 1):
  1057. if self.fn >= (self.dim + 2):
  1058. # TODO: Check on strange Qhull error where the number of vertices
  1059. # required for an initial simplex is higher than n + 1?
  1060. if self.dim < 2: # Scalar objective functions
  1061. if self.disp:
  1062. print('Constructing 1-D minimizer pool')
  1063. self.ax_subspace()
  1064. self.surface_topo_ref()
  1065. self.minimizers_1D()
  1066. else: # Multivariate functions.
  1067. if self.disp:
  1068. print('Constructing Gabrial graph and minimizer pool')
  1069. if self.iters == 1:
  1070. self.delaunay_triangulation(grow=False)
  1071. else:
  1072. self.delaunay_triangulation(grow=True, n_prc=self.n_prc)
  1073. self.n_prc = self.C.shape[0]
  1074. if self.disp:
  1075. print('Triangulation completed, building minimizer pool')
  1076. self.delaunay_minimizers()
  1077. if self.disp:
  1078. logging.info(
  1079. "Minimizer pool = SHGO.X_min = {}".format(self.X_min))
  1080. else:
  1081. if self.disp:
  1082. print(
  1083. 'Not enough sampling points found in the feasible domain.')
  1084. self.minimizer_pool = [None]
  1085. try:
  1086. self.X_min
  1087. except AttributeError:
  1088. self.X_min = []
  1089. def sampling_custom(self, n, dim):
  1090. """
  1091. Generates uniform sampling points in a hypercube and scales the points
  1092. to the bound limits.
  1093. """
  1094. # Generate sampling points.
  1095. # Generate uniform sample points in [0, 1]^m \subset R^m
  1096. self.C = self.sampling_function(n, dim)
  1097. # Distribute over bounds
  1098. for i in range(len(self.bounds)):
  1099. self.C[:, i] = (self.C[:, i] *
  1100. (self.bounds[i][1] - self.bounds[i][0])
  1101. + self.bounds[i][0])
  1102. return self.C
  1103. def sampling_subspace(self):
  1104. """Find subspace of feasible points from g_func definition"""
  1105. # Subspace of feasible points.
  1106. for ind, g in enumerate(self.g_cons):
  1107. self.C = self.C[g(self.C.T, *self.g_args[ind]) >= 0.0]
  1108. if self.C.size == 0:
  1109. self.res.message = ('No sampling point found within the '
  1110. + 'feasible set. Increasing sampling '
  1111. + 'size.')
  1112. # sampling correctly for both 1-D and >1-D cases
  1113. if self.disp:
  1114. print(self.res.message)
  1115. def sorted_samples(self): # Validated
  1116. """Find indexes of the sorted sampling points"""
  1117. self.Ind_sorted = np.argsort(self.C, axis=0)
  1118. self.Xs = self.C[self.Ind_sorted]
  1119. return self.Ind_sorted, self.Xs
  1120. def ax_subspace(self): # Validated
  1121. """
  1122. Finds the subspace vectors along each component axis.
  1123. """
  1124. self.Ci = []
  1125. self.Xs_i = []
  1126. self.Ii = []
  1127. for i in range(self.dim):
  1128. self.Ci.append(self.C[:, i])
  1129. self.Ii.append(self.Ind_sorted[:, i])
  1130. self.Xs_i.append(self.Xs[:, i])
  1131. def fun_ref(self):
  1132. """
  1133. Find the objective function output reference table
  1134. """
  1135. # TODO: Replace with cached wrapper
  1136. # Note: This process can be pooled easily
  1137. # Obj. function returns to be used as reference table.:
  1138. f_cache_bool = False
  1139. if self.fn > 0: # Store old function evaluations
  1140. Ftemp = self.F
  1141. fn_old = self.fn
  1142. f_cache_bool = True
  1143. self.F = np.zeros(np.shape(self.C)[0])
  1144. # NOTE: It might be easier to replace this with a cached
  1145. # objective function
  1146. for i in range(self.fn, np.shape(self.C)[0]):
  1147. eval_f = True
  1148. if self.g_cons is not None:
  1149. for g in self.g_cons:
  1150. if g(self.C[i, :], *self.args) < 0.0:
  1151. eval_f = False
  1152. break # Breaks the g loop
  1153. if eval_f:
  1154. self.F[i] = self.func(self.C[i, :])
  1155. self.fn += 1
  1156. elif self.infty_cons_sampl:
  1157. self.F[i] = np.inf
  1158. self.fn += 1
  1159. if f_cache_bool:
  1160. if fn_old > 0: # Restore saved function evaluations
  1161. self.F[0:fn_old] = Ftemp
  1162. return self.F
  1163. def surface_topo_ref(self): # Validated
  1164. """
  1165. Find the BD and FD finite differences along each component vector.
  1166. """
  1167. # Replace numpy inf, -inf and nan objects with floating point numbers
  1168. # nan --> float
  1169. self.F[np.isnan(self.F)] = np.inf
  1170. # inf, -inf --> floats
  1171. self.F = np.nan_to_num(self.F)
  1172. self.Ft = self.F[self.Ind_sorted]
  1173. self.Ftp = np.diff(self.Ft, axis=0) # FD
  1174. self.Ftm = np.diff(self.Ft[::-1], axis=0)[::-1] # BD
  1175. def sample_topo(self, ind):
  1176. # Find the position of the sample in the component axial directions
  1177. self.Xi_ind_pos = []
  1178. self.Xi_ind_topo_i = []
  1179. for i in range(self.dim):
  1180. for x, I_ind in zip(self.Ii[i], range(len(self.Ii[i]))):
  1181. if x == ind:
  1182. self.Xi_ind_pos.append(I_ind)
  1183. # Use the topo reference tables to find if point is a minimizer on
  1184. # the current axis
  1185. # First check if index is on the boundary of the sampling points:
  1186. if self.Xi_ind_pos[i] == 0:
  1187. # if boundary is in basin
  1188. self.Xi_ind_topo_i.append(self.Ftp[:, i][0] > 0)
  1189. elif self.Xi_ind_pos[i] == self.fn - 1:
  1190. # Largest value at sample size
  1191. self.Xi_ind_topo_i.append(self.Ftp[:, i][self.fn - 2] < 0)
  1192. # Find axial reference for other points
  1193. else:
  1194. Xi_ind_top_p = self.Ftp[:, i][self.Xi_ind_pos[i]] > 0
  1195. Xi_ind_top_m = self.Ftm[:, i][self.Xi_ind_pos[i] - 1] > 0
  1196. self.Xi_ind_topo_i.append(Xi_ind_top_p and Xi_ind_top_m)
  1197. if np.array(self.Xi_ind_topo_i).all():
  1198. self.Xi_ind_topo = True
  1199. else:
  1200. self.Xi_ind_topo = False
  1201. self.Xi_ind_topo = np.array(self.Xi_ind_topo_i).all()
  1202. return self.Xi_ind_topo
  1203. def minimizers_1D(self):
  1204. """
  1205. Returns the indices of all minimizers
  1206. """
  1207. self.minimizer_pool = []
  1208. # Note: Can implement parallelization here
  1209. for ind in range(self.fn):
  1210. min_bool = self.sample_topo(ind)
  1211. if min_bool:
  1212. self.minimizer_pool.append(ind)
  1213. self.minimizer_pool_F = self.F[self.minimizer_pool]
  1214. # Sort to find minimum func value in min_pool
  1215. self.sort_min_pool()
  1216. if not len(self.minimizer_pool) == 0:
  1217. self.X_min = self.C[self.minimizer_pool]
  1218. # If function is called again and pool is found unbreak:
  1219. else:
  1220. self.X_min = []
  1221. return self.X_min
  1222. def delaunay_triangulation(self, grow=False, n_prc=0):
  1223. if not grow:
  1224. self.Tri = spatial.Delaunay(self.C)
  1225. else:
  1226. if hasattr(self, 'Tri'):
  1227. self.Tri.add_points(self.C[n_prc:, :])
  1228. else:
  1229. self.Tri = spatial.Delaunay(self.C, incremental=True)
  1230. return self.Tri
  1231. @staticmethod
  1232. def find_neighbors_delaunay(pindex, triang):
  1233. """
  1234. Returns the indices of points connected to ``pindex`` on the Gabriel
  1235. chain subgraph of the Delaunay triangulation.
  1236. """
  1237. return triang.vertex_neighbor_vertices[1][
  1238. triang.vertex_neighbor_vertices[0][pindex]:
  1239. triang.vertex_neighbor_vertices[0][pindex + 1]]
  1240. def sample_delaunay_topo(self, ind):
  1241. self.Xi_ind_topo_i = []
  1242. # Find the position of the sample in the component Gabrial chain
  1243. G_ind = self.find_neighbors_delaunay(ind, self.Tri)
  1244. # Find finite deference between each point
  1245. for g_i in G_ind:
  1246. rel_topo_bool = self.F[ind] < self.F[g_i]
  1247. self.Xi_ind_topo_i.append(rel_topo_bool)
  1248. # Check if minimizer
  1249. self.Xi_ind_topo = np.array(self.Xi_ind_topo_i).all()
  1250. return self.Xi_ind_topo
  1251. def delaunay_minimizers(self):
  1252. """
  1253. Returns the indices of all minimizers
  1254. """
  1255. self.minimizer_pool = []
  1256. # Note: Can easily be parralized
  1257. if self.disp:
  1258. logging.info('self.fn = {}'.format(self.fn))
  1259. logging.info('self.nc = {}'.format(self.nc))
  1260. logging.info('np.shape(self.C)'
  1261. ' = {}'.format(np.shape(self.C)))
  1262. for ind in range(self.fn):
  1263. min_bool = self.sample_delaunay_topo(ind)
  1264. if min_bool:
  1265. self.minimizer_pool.append(ind)
  1266. self.minimizer_pool_F = self.F[self.minimizer_pool]
  1267. # Sort to find minimum func value in min_pool
  1268. self.sort_min_pool()
  1269. if self.disp:
  1270. logging.info('self.minimizer_pool = {}'.format(self.minimizer_pool))
  1271. if not len(self.minimizer_pool) == 0:
  1272. self.X_min = self.C[self.minimizer_pool]
  1273. else:
  1274. self.X_min = [] # Empty pool breaks main routine
  1275. return self.X_min
  1276. class LMap:
  1277. def __init__(self, v):
  1278. self.v = v
  1279. self.x_l = None
  1280. self.lres = None
  1281. self.f_min = None
  1282. self.lbounds = []
  1283. class LMapCache:
  1284. def __init__(self):
  1285. self.cache = {}
  1286. # Lists for search queries
  1287. self.v_maps = []
  1288. self.xl_maps = []
  1289. self.f_maps = []
  1290. self.lbound_maps = []
  1291. self.size = 0
  1292. def __getitem__(self, v):
  1293. v = np.ndarray.tolist(v)
  1294. v = tuple(v)
  1295. try:
  1296. return self.cache[v]
  1297. except KeyError:
  1298. xval = LMap(v)
  1299. self.cache[v] = xval
  1300. return self.cache[v]
  1301. def add_res(self, v, lres, bounds=None):
  1302. v = np.ndarray.tolist(v)
  1303. v = tuple(v)
  1304. self.cache[v].x_l = lres.x
  1305. self.cache[v].lres = lres
  1306. self.cache[v].f_min = lres.fun
  1307. self.cache[v].lbounds = bounds
  1308. # Update cache size
  1309. self.size += 1
  1310. # Cache lists for search queries
  1311. self.v_maps.append(v)
  1312. self.xl_maps.append(lres.x)
  1313. self.f_maps.append(lres.fun)
  1314. self.lbound_maps.append(bounds)
  1315. def sort_cache_result(self):
  1316. """
  1317. Sort results and build the global return object
  1318. """
  1319. results = {}
  1320. # Sort results and save
  1321. self.xl_maps = np.array(self.xl_maps)
  1322. self.f_maps = np.array(self.f_maps)
  1323. # Sorted indexes in Func_min
  1324. ind_sorted = np.argsort(self.f_maps)
  1325. # Save ordered list of minima
  1326. results['xl'] = self.xl_maps[ind_sorted] # Ordered x vals
  1327. self.f_maps = np.array(self.f_maps)
  1328. results['funl'] = self.f_maps[ind_sorted]
  1329. results['funl'] = results['funl'].T
  1330. # Find global of all minimizers
  1331. results['x'] = self.xl_maps[ind_sorted[0]] # Save global minima
  1332. results['fun'] = self.f_maps[ind_sorted[0]] # Save global fun value
  1333. self.xl_maps = np.ndarray.tolist(self.xl_maps)
  1334. self.f_maps = np.ndarray.tolist(self.f_maps)
  1335. return results