# -*- coding: utf-8 -*-
"""
:Author: Dominic Hunt
"""
import logging
import numpy as np
import scipy as sp
import itertools
from fitAlgs.fitAlg import FitAlg
[docs]class Basinhopping(FitAlg):
"""The class for fitting data using scipy.optimise.basinhopping
Parameters
----------
fit_sim : fitAlgs.fitSims.FitSim instance, optional
An instance of one of the fitting simulation methods. Default ``fitAlgs.fitSims.FitSim``
fit_measure : string, optional
The name of the function used to calculate the quality of the fit.
The value it returns provides the fitter with its fitting guide. Default ``-loge``
fit_measure_args : dict, optional
The parameters used to initialise fit_measure and extra_fit_measures. Default ``None``
extra_fit_measures : list of strings, optional
List of fit measures not used to fit the model, but to provide more information. Any arguments needed for these
measures should be placed in fit_measure_args. Default ``None``
bounds : dictionary of tuples of length two with floats, optional
The boundaries for methods that use bounds. If unbounded methods are
specified then the bounds will be ignored. Default is ``None``, which
translates to boundaries of (0, np.inf) for each parameter.
boundary_excess_cost : str or callable returning a function, optional
The function is used to calculate the penalty for exceeding the boundaries.
Default is ``boundFunc.scalarBound()``
boundary_excess_cost_properties : dict, optional
The parameters for the boundary_excess_cost function. Default {}
method : string or list of strings, optional
The name of the fitting method or list of names of fitting methods or
name of list of fitting methods. Valid names found in the notes.
Default ``unconstrained``
number_start_points : int, optional
The number of starting points generated for each parameter.
Default 4
allow_boundary_fits : bool, optional
Defines if fits that reach a boundary should be considered the same way
as those that do not. Default is True
boundSensitivity : int, optional
Defines the smallest number of decimal places difference (so the
minimal difference) between a fit value and its related boundaries
before a fit value is considered different from a boundary. The default
is `5`. This is only valid if ``allow_boundary_fits`` is ``False``
Attributes
----------
Name : string
The name of the fitting method
unconstrained : list
The list of valid unconstrained fitting methods
constrained : list
The list of valid constrained fitting methods
Notes
-----
unconstrained = ['Nelder-Mead','Powell','CG','BFGS']
constrained = ['L-BFGS-B','TNC','SLSQP']
Custom fitting algorithms are also allowed in theory, but it has yet to be
implemented.
For each fitting function a set of different starting parameters will be
tried. These are the combinations of all the values of the different
parameters. For each starting parameter provided a set of number_start_points
starting points will be chosen, surrounding the starting point provided. If
the starting point provided is less than one it will be assumed that the
values cannot exceed 1, otherwise, unless otherwise told, it will be
assumed that they can take any value and will be chosen to be eavenly
spaced around the provided value.
See Also
--------
fitAlgs.fitAlg.fitAlg : The general fitting method class, from which this one inherits
filtAlgs.fitSims.fitSim : The general fitting class
scipy.optimise.basinhopping : The fitting class this wraps around
"""
unconstrained = ['Nelder-Mead', 'Powell', 'CG', 'BFGS']
constrained = ['L-BFGS-B', 'TNC', 'SLSQP']
def __init__(self, method=None,
number_start_points=4,
allow_boundary_fits=True,
boundary_fit_sensitivity=5,
**kwargs):
super(Basinhopping, self).__init__(**kwargs)
self.number_start_points = number_start_points
self.allow_boundary_fits = allow_boundary_fits
self.boundary_fit_sensitivity = boundary_fit_sensitivity
self._setType(method, self.boundaries)
self.fit_info['number_start_points'] = self.number_start_points
self.fit_info['allow_boundary_fits'] = self.allow_boundary_fits
self.fit_info['boundary_fit_sensitivity'] = self.boundary_fit_sensitivity
if self.methodSet is None:
self.fit_info['method'] = self.method
else:
self.fit_info['method'] = self.methodSet
self.iterBestParams = []
self.iterFuncValueMin = []
self.iterParameterAccept = []
[docs] def callback(self, x, f, accept):
"""
Used for storing the state after each stage of fitter
Parameters
----------
x : coordinates of the trial minimum
f : function value of the trial minimum
accept : whether or not that minimum was accepted
"""
self.iterBestParams.append(x)
self.iterFuncValueMin.append(f)
self.iterParameterAccept.append(accept)
[docs] def fit(self, simulator, model_parameter_names, model_initial_parameters):
"""
Runs the model through the fitting algorithms and starting parameters
and returns the best one.
Parameters
----------
simulator : function
The function used by a fitting algorithm to generate a fit for
given model parameters. One example is fitAlgs.fitAlg.fitness
model_parameter_names : list of strings
The list of initial parameter names
model_initial_parameters : list of floats
The list of the intial parameters
Returns
-------
best_fit_parameters : list of floats
The best fitting parameters
fit_quality : float
The quality of the fit as defined by the quality function chosen.
testedParams : tuple of two lists and a dictionary
The two lists are a list containing the parameter values tested, in the order they were tested, and the
fit qualities of these parameters. The dictionary contains the coordinates of the trial minimum, the
function value of the trial minimum and whether or not that minimum was accepted. Each is stored in a list.
See Also
--------
fitAlgs.fitAlg.fitness
"""
self.simulator = simulator
self.tested_parameters = []
self.tested_parameter_qualities = []
self.iterBestParams = []
self.iterFuncValueMin = []
self.iterParameterAccept = []
method = self.method
methodSet = self.methodSet
bounds = self.bounds
boundVals = self.boundVals
allow_boundary_fits = self.allow_boundary_fits
boundary_fit_sensitivity = self.boundary_fit_sensitivity
number_start_points = self.number_start_points
if bounds is None:
boundVals = [(0, float('Inf')) for i in model_initial_parameters]
bounds = {k: v for k, v in zip(model_parameter_names, boundVals)}
self.bounds = bounds
self.boundVals = np.array(boundVals)
if boundVals is None:
boundVals = np.array([bounds[k] for k in model_parameter_names])
self.boundVals = boundVals
initParamSets = self.startParams(model_initial_parameters, bounds=boundVals, number_starting_points=number_start_points)
if method is None:
resultSet = []
methodSuccessSet = []
for method in methodSet:
optimizeResult = self._methodFit(method, initParamSets, boundVals, allow_boundary_fits=allow_boundary_fits)
if optimizeResult is not None:
resultSet.append(optimizeResult)
methodSuccessSet.append(method)
bestResult = self._bestfit(resultSet, boundVals, allow_boundary_fits=allow_boundary_fits, boundary_fit_sensitivity=boundary_fit_sensitivity)
if bestResult is None:
return model_initial_parameters, float("inf")
else:
best_fit_parameters = bestResult.x
fit_quality = bestResult.fun
return best_fit_parameters, fit_quality
else:
optimizeResult = self._methodFit(method, initParamSets, boundVals, allow_boundary_fits=allow_boundary_fits)
best_fit_parameters = optimizeResult.x
fit_quality = optimizeResult.fun
iterDetails = dict(bestParams=self.iterBestParams, funcVal=self.iterFuncValueMin, paramAccept=self.iterParameterAccept)
return best_fit_parameters, fit_quality, (self.tested_parameters, self.tested_parameter_qualities, iterDetails)
def _methodFit(self, method, initParamSets, bounds, allow_boundary_fits=True, boundary_fit_sensitivity=5):
resultSet = []
boundFunc = self._bounds
for i in initParamSets:
optimizeResult = sp.optimize.basinhopping(self.fitQualFunc, i[:],
accept_test=boundFunc,
callback=self.callback
# minimizer_kwargs={'method': method,
# 'bounds': bounds}
# }
)
resultSet.append(optimizeResult)
bestResult = self._bestfit(resultSet, bounds, allow_boundary_fits=allow_boundary_fits, boundary_fit_sensitivity=boundary_fit_sensitivity)
return bestResult
def _bestfit(self, resultSet, bounds, allow_boundary_fits=True, boundary_fit_sensitivity=5):
# Check that there are fits
if len(resultSet) == 0:
return None
genFitVal, genFitid = min((r.fun, idx) for (idx, r) in enumerate(resultSet))
# Debug code
# data = {}
# data["fitVal"] = np.array([o.fun for o in resultSet])
# data['nIter'] = np.array([o.nit for o in resultSet])
# data['parameters'] = np.array([o.x for o in resultSet])
# data['nfev'] = np.array([o.nfev for o in resultSet])
# data['message'] = np.array([o.message for o in resultSet])
# data['minimization_failures'] = np.array([o.minimization_failures for o in resultSet])
# print(np.array([data['parameters'].T[0], data['parameters'].T[1], data["fitVal"]]).T)
# pytest.set_trace()
# If boundary fits are acceptable
if allow_boundary_fits:
return resultSet[genFitid]
else:
reducedResults = []
for r in resultSet:
invalid = [1 for fitVal, boundVals in zip(r.x, bounds) if any(np.around(fitVal-boundVals, boundary_fit_sensitivity) == 0)]
if 1 not in invalid:
reducedResults.append(r)
if len(reducedResults) == 0:
return resultSet[genFitid]
else:
fitVal, fitid = min((r.fun, idx) for (idx, r) in enumerate(reducedResults))
return reducedResults[fitid]
def _setType(self, method, bounds):
self.method = None
self.methodSet = None
self.bounds = None
if isinstance(method, list):
self.methodSet = method
self.bounds = bounds
elif method in self.unconstrained:
self.method = method
elif method in self.constrained:
self.method = method
self.bounds = bounds
elif callable(method):
self.method = method
self.bounds = bounds
elif method == 'constrained':
self.methodSet = self.constrained
self.bounds = bounds
elif method == 'unconstrained':
self.methodSet = self.unconstrained
else:
self.methodSet = self.unconstrained
def _bounds(self, **kwargs):
"""
Based on http://docs.scipy.org/doc/scipy-dev/reference/generated/scipy.optimize.basinhopping.html
"""
boundArr = self.boundVals
x = kwargs["x_new"]
tmax = bool(np.all(x < boundArr[:, 1]))
tmin = bool(np.all(x > boundArr[:, 0]))
return tmax and tmin