source: branches/alta/mystic-0.1a2/mystic/anneal_solver.py @ 175

Revision 175, 17.7 KB checked in by altafang, 7 years ago (diff)

Editing termination conditions and making various edits to annealing and snobfit

  • Property svn:executable set to *
Line 
1"""
2Solvers
3=======
4
5This module contains an optimization routine based on the simulated
6annealing algorithm. 
7
8A minimal interface that mimics a scipy.optimize interface has been
9implemented, and functionality from the mystic solver API has been added
10with reasonable defaults. 
11
12Minimal function interface to optimization routines::
13    anneal -- Simulated Annealing solver
14
15The corresponding solver built on mystic's AbstractSolver is::
16    AnnealSolver -- a simulated annealing solver
17
18Mystic solver behavior activated in anneal::
19    - EvaluationMonitor = Sow()
20    - StepMonitor = Sow()
21    - enable_signal_handler()
22    - termination = AveragePopulationImprovement(tolerance)
23
24Usage
25=====
26
27See `mystic.examples.test_anneal` for an example of using
28AnnealSolver.
29
30All solvers included in this module provide the standard signal handling.
31For more information, see `mystic.mystic.abstract_solver`.
32
33
34History
35==========
36Based on anneal.py from scipy.optimize:
37Original Author: Travis Oliphant 2002
38Bug-fixes in 2006 by Tim Leslie
39
40Adapted for Mystic, 2009
41"""
42
43__all__ = ['AnnealSolver','anneal']
44
45# Mystic and numpy imports
46from mystic.tools import Null, wrap_function
47from mystic.tools import wrap_bounds
48import numpy
49from numpy import asarray, tan, exp, ones, squeeze, sign, \
50     all, log, sqrt, pi, shape, array, minimum, where
51from numpy import random
52
53from 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
62class 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
141class 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
164class 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
177class 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
191class _state(object):
192    def __init__(self):
193        self.x = None
194        self.cost = None
195
196################################################################################
197
198class AnnealSolver(AbstractSolver):
199    """Simulated annealing optimization."""
200   
201    def __init__(self, dim):
202        """
203Takes one initial input:
204    dim  -- dimensionality of the problem
205
206All 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
214Description:
215
216    Uses a simulated annealing algorithm to find the minimum of
217    a function of one or more variables.
218
219Inputs:
220
221    func -- the Python function or method to be minimized.
222    termination -- callable object providing termination conditions.
223
224Additional 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
233Further 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
421def 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
527if __name__=='__main__':
528    help(__name__)
529
530# end of file
Note: See TracBrowser for help on using the repository browser.