# pyerrors.fits

View Source

      1import gc
      2from collections.abc import Sequence
      3import warnings
      4import numpy as np
      5import autograd.numpy as anp
      6import scipy.optimize
      7import scipy.stats
      8import matplotlib.pyplot as plt
      9from matplotlib import gridspec
     10from scipy.odr import ODR, Model, RealData
     11import iminuit
     12from autograd import jacobian as auto_jacobian
     13from autograd import hessian as auto_hessian
     14from autograd import elementwise_grad as egrad
     15from numdifftools import Jacobian as num_jacobian
     16from numdifftools import Hessian as num_hessian
     17from .obs import Obs, derived_observable, covariance, cov_Obs, invert_corr_cov_cholesky
     18
     19
     20class Fit_result(Sequence):
     21    """Represents fit results.
     22
     23    Attributes
     24    ----------
     25    fit_parameters : list
     26        results for the individual fit parameters,
     27        also accessible via indices.
     28    chisquare_by_dof : float
     29        reduced chisquare.
     30    p_value : float
     31        p-value of the fit
     32    t2_p_value : float
     33        Hotelling t-squared p-value for correlated fits.
     34    """
     35
     36    def __init__(self):
     37        self.fit_parameters = None
     38
     39    def __getitem__(self, idx):
     40        return self.fit_parameters[idx]
     41
     42    def __len__(self):
     43        return len(self.fit_parameters)
     44
     45    def gamma_method(self, **kwargs):
     46        """Apply the gamma method to all fit parameters"""
     47        [o.gamma_method(**kwargs) for o in self.fit_parameters]
     48
     49    gm = gamma_method
     50
     51    def __str__(self):
     52        my_str = 'Goodness of fit:\n'
     53        if hasattr(self, 'chisquare_by_dof'):
     54            my_str += '\u03C7\u00b2/d.o.f. = ' + f'{self.chisquare_by_dof:2.6f}' + '\n'
     55        elif hasattr(self, 'residual_variance'):
     56            my_str += 'residual variance = ' + f'{self.residual_variance:2.6f}' + '\n'
     57        if hasattr(self, 'chisquare_by_expected_chisquare'):
     58            my_str += '\u03C7\u00b2/\u03C7\u00b2exp  = ' + f'{self.chisquare_by_expected_chisquare:2.6f}' + '\n'
     59        if hasattr(self, 'p_value'):
     60            my_str += 'p-value   = ' + f'{self.p_value:2.4f}' + '\n'
     61        if hasattr(self, 't2_p_value'):
     62            my_str += 't\u00B2p-value = ' + f'{self.t2_p_value:2.4f}' + '\n'
     63        my_str += 'Fit parameters:\n'
     64        for i_par, par in enumerate(self.fit_parameters):
     65            my_str += str(i_par) + '\t' + ' ' * int(par >= 0) + str(par).rjust(int(par < 0.0)) + '\n'
     66        return my_str
     67
     68    def __repr__(self):
     69        m = max(map(len, list(self.__dict__.keys()))) + 1
     70        return '\n'.join([key.rjust(m) + ': ' + repr(value) for key, value in sorted(self.__dict__.items())])
     71
     72
     73def least_squares(x, y, func, priors=None, silent=False, **kwargs):
     74    r'''Performs a non-linear fit to y = func(x).
     75        ```
     76
     77    Parameters
     78    ----------
     79    For an uncombined fit:
     80
     81    x : list
     82        list of floats.
     83    y : list
     84        list of Obs.
     85    func : object
     86        fit function, has to be of the form
     87
     88        ```python
     89        import autograd.numpy as anp
     90
     91        def func(a, x):
     92            return a[0] + a[1] * x + a[2] * anp.sinh(x)
     93        ```
     94
     95        For multiple x values func can be of the form
     96
     97        ```python
     98        def func(a, x):
     99            (x1, x2) = x
    100            return a[0] * x1 ** 2 + a[1] * x2
    101        ```
    102        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
    103        will not work.
    104
    105    OR For a combined fit:
    106
    107    x : dict
    108        dict of lists.
    109    y : dict
    110        dict of lists of Obs.
    111    funcs : dict
    112        dict of objects
    113        fit functions have to be of the form (here a[0] is the common fit parameter)
    114        ```python
    115        import autograd.numpy as anp
    116        funcs = {"a": func_a,
    117                "b": func_b}
    118
    119        def func_a(a, x):
    120            return a[1] * anp.exp(-a[0] * x)
    121
    122        def func_b(a, x):
    123            return a[2] * anp.exp(-a[0] * x)
    124
    125        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
    126        will not work.
    127
    128    priors : dict or list, optional
    129        priors can either be a dictionary with integer keys and the corresponding priors as values or
    130        a list with an entry for every parameter in the fit. The entries can either be
    131        Obs (e.g. results from a previous fit) or strings containing a value and an error formatted like
    132        0.548(23), 500(40) or 0.5(0.4)
    133    silent : bool, optional
    134        If true all output to the console is omitted (default False).
    135    initial_guess : list
    136        can provide an initial guess for the input parameters. Relevant for
    137        non-linear fits with many parameters. In case of correlated fits the guess is used to perform
    138        an uncorrelated fit which then serves as guess for the correlated fit.
    139    method : str, optional
    140        can be used to choose an alternative method for the minimization of chisquare.
    141        The possible methods are the ones which can be used for scipy.optimize.minimize and
    142        migrad of iminuit. If no method is specified, Levenberg-Marquard is used.
    143        Reliable alternatives are migrad, Powell and Nelder-Mead.
    144    tol: float, optional
    145        can be used (only for combined fits and methods other than Levenberg-Marquard) to set the tolerance for convergence
    146        to a different value to either speed up convergence at the cost of a larger error on the fitted parameters (and possibly
    147        invalid estimates for parameter uncertainties) or smaller values to get more accurate parameter values
    148        The stopping criterion depends on the method, e.g. migrad: edm_max = 0.002 * tol * errordef (EDM criterion: edm < edm_max)
    149    correlated_fit : bool
    150        If True, use the full inverse covariance matrix in the definition of the chisquare cost function.
    151        For details about how the covariance matrix is estimated see `pyerrors.obs.covariance`.
    152        In practice the correlation matrix is Cholesky decomposed and inverted (instead of the covariance matrix).
    153        This procedure should be numerically more stable as the correlation matrix is typically better conditioned (Jacobi preconditioning).
    154    inv_chol_cov_matrix [array,list], optional
    155        array: shape = (no of y values) X (no of y values)
    156        list:   for an uncombined fit: [""]
    157                for a combined fit: list of keys belonging to the corr_matrix saved in the array, must be the same as the keys of the y dict in alphabetical order
    158        If correlated_fit=True is set as well, can provide an inverse covariance matrix (y errors, dy_f included!) of your own choosing for a correlated fit.
    159        The matrix must be a lower triangular matrix constructed from a Cholesky decomposition: The function invert_corr_cov_cholesky(corr, inverrdiag) can be
    160        used to construct it from a correlation matrix (corr) and the errors dy_f of the data points (inverrdiag = np.diag(1 / np.asarray(dy_f))). For the correct
    161        ordering the correlation matrix (corr) can be sorted via the function sort_corr(corr, kl, yd) where kl is the list of keys and yd the y dict.
    162    expected_chisquare : bool
    163        If True estimates the expected chisquare which is
    164        corrected by effects caused by correlated input data (default False).
    165    resplot : bool
    166        If True, a plot which displays fit, data and residuals is generated (default False).
    167    qqplot : bool
    168        If True, a quantile-quantile plot of the fit result is generated (default False).
    169    num_grad : bool
    170        Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
    171
    172    Returns
    173    -------
    174    output : Fit_result
    175        Parameters and information on the fitted result.
    176    Examples
    177    ------
    178    >>> # Example of a correlated (correlated_fit = True, inv_chol_cov_matrix handed over) combined fit, based on a randomly generated data set
    179    >>> import numpy as np
    180    >>> from scipy.stats import norm
    181    >>> from scipy.linalg import cholesky
    182    >>> import pyerrors as pe
    183    >>> # generating the random data set
    184    >>> num_samples = 400
    185    >>> N = 3
    186    >>> x = np.arange(N)
    187    >>> x1 = norm.rvs(size=(N, num_samples)) # generate random numbers
    188    >>> x2 = norm.rvs(size=(N, num_samples)) # generate random numbers
    189    >>> r = r1 = r2 = np.zeros((N, N))
    190    >>> y = {}
    191    >>> for i in range(N):
    192    >>>    for j in range(N):
    193    >>>        r[i, j] = np.exp(-0.8 * np.fabs(i - j)) # element in correlation matrix
    194    >>> errl = np.sqrt([3.4, 2.5, 3.6]) # set y errors
    195    >>> for i in range(N):
    196    >>>    for j in range(N):
    197    >>>        r[i, j] *= errl[i] * errl[j] # element in covariance matrix
    198    >>> c = cholesky(r, lower=True)
    199    >>> y = {'a': np.dot(c, x1), 'b': np.dot(c, x2)} # generate y data with the covariance matrix defined
    200    >>> # random data set has been generated, now the dictionaries and the inverse covariance matrix to be handed over are built
    201    >>> x_dict = {}
    202    >>> y_dict = {}
    203    >>> chol_inv_dict = {}
    204    >>> data = []
    205    >>> for key in y.keys():
    206    >>>    x_dict[key] = x
    207    >>>    for i in range(N):
    208    >>>        data.append(pe.Obs([[i + 1 + o for o in y[key][i]]], ['ens'])) # generate y Obs from the y data
    209    >>>    [o.gamma_method() for o in data]
    210    >>>    corr = pe.covariance(data, correlation=True)
    211    >>>    inverrdiag = np.diag(1 / np.asarray([o.dvalue for o in data]))
    212    >>>    chol_inv = pe.obs.invert_corr_cov_cholesky(corr, inverrdiag) # gives form of the inverse covariance matrix needed for the combined correlated fit below
    213    >>> y_dict = {'a': data[:3], 'b': data[3:]}
    214    >>> # common fit parameter p[0] in combined fit
    215    >>> def fit1(p, x):
    216    >>>    return p[0] + p[1] * x
    217    >>> def fit2(p, x):
    218    >>>    return p[0] + p[2] * x
    219    >>> fitf_dict = {'a': fit1, 'b':fit2}
    220    >>> fitp_inv_cov_combined_fit = pe.least_squares(x_dict,y_dict, fitf_dict, correlated_fit = True, inv_chol_cov_matrix = [chol_inv,['a','b']])
    221    Fit with 3 parameters
    222    Method: Levenberg-Marquardt
    223    `ftol` termination condition is satisfied.
    224    chisquare/d.o.f.: 0.5388013574561786 # random
    225    fit parameters [1.11897846 0.96361162 0.92325319] # random
    226
    227    '''
    228    output = Fit_result()
    229
    230    if (isinstance(x, dict) and isinstance(y, dict) and isinstance(func, dict)):
    231        xd = {key: anp.asarray(x[key]) for key in x}
    232        yd = y
    233        funcd = func
    234        output.fit_function = func
    235    elif (isinstance(x, dict) or isinstance(y, dict) or isinstance(func, dict)):
    236        raise TypeError("All arguments have to be dictionaries in order to perform a combined fit.")
    237    else:
    238        x = np.asarray(x)
    239        xd = {"": x}
    240        yd = {"": y}
    241        funcd = {"": func}
    242        output.fit_function = func
    243
    244    if kwargs.get('num_grad') is True:
    245        jacobian = num_jacobian
    246        hessian = num_hessian
    247    else:
    248        jacobian = auto_jacobian
    249        hessian = auto_hessian
    250
    251    key_ls = sorted(list(xd.keys()))
    252
    253    if sorted(list(yd.keys())) != key_ls:
    254        raise ValueError('x and y dictionaries do not contain the same keys.')
    255
    256    if sorted(list(funcd.keys())) != key_ls:
    257        raise ValueError('x and func dictionaries do not contain the same keys.')
    258
    259    x_all = np.concatenate([np.array(xd[key]).transpose() for key in key_ls]).transpose()
    260    y_all = np.concatenate([np.array(yd[key]) for key in key_ls])
    261
    262    y_f = [o.value for o in y_all]
    263    dy_f = [o.dvalue for o in y_all]
    264
    265    if len(x_all.shape) > 2:
    266        raise ValueError("Unknown format for x values")
    267
    268    if np.any(np.asarray(dy_f) <= 0.0):
    269        raise Exception("No y errors available, run the gamma method first.")
    270
    271    # number of fit parameters
    272    n_parms_ls = []
    273    for key in key_ls:
    274        if not callable(funcd[key]):
    275            raise TypeError('func (key=' + key + ') is not a function.')
    276        if np.asarray(xd[key]).shape[-1] != len(yd[key]):
    277            raise ValueError('x and y input (key=' + key + ') do not have the same length')
    278        for n_loc in range(100):
    279            try:
    280                funcd[key](np.arange(n_loc), x_all.T[0])
    281            except TypeError:
    282                continue
    283            except IndexError:
    284                continue
    285            else:
    286                break
    287        else:
    288            raise RuntimeError("Fit function (key=" + key + ") is not valid.")
    289        n_parms_ls.append(n_loc)
    290
    291    n_parms = max(n_parms_ls)
    292
    293    if len(key_ls) > 1:
    294        for key in key_ls:
    295            if np.asarray(yd[key]).shape != funcd[key](np.arange(n_parms), xd[key]).shape:
    296                raise ValueError(f"Fit function {key} returns the wrong shape ({funcd[key](np.arange(n_parms), xd[key]).shape} instead of {np.asarray(yd[key]).shape})\nIf the fit function is just a constant you could try adding x*0 to get the correct shape.")
    297
    298    if not silent:
    299        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
    300
    301    if priors is not None:
    302        if isinstance(priors, (list, np.ndarray)):
    303            if n_parms != len(priors):
    304                raise ValueError("'priors' does not have the correct length.")
    305
    306            loc_priors = []
    307            for i_n, i_prior in enumerate(priors):
    308                loc_priors.append(_construct_prior_obs(i_prior, i_n))
    309
    310            prior_mask = np.arange(len(priors))
    311            output.priors = loc_priors
    312
    313        elif isinstance(priors, dict):
    314            loc_priors = []
    315            prior_mask = []
    316            output.priors = {}
    317            for pos, prior in priors.items():
    318                if isinstance(pos, int):
    319                    prior_mask.append(pos)
    320                else:
    321                    raise TypeError("Prior position needs to be an integer.")
    322                loc_priors.append(_construct_prior_obs(prior, pos))
    323
    324                output.priors[pos] = loc_priors[-1]
    325            if max(prior_mask) >= n_parms:
    326                raise ValueError("Prior position out of range.")
    327        else:
    328            raise TypeError("Unkown type for `priors`.")
    329
    330        p_f = [o.value for o in loc_priors]
    331        dp_f = [o.dvalue for o in loc_priors]
    332        if np.any(np.asarray(dp_f) <= 0.0):
    333            raise Exception("No prior errors available, run the gamma method first.")
    334    else:
    335        p_f = dp_f = np.array([])
    336        prior_mask = []
    337        loc_priors = []
    338
    339    if 'initial_guess' in kwargs:
    340        x0 = kwargs.get('initial_guess')
    341        if len(x0) != n_parms:
    342            raise ValueError('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
    343    else:
    344        x0 = [0.1] * n_parms
    345
    346    if priors is None:
    347        def general_chisqfunc_uncorr(p, ivars, pr):
    348            model = anp.concatenate([anp.array(funcd[key](p, xd[key])).reshape(-1) for key in key_ls])
    349            return (ivars - model) / dy_f
    350    else:
    351        def general_chisqfunc_uncorr(p, ivars, pr):
    352            model = anp.concatenate([anp.array(funcd[key](p, xd[key])).reshape(-1) for key in key_ls])
    353            return anp.concatenate(((ivars - model) / dy_f, (p[prior_mask] - pr) / dp_f))
    354
    355    def chisqfunc_uncorr(p):
    356        return anp.sum(general_chisqfunc_uncorr(p, y_f, p_f) ** 2)
    357
    358    if kwargs.get('correlated_fit') is True:
    359        if 'inv_chol_cov_matrix' in kwargs:
    360            chol_inv = kwargs.get('inv_chol_cov_matrix')
    361            if (chol_inv[0].shape[0] != len(dy_f)):
    362                raise TypeError('The number of columns of the inverse covariance matrix handed over needs to be equal to the number of y errors.')
    363            if (chol_inv[0].shape[0] != chol_inv[0].shape[1]):
    364                raise TypeError('The inverse covariance matrix handed over needs to have the same number of rows as columns.')
    365            if (chol_inv[1] != key_ls):
    366                raise ValueError('The keys of inverse covariance matrix are not the same or do not appear in the same order as the x and y values.')
    367            chol_inv = chol_inv[0]
    368            if np.any(np.diag(chol_inv) <= 0) or (not np.all(chol_inv == np.tril(chol_inv))):
    369                raise ValueError('The inverse covariance matrix inv_chol_cov_matrix[0] has to be a lower triangular matrix constructed from a Cholesky decomposition.')
    370        else:
    371            corr = covariance(y_all, correlation=True, **kwargs)
    372            inverrdiag = np.diag(1 / np.asarray(dy_f))
    373            chol_inv = invert_corr_cov_cholesky(corr, inverrdiag)
    374
    375        def general_chisqfunc(p, ivars, pr):
    376            model = anp.concatenate([anp.array(funcd[key](p, xd[key])).reshape(-1) for key in key_ls])
    377            return anp.concatenate((anp.dot(chol_inv, (ivars - model)), (p[prior_mask] - pr) / dp_f))
    378
    379        def chisqfunc(p):
    380            return anp.sum(general_chisqfunc(p, y_f, p_f) ** 2)
    381    else:
    382        general_chisqfunc = general_chisqfunc_uncorr
    383        chisqfunc = chisqfunc_uncorr
    384
    385    output.method = kwargs.get('method', 'Levenberg-Marquardt')
    386    if not silent:
    387        print('Method:', output.method)
    388
    389    if output.method != 'Levenberg-Marquardt':
    390        if output.method == 'migrad':
    391            tolerance = 1e-4  # default value of 1e-1 set by iminuit can be problematic
    392            if 'tol' in kwargs:
    393                tolerance = kwargs.get('tol')
    394            fit_result = iminuit.minimize(chisqfunc_uncorr, x0, tol=tolerance)  # Stopping criterion 0.002 * tol * errordef
    395            if kwargs.get('correlated_fit') is True:
    396                fit_result = iminuit.minimize(chisqfunc, fit_result.x, tol=tolerance)
    397            output.iterations = fit_result.nfev
    398        else:
    399            tolerance = 1e-12
    400            if 'tol' in kwargs:
    401                tolerance = kwargs.get('tol')
    402            fit_result = scipy.optimize.minimize(chisqfunc_uncorr, x0, method=kwargs.get('method'), tol=tolerance)
    403            if kwargs.get('correlated_fit') is True:
    404                fit_result = scipy.optimize.minimize(chisqfunc, fit_result.x, method=kwargs.get('method'), tol=tolerance)
    405            output.iterations = fit_result.nit
    406
    407        chisquare = fit_result.fun
    408
    409    else:
    410        if 'tol' in kwargs:
    411            print('tol cannot be set for Levenberg-Marquardt')
    412
    413        def chisqfunc_residuals_uncorr(p):
    414            return general_chisqfunc_uncorr(p, y_f, p_f)
    415
    416        fit_result = scipy.optimize.least_squares(chisqfunc_residuals_uncorr, x0, method='lm', ftol=1e-15, gtol=1e-15, xtol=1e-15)
    417        if kwargs.get('correlated_fit') is True:
    418            def chisqfunc_residuals(p):
    419                return general_chisqfunc(p, y_f, p_f)
    420
    421            fit_result = scipy.optimize.least_squares(chisqfunc_residuals, fit_result.x, method='lm', ftol=1e-15, gtol=1e-15, xtol=1e-15)
    422
    423        chisquare = np.sum(fit_result.fun ** 2)
    424        assert np.isclose(chisquare, chisqfunc(fit_result.x), atol=1e-14)
    425
    426        output.iterations = fit_result.nfev
    427
    428    if not fit_result.success:
    429        raise Exception('The minimization procedure did not converge.')
    430
    431    output.chisquare = chisquare
    432    output.dof = y_all.shape[-1] - n_parms + len(loc_priors)
    433    output.p_value = 1 - scipy.stats.chi2.cdf(output.chisquare, output.dof)
    434    if output.dof > 0:
    435        output.chisquare_by_dof = output.chisquare / output.dof
    436    else:
    437        output.chisquare_by_dof = float('nan')
    438
    439    output.message = fit_result.message
    440    if not silent:
    441        print(fit_result.message)
    442        print('chisquare/d.o.f.:', output.chisquare_by_dof)
    443        print('fit parameters', fit_result.x)
    444
    445    def prepare_hat_matrix():
    446        hat_vector = []
    447        for key in key_ls:
    448            if (len(xd[key]) != 0):
    449                hat_vector.append(jacobian(funcd[key])(fit_result.x, xd[key]))
    450        hat_vector = [item for sublist in hat_vector for item in sublist]
    451        return hat_vector
    452
    453    if kwargs.get('expected_chisquare') is True:
    454        if kwargs.get('correlated_fit') is not True:
    455            W = np.diag(1 / np.asarray(dy_f))
    456            cov = covariance(y_all)
    457            hat_vector = prepare_hat_matrix()
    458            A = W @ hat_vector
    459            P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
    460            expected_chisquare = np.trace((np.identity(y_all.shape[-1]) - P_phi) @ W @ cov @ W)
    461            output.chisquare_by_expected_chisquare = output.chisquare / expected_chisquare
    462            if not silent:
    463                print('chisquare/expected_chisquare:', output.chisquare_by_expected_chisquare)
    464
    465    fitp = fit_result.x
    466
    467    try:
    468        hess = hessian(chisqfunc)(fitp)
    469    except TypeError:
    470        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
    471
    472    len_y = len(y_f)
    473
    474    def chisqfunc_compact(d):
    475        return anp.sum(general_chisqfunc(d[:n_parms], d[n_parms: n_parms + len_y], d[n_parms + len_y:]) ** 2)
    476
    477    jac_jac_y = hessian(chisqfunc_compact)(np.concatenate((fitp, y_f, p_f)))
    478
    479    # Compute hess^{-1} @ jac_jac_y[:n_parms + m, n_parms + m:] using LAPACK dgesv
    480    try:
    481        deriv_y = -scipy.linalg.solve(hess, jac_jac_y[:n_parms, n_parms:])
    482    except np.linalg.LinAlgError:
    483        raise Exception("Cannot invert hessian matrix.")
    484
    485    result = []
    486    for i in range(n_parms):
    487        result.append(derived_observable(lambda x_all, **kwargs: (x_all[0] + np.finfo(np.float64).eps) / (y_all[0].value + np.finfo(np.float64).eps) * fitp[i], list(y_all) + loc_priors, man_grad=list(deriv_y[i])))
    488
    489    output.fit_parameters = result
    490
    491    # Hotelling t-squared p-value for correlated fits.
    492    if kwargs.get('correlated_fit') is True:
    493        n_cov = np.min(np.vectorize(lambda x_all: x_all.N)(y_all))
    494        output.t2_p_value = 1 - scipy.stats.f.cdf((n_cov - output.dof) / (output.dof * (n_cov - 1)) * output.chisquare,
    495                                                  output.dof, n_cov - output.dof)
    496
    497    if kwargs.get('resplot') is True:
    498        for key in key_ls:
    499            residual_plot(xd[key], yd[key], funcd[key], result, title=key)
    500
    501    if kwargs.get('qqplot') is True:
    502        for key in key_ls:
    503            qqplot(xd[key], yd[key], funcd[key], result, title=key)
    504
    505    return output
    506
    507
    508def total_least_squares(x, y, func, silent=False, **kwargs):
    509    r'''Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.
    510
    511    Parameters
    512    ----------
    513    x : list
    514        list of Obs, or a tuple of lists of Obs
    515    y : list
    516        list of Obs. The dvalues of the Obs are used as x- and yerror for the fit.
    517    func : object
    518        func has to be of the form
    519
    520        ```python
    521        import autograd.numpy as anp
    522
    523        def func(a, x):
    524            return a[0] + a[1] * x + a[2] * anp.sinh(x)
    525        ```
    526
    527        For multiple x values func can be of the form
    528
    529        ```python
    530        def func(a, x):
    531            (x1, x2) = x
    532            return a[0] * x1 ** 2 + a[1] * x2
    533        ```
    534
    535        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
    536        will not work.
    537    silent : bool, optional
    538        If true all output to the console is omitted (default False).
    539    initial_guess : list
    540        can provide an initial guess for the input parameters. Relevant for non-linear
    541        fits with many parameters.
    542    expected_chisquare : bool
    543        If true prints the expected chisquare which is
    544        corrected by effects caused by correlated input data.
    545        This can take a while as the full correlation matrix
    546        has to be calculated (default False).
    547    num_grad : bool
    548        Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
    549
    550    Notes
    551    -----
    552    Based on the orthogonal distance regression module of scipy.
    553
    554    Returns
    555    -------
    556    output : Fit_result
    557        Parameters and information on the fitted result.
    558    '''
    559
    560    output = Fit_result()
    561
    562    output.fit_function = func
    563
    564    x = np.array(x)
    565
    566    x_shape = x.shape
    567
    568    if kwargs.get('num_grad') is True:
    569        jacobian = num_jacobian
    570        hessian = num_hessian
    571    else:
    572        jacobian = auto_jacobian
    573        hessian = auto_hessian
    574
    575    if not callable(func):
    576        raise TypeError('func has to be a function.')
    577
    578    for i in range(42):
    579        try:
    580            func(np.arange(i), x.T[0])
    581        except TypeError:
    582            continue
    583        except IndexError:
    584            continue
    585        else:
    586            break
    587    else:
    588        raise RuntimeError("Fit function is not valid.")
    589
    590    n_parms = i
    591    if not silent:
    592        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
    593
    594    x_f = np.vectorize(lambda o: o.value)(x)
    595    dx_f = np.vectorize(lambda o: o.dvalue)(x)
    596    y_f = np.array([o.value for o in y])
    597    dy_f = np.array([o.dvalue for o in y])
    598
    599    if np.any(np.asarray(dx_f) <= 0.0):
    600        raise Exception('No x errors available, run the gamma method first.')
    601
    602    if np.any(np.asarray(dy_f) <= 0.0):
    603        raise Exception('No y errors available, run the gamma method first.')
    604
    605    if 'initial_guess' in kwargs:
    606        x0 = kwargs.get('initial_guess')
    607        if len(x0) != n_parms:
    608            raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
    609    else:
    610        x0 = [1] * n_parms
    611
    612    data = RealData(x_f, y_f, sx=dx_f, sy=dy_f)
    613    model = Model(func)
    614    odr = ODR(data, model, x0, partol=np.finfo(np.float64).eps)
    615    odr.set_job(fit_type=0, deriv=1)
    616    out = odr.run()
    617
    618    output.residual_variance = out.res_var
    619
    620    output.method = 'ODR'
    621
    622    output.message = out.stopreason
    623
    624    output.xplus = out.xplus
    625
    626    if not silent:
    627        print('Method: ODR')
    628        print(*out.stopreason)
    629        print('Residual variance:', output.residual_variance)
    630
    631    if out.info > 3:
    632        raise Exception('The minimization procedure did not converge.')
    633
    634    m = x_f.size
    635
    636    def odr_chisquare(p):
    637        model = func(p[:n_parms], p[n_parms:].reshape(x_shape))
    638        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((x_f - p[n_parms:].reshape(x_shape)) / dx_f) ** 2)
    639        return chisq
    640
    641    if kwargs.get('expected_chisquare') is True:
    642        W = np.diag(1 / np.asarray(np.concatenate((dy_f.ravel(), dx_f.ravel()))))
    643
    644        if kwargs.get('covariance') is not None:
    645            cov = kwargs.get('covariance')
    646        else:
    647            cov = covariance(np.concatenate((y, x.ravel())))
    648
    649        number_of_x_parameters = int(m / x_f.shape[-1])
    650
    651        old_jac = jacobian(func)(out.beta, out.xplus)
    652        fused_row1 = np.concatenate((old_jac, np.concatenate((number_of_x_parameters * [np.zeros(old_jac.shape)]), axis=0)))
    653        fused_row2 = np.concatenate((jacobian(lambda x, y: func(y, x))(out.xplus, out.beta).reshape(x_f.shape[-1], x_f.shape[-1] * number_of_x_parameters), np.identity(number_of_x_parameters * old_jac.shape[0])))
    654        new_jac = np.concatenate((fused_row1, fused_row2), axis=1)
    655
    656        A = W @ new_jac
    657        P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
    658        expected_chisquare = np.trace((np.identity(P_phi.shape[0]) - P_phi) @ W @ cov @ W)
    659        if expected_chisquare <= 0.0:
    660            warnings.warn("Negative expected_chisquare.", RuntimeWarning)
    661            expected_chisquare = np.abs(expected_chisquare)
    662        output.chisquare_by_expected_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel()))) / expected_chisquare
    663        if not silent:
    664            print('chisquare/expected_chisquare:',
    665                  output.chisquare_by_expected_chisquare)
    666
    667    fitp = out.beta
    668    try:
    669        hess = hessian(odr_chisquare)(np.concatenate((fitp, out.xplus.ravel())))
    670    except TypeError:
    671        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
    672
    673    def odr_chisquare_compact_x(d):
    674        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
    675        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((d[n_parms + m:].reshape(x_shape) - d[n_parms:n_parms + m].reshape(x_shape)) / dx_f) ** 2)
    676        return chisq
    677
    678    jac_jac_x = hessian(odr_chisquare_compact_x)(np.concatenate((fitp, out.xplus.ravel(), x_f.ravel())))
    679
    680    # Compute hess^{-1} @ jac_jac_x[:n_parms + m, n_parms + m:] using LAPACK dgesv
    681    try:
    682        deriv_x = -scipy.linalg.solve(hess, jac_jac_x[:n_parms + m, n_parms + m:])
    683    except np.linalg.LinAlgError:
    684        raise Exception("Cannot invert hessian matrix.")
    685
    686    def odr_chisquare_compact_y(d):
    687        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
    688        chisq = anp.sum(((d[n_parms + m:] - model) / dy_f) ** 2) + anp.sum(((x_f - d[n_parms:n_parms + m].reshape(x_shape)) / dx_f) ** 2)
    689        return chisq
    690
    691    jac_jac_y = hessian(odr_chisquare_compact_y)(np.concatenate((fitp, out.xplus.ravel(), y_f)))
    692
    693    # Compute hess^{-1} @ jac_jac_y[:n_parms + m, n_parms + m:] using LAPACK dgesv
    694    try:
    695        deriv_y = -scipy.linalg.solve(hess, jac_jac_y[:n_parms + m, n_parms + m:])
    696    except np.linalg.LinAlgError:
    697        raise Exception("Cannot invert hessian matrix.")
    698
    699    result = []
    700    for i in range(n_parms):
    701        result.append(derived_observable(lambda my_var, **kwargs: (my_var[0] + np.finfo(np.float64).eps) / (x.ravel()[0].value + np.finfo(np.float64).eps) * out.beta[i], list(x.ravel()) + list(y), man_grad=list(deriv_x[i]) + list(deriv_y[i])))
    702
    703    output.fit_parameters = result
    704
    705    output.odr_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel())))
    706    output.dof = x.shape[-1] - n_parms
    707    output.p_value = 1 - scipy.stats.chi2.cdf(output.odr_chisquare, output.dof)
    708
    709    return output
    710
    711
    712def fit_lin(x, y, **kwargs):
    713    """Performs a linear fit to y = n + m * x and returns two Obs n, m.
    714
    715    Parameters
    716    ----------
    717    x : list
    718        Can either be a list of floats in which case no xerror is assumed, or
    719        a list of Obs, where the dvalues of the Obs are used as xerror for the fit.
    720    y : list
    721        List of Obs, the dvalues of the Obs are used as yerror for the fit.
    722
    723    Returns
    724    -------
    725    fit_parameters : list[Obs]
    726        LIist of fitted observables.
    727    """
    728
    729    def f(a, x):
    730        y = a[0] + a[1] * x
    731        return y
    732
    733    if all(isinstance(n, Obs) for n in x):
    734        out = total_least_squares(x, y, f, **kwargs)
    735        return out.fit_parameters
    736    elif all(isinstance(n, float) or isinstance(n, int) for n in x) or isinstance(x, np.ndarray):
    737        out = least_squares(x, y, f, **kwargs)
    738        return out.fit_parameters
    739    else:
    740        raise TypeError('Unsupported types for x')
    741
    742
    743def qqplot(x, o_y, func, p, title=""):
    744    """Generates a quantile-quantile plot of the fit result which can be used to
    745       check if the residuals of the fit are gaussian distributed.
    746
    747    Returns
    748    -------
    749    None
    750    """
    751
    752    residuals = []
    753    for i_x, i_y in zip(x, o_y):
    754        residuals.append((i_y - func(p, i_x)) / i_y.dvalue)
    755    residuals = sorted(residuals)
    756    my_y = [o.value for o in residuals]
    757    probplot = scipy.stats.probplot(my_y)
    758    my_x = probplot[0][0]
    759    plt.figure(figsize=(8, 8 / 1.618))
    760    plt.errorbar(my_x, my_y, fmt='o')
    761    fit_start = my_x[0]
    762    fit_stop = my_x[-1]
    763    samples = np.arange(fit_start, fit_stop, 0.01)
    764    plt.plot(samples, samples, 'k--', zorder=11, label='Standard normal distribution')
    765    plt.plot(samples, probplot[1][0] * samples + probplot[1][1], zorder=10, label='Least squares fit, r=' + str(np.around(probplot[1][2], 3)), marker='', ls='-')
    766
    767    plt.xlabel('Theoretical quantiles')
    768    plt.ylabel('Ordered Values')
    769    plt.legend(title=title)
    770    plt.draw()
    771
    772
    773def residual_plot(x, y, func, fit_res, title=""):
    774    """Generates a plot which compares the fit to the data and displays the corresponding residuals
    775
    776    For uncorrelated data the residuals are expected to be distributed ~N(0,1).
    777
    778    Returns
    779    -------
    780    None
    781    """
    782    sorted_x = sorted(x)
    783    xstart = sorted_x[0] - 0.5 * (sorted_x[1] - sorted_x[0])
    784    xstop = sorted_x[-1] + 0.5 * (sorted_x[-1] - sorted_x[-2])
    785    x_samples = np.arange(xstart, xstop + 0.01, 0.01)
    786
    787    plt.figure(figsize=(8, 8 / 1.618))
    788    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], wspace=0.0, hspace=0.0)
    789    ax0 = plt.subplot(gs[0])
    790    ax0.errorbar(x, [o.value for o in y], yerr=[o.dvalue for o in y], ls='none', fmt='o', capsize=3, markersize=5, label='Data')
    791    ax0.plot(x_samples, func([o.value for o in fit_res], x_samples), label='Fit', zorder=10, ls='-', ms=0)
    792    ax0.set_xticklabels([])
    793    ax0.set_xlim([xstart, xstop])
    794    ax0.set_xticklabels([])
    795    ax0.legend(title=title)
    796
    797    residuals = (np.asarray([o.value for o in y]) - func([o.value for o in fit_res], np.asarray(x))) / np.asarray([o.dvalue for o in y])
    798    ax1 = plt.subplot(gs[1])
    799    ax1.plot(x, residuals, 'ko', ls='none', markersize=5)
    800    ax1.tick_params(direction='out')
    801    ax1.tick_params(axis="x", bottom=True, top=True, labelbottom=True)
    802    ax1.axhline(y=0.0, ls='--', color='k', marker=" ")
    803    ax1.fill_between(x_samples, -1.0, 1.0, alpha=0.1, facecolor='k')
    804    ax1.set_xlim([xstart, xstop])
    805    ax1.set_ylabel('Residuals')
    806    plt.subplots_adjust(wspace=None, hspace=None)
    807    plt.draw()
    808
    809
    810def error_band(x, func, beta):
    811    """Calculate the error band for an array of sample values x, for given fit function func with optimized parameters beta.
    812
    813    Returns
    814    -------
    815    err : np.array(Obs)
    816        Error band for an array of sample values x
    817    """
    818    cov = covariance(beta)
    819    if np.any(np.abs(cov - cov.T) > 1000 * np.finfo(np.float64).eps):
    820        warnings.warn("Covariance matrix is not symmetric within floating point precision", RuntimeWarning)
    821
    822    deriv = []
    823    for i, item in enumerate(x):
    824        deriv.append(np.array(egrad(func)([o.value for o in beta], item)))
    825
    826    err = []
    827    for i, item in enumerate(x):
    828        err.append(np.sqrt(deriv[i] @ cov @ deriv[i]))
    829    err = np.array(err)
    830
    831    return err
    832
    833
    834def ks_test(objects=None):
    835    """Performs a Kolmogorov–Smirnov test for the p-values of all fit object.
    836
    837    Parameters
    838    ----------
    839    objects : list
    840        List of fit results to include in the analysis (optional).
    841
    842    Returns
    843    -------
    844    None
    845    """
    846
    847    if objects is None:
    848        obs_list = []
    849        for obj in gc.get_objects():
    850            if isinstance(obj, Fit_result):
    851                obs_list.append(obj)
    852    else:
    853        obs_list = objects
    854
    855    p_values = [o.p_value for o in obs_list]
    856
    857    bins = len(p_values)
    858    x = np.arange(0, 1.001, 0.001)
    859    plt.plot(x, x, 'k', zorder=1)
    860    plt.xlim(0, 1)
    861    plt.ylim(0, 1)
    862    plt.xlabel('p-value')
    863    plt.ylabel('Cumulative probability')
    864    plt.title(str(bins) + ' p-values')
    865
    866    n = np.arange(1, bins + 1) / np.float64(bins)
    867    Xs = np.sort(p_values)
    868    plt.step(Xs, n)
    869    diffs = n - Xs
    870    loc_max_diff = np.argmax(np.abs(diffs))
    871    loc = Xs[loc_max_diff]
    872    plt.annotate('', xy=(loc, loc), xytext=(loc, loc + diffs[loc_max_diff]), arrowprops=dict(arrowstyle='<->', shrinkA=0, shrinkB=0))
    873    plt.draw()
    874
    875    print(scipy.stats.kstest(p_values, 'uniform'))
    876
    877
    878def _extract_val_and_dval(string):
    879    split_string = string.split('(')
    880    if '.' in split_string[0] and '.' not in split_string[1][:-1]:
    881        factor = 10 ** -len(split_string[0].partition('.')[2])
    882    else:
    883        factor = 1
    884    return float(split_string[0]), float(split_string[1][:-1]) * factor
    885
    886
    887def _construct_prior_obs(i_prior, i_n):
    888    if isinstance(i_prior, Obs):
    889        return i_prior
    890    elif isinstance(i_prior, str):
    891        loc_val, loc_dval = _extract_val_and_dval(i_prior)
    892        return cov_Obs(loc_val, loc_dval ** 2, '#prior' + str(i_n) + f"_{np.random.randint(2147483647):010d}")
    893    else:
    894        raise TypeError("Prior entries need to be 'Obs' or 'str'.")

class Fit_result(collections.abc.Sequence): View Source

    21class Fit_result(Sequence):
    22    """Represents fit results.
    23
    24    Attributes
    25    ----------
    26    fit_parameters : list
    27        results for the individual fit parameters,
    28        also accessible via indices.
    29    chisquare_by_dof : float
    30        reduced chisquare.
    31    p_value : float
    32        p-value of the fit
    33    t2_p_value : float
    34        Hotelling t-squared p-value for correlated fits.
    35    """
    36
    37    def __init__(self):
    38        self.fit_parameters = None
    39
    40    def __getitem__(self, idx):
    41        return self.fit_parameters[idx]
    42
    43    def __len__(self):
    44        return len(self.fit_parameters)
    45
    46    def gamma_method(self, **kwargs):
    47        """Apply the gamma method to all fit parameters"""
    48        [o.gamma_method(**kwargs) for o in self.fit_parameters]
    49
    50    gm = gamma_method
    51
    52    def __str__(self):
    53        my_str = 'Goodness of fit:\n'
    54        if hasattr(self, 'chisquare_by_dof'):
    55            my_str += '\u03C7\u00b2/d.o.f. = ' + f'{self.chisquare_by_dof:2.6f}' + '\n'
    56        elif hasattr(self, 'residual_variance'):
    57            my_str += 'residual variance = ' + f'{self.residual_variance:2.6f}' + '\n'
    58        if hasattr(self, 'chisquare_by_expected_chisquare'):
    59            my_str += '\u03C7\u00b2/\u03C7\u00b2exp  = ' + f'{self.chisquare_by_expected_chisquare:2.6f}' + '\n'
    60        if hasattr(self, 'p_value'):
    61            my_str += 'p-value   = ' + f'{self.p_value:2.4f}' + '\n'
    62        if hasattr(self, 't2_p_value'):
    63            my_str += 't\u00B2p-value = ' + f'{self.t2_p_value:2.4f}' + '\n'
    64        my_str += 'Fit parameters:\n'
    65        for i_par, par in enumerate(self.fit_parameters):
    66            my_str += str(i_par) + '\t' + ' ' * int(par >= 0) + str(par).rjust(int(par < 0.0)) + '\n'
    67        return my_str
    68
    69    def __repr__(self):
    70        m = max(map(len, list(self.__dict__.keys()))) + 1
    71        return '\n'.join([key.rjust(m) + ': ' + repr(value) for key, value in sorted(self.__dict__.items())])

Represents fit results.

###### Attributes

- fit_parameters (list): results for the individual fit parameters, also accessible via indices.
- chisquare_by_dof (float): reduced chisquare.
- p_value (float): p-value of the fit
- t2_p_value (float): Hotelling t-squared p-value for correlated fits.

fit_parameters
def gamma_method(self, **kwargs): View Source

    46    def gamma_method(self, **kwargs):
    47        """Apply the gamma method to all fit parameters"""
    48        [o.gamma_method(**kwargs) for o in self.fit_parameters]

Apply the gamma method to all fit parameters

def gm(self, **kwargs): View Source

    46    def gamma_method(self, **kwargs):
    47        """Apply the gamma method to all fit parameters"""
    48        [o.gamma_method(**kwargs) for o in self.fit_parameters]

Apply the gamma method to all fit parameters

def least_squares(x, y, func, priors=None, silent=False, **kwargs): View Source

     74def least_squares(x, y, func, priors=None, silent=False, **kwargs):
     75    r'''Performs a non-linear fit to y = func(x).
     76        ```
     77
     78    Parameters
     79    ----------
     80    For an uncombined fit:
     81
     82    x : list
     83        list of floats.
     84    y : list
     85        list of Obs.
     86    func : object
     87        fit function, has to be of the form
     88
     89        ```python
     90        import autograd.numpy as anp
     91
     92        def func(a, x):
     93            return a[0] + a[1] * x + a[2] * anp.sinh(x)
     94        ```
     95
     96        For multiple x values func can be of the form
     97
     98        ```python
     99        def func(a, x):
    100            (x1, x2) = x
    101            return a[0] * x1 ** 2 + a[1] * x2
    102        ```
    103        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
    104        will not work.
    105
    106    OR For a combined fit:
    107
    108    x : dict
    109        dict of lists.
    110    y : dict
    111        dict of lists of Obs.
    112    funcs : dict
    113        dict of objects
    114        fit functions have to be of the form (here a[0] is the common fit parameter)
    115        ```python
    116        import autograd.numpy as anp
    117        funcs = {"a": func_a,
    118                "b": func_b}
    119
    120        def func_a(a, x):
    121            return a[1] * anp.exp(-a[0] * x)
    122
    123        def func_b(a, x):
    124            return a[2] * anp.exp(-a[0] * x)
    125
    126        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
    127        will not work.
    128
    129    priors : dict or list, optional
    130        priors can either be a dictionary with integer keys and the corresponding priors as values or
    131        a list with an entry for every parameter in the fit. The entries can either be
    132        Obs (e.g. results from a previous fit) or strings containing a value and an error formatted like
    133        0.548(23), 500(40) or 0.5(0.4)
    134    silent : bool, optional
    135        If true all output to the console is omitted (default False).
    136    initial_guess : list
    137        can provide an initial guess for the input parameters. Relevant for
    138        non-linear fits with many parameters. In case of correlated fits the guess is used to perform
    139        an uncorrelated fit which then serves as guess for the correlated fit.
    140    method : str, optional
    141        can be used to choose an alternative method for the minimization of chisquare.
    142        The possible methods are the ones which can be used for scipy.optimize.minimize and
    143        migrad of iminuit. If no method is specified, Levenberg-Marquard is used.
    144        Reliable alternatives are migrad, Powell and Nelder-Mead.
    145    tol: float, optional
    146        can be used (only for combined fits and methods other than Levenberg-Marquard) to set the tolerance for convergence
    147        to a different value to either speed up convergence at the cost of a larger error on the fitted parameters (and possibly
    148        invalid estimates for parameter uncertainties) or smaller values to get more accurate parameter values
    149        The stopping criterion depends on the method, e.g. migrad: edm_max = 0.002 * tol * errordef (EDM criterion: edm < edm_max)
    150    correlated_fit : bool
    151        If True, use the full inverse covariance matrix in the definition of the chisquare cost function.
    152        For details about how the covariance matrix is estimated see `pyerrors.obs.covariance`.
    153        In practice the correlation matrix is Cholesky decomposed and inverted (instead of the covariance matrix).
    154        This procedure should be numerically more stable as the correlation matrix is typically better conditioned (Jacobi preconditioning).
    155    inv_chol_cov_matrix [array,list], optional
    156        array: shape = (no of y values) X (no of y values)
    157        list:   for an uncombined fit: [""]
    158                for a combined fit: list of keys belonging to the corr_matrix saved in the array, must be the same as the keys of the y dict in alphabetical order
    159        If correlated_fit=True is set as well, can provide an inverse covariance matrix (y errors, dy_f included!) of your own choosing for a correlated fit.
    160        The matrix must be a lower triangular matrix constructed from a Cholesky decomposition: The function invert_corr_cov_cholesky(corr, inverrdiag) can be
    161        used to construct it from a correlation matrix (corr) and the errors dy_f of the data points (inverrdiag = np.diag(1 / np.asarray(dy_f))). For the correct
    162        ordering the correlation matrix (corr) can be sorted via the function sort_corr(corr, kl, yd) where kl is the list of keys and yd the y dict.
    163    expected_chisquare : bool
    164        If True estimates the expected chisquare which is
    165        corrected by effects caused by correlated input data (default False).
    166    resplot : bool
    167        If True, a plot which displays fit, data and residuals is generated (default False).
    168    qqplot : bool
    169        If True, a quantile-quantile plot of the fit result is generated (default False).
    170    num_grad : bool
    171        Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
    172
    173    Returns
    174    -------
    175    output : Fit_result
    176        Parameters and information on the fitted result.
    177    Examples
    178    ------
    179    >>> # Example of a correlated (correlated_fit = True, inv_chol_cov_matrix handed over) combined fit, based on a randomly generated data set
    180    >>> import numpy as np
    181    >>> from scipy.stats import norm
    182    >>> from scipy.linalg import cholesky
    183    >>> import pyerrors as pe
    184    >>> # generating the random data set
    185    >>> num_samples = 400
    186    >>> N = 3
    187    >>> x = np.arange(N)
    188    >>> x1 = norm.rvs(size=(N, num_samples)) # generate random numbers
    189    >>> x2 = norm.rvs(size=(N, num_samples)) # generate random numbers
    190    >>> r = r1 = r2 = np.zeros((N, N))
    191    >>> y = {}
    192    >>> for i in range(N):
    193    >>>    for j in range(N):
    194    >>>        r[i, j] = np.exp(-0.8 * np.fabs(i - j)) # element in correlation matrix
    195    >>> errl = np.sqrt([3.4, 2.5, 3.6]) # set y errors
    196    >>> for i in range(N):
    197    >>>    for j in range(N):
    198    >>>        r[i, j] *= errl[i] * errl[j] # element in covariance matrix
    199    >>> c = cholesky(r, lower=True)
    200    >>> y = {'a': np.dot(c, x1), 'b': np.dot(c, x2)} # generate y data with the covariance matrix defined
    201    >>> # random data set has been generated, now the dictionaries and the inverse covariance matrix to be handed over are built
    202    >>> x_dict = {}
    203    >>> y_dict = {}
    204    >>> chol_inv_dict = {}
    205    >>> data = []
    206    >>> for key in y.keys():
    207    >>>    x_dict[key] = x
    208    >>>    for i in range(N):
    209    >>>        data.append(pe.Obs([[i + 1 + o for o in y[key][i]]], ['ens'])) # generate y Obs from the y data
    210    >>>    [o.gamma_method() for o in data]
    211    >>>    corr = pe.covariance(data, correlation=True)
    212    >>>    inverrdiag = np.diag(1 / np.asarray([o.dvalue for o in data]))
    213    >>>    chol_inv = pe.obs.invert_corr_cov_cholesky(corr, inverrdiag) # gives form of the inverse covariance matrix needed for the combined correlated fit below
    214    >>> y_dict = {'a': data[:3], 'b': data[3:]}
    215    >>> # common fit parameter p[0] in combined fit
    216    >>> def fit1(p, x):
    217    >>>    return p[0] + p[1] * x
    218    >>> def fit2(p, x):
    219    >>>    return p[0] + p[2] * x
    220    >>> fitf_dict = {'a': fit1, 'b':fit2}
    221    >>> fitp_inv_cov_combined_fit = pe.least_squares(x_dict,y_dict, fitf_dict, correlated_fit = True, inv_chol_cov_matrix = [chol_inv,['a','b']])
    222    Fit with 3 parameters
    223    Method: Levenberg-Marquardt
    224    `ftol` termination condition is satisfied.
    225    chisquare/d.o.f.: 0.5388013574561786 # random
    226    fit parameters [1.11897846 0.96361162 0.92325319] # random
    227
    228    '''
    229    output = Fit_result()
    230
    231    if (isinstance(x, dict) and isinstance(y, dict) and isinstance(func, dict)):
    232        xd = {key: anp.asarray(x[key]) for key in x}
    233        yd = y
    234        funcd = func
    235        output.fit_function = func
    236    elif (isinstance(x, dict) or isinstance(y, dict) or isinstance(func, dict)):
    237        raise TypeError("All arguments have to be dictionaries in order to perform a combined fit.")
    238    else:
    239        x = np.asarray(x)
    240        xd = {"": x}
    241        yd = {"": y}
    242        funcd = {"": func}
    243        output.fit_function = func
    244
    245    if kwargs.get('num_grad') is True:
    246        jacobian = num_jacobian
    247        hessian = num_hessian
    248    else:
    249        jacobian = auto_jacobian
    250        hessian = auto_hessian
    251
    252    key_ls = sorted(list(xd.keys()))
    253
    254    if sorted(list(yd.keys())) != key_ls:
    255        raise ValueError('x and y dictionaries do not contain the same keys.')
    256
    257    if sorted(list(funcd.keys())) != key_ls:
    258        raise ValueError('x and func dictionaries do not contain the same keys.')
    259
    260    x_all = np.concatenate([np.array(xd[key]).transpose() for key in key_ls]).transpose()
    261    y_all = np.concatenate([np.array(yd[key]) for key in key_ls])
    262
    263    y_f = [o.value for o in y_all]
    264    dy_f = [o.dvalue for o in y_all]
    265
    266    if len(x_all.shape) > 2:
    267        raise ValueError("Unknown format for x values")
    268
    269    if np.any(np.asarray(dy_f) <= 0.0):
    270        raise Exception("No y errors available, run the gamma method first.")
    271
    272    # number of fit parameters
    273    n_parms_ls = []
    274    for key in key_ls:
    275        if not callable(funcd[key]):
    276            raise TypeError('func (key=' + key + ') is not a function.')
    277        if np.asarray(xd[key]).shape[-1] != len(yd[key]):
    278            raise ValueError('x and y input (key=' + key + ') do not have the same length')
    279        for n_loc in range(100):
    280            try:
    281                funcd[key](np.arange(n_loc), x_all.T[0])
    282            except TypeError:
    283                continue
    284            except IndexError:
    285                continue
    286            else:
    287                break
    288        else:
    289            raise RuntimeError("Fit function (key=" + key + ") is not valid.")
    290        n_parms_ls.append(n_loc)
    291
    292    n_parms = max(n_parms_ls)
    293
    294    if len(key_ls) > 1:
    295        for key in key_ls:
    296            if np.asarray(yd[key]).shape != funcd[key](np.arange(n_parms), xd[key]).shape:
    297                raise ValueError(f"Fit function {key} returns the wrong shape ({funcd[key](np.arange(n_parms), xd[key]).shape} instead of {np.asarray(yd[key]).shape})\nIf the fit function is just a constant you could try adding x*0 to get the correct shape.")
    298
    299    if not silent:
    300        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
    301
    302    if priors is not None:
    303        if isinstance(priors, (list, np.ndarray)):
    304            if n_parms != len(priors):
    305                raise ValueError("'priors' does not have the correct length.")
    306
    307            loc_priors = []
    308            for i_n, i_prior in enumerate(priors):
    309                loc_priors.append(_construct_prior_obs(i_prior, i_n))
    310
    311            prior_mask = np.arange(len(priors))
    312            output.priors = loc_priors
    313
    314        elif isinstance(priors, dict):
    315            loc_priors = []
    316            prior_mask = []
    317            output.priors = {}
    318            for pos, prior in priors.items():
    319                if isinstance(pos, int):
    320                    prior_mask.append(pos)
    321                else:
    322                    raise TypeError("Prior position needs to be an integer.")
    323                loc_priors.append(_construct_prior_obs(prior, pos))
    324
    325                output.priors[pos] = loc_priors[-1]
    326            if max(prior_mask) >= n_parms:
    327                raise ValueError("Prior position out of range.")
    328        else:
    329            raise TypeError("Unkown type for `priors`.")
    330
    331        p_f = [o.value for o in loc_priors]
    332        dp_f = [o.dvalue for o in loc_priors]
    333        if np.any(np.asarray(dp_f) <= 0.0):
    334            raise Exception("No prior errors available, run the gamma method first.")
    335    else:
    336        p_f = dp_f = np.array([])
    337        prior_mask = []
    338        loc_priors = []
    339
    340    if 'initial_guess' in kwargs:
    341        x0 = kwargs.get('initial_guess')
    342        if len(x0) != n_parms:
    343            raise ValueError('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
    344    else:
    345        x0 = [0.1] * n_parms
    346
    347    if priors is None:
    348        def general_chisqfunc_uncorr(p, ivars, pr):
    349            model = anp.concatenate([anp.array(funcd[key](p, xd[key])).reshape(-1) for key in key_ls])
    350            return (ivars - model) / dy_f
    351    else:
    352        def general_chisqfunc_uncorr(p, ivars, pr):
    353            model = anp.concatenate([anp.array(funcd[key](p, xd[key])).reshape(-1) for key in key_ls])
    354            return anp.concatenate(((ivars - model) / dy_f, (p[prior_mask] - pr) / dp_f))
    355
    356    def chisqfunc_uncorr(p):
    357        return anp.sum(general_chisqfunc_uncorr(p, y_f, p_f) ** 2)
    358
    359    if kwargs.get('correlated_fit') is True:
    360        if 'inv_chol_cov_matrix' in kwargs:
    361            chol_inv = kwargs.get('inv_chol_cov_matrix')
    362            if (chol_inv[0].shape[0] != len(dy_f)):
    363                raise TypeError('The number of columns of the inverse covariance matrix handed over needs to be equal to the number of y errors.')
    364            if (chol_inv[0].shape[0] != chol_inv[0].shape[1]):
    365                raise TypeError('The inverse covariance matrix handed over needs to have the same number of rows as columns.')
    366            if (chol_inv[1] != key_ls):
    367                raise ValueError('The keys of inverse covariance matrix are not the same or do not appear in the same order as the x and y values.')
    368            chol_inv = chol_inv[0]
    369            if np.any(np.diag(chol_inv) <= 0) or (not np.all(chol_inv == np.tril(chol_inv))):
    370                raise ValueError('The inverse covariance matrix inv_chol_cov_matrix[0] has to be a lower triangular matrix constructed from a Cholesky decomposition.')
    371        else:
    372            corr = covariance(y_all, correlation=True, **kwargs)
    373            inverrdiag = np.diag(1 / np.asarray(dy_f))
    374            chol_inv = invert_corr_cov_cholesky(corr, inverrdiag)
    375
    376        def general_chisqfunc(p, ivars, pr):
    377            model = anp.concatenate([anp.array(funcd[key](p, xd[key])).reshape(-1) for key in key_ls])
    378            return anp.concatenate((anp.dot(chol_inv, (ivars - model)), (p[prior_mask] - pr) / dp_f))
    379
    380        def chisqfunc(p):
    381            return anp.sum(general_chisqfunc(p, y_f, p_f) ** 2)
    382    else:
    383        general_chisqfunc = general_chisqfunc_uncorr
    384        chisqfunc = chisqfunc_uncorr
    385
    386    output.method = kwargs.get('method', 'Levenberg-Marquardt')
    387    if not silent:
    388        print('Method:', output.method)
    389
    390    if output.method != 'Levenberg-Marquardt':
    391        if output.method == 'migrad':
    392            tolerance = 1e-4  # default value of 1e-1 set by iminuit can be problematic
    393            if 'tol' in kwargs:
    394                tolerance = kwargs.get('tol')
    395            fit_result = iminuit.minimize(chisqfunc_uncorr, x0, tol=tolerance)  # Stopping criterion 0.002 * tol * errordef
    396            if kwargs.get('correlated_fit') is True:
    397                fit_result = iminuit.minimize(chisqfunc, fit_result.x, tol=tolerance)
    398            output.iterations = fit_result.nfev
    399        else:
    400            tolerance = 1e-12
    401            if 'tol' in kwargs:
    402                tolerance = kwargs.get('tol')
    403            fit_result = scipy.optimize.minimize(chisqfunc_uncorr, x0, method=kwargs.get('method'), tol=tolerance)
    404            if kwargs.get('correlated_fit') is True:
    405                fit_result = scipy.optimize.minimize(chisqfunc, fit_result.x, method=kwargs.get('method'), tol=tolerance)
    406            output.iterations = fit_result.nit
    407
    408        chisquare = fit_result.fun
    409
    410    else:
    411        if 'tol' in kwargs:
    412            print('tol cannot be set for Levenberg-Marquardt')
    413
    414        def chisqfunc_residuals_uncorr(p):
    415            return general_chisqfunc_uncorr(p, y_f, p_f)
    416
    417        fit_result = scipy.optimize.least_squares(chisqfunc_residuals_uncorr, x0, method='lm', ftol=1e-15, gtol=1e-15, xtol=1e-15)
    418        if kwargs.get('correlated_fit') is True:
    419            def chisqfunc_residuals(p):
    420                return general_chisqfunc(p, y_f, p_f)
    421
    422            fit_result = scipy.optimize.least_squares(chisqfunc_residuals, fit_result.x, method='lm', ftol=1e-15, gtol=1e-15, xtol=1e-15)
    423
    424        chisquare = np.sum(fit_result.fun ** 2)
    425        assert np.isclose(chisquare, chisqfunc(fit_result.x), atol=1e-14)
    426
    427        output.iterations = fit_result.nfev
    428
    429    if not fit_result.success:
    430        raise Exception('The minimization procedure did not converge.')
    431
    432    output.chisquare = chisquare
    433    output.dof = y_all.shape[-1] - n_parms + len(loc_priors)
    434    output.p_value = 1 - scipy.stats.chi2.cdf(output.chisquare, output.dof)
    435    if output.dof > 0:
    436        output.chisquare_by_dof = output.chisquare / output.dof
    437    else:
    438        output.chisquare_by_dof = float('nan')
    439
    440    output.message = fit_result.message
    441    if not silent:
    442        print(fit_result.message)
    443        print('chisquare/d.o.f.:', output.chisquare_by_dof)
    444        print('fit parameters', fit_result.x)
    445
    446    def prepare_hat_matrix():
    447        hat_vector = []
    448        for key in key_ls:
    449            if (len(xd[key]) != 0):
    450                hat_vector.append(jacobian(funcd[key])(fit_result.x, xd[key]))
    451        hat_vector = [item for sublist in hat_vector for item in sublist]
    452        return hat_vector
    453
    454    if kwargs.get('expected_chisquare') is True:
    455        if kwargs.get('correlated_fit') is not True:
    456            W = np.diag(1 / np.asarray(dy_f))
    457            cov = covariance(y_all)
    458            hat_vector = prepare_hat_matrix()
    459            A = W @ hat_vector
    460            P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
    461            expected_chisquare = np.trace((np.identity(y_all.shape[-1]) - P_phi) @ W @ cov @ W)
    462            output.chisquare_by_expected_chisquare = output.chisquare / expected_chisquare
    463            if not silent:
    464                print('chisquare/expected_chisquare:', output.chisquare_by_expected_chisquare)
    465
    466    fitp = fit_result.x
    467
    468    try:
    469        hess = hessian(chisqfunc)(fitp)
    470    except TypeError:
    471        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
    472
    473    len_y = len(y_f)
    474
    475    def chisqfunc_compact(d):
    476        return anp.sum(general_chisqfunc(d[:n_parms], d[n_parms: n_parms + len_y], d[n_parms + len_y:]) ** 2)
    477
    478    jac_jac_y = hessian(chisqfunc_compact)(np.concatenate((fitp, y_f, p_f)))
    479
    480    # Compute hess^{-1} @ jac_jac_y[:n_parms + m, n_parms + m:] using LAPACK dgesv
    481    try:
    482        deriv_y = -scipy.linalg.solve(hess, jac_jac_y[:n_parms, n_parms:])
    483    except np.linalg.LinAlgError:
    484        raise Exception("Cannot invert hessian matrix.")
    485
    486    result = []
    487    for i in range(n_parms):
    488        result.append(derived_observable(lambda x_all, **kwargs: (x_all[0] + np.finfo(np.float64).eps) / (y_all[0].value + np.finfo(np.float64).eps) * fitp[i], list(y_all) + loc_priors, man_grad=list(deriv_y[i])))
    489
    490    output.fit_parameters = result
    491
    492    # Hotelling t-squared p-value for correlated fits.
    493    if kwargs.get('correlated_fit') is True:
    494        n_cov = np.min(np.vectorize(lambda x_all: x_all.N)(y_all))
    495        output.t2_p_value = 1 - scipy.stats.f.cdf((n_cov - output.dof) / (output.dof * (n_cov - 1)) * output.chisquare,
    496                                                  output.dof, n_cov - output.dof)
    497
    498    if kwargs.get('resplot') is True:
    499        for key in key_ls:
    500            residual_plot(xd[key], yd[key], funcd[key], result, title=key)
    501
    502    if kwargs.get('qqplot') is True:
    503        for key in key_ls:
    504            qqplot(xd[key], yd[key], funcd[key], result, title=key)
    505
    506    return output

Performs a non-linear fit to y = func(x). ```

###### Parameters

- For an uncombined fit:
- x (list): list of floats.
- y (list): list of Obs.

- func (object): fit function, has to be of the form

      import autograd.numpy as anp

      def func(a, x):
          return a[0] + a[1] * x + a[2] * anp.sinh(x)

  For multiple x values func can be of the form

      def func(a, x):
          (x1, x2) = x
          return a[0] * x1 ** 2 + a[1] * x2

  It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation will not work.

- OR For a combined fit:
- x (dict): dict of lists.
- y (dict): dict of lists of Obs.

- funcs (dict): dict of objects fit functions have to be of the form (here a[0] is the common fit parameter) ```python import autograd.numpy as anp funcs = {"a": func_a, "b": func_b}

  def func_a(a, x): return a[1] * anp.exp(-a[0] * x)

  def func_b(a, x): return a[2] * anp.exp(-a[0] * x)

  It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation will not work.

- priors (dict or list, optional): priors can either be a dictionary with integer keys and the corresponding priors as values or a list with an entry for every parameter in the fit. The entries can either be Obs (e.g. results from a previous fit) or strings containing a value and an error formatted like 0.548(23), 500(40) or 0.5(0.4)
- silent (bool, optional): If true all output to the console is omitted (default False).
- initial_guess (list): can provide an initial guess for the input parameters. Relevant for non-linear fits with many parameters. In case of correlated fits the guess is used to perform an uncorrelated fit which then serves as guess for the correlated fit.
- method (str, optional): can be used to choose an alternative method for the minimization of chisquare. The possible methods are the ones which can be used for scipy.optimize.minimize and migrad of iminuit. If no method is specified, Levenberg-Marquard is used. Reliable alternatives are migrad, Powell and Nelder-Mead.
- tol (float, optional): can be used (only for combined fits and methods other than Levenberg-Marquard) to set the tolerance for convergence to a different value to either speed up convergence at the cost of a larger error on the fitted parameters (and possibly invalid estimates for parameter uncertainties) or smaller values to get more accurate parameter values The stopping criterion depends on the method, e.g. migrad: edm_max = 0.002 * tol * errordef (EDM criterion: edm < edm_max)
- correlated_fit (bool): If True, use the full inverse covariance matrix in the definition of the chisquare cost function. For details about how the covariance matrix is estimated see `pyerrors.obs.covariance`. In practice the correlation matrix is Cholesky decomposed and inverted (instead of the covariance matrix). This procedure should be numerically more stable as the correlation matrix is typically better conditioned (Jacobi preconditioning).
- inv_chol_cov_matrix [array,list], optional: array: shape = (no of y values) X (no of y values) list: for an uncombined fit: [""] for a combined fit: list of keys belonging to the corr_matrix saved in the array, must be the same as the keys of the y dict in alphabetical order If correlated_fit=True is set as well, can provide an inverse covariance matrix (y errors, dy_f included!) of your own choosing for a correlated fit. The matrix must be a lower triangular matrix constructed from a Cholesky decomposition: The function invert_corr_cov_cholesky(corr, inverrdiag) can be used to construct it from a correlation matrix (corr) and the errors dy_f of the data points (inverrdiag = np.diag(1 / np.asarray(dy_f))). For the correct ordering the correlation matrix (corr) can be sorted via the function sort_corr(corr, kl, yd) where kl is the list of keys and yd the y dict.
- expected_chisquare (bool): If True estimates the expected chisquare which is corrected by effects caused by correlated input data (default False).
- resplot (bool): If True, a plot which displays fit, data and residuals is generated (default False).
- qqplot (bool): If True, a quantile-quantile plot of the fit result is generated (default False).
- num_grad (bool): Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).

###### Returns

- output (Fit_result): Parameters and information on the fitted result.

###### Examples

    >>> # Example of a correlated (correlated_fit = True, inv_chol_cov_matrix handed over) combined fit, based on a randomly generated data set
    >>> import numpy as np
    >>> from scipy.stats import norm
    >>> from scipy.linalg import cholesky
    >>> import pyerrors as pe
    >>> # generating the random data set
    >>> num_samples = 400
    >>> N = 3
    >>> x = np.arange(N)
    >>> x1 = norm.rvs(size=(N, num_samples)) # generate random numbers
    >>> x2 = norm.rvs(size=(N, num_samples)) # generate random numbers
    >>> r = r1 = r2 = np.zeros((N, N))
    >>> y = {}
    >>> for i in range(N):
    >>>    for j in range(N):
    >>>        r[i, j] = np.exp(-0.8 * np.fabs(i - j)) # element in correlation matrix
    >>> errl = np.sqrt([3.4, 2.5, 3.6]) # set y errors
    >>> for i in range(N):
    >>>    for j in range(N):
    >>>        r[i, j] *= errl[i] * errl[j] # element in covariance matrix
    >>> c = cholesky(r, lower=True)
    >>> y = {'a': np.dot(c, x1), 'b': np.dot(c, x2)} # generate y data with the covariance matrix defined
    >>> # random data set has been generated, now the dictionaries and the inverse covariance matrix to be handed over are built
    >>> x_dict = {}
    >>> y_dict = {}
    >>> chol_inv_dict = {}
    >>> data = []
    >>> for key in y.keys():
    >>>    x_dict[key] = x
    >>>    for i in range(N):
    >>>        data.append(pe.Obs([[i + 1 + o for o in y[key][i]]], ['ens'])) # generate y Obs from the y data
    >>>    [o.gamma_method() for o in data]
    >>>    corr = pe.covariance(data, correlation=True)
    >>>    inverrdiag = np.diag(1 / np.asarray([o.dvalue for o in data]))
    >>>    chol_inv = pe.obs.invert_corr_cov_cholesky(corr, inverrdiag) # gives form of the inverse covariance matrix needed for the combined correlated fit below
    >>> y_dict = {'a': data[:3], 'b': data[3:]}
    >>> # common fit parameter p[0] in combined fit
    >>> def fit1(p, x):
    >>>    return p[0] + p[1] * x
    >>> def fit2(p, x):
    >>>    return p[0] + p[2] * x
    >>> fitf_dict = {'a': fit1, 'b':fit2}
    >>> fitp_inv_cov_combined_fit = pe.least_squares(x_dict,y_dict, fitf_dict, correlated_fit = True, inv_chol_cov_matrix = [chol_inv,['a','b']])
    Fit with 3 parameters
    Method: Levenberg-Marquardt
    `ftol` termination condition is satisfied.
    chisquare/d.o.f.: 0.5388013574561786 # random
    fit parameters [1.11897846 0.96361162 0.92325319] # random

def total_least_squares(x, y, func, silent=False, **kwargs): View Source

    509def total_least_squares(x, y, func, silent=False, **kwargs):
    510    r'''Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.
    511
    512    Parameters
    513    ----------
    514    x : list
    515        list of Obs, or a tuple of lists of Obs
    516    y : list
    517        list of Obs. The dvalues of the Obs are used as x- and yerror for the fit.
    518    func : object
    519        func has to be of the form
    520
    521        ```python
    522        import autograd.numpy as anp
    523
    524        def func(a, x):
    525            return a[0] + a[1] * x + a[2] * anp.sinh(x)
    526        ```
    527
    528        For multiple x values func can be of the form
    529
    530        ```python
    531        def func(a, x):
    532            (x1, x2) = x
    533            return a[0] * x1 ** 2 + a[1] * x2
    534        ```
    535
    536        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
    537        will not work.
    538    silent : bool, optional
    539        If true all output to the console is omitted (default False).
    540    initial_guess : list
    541        can provide an initial guess for the input parameters. Relevant for non-linear
    542        fits with many parameters.
    543    expected_chisquare : bool
    544        If true prints the expected chisquare which is
    545        corrected by effects caused by correlated input data.
    546        This can take a while as the full correlation matrix
    547        has to be calculated (default False).
    548    num_grad : bool
    549        Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
    550
    551    Notes
    552    -----
    553    Based on the orthogonal distance regression module of scipy.
    554
    555    Returns
    556    -------
    557    output : Fit_result
    558        Parameters and information on the fitted result.
    559    '''
    560
    561    output = Fit_result()
    562
    563    output.fit_function = func
    564
    565    x = np.array(x)
    566
    567    x_shape = x.shape
    568
    569    if kwargs.get('num_grad') is True:
    570        jacobian = num_jacobian
    571        hessian = num_hessian
    572    else:
    573        jacobian = auto_jacobian
    574        hessian = auto_hessian
    575
    576    if not callable(func):
    577        raise TypeError('func has to be a function.')
    578
    579    for i in range(42):
    580        try:
    581            func(np.arange(i), x.T[0])
    582        except TypeError:
    583            continue
    584        except IndexError:
    585            continue
    586        else:
    587            break
    588    else:
    589        raise RuntimeError("Fit function is not valid.")
    590
    591    n_parms = i
    592    if not silent:
    593        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
    594
    595    x_f = np.vectorize(lambda o: o.value)(x)
    596    dx_f = np.vectorize(lambda o: o.dvalue)(x)
    597    y_f = np.array([o.value for o in y])
    598    dy_f = np.array([o.dvalue for o in y])
    599
    600    if np.any(np.asarray(dx_f) <= 0.0):
    601        raise Exception('No x errors available, run the gamma method first.')
    602
    603    if np.any(np.asarray(dy_f) <= 0.0):
    604        raise Exception('No y errors available, run the gamma method first.')
    605
    606    if 'initial_guess' in kwargs:
    607        x0 = kwargs.get('initial_guess')
    608        if len(x0) != n_parms:
    609            raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
    610    else:
    611        x0 = [1] * n_parms
    612
    613    data = RealData(x_f, y_f, sx=dx_f, sy=dy_f)
    614    model = Model(func)
    615    odr = ODR(data, model, x0, partol=np.finfo(np.float64).eps)
    616    odr.set_job(fit_type=0, deriv=1)
    617    out = odr.run()
    618
    619    output.residual_variance = out.res_var
    620
    621    output.method = 'ODR'
    622
    623    output.message = out.stopreason
    624
    625    output.xplus = out.xplus
    626
    627    if not silent:
    628        print('Method: ODR')
    629        print(*out.stopreason)
    630        print('Residual variance:', output.residual_variance)
    631
    632    if out.info > 3:
    633        raise Exception('The minimization procedure did not converge.')
    634
    635    m = x_f.size
    636
    637    def odr_chisquare(p):
    638        model = func(p[:n_parms], p[n_parms:].reshape(x_shape))
    639        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((x_f - p[n_parms:].reshape(x_shape)) / dx_f) ** 2)
    640        return chisq
    641
    642    if kwargs.get('expected_chisquare') is True:
    643        W = np.diag(1 / np.asarray(np.concatenate((dy_f.ravel(), dx_f.ravel()))))
    644
    645        if kwargs.get('covariance') is not None:
    646            cov = kwargs.get('covariance')
    647        else:
    648            cov = covariance(np.concatenate((y, x.ravel())))
    649
    650        number_of_x_parameters = int(m / x_f.shape[-1])
    651
    652        old_jac = jacobian(func)(out.beta, out.xplus)
    653        fused_row1 = np.concatenate((old_jac, np.concatenate((number_of_x_parameters * [np.zeros(old_jac.shape)]), axis=0)))
    654        fused_row2 = np.concatenate((jacobian(lambda x, y: func(y, x))(out.xplus, out.beta).reshape(x_f.shape[-1], x_f.shape[-1] * number_of_x_parameters), np.identity(number_of_x_parameters * old_jac.shape[0])))
    655        new_jac = np.concatenate((fused_row1, fused_row2), axis=1)
    656
    657        A = W @ new_jac
    658        P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
    659        expected_chisquare = np.trace((np.identity(P_phi.shape[0]) - P_phi) @ W @ cov @ W)
    660        if expected_chisquare <= 0.0:
    661            warnings.warn("Negative expected_chisquare.", RuntimeWarning)
    662            expected_chisquare = np.abs(expected_chisquare)
    663        output.chisquare_by_expected_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel()))) / expected_chisquare
    664        if not silent:
    665            print('chisquare/expected_chisquare:',
    666                  output.chisquare_by_expected_chisquare)
    667
    668    fitp = out.beta
    669    try:
    670        hess = hessian(odr_chisquare)(np.concatenate((fitp, out.xplus.ravel())))
    671    except TypeError:
    672        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
    673
    674    def odr_chisquare_compact_x(d):
    675        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
    676        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((d[n_parms + m:].reshape(x_shape) - d[n_parms:n_parms + m].reshape(x_shape)) / dx_f) ** 2)
    677        return chisq
    678
    679    jac_jac_x = hessian(odr_chisquare_compact_x)(np.concatenate((fitp, out.xplus.ravel(), x_f.ravel())))
    680
    681    # Compute hess^{-1} @ jac_jac_x[:n_parms + m, n_parms + m:] using LAPACK dgesv
    682    try:
    683        deriv_x = -scipy.linalg.solve(hess, jac_jac_x[:n_parms + m, n_parms + m:])
    684    except np.linalg.LinAlgError:
    685        raise Exception("Cannot invert hessian matrix.")
    686
    687    def odr_chisquare_compact_y(d):
    688        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
    689        chisq = anp.sum(((d[n_parms + m:] - model) / dy_f) ** 2) + anp.sum(((x_f - d[n_parms:n_parms + m].reshape(x_shape)) / dx_f) ** 2)
    690        return chisq
    691
    692    jac_jac_y = hessian(odr_chisquare_compact_y)(np.concatenate((fitp, out.xplus.ravel(), y_f)))
    693
    694    # Compute hess^{-1} @ jac_jac_y[:n_parms + m, n_parms + m:] using LAPACK dgesv
    695    try:
    696        deriv_y = -scipy.linalg.solve(hess, jac_jac_y[:n_parms + m, n_parms + m:])
    697    except np.linalg.LinAlgError:
    698        raise Exception("Cannot invert hessian matrix.")
    699
    700    result = []
    701    for i in range(n_parms):
    702        result.append(derived_observable(lambda my_var, **kwargs: (my_var[0] + np.finfo(np.float64).eps) / (x.ravel()[0].value + np.finfo(np.float64).eps) * out.beta[i], list(x.ravel()) + list(y), man_grad=list(deriv_x[i]) + list(deriv_y[i])))
    703
    704    output.fit_parameters = result
    705
    706    output.odr_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel())))
    707    output.dof = x.shape[-1] - n_parms
    708    output.p_value = 1 - scipy.stats.chi2.cdf(output.odr_chisquare, output.dof)
    709
    710    return output

Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.

###### Parameters

- x (list): list of Obs, or a tuple of lists of Obs
- y (list): list of Obs. The dvalues of the Obs are used as x- and yerror for the fit.

- func (object): func has to be of the form

      import autograd.numpy as anp

      def func(a, x):
          return a[0] + a[1] * x + a[2] * anp.sinh(x)

  For multiple x values func can be of the form

      def func(a, x):
          (x1, x2) = x
          return a[0] * x1 ** 2 + a[1] * x2

  It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation will not work.

- silent (bool, optional): If true all output to the console is omitted (default False).
- initial_guess (list): can provide an initial guess for the input parameters. Relevant for non-linear fits with many parameters.
- expected_chisquare (bool): If true prints the expected chisquare which is corrected by effects caused by correlated input data. This can take a while as the full correlation matrix has to be calculated (default False).
- num_grad (bool): Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).

###### Notes

Based on the orthogonal distance regression module of scipy.

###### Returns

- output (Fit_result): Parameters and information on the fitted result.

def fit_lin(x, y, **kwargs): View Source

    713def fit_lin(x, y, **kwargs):
    714    """Performs a linear fit to y = n + m * x and returns two Obs n, m.
    715
    716    Parameters
    717    ----------
    718    x : list
    719        Can either be a list of floats in which case no xerror is assumed, or
    720        a list of Obs, where the dvalues of the Obs are used as xerror for the fit.
    721    y : list
    722        List of Obs, the dvalues of the Obs are used as yerror for the fit.
    723
    724    Returns
    725    -------
    726    fit_parameters : list[Obs]
    727        LIist of fitted observables.
    728    """
    729
    730    def f(a, x):
    731        y = a[0] + a[1] * x
    732        return y
    733
    734    if all(isinstance(n, Obs) for n in x):
    735        out = total_least_squares(x, y, f, **kwargs)
    736        return out.fit_parameters
    737    elif all(isinstance(n, float) or isinstance(n, int) for n in x) or isinstance(x, np.ndarray):
    738        out = least_squares(x, y, f, **kwargs)
    739        return out.fit_parameters
    740    else:
    741        raise TypeError('Unsupported types for x')

Performs a linear fit to y = n + m * x and returns two Obs n, m.

###### Parameters

- x (list): Can either be a list of floats in which case no xerror is assumed, or a list of Obs, where the dvalues of the Obs are used as xerror for the fit.
- y (list): List of Obs, the dvalues of the Obs are used as yerror for the fit.

###### Returns

- fit_parameters (list[Obs]): LIist of fitted observables.

def qqplot(x, o_y, func, p, title=''): View Source

    744def qqplot(x, o_y, func, p, title=""):
    745    """Generates a quantile-quantile plot of the fit result which can be used to
    746       check if the residuals of the fit are gaussian distributed.
    747
    748    Returns
    749    -------
    750    None
    751    """
    752
    753    residuals = []
    754    for i_x, i_y in zip(x, o_y):
    755        residuals.append((i_y - func(p, i_x)) / i_y.dvalue)
    756    residuals = sorted(residuals)
    757    my_y = [o.value for o in residuals]
    758    probplot = scipy.stats.probplot(my_y)
    759    my_x = probplot[0][0]
    760    plt.figure(figsize=(8, 8 / 1.618))
    761    plt.errorbar(my_x, my_y, fmt='o')
    762    fit_start = my_x[0]
    763    fit_stop = my_x[-1]
    764    samples = np.arange(fit_start, fit_stop, 0.01)
    765    plt.plot(samples, samples, 'k--', zorder=11, label='Standard normal distribution')
    766    plt.plot(samples, probplot[1][0] * samples + probplot[1][1], zorder=10, label='Least squares fit, r=' + str(np.around(probplot[1][2], 3)), marker='', ls='-')
    767
    768    plt.xlabel('Theoretical quantiles')
    769    plt.ylabel('Ordered Values')
    770    plt.legend(title=title)
    771    plt.draw()

Generates a quantile-quantile plot of the fit result which can be used to check if the residuals of the fit are gaussian distributed.

###### Returns

- None

def residual_plot(x, y, func, fit_res, title=''): View Source

    774def residual_plot(x, y, func, fit_res, title=""):
    775    """Generates a plot which compares the fit to the data and displays the corresponding residuals
    776
    777    For uncorrelated data the residuals are expected to be distributed ~N(0,1).
    778
    779    Returns
    780    -------
    781    None
    782    """
    783    sorted_x = sorted(x)
    784    xstart = sorted_x[0] - 0.5 * (sorted_x[1] - sorted_x[0])
    785    xstop = sorted_x[-1] + 0.5 * (sorted_x[-1] - sorted_x[-2])
    786    x_samples = np.arange(xstart, xstop + 0.01, 0.01)
    787
    788    plt.figure(figsize=(8, 8 / 1.618))
    789    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], wspace=0.0, hspace=0.0)
    790    ax0 = plt.subplot(gs[0])
    791    ax0.errorbar(x, [o.value for o in y], yerr=[o.dvalue for o in y], ls='none', fmt='o', capsize=3, markersize=5, label='Data')
    792    ax0.plot(x_samples, func([o.value for o in fit_res], x_samples), label='Fit', zorder=10, ls='-', ms=0)
    793    ax0.set_xticklabels([])
    794    ax0.set_xlim([xstart, xstop])
    795    ax0.set_xticklabels([])
    796    ax0.legend(title=title)
    797
    798    residuals = (np.asarray([o.value for o in y]) - func([o.value for o in fit_res], np.asarray(x))) / np.asarray([o.dvalue for o in y])
    799    ax1 = plt.subplot(gs[1])
    800    ax1.plot(x, residuals, 'ko', ls='none', markersize=5)
    801    ax1.tick_params(direction='out')
    802    ax1.tick_params(axis="x", bottom=True, top=True, labelbottom=True)
    803    ax1.axhline(y=0.0, ls='--', color='k', marker=" ")
    804    ax1.fill_between(x_samples, -1.0, 1.0, alpha=0.1, facecolor='k')
    805    ax1.set_xlim([xstart, xstop])
    806    ax1.set_ylabel('Residuals')
    807    plt.subplots_adjust(wspace=None, hspace=None)
    808    plt.draw()

Generates a plot which compares the fit to the data and displays the corresponding residuals

For uncorrelated data the residuals are expected to be distributed ~N(0,1).

###### Returns

- None

def error_band(x, func, beta): View Source

    811def error_band(x, func, beta):
    812    """Calculate the error band for an array of sample values x, for given fit function func with optimized parameters beta.
    813
    814    Returns
    815    -------
    816    err : np.array(Obs)
    817        Error band for an array of sample values x
    818    """
    819    cov = covariance(beta)
    820    if np.any(np.abs(cov - cov.T) > 1000 * np.finfo(np.float64).eps):
    821        warnings.warn("Covariance matrix is not symmetric within floating point precision", RuntimeWarning)
    822
    823    deriv = []
    824    for i, item in enumerate(x):
    825        deriv.append(np.array(egrad(func)([o.value for o in beta], item)))
    826
    827    err = []
    828    for i, item in enumerate(x):
    829        err.append(np.sqrt(deriv[i] @ cov @ deriv[i]))
    830    err = np.array(err)
    831
    832    return err

Calculate the error band for an array of sample values x, for given fit function func with optimized parameters beta.

###### Returns

- err (np.array(Obs)): Error band for an array of sample values x

def ks_test(objects=None): View Source

    835def ks_test(objects=None):
    836    """Performs a Kolmogorov–Smirnov test for the p-values of all fit object.
    837
    838    Parameters
    839    ----------
    840    objects : list
    841        List of fit results to include in the analysis (optional).
    842
    843    Returns
    844    -------
    845    None
    846    """
    847
    848    if objects is None:
    849        obs_list = []
    850        for obj in gc.get_objects():
    851            if isinstance(obj, Fit_result):
    852                obs_list.append(obj)
    853    else:
    854        obs_list = objects
    855
    856    p_values = [o.p_value for o in obs_list]
    857
    858    bins = len(p_values)
    859    x = np.arange(0, 1.001, 0.001)
    860    plt.plot(x, x, 'k', zorder=1)
    861    plt.xlim(0, 1)
    862    plt.ylim(0, 1)
    863    plt.xlabel('p-value')
    864    plt.ylabel('Cumulative probability')
    865    plt.title(str(bins) + ' p-values')
    866
    867    n = np.arange(1, bins + 1) / np.float64(bins)
    868    Xs = np.sort(p_values)
    869    plt.step(Xs, n)
    870    diffs = n - Xs
    871    loc_max_diff = np.argmax(np.abs(diffs))
    872    loc = Xs[loc_max_diff]
    873    plt.annotate('', xy=(loc, loc), xytext=(loc, loc + diffs[loc_max_diff]), arrowprops=dict(arrowstyle='<->', shrinkA=0, shrinkB=0))
    874    plt.draw()
    875
    876    print(scipy.stats.kstest(p_values, 'uniform'))

Performs a Kolmogorov–Smirnov test for the p-values of all fit object.

###### Parameters

- objects (list): List of fit results to include in the analysis (optional).

###### Returns

- None