1 | """ |
---|
2 | Solvers |
---|
3 | ======= |
---|
4 | |
---|
5 | This module contains an optimization routine based on the simulated |
---|
6 | annealing algorithm. |
---|
7 | |
---|
8 | A minimal interface that mimics a scipy.optimize interface has been |
---|
9 | implemented, and functionality from the mystic solver API has been added |
---|
10 | with reasonable defaults. |
---|
11 | |
---|
12 | Minimal function interface to optimization routines:: |
---|
13 | anneal -- Simulated Annealing solver |
---|
14 | |
---|
15 | The corresponding solver built on mystic's AbstractSolver is:: |
---|
16 | AnnealSolver -- a simulated annealing solver |
---|
17 | |
---|
18 | Mystic solver behavior activated in anneal:: |
---|
19 | - EvaluationMonitor = Sow() |
---|
20 | - StepMonitor = Sow() |
---|
21 | - enable_signal_handler() |
---|
22 | - termination = AveragePopulationImprovement(tolerance) |
---|
23 | |
---|
24 | Usage |
---|
25 | ===== |
---|
26 | |
---|
27 | See `mystic.examples.test_anneal` for an example of using |
---|
28 | AnnealSolver. |
---|
29 | |
---|
30 | All solvers included in this module provide the standard signal handling. |
---|
31 | For more information, see `mystic.mystic.abstract_solver`. |
---|
32 | |
---|
33 | |
---|
34 | History |
---|
35 | ========== |
---|
36 | Based on anneal.py from scipy.optimize: |
---|
37 | Original Author: Travis Oliphant 2002 |
---|
38 | Bug-fixes in 2006 by Tim Leslie |
---|
39 | |
---|
40 | Adapted for Mystic, 2009 |
---|
41 | """ |
---|
42 | |
---|
43 | __all__ = ['AnnealSolver','anneal'] |
---|
44 | |
---|
45 | # Mystic and numpy imports |
---|
46 | from mystic.tools import Null, wrap_function |
---|
47 | from mystic.tools import wrap_bounds |
---|
48 | import numpy |
---|
49 | from numpy import asarray, tan, exp, ones, squeeze, sign, \ |
---|
50 | all, log, sqrt, pi, shape, array, minimum, where |
---|
51 | from numpy import random |
---|
52 | |
---|
53 | from abstract_solver import AbstractSolver |
---|
54 | |
---|
55 | ############################################################# |
---|
56 | # Helper classes and methods |
---|
57 | |
---|
58 | _double_min = numpy.finfo(float).min |
---|
59 | _double_max = numpy.finfo(float).max |
---|
60 | |
---|
61 | # Base class for annealing schedules |
---|
62 | class base_schedule(object): |
---|
63 | def __init__(self): |
---|
64 | self.dwell = 20 |
---|
65 | self.learn_rate = 0.5 |
---|
66 | self.lower = -10 |
---|
67 | self.upper = 10 |
---|
68 | self.Ninit = 50 |
---|
69 | self.accepted = 0 |
---|
70 | self.tests = 0 |
---|
71 | self.feval = 0 |
---|
72 | self.k = 0 |
---|
73 | self.T = None |
---|
74 | |
---|
75 | def init(self, **options): |
---|
76 | self.__dict__.update(options) |
---|
77 | self.lower = asarray(self.lower) |
---|
78 | self.lower = where(self.lower == numpy.NINF, -_double_max, self.lower) |
---|
79 | self.upper = asarray(self.upper) |
---|
80 | self.upper = where(self.upper == numpy.PINF, _double_max, self.upper) |
---|
81 | self.k = 0 |
---|
82 | self.accepted = 0 |
---|
83 | self.feval = 0 |
---|
84 | self.tests = 0 |
---|
85 | |
---|
86 | def getstart_temp(self): |
---|
87 | """ Find a matching starting temperature and starting parameters vector |
---|
88 | i.e. find x0 such that func(x0) = T0. |
---|
89 | |
---|
90 | Parameters |
---|
91 | ---------- |
---|
92 | best_state : _state |
---|
93 | A _state object to store the function value and x0 found. |
---|
94 | |
---|
95 | Returns |
---|
96 | ------- |
---|
97 | x0 : array |
---|
98 | The starting parameters vector. |
---|
99 | """ |
---|
100 | |
---|
101 | assert(not self.dims is None) |
---|
102 | lrange = self.lower |
---|
103 | urange = self.upper |
---|
104 | fmax = _double_min |
---|
105 | fmin = _double_max |
---|
106 | for _ in range(self.Ninit): |
---|
107 | x0 = random.uniform(size=self.dims)*(urange-lrange) + lrange |
---|
108 | fval = self.func(x0, *self.args) |
---|
109 | self.feval += 1 |
---|
110 | if fval > fmax: |
---|
111 | fmax = fval |
---|
112 | if fval < fmin: |
---|
113 | fmin = fval |
---|
114 | bestEnergy = fval |
---|
115 | bestSolution = array(x0) |
---|
116 | |
---|
117 | self.T0 = (fmax-fmin)*1.5 |
---|
118 | return bestSolution, bestEnergy |
---|
119 | |
---|
120 | def accept_test(self, dE): |
---|
121 | T = self.T |
---|
122 | self.tests += 1 |
---|
123 | if dE < 0: |
---|
124 | self.accepted += 1 |
---|
125 | return 1 |
---|
126 | p = exp(-dE*1.0/self.boltzmann/T) |
---|
127 | if (p > random.uniform(0.0, 1.0)): |
---|
128 | self.accepted += 1 |
---|
129 | return 1 |
---|
130 | return 0 |
---|
131 | |
---|
132 | def update_guess(self, x0): |
---|
133 | pass |
---|
134 | |
---|
135 | def update_temp(self, x0): |
---|
136 | pass |
---|
137 | |
---|
138 | # Simulated annealing schedules: 'fast', 'cauchy', and 'boltzmann' |
---|
139 | |
---|
140 | # A schedule due to Lester Ingber |
---|
141 | class fast_sa(base_schedule): |
---|
142 | def init(self, **options): |
---|
143 | self.__dict__.update(options) |
---|
144 | if self.m is None: |
---|
145 | self.m = 1.0 |
---|
146 | if self.n is None: |
---|
147 | self.n = 1.0 |
---|
148 | self.c = self.m * exp(-self.n * self.quench) |
---|
149 | |
---|
150 | def update_guess(self, x0): |
---|
151 | x0 = asarray(x0) |
---|
152 | u = squeeze(random.uniform(0.0, 1.0, size=self.dims)) |
---|
153 | T = self.T |
---|
154 | y = sign(u-0.5)*T*((1+1.0/T)**abs(2*u-1)-1.0) |
---|
155 | xc = y*(self.upper - self.lower) |
---|
156 | xnew = x0 + xc |
---|
157 | return xnew |
---|
158 | |
---|
159 | def update_temp(self): |
---|
160 | self.T = self.T0*exp(-self.c * self.k**(self.quench)) |
---|
161 | self.k += 1 |
---|
162 | return |
---|
163 | |
---|
164 | class cauchy_sa(base_schedule): |
---|
165 | def update_guess(self, x0): |
---|
166 | x0 = asarray(x0) |
---|
167 | numbers = squeeze(random.uniform(-pi/2, pi/2, size=self.dims)) |
---|
168 | xc = self.learn_rate * self.T * tan(numbers) |
---|
169 | xnew = x0 + xc |
---|
170 | return xnew |
---|
171 | |
---|
172 | def update_temp(self): |
---|
173 | self.T = self.T0/(1+self.k) |
---|
174 | self.k += 1 |
---|
175 | return |
---|
176 | |
---|
177 | class boltzmann_sa(base_schedule): |
---|
178 | def update_guess(self, x0): |
---|
179 | std = minimum(sqrt(self.T)*ones(self.dims), (self.upper-self.lower)/3.0/self.learn_rate) |
---|
180 | x0 = asarray(x0) |
---|
181 | xc = squeeze(random.normal(0, 1.0, size=self.dims)) |
---|
182 | |
---|
183 | xnew = x0 + xc*std*self.learn_rate |
---|
184 | return xnew |
---|
185 | |
---|
186 | def update_temp(self): |
---|
187 | self.k += 1 |
---|
188 | self.T = self.T0 / log(self.k+1.0) |
---|
189 | return |
---|
190 | |
---|
191 | class _state(object): |
---|
192 | def __init__(self): |
---|
193 | self.x = None |
---|
194 | self.cost = None |
---|
195 | |
---|
196 | ################################################################################ |
---|
197 | |
---|
198 | class AnnealSolver(AbstractSolver): |
---|
199 | """Simulated annealing optimization.""" |
---|
200 | |
---|
201 | def __init__(self, dim): |
---|
202 | """ |
---|
203 | Takes one initial input: |
---|
204 | dim -- dimensionality of the problem |
---|
205 | |
---|
206 | All important class members are inherited from AbstractSolver. |
---|
207 | """ |
---|
208 | AbstractSolver.__init__(self, dim) |
---|
209 | |
---|
210 | def Solve(self, func, termination, sigint_callback=None, |
---|
211 | EvaluationMonitor=Null, StepMonitor=Null, ExtraArgs=(), **kwds): |
---|
212 | """Minimize a function using simulated annealing. |
---|
213 | |
---|
214 | Description: |
---|
215 | |
---|
216 | Uses a simulated annealing algorithm to find the minimum of |
---|
217 | a function of one or more variables. |
---|
218 | |
---|
219 | Inputs: |
---|
220 | |
---|
221 | func -- the Python function or method to be minimized. |
---|
222 | termination -- callable object providing termination conditions. |
---|
223 | |
---|
224 | Additional Inputs: |
---|
225 | |
---|
226 | sigint_callback -- callback function for signal handler. |
---|
227 | EvaluationMonitor -- a callable object that will be passed x, fval |
---|
228 | whenever the cost function is evaluated. |
---|
229 | StepMonitor -- a callable object that will be passed x, fval |
---|
230 | after the end of an iteration. |
---|
231 | ExtraArgs -- extra arguments for func. |
---|
232 | |
---|
233 | Further Inputs: |
---|
234 | |
---|
235 | schedule -- Annealing schedule to use: 'cauchy', 'fast', or 'boltzmann' |
---|
236 | [default='fast'] |
---|
237 | T0 -- Initial Temperature (estimated as 1.2 times the largest |
---|
238 | cost-function deviation over random points in the range) |
---|
239 | [default=None] |
---|
240 | learn_rate -- scale constant for adjusting guesses |
---|
241 | [default=0.5] |
---|
242 | boltzmann -- Boltzmann constant in acceptance test |
---|
243 | (increase for less stringent test at each temperature). |
---|
244 | [default=1.0] |
---|
245 | quench, m, n -- Parameters to alter fast_sa schedule |
---|
246 | [all default to 1.0] |
---|
247 | dwell -- The number of times to search the space at each temperature. |
---|
248 | [default=50] |
---|
249 | |
---|
250 | Optional Termination Conditions: |
---|
251 | Tf -- Final goal temperature |
---|
252 | [default=1e-12] |
---|
253 | maxaccept -- Maximum changes to accept |
---|
254 | [default=None] |
---|
255 | """ |
---|
256 | #allow for inputs that don't conform to AbstractSolver interface |
---|
257 | args = ExtraArgs |
---|
258 | x0 = self.population[0] |
---|
259 | |
---|
260 | schedule = "fast" |
---|
261 | T0 = None |
---|
262 | boltzmann = 1.0 |
---|
263 | learn_rate = 0.5 |
---|
264 | dwell = 50 |
---|
265 | quench = 1.0 |
---|
266 | m = 1.0 |
---|
267 | n = 1.0 |
---|
268 | |
---|
269 | Tf = 1e-12 # or None? |
---|
270 | self._maxaccept = None |
---|
271 | |
---|
272 | self.disp = 0 |
---|
273 | self.callback = None |
---|
274 | |
---|
275 | if kwds.has_key('schedule'): schedule = kwds['schedule'] |
---|
276 | if kwds.has_key('T0'): T0 = kwds['T0'] |
---|
277 | if kwds.has_key('boltzmann'): boltzmann = kwds['boltzmann'] |
---|
278 | if kwds.has_key('learn_rate'): learn_rate = kwds['learn_rate'] |
---|
279 | if kwds.has_key('dwell'): dwell = kwds['dwell'] |
---|
280 | if kwds.has_key('quench'): quench = kwds['quench'] |
---|
281 | if kwds.has_key('m'): m = kwds['m'] |
---|
282 | if kwds.has_key('n'): n = kwds['n'] |
---|
283 | |
---|
284 | if kwds.has_key('Tf'): Tf = kwds['Tf'] |
---|
285 | if kwds.has_key('maxaccept'): self._maxaccept = kwds['maxaccept'] |
---|
286 | |
---|
287 | if kwds.has_key('disp'): self.disp = kwds['disp'] |
---|
288 | if kwds.has_key('callback'): self.callback = kwds['callback'] |
---|
289 | |
---|
290 | #------------------------------------------------------------- |
---|
291 | |
---|
292 | import signal |
---|
293 | self._EARLYEXIT = False |
---|
294 | |
---|
295 | fcalls, func = wrap_function(func, ExtraArgs, EvaluationMonitor) |
---|
296 | |
---|
297 | if self._useStrictRange: |
---|
298 | x0 = self._clipGuessWithinRangeBoundary(x0) |
---|
299 | # Note: wrap_bounds changes the results slightly from the original |
---|
300 | func = wrap_bounds(func, self._strictMin, self._strictMax) |
---|
301 | |
---|
302 | #generate signal_handler |
---|
303 | self._generateHandler(sigint_callback) |
---|
304 | if self._handle_sigint: signal.signal(signal.SIGINT, self.signal_handler) |
---|
305 | #------------------------------------------------------------- |
---|
306 | |
---|
307 | schedule = eval(schedule+'_sa()') |
---|
308 | # initialize the schedule |
---|
309 | schedule.init(dims=shape(x0),func=func,args=args,boltzmann=boltzmann,T0=T0, |
---|
310 | learn_rate=learn_rate, lower=self._strictMin, upper=self._strictMax, |
---|
311 | m=m, n=n, quench=quench, dwell=dwell) |
---|
312 | |
---|
313 | if self._maxiter is None: |
---|
314 | self._maxiter = 400 |
---|
315 | |
---|
316 | current_state, last_state = _state(), _state() |
---|
317 | if T0 is None: |
---|
318 | x0, self.bestEnergy = schedule.getstart_temp() |
---|
319 | self.bestSolution = x0 |
---|
320 | else: |
---|
321 | #self.bestSolution = None |
---|
322 | self.bestSolution = x0 |
---|
323 | self.bestEnergy = 300e8 |
---|
324 | |
---|
325 | retval = 0 |
---|
326 | last_state.x = asarray(x0).copy() |
---|
327 | fval = func(x0,*args) |
---|
328 | schedule.feval += 1 |
---|
329 | last_state.cost = fval |
---|
330 | if last_state.cost < self.bestEnergy: |
---|
331 | self.bestEnergy = fval |
---|
332 | self.bestSolution = asarray(x0).copy() |
---|
333 | schedule.T = schedule.T0 |
---|
334 | fqueue = [100, 300, 500, 700] |
---|
335 | self.population = asarray(fqueue)*1.0 |
---|
336 | iters = 0 |
---|
337 | while 1: |
---|
338 | StepMonitor(self.bestSolution, self.bestEnergy) |
---|
339 | for n in range(dwell): |
---|
340 | current_state.x = schedule.update_guess(last_state.x) |
---|
341 | current_state.cost = func(current_state.x,*args) |
---|
342 | schedule.feval += 1 |
---|
343 | |
---|
344 | dE = current_state.cost - last_state.cost |
---|
345 | if schedule.accept_test(dE): |
---|
346 | last_state.x = current_state.x.copy() |
---|
347 | last_state.cost = current_state.cost |
---|
348 | if last_state.cost < self.bestEnergy: |
---|
349 | self.bestSolution = last_state.x.copy() |
---|
350 | self.bestEnergy = last_state.cost |
---|
351 | schedule.update_temp() |
---|
352 | |
---|
353 | iters += 1 |
---|
354 | fqueue.append(squeeze(last_state.cost)) |
---|
355 | fqueue.pop(0) |
---|
356 | af = asarray(fqueue)*1.0 |
---|
357 | |
---|
358 | # Update monitors/variables |
---|
359 | self.population = af |
---|
360 | self.energy_history.append(self.bestEnergy) |
---|
361 | |
---|
362 | if self.callback is not None: |
---|
363 | self.callback(self.bestSolution) |
---|
364 | |
---|
365 | # Stopping conditions |
---|
366 | # - last saved values of f from each cooling step |
---|
367 | # are all very similar (effectively cooled) |
---|
368 | # - Tf is set and we are below it |
---|
369 | # - maxfun is set and we are past it |
---|
370 | # - maxiter is set and we are past it |
---|
371 | # - maxaccept is set and we are past it |
---|
372 | |
---|
373 | if self._EARLYEXIT or termination(self): |
---|
374 | # How to deal with the below warning? It uses feps, which is passed |
---|
375 | # to termination, so it would be repetitive to also pass it to Solve(). |
---|
376 | #if abs(af[-1]-best_state.cost) > feps*10: |
---|
377 | #print "Warning: Cooled to %f at %s but this is not" \ |
---|
378 | # % (squeeze(last_state.cost), str(squeeze(last_state.x))) \ |
---|
379 | # + " the smallest point found." |
---|
380 | break |
---|
381 | if (Tf is not None) and (schedule.T < Tf): |
---|
382 | break |
---|
383 | # if (self._maxfun is not None) and (schedule.feval > self._maxfun): |
---|
384 | # retval = 1 |
---|
385 | # break |
---|
386 | if (self._maxfun is not None) and (fcalls[0] > self._maxfun): |
---|
387 | retval = 1 |
---|
388 | break |
---|
389 | if (iters > self._maxiter): |
---|
390 | retval = 2 |
---|
391 | break |
---|
392 | if (self._maxaccept is not None) and (schedule.accepted > self._maxaccept): |
---|
393 | break |
---|
394 | |
---|
395 | signal.signal(signal.SIGINT,signal.default_int_handler) |
---|
396 | |
---|
397 | # Store some information. Is there a better way to do this? |
---|
398 | self.generations = iters # Number of iterations |
---|
399 | self.T = schedule.T # Final temperature |
---|
400 | self.accept = schedule.accepted # Number of tests accepted |
---|
401 | |
---|
402 | # code below here pushes output to scipy.optimize interface |
---|
403 | |
---|
404 | if self.disp: |
---|
405 | if retval == 1: |
---|
406 | print "Warning: Maximum number of function evaluations has "\ |
---|
407 | "been exceeded." |
---|
408 | elif retval == 2: |
---|
409 | print "Warning: Maximum number of iterations has been exceeded" |
---|
410 | else: |
---|
411 | print "Optimization terminated successfully." |
---|
412 | print " Current function value: %f" % self.bestEnergy |
---|
413 | print " Iterations: %d" % iters |
---|
414 | print " Function evaluations: %d" % fcalls[0] |
---|
415 | |
---|
416 | return |
---|
417 | |
---|
418 | ################################################################################## |
---|
419 | # function interface for using AnnealSolver |
---|
420 | |
---|
421 | def anneal(func, x0, args=(), schedule='fast', full_output=0, |
---|
422 | T0=None, Tf=1e-12, maxaccept=None, lower= -100, upper= 100, maxiter=400, |
---|
423 | boltzmann=1.0, learn_rate=0.5, feps=1e-6, quench=1.0, m=1.0, n=1.0, |
---|
424 | maxfun=None, dwell=50, callback=None, disp=0, retall=0): |
---|
425 | """Minimize a function using simulated annealing. |
---|
426 | |
---|
427 | Inputs: |
---|
428 | |
---|
429 | func -- Function to be optimized |
---|
430 | x0 -- Parameters to be optimized over. Should be a list. |
---|
431 | |
---|
432 | Optional Inputs: |
---|
433 | |
---|
434 | args -- Extra parameters to function |
---|
435 | schedule -- Annealing schedule to use. |
---|
436 | Choices are: 'fast', 'cauchy', 'boltzmann' |
---|
437 | full_output -- Non-zero to return optional outputs |
---|
438 | retall -- Non-zero to return list of solutions at each iteration. |
---|
439 | disp -- Non-zero to print convergence messages. |
---|
440 | T0 -- Initial Temperature (estimated as 1.2 times the largest |
---|
441 | cost-function deviation over random points in the range) |
---|
442 | Tf -- Final goal temperature |
---|
443 | maxfun -- Maximum function evaluations |
---|
444 | maxaccept -- Maximum changes to accept |
---|
445 | maxiter -- Maximum cooling iterations |
---|
446 | learn_rate -- scale constant for adjusting guesses |
---|
447 | boltzmann -- Boltzmann constant in acceptance test |
---|
448 | (increase for less stringent test at each temperature). |
---|
449 | feps -- Stopping relative error tolerance for the function value in |
---|
450 | last four coolings. |
---|
451 | quench, m, n -- Parameters to alter fast_sa schedule |
---|
452 | lower, upper -- lower and upper bounds on x0. |
---|
453 | dwell -- The number of times to search the space at each temperature. |
---|
454 | |
---|
455 | Outputs: (xmin, {fopt, iters, feval, T, accept}, retval, {allvecs}) |
---|
456 | |
---|
457 | xmin -- Point giving smallest value found |
---|
458 | fopt -- Minimum value of function found |
---|
459 | iters -- Number of cooling iterations |
---|
460 | feval -- Number of function evaluations |
---|
461 | T -- final temperature |
---|
462 | accept -- Number of tests accepted. |
---|
463 | retval -- Flag indicating stopping condition: |
---|
464 | 1 : Maximum function evaluations |
---|
465 | 2 : Maximum iterations reached |
---|
466 | 3 : Maximum accepted query locations reached |
---|
467 | allvecs -- a list of solutions at each iteration. |
---|
468 | """ |
---|
469 | |
---|
470 | from mystic.tools import Sow |
---|
471 | from mystic.termination import AveragePopulationImprovement |
---|
472 | stepmon = Sow() |
---|
473 | evalmon = Sow() |
---|
474 | |
---|
475 | x0 = asarray(x0) |
---|
476 | solver = AnnealSolver(len(x0)) |
---|
477 | solver.SetInitialPoints(x0) |
---|
478 | solver.enable_signal_handler() |
---|
479 | solver.SetEvaluationLimits(maxiter,maxfun) |
---|
480 | |
---|
481 | # Default bounds cause errors when function is not 1-dimensional |
---|
482 | solver.SetStrictRanges(lower,upper) |
---|
483 | |
---|
484 | solver.Solve(func,termination=AveragePopulationImprovement(tolerance=feps),\ |
---|
485 | EvaluationMonitor=evalmon,StepMonitor=stepmon,\ |
---|
486 | ExtraArgs=args, callback=callback,\ |
---|
487 | schedule=schedule, T0=T0, Tf=Tf,\ |
---|
488 | boltzmann=boltzmann, learn_rate=learn_rate,\ |
---|
489 | dwell=dwell, quench=quench, m=m, n=n,\ |
---|
490 | maxaccept=maxaccept, disp=disp) |
---|
491 | solution = solver.Solution() |
---|
492 | |
---|
493 | # code below here pushes output to scipy.optimize interface |
---|
494 | #x = list(solver.bestSolution) |
---|
495 | x = solver.bestSolution |
---|
496 | fval = solver.bestEnergy |
---|
497 | fcalls = len(evalmon.x) |
---|
498 | iterations = len(stepmon.x) |
---|
499 | allvecs = [] |
---|
500 | for i in range(len(stepmon.x)): |
---|
501 | #allvecs.append(list(stepmon.x[i])) |
---|
502 | allvecs.append(stepmon.x[i]) |
---|
503 | |
---|
504 | # to be fixed |
---|
505 | T = solver.T |
---|
506 | accept = solver.accept |
---|
507 | |
---|
508 | retval = 0 |
---|
509 | if (maxfun is not None) and (fcalls > maxfun): |
---|
510 | retval = 1 |
---|
511 | if (iterations > maxiter): |
---|
512 | retval = 2 |
---|
513 | if (maxaccept is not None) and (accept > maxaccept): |
---|
514 | retval = 3 |
---|
515 | |
---|
516 | if full_output: |
---|
517 | retlist = x, fval, iterations, fcalls, T, accept, retval |
---|
518 | if retall: |
---|
519 | retlist += (allvecs,) |
---|
520 | else: |
---|
521 | retlist = x, retval |
---|
522 | if retall: |
---|
523 | retlist = (x, allvecs) |
---|
524 | |
---|
525 | return retlist |
---|
526 | |
---|
527 | if __name__=='__main__': |
---|
528 | help(__name__) |
---|
529 | |
---|
530 | # end of file |
---|