def SetGenerationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each solver iteration input:: - a monitor instance or monitor type used to track (x, f(x)). Any data collected in an existing generation monitor will be prepended, unless new is True.""" from mystic.monitors import Null, Monitor #, CustomMonitor if monitor is None: monitor = Null() current = Null() if new else self._stepmon if current is monitor: current = Null() if isinstance(monitor, Monitor): # is Monitor() self._stepmon = monitor self._stepmon.prepend(current) elif isinstance(monitor, Null) or monitor == Null: # is Null() or Null self._stepmon = Monitor() #XXX: don't allow Null self._stepmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._stepmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) self.energy_history = None # sync with self._stepmon self.solution_history = None # sync with self._stepmon return
def _decorate_objective(self, cost, ExtraArgs=None): """decorate cost function with bounds, penalties, monitors, etc""" #print("@%r %r %r" % (cost, ExtraArgs, max)) raw = cost if ExtraArgs is None: ExtraArgs = () from mystic.python_map import python_map if self._map != python_map: #FIXME: EvaluationMonitor fails for MPI, throws error for 'pp' from mystic.monitors import Null evalmon = Null() else: evalmon = self._evalmon fcalls, cost = wrap_function(cost, ExtraArgs, evalmon) if self._useStrictRange: indx = list(self.popEnergy).index(self.bestEnergy) ngen = self.generations #XXX: no random if generations=0 ? for i in range(self.nPop): self.population[i] = self._clipGuessWithinRangeBoundary( self.population[i], (not ngen) or (i is indx)) cost = wrap_bounds(cost, self._strictMin, self._strictMax) cost = wrap_penalty(cost, self._penalty) if self._reducer: #cost = reduced(*self._reducer)(cost) # was self._reducer = (f,bool) cost = reduced(self._reducer, arraylike=True)(cost) # hold on to the 'wrapped' and 'raw' cost function self._cost = (cost, raw, ExtraArgs) self._live = True return cost
def SetEvaluationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each cost function evaluation""" from mystic.monitors import Null, Monitor#, CustomMonitor if monitor is None: monitor = Null() current = Null() if new else self._evalmon if current is monitor: current = Null() if isinstance(monitor, (Null, Monitor) ): # is Monitor() or Null() self._evalmon = monitor self._evalmon.prepend(current) elif monitor == Null: # is Null self._evalmon = monitor() self._evalmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._evalmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) return
def __init__(self, bounds, model, npts=None, **kwds): """ Inputs: bounds -- list[tuples]: (lower,upper) bound for each model input model -- function: y = model(x), where x is an input parameter vector npts -- int: number of points to sample the model NOTE: additional keywords (evalmon, stepmon, maxiter, maxfun, dist, tight, saveiter, state, termination, constraints, penalty, reducer, solver) are available for use. See mystic.ensemble for more details. """ self._bounds = bounds self._model = model #FIXME: if None, interpolate self._npts = npts self._evalmon = kwds.pop('evalmon', None) if self._evalmon is None: from mystic.monitors import Null self._evalmon = Null() from collections import defaultdict self._kwds = defaultdict(lambda: None) self._kwds.update(kwds) s = self._init_solver() s.SetStrictRanges(*zip(*bounds), tight=self._kwds['tight']) #s.SetObjective(memo) #model) #XXX: ExtraArgs: axis ??? s.SetObjective(model) #FIXME: ensure cached model # apply additional kwds solver = self._kwds['solver'] if solver is not None: s.SetNestedSolver(solver) s.SetDistribution(self._kwds['dist']) s.SetEvaluationLimits(self._kwds['maxiter'], self._kwds['maxfun']) s.SetSaveFrequency(self._kwds['saveiter'], self._kwds['state']) termination = self._kwds['termination'] if termination is not None: s.SetTermination(termination) #XXX:? s.SetConstraints(self._kwds['constraints']) s.SetPenalty(self._kwds['penalty']) s.SetReducer(self._kwds['reducer'], arraylike=True) s.SetGenerationMonitor(self._kwds['stepmon']) #XXX: use self._stepmon? # pass a copy of the monitor to all instances import copy m = copy.deepcopy(self._evalmon) #XXX: no change with direct reference s.SetEvaluationMonitor(m) self._sampler = s self._npts = s._npts self._evals = [0] * s._npts #XXX: collect count or monitor? self._iters = [0] * s._npts #XXX: collect count or monitor? return
def SetEvaluationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each cost function evaluation input:: - a monitor instance or monitor type used to track (x, f(x)). Any data collected in an existing evaluation monitor will be prepended, unless new is True.""" from mystic.monitors import Null, Monitor #, CustomMonitor if monitor is None: monitor = Null() current = Null() if new else self._evalmon if current is monitor: current = Null() if isinstance(monitor, (Null, Monitor)): # is Monitor() or Null() self._evalmon = monitor self._evalmon.prepend(current) elif monitor == Null: # is Null self._evalmon = monitor() self._evalmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._evalmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) return
def SetGenerationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each solver iteration""" from mystic.monitors import Null, Monitor #, CustomMonitor current = Null() if new else self._stepmon if isinstance(monitor, Monitor): # is Monitor() self._stepmon = monitor self._stepmon.prepend(current) elif isinstance(monitor, Null) or monitor == Null: # is Null() or Null self._stepmon = Monitor() #XXX: don't allow Null self._stepmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._stepmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) self.energy_history = None # sync with self._stepmon self.solution_history = None # sync with self._stepmon return
def __init__(self, dim, **kwds): """ Takes one initial input:: dim -- dimensionality of the problem. Additional inputs:: npop -- size of the trial solution population. [default = 1] Important class members:: nDim, nPop = dim, npop generations - an iteration counter. evaluations - an evaluation counter. bestEnergy - current best energy. bestSolution - current best parameter set. [size = dim] popEnergy - set of all trial energy solutions. [size = npop] population - set of all trial parameter solutions. [size = dim*npop] solution_history - history of bestSolution status. [StepMonitor.x] energy_history - history of bestEnergy status. [StepMonitor.y] signal_handler - catches the interrupt signal. """ NP = kwds['npop'] if 'npop' in kwds else 1 self.nDim = dim self.nPop = NP self._init_popEnergy = inf self.popEnergy = [self._init_popEnergy] * NP self.population = [[0.0 for i in range(dim)] for j in range(NP)] self.trialSolution = [0.0] * dim self._map_solver = False self._bestEnergy = None self._bestSolution = None self._state = None self._type = self.__class__.__name__ self.sigint_callback = None self._handle_sigint = False self._useStrictRange = False self._defaultMin = [-1e3] * dim self._defaultMax = [1e3] * dim self._strictMin = [] self._strictMax = [] self._maxiter = None self._maxfun = None self._saveiter = None #self._saveeval = None from mystic.monitors import Null, Monitor self._evalmon = Null() self._stepmon = Monitor() self._fcalls = [0] self._energy_history = None self._solution_history = None self.id = None # identifier (use like "rank" for MPI) self._constraints = lambda x: x self._penalty = lambda x: 0.0 self._reducer = None self._cost = (None, None, None) # (cost, raw_cost, args) #,callback) self._collapse = False self._termination = lambda x, *ar, **kw: False if len(ar) < 1 or ar[ 0] is False or (kw['info'] if 'info' in kw else True ) == False else '' #XXX: better default ? # (get termination details with self._termination.__doc__) import mystic.termination as mt self._EARLYEXIT = mt.EARLYEXIT self._live = False return
class AbstractSolver(object): """AbstractSolver base class for mystic optimizers. """ def __init__(self, dim, **kwds): """ Takes one initial input:: dim -- dimensionality of the problem. Additional inputs:: npop -- size of the trial solution population. [default = 1] Important class members:: nDim, nPop = dim, npop generations - an iteration counter. evaluations - an evaluation counter. bestEnergy - current best energy. bestSolution - current best parameter set. [size = dim] popEnergy - set of all trial energy solutions. [size = npop] population - set of all trial parameter solutions. [size = dim*npop] solution_history - history of bestSolution status. [StepMonitor.x] energy_history - history of bestEnergy status. [StepMonitor.y] signal_handler - catches the interrupt signal. """ NP = kwds['npop'] if 'npop' in kwds else 1 self.nDim = dim self.nPop = NP self._init_popEnergy = inf self.popEnergy = [self._init_popEnergy] * NP self.population = [[0.0 for i in range(dim)] for j in range(NP)] self.trialSolution = [0.0] * dim self._map_solver = False self._bestEnergy = None self._bestSolution = None self._state = None self._type = self.__class__.__name__ self.sigint_callback = None self._handle_sigint = False self._useStrictRange = False self._defaultMin = [-1e3] * dim self._defaultMax = [1e3] * dim self._strictMin = [] self._strictMax = [] self._maxiter = None self._maxfun = None self._saveiter = None #self._saveeval = None from mystic.monitors import Null, Monitor self._evalmon = Null() self._stepmon = Monitor() self._fcalls = [0] self._energy_history = None self._solution_history = None self.id = None # identifier (use like "rank" for MPI) self._constraints = lambda x: x self._penalty = lambda x: 0.0 self._reducer = None self._cost = (None, None, None) # (cost, raw_cost, args) #,callback) self._collapse = False self._termination = lambda x, *ar, **kw: False if len(ar) < 1 or ar[ 0] is False or (kw['info'] if 'info' in kw else True ) == False else '' #XXX: better default ? # (get termination details with self._termination.__doc__) import mystic.termination as mt self._EARLYEXIT = mt.EARLYEXIT self._live = False return def Solution(self): """return the best solution""" return self.bestSolution def __evaluations(self): """get the number of function calls""" return self._fcalls[0] def __generations(self): """get the number of iterations""" return max(0, len(self._stepmon) - 1) def __energy_history(self): """get the energy_history (default: energy_history = _stepmon._y)""" if self._energy_history is None: return self._stepmon._y return self._energy_history def __set_energy_history(self, energy): """set the energy_history (energy=None will sync with _stepmon._y)""" self._energy_history = energy return def __solution_history(self): """get the solution_history (default: solution_history = _stepmon.x)""" if self._solution_history is None: return self._stepmon.x return self._solution_history def __set_solution_history(self, params): """set the solution_history (params=None will sync with _stepmon.x)""" self._solution_history = params return def __bestSolution(self): """get the bestSolution (default: bestSolution = population[0])""" if self._bestSolution is None: return self.population[0] return self._bestSolution def __set_bestSolution(self, params): """set the bestSolution (params=None will sync with population[0])""" self._bestSolution = params return def __bestEnergy(self): """get the bestEnergy (default: bestEnergy = popEnergy[0])""" if self._bestEnergy is None: return self.popEnergy[0] return self._bestEnergy def __set_bestEnergy(self, energy): """set the bestEnergy (energy=None will sync with popEnergy[0])""" self._bestEnergy = energy return def SetReducer(self, reducer, arraylike=False): """apply a reducer function to the cost function input:: - a reducer function of the form: y' = reducer(yk), where yk is a results vector and y' is a single value. Ideally, this method is applied to a cost function with a multi-value return, to reduce the output to a single value. If arraylike, the reducer provided should take a single array as input and produce a scalar; otherwise, the reducer provided should meet the requirements of the python's builtin 'reduce' method (e.g. lambda x,y: x+y), taking two scalars and producing a scalar.""" if not reducer: self._reducer = None elif not isinstance(reducer, collections.Callable): raise TypeError("'%s' is not a callable function" % reducer) elif not arraylike: self._reducer = wrap_reducer(reducer) else: #XXX: check if is arraylike? self._reducer = reducer return self._update_objective() def SetPenalty(self, penalty): """apply a penalty function to the optimization input:: - a penalty function of the form: y' = penalty(xk), with y = cost(xk) + y', where xk is the current parameter vector. Ideally, this function is constructed so a penalty is applied when the desired (i.e. encoded) constraints are violated. Equality constraints should be considered satisfied when the penalty condition evaluates to zero, while inequality constraints are satisfied when the penalty condition evaluates to a non-positive number.""" if not penalty: self._penalty = lambda x: 0.0 elif not isinstance(penalty, collections.Callable): raise TypeError("'%s' is not a callable function" % penalty) else: #XXX: check for format: y' = penalty(x) ? self._penalty = penalty return self._update_objective() def SetConstraints(self, constraints): """apply a constraints function to the optimization input:: - a constraints function of the form: xk' = constraints(xk), where xk is the current parameter vector. Ideally, this function is constructed so the parameter vector it passes to the cost function will satisfy the desired (i.e. encoded) constraints.""" if not constraints: self._constraints = lambda x: x elif not isinstance(constraints, collections.Callable): raise TypeError("'%s' is not a callable function" % constraints) else: #XXX: check for format: x' = constraints(x) ? self._constraints = constraints return self._update_objective() def SetGenerationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each solver iteration""" from mystic.monitors import Null, Monitor #, CustomMonitor current = Null() if new else self._stepmon if isinstance(monitor, Monitor): # is Monitor() self._stepmon = monitor self._stepmon.prepend(current) elif isinstance(monitor, Null) or monitor == Null: # is Null() or Null self._stepmon = Monitor() #XXX: don't allow Null self._stepmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._stepmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) self.energy_history = None # sync with self._stepmon self.solution_history = None # sync with self._stepmon return def SetEvaluationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each cost function evaluation""" from mystic.monitors import Null, Monitor #, CustomMonitor current = Null() if new else self._evalmon if isinstance(monitor, (Null, Monitor)): # is Monitor() or Null() self._evalmon = monitor self._evalmon.prepend(current) elif monitor == Null: # is Null self._evalmon = monitor() self._evalmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._evalmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) return def SetStrictRanges(self, min=None, max=None): """ensure solution is within bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i] note:: SetStrictRanges(None) will remove strict range constraints""" if min is False or max is False: self._useStrictRange = False return self._update_objective() #XXX: better to use 'defaultMin,defaultMax' or '-inf,inf' ??? if min is None: min = self._defaultMin if max is None: max = self._defaultMax # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] is None: min[i] = self._defaultMin[0] if max[i] is None: max[i] = self._defaultMax[0] min = asarray(min) max = asarray(max) if numpy.any((min > max), 0): raise ValueError("each min[i] must be <= the corresponding max[i]") if len(min) != self.nDim: raise ValueError("bounds array must be length %s" % self.nDim) self._useStrictRange = True self._strictMin = min self._strictMax = max return self._update_objective() def _clipGuessWithinRangeBoundary(self, x0, at=True): """ensure that initial guess is set within bounds input:: - x0: must be a sequence of length self.nDim""" #if len(x0) != self.nDim: #XXX: unnecessary w/ self.trialSolution # raise ValueError, "initial guess must be length %s" % self.nDim x0 = asarray(x0) bounds = (self._strictMin, self._strictMax) if not len(self._strictMin): return x0 # clip x0 at bounds settings = numpy.seterr(all='ignore') x_ = x0.clip(*bounds) numpy.seterr(**settings) if at: return x_ # clip x0 within bounds x_ = x_ != x0 x0[x_] = random.uniform(self._strictMin, self._strictMax)[x_] return x0 def SetInitialPoints(self, x0, radius=0.05): """Set Initial Points with Guess (x0) input:: - x0: must be a sequence of length self.nDim - radius: generate random points within [-radius*x0, radius*x0] for i!=0 when a simplex-type initial guess in required""" x0 = asfarray(x0) rank = len(x0.shape) if rank is 0: x0 = asfarray([x0]) rank = 1 if not -1 < rank < 2: raise ValueError( "Initial guess must be a scalar or rank-1 sequence.") if len(x0) != self.nDim: raise ValueError("Initial guess must be length %s" % self.nDim) #slightly alter initial values for solvers that depend on randomness min = x0 * (1 - radius) max = x0 * (1 + radius) numzeros = len(x0[x0 == 0]) min[min == 0] = asarray([-radius for i in range(numzeros)]) max[max == 0] = asarray([radius for i in range(numzeros)]) self.SetRandomInitialPoints(min, max) #stick initial values in population[i], i=0 self.population[0] = x0.tolist() def SetRandomInitialPoints(self, min=None, max=None): """Generate Random Initial Points within given Bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i]""" if min is None: min = self._defaultMin if max is None: max = self._defaultMax #if numpy.any(( asarray(min) > asarray(max) ),0): # raise ValueError, "each min[i] must be <= the corresponding max[i]" if len(min) != self.nDim or len(max) != self.nDim: raise ValueError("bounds array must be length %s" % self.nDim) # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] is None: min[i] = self._defaultMin[0] if max[i] is None: max[i] = self._defaultMax[0] #generate random initial values for i in range(len(self.population)): for j in range(self.nDim): self.population[i][j] = random.uniform(min[j], max[j]) def SetMultinormalInitialPoints(self, mean, var=None): """Generate Initial Points from Multivariate Normal. input:: - mean must be a sequence of length self.nDim - var can be... None: -> it becomes the identity scalar: -> var becomes scalar * I matrix: -> the variance matrix. must be the right size! """ from mystic.tools import random_state rng = random_state(module='numpy.random') assert (len(mean) == self.nDim) if var is None: var = numpy.eye(self.nDim) else: try: # scalar ? float(var) except: # nope. var better be matrix of the right size (no check) pass else: var = var * numpy.eye(self.nDim) for i in range(len(self.population)): self.population[i] = rng.multivariate_normal(mean, var).tolist() return def SetSampledInitialPoints(self, dist=None): """Generate Random Initial Points from Distribution (dist) input:: - dist: a mystic.math.Distribution instance """ from mystic.math import Distribution _dist = Distribution() if dist is None: dist = _dist elif type(_dist) not in dist.__class__.mro(): dist = Distribution(dist) #XXX: or throw error? for i in range(self.nPop): self.population[i] = dist(self.nDim) return def enable_signal_handler(self): #, callback='*'): """enable workflow interrupt handler while solver is running""" """ #XXX: disabled, as would add state to solver input:: - if a callback function is provided, generate a new handler with the given callback. If callback is None, do not use a callback. If callback is not provided, just turn on the existing handler. """ ## always _generate handler on first call #if (self.signal_handler is None) and callback == '*': # callback = None ## when a new callback is given, generate a new handler #if callback != '*': # self._generateHandler(callback) self._handle_sigint = True def disable_signal_handler(self): """disable workflow interrupt handler while solver is running""" self._handle_sigint = False def SetSaveFrequency(self, generations=None, filename=None, **kwds): """set frequency for saving solver restart file input:: - generations = number of solver iterations before next save of state - filename = name of file in which to save solver state note:: SetSaveFrequency(None) will disable saving solver restart file""" self._saveiter = generations #self._saveeval = evaluations self._state = filename return def SetEvaluationLimits(self, generations=None, evaluations=None, \ new=False, **kwds): """set limits for generations and/or evaluations input:: - generations = maximum number of solver iterations (i.e. steps) - evaluations = maximum number of function evaluations""" # backward compatibility self._maxiter = kwds['maxiter'] if 'maxiter' in kwds else generations self._maxfun = kwds['maxfun'] if 'maxfun' in kwds else evaluations # handle if new (reset counter, instead of extend counter) if new: if generations is not None: self._maxiter += self.generations else: self._maxiter = "*" #XXX: better as self._newmax = True ? if evaluations is not None: self._maxfun += self.evaluations else: self._maxfun = "*" return def _SetEvaluationLimits(self, iterscale=None, evalscale=None): """set the evaluation limits""" if iterscale is None: iterscale = 10 if evalscale is None: evalscale = 1000 N = len(self.population[0]) # usually self.nDim # if SetEvaluationLimits not applied, use the solver default if self._maxiter is None: self._maxiter = N * self.nPop * iterscale elif self._maxiter == "*": # (i.e. None, but 'reset counter') self._maxiter = (N * self.nPop * iterscale) + self.generations if self._maxfun is None: self._maxfun = N * self.nPop * evalscale elif self._maxfun == "*": self._maxfun = (N * self.nPop * evalscale) + self.evaluations return def Terminated(self, disp=False, info=False, termination=None): """check if the solver meets the given termination conditions Input:: - disp = if True, print termination statistics and/or warnings - info = if True, return termination message (instead of boolean) - termination = termination conditions to check against Notes:: If no termination conditions are given, the solver's stored termination conditions will be used. """ if termination is None: termination = self._termination # ensure evaluation limits have been imposed self._SetEvaluationLimits() # check for termination messages msg = termination(self, info=True) sig = "SolverInterrupt with %s" % {} lim = "EvaluationLimits with %s" % { 'evaluations': self._maxfun, 'generations': self._maxiter } # push solver internals to scipy.optimize.fmin interface if self._fcalls[0] >= self._maxfun and self._maxfun is not None: msg = lim #XXX: prefer the default stop ? if disp: print("Warning: Maximum number of function evaluations has "\ "been exceeded.") elif self.generations >= self._maxiter and self._maxiter is not None: msg = lim #XXX: prefer the default stop ? if disp: print( "Warning: Maximum number of iterations has been exceeded") elif self._EARLYEXIT: msg = sig if disp: print( "Warning: Optimization terminated with signal interrupt.") elif msg and disp: print("Optimization terminated successfully.") print(" Current function value: %f" % self.bestEnergy) print(" Iterations: %d" % self.generations) print(" Function evaluations: %d" % self._fcalls[0]) if info: return msg return bool(msg) def SetTermination(self, termination): # disp ? """set the termination conditions""" #XXX: validate that termination is a 'condition' ? self._termination = termination self._collapse = False if termination is not None: from mystic.termination import state stop = state(termination) stop = getattr(stop, 'iterkeys', stop.keys)() self._collapse = any(key.startswith('Collapse') for key in stop) return def SetObjective(self, cost, ExtraArgs=None): # callback=None/False ? """decorate the cost function with bounds, penalties, monitors, etc""" _cost, _raw, _args = self._cost # check if need to 'wrap' or can return the stored cost if (cost is None or cost is _raw or cost is _cost) and \ (ExtraArgs is None or ExtraArgs is _args): return # get cost and args if None was given if cost is None: cost = _raw args = _args if ExtraArgs is None else ExtraArgs args = () if args is None else args # quick validation check (so doesn't screw up internals) if not isvalid(cost, [0] * self.nDim, *args): try: name = cost.__name__ except AttributeError: # raise new error for non-callables cost(*args) validate(cost, None, *args) #val = len(args) + 1 #XXX: 'klepto.validate' for better error? #msg = '%s() invalid number of arguments (%d given)' % (name, val) #raise TypeError(msg) # hold on to the 'raw' cost function self._cost = (None, cost, ExtraArgs) self._live = False return def Collapsed(self, disp=False, info=False): """check if the solver meets the given collapse conditions Input:: - disp = if True, print details about the solver state at collapse - info = if True, return collapsed state (instead of boolean) """ stop = getattr(self, '__stop__', self.Terminated(info=True)) import mystic.collapse as ct collapses = ct.collapsed(stop) or dict() if collapses and disp: for (k, v) in getattr(collapses, 'iteritems', collapses.items)(): print(" %s: %s" % (k.split()[0], v)) #print("# Collapse at: Generation", self._stepmon._step-1, \ # "with", self.bestEnergy, "@\n#", list(self.bestSolution)) return collapses if info else bool(collapses) def Collapse(self, disp=False): """if solver has terminated by collapse, apply the collapse""" collapses = self.Collapsed(disp=disp, info=True) if collapses: # then stomach a bunch of module imports (yuck) import mystic.tools as to import mystic.termination as mt import mystic.constraints as cn import mystic.mask as ma # get collapse conditions #XXX: efficient? 4x loops over collapses state = mt.state(self._termination) npts = getattr(self._stepmon, '_npts', None) #XXX: default? #conditions = [cn.impose_at(*to.select_params(self,collapses[k])) if state[k].get('target') is None else cn.impose_at(collapses[k],state[k].get('target')) for k in collapses if k.startswith('CollapseAt')] #conditions += [cn.impose_as(collapses[k],state[k].get('offset')) for k in collapses if k.startswith('CollapseAs')] conditions = [] _conditions = [] for k in collapses: if k.startswith('CollapseAt'): t = state[k] t = t['target'] if 'target' in t else None if t is None: t = cn.impose_at(*to.select_params(self, collapses[k])) else: t = cn.impose_at(collapses[k], t) conditions.append(t) elif k.startswith('CollapseAs'): t = state[k] t = t['offset'] if 'offset' in t else None _conditions.append(cn.impose_as(collapses[k], t)) conditions.extend(_conditions) del _conditions # get measure collapse conditions if npts: #XXX: faster/better if comes first or last? conditions += [ cn.impose_measure(npts, [ collapses[k] for k in collapses if k.startswith('CollapsePosition') ], [ collapses[k] for k in collapses if k.startswith('CollapseWeight') ]) ] # update termination and constraints in solver constraints = to.chain(*conditions)(self._constraints) termination = ma.update_mask(self._termination, collapses) self.SetConstraints(constraints) self.SetTermination(termination) #print(mt.state(self._termination).keys()) return collapses def _update_objective(self): """decorate the cost function with bounds, penalties, monitors, etc""" # rewrap the cost if the solver has been run if False: # trigger immediately self._decorate_objective(*self._cost[1:]) else: # delay update until _bootstrap self.Finalize() return def _decorate_objective(self, cost, ExtraArgs=None): """decorate the cost function with bounds, penalties, monitors, etc""" #print("@%r %r %r" % (cost, ExtraArgs, max)) raw = cost if ExtraArgs is None: ExtraArgs = () self._fcalls, cost = wrap_function(cost, ExtraArgs, self._evalmon) if self._useStrictRange: indx = list(self.popEnergy).index(self.bestEnergy) ngen = self.generations #XXX: no random if generations=0 ? for i in range(self.nPop): self.population[i] = self._clipGuessWithinRangeBoundary( self.population[i], (not ngen) or (i is indx)) cost = wrap_bounds(cost, self._strictMin, self._strictMax) cost = wrap_penalty(cost, self._penalty) cost = wrap_nested(cost, self._constraints) if self._reducer: #cost = reduced(*self._reducer)(cost) # was self._reducer = (f,bool) cost = reduced(self._reducer, arraylike=True)(cost) # hold on to the 'wrapped' and 'raw' cost function self._cost = (cost, raw, ExtraArgs) self._live = True return cost def _bootstrap_objective(self, cost=None, ExtraArgs=None): """HACK to enable not explicitly calling _decorate_objective""" _cost, _raw, _args = self._cost # check if need to 'wrap' or can return the stored cost if (cost is None or cost is _raw or cost is _cost) and \ (ExtraArgs is None or ExtraArgs is _args) and self._live: return _cost # 'wrap' the 'new' cost function with _decorate self.SetObjective(cost, ExtraArgs) return self._decorate_objective(*self._cost[1:]) #XXX: when _decorate called, solver._fcalls will be reset ? def _Step(self, cost=None, ExtraArgs=None, **kwds): """perform a single optimization iteration *** this method must be overwritten ***""" raise NotImplementedError("an optimization algorithm was not provided") def SaveSolver(self, filename=None, **kwds): """save solver state to a restart file""" import dill fd = None if filename is None: # then check if already has registered file if self._state is None: # then create a new one import os, tempfile fd, self._state = tempfile.mkstemp(suffix='.pkl') os.close(fd) filename = self._state self._state = filename f = open(filename, 'wb') try: dill.dump(self, f, **kwds) self._stepmon.info('DUMPED("%s")' % filename) #XXX: before / after ? finally: f.close() return def __save_state(self, force=False): """save the solver state, if chosen save frequency is met""" # save the last iteration if force and bool(self._state): self.SaveSolver() return # save the zeroth iteration nonzero = True #XXX: or bool(self.generations) ? # after _saveiter generations, then save state iters = self._saveiter saveiter = bool(iters) and not bool(self.generations % iters) if nonzero and saveiter: self.SaveSolver() #FIXME: if _saveeval (or more) since last check, then save state #save = self.evaluations % self._saveeval return def __load_state(self, solver, **kwds): """load solver.__dict__ into self.__dict__; override with kwds""" #XXX: should do some filtering on kwds ? self.__dict__.update(solver.__dict__, **kwds) return def Finalize(self, **kwds): """cleanup upon exiting the main optimization loop""" self._live = False return def _process_inputs(self, kwds): """process and activate input settings""" #allow for inputs that don't conform to AbstractSolver interface #NOTE: not sticky: callback, disp #NOTE: sticky: EvaluationMonitor, StepMonitor, penalty, constraints settings = \ {'callback':None, #user-supplied function, called after each step 'disp':0} #non-zero to print convergence messages [settings.update({i: j}) for (i, j) in kwds.items() if i in settings] # backward compatibility if 'EvaluationMonitor' in kwds: \ self.SetEvaluationMonitor(kwds['EvaluationMonitor']) if 'StepMonitor' in kwds: \ self.SetGenerationMonitor(kwds['StepMonitor']) if 'penalty' in kwds: \ self.SetPenalty(kwds['penalty']) if 'constraints' in kwds: \ self.SetConstraints(kwds['constraints']) return settings def Step(self, cost=None, termination=None, ExtraArgs=None, **kwds): """Take a single optimization step using the given 'cost' function. Uses an optimization algorithm to take one 'step' toward the minimum of a function of one or more variables. Args: cost (func, default=None): the function to be minimized: ``y = cost(x)``. termination (termination, default=None): termination conditions. ExtraArgs (tuple, default=None): extra arguments for cost. callback (func, default=None): function to call after each iteration. The interface is ``callback(xk)``, with xk the current parameter vector. disp (bool, default=False): if True, print convergence messages. Returns: None Notes: To run the solver until termination, call ``Solve()``. Alternately, use ``Terminated()`` as the stop condition in a while loop over ``Step``. If the algorithm does not meet the given termination conditions after the call to ``Step``, the solver may be left in an "out-of-sync" state. When abandoning an non-terminated solver, one should call ``Finalize()`` to make sure the solver is fully returned to a "synchronized" state. """ if 'disp' in kwds: disp = kwds['disp'] del kwds['disp'] else: disp = False # register: cost, termination, ExtraArgs cost = self._bootstrap_objective(cost, ExtraArgs) if termination is not None: self.SetTermination(termination) # check termination before 'stepping' if len(self._stepmon): msg = self.Terminated(disp=disp, info=True) or None else: msg = None # if not terminated, then take a step if msg is None: self._Step(**kwds) #FIXME: not all kwds are given in __doc__ if self.Terminated(): # then cleanup/finalize self.Finalize() # get termination message and log state msg = self.Terminated(disp=disp, info=True) or None if msg: self._stepmon.info('STOP("%s")' % msg) self.__save_state(force=True) return msg def Solve(self, cost=None, termination=None, ExtraArgs=None, **kwds): """Minimize a 'cost' function with given termination conditions. Uses an optimization algorithm to find the minimum of a function of one or more variables. Args: cost (func, default=None): the function to be minimized: ``y = cost(x)``. termination (termination, default=None): termination conditions. ExtraArgs (tuple, default=None): extra arguments for cost. sigint_callback (func, default=None): callback function for signal handler. callback (func, default=None): function to call after each iteration. The interface is ``callback(xk)``, with xk the current parameter vector. disp (bool, default=False): if True, print convergence messages. Returns: None """ # process and activate input settings if 'sigint_callback' in kwds: self.sigint_callback = kwds['sigint_callback'] del kwds['sigint_callback'] else: self.sigint_callback = None settings = self._process_inputs(kwds) disp = settings['disp'] if 'disp' in settings else False # set up signal handler self._EARLYEXIT = False #XXX: why not use EARLYEXIT singleton? # activate signal handler #import threading as thread #mainthread = isinstance(thread.current_thread(), thread._MainThread) #if mainthread: #XXX: if not mainthread, signal will raise ValueError import mystic._signal as signal if self._handle_sigint: signal.signal(signal.SIGINT, signal.Handler(self)) # register: cost, termination, ExtraArgs cost = self._bootstrap_objective(cost, ExtraArgs) if termination is not None: self.SetTermination(termination) #XXX: self.Step(cost, termination, ExtraArgs, **settings) ? # the main optimization loop stop = False while not stop: stop = self.Step(**settings) #XXX: remove need to pass settings? continue # if collapse, then activate any relevant collapses and continue self.__stop__ = stop #HACK: avoid re-evaluation of Termination while self._collapse and self.Collapse(disp=disp): del self.__stop__ #HACK stop = False while not stop: stop = self.Step(** settings) #XXX: move Collapse inside of Step? continue self.__stop__ = stop #HACK del self.__stop__ #HACK # restore default handler for signal interrupts if self._handle_sigint: signal.signal(signal.SIGINT, signal.default_int_handler) return def __copy__(self): cls = self.__class__ result = cls.__new__(cls) result.__dict__.update(self.__dict__) return result def __deepcopy__(self, memo): import copy import dill cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): if v is self._cost: setattr(result, k, tuple(dill.copy(i) for i in v)) else: try: #XXX: work-around instancemethods in python2.6 setattr(result, k, copy.deepcopy(v, memo)) except TypeError: setattr(result, k, dill.copy(v)) return result # extensions to the solver interface evaluations = property(__evaluations) generations = property(__generations) energy_history = property(__energy_history, __set_energy_history) solution_history = property(__solution_history, __set_solution_history) bestEnergy = property(__bestEnergy, __set_bestEnergy) bestSolution = property(__bestSolution, __set_bestSolution) pass
def __init__(self, dim, **kwds): """ Takes one initial input: dim -- dimensionality of the problem. Additional inputs: npop -- size of the trial solution population. [default = 1] Important class members: nDim, nPop = dim, npop generations - an iteration counter. evaluations - an evaluation counter. bestEnergy - current best energy. bestSolution - current best parameter set. [size = dim] popEnergy - set of all trial energy solutions. [size = npop] population - set of all trial parameter solutions. [size = dim*npop] solution_history - history of bestSolution status. [StepMonitor.x] energy_history - history of bestEnergy status. [StepMonitor.y] signal_handler - catches the interrupt signal. """ NP = 1 if kwds.has_key('npop'): NP = kwds['npop'] self.nDim = dim self.nPop = NP self._init_popEnergy = inf self.popEnergy = [self._init_popEnergy] * NP self.population = [[0.0 for i in range(dim)] for j in range(NP)] self.trialSolution = [0.0] * dim self._map_solver = False self._bestEnergy = None self._bestSolution = None self._state = None self._type = self.__class__.__name__ self.signal_handler = None self._handle_sigint = False self._useStrictRange = False self._defaultMin = [-1e3] * dim self._defaultMax = [ 1e3] * dim self._strictMin = [] self._strictMax = [] self._maxiter = None self._maxfun = None self._saveiter = None #self._saveeval = None from mystic.monitors import Null, Monitor self._evalmon = Null() self._stepmon = Monitor() self._fcalls = [0] self._energy_history = None self._solution_history= None self.id = None # identifier (use like "rank" for MPI) self._constraints = lambda x: x self._penalty = lambda x: 0.0 self._reducer = None self._cost = (None, None) self._termination = lambda x, *ar, **kw: False if len(ar) < 1 or ar[0] is False or kw.get('info',True) == False else '' #XXX: better default ? # (get termination details with self._termination.__doc__) import mystic.termination self._EARLYEXIT = mystic.termination.EARLYEXIT return
class AbstractSolver(object): """ AbstractSolver base class for mystic optimizers. """ def __init__(self, dim, **kwds): """ Takes one initial input: dim -- dimensionality of the problem. Additional inputs: npop -- size of the trial solution population. [default = 1] Important class members: nDim, nPop = dim, npop generations - an iteration counter. evaluations - an evaluation counter. bestEnergy - current best energy. bestSolution - current best parameter set. [size = dim] popEnergy - set of all trial energy solutions. [size = npop] population - set of all trial parameter solutions. [size = dim*npop] solution_history - history of bestSolution status. [StepMonitor.x] energy_history - history of bestEnergy status. [StepMonitor.y] signal_handler - catches the interrupt signal. """ NP = 1 if kwds.has_key('npop'): NP = kwds['npop'] self.nDim = dim self.nPop = NP self._init_popEnergy = inf self.popEnergy = [self._init_popEnergy] * NP self.population = [[0.0 for i in range(dim)] for j in range(NP)] self.trialSolution = [0.0] * dim self._map_solver = False self._bestEnergy = None self._bestSolution = None self._state = None self._type = self.__class__.__name__ self.signal_handler = None self._handle_sigint = False self._useStrictRange = False self._defaultMin = [-1e3] * dim self._defaultMax = [ 1e3] * dim self._strictMin = [] self._strictMax = [] self._maxiter = None self._maxfun = None self._saveiter = None #self._saveeval = None from mystic.monitors import Null, Monitor self._evalmon = Null() self._stepmon = Monitor() self._fcalls = [0] self._energy_history = None self._solution_history= None self.id = None # identifier (use like "rank" for MPI) self._constraints = lambda x: x self._penalty = lambda x: 0.0 self._reducer = None self._cost = (None, None) self._termination = lambda x, *ar, **kw: False if len(ar) < 1 or ar[0] is False or kw.get('info',True) == False else '' #XXX: better default ? # (get termination details with self._termination.__doc__) import mystic.termination self._EARLYEXIT = mystic.termination.EARLYEXIT return def Solution(self): """return the best solution""" return self.bestSolution def __evaluations(self): """get the number of function calls""" return self._fcalls[0] def __generations(self): """get the number of iterations""" return max(0,len(self.energy_history)-1) #return max(0,len(self._stepmon)-1) def __energy_history(self): """get the energy_history (default: energy_history = _stepmon.y)""" if self._energy_history is None: return self._stepmon.y return self._energy_history def __set_energy_history(self, energy): """set the energy_history (energy=None will sync with _stepmon.y)""" self._energy_history = energy return def __solution_history(self): """get the solution_history (default: solution_history = _stepmon.x)""" if self._solution_history is None: return self._stepmon.x return self._solution_history def __set_solution_history(self, params): """set the solution_history (params=None will sync with _stepmon.x)""" self._solution_history = params return def __bestSolution(self): """get the bestSolution (default: bestSolution = population[0])""" if self._bestSolution is None: return self.population[0] return self._bestSolution def __set_bestSolution(self, params): """set the bestSolution (params=None will sync with population[0])""" self._bestSolution = params return def __bestEnergy(self): """get the bestEnergy (default: bestEnergy = popEnergy[0])""" if self._bestEnergy is None: return self.popEnergy[0] return self._bestEnergy def __set_bestEnergy(self, energy): """set the bestEnergy (energy=None will sync with popEnergy[0])""" self._bestEnergy = energy return def SetReducer(self, reducer, arraylike=False): """apply a reducer function to the cost function input:: - a reducer function of the form: y' = reducer(yk), where yk is a results vector and y' is a single value. Ideally, this method is applied to a cost function with a multi-value return, to reduce the output to a single value. If arraylike, the reducer provided should take a single array as input and produce a scalar; otherwise, the reducer provided should meet the requirements of the python's builtin 'reduce' method (e.g. lambda x,y: x+y), taking two scalars and producing a scalar.""" if not reducer: self._reducer = None elif not callable(reducer): raise TypeError, "'%s' is not a callable function" % reducer elif not arraylike: self._reducer = wrap_reducer(reducer) else: #XXX: check if is arraylike? self._reducer = reducer return def SetPenalty(self, penalty): """apply a penalty function to the optimization input:: - a penalty function of the form: y' = penalty(xk), with y = cost(xk) + y', where xk is the current parameter vector. Ideally, this function is constructed so a penalty is applied when the desired (i.e. encoded) constraints are violated. Equality constraints should be considered satisfied when the penalty condition evaluates to zero, while inequality constraints are satisfied when the penalty condition evaluates to a non-positive number.""" if not penalty: self._penalty = lambda x: 0.0 elif not callable(penalty): raise TypeError, "'%s' is not a callable function" % penalty else: #XXX: check for format: y' = penalty(x) ? self._penalty = penalty return def SetConstraints(self, constraints): """apply a constraints function to the optimization input:: - a constraints function of the form: xk' = constraints(xk), where xk is the current parameter vector. Ideally, this function is constructed so the parameter vector it passes to the cost function will satisfy the desired (i.e. encoded) constraints.""" if not constraints: self._constraints = lambda x: x elif not callable(constraints): raise TypeError, "'%s' is not a callable function" % constraints else: #XXX: check for format: x' = constraints(x) ? self._constraints = constraints return def SetGenerationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each solver iteration""" from mystic.monitors import Null, Monitor#, CustomMonitor current = Null() if new else self._stepmon if isinstance(monitor, Monitor): # is Monitor() self._stepmon = monitor self._stepmon.prepend(current) elif isinstance(monitor, Null) or monitor == Null: # is Null() or Null self._stepmon = Monitor() #XXX: don't allow Null self._stepmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._stepmon = monitor #FIXME: need .prepend(current) else: raise TypeError, "'%s' is not a monitor instance" % monitor self.energy_history = self._stepmon.y self.solution_history = self._stepmon.x return def SetEvaluationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each cost function evaluation""" from mystic.monitors import Null, Monitor#, CustomMonitor current = Null() if new else self._evalmon if isinstance(monitor, (Null, Monitor) ): # is Monitor() or Null() self._evalmon = monitor self._evalmon.prepend(current) elif monitor == Null: # is Null self._evalmon = monitor() self._evalmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._evalmon = monitor #FIXME: need .prepend(current) else: raise TypeError, "'%s' is not a monitor instance" % monitor return def SetStrictRanges(self, min=None, max=None): """ensure solution is within bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i] note:: SetStrictRanges(None) will remove strict range constraints""" if min is False or max is False: self._useStrictRange = False return #XXX: better to use 'defaultMin,defaultMax' or '-inf,inf' ??? if min == None: min = self._defaultMin if max == None: max = self._defaultMax # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] == None: min[i] = self._defaultMin[0] if max[i] == None: max[i] = self._defaultMax[0] min = asarray(min); max = asarray(max) if numpy.any(( min > max ),0): raise ValueError, "each min[i] must be <= the corresponding max[i]" if len(min) != self.nDim: raise ValueError, "bounds array must be length %s" % self.nDim self._useStrictRange = True self._strictMin = min self._strictMax = max return def _clipGuessWithinRangeBoundary(self, x0): #FIXME: use self.trialSolution? """ensure that initial guess is set within bounds input:: - x0: must be a sequence of length self.nDim""" #if len(x0) != self.nDim: #XXX: unnecessary w/ self.trialSolution # raise ValueError, "initial guess must be length %s" % self.nDim x0 = asarray(x0) lo = self._strictMin hi = self._strictMax # crop x0 at bounds x0[x0<lo] = lo[x0<lo] x0[x0>hi] = hi[x0>hi] return x0 def SetInitialPoints(self, x0, radius=0.05): """Set Initial Points with Guess (x0) input:: - x0: must be a sequence of length self.nDim - radius: generate random points within [-radius*x0, radius*x0] for i!=0 when a simplex-type initial guess in required""" x0 = asfarray(x0) rank = len(x0.shape) if rank is 0: x0 = asfarray([x0]) rank = 1 if not -1 < rank < 2: raise ValueError, "Initial guess must be a scalar or rank-1 sequence." if len(x0) != self.nDim: raise ValueError, "Initial guess must be length %s" % self.nDim #slightly alter initial values for solvers that depend on randomness min = x0*(1-radius) max = x0*(1+radius) numzeros = len(x0[x0==0]) min[min==0] = asarray([-radius for i in range(numzeros)]) max[max==0] = asarray([radius for i in range(numzeros)]) self.SetRandomInitialPoints(min,max) #stick initial values in population[i], i=0 self.population[0] = x0.tolist() def SetRandomInitialPoints(self, min=None, max=None): """Generate Random Initial Points within given Bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i]""" if min == None: min = self._defaultMin if max == None: max = self._defaultMax #if numpy.any(( asarray(min) > asarray(max) ),0): # raise ValueError, "each min[i] must be <= the corresponding max[i]" if len(min) != self.nDim or len(max) != self.nDim: raise ValueError, "bounds array must be length %s" % self.nDim # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] == None: min[i] = self._defaultMin[0] if max[i] == None: max[i] = self._defaultMax[0] import random #generate random initial values for i in range(len(self.population)): for j in range(self.nDim): self.population[i][j] = random.uniform(min[j],max[j]) def SetMultinormalInitialPoints(self, mean, var = None): """Generate Initial Points from Multivariate Normal. input:: - mean must be a sequence of length self.nDim - var can be... None: -> it becomes the identity scalar: -> var becomes scalar * I matrix: -> the variance matrix. must be the right size! """ from numpy.random import multivariate_normal assert(len(mean) == self.nDim) if var == None: var = numpy.eye(self.nDim) else: try: # scalar ? float(var) except: # nope. var better be matrix of the right size (no check) pass else: var = var * numpy.eye(self.nDim) for i in range(len(self.population)): self.population[i] = multivariate_normal(mean, var).tolist() return def enable_signal_handler(self): """enable workflow interrupt handler while solver is running""" self._handle_sigint = True def disable_signal_handler(self): """disable workflow interrupt handler while solver is running""" self._handle_sigint = False def _generateHandler(self,sigint_callback): """factory to generate signal handler Available switches:: - sol --> Print current best solution. - cont --> Continue calculation. - call --> Executes sigint_callback, if provided. - exit --> Exits with current best solution. """ def handler(signum, frame): import inspect print inspect.getframeinfo(frame) print inspect.trace() while 1: s = raw_input(\ """ Enter sense switch. sol: Print current best solution. cont: Continue calculation. call: Executes sigint_callback [%s]. exit: Exits with current best solution. >>> """ % sigint_callback) if s.lower() == 'sol': print self.bestSolution elif s.lower() == 'cont': return elif s.lower() == 'call': # sigint call_back if sigint_callback is not None: sigint_callback(self.bestSolution) elif s.lower() == 'exit': self._EARLYEXIT = True return else: print "unknown option : %s" % s return self.signal_handler = handler return def SetSaveFrequency(self, generations=None, filename=None, **kwds): """set frequency for saving solver restart file input:: - generations = number of solver iterations before next save of state - filename = name of file in which to save solver state note:: SetSaveFrequency(None) will disable saving solver restart file""" self._saveiter = generations #self._saveeval = evaluations self._state = filename return def SetEvaluationLimits(self, generations=None, evaluations=None, \ new=False, **kwds): """set limits for generations and/or evaluations input:: - generations = maximum number of solver iterations (i.e. steps) - evaluations = maximum number of function evaluations""" self._maxiter = generations self._maxfun = evaluations # backward compatibility if kwds.has_key('maxiter'): self._maxiter = kwds['maxiter'] if kwds.has_key('maxfun'): self._maxfun = kwds['maxfun'] # handle if new (reset counter, instead of extend counter) if new: if generations is not None: self._maxiter += self.generations else: self._maxiter = "*" #XXX: better as self._newmax = True ? if evaluations is not None: self._maxfun += self.evaluations else: self._maxfun = "*" return def _SetEvaluationLimits(self, iterscale=None, evalscale=None): """set the evaluation limits""" if iterscale is None: iterscale = 10 if evalscale is None: evalscale = 1000 N = len(self.population[0]) # usually self.nDim # if SetEvaluationLimits not applied, use the solver default if self._maxiter is None: self._maxiter = N * self.nPop * iterscale elif self._maxiter == "*": # (i.e. None, but 'reset counter') self._maxiter = (N * self.nPop * iterscale) + self.generations if self._maxfun is None: self._maxfun = N * self.nPop * evalscale elif self._maxiter == "*": self._maxfun = (N * self.nPop * evalscale) + self.evaluations return def CheckTermination(self, disp=False, info=False, termination=None): """check if the solver meets the given termination conditions Input:: - disp = if True, print termination statistics and/or warnings - info = if True, return termination message (instead of boolean) - termination = termination conditions to check against Note:: If no termination conditions are given, the solver's stored termination conditions will be used. """ if termination == None: termination = self._termination # check for termination messages msg = termination(self, info=True) lim = "EvaluationLimits with %s" % {'evaluations':self._maxfun, 'generations':self._maxiter} # push solver internals to scipy.optimize.fmin interface if self._fcalls[0] >= self._maxfun and self._maxfun is not None: msg = lim #XXX: prefer the default stop ? if disp: print "Warning: Maximum number of function evaluations has "\ "been exceeded." elif self.generations >= self._maxiter and self._maxiter is not None: msg = lim #XXX: prefer the default stop ? if disp: print "Warning: Maximum number of iterations has been exceeded" elif msg and disp: print "Optimization terminated successfully." print " Current function value: %f" % self.bestEnergy print " Iterations: %d" % self.generations print " Function evaluations: %d" % self._fcalls[0] if info: return msg return bool(msg) def SetTermination(self, termination): """set the termination conditions""" #XXX: validate that termination is a 'condition' ? self._termination = termination return def _RegisterObjective(self, cost, ExtraArgs=None): """decorate cost function with bounds, penalties, monitors, etc""" if ExtraArgs == None: ExtraArgs = () self._fcalls, cost = wrap_function(cost, ExtraArgs, self._evalmon) if self._useStrictRange: for i in range(self.nPop): self.population[i] = self._clipGuessWithinRangeBoundary(self.population[i]) cost = wrap_bounds(cost, self._strictMin, self._strictMax) cost = wrap_penalty(cost, self._penalty) cost = wrap_nested(cost, self._constraints) if self._reducer: #cost = reduced(*self._reducer)(cost) # was self._reducer = (f,bool) cost = reduced(self._reducer, arraylike=True)(cost) # hold on to the 'wrapped' cost function self._cost = (cost, ExtraArgs) return cost def _bootstrap_decorate(self, cost=None, ExtraArgs=None): """HACK to enable not explicitly calling _RegisterObjective""" args = None if cost == None: # 'use existing cost' cost,args = self._cost # use args, unless override with ExtraArgs if ExtraArgs != None: args = ExtraArgs if self._cost[0] == None: # '_RegisterObjective not yet called' if args is None: args = () cost = self._RegisterObjective(cost, args) return cost def Step(self, cost=None, ExtraArgs=None, **kwds): """perform a single optimization iteration *** this method must be overwritten ***""" raise NotImplementedError, "an optimization algorithm was not provided" def SaveSolver(self, filename=None, **kwds): """save solver state to a restart file""" import dill if filename == None: # then check if already has registered file if self._state == None: # then create a new one import tempfile self._state = tempfile.mkstemp(suffix='.pkl')[-1] filename = self._state self._state = filename f = file(filename, 'wb') try: dill.dump(self, f, **kwds) self._stepmon.info('DUMPED("%s")' % filename) #XXX: before / after ? finally: f.close() return def __save_state(self, force=False): """save the solver state, if chosen save frequency is met""" # save the last iteration if force and bool(self._state): self.SaveSolver() return # save the zeroth iteration nonzero = True #XXX: or bool(self.generations) ? # after _saveiter generations, then save state iters = self._saveiter saveiter = bool(iters) and not bool(self.generations % iters) if nonzero and saveiter: self.SaveSolver() #FIXME: if _saveeval (or more) since last check, then save state #save = self.evaluations % self._saveeval return def __load_state(self, solver, **kwds): """load solver.__dict__ into self.__dict__; override with kwds""" #XXX: should do some filtering on kwds ? self.__dict__.update(solver.__dict__, **kwds) return def _exitMain(self, **kwds): """cleanup upon exiting the main optimization loop""" pass def _process_inputs(self, kwds): """process and activate input settings""" #allow for inputs that don't conform to AbstractSolver interface settings = \ {'callback':None, #user-supplied function, called after each step 'disp':0} #non-zero to print convergence messages [settings.update({i:j}) for (i,j) in kwds.items() if i in settings] # backward compatibility if kwds.has_key('EvaluationMonitor'): \ self.SetEvaluationMonitor(kwds.get('EvaluationMonitor')) if kwds.has_key('StepMonitor'): \ self.SetGenerationMonitor(kwds.get('StepMonitor')) if kwds.has_key('penalty'): \ self.SetPenalty(kwds.get('penalty')) if kwds.has_key('constraints'): \ self.SetConstraints(kwds.get('constraints')) return settings def Solve(self, cost=None, termination=None, sigint_callback=None, ExtraArgs=None, **kwds): """Minimize a 'cost' function with given termination conditions. Description: Uses an optimization algorith to find the minimum of a function of one or more variables. Inputs: cost -- the Python function or method to be minimized. Additional Inputs: termination -- callable object providing termination conditions. sigint_callback -- callback function for signal handler. ExtraArgs -- extra arguments for cost. Further Inputs: callback -- an optional user-supplied function to call after each iteration. It is called as callback(xk), where xk is the current parameter vector. [default = None] disp -- non-zero to print convergence messages. """ # HACK to enable not explicitly calling _RegisterObjective cost = self._bootstrap_decorate(cost, ExtraArgs) # process and activate input settings settings = self._process_inputs(kwds) for key in settings: exec "%s = settings['%s']" % (key,key) # set up signal handler import signal self._EARLYEXIT = False self._generateHandler(sigint_callback) if self._handle_sigint: signal.signal(signal.SIGINT, self.signal_handler) ## decorate cost function with bounds, penalties, monitors, etc #self._RegisterObjective(cost, ExtraArgs) #XXX: SetObjective ? # register termination function if termination is not None: self.SetTermination(termination) # the initital optimization iteration if not len(self._stepmon): # do generation = 0 self.Step() if callback is not None: callback(self.bestSolution) # initialize termination conditions, if needed self._termination(self) #XXX: call at generation 0 or always? # impose the evaluation limits self._SetEvaluationLimits() # the main optimization loop while not self.CheckTermination() and not self._EARLYEXIT: self.Step(**settings) if callback is not None: callback(self.bestSolution) else: self._exitMain() # handle signal interrupts signal.signal(signal.SIGINT,signal.default_int_handler) # log any termination messages msg = self.CheckTermination(disp=disp, info=True) if msg: self._stepmon.info('STOP("%s")' % msg) # save final state self.__save_state(force=True) return # extensions to the solver interface evaluations = property(__evaluations ) generations = property(__generations ) energy_history = property(__energy_history,__set_energy_history ) solution_history = property(__solution_history,__set_solution_history ) bestEnergy = property(__bestEnergy,__set_bestEnergy ) bestSolution = property(__bestSolution,__set_bestSolution ) pass
class AbstractSolver(object): """AbstractSolver base class for mystic optimizers. """ def __init__(self, dim, **kwds): """ Takes one initial input:: dim -- dimensionality of the problem. Additional inputs:: npop -- size of the trial solution population. [default = 1] Important class members:: nDim, nPop = dim, npop generations - an iteration counter. evaluations - an evaluation counter. bestEnergy - current best energy. bestSolution - current best parameter set. [size = dim] popEnergy - set of all trial energy solutions. [size = npop] population - set of all trial parameter solutions. [size = dim*npop] solution_history - history of bestSolution status. [StepMonitor.x] energy_history - history of bestEnergy status. [StepMonitor.y] signal_handler - catches the interrupt signal. """ NP = kwds['npop'] if 'npop' in kwds else 1 self.nDim = dim self.nPop = NP self._init_popEnergy = inf self.popEnergy = [self._init_popEnergy] * NP self.population = [[0.0 for i in range(dim)] for j in range(NP)] self.trialSolution = [0.0] * dim self._map_solver = False self._bestEnergy = None self._bestSolution = None self._state = None self._type = self.__class__.__name__ self.sigint_callback = None self._handle_sigint = False self._useStrictRange = False self._defaultMin = [-1e3] * dim self._defaultMax = [ 1e3] * dim self._strictMin = [] self._strictMax = [] self._maxiter = None self._maxfun = None self._saveiter = None #self._saveeval = None from mystic.monitors import Null, Monitor self._evalmon = Null() self._stepmon = Monitor() self._fcalls = [0] self._energy_history = None self._solution_history= None self.id = None # identifier (use like "rank" for MPI) self._constraints = lambda x: x self._penalty = lambda x: 0.0 self._reducer = None self._cost = (None, None, None) # (cost, raw_cost, args) #,callback) self._collapse = False self._termination = lambda x, *ar, **kw: False if len(ar) < 1 or ar[0] is False or (kw['info'] if 'info' in kw else True) == False else '' #XXX: better default ? # (get termination details with self._termination.__doc__) import mystic.termination as mt self._EARLYEXIT = mt.EARLYEXIT self._live = False return def Solution(self): """return the best solution""" return self.bestSolution def __evaluations(self): """get the number of function calls""" return self._fcalls[0] def __generations(self): """get the number of iterations""" return max(0,len(self._stepmon)-1) def __energy_history(self): """get the energy_history (default: energy_history = _stepmon._y)""" if self._energy_history is None: return self._stepmon._y return self._energy_history def __set_energy_history(self, energy): """set the energy_history (energy=None will sync with _stepmon._y)""" self._energy_history = energy return def __solution_history(self): """get the solution_history (default: solution_history = _stepmon.x)""" if self._solution_history is None: return self._stepmon.x return self._solution_history def __set_solution_history(self, params): """set the solution_history (params=None will sync with _stepmon.x)""" self._solution_history = params return def __bestSolution(self): """get the bestSolution (default: bestSolution = population[0])""" if self._bestSolution is None: return self.population[0] return self._bestSolution def __set_bestSolution(self, params): """set the bestSolution (params=None will sync with population[0])""" self._bestSolution = params return def __bestEnergy(self): """get the bestEnergy (default: bestEnergy = popEnergy[0])""" if self._bestEnergy is None: return self.popEnergy[0] return self._bestEnergy def __set_bestEnergy(self, energy): """set the bestEnergy (energy=None will sync with popEnergy[0])""" self._bestEnergy = energy return def SetReducer(self, reducer, arraylike=False): """apply a reducer function to the cost function input:: - a reducer function of the form: y' = reducer(yk), where yk is a results vector and y' is a single value. Ideally, this method is applied to a cost function with a multi-value return, to reduce the output to a single value. If arraylike, the reducer provided should take a single array as input and produce a scalar; otherwise, the reducer provided should meet the requirements of the python's builtin 'reduce' method (e.g. lambda x,y: x+y), taking two scalars and producing a scalar.""" if not reducer: self._reducer = None elif not isinstance(reducer, collections.Callable): raise TypeError("'%s' is not a callable function" % reducer) elif not arraylike: self._reducer = wrap_reducer(reducer) else: #XXX: check if is arraylike? self._reducer = reducer return self._update_objective() def SetPenalty(self, penalty): """apply a penalty function to the optimization input:: - a penalty function of the form: y' = penalty(xk), with y = cost(xk) + y', where xk is the current parameter vector. Ideally, this function is constructed so a penalty is applied when the desired (i.e. encoded) constraints are violated. Equality constraints should be considered satisfied when the penalty condition evaluates to zero, while inequality constraints are satisfied when the penalty condition evaluates to a non-positive number.""" if not penalty: self._penalty = lambda x: 0.0 elif not isinstance(penalty, collections.Callable): raise TypeError("'%s' is not a callable function" % penalty) else: #XXX: check for format: y' = penalty(x) ? self._penalty = penalty return self._update_objective() def SetConstraints(self, constraints): """apply a constraints function to the optimization input:: - a constraints function of the form: xk' = constraints(xk), where xk is the current parameter vector. Ideally, this function is constructed so the parameter vector it passes to the cost function will satisfy the desired (i.e. encoded) constraints.""" if not constraints: self._constraints = lambda x: x elif not isinstance(constraints, collections.Callable): raise TypeError("'%s' is not a callable function" % constraints) else: #XXX: check for format: x' = constraints(x) ? self._constraints = constraints return self._update_objective() def SetGenerationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each solver iteration""" from mystic.monitors import Null, Monitor#, CustomMonitor current = Null() if new else self._stepmon if isinstance(monitor, Monitor): # is Monitor() self._stepmon = monitor self._stepmon.prepend(current) elif isinstance(monitor, Null) or monitor == Null: # is Null() or Null self._stepmon = Monitor() #XXX: don't allow Null self._stepmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._stepmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) self.energy_history = None # sync with self._stepmon self.solution_history = None # sync with self._stepmon return def SetEvaluationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each cost function evaluation""" from mystic.monitors import Null, Monitor#, CustomMonitor current = Null() if new else self._evalmon if isinstance(monitor, (Null, Monitor) ): # is Monitor() or Null() self._evalmon = monitor self._evalmon.prepend(current) elif monitor == Null: # is Null self._evalmon = monitor() self._evalmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._evalmon = monitor #FIXME: need .prepend(current) else: raise TypeError("'%s' is not a monitor instance" % monitor) return def SetStrictRanges(self, min=None, max=None): """ensure solution is within bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i] note:: SetStrictRanges(None) will remove strict range constraints""" if min is False or max is False: self._useStrictRange = False return self._update_objective() #XXX: better to use 'defaultMin,defaultMax' or '-inf,inf' ??? if min is None: min = self._defaultMin if max is None: max = self._defaultMax # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] is None: min[i] = self._defaultMin[0] if max[i] is None: max[i] = self._defaultMax[0] min = asarray(min); max = asarray(max) if numpy.any(( min > max ),0): raise ValueError("each min[i] must be <= the corresponding max[i]") if len(min) != self.nDim: raise ValueError("bounds array must be length %s" % self.nDim) self._useStrictRange = True self._strictMin = min self._strictMax = max return self._update_objective() def _clipGuessWithinRangeBoundary(self, x0, at=True): """ensure that initial guess is set within bounds input:: - x0: must be a sequence of length self.nDim""" #if len(x0) != self.nDim: #XXX: unnecessary w/ self.trialSolution # raise ValueError, "initial guess must be length %s" % self.nDim x0 = asarray(x0) bounds = (self._strictMin,self._strictMax) if not len(self._strictMin): return x0 # clip x0 at bounds settings = numpy.seterr(all='ignore') x_ = x0.clip(*bounds) numpy.seterr(**settings) if at: return x_ # clip x0 within bounds x_ = x_ != x0 x0[x_] = random.uniform(self._strictMin,self._strictMax)[x_] return x0 def SetInitialPoints(self, x0, radius=0.05): """Set Initial Points with Guess (x0) input:: - x0: must be a sequence of length self.nDim - radius: generate random points within [-radius*x0, radius*x0] for i!=0 when a simplex-type initial guess in required""" x0 = asfarray(x0) rank = len(x0.shape) if rank is 0: x0 = asfarray([x0]) rank = 1 if not -1 < rank < 2: raise ValueError("Initial guess must be a scalar or rank-1 sequence.") if len(x0) != self.nDim: raise ValueError("Initial guess must be length %s" % self.nDim) #slightly alter initial values for solvers that depend on randomness min = x0*(1-radius) max = x0*(1+radius) numzeros = len(x0[x0==0]) min[min==0] = asarray([-radius for i in range(numzeros)]) max[max==0] = asarray([radius for i in range(numzeros)]) self.SetRandomInitialPoints(min,max) #stick initial values in population[i], i=0 self.population[0] = x0.tolist() def SetRandomInitialPoints(self, min=None, max=None): """Generate Random Initial Points within given Bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i]""" if min is None: min = self._defaultMin if max is None: max = self._defaultMax #if numpy.any(( asarray(min) > asarray(max) ),0): # raise ValueError, "each min[i] must be <= the corresponding max[i]" if len(min) != self.nDim or len(max) != self.nDim: raise ValueError("bounds array must be length %s" % self.nDim) # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] is None: min[i] = self._defaultMin[0] if max[i] is None: max[i] = self._defaultMax[0] #generate random initial values for i in range(len(self.population)): for j in range(self.nDim): self.population[i][j] = random.uniform(min[j],max[j]) def SetMultinormalInitialPoints(self, mean, var=None): """Generate Initial Points from Multivariate Normal. input:: - mean must be a sequence of length self.nDim - var can be... None: -> it becomes the identity scalar: -> var becomes scalar * I matrix: -> the variance matrix. must be the right size! """ from mystic.tools import random_state rng = random_state(module='numpy.random') assert(len(mean) == self.nDim) if var is None: var = numpy.eye(self.nDim) else: try: # scalar ? float(var) except: # nope. var better be matrix of the right size (no check) pass else: var = var * numpy.eye(self.nDim) for i in range(len(self.population)): self.population[i] = rng.multivariate_normal(mean, var).tolist() return def SetSampledInitialPoints(self, dist=None): """Generate Random Initial Points from Distribution (dist) input:: - dist: a mystic.math.Distribution instance """ from mystic.math import Distribution _dist = Distribution() if dist is None: dist = _dist elif type(_dist) not in dist.__class__.mro(): dist = Distribution(dist) #XXX: or throw error? for i in range(self.nPop): self.population[i] = dist(self.nDim) return def enable_signal_handler(self):#, callback='*'): """enable workflow interrupt handler while solver is running""" """ #XXX: disabled, as would add state to solver input:: - if a callback function is provided, generate a new handler with the given callback. If callback is None, do not use a callback. If callback is not provided, just turn on the existing handler. """ ## always _generate handler on first call #if (self.signal_handler is None) and callback == '*': # callback = None ## when a new callback is given, generate a new handler #if callback != '*': # self._generateHandler(callback) self._handle_sigint = True def disable_signal_handler(self): """disable workflow interrupt handler while solver is running""" self._handle_sigint = False def SetSaveFrequency(self, generations=None, filename=None, **kwds): """set frequency for saving solver restart file input:: - generations = number of solver iterations before next save of state - filename = name of file in which to save solver state note:: SetSaveFrequency(None) will disable saving solver restart file""" self._saveiter = generations #self._saveeval = evaluations self._state = filename return def SetEvaluationLimits(self, generations=None, evaluations=None, \ new=False, **kwds): """set limits for generations and/or evaluations input:: - generations = maximum number of solver iterations (i.e. steps) - evaluations = maximum number of function evaluations""" # backward compatibility self._maxiter = kwds['maxiter'] if 'maxiter' in kwds else generations self._maxfun = kwds['maxfun'] if 'maxfun' in kwds else evaluations # handle if new (reset counter, instead of extend counter) if new: if generations is not None: self._maxiter += self.generations else: self._maxiter = "*" #XXX: better as self._newmax = True ? if evaluations is not None: self._maxfun += self.evaluations else: self._maxfun = "*" return def _SetEvaluationLimits(self, iterscale=None, evalscale=None): """set the evaluation limits""" if iterscale is None: iterscale = 10 if evalscale is None: evalscale = 1000 N = len(self.population[0]) # usually self.nDim # if SetEvaluationLimits not applied, use the solver default if self._maxiter is None: self._maxiter = N * self.nPop * iterscale elif self._maxiter == "*": # (i.e. None, but 'reset counter') self._maxiter = (N * self.nPop * iterscale) + self.generations if self._maxfun is None: self._maxfun = N * self.nPop * evalscale elif self._maxfun == "*": self._maxfun = (N * self.nPop * evalscale) + self.evaluations return def Terminated(self, disp=False, info=False, termination=None, **kwds): """check if the solver meets the given termination conditions Input:: - disp = if True, print termination statistics and/or warnings - info = if True, return termination message (instead of boolean) - termination = termination conditions to check against Notes:: If no termination conditions are given, the solver's stored termination conditions will be used. """ if termination is None: termination = self._termination # ensure evaluation limits have been imposed self._SetEvaluationLimits() # check for termination messages msg = termination(self, info=True) sig = "SolverInterrupt with %s" % {} lim = "EvaluationLimits with %s" % {'evaluations':self._maxfun, 'generations':self._maxiter} # push solver internals to scipy.optimize.fmin interface if self._fcalls[0] >= self._maxfun and self._maxfun is not None: msg = lim #XXX: prefer the default stop ? if disp: print("Warning: Maximum number of function evaluations has "\ "been exceeded.") elif self.generations >= self._maxiter and self._maxiter is not None: msg = lim #XXX: prefer the default stop ? if disp: print("Warning: Maximum number of iterations has been exceeded") elif self._EARLYEXIT: msg = sig if disp: print("Warning: Optimization terminated with signal interrupt.") elif msg and disp: print("Optimization terminated successfully.") print(" Current function value: %f" % self.bestEnergy) print(" Iterations: %d" % self.generations) print(" Function evaluations: %d" % self._fcalls[0]) if info: return msg return bool(msg) def SetTermination(self, termination): # disp ? """set the termination conditions""" #XXX: validate that termination is a 'condition' ? self._termination = termination self._collapse = False if termination is not None: from mystic.termination import state stop = state(termination) stop = getattr(stop, 'iterkeys', stop.keys)() self._collapse = any(key.startswith('Collapse') for key in stop) return def SetObjective(self, cost, ExtraArgs=None): # callback=None/False ? """decorate the cost function with bounds, penalties, monitors, etc""" _cost,_raw,_args = self._cost # check if need to 'wrap' or can return the stored cost if (cost is None or cost is _raw or cost is _cost) and \ (ExtraArgs is None or ExtraArgs is _args): return # get cost and args if None was given if cost is None: cost = _raw args = _args if ExtraArgs is None else ExtraArgs args = () if args is None else args # quick validation check (so doesn't screw up internals) if not isvalid(cost, [0]*self.nDim, *args): try: name = cost.__name__ except AttributeError: # raise new error for non-callables cost(*args) validate(cost, None, *args) #val = len(args) + 1 #XXX: 'klepto.validate' for better error? #msg = '%s() invalid number of arguments (%d given)' % (name, val) #raise TypeError(msg) # hold on to the 'raw' cost function self._cost = (None, cost, ExtraArgs) self._live = False return def Collapsed(self, disp=False, info=False): """check if the solver meets the given collapse conditions Input:: - disp = if True, print details about the solver state at collapse - info = if True, return collapsed state (instead of boolean) """ stop = getattr(self, '__stop__', self.Terminated(info=True)) import mystic.collapse as ct collapses = ct.collapsed(stop) or dict() if collapses and disp: for (k,v) in getattr(collapses, 'iteritems', collapses.items)(): print(" %s: %s" % (k.split()[0],v)) #print("# Collapse at: Generation", self._stepmon._step-1, \ # "with", self.bestEnergy, "@\n#", list(self.bestSolution)) return collapses if info else bool(collapses) def Collapse(self, disp=False): """if solver has terminated by collapse, apply the collapse (unless both collapse and "stop" are simultaneously satisfied) """ #XXX: return True for "collapse and continue" and False otherwise? collapses = self.Collapsed(disp=disp, info=True) if collapses: # stop if any Termination is not from Collapse stop = getattr(self, '__stop__', self.Terminated(info=True)) stop = not all(k.startswith("Collapse") for k in stop.split("; ")) if stop: return {} #XXX: self._collapse = False ? else: stop = True if collapses: # then stomach a bunch of module imports (yuck) import mystic.tools as to import mystic.termination as mt import mystic.constraints as cn import mystic.mask as ma # get collapse conditions #XXX: efficient? 4x loops over collapses state = mt.state(self._termination) npts = getattr(self._stepmon, '_npts', None) #XXX: default? #conditions = [cn.impose_at(*to.select_params(self,collapses[k])) if state[k].get('target') is None else cn.impose_at(collapses[k],state[k].get('target')) for k in collapses if k.startswith('CollapseAt')] #conditions += [cn.impose_as(collapses[k],state[k].get('offset')) for k in collapses if k.startswith('CollapseAs')] #randomize = False conditions = []; _conditions = []; conditions_ = [] for k in collapses: #FIXME: these should be encapsulted in termination instance if k.startswith('CollapseAt'): t = state[k] t = t['target'] if 'target' in t else None if t is None: t = cn.impose_at(*to.select_params(self,collapses[k])) else: t = cn.impose_at(collapses[k],t) conditions.append(t) elif k.startswith('CollapseAs'): t = state[k] t = t['offset'] if 'offset' in t else None _conditions.append(cn.impose_as(collapses[k],t)) elif k.startswith(('CollapseCost','CollapseGrad')): t = state[k] t = t['clip'] if 'clip' in t else True conditions_.append(cn.impose_bounds(collapses[k],clip=t)) #randomize = True conditions.extend(_conditions) conditions.extend(conditions_) del _conditions; del conditions_ # get measure collapse conditions if npts: #XXX: faster/better if comes first or last? conditions += [cn.impose_measure( npts, [collapses[k] for k in collapses if k.startswith('CollapsePosition')], [collapses[k] for k in collapses if k.startswith('CollapseWeight')] )] # update termination and constraints in solver constraints = to.chain(*conditions)(self._constraints) termination = ma.update_mask(self._termination, collapses) self.SetConstraints(constraints) self.SetTermination(termination) #if randomize: self.SetInitialPoints(self.population[0]) #print(mt.state(self._termination).keys()) #return bool(collapses) and not stop return collapses def _update_objective(self): """decorate the cost function with bounds, penalties, monitors, etc""" # rewrap the cost if the solver has been run if False: # trigger immediately self._decorate_objective(*self._cost[1:]) else: # delay update until _bootstrap self.Finalize() return def _decorate_objective(self, cost, ExtraArgs=None): """decorate the cost function with bounds, penalties, monitors, etc""" #print("@%r %r %r" % (cost, ExtraArgs, max)) raw = cost if ExtraArgs is None: ExtraArgs = () self._fcalls, cost = wrap_function(cost, ExtraArgs, self._evalmon) if self._useStrictRange: indx = list(self.popEnergy).index(self.bestEnergy) ngen = self.generations #XXX: no random if generations=0 ? for i in range(self.nPop): self.population[i] = self._clipGuessWithinRangeBoundary(self.population[i], (not ngen) or (i is indx)) cost = wrap_bounds(cost, self._strictMin, self._strictMax) cost = wrap_penalty(cost, self._penalty) cost = wrap_nested(cost, self._constraints) if self._reducer: #cost = reduced(*self._reducer)(cost) # was self._reducer = (f,bool) cost = reduced(self._reducer, arraylike=True)(cost) # hold on to the 'wrapped' and 'raw' cost function self._cost = (cost, raw, ExtraArgs) self._live = True return cost def _bootstrap_objective(self, cost=None, ExtraArgs=None): """HACK to enable not explicitly calling _decorate_objective""" _cost,_raw,_args = self._cost # check if need to 'wrap' or can return the stored cost if (cost is None or cost is _raw or cost is _cost) and \ (ExtraArgs is None or ExtraArgs is _args) and self._live: return _cost # 'wrap' the 'new' cost function with _decorate self.SetObjective(cost, ExtraArgs) return self._decorate_objective(*self._cost[1:]) #XXX: when _decorate called, solver._fcalls will be reset ? def _Step(self, cost=None, ExtraArgs=None, **kwds): """perform a single optimization iteration *** this method must be overwritten ***""" raise NotImplementedError("an optimization algorithm was not provided") def SaveSolver(self, filename=None, **kwds): """save solver state to a restart file""" import dill fd = None if filename is None: # then check if already has registered file if self._state is None: # then create a new one import os, tempfile fd, self._state = tempfile.mkstemp(suffix='.pkl') os.close(fd) filename = self._state self._state = filename f = open(filename, 'wb') try: dill.dump(self, f, **kwds) self._stepmon.info('DUMPED("%s")' % filename) #XXX: before / after ? finally: f.close() return def __save_state(self, force=False): """save the solver state, if chosen save frequency is met""" # save the last iteration if force and bool(self._state): self.SaveSolver() return # save the zeroth iteration nonzero = True #XXX: or bool(self.generations) ? # after _saveiter generations, then save state iters = self._saveiter saveiter = bool(iters) and not bool(self.generations % iters) if nonzero and saveiter: self.SaveSolver() #FIXME: if _saveeval (or more) since last check, then save state #save = self.evaluations % self._saveeval return def __load_state(self, solver, **kwds): """load solver.__dict__ into self.__dict__; override with kwds""" #XXX: should do some filtering on kwds ? self.__dict__.update(solver.__dict__, **kwds) return def Finalize(self, **kwds): """cleanup upon exiting the main optimization loop""" self._live = False return def _process_inputs(self, kwds): """process and activate input settings""" #allow for inputs that don't conform to AbstractSolver interface #NOTE: not sticky: callback, disp #NOTE: sticky: EvaluationMonitor, StepMonitor, penalty, constraints settings = \ {'callback':None, #user-supplied function, called after each step 'disp':0} #non-zero to print convergence messages [settings.update({i:j}) for (i,j) in kwds.items() if i in settings] # backward compatibility if 'EvaluationMonitor' in kwds: \ self.SetEvaluationMonitor(kwds['EvaluationMonitor']) if 'StepMonitor' in kwds: \ self.SetGenerationMonitor(kwds['StepMonitor']) if 'penalty' in kwds: \ self.SetPenalty(kwds['penalty']) if 'constraints' in kwds: \ self.SetConstraints(kwds['constraints']) return settings def Step(self, cost=None, termination=None, ExtraArgs=None, **kwds): """Take a single optimization step using the given 'cost' function. Uses an optimization algorithm to take one 'step' toward the minimum of a function of one or more variables. Args: cost (func, default=None): the function to be minimized: ``y = cost(x)``. termination (termination, default=None): termination conditions. ExtraArgs (tuple, default=None): extra arguments for cost. callback (func, default=None): function to call after each iteration. The interface is ``callback(xk)``, with xk the current parameter vector. disp (bool, default=False): if True, print convergence messages. Returns: None Notes: To run the solver until termination, call ``Solve()``. Alternately, use ``Terminated()`` as the stop condition in a while loop over ``Step``. If the algorithm does not meet the given termination conditions after the call to ``Step``, the solver may be left in an "out-of-sync" state. When abandoning an non-terminated solver, one should call ``Finalize()`` to make sure the solver is fully returned to a "synchronized" state. """ if 'disp' in kwds: disp = bool(kwds['disp'])#; del kwds['disp'] else: disp = False # register: cost, termination, ExtraArgs cost = self._bootstrap_objective(cost, ExtraArgs) if termination is not None: self.SetTermination(termination) # check termination before 'stepping' if len(self._stepmon): msg = self.Terminated(disp=disp, info=True) or None else: msg = None # if not terminated, then take a step if msg is None: self._Step(**kwds) #FIXME: not all kwds are given in __doc__ if self.Terminated(): # then cleanup/finalize self.Finalize() # get termination message and log state msg = self.Terminated(disp=disp, info=True) or None if msg: self._stepmon.info('STOP("%s")' % msg) self.__save_state(force=True) return msg def _Solve(self, cost, ExtraArgs, **settings): """Run the optimizer to termination, using the given settings. Args: cost (func): the function to be minimized: ``y = cost(x)``. ExtraArgs (tuple): tuple of extra arguments for ``cost``. settings (dict): optimizer settings (produced by _process_inputs) Returns: None """ disp = settings['disp'] if 'disp' in settings else False # the main optimization loop stop = False while not stop: stop = self.Step(**settings) #XXX: remove need to pass settings? continue # if collapse, then activate any relevant collapses and continue self.__stop__ = stop #HACK: avoid re-evaluation of Termination while self._collapse and self.Collapse(disp=disp): del self.__stop__ #HACK stop = False while not stop: stop = self.Step(**settings) #XXX: move Collapse inside of Step? continue self.__stop__ = stop #HACK del self.__stop__ #HACK return def Solve(self, cost=None, termination=None, ExtraArgs=None, **kwds): """Minimize a 'cost' function with given termination conditions. Uses an optimization algorithm to find the minimum of a function of one or more variables. Args: cost (func, default=None): the function to be minimized: ``y = cost(x)``. termination (termination, default=None): termination conditions. ExtraArgs (tuple, default=None): extra arguments for cost. sigint_callback (func, default=None): callback function for signal handler. callback (func, default=None): function to call after each iteration. The interface is ``callback(xk)``, with xk the current parameter vector. disp (bool, default=False): if True, print convergence messages. Returns: None """ # process and activate input settings if 'sigint_callback' in kwds: self.sigint_callback = kwds['sigint_callback'] del kwds['sigint_callback'] else: self.sigint_callback = None settings = self._process_inputs(kwds) # set up signal handler #FIXME: sigint doesn't behave well in parallel self._EARLYEXIT = False #XXX: why not use EARLYEXIT singleton? # activate signal handler #import threading as thread #mainthread = isinstance(thread.current_thread(), thread._MainThread) #if mainthread: #XXX: if not mainthread, signal will raise ValueError import mystic._signal as signal if self._handle_sigint: signal.signal(signal.SIGINT, signal.Handler(self)) # register: cost, termination, ExtraArgs cost = self._bootstrap_objective(cost, ExtraArgs) if termination is not None: self.SetTermination(termination) #XXX: self.Step(cost, termination, ExtraArgs, **settings) ? # run the optimizer to termination self._Solve(cost, ExtraArgs, **settings) # restore default handler for signal interrupts if self._handle_sigint: signal.signal(signal.SIGINT, signal.default_int_handler) return def __copy__(self): cls = self.__class__ result = cls.__new__(cls) result.__dict__.update(self.__dict__) return result def __deepcopy__(self, memo): import copy import dill cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): if v is self._cost: setattr(result, k, tuple(dill.copy(i) for i in v)) else: try: #XXX: work-around instancemethods in python2.6 setattr(result, k, copy.deepcopy(v, memo)) except TypeError: setattr(result, k, dill.copy(v)) return result # extensions to the solver interface evaluations = property(__evaluations ) generations = property(__generations ) energy_history = property(__energy_history,__set_energy_history ) solution_history = property(__solution_history,__set_solution_history ) bestEnergy = property(__bestEnergy,__set_bestEnergy ) bestSolution = property(__bestSolution,__set_bestSolution ) pass
def Solve(self, cost, termination=None, ExtraArgs=(), **kwds): """Minimize a 'cost' function with given termination conditions. Uses an ensemble of optimizers to find the minimum of a function of one or more variables. Args: cost (func, default=None): the function to be minimized: ``y = cost(x)``. termination (termination, default=None): termination conditions. ExtraArgs (tuple, default=None): extra arguments for cost. sigint_callback (func, default=None): callback function for signal handler. callback (func, default=None): function to call after each iteration. The interface is ``callback(xk)``, with xk the current parameter vector. disp (bool, default=False): if True, print convergence messages. Returns: None """ # process and activate input settings if 'sigint_callback' in kwds: self.sigint_callback = kwds['sigint_callback'] del kwds['sigint_callback'] else: self.sigint_callback = None settings = self._process_inputs(kwds) disp = settings['disp'] if 'disp' in settings else False echo = settings['callback'] if 'callback' in settings else None # for key in settings: # exec "%s = settings['%s']" % (key,key) if disp in ['verbose', 'all']: verbose = True else: verbose = False #------------------------------------------------------------- from mystic.python_map import python_map if self._map != python_map: #FIXME: EvaluationMonitor fails for MPI, throws error for 'pp' from mystic.monitors import Null evalmon = Null() else: evalmon = self._evalmon fcalls, cost = wrap_function(cost, ExtraArgs, evalmon) # set up signal handler #self._EARLYEXIT = False # activate signal_handler #import threading as thread #mainthread = isinstance(thread.current_thread(), thread._MainThread) #if mainthread: #XXX: if not mainthread, signal will raise ValueError import mystic._signal as signal if self._handle_sigint: signal.signal(signal.SIGINT, signal.Handler(self)) # register termination function if termination is not None: self.SetTermination(termination) # get the nested solver instance solver = self._AbstractEnsembleSolver__get_solver_instance() #------------------------------------------------------------- # generate starting points initial_values = self._InitialPoints() # run optimizer for each grid point from copy import deepcopy as _copy op = [_copy(solver) for i in range(len(initial_values))] #cf = [cost for i in range(len(initial_values))] vb = [verbose for i in range(len(initial_values))] cb = [echo for i in range(len(initial_values))] #XXX: remove? at = self.id if self.id else 0 # start at self.id id = range(at, at + len(initial_values)) # generate the local_optimize function def local_optimize(solver, x0, rank=None, disp=False, callback=None): from copy import deepcopy as _copy from mystic.tools import isNull solver.id = rank solver.SetInitialPoints(x0) if solver._useStrictRange: #XXX: always, settable, or sync'd ? solver.SetStrictRanges(min=solver._strictMin, \ max=solver._strictMax) # or lower,upper ? solver.Solve(cost, disp=disp, callback=callback) sm = solver._stepmon em = solver._evalmon if isNull(sm): sm = ([], [], [], []) else: sm = (_copy(sm._x), _copy(sm._y), _copy(sm._id), _copy(sm._info)) if isNull(em): em = ([], [], [], []) else: em = (_copy(em._x), _copy(em._y), _copy(em._id), _copy(em._info)) return solver, sm, em # map:: solver = local_optimize(solver, x0, id, verbose) results = list(self._map(local_optimize, op, initial_values, id, \ vb, cb, **self._mapconfig)) # save initial state self._AbstractSolver__save_state() #XXX: HACK TO GET CONTENT OF ALL MONITORS # reconnect monitors; save all solvers from mystic.monitors import Monitor while results: #XXX: option to not save allSolvers? skip this and _copy _solver, _stepmon, _evalmon = results.pop() sm = Monitor() sm._x, sm._y, sm._id, sm._info = _stepmon _solver._stepmon.extend(sm) del sm em = Monitor() em._x, em._y, em._id, em._info = _evalmon _solver._evalmon.extend(em) del em self._allSolvers[len(results)] = _solver del results, _solver, _stepmon, _evalmon #XXX: END HACK # get the results with the lowest energy self._bestSolver = self._allSolvers[0] bestpath = self._bestSolver._stepmon besteval = self._bestSolver._evalmon self._total_evals = self._bestSolver.evaluations for solver in self._allSolvers[1:]: self._total_evals += solver.evaluations # add func evals if solver.bestEnergy < self._bestSolver.bestEnergy: self._bestSolver = solver bestpath = solver._stepmon besteval = solver._evalmon # return results to internals self.population = self._bestSolver.population #XXX: pointer? copy? self.popEnergy = self._bestSolver.popEnergy #XXX: pointer? copy? self.bestSolution = self._bestSolver.bestSolution #XXX: pointer? copy? self.bestEnergy = self._bestSolver.bestEnergy self.trialSolution = self._bestSolver.trialSolution #XXX: pointer? copy? self._fcalls = self._bestSolver._fcalls #XXX: pointer? copy? self._maxiter = self._bestSolver._maxiter self._maxfun = self._bestSolver._maxfun # write 'bests' to monitors #XXX: non-best monitors may be useful too self._stepmon = bestpath #XXX: pointer? copy? self._evalmon = besteval #XXX: pointer? copy? self.energy_history = None self.solution_history = None #from mystic.tools import isNull #if isNull(bestpath): # self._stepmon = bestpath #else: # for i in range(len(bestpath.y)): # self._stepmon(bestpath.x[i], bestpath.y[i], self.id) # #XXX: could apply callback here, or in exec'd code #if isNull(besteval): # self._evalmon = besteval #else: # for i in range(len(besteval.y)): # self._evalmon(besteval.x[i], besteval.y[i]) #------------------------------------------------------------- # restore default handler for signal interrupts if self._handle_sigint: signal.signal(signal.SIGINT, signal.default_int_handler) # log any termination messages msg = self.Terminated(disp=disp, info=True) if msg: self._stepmon.info('STOP("%s")' % msg) # save final state self._AbstractSolver__save_state(force=True) return
def _Solve(self, cost, ExtraArgs, **settings): #XXX: self._cost? """Run the optimizer to termination, using the given settings. Args: cost (func): the function to be minimized: ``y = cost(x)``. ExtraArgs (tuple): tuple of extra arguments for ``cost``. settings (dict): optimizer settings (produced by _process_inputs) Returns: None """ #FIXME: 'step' is undocumented (in Solve) #NOTE: if Step once, will ensure always uses step=True, unless... #TODO: if step=False passed after Step taken... this is still TODO! step = settings['step'] if 'step' in settings else False if step: #FIXME: use abstract_solver _Solve super(AbstractEnsembleSolver, self)._Solve(cost, ExtraArgs, **settings) return #FIXME: evalmon wrapped around _cost doesn't work (__class__()?) #FIXME: fix the above... remove HACK below (and previous evalmon HACKs) #FIXME: HACK evalmon wrapped around _cost is disabled (bc it works!) evalmon, self._evalmon = self._evalmon, Null() #XXX: fcalls start=0 self._live = False #XXX: redo wrap objective with dummy evalmon cost = self._bootstrap_objective(self._cost[1], ExtraArgs) self._evalmon = evalmon #FIXME: HACK (end) to disable evalmon wrapped around cost disp = settings['disp'] if 'disp' in settings else False echo = settings['callback'] if 'callback' in settings else None if disp in ['verbose', 'all']: verbose = True else: verbose = False # generate starting points if self._is_new(): iv = self._InitialPoints() else: iv = [None] * len(self._allSolvers) op = self._AbstractEnsembleSolver__init_allSolvers() vb = [verbose] * len(op) cb = [echo] * len(op) #XXX: remove? # generate the _solve function def _solve(solver, x0, disp=False, callback=None): from copy import deepcopy as _copy from mystic.tools import isNull if x0 is not None: solver.SetInitialPoints(x0) if solver._useStrictRange: #XXX: always, settable, or sync'd ? solver.SetStrictRanges(solver._strictMin, solver._strictMax) _term = (solver._live is False) and solver.Terminated() if _term is True: solver._live = True #XXX: HACK don't reset _fcalls if solver._cost[1] is None: #XXX: HACK for configured NestedSolver solver.SetObjective(cost, ExtraArgs=ExtraArgs) solver.Solve(disp=disp, callback=callback) if _term is True: solver._live = False sm = solver._stepmon em = solver._evalmon if isNull(sm): sm = ([], [], [], []) else: sm = _copy(sm) sm = (sm._x, sm._y, sm._id, sm._info) if isNull(em): em = ([], [], [], []) else: em = _copy(em) em = (em._x, em._y, em._id, em._info) return solver, sm, em # map:: solver = _solve(solver, x0, id, verbose) results = list(self._map(_solve, op, iv, vb, cb, **self._mapconfig)) del op, iv, vb, cb # save initial state self._AbstractSolver__save_state() # save results to allSolvers self._AbstractEnsembleSolver__update_allSolvers(results) del results # update state from bestSolver self._AbstractEnsembleSolver__update_state() # log any termination messages msg = self.Terminated(disp=disp, info=True) if msg: self._stepmon.info('STOP("%s")' % msg) # save final state self._AbstractSolver__save_state(force=True) return
class AbstractSolver(object): """ AbstractSolver base class for mystic optimizers. """ def __init__(self, dim, **kwds): """ Takes one initial input: dim -- dimensionality of the problem. Additional inputs: npop -- size of the trial solution population. [default = 1] Important class members: nDim, nPop = dim, npop generations - an iteration counter. evaluations - an evaluation counter. bestEnergy - current best energy. bestSolution - current best parameter set. [size = dim] popEnergy - set of all trial energy solutions. [size = npop] population - set of all trial parameter solutions. [size = dim*npop] solution_history - history of bestSolution status. [StepMonitor.x] energy_history - history of bestEnergy status. [StepMonitor.y] signal_handler - catches the interrupt signal. """ NP = 1 if kwds.has_key('npop'): NP = kwds['npop'] self.nDim = dim self.nPop = NP self._init_popEnergy = inf self.popEnergy = [self._init_popEnergy] * NP self.population = [[0.0 for i in range(dim)] for j in range(NP)] self.trialSolution = [0.0] * dim self._map_solver = False self._bestEnergy = None self._bestSolution = None self._state = None self._type = self.__class__.__name__ self.signal_handler = None self._handle_sigint = False self._useStrictRange = False self._defaultMin = [-1e3] * dim self._defaultMax = [1e3] * dim self._strictMin = [] self._strictMax = [] self._maxiter = None self._maxfun = None self._saveiter = None #self._saveeval = None from mystic.monitors import Null, Monitor self._evalmon = Null() self._stepmon = Monitor() self._fcalls = [0] self._energy_history = None self._solution_history = None self.id = None # identifier (use like "rank" for MPI) self._constraints = lambda x: x self._penalty = lambda x: 0.0 self._cost = (None, None) self._termination = lambda x, *ar, **kw: False if len(ar) < 1 or ar[ 0] is False or kw.get('info', True ) == False else '' #XXX: better default ? # (get termination details with self._termination.__doc__) import mystic.termination self._EARLYEXIT = mystic.termination.EARLYEXIT return def Solution(self): """return the best solution""" return self.bestSolution def __evaluations(self): """get the number of function calls""" return self._fcalls[0] def __generations(self): """get the number of iterations""" return max(0, len(self.energy_history) - 1) #return max(0,len(self._stepmon)-1) def __energy_history(self): """get the energy_history (default: energy_history = _stepmon.y)""" if self._energy_history is None: return self._stepmon.y return self._energy_history def __set_energy_history(self, energy): """set the energy_history (energy=None will sync with _stepmon.y)""" self._energy_history = energy return def __solution_history(self): """get the solution_history (default: solution_history = _stepmon.x)""" if self._solution_history is None: return self._stepmon.x return self._solution_history def __set_solution_history(self, params): """set the solution_history (params=None will sync with _stepmon.x)""" self._solution_history = params return def __bestSolution(self): """get the bestSolution (default: bestSolution = population[0])""" if self._bestSolution is None: return self.population[0] return self._bestSolution def __set_bestSolution(self, params): """set the bestSolution (params=None will sync with population[0])""" self._bestSolution = params return def __bestEnergy(self): """get the bestEnergy (default: bestEnergy = popEnergy[0])""" if self._bestEnergy is None: return self.popEnergy[0] return self._bestEnergy def __set_bestEnergy(self, energy): """set the bestEnergy (energy=None will sync with popEnergy[0])""" self._bestEnergy = energy return def SetPenalty(self, penalty): """apply a penalty function to the optimization input:: - a penalty function of the form: y' = penalty(xk), with y = cost(xk) + y', where xk is the current parameter vector. Ideally, this function is constructed so a penalty is applied when the desired (i.e. encoded) constraints are violated. Equality constraints should be considered satisfied when the penalty condition evaluates to zero, while inequality constraints are satisfied when the penalty condition evaluates to a non-positive number.""" if not penalty: self._penalty = lambda x: 0.0 elif not callable(penalty): raise TypeError, "'%s' is not a callable function" % penalty else: #XXX: check for format: y' = penalty(x) ? self._penalty = penalty return def SetConstraints(self, constraints): """apply a constraints function to the optimization input:: - a constraints function of the form: xk' = constraints(xk), where xk is the current parameter vector. Ideally, this function is constructed so the parameter vector it passes to the cost function will satisfy the desired (i.e. encoded) constraints.""" if not constraints: self._constraints = lambda x: x elif not callable(constraints): raise TypeError, "'%s' is not a callable function" % constraints else: #XXX: check for format: x' = constraints(x) ? self._constraints = constraints return def SetGenerationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each solver iteration""" from mystic.monitors import Null, Monitor #, CustomMonitor current = Null() if new else self._stepmon if isinstance(monitor, Monitor): # is Monitor() self._stepmon = monitor self._stepmon.prepend(current) elif isinstance(monitor, Null) or monitor == Null: # is Null() or Null self._stepmon = Monitor() #XXX: don't allow Null self._stepmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._stepmon = monitor #FIXME: need .prepend(current) else: raise TypeError, "'%s' is not a monitor instance" % monitor self.energy_history = self._stepmon.y self.solution_history = self._stepmon.x return def SetEvaluationMonitor(self, monitor, new=False): """select a callable to monitor (x, f(x)) after each cost function evaluation""" from mystic.monitors import Null, Monitor #, CustomMonitor current = Null() if new else self._evalmon if isinstance(monitor, (Null, Monitor)): # is Monitor() or Null() self._evalmon = monitor self._evalmon.prepend(current) elif monitor == Null: # is Null self._evalmon = monitor() self._evalmon.prepend(current) elif hasattr(monitor, '__module__'): # is CustomMonitor() if monitor.__module__ in ['mystic._genSow']: self._evalmon = monitor #FIXME: need .prepend(current) else: raise TypeError, "'%s' is not a monitor instance" % monitor return def SetStrictRanges(self, min=None, max=None): """ensure solution is within bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i] note:: SetStrictRanges(None) will remove strict range constraints""" if min is False or max is False: self._useStrictRange = False return #XXX: better to use 'defaultMin,defaultMax' or '-inf,inf' ??? if min == None: min = self._defaultMin if max == None: max = self._defaultMax # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] == None: min[i] = self._defaultMin[0] if max[i] == None: max[i] = self._defaultMax[0] min = asarray(min) max = asarray(max) if numpy.any((min > max), 0): raise ValueError, "each min[i] must be <= the corresponding max[i]" if len(min) != self.nDim: raise ValueError, "bounds array must be length %s" % self.nDim self._useStrictRange = True self._strictMin = min self._strictMax = max return def _clipGuessWithinRangeBoundary(self, x0): #FIXME: use self.trialSolution? """ensure that initial guess is set within bounds input:: - x0: must be a sequence of length self.nDim""" #if len(x0) != self.nDim: #XXX: unnecessary w/ self.trialSolution # raise ValueError, "initial guess must be length %s" % self.nDim x0 = asarray(x0) lo = self._strictMin hi = self._strictMax # crop x0 at bounds x0[x0 < lo] = lo[x0 < lo] x0[x0 > hi] = hi[x0 > hi] return x0 def SetInitialPoints(self, x0, radius=0.05): """Set Initial Points with Guess (x0) input:: - x0: must be a sequence of length self.nDim - radius: generate random points within [-radius*x0, radius*x0] for i!=0 when a simplex-type initial guess in required""" x0 = asfarray(x0) rank = len(x0.shape) if rank is 0: x0 = asfarray([x0]) rank = 1 if not -1 < rank < 2: raise ValueError, "Initial guess must be a scalar or rank-1 sequence." if len(x0) != self.nDim: raise ValueError, "Initial guess must be length %s" % self.nDim #slightly alter initial values for solvers that depend on randomness min = x0 * (1 - radius) max = x0 * (1 + radius) numzeros = len(x0[x0 == 0]) min[min == 0] = asarray([-radius for i in range(numzeros)]) max[max == 0] = asarray([radius for i in range(numzeros)]) self.SetRandomInitialPoints(min, max) #stick initial values in population[i], i=0 self.population[0] = x0.tolist() def SetRandomInitialPoints(self, min=None, max=None): """Generate Random Initial Points within given Bounds input:: - min, max: must be a sequence of length self.nDim - each min[i] should be <= the corresponding max[i]""" if min == None: min = self._defaultMin if max == None: max = self._defaultMax #if numpy.any(( asarray(min) > asarray(max) ),0): # raise ValueError, "each min[i] must be <= the corresponding max[i]" if len(min) != self.nDim or len(max) != self.nDim: raise ValueError, "bounds array must be length %s" % self.nDim # when 'some' of the bounds are given as 'None', replace with default for i in range(len(min)): if min[i] == None: min[i] = self._defaultMin[0] if max[i] == None: max[i] = self._defaultMax[0] import random #generate random initial values for i in range(len(self.population)): for j in range(self.nDim): self.population[i][j] = random.uniform(min[j], max[j]) def SetMultinormalInitialPoints(self, mean, var=None): """Generate Initial Points from Multivariate Normal. input:: - mean must be a sequence of length self.nDim - var can be... None: -> it becomes the identity scalar: -> var becomes scalar * I matrix: -> the variance matrix. must be the right size! """ from numpy.random import multivariate_normal assert (len(mean) == self.nDim) if var == None: var = numpy.eye(self.nDim) else: try: # scalar ? float(var) except: # nope. var better be matrix of the right size (no check) pass else: var = var * numpy.eye(self.nDim) for i in range(len(self.population)): self.population[i] = multivariate_normal(mean, var).tolist() return def enable_signal_handler(self): """enable workflow interrupt handler while solver is running""" self._handle_sigint = True def disable_signal_handler(self): """disable workflow interrupt handler while solver is running""" self._handle_sigint = False def _generateHandler(self, sigint_callback): """factory to generate signal handler Available switches:: - sol --> Print current best solution. - cont --> Continue calculation. - call --> Executes sigint_callback, if provided. - exit --> Exits with current best solution. """ def handler(signum, frame): import inspect print inspect.getframeinfo(frame) print inspect.trace() while 1: s = raw_input(\ """ Enter sense switch. sol: Print current best solution. cont: Continue calculation. call: Executes sigint_callback [%s]. exit: Exits with current best solution. >>> """ % sigint_callback) if s.lower() == 'sol': print self.bestSolution elif s.lower() == 'cont': return elif s.lower() == 'call': # sigint call_back if sigint_callback is not None: sigint_callback(self.bestSolution) elif s.lower() == 'exit': self._EARLYEXIT = True return else: print "unknown option : %s" % s return self.signal_handler = handler return def SetSaveFrequency(self, generations=None, filename=None, **kwds): """set frequency for saving solver restart file input:: - generations = number of solver iterations before next save of state - filename = name of file in which to save solver state note:: SetSaveFrequency(None) will disable saving solver restart file""" self._saveiter = generations #self._saveeval = evaluations self._state = filename return def SetEvaluationLimits(self, generations=None, evaluations=None, \ new=False, **kwds): """set limits for generations and/or evaluations input:: - generations = maximum number of solver iterations (i.e. steps) - evaluations = maximum number of function evaluations""" self._maxiter = generations self._maxfun = evaluations # backward compatibility if kwds.has_key('maxiter'): self._maxiter = kwds['maxiter'] if kwds.has_key('maxfun'): self._maxfun = kwds['maxfun'] # handle if new (reset counter, instead of extend counter) if new: if generations is not None: self._maxiter += self.generations else: self._maxiter = "*" #XXX: better as self._newmax = True ? if evaluations is not None: self._maxfun += self.evaluations else: self._maxfun = "*" return def _SetEvaluationLimits(self, iterscale=None, evalscale=None): """set the evaluation limits""" if iterscale is None: iterscale = 10 if evalscale is None: evalscale = 1000 N = len(self.population[0]) # usually self.nDim # if SetEvaluationLimits not applied, use the solver default if self._maxiter is None: self._maxiter = N * self.nPop * iterscale elif self._maxiter == "*": # (i.e. None, but 'reset counter') self._maxiter = (N * self.nPop * iterscale) + self.generations if self._maxfun is None: self._maxfun = N * self.nPop * evalscale elif self._maxiter == "*": self._maxfun = (N * self.nPop * evalscale) + self.evaluations return def CheckTermination(self, disp=False, info=False, termination=None): """check if the solver meets the given termination conditions Input:: - disp = if True, print termination statistics and/or warnings - info = if True, return termination message (instead of boolean) - termination = termination conditions to check against Note:: If no termination conditions are given, the solver's stored termination conditions will be used. """ if termination == None: termination = self._termination # check for termination messages msg = termination(self, info=True) lim = "EvaluationLimits with %s" % { 'evaluations': self._maxfun, 'generations': self._maxiter } # push solver internals to scipy.optimize.fmin interface if self._fcalls[0] >= self._maxfun and self._maxfun is not None: msg = lim #XXX: prefer the default stop ? if disp: print "Warning: Maximum number of function evaluations has "\ "been exceeded." elif self.generations >= self._maxiter and self._maxiter is not None: msg = lim #XXX: prefer the default stop ? if disp: print "Warning: Maximum number of iterations has been exceeded" elif msg and disp: print "Optimization terminated successfully." print " Current function value: %f" % self.bestEnergy print " Iterations: %d" % self.generations print " Function evaluations: %d" % self._fcalls[0] if info: return msg return bool(msg) def SetTermination(self, termination): """set the termination conditions""" #XXX: validate that termination is a 'condition' ? self._termination = termination return def _RegisterObjective(self, cost, ExtraArgs=None): """decorate cost function with bounds, penalties, monitors, etc""" if ExtraArgs == None: ExtraArgs = () self._fcalls, cost = wrap_function(cost, ExtraArgs, self._evalmon) if self._useStrictRange: for i in range(self.nPop): self.population[i] = self._clipGuessWithinRangeBoundary( self.population[i]) cost = wrap_bounds(cost, self._strictMin, self._strictMax) cost = wrap_penalty(cost, self._penalty) cost = wrap_nested(cost, self._constraints) # hold on to the 'wrapped' cost function self._cost = (cost, ExtraArgs) return cost def _bootstrap_decorate(self, cost=None, ExtraArgs=None): """HACK to enable not explicitly calling _RegisterObjective""" args = None if cost == None: # 'use existing cost' cost, args = self._cost # use args, unless override with ExtraArgs if ExtraArgs != None: args = ExtraArgs if self._cost[0] == None: # '_RegisterObjective not yet called' if args is None: args = () cost = self._RegisterObjective(cost, args) return cost def Step(self, cost=None, ExtraArgs=None, **kwds): """perform a single optimization iteration *** this method must be overwritten ***""" raise NotImplementedError, "an optimization algorithm was not provided" def SaveSolver(self, filename=None, **kwds): """save solver state to a restart file""" import dill if filename == None: # then check if already has registered file if self._state == None: # then create a new one import tempfile self._state = tempfile.mkstemp(suffix='.pkl')[-1] filename = self._state self._state = filename f = file(filename, 'wb') try: dill.dump(self, f, **kwds) self._stepmon.info('DUMPED("%s")' % filename) #XXX: before / after ? finally: f.close() return def __save_state(self, force=False): """save the solver state, if chosen save frequency is met""" # save the last iteration if force and bool(self._state): self.SaveSolver() return # save the zeroth iteration nonzero = True #XXX: or bool(self.generations) ? # after _saveiter generations, then save state iters = self._saveiter saveiter = bool(iters) and not bool(self.generations % iters) if nonzero and saveiter: self.SaveSolver() #FIXME: if _saveeval (or more) since last check, then save state #save = self.evaluations % self._saveeval return def __load_state(self, solver, **kwds): """load solver.__dict__ into self.__dict__; override with kwds""" #XXX: should do some filtering on kwds ? self.__dict__.update(solver.__dict__, **kwds) return def _exitMain(self, **kwds): """cleanup upon exiting the main optimization loop""" pass def _process_inputs(self, kwds): """process and activate input settings""" #allow for inputs that don't conform to AbstractSolver interface settings = \ {'callback':None, #user-supplied function, called after each step 'disp':0} #non-zero to print convergence messages [settings.update({i: j}) for (i, j) in kwds.items() if i in settings] # backward compatibility if kwds.has_key('EvaluationMonitor'): \ self.SetEvaluationMonitor(kwds.get('EvaluationMonitor')) if kwds.has_key('StepMonitor'): \ self.SetGenerationMonitor(kwds.get('StepMonitor')) if kwds.has_key('penalty'): \ self.SetPenalty(kwds.get('penalty')) if kwds.has_key('constraints'): \ self.SetConstraints(kwds.get('constraints')) return settings def Solve(self, cost=None, termination=None, sigint_callback=None, ExtraArgs=None, **kwds): """Minimize a 'cost' function with given termination conditions. Description: Uses an optimization algorith to find the minimum of a function of one or more variables. Inputs: cost -- the Python function or method to be minimized. Additional Inputs: termination -- callable object providing termination conditions. sigint_callback -- callback function for signal handler. ExtraArgs -- extra arguments for cost. Further Inputs: callback -- an optional user-supplied function to call after each iteration. It is called as callback(xk), where xk is the current parameter vector. [default = None] disp -- non-zero to print convergence messages. """ # HACK to enable not explicitly calling _RegisterObjective cost = self._bootstrap_decorate(cost, ExtraArgs) # process and activate input settings settings = self._process_inputs(kwds) for key in settings: exec "%s = settings['%s']" % (key, key) # set up signal handler import signal self._EARLYEXIT = False self._generateHandler(sigint_callback) if self._handle_sigint: signal.signal(signal.SIGINT, self.signal_handler) ## decorate cost function with bounds, penalties, monitors, etc #self._RegisterObjective(cost, ExtraArgs) #XXX: SetObjective ? # register termination function if termination is not None: self.SetTermination(termination) # the initital optimization iteration if not len(self._stepmon): # do generation = 0 self.Step() if callback is not None: callback(self.bestSolution) # initialize termination conditions, if needed self._termination(self) #XXX: call at generation 0 or always? # impose the evaluation limits self._SetEvaluationLimits() # the main optimization loop while not self.CheckTermination() and not self._EARLYEXIT: self.Step(**settings) if callback is not None: callback(self.bestSolution) else: self._exitMain() # handle signal interrupts signal.signal(signal.SIGINT, signal.default_int_handler) # log any termination messages msg = self.CheckTermination(disp=disp, info=True) if msg: self._stepmon.info('STOP("%s")' % msg) # save final state self.__save_state(force=True) return # extensions to the solver interface evaluations = property(__evaluations) generations = property(__generations) energy_history = property(__energy_history, __set_energy_history) solution_history = property(__solution_history, __set_solution_history) bestEnergy = property(__bestEnergy, __set_bestEnergy) bestSolution = property(__bestSolution, __set_bestSolution) pass
def Solve(self, cost, termination=None, ExtraArgs=(), **kwds): """Minimize a function using batch grid optimization. Description: Uses parallel mapping of solvers on a regular grid to find the minimum of a function of one or more variables. Inputs: cost -- the Python function or method to be minimized. Additional Inputs: termination -- callable object providing termination conditions. ExtraArgs -- extra arguments for cost. Further Inputs: sigint_callback -- callback function for signal handler. callback -- an optional user-supplied function to call after each iteration. It is called as callback(xk), where xk is the current parameter vector. [default = None] disp -- non-zero to print convergence messages. [default = 0] """ # process and activate input settings sigint_callback = kwds.pop('sigint_callback', None) settings = self._process_inputs(kwds) disp = settings['disp'] if 'disp' in settings else False echo = settings['callback'] if 'callback' in settings else None # for key in settings: # exec "%s = settings['%s']" % (key,key) if disp in ['verbose', 'all']: verbose = True else: verbose = False #------------------------------------------------------------- from python_map import python_map if self._map != python_map: #FIXME: EvaluationMonitor fails for MPI, throws error for 'pp' from mystic.monitors import Null evalmon = Null() else: evalmon = self._evalmon fcalls, cost = wrap_function(cost, ExtraArgs, evalmon) # set up signal handler #self._EARLYEXIT = False self._generateHandler(sigint_callback) # activate signal_handler import signal if self._handle_sigint: signal.signal(signal.SIGINT, self.signal_handler) # register termination function if termination is not None: self.SetTermination(termination) # get the nested solver instance solver = self._AbstractEnsembleSolver__get_solver_instance() #------------------------------------------------------------- nbins = self._nbins if len(self._strictMax): upper = list(self._strictMax) else: upper = list(self._defaultMax) if len(self._strictMin): lower = list(self._strictMin) else: lower = list(self._defaultMin) # generate arrays of points defining a grid in parameter space grid_dimensions = self.nDim bins = [] for i in range(grid_dimensions): step = abs(upper[i] - lower[i]) / nbins[i] bins.append([lower[i] + (j + 0.5) * step for j in range(nbins[i])]) # build a grid of starting points from mystic.math import gridpts initial_values = gridpts(bins) # run optimizer for each grid point from copy import deepcopy as _copy op = [_copy(solver) for i in range(len(initial_values))] #cf = [cost for i in range(len(initial_values))] vb = [verbose for i in range(len(initial_values))] cb = [echo for i in range(len(initial_values))] #XXX: remove? at = self.id if self.id else 0 # start at self.id id = range(at, at + len(initial_values)) # generate the local_optimize function def local_optimize(solver, x0, rank=None, disp=False, callback=None): from copy import deepcopy as _copy from mystic.tools import isNull solver.id = rank solver.SetInitialPoints(x0) if solver._useStrictRange: #XXX: always, settable, or sync'd ? solver.SetStrictRanges(min=solver._strictMin, \ max=solver._strictMax) # or lower,upper ? solver.Solve(cost, disp=disp, callback=callback) sm = solver._stepmon em = solver._evalmon if isNull(sm): sm = ([], [], [], []) else: sm = (_copy(sm._x), _copy(sm._y), _copy(sm._id), _copy(sm._info)) if isNull(em): em = ([], [], [], []) else: em = (_copy(em._x), _copy(em._y), _copy(em._id), _copy(em._info)) return solver, sm, em # map:: solver = local_optimize(solver, x0, id, verbose) results = self._map(local_optimize, op, initial_values, id, \ vb, cb, **self._mapconfig) # save initial state self._AbstractSolver__save_state() #XXX: HACK TO GET CONTENT OF ALL MONITORS # reconnect monitors; save all solvers from mystic.monitors import Monitor while results: #XXX: option to not save allSolvers? skip this and _copy _solver, _stepmon, _evalmon = results.pop() sm = Monitor() sm._x, sm._y, sm._id, sm._info = _stepmon _solver._stepmon.extend(sm) del sm em = Monitor() em._x, em._y, em._id, em._info = _evalmon _solver._evalmon.extend(em) del em self._allSolvers[len(results)] = _solver del results, _solver, _stepmon, _evalmon #XXX: END HACK # get the results with the lowest energy self._bestSolver = self._allSolvers[0] bestpath = self._bestSolver._stepmon besteval = self._bestSolver._evalmon self._total_evals = self._bestSolver.evaluations for solver in self._allSolvers[1:]: self._total_evals += solver.evaluations # add func evals if solver.bestEnergy < self._bestSolver.bestEnergy: self._bestSolver = solver bestpath = solver._stepmon besteval = solver._evalmon # return results to internals self.population = self._bestSolver.population #XXX: pointer? copy? self.popEnergy = self._bestSolver.popEnergy #XXX: pointer? copy? self.bestSolution = self._bestSolver.bestSolution #XXX: pointer? copy? self.bestEnergy = self._bestSolver.bestEnergy self.trialSolution = self._bestSolver.trialSolution #XXX: pointer? copy? self._fcalls = self._bestSolver._fcalls #XXX: pointer? copy? self._maxiter = self._bestSolver._maxiter self._maxfun = self._bestSolver._maxfun # write 'bests' to monitors #XXX: non-best monitors may be useful too self._stepmon = bestpath #XXX: pointer? copy? self._evalmon = besteval #XXX: pointer? copy? self.energy_history = None self.solution_history = None #from mystic.tools import isNull #if isNull(bestpath): # self._stepmon = bestpath #else: # for i in range(len(bestpath.y)): # self._stepmon(bestpath.x[i], bestpath.y[i], self.id) # #XXX: could apply callback here, or in exec'd code #if isNull(besteval): # self._evalmon = besteval #else: # for i in range(len(besteval.y)): # self._evalmon(besteval.x[i], besteval.y[i]) #------------------------------------------------------------- # restore default handler for signal interrupts signal.signal(signal.SIGINT, signal.default_int_handler) # log any termination messages msg = self.Terminated(disp=disp, info=True) if msg: self._stepmon.info('STOP("%s")' % msg) # save final state self._AbstractSolver__save_state(force=True) return