pyerrors.fits

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

Inherited Members
collections.abc.Sequence
index
count
def least_squares(x, y, func, priors=None, silent=False, **kwargs):
 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 : list, optional
130        priors has to be 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        At the moment this option only works for `prior==None` and when no `method` is given.
155    expected_chisquare : bool
156        If True estimates the expected chisquare which is
157        corrected by effects caused by correlated input data (default False).
158    resplot : bool
159        If True, a plot which displays fit, data and residuals is generated (default False).
160    qqplot : bool
161        If True, a quantile-quantile plot of the fit result is generated (default False).
162    num_grad : bool
163        Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
164
165    Returns
166    -------
167    output : Fit_result
168        Parameters and information on the fitted result.
169    '''
170    if priors is not None:
171        return _prior_fit(x, y, func, priors, silent=silent, **kwargs)
172    else:
173        return _combined_fit(x, y, func, silent=silent, **kwargs)

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 (list, optional): priors has to be 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). At the moment this option only works for prior==None and when no method is given.
  • 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.
def total_least_squares(x, y, func, silent=False, **kwargs):
176def total_least_squares(x, y, func, silent=False, **kwargs):
177    r'''Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.
178
179    Parameters
180    ----------
181    x : list
182        list of Obs, or a tuple of lists of Obs
183    y : list
184        list of Obs. The dvalues of the Obs are used as x- and yerror for the fit.
185    func : object
186        func has to be of the form
187
188        ```python
189        import autograd.numpy as anp
190
191        def func(a, x):
192            return a[0] + a[1] * x + a[2] * anp.sinh(x)
193        ```
194
195        For multiple x values func can be of the form
196
197        ```python
198        def func(a, x):
199            (x1, x2) = x
200            return a[0] * x1 ** 2 + a[1] * x2
201        ```
202
203        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
204        will not work.
205    silent : bool, optional
206        If true all output to the console is omitted (default False).
207    initial_guess : list
208        can provide an initial guess for the input parameters. Relevant for non-linear
209        fits with many parameters.
210    expected_chisquare : bool
211        If true prints the expected chisquare which is
212        corrected by effects caused by correlated input data.
213        This can take a while as the full correlation matrix
214        has to be calculated (default False).
215    num_grad : bool
216        Use numerical differentation instead of automatic differentiation to perform the error propagation (default False).
217
218    Notes
219    -----
220    Based on the orthogonal distance regression module of scipy.
221
222    Returns
223    -------
224    output : Fit_result
225        Parameters and information on the fitted result.
226    '''
227
228    output = Fit_result()
229
230    output.fit_function = func
231
232    x = np.array(x)
233
234    x_shape = x.shape
235
236    if kwargs.get('num_grad') is True:
237        jacobian = num_jacobian
238        hessian = num_hessian
239    else:
240        jacobian = auto_jacobian
241        hessian = auto_hessian
242
243    if not callable(func):
244        raise TypeError('func has to be a function.')
245
246    for i in range(42):
247        try:
248            func(np.arange(i), x.T[0])
249        except TypeError:
250            continue
251        except IndexError:
252            continue
253        else:
254            break
255    else:
256        raise RuntimeError("Fit function is not valid.")
257
258    n_parms = i
259    if not silent:
260        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
261
262    x_f = np.vectorize(lambda o: o.value)(x)
263    dx_f = np.vectorize(lambda o: o.dvalue)(x)
264    y_f = np.array([o.value for o in y])
265    dy_f = np.array([o.dvalue for o in y])
266
267    if np.any(np.asarray(dx_f) <= 0.0):
268        raise Exception('No x errors available, run the gamma method first.')
269
270    if np.any(np.asarray(dy_f) <= 0.0):
271        raise Exception('No y errors available, run the gamma method first.')
272
273    if 'initial_guess' in kwargs:
274        x0 = kwargs.get('initial_guess')
275        if len(x0) != n_parms:
276            raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
277    else:
278        x0 = [1] * n_parms
279
280    data = RealData(x_f, y_f, sx=dx_f, sy=dy_f)
281    model = Model(func)
282    odr = ODR(data, model, x0, partol=np.finfo(np.float64).eps)
283    odr.set_job(fit_type=0, deriv=1)
284    out = odr.run()
285
286    output.residual_variance = out.res_var
287
288    output.method = 'ODR'
289
290    output.message = out.stopreason
291
292    output.xplus = out.xplus
293
294    if not silent:
295        print('Method: ODR')
296        print(*out.stopreason)
297        print('Residual variance:', output.residual_variance)
298
299    if out.info > 3:
300        raise Exception('The minimization procedure did not converge.')
301
302    m = x_f.size
303
304    def odr_chisquare(p):
305        model = func(p[:n_parms], p[n_parms:].reshape(x_shape))
306        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((x_f - p[n_parms:].reshape(x_shape)) / dx_f) ** 2)
307        return chisq
308
309    if kwargs.get('expected_chisquare') is True:
310        W = np.diag(1 / np.asarray(np.concatenate((dy_f.ravel(), dx_f.ravel()))))
311
312        if kwargs.get('covariance') is not None:
313            cov = kwargs.get('covariance')
314        else:
315            cov = covariance(np.concatenate((y, x.ravel())))
316
317        number_of_x_parameters = int(m / x_f.shape[-1])
318
319        old_jac = jacobian(func)(out.beta, out.xplus)
320        fused_row1 = np.concatenate((old_jac, np.concatenate((number_of_x_parameters * [np.zeros(old_jac.shape)]), axis=0)))
321        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])))
322        new_jac = np.concatenate((fused_row1, fused_row2), axis=1)
323
324        A = W @ new_jac
325        P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
326        expected_chisquare = np.trace((np.identity(P_phi.shape[0]) - P_phi) @ W @ cov @ W)
327        if expected_chisquare <= 0.0:
328            warnings.warn("Negative expected_chisquare.", RuntimeWarning)
329            expected_chisquare = np.abs(expected_chisquare)
330        output.chisquare_by_expected_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel()))) / expected_chisquare
331        if not silent:
332            print('chisquare/expected_chisquare:',
333                  output.chisquare_by_expected_chisquare)
334
335    fitp = out.beta
336    try:
337        hess = hessian(odr_chisquare)(np.concatenate((fitp, out.xplus.ravel())))
338    except TypeError:
339        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
340
341    def odr_chisquare_compact_x(d):
342        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
343        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)
344        return chisq
345
346    jac_jac_x = hessian(odr_chisquare_compact_x)(np.concatenate((fitp, out.xplus.ravel(), x_f.ravel())))
347
348    # Compute hess^{-1} @ jac_jac_x[:n_parms + m, n_parms + m:] using LAPACK dgesv
349    try:
350        deriv_x = -scipy.linalg.solve(hess, jac_jac_x[:n_parms + m, n_parms + m:])
351    except np.linalg.LinAlgError:
352        raise Exception("Cannot invert hessian matrix.")
353
354    def odr_chisquare_compact_y(d):
355        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
356        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)
357        return chisq
358
359    jac_jac_y = hessian(odr_chisquare_compact_y)(np.concatenate((fitp, out.xplus.ravel(), y_f)))
360
361    # Compute hess^{-1} @ jac_jac_y[:n_parms + m, n_parms + m:] using LAPACK dgesv
362    try:
363        deriv_y = -scipy.linalg.solve(hess, jac_jac_y[:n_parms + m, n_parms + m:])
364    except np.linalg.LinAlgError:
365        raise Exception("Cannot invert hessian matrix.")
366
367    result = []
368    for i in range(n_parms):
369        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])))
370
371    output.fit_parameters = result
372
373    output.odr_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel())))
374    output.dof = x.shape[-1] - n_parms
375    output.p_value = 1 - scipy.stats.chi2.cdf(output.odr_chisquare, output.dof)
376
377    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):
737def fit_lin(x, y, **kwargs):
738    """Performs a linear fit to y = n + m * x and returns two Obs n, m.
739
740    Parameters
741    ----------
742    x : list
743        Can either be a list of floats in which case no xerror is assumed, or
744        a list of Obs, where the dvalues of the Obs are used as xerror for the fit.
745    y : list
746        List of Obs, the dvalues of the Obs are used as yerror for the fit.
747
748    Returns
749    -------
750    fit_parameters : list[Obs]
751        LIist of fitted observables.
752    """
753
754    def f(a, x):
755        y = a[0] + a[1] * x
756        return y
757
758    if all(isinstance(n, Obs) for n in x):
759        out = total_least_squares(x, y, f, **kwargs)
760        return out.fit_parameters
761    elif all(isinstance(n, float) or isinstance(n, int) for n in x) or isinstance(x, np.ndarray):
762        out = least_squares(x, y, f, **kwargs)
763        return out.fit_parameters
764    else:
765        raise Exception('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=''):
768def qqplot(x, o_y, func, p, title=""):
769    """Generates a quantile-quantile plot of the fit result which can be used to
770       check if the residuals of the fit are gaussian distributed.
771
772    Returns
773    -------
774    None
775    """
776
777    residuals = []
778    for i_x, i_y in zip(x, o_y):
779        residuals.append((i_y - func(p, i_x)) / i_y.dvalue)
780    residuals = sorted(residuals)
781    my_y = [o.value for o in residuals]
782    probplot = scipy.stats.probplot(my_y)
783    my_x = probplot[0][0]
784    plt.figure(figsize=(8, 8 / 1.618))
785    plt.errorbar(my_x, my_y, fmt='o')
786    fit_start = my_x[0]
787    fit_stop = my_x[-1]
788    samples = np.arange(fit_start, fit_stop, 0.01)
789    plt.plot(samples, samples, 'k--', zorder=11, label='Standard normal distribution')
790    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='-')
791
792    plt.xlabel('Theoretical quantiles')
793    plt.ylabel('Ordered Values')
794    plt.legend(title=title)
795    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=''):
798def residual_plot(x, y, func, fit_res, title=""):
799    """Generates a plot which compares the fit to the data and displays the corresponding residuals
800
801    For uncorrelated data the residuals are expected to be distributed ~N(0,1).
802
803    Returns
804    -------
805    None
806    """
807    sorted_x = sorted(x)
808    xstart = sorted_x[0] - 0.5 * (sorted_x[1] - sorted_x[0])
809    xstop = sorted_x[-1] + 0.5 * (sorted_x[-1] - sorted_x[-2])
810    x_samples = np.arange(xstart, xstop + 0.01, 0.01)
811
812    plt.figure(figsize=(8, 8 / 1.618))
813    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], wspace=0.0, hspace=0.0)
814    ax0 = plt.subplot(gs[0])
815    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')
816    ax0.plot(x_samples, func([o.value for o in fit_res], x_samples), label='Fit', zorder=10, ls='-', ms=0)
817    ax0.set_xticklabels([])
818    ax0.set_xlim([xstart, xstop])
819    ax0.set_xticklabels([])
820    ax0.legend(title=title)
821
822    residuals = (np.asarray([o.value for o in y]) - func([o.value for o in fit_res], x)) / np.asarray([o.dvalue for o in y])
823    ax1 = plt.subplot(gs[1])
824    ax1.plot(x, residuals, 'ko', ls='none', markersize=5)
825    ax1.tick_params(direction='out')
826    ax1.tick_params(axis="x", bottom=True, top=True, labelbottom=True)
827    ax1.axhline(y=0.0, ls='--', color='k', marker=" ")
828    ax1.fill_between(x_samples, -1.0, 1.0, alpha=0.1, facecolor='k')
829    ax1.set_xlim([xstart, xstop])
830    ax1.set_ylabel('Residuals')
831    plt.subplots_adjust(wspace=None, hspace=None)
832    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):
835def error_band(x, func, beta):
836    """Calculate the error band for an array of sample values x, for given fit function func with optimized parameters beta.
837
838    Returns
839    -------
840    err : np.array(Obs)
841        Error band for an array of sample values x
842    """
843    cov = covariance(beta)
844    if np.any(np.abs(cov - cov.T) > 1000 * np.finfo(np.float64).eps):
845        warnings.warn("Covariance matrix is not symmetric within floating point precision", RuntimeWarning)
846
847    deriv = []
848    for i, item in enumerate(x):
849        deriv.append(np.array(egrad(func)([o.value for o in beta], item)))
850
851    err = []
852    for i, item in enumerate(x):
853        err.append(np.sqrt(deriv[i] @ cov @ deriv[i]))
854    err = np.array(err)
855
856    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):
859def ks_test(objects=None):
860    """Performs a Kolmogorov–Smirnov test for the p-values of all fit object.
861
862    Parameters
863    ----------
864    objects : list
865        List of fit results to include in the analysis (optional).
866
867    Returns
868    -------
869    None
870    """
871
872    if objects is None:
873        obs_list = []
874        for obj in gc.get_objects():
875            if isinstance(obj, Fit_result):
876                obs_list.append(obj)
877    else:
878        obs_list = objects
879
880    p_values = [o.p_value for o in obs_list]
881
882    bins = len(p_values)
883    x = np.arange(0, 1.001, 0.001)
884    plt.plot(x, x, 'k', zorder=1)
885    plt.xlim(0, 1)
886    plt.ylim(0, 1)
887    plt.xlabel('p-value')
888    plt.ylabel('Cumulative probability')
889    plt.title(str(bins) + ' p-values')
890
891    n = np.arange(1, bins + 1) / np.float64(bins)
892    Xs = np.sort(p_values)
893    plt.step(Xs, n)
894    diffs = n - Xs
895    loc_max_diff = np.argmax(np.abs(diffs))
896    loc = Xs[loc_max_diff]
897    plt.annotate('', xy=(loc, loc), xytext=(loc, loc + diffs[loc_max_diff]), arrowprops=dict(arrowstyle='<->', shrinkA=0, shrinkB=0))
898    plt.draw()
899
900    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