Source code for Stoner.analysis.fitting.mixins

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Fitting Functions and classes to mixin for :py:class:`Stoner.Data`."""

__all__ = ["odr_Model", "FittingMixin"]

from copy import deepcopy as copy
from inspect import isclass, getfullargspec
from collections.abc import Mapping

import numpy as np
import numpy.ma as ma
import scipy as sp
from scipy.odr import Model as odrModel
from scipy.optimize import curve_fit, differential_evolution

import lmfit
from lmfit.model import Model

from ...compat import string_types, index_types, get_func_params
from ...tools import isnone, isiterable, isLikeList, AttributeStore, ordinal

_lmfit = True


[docs]class odr_Model(odrModel): """A wrapper for converting lmfit models to odr models.""" def __init__(self, *args, **kargs): """Initialise with lmfit.models.Model or callable.""" meta = kargs.pop("meta", {}) kargs = copy(kargs) for n in list(kargs.keys()): if n in ["replace", "header", "result", "output", "residuals", "prefix"]: del kargs[n] p0 = kargs.pop("p0", kargs.pop("estimate", None)) if args: args = list(args) model = args.pop(0) else: raise RuntimeError("Need at least one argument to make a fitting model." "") if isclass(model) and issubclass(model, Model): # Instantiate if only a class passed in model = model() if isclass(model) and issubclass(model, odrModel): model = model() if isinstance(model, Model): self.model = model self.func = model.func modelfunc = lambda beta, x, **kargs: self.func(x, *beta, **kargs) meta["param_names"] = self.model.param_names meta["param_hints"] = self.model.param_hints meta["name"] = type(self.model).__name__ elif isinstance(model, odrModel): self.model = model meta.update(model.meta) meta["param_names"] = model.meta.pop("param_names", [f"Param_{ix}" for ix, p in enumerate(p0)]) meta["name"] = model.fcn.__name__ modelfunc = model.fcn self.model.meta.update(meta) elif callable(model): self.model = None meta["name"] = model.__name__ arguments = getfullargspec(model)[0] # pylint: disable=W1505 meta["param_names"] = list(arguments[1:]) meta["param_hints"] = {x: {"value": 1.0} for x in arguments[1:]} # print(arguments,carargs,jeywords,defaults) self.func = model def modelfunc(beta, x, **_): # pylint: disable=E0102 """Warapper for model function.""" return model(x, *beta) meta["__name__"] = meta["name"] else: raise ValueError( "".join( [ f"Cannot construct a model instance from a {model} - ", "need a callable, lmfit.Model or scipy.odr.Model", ] ) ) if not isinstance(p0, lmfit.Parameters): # This can happen if we are creating an odr_Model in advance. tmp_model = AttributeStore(meta) p0 = _prep_lmfit_p0(tmp_model, None, None, p0, kargs)[0] p_new = list() meta["params"] = copy(p0) for p in p0.values(): p_new.append(p.value) p0 = p_new kargs["estimate"] = p0 kargs["meta"] = meta super().__init__(modelfunc, *args, **kargs) @property def p0(self): """Convert an estimate attribute as p0.""" return getattr(self, "estimate", None) @property def param_names(self): """Convert the meta parameter key param_names to an attribute.""" return self.meta["param_names"]
class MimizerAdaptor: """Work with an lmfit.Model or generic callable to use with scipy.optimize global minimization functions. The :pymod:`scipy.optimize` module's minimizers generally expect functions which take an array like parameter space variable and then other arguments. This class will produce a suitable wrapper function and bounds variables from information int he lmfit.Model. """ def __init__(self, model, *args, **kargs): # pylint: disable=unused-argument """Prepare the wrapper from the minimuzer. Args: modelower (lmfit): The model that has been fitted. *args (tuple): Positional parameters to initialise class. Keyword Arguments: params (lmfit:parameter or dict): Parameters used to fit model. **kargs (dict): Keyword arguments to initialise the result object/. Raises: RuntimeError: Fails if a *params* Parameter does not supply a fitted value. """ self.func = model.func hints = kargs.pop("params") p0 = list() upper = list() lower = list() for name, hint in hints.items(): if not isinstance(hint, lmfit.Parameter): hint = lmfit.Parameter(**hint) if not hasattr(hint, "value"): raise RuntimeError(f"At the very least we need a starting value for the {name} parameter") v = hint.value p0.append(v) limits = [v * 10, v * 0.1] hint_upper = getattr(hint, "max", max(limits)) hint_lower = getattr(hint, "min", min(limits)) upper.append(hint_upper if not np.isinf(hint_upper) else max(limits)) lower.append(hint_lower if not np.isinf(hint_lower) else min(limits)) self.p0 = p0 self.bounds = [ix for ix in zip(lower, upper)] def wrapper(beta, x, y, sigma, *args): """Calculate a least-squares goodness from the model functiuon.""" beta = tuple(beta) + tuple(args) if sigma is None: sigma = np.ones_like(x) sigma = sigma / sigma.sum() # normalise uncertainties sigma += np.finfo(float).eps weights = 1.0 / sigma**2 variance = ((y - self.func(x, *beta)) ** 2) * weights return np.sum(variance) / (len(x) - len(beta)) self.minimize_func = wrapper class _curve_fit_result: """Represent a result from fitting using :py:func:`scipy.optimize.curve_fit` as a class to make handling easier. """ def __init__(self, popt, pcov, infodict, mesg, ier): """Store the results of the curve fit full_output fit. Args: popt (1D array): Optimal parameters for fit. pcov (2D array): Variance-co-variance matrix. infodict (dict): Additional information from curve_fit. mesg (str): Descriptive information from curve_fit. ier (int): Numerical error message. """ self.popt = popt self.pcov = pcov self.perr = np.sqrt(np.diag(pcov)) self.mesg = mesg self.ier = ier self.nfev = None self.fvec = None self.fjac = None self.ipvt = None self.qtf = None self.func = None self.p0 = None self.residual_vals = None self.chisq = None self.nfree = None self.infodict = infodict for k in infodict: setattr(self, k, infodict[k]) # Following properties used to return desired information @property def name(self): """Name of the model fitted.""" return self.func.__name__ @property def dict(self): """Optimal parameters and errors as a Dictionary.""" ret = {} for p, v, e in zip(self.params, self.popt, self.perr): ret[p] = v ret["d_{}", format(p)] = e ret["chi-square"] = self.chisqr ret["red. chi-sqr"] = self.redchi ret["nfev"] = self.nfev return ret @property def full(self): """Return the same as :py:attr:`_curve_fit_result.row`.""" return self, self.row @property def row(self): """Optimal parameters and errors as a single row.""" ret = np.zeros(self.popt.size * 2) ret[0::2] = self.popt ret[1::2] = self.perr return ret @property def fit(self): """Copy of the fit report and optimal parameters and covariance.""" return (self.popt, self.pcov) @property def data(self): """Return the data that was fitted.""" self._data = getattr(self, "_data", np.array([])) return self._data @data.setter def data(self, data): """Return the data that was fitted.""" self._data = data @property def report(self): """Copy of the fit report.""" return self @property def N(self): """Return th number of data points in dataset.""" return len(self.data) @property def n_p(self): """Return the number of parameters in model.""" return len(self.popt) @property def redchi(self): r"""Reduced $\chi^2$ Statistic.""" return self.chisq @property def chisqr(self): r"""$\chi^2$ Statistic.""" return self.chisq * (self.N - self.n_p) @property def aic(self): """Return the Akaike Information Criterion statistic.""" return self.N * np.log(self.chisqr / self.N) + 2 * self.n_p @property def bic(self): """Return the Bayesian Information Criterion statistic.""" return self.N * np.log(self.chisqr / self.N) + np.log(self.N) * self.n_p @property def params(self): """List the parameter class objects.""" return get_func_params(self.func) def fit_report(self): """Create a Fit report like lmfit does.""" template = f"""[[ Model ]] {self.name} [[ Fit Statistics ]] # function evals = {self.nfev} # data points = {self.N} # variables = {self.n_p} chi-square = {self.chisqr} reduced chi-square = {self.redchi} Akaike info crit = {self.aic} Bayesian info crit = {self.bic} [[ Variables ]]\n""" for p, v, e, p0 in zip(self.params, self.popt, self.perr, self.p0): template += f"\t{p}: {v} +/- {e} ({(e * 100 / v):.3f}%) (init {p0})\n" template += "[[Correlations]] (unreported correlations are < 0.100)\n" for i, p in enumerate(self.params): for j in range(i + 1, len(self.params)): if np.abs(self.pcov[i, j]) > 0.1: template += f"\t({p},{list(self.params)[j]})\t\t={self.pcov[i, j]:.3f}" return template def _get_model_parnames(model): """Get a list of the model parameter names.""" if isinstance(model, type) and (issubclass(model, Model) or issubclass(model, odrModel)): model = Model() if isinstance(model, Model): return model.param_names if isinstance(model, odrModel): if "param_names" in model.meta: return model.meta["param_names"] model = model.fcn if not callable(model): raise ValueError( "".join( [ f"Unrecognised type for model! - should be lmfit.Model, scipy.odr.Model", f" or callable, not {type(model)}", ] ) ) arguments = getfullargspec(model)[0] # pylint: disable=W1505 return list(arguments[1:]) def _curve_fit_p0_list(p0, model): """Take something containing an initial vector and turns it into a list for curve_fit. Args: model (callable, lmfit/Model, odr.Model): miodel object for parameter names o0 (list,array,type or Mapping): Object containing the parameter gues values Returns: A list of starting values in the order in which they appear in the model. """ if p0 is None: return p0 if isinstance(p0, Mapping): p_new = {} for x, v in p0.items(): p_new[x] = getattr(v, "value", float(v)) ret = [] for x in _get_model_parnames(model): ret.append(p_new.get(x, None)) return ret if isiterable(p0): return [float(x) for x in p0] raise RuntimeError("Shouldn't have returned None from _curve_fit_p0_list!") def _prep_lmfit_model(model, kargs): """Prepare an lmfit model instance. Arguments: model (lmfit Model class or instance, or callable): the model to be fitted to the data. p0 (iterable or floats): The initial values of the fitting parameters. kargs (dict):Other keyword arguments passed to the fitting function Returns: model,p0, prefix (lmfit.Model instance, iterable, str) Converts the model parameter into an instance of lmfit.Model - either by instantiating the class or wrapping a callable into an lmfit.Model class and establishes a prefix string from the model if not provided in the keyword arguments. """ if Model is None: # Will be the case if lmfit is not imported. raise RuntimeError( """To use the lmfit function you need to be able to import the lmfit module\n Try pip install lmfit\nat a command prompt.""" ) # Enure that model is an instance of an lmfit.Model() class if isinstance(model, Model): pass elif isclass(model) and issubclass(model, Model): model = model(nan_policy="propagate") elif callable(model): model = Model(model) else: raise TypeError(f"{model} must be an instance of lmfit.Model or a cllable function!") # Nprmalise p0 to be lmfit.Parameters # Get a default prefix for the model prefix = str(kargs.pop("prefix", type(model).__name__)) return model, prefix def _prep_lmfit_p0(model, ydata, xdata, p0, kargs): """Prepare the initial start vector for an lmfit. Arguments: model (lmfit.Model instance): model to fit with ydata,xdata (array): y and x data ppoints for fitting p0 (iterable of float): Existing p0 vector if defined kargs (dict): Other keyword arguments for the lmfit method. Returns: p0,single_fit (iterable of floats, bool): The revised initial starting vector and whether this is a single fit operation. """ single_fit = True if p0 is None: # First guess the p0 values using the model if isinstance(model, odrModel): p0 = model.estimate else: for p_name in model.param_names: if p_name in kargs: model.set_param_hint(p_name, value=kargs.get(p_name)) try: p0 = model.guess(ydata, x=xdata) except Exception: # pylint: disable=W0703 Don't be fussy here p0 = lmfit.Parameters() for p_name in model.param_names: if p_name in kargs: p0[p_name] = lmfit.Parameter(name=p_name, value=kargs.get(p_name)) single_fit = True if callable(p0): p0 = p0(ydata, xdata) if isinstance(p0, (list, tuple)): p0 = np.array(p0) if isinstance(p0, np.ndarray) and (p0.ndim == 1 or (p0.ndim == 2 and np.max(p0.shape) == p0.size)): single_fit = True p_new = lmfit.Parameters() p0 = p0.ravel() for n, v in zip(model.param_names, p0): if hasattr(model, "param_hints"): hint = model.param_hints.get(n, {}) else: hint = {} hint["value"] = v hint["name"] = n p_new[n] = lmfit.Parameter(**hint) p0 = p_new for p_name in model.param_names: if p_name in kargs: p0[p_name] = lmfit.Parameter(p_name, value=kargs.pop(p_name)) elif isinstance(p0, np.ndarray) and p0.ndim == 2: # chi^2 mapping single_fit = False return p0, single_fit if not isinstance(p0, lmfit.Parameters): raise RuntimeError(f"Unknown data type for initial guess vector p0: {type(p0)}") if set(p0.keys()) < set(model.param_names): raise RuntimeError( f"Missing some values from the initial guess vector p0: {set(model.param_names) - set(p0.keys())}" ) return p0, single_fit
[docs]class FittingMixin: """A mixin calss for :py:class:`Stoner.Core.DataFile` to provide additional curve_fiotting methods."""
[docs] def annotate_fit(self, model, x=None, y=None, z=None, text_only=False, **kargs): """Annotate a plot with some information about a fit. Args: mode (callable or lmfit.Model): The function/model used to describe the fit to be annotated. Keyword Parameters: x (float): x coordinate of the label y (float): y coordinate of the label z (float): z co-ordinbate of the label if the current axes are 3D prefix (str): The prefix placed ahead of the model parameters in the metadata. text_only (bool): If False (default), add the text to the plot and return the current object, otherwise, return just the text and don't add to a plot. prefix(str): If given overridges the prefix from the model to determine a prefix to the parameter names in the metadata Returns: (Datam, str): A copy of the current Data instance if text_only is False, otherwise returns the text. If *prefix* is not given, then the first prefix in the metadata lmfit.prefix is used if present, otherwise a prefix is generated from the model.prefix attribute. If *x* and *y* are not specified then they are set to be 0.75 * maximum x and y limit of the plot. """ mode = kargs.pop("mode", "float") if isclass(model) and ((_lmfit and issubclass(model, Model)) or issubclass(model, odrModel)): model = model() # Instantiate a bare class first if isinstance(model, odrModel): # Get predix from odrModel model_prefix = model.meta.get("__name__", type(model).__name__) prefix = kargs.pop("prefix", self.get("odr.prefix", model_prefix)) param_names = model.meta.get("param_names", []) display_names = model.meta.get("display_names", param_names) units = model.meta.get("units", [""] * len(param_names)) elif _lmfit and isinstance(model, Model): # Get prefix from lmfit prefix = kargs.pop("prefix", self.get("lmfit.prefix", type(model).__name__)) param_names = model.param_names display_names = getattr(model, "display_names", model.param_names) units = getattr(model, "units", [""] * len(param_names)) elif callable(model): # Get prefix from callable name prefix = kargs.pop("prefix", model.__name__) model = Model(model) param_names = model.param_names display_names = getattr(model, "display_names", model.param_names) units = getattr(model, "units", [""] * len(param_names)) else: raise RuntimeError(f"model should be either an lmfit.Model or a callable function, not a {type(model)}") if prefix is not None: if isinstance(prefix, (list, tuple)): prefix = prefix[0] prefix = prefix.strip(" :") prefix = "" if prefix == "" else prefix + ":" else: if isinstance(prefix, (list, tuple)): prefix = prefix[0] if model.prefix == "": prefix = "" else: prefix = model.prefix + ":" x = 0.75 if x is None else x y = 0.5 if y is None else y try: # if the model has an attribute display params then use these as the parameter anmes for k, display_name, unit in zip(param_names, display_names, units): if prefix: self[f"{prefix}{k} label"] = display_name self[f"{prefix}{k} units"] = unit else: self[f"{k} label"] = display_name self[f"{k} units"] = unit except (AttributeError, KeyError): pass text = "\n".join([self.format(f"{prefix}{k}", fmt="latex", mode=mode) for k in model.param_names]) try: self[f"{prefix}chi^2 label"] = r"\chi^2" text += "\n" + self.format(f"{prefix}chi^2", fmt="latex", mode=mode) except KeyError: pass if not text_only: ax = self.fig.gca() if "zlim" in ax.properties(): # 3D plot then if z is None: zb, zt = ax.properties()["zlim"] z = 0.5 * (zt - zb) + zb ax.text3D(x, y, z, text) elif "arrowprops" in kargs: ax.annotate(text, xy=(x, y), **kargs) else: kargs.pop("xycoords", None) kargs["transform"] = ax.transAxes ax.text(x, y, text, **kargs) ret = self else: ret = text return ret
def _get_curve_fit_data(self, xcol, ycol, bounds, sigma): """Gather up the xdata and sigma columns for curve_fit.""" working = self.search(xcol, bounds) working = ma.mask_rowcols(working, axis=0) if not isnone(sigma): sigma = working[:, self.find_col(sigma)] else: sigma = None if isinstance(xcol, string_types): xdat = working[:, self.find_col(xcol)] elif isiterable(xcol): for ix, c in enumerate(xcol): if ix == 0: xdat = working[:, self.find_col(c)] else: xdat = np.column_stack((xdat, working[:, self.find_col(c)])) else: xdat = working[:, self.find_col(xcol)] for i, yc in enumerate(ycol): if isinstance(yc, index_types): ydat = working[:, self.find_col(yc)] elif isinstance(yc, np.ndarray) and yc.ndim == 1 and len(yc) == len(self): ydat = yc else: raise RuntimeError( """Y-data for fitting not defined - should either be an index or a 1D numpy array of the same length as the dataset""" ) if i == 0: ydata = np.atleast_2d(ydat) else: ydata = np.row_stack([ydata, ydat]) return xdat, ydata, sigma def _assemnle_data_to_fit(self, xcol, ycol, sigma, bounds, scale_covar, sigma_x=None): """Marshall the data for doing a curve_fit or equivalent. Parameters: xcol (index): Column with xdata in it ycol(index): Column with ydata in it sigma (index or array-like): column of y-errors or uncertainty values. bounds (callable): Used to select the data rows to fit scale_covar (bool,None): If set the flag to scale the covariance. Returns: (data,scale_covar,col_assignments): data is a tuple of (x,y,sigma). scale_covar is False if sigma is real errors. """ _ = self._col_args(xcol=xcol, ycol=ycol) working = self.search(_.xcol, bounds) working = ma.mask_rowcols(working, axis=0) xdata = working[:, self.find_col(_.xcol)] ydata = working[:, self.find_col(_.ycol)] # Now check for sigma_y and sigma_x and have them default to sigma (which in turn defaults to None) if sigma is not None: if isinstance(sigma, index_types): _ = self._col(xcol=xcol, ycol=ycol, yerr=sigma) sigma = working[:, self.find_col(sigma)] elif isinstance(sigma, (list, tuple)): sigma = ma.array(sigma) elif isinstance(sigma, np.ndarray): sigma = ma.array(sigma) # ensure masked else: raise RuntimeError("Sigma should have been a column index or list of values") elif not isnone(_.yerr): sigma = working[:, self.find_col(_.yerr)] else: sigma = ma.ones(len(xdata)) scale_covar = True if sigma_x is None: if _.xerr is None: sigma_x = sigma else: sigma_x = working[:, self.find_col(_.xerr)] else: if isinstance(sigma_x, index_types): _ = self._col(xcol=xcol, ycol=ycol, yerr=_.yerr, xerr=sigma_x) sigma_x = working[:, self.find_col(sigma_x)] elif isinstance(sigma_x, (list, tuple)): sigma_x = ma.array(sigma_x) elif isinstance(sigma_x, np.ndarray): sigma_x = ma.array(sigma_x) # ensure masked else: raise RuntimeError("Sigma_x should have been a column index or list of values") mask = np.invert(ydata.mask) sigma = sigma[mask] ydata = ydata[mask] xdata = xdata[mask] # lmfit doesn't seem to work well with masked data - here we just delete masked points return (xdata, ydata, sigma, sigma_x), scale_covar, _ def __lmfit_one(self, model, data, params, prefix, columns, scale_covar, **kargs): """Carry out a single fit wioth lmfit. Args: model (lmfit.Model): Configured model data (tuple of xdata,ydata,sigma): Data and errors to use in fitting params (lmfit.Parameters): The parameters to use on the model for the fitting. prefix (str): Prefix for labels in metadata columns (attribute dict): Column assignments scale_covat (bool): Whether sigmas are absolute or relative. Keyword Arguments: result (bool,str): Where the result goes header (str): Name of new data column if used replace (bool): whether to add new dataa output (str): What to return Returns: (various): Results froma fit or raises and exception. """ if not _lmfit: raise RuntimeError("lmfit module not available.") replace = kargs.pop("replace", False) result = kargs.pop("result", False) header = kargs.pop("header", "") residuals = kargs.pop("residuals", False) output = kargs.pop("output", "row") nan_policy = kargs.pop("nan_policy", "raise") kargs[model.independent_vars[0]] = data[0] fit = model.fit( data[1], params, scale_covar=scale_covar, weights=1.0 / data[2], nan_policy=nan_policy, **kargs ) if fit.success: row = self._record_curve_fit_result( model, fit, columns.xcol, header, result, replace, residuals=residuals, prefix=prefix, ycol=columns.ycol, ) res_dict = {} for p in fit.params: res_dict[p] = fit.params[p].value res_dict[f"d_{p}"] = fit.params[p].stderr res_dict["chi-square"] = fit.chisqr res_dict["red. chi-sqr"] = fit.redchi res_dict["nfev"] = fit.nfev retval = { "fit": (row[::2], fit.covar), "report": fit, "row": row, "full": (fit, row), "data": self, "dict": res_dict, } if output not in retval: raise RuntimeError(f"Failed to recognise output format:{output}") else: return retval[output] else: raise RuntimeError(f"Failed to complete fit. Error was:\n{fit.lmdif_message}\n{fit.message}") def _record_curve_fit_result( self, func, fit, xcol, header, result, replace, residuals=False, ycol=None, prefix=None ): """Annotate the DataFile object with the curve_fit result.""" if isinstance(func, (lmfit.Model)): f_name = type(func).__name__ labels = getattr(type(func), "labels", None) units = getattr(type(func), "units", None) func = func.func args = getfullargspec(func)[0] # pylint: disable=W1505 if args[0] == "self": del args[0] if len(args) > 0: del args[0] elif isclass(func) and issubclass(func, lmfit.Model): f_name = func.__name__ labels = getattr(func, "labels", None) units = getattr(func, "units", None) args = getfullargspec(func)[0] # pylint: disable=W1505 func = func.func if args[0] == "self": del args[0] if len(args) > 0: del args[0] elif isinstance(func, (sp.odr.Model)): f_name = func.meta["name"] labels = getattr(func, "labels", None) units = getattr(func, "units", None) args = func.meta["param_names"] model = func def _func(x, *beta): return model.fcn(beta, x) func = _func else: f_name = func.__name__ labels = getattr(func, "labels", None) units = getattr(func, "units", None) args = getfullargspec(func)[0] # pylint: disable=W1505 if args[0] == "self": del args[0] if len(args) > 0: del args[0] if prefix is not None: f_name = prefix if labels is None: labels = args if units is None: units = [""] * len(args) if isinstance(fit, _curve_fit_result): # Come from curve_fit popt = fit.popt perr = fit.perr nfev = fit.nfev chisq = fit.chisq elif isinstance(fit, lmfit.model.ModelResult): # Come form an lmfit operation popt = [fit.params[x].value for x in args] perr = [fit.params[x].stderr for x in args] nfev = fit.nfev chisq = fit.redchi elif isinstance(fit, sp.odr.Output): popt = fit.beta perr = fit.sd_beta delta, eps = fit.delta, fit.eps nfree = len(delta) - len(popt) chisq = np.sum((delta**2 + eps**2)) / nfree nfev = None elif isinstance(fit, sp.optimize.OptimizeResult): popt = fit.popt perr = fit.perr nfev = fit.nfev nfree = len(self) - len(popt) data = self.data[:, ycol] fit_data = func(self.data[:, xcol], *popt) chisq = np.sum((data - fit_data) ** 2) / nfree else: raise RuntimeError("Unable to understand {type(fit)} as a fitting result") for val, err, name, label, unit in zip(popt, perr, args, labels, units): self[f"{f_name}:{name}"] = val self[f"{f_name}:{name} err"] = err self[f"{f_name}:{name} label"] = self.metadata.get(f"{f_name}:{name} label", label) self[f"{f_name}:{name} unit"] = self.metadata.get(f"{f_name}:{name} unit", unit) if not isinstance(header, string_types): header = "Fitted with " + func.__name__ # Store our current mask, calculate new column's mask and turn off mask tmp_mask = self.mask col_mask = np.any(tmp_mask, axis=1) self.mask = False if isinstance(result, bool) and result: # Appending data to end of data result = self.shape[1] tmp_mask = np.column_stack((tmp_mask, col_mask)) else: # Inserting data tmp_mask = np.column_stack((tmp_mask[:, 0:result], col_mask, tmp_mask[:, result:])) if isLikeList(xcol): new_col = func(self[:, xcol], *popt) else: new_col = func(self.column(xcol), *popt) if result: self.add_column(new_col, index=result, replace=replace, header=header) if residuals and result: if not isLikeList(ycol): ycol = [ycol] for yc in ycol: residual_vals = self.column(yc) - new_col if isinstance(residuals, bool) and residuals: if result is None: residuals_idx = None else: residuals_idx = self.find_col(result) + 1 else: residuals_idx = residuals self.add_column(residual_vals, index=residuals_idx, replace=False, header=header + ":residuals") self[f"{f_name}:mean residual"] = np.mean(residual_vals) self[f"{f_name}:std residual"] = np.std(residual_vals) self[f"{f_name}:chi^2"] = chisq self[f"{f_name}:chi^2 err"] = np.sqrt(2 / len(residual_vals)) * chisq if nfev is not None: self[f"{f_name}:nfev"] = nfev self.mask = tmp_mask # Make row object row = [] ch = [] for v, e, a in zip(popt, perr, args): row.extend([v, e]) ch.extend([a, f"{a} stderr"]) row.append(chisq) ch.append("$\\chi^2$") cls = type(self.data) row = cls(row) row.column_headers = ch return row def _odr_one(self, data, model, prefix, _, **kargs): """Carry out a single fit wioth odr. Args: data (odr.Data): configured data model (odr.Model): Configured model prefix (str): Prefix for labels in metadata Keyword Arguments: result (bool,str): Where the result goes header (str): Name of new data column if used replace (bool): whether to add new dataa output (str): What to return Returns: (various): Results froma fit or raises and exception. """ result = kargs.pop("result", None) replace = kargs.pop("replace", False) header = kargs.pop("header", None) residuals = kargs.pop("residuals", False) asrow = kargs.pop("asrow", False) output = kargs.pop("output", "row" if asrow else "fit") fit = sp.odr.ODR(data, model, beta0=model.estimate) try: fit_result = fit.run() fit_result.redchi = fit_result.sum_square / (len(fit_result.y) - len(fit_result.beta)) fit_result.chisqr = fit_result.sum_square tmp = f"""Beta:{fit_result.beta} Beta Std Error:{fit_result.sd_beta} Beta Covariance:{fit_result.cov_beta} """ if hasattr(fit_result, "info"): tmp += f"""Residual Variance:{fit_result.res_var} Inverse Condition #:{fit_result.inv_condnum} Reason(s) for Halting: """ for r in fit_result.stopreason: tmp += f" {r}\n" tmp += f""""Sum of orthogonal distance (~chi^2):{fit_result.chisqr} Reduced Sum of Orthogonal distances (~reduced chi^2): {fit_result.redchi}""" fit_result.fit_report = lambda: tmp except sp.odr.OdrError as err: print(err) return None except sp.odr.OdrStop as err: print(err) return None self._record_curve_fit_result( model, fit_result, _.xcol, header, result, replace, residuals, ycol=_.ycol, prefix=prefix ) row = [] # Store our current mask, calculate new column's mask and turn off mask param_names = getattr(model, "param_names", None) ret_dict = {} for i, p in enumerate(param_names): row.extend([fit_result.beta[i], fit_result.sd_beta[i]]) ret_dict[p], ret_dict[f"d_{p}"] = fit_result.beta[i], fit_result.sd_beta[i] row.append(fit_result.redchi) ret_dict["chi-square"] = fit_result.chisqr ret_dict["red. chi-sqr"] = fit_result.redchi row = np.array(row) retval = { "fit": (row[::2], fit_result.cov_beta), "report": fit_result, "row": row, "full": (fit_result, row), "data": self, "dict": ret_dict, } if output not in retval: raise RuntimeError(f"Failed to recognise output format:{output}") return retval[output]
[docs] def curve_fit(self, func, xcol=None, ycol=None, sigma=None, **kargs): """General curve fitting function passed through from scipy. Args: func (callable, lmfit.Model, odr.Model): The fitting function with the form def f(x,*p) where p is a list of fitting parameters xcol (index, Iterable): The index of the x-column data to fit. If list or other iterable sends a tuple of x columns to func for N-d fitting. ycol (index, list of indices or array): The index of the y-column data to fit. If an array, then should be 1D and the same length as the data. If ycol is a list of indices then the columns are iterated over in turn, fitting occurring for each one. In this case the return value is a list of what would be returned for a single column fit. Keyword Arguments: p0 (list, tuple, array or callable): A vector of initial parameter values to try. See notes below. sigma (index): The index of the column with the y-error bars bounds (callable): A callable object that evaluates true if a row is to be included. Should be of the form f(x,y) result (bool): Determines whether the fitted data should be added into the DataFile object. If result is True then the last column will be used. If result is a string or an integer then it is used as a column index. Default to None for not adding fitted data replace (bool): Inidcatesa whether the fitted data replaces existing data or is inserted as a new column (default False) header (string or None): If this is a string then it is used as the name of the fitted data. (default None) absolute_sigma (bool): If False, `sigma` denotes relative weights of the data points. The default True means that the sigma parameter is the reciprocal of the absolute standard deviation. output (str, default "fit"): Specify what to return. Returns: (various): The return value is determined by the *output* parameter. Options are: * "fit" (tuple of popt,pcov) Optimal values of the fitting parameters p, and the variance-co-variance matrix for the fitting parameters. * "row" just a one dimensional numpy array of the fit parameters interleaved with their uncertainties * "full" a tuple of (popt,pcov,dictionary of optional outputs, message, return code, row). * "data" a copy of the :py:class:`Stoner.Core.DataFile` object with fit recorded in the metadata and optionally as a new column. Note: If the columns are not specified (or set to None) then the X and Y data are taken using the :py:attr:`Stoner.Core.DataFile.setas` attribute. The fitting function should have prototype y=f(x,p[0],p[1],p[2]...) The x-column and y-column can be anything that :py:meth:`Stoner.Core.DataFile.find_col` can use as an index but typucally either strings to be matched against column headings or integers. The initial parameter values and weightings default to None which corresponds to all parameters starting at 1 and all points equally weighted. The bounds function has format b(x, y-vec) and rewturns true if the point is to be used in the fit and false if not. The *absolute_sigma* keyword determines whether the returned covariance matrix `pcov` is based on *estimated* errors in the data, and is not affected by the overall magnitude of the values in `sigma`. Only the relative magnitudes of the *sigma* values matter. If True, `sigma` describes one standard deviation errors of the input data points. The estimated covariance in `pcov` is based on these values. The starting vector *p0* can be either a list, tuple or array, or a callable that will produce a list, tuple or array. IF callable, it should take the form: def p0_func(ydata,x=xdata): .... and return a list of parameter values that is in the same order as the model function. If p0 is not given and a :py:class:`lmfit.Model` or :py:class:`scipy.odr.Model` is supplied as the model function, then the model's estimates of the starting values will be used instead. See Also: * :py:meth:`Stoner.Data.lmfit` * :py:meth:`Stoner.Data.odr` * :py:meth:`Stoner.Data.differential_evolution` * User guide section :ref:`curve_fit_guide` """ _ = self._col_args(scalar=False, xcol=xcol, ycol=ycol, yerr=sigma) xcol, ycol, sigma = _.xcol, _.ycol, _.yerr bounds = kargs.pop("bounds", lambda x, y: True) result = kargs.pop("result", None) replace = kargs.pop("replace", False) header = kargs.pop("header", None) residuals = kargs.pop("residuals", False) prefix = kargs.pop("prefix", None) # Support either scale_covar or absolute_sigma, the latter wins if both supplied # If neither are specified, then if sigma is not given, absolute sigma will be False. scale_covar = kargs.pop("scale_covar", sigma is not None) absolute_sigma = kargs.pop("absolute_sigma", not scale_covar) # Support both asrow and output, the latter wins if both supplied asrow = kargs.pop("asrow", False) output = kargs.pop("output", "row" if asrow else "fit") kargs["full_output"] = True if not isinstance(ycol, list): ycol = [ycol] xdat, ydata, sigma = self._get_curve_fit_data(xcol, ycol, bounds, sigma) # Support any of our alternatives for the fitting function if isinstance(func, type) and issubclass(func, (Model, sp.odr.Model)): func = func() if isinstance(func, sp.odr.Model): # scipy othrothogonal model hack def _func(x, *beta): return func.fcn(beta, x) p0 = kargs.pop("p0", func.estimate) elif isinstance(func, Model): _func = func.func try: if "p0" not in kargs: # Avoid expensive guess if we have a p0 pguess = func.guess else: pguess = None except ( ArithmeticError, AttributeError, LookupError, RuntimeError, NameError, OSError, TypeError, ValueError, ): pguess = None p0 = kargs.pop("p0", pguess) elif callable(func): _func = func p0 = kargs.pop("p0", None) else: raise TypeError( "".join( [ "curve_fit parameter 1 must be either a Model class from", f" lmfit or scipy.odr, or a callable, not a {type(func)}", ] ) ) if callable(p0): # Allow the user to supply p0 as a callanble function if ydata.ndim != 1: yy = ydata.ravel() else: yy = ydata try: # Skip the guess if it fails p0 = p0(yy, xdat) except ( ArithmeticError, AttributeError, LookupError, RuntimeError, NameError, OSError, TypeError, ValueError, ): p0 = None p0 = _curve_fit_p0_list(p0, func) retvals = [] i = None for i, ydat in enumerate(ydata): if isinstance(sigma, np.ndarray) and sigma.shape[0] > 1: if sigma.shape[0] == len(ycol): s = sigma[i] elif sigma.ndim == 2 and sigma.shape[1] == len(ycol): s = sigma[:, i] else: s = sigma # probably this will fail! else: s = sigma report = _curve_fit_result( *curve_fit(_func, xdat, ydat, p0=p0, sigma=s, absolute_sigma=absolute_sigma, **kargs) ) report.func = func if p0 is None: report.p0 = np.ones(len(report.popt)) else: report.p0 = p0 report.data = self report.residual_vals = ydata - report.fvec report.chisq = (report.residual_vals**2).sum() report.nfree = len(self) - len(report.popt) report.chisq /= report.nfree if result is not None: self._record_curve_fit_result( func, report, xcol, header, result, replace, residuals=residuals, ycol=ycol, prefix=prefix ) try: retvals.append(getattr(report, output)) except AttributeError as err: raise RuntimeError(f"Specified output: {kargs['output']}, from curve_fit not recognised") from err if i == 0: retvals = retvals[0] return retvals
[docs] def differential_evolution(self, model, xcol=None, ycol=None, p0=None, sigma=None, **kargs): """Fit model to the data using a differential evolution algorithm. Args: model (lmfit.Model): An instance of an lmfit.Model that represents the model to be fitted to the data xcol (index or None): Columns to be used for the x data for the fitting. If not givem defaults to the :py:attr:`Stoner.Core.DataFile.setas` x column ycol (index or None): Columns to be used for the y data for the fitting. If not givem defaults to the :py:attr:`Stoner.Core.DataFile.setas` y column Keyword Arguments: p0 (list, tuple, array or callable): A vector of initial parameter values to try. See the notes in :py:meth:`Stoner.Data.curve_fit` for more details. sigma (index): The index of the column with the y-error bars bounds (callable): A callable object that evaluates true if a row is to be included. Should be of the form f(x,y) result (bool): Determines whether the fitted data should be added into the DataFile object. If result is True then the last column will be used. If result is a string or an integer then it is used as a column index. Default to None for not adding fitted data replace (bool): Inidcatesa whether the fitted data replaces existing data or is inserted as a new column (default False) header (string or None): If this is a string then it is used as the name of the fitted data. (default None) scale_covar (bool) : whether to automatically scale covariance matrix (leastsq only) output (str, default "fit"): Specify what to return. Returns: ( various ) : The return value is determined by the *output* parameter. Options are - "fit" just the :py:class:`lmfit.model.ModelFit` instance that contains all relevant information about the fit. - "row" just a one dimensional numpy array of the fit parameters interleaved with their uncertainties - "full" a tuple of the fit instance and the row. - "data" a copy of the :py:class:`Stoner.Core.DataFile` object with the fit recorded in the emtadata and optionally as a column of data. This function is essentially a wrapper around the :py:func:`scipy.optimize.differential_evolution` function that presents the same interface as the other Stoner package curve fitting functions. The parent function, however, does not provide the variance-covariance matrix to estimate the fitting errors. To work around this, this function does the initial fit with the differential evolution, but then uses that to give a starting vector to a call to :py:func:`scipy.optimize.curve_fit` to calculate the covariance matrix. See Also: - :py:meth:`Stoner.Data.curve_fit` - :py:meth:`Stoner.Data.lmfit` - :py:meth:`Stoner.Data.odr` - User guide section :ref:`curve_fit_guide` Example: .. plot:: samples/differential_evolution_simple.py :include-source: :outname: diffev1 """ bounds = kargs.pop("bounds", lambda x, y: True) result = kargs.pop("result", None) replace = kargs.pop("replace", False) residuals = kargs.pop("residuals", False) header = kargs.pop("header", None) # Support both absolute_sigma and scale_covar, but scale_covar wins here (c.f.curve_fit) absolute_sigma = kargs.pop("absolute_sigma", True) scale_covar = kargs.pop("scale_covar", not absolute_sigma) # Support both asrow and output, the latter wins if both supplied asrow = kargs.pop("asrow", False) output = kargs.pop("output", "row" if asrow else "fit") data, scale_covar, _ = self._assemnle_data_to_fit(xcol, ycol, sigma, bounds, scale_covar) data = data[0:3] model, prefix = _prep_lmfit_model(model, kargs) p0, single_fit = _prep_lmfit_p0(model, data[1], data[0], p0, kargs) for k in model.param_names: kargs.pop(k, None) diff_model = MimizerAdaptor(model, params=p0) kargs["polish"] = kargs.get("polish", True) if not single_fit: raise NotImplementedError("Sorry chi^2 mapping not implemented for differential evolution yet.") fit = differential_evolution(diff_model.minimize_func, diff_model.bounds, data, **kargs) if not fit.success: raise RuntimeError(fit.message) kargs.pop("polish", None) kargs["full_output"] = True polish = _curve_fit_result( *curve_fit(model.func, data[0], data[1], sigma=data[2], p0=fit.x, absolute_sigma=not scale_covar, **kargs) ) polish.func = model.func polish.p0 = p0 polish.data = self polish.residual_vals = data[1] - polish.fvec polish.chisq = (polish.residual_vals**2).sum() polish.nfree = len(self) - len(polish.popt) polish.chisq /= polish.nfree model.popt = polish.popt fit.covar = polish.pcov fit.popt = polish.popt fit.perr = polish.perr fit.fit_report = polish.fit_report row = self._record_curve_fit_result( model, fit, _.xcol, header, result, replace, residuals=residuals, prefix=prefix, ycol=_.ycol ) ret_dict = {} for i, p in enumerate(model.param_names): ret_dict[p], ret_dict[f"d_{p}"] = row[2 * i], row[2 * i + 1] ret_dict["chi-square"] = polish.chisqr ret_dict["red. chi-sqr"] = polish.redchi ret_dict["nfev"] = fit.nfev fit.dict = ret_dict retval = { "fit": (row[::2], fit.covar), "report": fit, "row": row, "full": (fit, row), "data": self, "dict": fit.dict, } if output not in retval: raise RuntimeError(f"Failed to recognise output format:{output}") return retval[output]
[docs] def lmfit(self, model, xcol=None, ycol=None, p0=None, sigma=None, **kargs): r"""Wrap the lmfit module fitting. Args: model (lmfit.Model): An instance of an lmfit.Model that represents the model to be fitted to the data xcol (index or None): Columns to be used for the x data for the fitting. If not givem defaults to the :py:attr:`Stoner.Core.DataFile.setas` x column ycol (index or None): Columns to be used for the y data for the fitting. If not givem defaults to the :py:attr:`Stoner.Core.DataFile.setas` y column Keyword Arguments: p0 (list, tuple, array or callable): A vector of initial parameter values to try. See the notes in :py:meth:`Stoner.Data.curve_fit` for more details. sigma (index): The index of the column with the y-error bars bounds (callable): A callable object that evaluates true if a row is to be included. Should be of the form f(x,y) result (bool): Determines whether the fitted data should be added into the DataFile object. If result is True then the last column will be used. If result is a string or an integer then it is used as a column index. Default to None for not adding fitted data replace (bool): Inidcatesa whether the fitted data replaces existing data or is inserted as a new column (default False) header (string or None): If this is a string then it is used as the name of the fitted data. (default None) scale_covar (bool) : whether to automatically scale covariance matrix (leastsq only) output (str, default "fit"): Specify what to return. Returns: ( various ) : The return value is determined by the *output* parameter. Options are - "fit" just the :py:class:`lmfit.model.ModelFit` instance that contains all relevant information about the fit. - "row" just a one dimensional numpy array of the fit parameters interleaved with their uncertainties - "full" a tuple of the fit instance and the row. - "data" a copy of the :py:class:`Stoner.Core.DataFile` object with the fit recorded in the emtadata and optionally as a column of data. See Also: - :py:meth:`Stoner.Data.curve_fit` - :py:meth:`Stoner.Data.odr` - :py:meth:`Stoner.Data.differential_evolution` - User guide section :ref:`fitting_with_limits` .. note:: If *p0* is fed a 2D array, then it assumed that you want to calculate :math:`\chi^2` for different starting parameters with some variables fixed. In this mode, fitting is carried out repeatedly with each row representing one attempt with different values of the parameters. In this mode the return value is a 2D array whose rows correspond to the inputs to the rows of p0, the columns are the fitted values of the parameters with an additional column for :math:`\chi^2`. Example: .. plot:: samples/lmfit_simple.py :include-source: :outname: lmfit2 """ bounds = kargs.pop("bounds", lambda x, y: True) result = kargs.pop("result", None) replace = kargs.pop("replace", False) residuals = kargs.pop("residuals", False) header = kargs.pop("header", None) # Support both absolute_sigma and scale_covar, but scale_covar wins here (c.f.curve_fit) absolute_sigma = kargs.pop("absolute_sigma", True) scale_covar = kargs.pop("scale_covar", not absolute_sigma) # Support both asrow and output, the latter wins if both supplied asrow = kargs.pop("asrow", False) output = kargs.pop("output", "row" if asrow else "fit") data, scale_covar, _ = self._assemnle_data_to_fit(xcol, ycol, sigma, bounds, scale_covar) model, prefix = _prep_lmfit_model(model, kargs) p0, single_fit = _prep_lmfit_p0(model, data[1], data[0], p0, kargs) nan_policy = kargs.pop("nan_policy", getattr(model, "nan_policy", "omit")) if single_fit: ret_val = self.__lmfit_one( model, data, p0, prefix, _, scale_covar, result=result, header=header, replace=replace, output=output, residuals=residuals, nan_policy=nan_policy, ) else: # chi^2 mode pn = p0 ret_val = np.zeros((pn.shape[0], pn.shape[1] * 2 + 1)) for i, pn_i in enumerate(pn): # iterate over every row in the supplied p0 values p0, single_fit = _prep_lmfit_p0( model, data[1], data[0], pn_i, kargs ) # model, data, params, prefix, columns, scale_covar,**kargs) ret_val[i, :] = self.__lmfit_one( model, data, p0, prefix, _, scale_covar, nan_policy=nan_policy, output="row" ) if output == "data": # Create a data object and seet column headers etc correctly ret = self.clone ret.data = ret_val ret.column_headers = [] ret.setas = "" prefix = ret["lmfit.prefix"][-1] ix = 0 for ix, p in enumerate(model.param_names): label = self.metadata.get(f"{prefix}{p} label", p) units = self.metadata.get(f"{prefix}{p} units", "") # Set columns ret.column_headers[2 * ix] = rf"${label} {units}$" ret.column_headers[2 * ix + 1] = rf"$\delta{label} {units}$" if not ret[f"{prefix}{p} vary"]: fixed = 2 * ix ret.column_headers[-1] = "$\\chi^2$" ret.labels = ret.column_headers # Workout which columns are y,e and x plots = list(range(0, ix * 2 + 1, 2)) errors = list(range(1, ix * 2 + 2, 2)) plots.append(ix * 2 + 2) plots.remove(fixed) errors.remove(fixed + 1) ret.setas[plots] = "y" ret.setas[errors] = "e" ret.setas[fixed] = "x" ret_val = ret return ret_val
[docs] def polyfit( self, xcol=None, ycol=None, polynomial_order=2, bounds=lambda x, y: True, result=None, replace=False, header=None, ): """Pass through to numpy.polyfit. Args: xcol (index): Index to the column in the data with the X data in it ycol (index): Index to the column int he data with the Y data in it polynomial_order (int): Order of polynomial to fit (default 2) bounds (callable): A function that evaluates True if the current row should be included in the fit result (index or None): Add the fitted data to the current data object in a new column (default don't add) replace (bool): Overwrite or insert new data if result is not None (default False) header (string or None): Name of column_header of replacement data. Default is construct a string from the y column headser and polynomial order. Returns: (numpy.poly): The best fit polynomial as a numpy.poly object. Note: If the x or y columns are not specified (or are None) the the setas attribute is used instead. This method is deprecated and may be removed in a future version in favour of the more general curve_fit """ _ = self._col_args(xcol=xcol, ycol=ycol, scalar=False) working = self.search(_.xcol, bounds) if not isiterable(_.ycol): _.ycol = [_.ycol] p = np.zeros((len(_.ycol), polynomial_order + 1)) if isinstance(result, bool) and result: result = self.shape[1] for i, ycolumn in enumerate(_.ycol): p[i, :] = np.polyfit( working[:, self.find_col(_.xcol)], working[:, self.find_col(ycolumn)], polynomial_order ) if result: if header is None: header = ( f"Fitted {self.column_headers[self.find_col(ycolumn)]} with " + f"{ordinal(polynomial_order)} order polynomial" ) self.add_column( np.polyval(p[i, :], x=self.column(_.xcol)), index=result, replace=replace, header=header ) if len(_.ycol) == 1: p = p[0, :] self[f"{ordinal(polynomial_order)}-order polyfit coefficients"] = list(p) return p
[docs] def odr(self, model, xcol=None, ycol=None, **kargs): """Wrap the scipy.odr orthogonal distance regression fitting. Args: model (scipy.odr.Model, lmfit.models.Model or callable): The model that describes the data. See below for more details. xcol (index or None): Columns to be used for the x data for the fitting. If not givem defaults to the :py:attr:`Stoner.Core.DataFile.setas` x column ycol (index or None): Columns to be used for the y data for the fitting. If not givem defaults to the :py:attr:`Stoner.Core.DataFile.setas` y column Keyword Arguments: p0 (list, tuple, array or callable): A vector of initial parameter values to try. See the notes to :py:meth:`Stoner.Data.curve_fit` for more details. sigma_x (index): The index of the column with the x-error bars sigma_y (index): The index of the column with the x-error bars bounds (callable): A callable object that evaluates true if a row is to be included. Should be of the form f(x,y) result (bool): Determines whether the fitted data should be added into the DataFile object. If result is True then the last column will be used. If result is a string or an integer then it is used as a column index. Default to None for not adding fitted data replace (bool): Inidcatesa whether the fitted data replaces existing data or is inserted as a new column (default False) header (string or None): If this is a string then it is used as the name of the fitted data. (default None) output (str, default "fit"): Specify what to return. Returns: ( various ) : The return value is determined by the *output* parameter. Options are - "fit" just the :py:class:`scipy.odr.Output` instance (default) - "row" just a one dimensional numpy array of the fit parameters interleaved with their uncertainties - "full" a tuple of the fit instance and the row. - "data" a copy of the :py:class:`Stoner.Core.DataFile` object with the fit recorded in the emtadata and optionally as a column of data. Notes: The function tries to make use of whatever model you give it. Specifically, it accepts: - A subclass or an instance of :py:class:`scipy.odr.Model` : this is the native model type for the underlying scipy odr package. - A subclass or instance of an lmfit.models.Model: the :py:mod:`Stoner.analysis.fitting.models` package has a number of useful prebuilt lmfit models that can be used directly by this function. - A callable function which should have a signature f(x,parameter1,parameter2...) and *not* the scip.odr standard f(beta,x) This function is designed to be as compatible as possible with :py:meth:`AnalysisMixin.curve_fit` and :py:meth:`AnalysisMixin.lmfit` to facilitate easy of switching between them. See Also: - :py:meth:`AnalysisMixin.curve_fit` - :py:meth:`AnalysisMixin.lmfit` - :py:meth:`Stoner.Data.differential_evolution` - User guide section :ref:`fitting_with_limits` Example: .. plot:: samples/odr_simple.py :include-source: :outname: odrfit1 """ # Support both absolute_sigma and scale_covar, but scale_covar wins here (c.f.curve_fit) absolute_sigma = kargs.pop("absolute_sigma", True) scale_covar = kargs.pop("scale_covar", not absolute_sigma) # Support both asrow and output, the latter wins if both supplied sigma = kargs.pop("sigma", None) sigma_x = kargs.pop("sigma_x", None) bounds = kargs.pop("boinds", lambda x, r: True) p0 = kargs.pop("p0", None) data, scale_covar, _ = self._assemnle_data_to_fit(xcol, ycol, sigma, bounds, scale_covar, sigma_x=sigma_x) if not isinstance(model, odrModel): model, prefix = _prep_lmfit_model(model, kargs) else: prefix = kargs.pop("prefix", getattr(model, "name", model.fcn.__name__)) p0, single_fit = _prep_lmfit_p0(model, data[1], data[0], p0, kargs) kargs["p0"] = p0 model = odr_Model(model, p0=p0) if not absolute_sigma: data = sp.odr.Data(data[0], data[1], wd=1 / data[3] ** 2, we=1 / data[2] ** 2) else: data = sp.odr.RealData(data[0], data[1], sx=data[3], sy=data[2]) if single_fit: ret_val = self._odr_one(data, model, prefix, _, **kargs) else: # chi^2 mode raise NotImplementedError("Sorry cannot do chi^2 mode for orthogonal distance regression yet!") return ret_val