Source code for symfit.core.minimizers

import abc
from collections import namedtuple

from scipy.optimize import minimize
import sympy
import numpy as np

from .support import key2str, keywordonly
from .leastsqbound import leastsqbound
from .fit_results import FitResults

DummyModel = namedtuple('DummyModel', 'params')

[docs]class BaseMinimizer(object): """ ABC for all Minimizers. """
[docs] def __init__(self, objective, parameters): """ :param objective: Objective function to be used. :param parameters: List of :class:`~symfit.core.argument.Parameter` instances """ self.objective = objective self.params = parameters
[docs] @abc.abstractmethod def execute(self, **options): """ The execute method should implement the actual minimization procedure, and should return a :class:`~symfit.core.fit_results.FitResults` instance. :param options: options to be used by the minimization procedure. :return: an instance of :class:`~symfit.core.fit_results.FitResults`. """ pass
@property def initial_guesses(self): return [p.value for p in self.params]
[docs]class BoundedMinimizer(BaseMinimizer): """ ABC for Minimizers that support bounds. """ @property def bounds(self): return [(p.min, p.max) for p in self.params]
[docs]class ConstrainedMinimizer(BaseMinimizer): """ ABC for Minimizers that support constraints """ @keywordonly(constraints=None) def __init__(self, *args, **kwargs): constraints = kwargs.pop('constraints') super(ConstrainedMinimizer, self).__init__(*args, **kwargs) self.constraints = constraints
[docs]class GradientMinimizer(BaseMinimizer): """ ABC for Minizers that support the use of a jacobian """ @keywordonly(jacobian=None) def __init__(self, *args, **kwargs): jacobian = kwargs.pop('jacobian') super(GradientMinimizer, self).__init__(*args, **kwargs) self.jacobian = jacobian
[docs]class ScipyMinimize(object): """ Mix-in class that handles the execute calls to scipy.optimize.minimize. """ def __init__(self, *args, **kwargs): self.constraints = [] self.jacobian = None self.wrapped_jacobian = None super(ScipyMinimize, self).__init__(*args, **kwargs) self.wrapped_objective = self.wrap_func(self.objective)
[docs] def wrap_func(self, func): """ Given an objective function `func`, make sure it is always called via keyword arguments with the relevant parameter names. :param func: Function to be wrapped to keyword only calls. :return: wrapped function """ # parameters = {param.name: value for param, value in zip(self.params, values)} if func is None: return None def wrapped_func(values): parameters = key2str(dict(zip(self.params, values))) return func(**parameters) return wrapped_func
@keywordonly(tol=1e-9) def execute(self, bounds=None, jacobian=None, **minimize_options): ans = minimize( self.wrapped_objective, self.initial_guesses, bounds=bounds, constraints=self.constraints, jac=self.wrapped_jacobian, **minimize_options ) # Build infodic infodic = { 'nfev': ans.nfev, } fit_results = dict( model=DummyModel(params=self.params), popt=ans.x, covariance_matrix=None, infodic=infodic, mesg=ans.message, ier=ans.nit, objective_value=ans.fun, ) if 'hess_inv' in ans: try: fit_results['hessian_inv'] = ans.hess_inv.todense() except AttributeError: fit_results['hessian_inv'] = ans.hess_inv return FitResults(**fit_results)
[docs] @staticmethod def scipy_constraints(constraints, data): """ Returns all constraints in a scipy compatible format. :return: dict of scipy compatible statements. """ cons = [] types = { # scipy only distinguishes two types of constraint. sympy.Eq: 'eq', sympy.Ge: 'ineq', } for key, constraint in enumerate(constraints): # jac = make_jac(c, p) cons.append({ 'type': types[constraint.constraint_type], # Assume the lhs is the equation. 'fun': lambda p, x, c: c(*(list(x.values()) + list(p)))[0], # Assume the lhs is the equation. 'jac': lambda p, x, c: [component(*(list(x.values()) + list(p))) for component in c.numerical_jacobian[0]], 'args': (data, constraint) }) cons = tuple(cons) return cons
class ScipyGradientMinimize(ScipyMinimize, GradientMinimizer): def __init__(self, *args, **kwargs): super(ScipyGradientMinimize, self).__init__(*args, **kwargs) self.wrapped_jacobian = self.wrap_func(self.jacobian) def execute(self, **minimize_options): return super(ScipyGradientMinimize, self).execute(jacobian=self.wrapped_jacobian, **minimize_options) class BFGS(ScipyGradientMinimize): def execute(self, **minimize_options): return super(BFGS, self).execute(method='BFGS', **minimize_options) class SLSQP(ScipyGradientMinimize, ConstrainedMinimizer, BoundedMinimizer): def execute(self, **minimize_options): return super(SLSQP, self).execute( method='SLSQP', bounds=self.bounds, **minimize_options ) class LBFGSB(ScipyGradientMinimize, BoundedMinimizer): def execute(self, **minimize_options): return super(LBFGSB, self).execute( method='L-BFGS-B', bounds=self.bounds, **minimize_options ) class NelderMead(ScipyMinimize, BaseMinimizer): def execute(self, **minimize_options): return super(NelderMead, self).execute(method='Nelder-Mead', **minimize_options)
[docs]class MINPACK(ScipyMinimize, GradientMinimizer, BoundedMinimizer): """ Wrapper to scipy's implementation of MINPACK, since it is the industry standard. """ def __init__(self, *args, **kwargs): self.jacobian = None super(MINPACK, self).__init__(*args, **kwargs)
[docs] def execute(self, **minpack_options): """ :param minpack_options: Any named arguments to be passed to leastsqbound """ popt, pcov, infodic, mesg, ier = leastsqbound( self.wrapped_objective, # Dfun=self.jacobian, x0=self.initial_guesses, bounds=self.bounds, full_output=True, **minpack_options ) fit_results = dict( model=DummyModel(params=self.params), popt=popt, covariance_matrix=None, infodic=infodic, mesg=mesg, ier=ier, chi_squared=np.sum(infodic['fvec']**2), ) return FitResults(**fit_results)