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
 11from scipy.stats import chi2
 12import iminuit
 13from autograd import jacobian
 14from autograd import elementwise_grad as egrad
 15from .obs import Obs, derived_observable, covariance, cov_Obs
 16
 17
 18class Fit_result(Sequence):
 19    """Represents fit results.
 20
 21    Attributes
 22    ----------
 23    fit_parameters : list
 24        results for the individual fit parameters,
 25        also accessible via indices.
 26    """
 27
 28    def __init__(self):
 29        self.fit_parameters = None
 30
 31    def __getitem__(self, idx):
 32        return self.fit_parameters[idx]
 33
 34    def __len__(self):
 35        return len(self.fit_parameters)
 36
 37    def gamma_method(self):
 38        """Apply the gamma method to all fit parameters"""
 39        [o.gamma_method() for o in self.fit_parameters]
 40
 41    def __str__(self):
 42        my_str = 'Goodness of fit:\n'
 43        if hasattr(self, 'chisquare_by_dof'):
 44            my_str += '\u03C7\u00b2/d.o.f. = ' + f'{self.chisquare_by_dof:2.6f}' + '\n'
 45        elif hasattr(self, 'residual_variance'):
 46            my_str += 'residual variance = ' + f'{self.residual_variance:2.6f}' + '\n'
 47        if hasattr(self, 'chisquare_by_expected_chisquare'):
 48            my_str += '\u03C7\u00b2/\u03C7\u00b2exp  = ' + f'{self.chisquare_by_expected_chisquare:2.6f}' + '\n'
 49        if hasattr(self, 'p_value'):
 50            my_str += 'p-value   = ' + f'{self.p_value:2.4f}' + '\n'
 51        my_str += 'Fit parameters:\n'
 52        for i_par, par in enumerate(self.fit_parameters):
 53            my_str += str(i_par) + '\t' + ' ' * int(par >= 0) + str(par).rjust(int(par < 0.0)) + '\n'
 54        return my_str
 55
 56    def __repr__(self):
 57        m = max(map(len, list(self.__dict__.keys()))) + 1
 58        return '\n'.join([key.rjust(m) + ': ' + repr(value) for key, value in sorted(self.__dict__.items())])
 59
 60
 61def least_squares(x, y, func, priors=None, silent=False, **kwargs):
 62    r'''Performs a non-linear fit to y = func(x).
 63
 64    Parameters
 65    ----------
 66    x : list
 67        list of floats.
 68    y : list
 69        list of Obs.
 70    func : object
 71        fit function, has to be of the form
 72
 73        ```python
 74        import autograd.numpy as anp
 75
 76        def func(a, x):
 77            return a[0] + a[1] * x + a[2] * anp.sinh(x)
 78        ```
 79
 80        For multiple x values func can be of the form
 81
 82        ```python
 83        def func(a, x):
 84            (x1, x2) = x
 85            return a[0] * x1 ** 2 + a[1] * x2
 86        ```
 87
 88        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
 89        will not work.
 90    priors : list, optional
 91        priors has to be a list with an entry for every parameter in the fit. The entries can either be
 92        Obs (e.g. results from a previous fit) or strings containing a value and an error formatted like
 93        0.548(23), 500(40) or 0.5(0.4)
 94    silent : bool, optional
 95        If true all output to the console is omitted (default False).
 96    initial_guess : list
 97        can provide an initial guess for the input parameters. Relevant for
 98        non-linear fits with many parameters.
 99    method : str, optional
100        can be used to choose an alternative method for the minimization of chisquare.
101        The possible methods are the ones which can be used for scipy.optimize.minimize and
102        migrad of iminuit. If no method is specified, Levenberg-Marquard is used.
103        Reliable alternatives are migrad, Powell and Nelder-Mead.
104    correlated_fit : bool
105        If True, use the full inverse covariance matrix in the definition of the chisquare cost function.
106        For details about how the covariance matrix is estimated see `pyerrors.obs.covariance`.
107        In practice the correlation matrix is Cholesky decomposed and inverted (instead of the covariance matrix).
108        This procedure should be numerically more stable as the correlation matrix is typically better conditioned (Jacobi preconditioning).
109        At the moment this option only works for `prior==None` and when no `method` is given.
110    expected_chisquare : bool
111        If True estimates the expected chisquare which is
112        corrected by effects caused by correlated input data (default False).
113    resplot : bool
114        If True, a plot which displays fit, data and residuals is generated (default False).
115    qqplot : bool
116        If True, a quantile-quantile plot of the fit result is generated (default False).
117    '''
118    if priors is not None:
119        return _prior_fit(x, y, func, priors, silent=silent, **kwargs)
120    else:
121        return _standard_fit(x, y, func, silent=silent, **kwargs)
122
123
124def total_least_squares(x, y, func, silent=False, **kwargs):
125    r'''Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.
126
127    Parameters
128    ----------
129    x : list
130        list of Obs, or a tuple of lists of Obs
131    y : list
132        list of Obs. The dvalues of the Obs are used as x- and yerror for the fit.
133    func : object
134        func has to be of the form
135
136        ```python
137        import autograd.numpy as anp
138
139        def func(a, x):
140            return a[0] + a[1] * x + a[2] * anp.sinh(x)
141        ```
142
143        For multiple x values func can be of the form
144
145        ```python
146        def func(a, x):
147            (x1, x2) = x
148            return a[0] * x1 ** 2 + a[1] * x2
149        ```
150
151        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
152        will not work.
153    silent : bool, optional
154        If true all output to the console is omitted (default False).
155    initial_guess : list
156        can provide an initial guess for the input parameters. Relevant for non-linear
157        fits with many parameters.
158    expected_chisquare : bool
159        If true prints the expected chisquare which is
160        corrected by effects caused by correlated input data.
161        This can take a while as the full correlation matrix
162        has to be calculated (default False).
163
164    Notes
165    -----
166    Based on the orthogonal distance regression module of scipy
167    '''
168
169    output = Fit_result()
170
171    output.fit_function = func
172
173    x = np.array(x)
174
175    x_shape = x.shape
176
177    if not callable(func):
178        raise TypeError('func has to be a function.')
179
180    for i in range(25):
181        try:
182            func(np.arange(i), x.T[0])
183        except Exception:
184            pass
185        else:
186            break
187
188    n_parms = i
189    if not silent:
190        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
191
192    x_f = np.vectorize(lambda o: o.value)(x)
193    dx_f = np.vectorize(lambda o: o.dvalue)(x)
194    y_f = np.array([o.value for o in y])
195    dy_f = np.array([o.dvalue for o in y])
196
197    if np.any(np.asarray(dx_f) <= 0.0):
198        raise Exception('No x errors available, run the gamma method first.')
199
200    if np.any(np.asarray(dy_f) <= 0.0):
201        raise Exception('No y errors available, run the gamma method first.')
202
203    if 'initial_guess' in kwargs:
204        x0 = kwargs.get('initial_guess')
205        if len(x0) != n_parms:
206            raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
207    else:
208        x0 = [1] * n_parms
209
210    data = RealData(x_f, y_f, sx=dx_f, sy=dy_f)
211    model = Model(func)
212    odr = ODR(data, model, x0, partol=np.finfo(np.float64).eps)
213    odr.set_job(fit_type=0, deriv=1)
214    out = odr.run()
215
216    output.residual_variance = out.res_var
217
218    output.method = 'ODR'
219
220    output.message = out.stopreason
221
222    output.xplus = out.xplus
223
224    if not silent:
225        print('Method: ODR')
226        print(*out.stopreason)
227        print('Residual variance:', output.residual_variance)
228
229    if out.info > 3:
230        raise Exception('The minimization procedure did not converge.')
231
232    m = x_f.size
233
234    def odr_chisquare(p):
235        model = func(p[:n_parms], p[n_parms:].reshape(x_shape))
236        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((x_f - p[n_parms:].reshape(x_shape)) / dx_f) ** 2)
237        return chisq
238
239    if kwargs.get('expected_chisquare') is True:
240        W = np.diag(1 / np.asarray(np.concatenate((dy_f.ravel(), dx_f.ravel()))))
241
242        if kwargs.get('covariance') is not None:
243            cov = kwargs.get('covariance')
244        else:
245            cov = covariance(np.concatenate((y, x.ravel())))
246
247        number_of_x_parameters = int(m / x_f.shape[-1])
248
249        old_jac = jacobian(func)(out.beta, out.xplus)
250        fused_row1 = np.concatenate((old_jac, np.concatenate((number_of_x_parameters * [np.zeros(old_jac.shape)]), axis=0)))
251        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])))
252        new_jac = np.concatenate((fused_row1, fused_row2), axis=1)
253
254        A = W @ new_jac
255        P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
256        expected_chisquare = np.trace((np.identity(P_phi.shape[0]) - P_phi) @ W @ cov @ W)
257        if expected_chisquare <= 0.0:
258            warnings.warn("Negative expected_chisquare.", RuntimeWarning)
259            expected_chisquare = np.abs(expected_chisquare)
260        output.chisquare_by_expected_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel()))) / expected_chisquare
261        if not silent:
262            print('chisquare/expected_chisquare:',
263                  output.chisquare_by_expected_chisquare)
264
265    fitp = out.beta
266    try:
267        hess = jacobian(jacobian(odr_chisquare))(np.concatenate((fitp, out.xplus.ravel())))
268    except TypeError:
269        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
270    condn = np.linalg.cond(hess)
271    if condn > 1e8:
272        warnings.warn("Hessian matrix might be ill-conditioned ({0:1.2e}), error propagation might be unreliable.\n \
273                       Maybe try rescaling the problem such that all parameters are of O(1).".format(condn), RuntimeWarning)
274    try:
275        hess_inv = np.linalg.inv(hess)
276    except np.linalg.LinAlgError:
277        raise Exception("Cannot invert hessian matrix.")
278    except Exception:
279        raise Exception("Unkown error in connection with Hessian inverse.")
280
281    def odr_chisquare_compact_x(d):
282        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
283        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)
284        return chisq
285
286    jac_jac_x = jacobian(jacobian(odr_chisquare_compact_x))(np.concatenate((fitp, out.xplus.ravel(), x_f.ravel())))
287
288    deriv_x = -hess_inv @ jac_jac_x[:n_parms + m, n_parms + m:]
289
290    def odr_chisquare_compact_y(d):
291        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
292        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)
293        return chisq
294
295    jac_jac_y = jacobian(jacobian(odr_chisquare_compact_y))(np.concatenate((fitp, out.xplus.ravel(), y_f)))
296
297    deriv_y = -hess_inv @ jac_jac_y[:n_parms + m, n_parms + m:]
298
299    result = []
300    for i in range(n_parms):
301        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])))
302
303    output.fit_parameters = result
304
305    output.odr_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel())))
306    output.dof = x.shape[-1] - n_parms
307    output.p_value = 1 - chi2.cdf(output.odr_chisquare, output.dof)
308
309    return output
310
311
312def _prior_fit(x, y, func, priors, silent=False, **kwargs):
313    output = Fit_result()
314
315    output.fit_function = func
316
317    x = np.asarray(x)
318
319    if not callable(func):
320        raise TypeError('func has to be a function.')
321
322    for i in range(100):
323        try:
324            func(np.arange(i), 0)
325        except Exception:
326            pass
327        else:
328            break
329
330    n_parms = i
331
332    if n_parms != len(priors):
333        raise Exception('Priors does not have the correct length.')
334
335    def extract_val_and_dval(string):
336        split_string = string.split('(')
337        if '.' in split_string[0] and '.' not in split_string[1][:-1]:
338            factor = 10 ** -len(split_string[0].partition('.')[2])
339        else:
340            factor = 1
341        return float(split_string[0]), float(split_string[1][:-1]) * factor
342
343    loc_priors = []
344    for i_n, i_prior in enumerate(priors):
345        if isinstance(i_prior, Obs):
346            loc_priors.append(i_prior)
347        else:
348            loc_val, loc_dval = extract_val_and_dval(i_prior)
349            loc_priors.append(cov_Obs(loc_val, loc_dval ** 2, '#prior' + str(i_n) + f"_{np.random.randint(2147483647):010d}"))
350
351    output.priors = loc_priors
352
353    if not silent:
354        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
355
356    y_f = [o.value for o in y]
357    dy_f = [o.dvalue for o in y]
358
359    if np.any(np.asarray(dy_f) <= 0.0):
360        raise Exception('No y errors available, run the gamma method first.')
361
362    p_f = [o.value for o in loc_priors]
363    dp_f = [o.dvalue for o in loc_priors]
364
365    if np.any(np.asarray(dp_f) <= 0.0):
366        raise Exception('No prior errors available, run the gamma method first.')
367
368    if 'initial_guess' in kwargs:
369        x0 = kwargs.get('initial_guess')
370        if len(x0) != n_parms:
371            raise Exception('Initial guess does not have the correct length.')
372    else:
373        x0 = p_f
374
375    def chisqfunc(p):
376        model = func(p, x)
377        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((p_f - p) / dp_f) ** 2)
378        return chisq
379
380    if not silent:
381        print('Method: migrad')
382
383    m = iminuit.Minuit(chisqfunc, x0)
384    m.errordef = 1
385    m.print_level = 0
386    if 'tol' in kwargs:
387        m.tol = kwargs.get('tol')
388    else:
389        m.tol = 1e-4
390    m.migrad()
391    params = np.asarray(m.values)
392
393    output.chisquare_by_dof = m.fval / len(x)
394
395    output.method = 'migrad'
396
397    if not silent:
398        print('chisquare/d.o.f.:', output.chisquare_by_dof)
399
400    if not m.fmin.is_valid:
401        raise Exception('The minimization procedure did not converge.')
402
403    hess_inv = np.linalg.pinv(jacobian(jacobian(chisqfunc))(params))
404
405    def chisqfunc_compact(d):
406        model = func(d[:n_parms], x)
407        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)
408        return chisq
409
410    jac_jac = jacobian(jacobian(chisqfunc_compact))(np.concatenate((params, y_f, p_f)))
411
412    deriv = -hess_inv @ jac_jac[:n_parms, n_parms:]
413
414    result = []
415    for i in range(n_parms):
416        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])))
417
418    output.fit_parameters = result
419    output.chisquare = chisqfunc(np.asarray(params))
420
421    if kwargs.get('resplot') is True:
422        residual_plot(x, y, func, result)
423
424    if kwargs.get('qqplot') is True:
425        qqplot(x, y, func, result)
426
427    return output
428
429
430def _standard_fit(x, y, func, silent=False, **kwargs):
431
432    output = Fit_result()
433
434    output.fit_function = func
435
436    x = np.asarray(x)
437
438    if x.shape[-1] != len(y):
439        raise Exception('x and y input have to have the same length')
440
441    if len(x.shape) > 2:
442        raise Exception('Unknown format for x values')
443
444    if not callable(func):
445        raise TypeError('func has to be a function.')
446
447    for i in range(25):
448        try:
449            func(np.arange(i), x.T[0])
450        except Exception:
451            pass
452        else:
453            break
454
455    n_parms = i
456
457    if not silent:
458        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
459
460    y_f = [o.value for o in y]
461    dy_f = [o.dvalue for o in y]
462
463    if np.any(np.asarray(dy_f) <= 0.0):
464        raise Exception('No y errors available, run the gamma method first.')
465
466    if 'initial_guess' in kwargs:
467        x0 = kwargs.get('initial_guess')
468        if len(x0) != n_parms:
469            raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
470    else:
471        x0 = [0.1] * n_parms
472
473    if kwargs.get('correlated_fit') is True:
474        corr = covariance(y, correlation=True, **kwargs)
475        covdiag = np.diag(1 / np.asarray(dy_f))
476        condn = np.linalg.cond(corr)
477        if condn > 0.1 / np.finfo(float).eps:
478            raise Exception(f"Cannot invert correlation matrix as its condition number exceeds machine precision ({condn:1.2e})")
479        if condn > 1 / np.sqrt(np.finfo(float).eps):
480            warnings.warn("Correlation matrix may be ill-conditioned, condition number: {%1.2e}" % (condn), RuntimeWarning)
481        chol = np.linalg.cholesky(corr)
482        chol_inv = np.linalg.inv(chol)
483        chol_inv = np.dot(chol_inv, covdiag)
484
485        def chisqfunc_corr(p):
486            model = func(p, x)
487            chisq = anp.sum(anp.dot(chol_inv, (y_f - model)) ** 2)
488            return chisq
489
490    def chisqfunc(p):
491        model = func(p, x)
492        chisq = anp.sum(((y_f - model) / dy_f) ** 2)
493        return chisq
494
495    output.method = kwargs.get('method', 'Levenberg-Marquardt')
496    if not silent:
497        print('Method:', output.method)
498
499    if output.method != 'Levenberg-Marquardt':
500        if output.method == 'migrad':
501            fit_result = iminuit.minimize(chisqfunc, x0, tol=1e-4)  # Stopping criterion 0.002 * tol * errordef
502            if kwargs.get('correlated_fit') is True:
503                fit_result = iminuit.minimize(chisqfunc_corr, fit_result.x, tol=1e-4)  # Stopping criterion 0.002 * tol * errordef
504            output.iterations = fit_result.nfev
505        else:
506            fit_result = scipy.optimize.minimize(chisqfunc, x0, method=kwargs.get('method'), tol=1e-12)
507            if kwargs.get('correlated_fit') is True:
508                fit_result = scipy.optimize.minimize(chisqfunc_corr, fit_result.x, method=kwargs.get('method'), tol=1e-12)
509            output.iterations = fit_result.nit
510
511        chisquare = fit_result.fun
512
513    else:
514        if kwargs.get('correlated_fit') is True:
515            def chisqfunc_residuals_corr(p):
516                model = func(p, x)
517                chisq = anp.dot(chol_inv, (y_f - model))
518                return chisq
519
520        def chisqfunc_residuals(p):
521            model = func(p, x)
522            chisq = ((y_f - model) / dy_f)
523            return chisq
524
525        fit_result = scipy.optimize.least_squares(chisqfunc_residuals, x0, method='lm', ftol=1e-15, gtol=1e-15, xtol=1e-15)
526        if kwargs.get('correlated_fit') is True:
527            fit_result = scipy.optimize.least_squares(chisqfunc_residuals_corr, fit_result.x, method='lm', ftol=1e-15, gtol=1e-15, xtol=1e-15)
528
529        chisquare = np.sum(fit_result.fun ** 2)
530
531        output.iterations = fit_result.nfev
532
533    if not fit_result.success:
534        raise Exception('The minimization procedure did not converge.')
535
536    if x.shape[-1] - n_parms > 0:
537        output.chisquare_by_dof = chisquare / (x.shape[-1] - n_parms)
538    else:
539        output.chisquare_by_dof = float('nan')
540
541    output.message = fit_result.message
542    if not silent:
543        print(fit_result.message)
544        print('chisquare/d.o.f.:', output.chisquare_by_dof)
545
546    if kwargs.get('expected_chisquare') is True:
547        if kwargs.get('correlated_fit') is not True:
548            W = np.diag(1 / np.asarray(dy_f))
549            cov = covariance(y)
550            A = W @ jacobian(func)(fit_result.x, x)
551            P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
552            expected_chisquare = np.trace((np.identity(x.shape[-1]) - P_phi) @ W @ cov @ W)
553            output.chisquare_by_expected_chisquare = chisquare / expected_chisquare
554            if not silent:
555                print('chisquare/expected_chisquare:',
556                      output.chisquare_by_expected_chisquare)
557
558    fitp = fit_result.x
559    try:
560        hess = jacobian(jacobian(chisqfunc))(fitp)
561    except TypeError:
562        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
563    condn = np.linalg.cond(hess)
564    if condn > 1e8:
565        warnings.warn("Hessian matrix might be ill-conditioned ({0:1.2e}), error propagation might be unreliable.\n \
566                       Maybe try rescaling the problem such that all parameters are of O(1).".format(condn), RuntimeWarning)
567    try:
568        hess_inv = np.linalg.inv(hess)
569    except np.linalg.LinAlgError:
570        raise Exception("Cannot invert hessian matrix.")
571    except Exception:
572        raise Exception("Unkown error in connection with Hessian inverse.")
573
574    if kwargs.get('correlated_fit') is True:
575        def chisqfunc_compact(d):
576            model = func(d[:n_parms], x)
577            chisq = anp.sum(anp.dot(chol_inv, (d[n_parms:] - model)) ** 2)
578            return chisq
579
580    else:
581        def chisqfunc_compact(d):
582            model = func(d[:n_parms], x)
583            chisq = anp.sum(((d[n_parms:] - model) / dy_f) ** 2)
584            return chisq
585
586    jac_jac = jacobian(jacobian(chisqfunc_compact))(np.concatenate((fitp, y_f)))
587
588    deriv = -hess_inv @ jac_jac[:n_parms, n_parms:]
589
590    result = []
591    for i in range(n_parms):
592        result.append(derived_observable(lambda x, **kwargs: (x[0] + np.finfo(np.float64).eps) / (y[0].value + np.finfo(np.float64).eps) * fit_result.x[i], list(y), man_grad=list(deriv[i])))
593
594    output.fit_parameters = result
595
596    output.chisquare = chisqfunc(fit_result.x)
597    output.dof = x.shape[-1] - n_parms
598    output.p_value = 1 - chi2.cdf(output.chisquare, output.dof)
599
600    if kwargs.get('resplot') is True:
601        residual_plot(x, y, func, result)
602
603    if kwargs.get('qqplot') is True:
604        qqplot(x, y, func, result)
605
606    return output
607
608
609def fit_lin(x, y, **kwargs):
610    """Performs a linear fit to y = n + m * x and returns two Obs n, m.
611
612    Parameters
613    ----------
614    x : list
615        Can either be a list of floats in which case no xerror is assumed, or
616        a list of Obs, where the dvalues of the Obs are used as xerror for the fit.
617    y : list
618        List of Obs, the dvalues of the Obs are used as yerror for the fit.
619    """
620
621    def f(a, x):
622        y = a[0] + a[1] * x
623        return y
624
625    if all(isinstance(n, Obs) for n in x):
626        out = total_least_squares(x, y, f, **kwargs)
627        return out.fit_parameters
628    elif all(isinstance(n, float) or isinstance(n, int) for n in x) or isinstance(x, np.ndarray):
629        out = least_squares(x, y, f, **kwargs)
630        return out.fit_parameters
631    else:
632        raise Exception('Unsupported types for x')
633
634
635def qqplot(x, o_y, func, p):
636    """Generates a quantile-quantile plot of the fit result which can be used to
637       check if the residuals of the fit are gaussian distributed.
638    """
639
640    residuals = []
641    for i_x, i_y in zip(x, o_y):
642        residuals.append((i_y - func(p, i_x)) / i_y.dvalue)
643    residuals = sorted(residuals)
644    my_y = [o.value for o in residuals]
645    probplot = scipy.stats.probplot(my_y)
646    my_x = probplot[0][0]
647    plt.figure(figsize=(8, 8 / 1.618))
648    plt.errorbar(my_x, my_y, fmt='o')
649    fit_start = my_x[0]
650    fit_stop = my_x[-1]
651    samples = np.arange(fit_start, fit_stop, 0.01)
652    plt.plot(samples, samples, 'k--', zorder=11, label='Standard normal distribution')
653    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='-')
654
655    plt.xlabel('Theoretical quantiles')
656    plt.ylabel('Ordered Values')
657    plt.legend()
658    plt.draw()
659
660
661def residual_plot(x, y, func, fit_res):
662    """ Generates a plot which compares the fit to the data and displays the corresponding residuals"""
663    sorted_x = sorted(x)
664    xstart = sorted_x[0] - 0.5 * (sorted_x[1] - sorted_x[0])
665    xstop = sorted_x[-1] + 0.5 * (sorted_x[-1] - sorted_x[-2])
666    x_samples = np.arange(xstart, xstop + 0.01, 0.01)
667
668    plt.figure(figsize=(8, 8 / 1.618))
669    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], wspace=0.0, hspace=0.0)
670    ax0 = plt.subplot(gs[0])
671    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')
672    ax0.plot(x_samples, func([o.value for o in fit_res], x_samples), label='Fit', zorder=10, ls='-', ms=0)
673    ax0.set_xticklabels([])
674    ax0.set_xlim([xstart, xstop])
675    ax0.set_xticklabels([])
676    ax0.legend()
677
678    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])
679    ax1 = plt.subplot(gs[1])
680    ax1.plot(x, residuals, 'ko', ls='none', markersize=5)
681    ax1.tick_params(direction='out')
682    ax1.tick_params(axis="x", bottom=True, top=True, labelbottom=True)
683    ax1.axhline(y=0.0, ls='--', color='k', marker=" ")
684    ax1.fill_between(x_samples, -1.0, 1.0, alpha=0.1, facecolor='k')
685    ax1.set_xlim([xstart, xstop])
686    ax1.set_ylabel('Residuals')
687    plt.subplots_adjust(wspace=None, hspace=None)
688    plt.draw()
689
690
691def error_band(x, func, beta):
692    """Returns the error band for an array of sample values x, for given fit function func with optimized parameters beta."""
693    cov = covariance(beta)
694    if np.any(np.abs(cov - cov.T) > 1000 * np.finfo(np.float64).eps):
695        warnings.warn("Covariance matrix is not symmetric within floating point precision", RuntimeWarning)
696
697    deriv = []
698    for i, item in enumerate(x):
699        deriv.append(np.array(egrad(func)([o.value for o in beta], item)))
700
701    err = []
702    for i, item in enumerate(x):
703        err.append(np.sqrt(deriv[i] @ cov @ deriv[i]))
704    err = np.array(err)
705
706    return err
707
708
709def ks_test(objects=None):
710    """Performs a Kolmogorov–Smirnov test for the p-values of all fit object.
711
712    Parameters
713    ----------
714    objects : list
715        List of fit results to include in the analysis (optional).
716    """
717
718    if objects is None:
719        obs_list = []
720        for obj in gc.get_objects():
721            if isinstance(obj, Fit_result):
722                obs_list.append(obj)
723    else:
724        obs_list = objects
725
726    p_values = [o.p_value for o in obs_list]
727
728    bins = len(p_values)
729    x = np.arange(0, 1.001, 0.001)
730    plt.plot(x, x, 'k', zorder=1)
731    plt.xlim(0, 1)
732    plt.ylim(0, 1)
733    plt.xlabel('p-value')
734    plt.ylabel('Cumulative probability')
735    plt.title(str(bins) + ' p-values')
736
737    n = np.arange(1, bins + 1) / np.float64(bins)
738    Xs = np.sort(p_values)
739    plt.step(Xs, n)
740    diffs = n - Xs
741    loc_max_diff = np.argmax(np.abs(diffs))
742    loc = Xs[loc_max_diff]
743    plt.annotate('', xy=(loc, loc), xytext=(loc, loc + diffs[loc_max_diff]), arrowprops=dict(arrowstyle='<->', shrinkA=0, shrinkB=0))
744    plt.draw()
745
746    print(scipy.stats.kstest(p_values, 'uniform'))
class Fit_result(collections.abc.Sequence):
19class Fit_result(Sequence):
20    """Represents fit results.
21
22    Attributes
23    ----------
24    fit_parameters : list
25        results for the individual fit parameters,
26        also accessible via indices.
27    """
28
29    def __init__(self):
30        self.fit_parameters = None
31
32    def __getitem__(self, idx):
33        return self.fit_parameters[idx]
34
35    def __len__(self):
36        return len(self.fit_parameters)
37
38    def gamma_method(self):
39        """Apply the gamma method to all fit parameters"""
40        [o.gamma_method() for o in self.fit_parameters]
41
42    def __str__(self):
43        my_str = 'Goodness of fit:\n'
44        if hasattr(self, 'chisquare_by_dof'):
45            my_str += '\u03C7\u00b2/d.o.f. = ' + f'{self.chisquare_by_dof:2.6f}' + '\n'
46        elif hasattr(self, 'residual_variance'):
47            my_str += 'residual variance = ' + f'{self.residual_variance:2.6f}' + '\n'
48        if hasattr(self, 'chisquare_by_expected_chisquare'):
49            my_str += '\u03C7\u00b2/\u03C7\u00b2exp  = ' + f'{self.chisquare_by_expected_chisquare:2.6f}' + '\n'
50        if hasattr(self, 'p_value'):
51            my_str += 'p-value   = ' + f'{self.p_value:2.4f}' + '\n'
52        my_str += 'Fit parameters:\n'
53        for i_par, par in enumerate(self.fit_parameters):
54            my_str += str(i_par) + '\t' + ' ' * int(par >= 0) + str(par).rjust(int(par < 0.0)) + '\n'
55        return my_str
56
57    def __repr__(self):
58        m = max(map(len, list(self.__dict__.keys()))) + 1
59        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.
Fit_result()
29    def __init__(self):
30        self.fit_parameters = None
def gamma_method(self)
38    def gamma_method(self):
39        """Apply the gamma method to all fit parameters"""
40        [o.gamma_method() 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)
 62def least_squares(x, y, func, priors=None, silent=False, **kwargs):
 63    r'''Performs a non-linear fit to y = func(x).
 64
 65    Parameters
 66    ----------
 67    x : list
 68        list of floats.
 69    y : list
 70        list of Obs.
 71    func : object
 72        fit function, has to be of the form
 73
 74        ```python
 75        import autograd.numpy as anp
 76
 77        def func(a, x):
 78            return a[0] + a[1] * x + a[2] * anp.sinh(x)
 79        ```
 80
 81        For multiple x values func can be of the form
 82
 83        ```python
 84        def func(a, x):
 85            (x1, x2) = x
 86            return a[0] * x1 ** 2 + a[1] * x2
 87        ```
 88
 89        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
 90        will not work.
 91    priors : list, optional
 92        priors has to be a list with an entry for every parameter in the fit. The entries can either be
 93        Obs (e.g. results from a previous fit) or strings containing a value and an error formatted like
 94        0.548(23), 500(40) or 0.5(0.4)
 95    silent : bool, optional
 96        If true all output to the console is omitted (default False).
 97    initial_guess : list
 98        can provide an initial guess for the input parameters. Relevant for
 99        non-linear fits with many parameters.
100    method : str, optional
101        can be used to choose an alternative method for the minimization of chisquare.
102        The possible methods are the ones which can be used for scipy.optimize.minimize and
103        migrad of iminuit. If no method is specified, Levenberg-Marquard is used.
104        Reliable alternatives are migrad, Powell and Nelder-Mead.
105    correlated_fit : bool
106        If True, use the full inverse covariance matrix in the definition of the chisquare cost function.
107        For details about how the covariance matrix is estimated see `pyerrors.obs.covariance`.
108        In practice the correlation matrix is Cholesky decomposed and inverted (instead of the covariance matrix).
109        This procedure should be numerically more stable as the correlation matrix is typically better conditioned (Jacobi preconditioning).
110        At the moment this option only works for `prior==None` and when no `method` is given.
111    expected_chisquare : bool
112        If True estimates the expected chisquare which is
113        corrected by effects caused by correlated input data (default False).
114    resplot : bool
115        If True, a plot which displays fit, data and residuals is generated (default False).
116    qqplot : bool
117        If True, a quantile-quantile plot of the fit result is generated (default False).
118    '''
119    if priors is not None:
120        return _prior_fit(x, y, func, priors, silent=silent, **kwargs)
121    else:
122        return _standard_fit(x, y, func, silent=silent, **kwargs)

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

Parameters
  • 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.

  • 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.
  • 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.
  • 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).
def total_least_squares(x, y, func, silent=False, **kwargs)
125def total_least_squares(x, y, func, silent=False, **kwargs):
126    r'''Performs a non-linear fit to y = func(x) and returns a list of Obs corresponding to the fit parameters.
127
128    Parameters
129    ----------
130    x : list
131        list of Obs, or a tuple of lists of Obs
132    y : list
133        list of Obs. The dvalues of the Obs are used as x- and yerror for the fit.
134    func : object
135        func has to be of the form
136
137        ```python
138        import autograd.numpy as anp
139
140        def func(a, x):
141            return a[0] + a[1] * x + a[2] * anp.sinh(x)
142        ```
143
144        For multiple x values func can be of the form
145
146        ```python
147        def func(a, x):
148            (x1, x2) = x
149            return a[0] * x1 ** 2 + a[1] * x2
150        ```
151
152        It is important that all numpy functions refer to autograd.numpy, otherwise the differentiation
153        will not work.
154    silent : bool, optional
155        If true all output to the console is omitted (default False).
156    initial_guess : list
157        can provide an initial guess for the input parameters. Relevant for non-linear
158        fits with many parameters.
159    expected_chisquare : bool
160        If true prints the expected chisquare which is
161        corrected by effects caused by correlated input data.
162        This can take a while as the full correlation matrix
163        has to be calculated (default False).
164
165    Notes
166    -----
167    Based on the orthogonal distance regression module of scipy
168    '''
169
170    output = Fit_result()
171
172    output.fit_function = func
173
174    x = np.array(x)
175
176    x_shape = x.shape
177
178    if not callable(func):
179        raise TypeError('func has to be a function.')
180
181    for i in range(25):
182        try:
183            func(np.arange(i), x.T[0])
184        except Exception:
185            pass
186        else:
187            break
188
189    n_parms = i
190    if not silent:
191        print('Fit with', n_parms, 'parameter' + 's' * (n_parms > 1))
192
193    x_f = np.vectorize(lambda o: o.value)(x)
194    dx_f = np.vectorize(lambda o: o.dvalue)(x)
195    y_f = np.array([o.value for o in y])
196    dy_f = np.array([o.dvalue for o in y])
197
198    if np.any(np.asarray(dx_f) <= 0.0):
199        raise Exception('No x errors available, run the gamma method first.')
200
201    if np.any(np.asarray(dy_f) <= 0.0):
202        raise Exception('No y errors available, run the gamma method first.')
203
204    if 'initial_guess' in kwargs:
205        x0 = kwargs.get('initial_guess')
206        if len(x0) != n_parms:
207            raise Exception('Initial guess does not have the correct length: %d vs. %d' % (len(x0), n_parms))
208    else:
209        x0 = [1] * n_parms
210
211    data = RealData(x_f, y_f, sx=dx_f, sy=dy_f)
212    model = Model(func)
213    odr = ODR(data, model, x0, partol=np.finfo(np.float64).eps)
214    odr.set_job(fit_type=0, deriv=1)
215    out = odr.run()
216
217    output.residual_variance = out.res_var
218
219    output.method = 'ODR'
220
221    output.message = out.stopreason
222
223    output.xplus = out.xplus
224
225    if not silent:
226        print('Method: ODR')
227        print(*out.stopreason)
228        print('Residual variance:', output.residual_variance)
229
230    if out.info > 3:
231        raise Exception('The minimization procedure did not converge.')
232
233    m = x_f.size
234
235    def odr_chisquare(p):
236        model = func(p[:n_parms], p[n_parms:].reshape(x_shape))
237        chisq = anp.sum(((y_f - model) / dy_f) ** 2) + anp.sum(((x_f - p[n_parms:].reshape(x_shape)) / dx_f) ** 2)
238        return chisq
239
240    if kwargs.get('expected_chisquare') is True:
241        W = np.diag(1 / np.asarray(np.concatenate((dy_f.ravel(), dx_f.ravel()))))
242
243        if kwargs.get('covariance') is not None:
244            cov = kwargs.get('covariance')
245        else:
246            cov = covariance(np.concatenate((y, x.ravel())))
247
248        number_of_x_parameters = int(m / x_f.shape[-1])
249
250        old_jac = jacobian(func)(out.beta, out.xplus)
251        fused_row1 = np.concatenate((old_jac, np.concatenate((number_of_x_parameters * [np.zeros(old_jac.shape)]), axis=0)))
252        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])))
253        new_jac = np.concatenate((fused_row1, fused_row2), axis=1)
254
255        A = W @ new_jac
256        P_phi = A @ np.linalg.pinv(A.T @ A) @ A.T
257        expected_chisquare = np.trace((np.identity(P_phi.shape[0]) - P_phi) @ W @ cov @ W)
258        if expected_chisquare <= 0.0:
259            warnings.warn("Negative expected_chisquare.", RuntimeWarning)
260            expected_chisquare = np.abs(expected_chisquare)
261        output.chisquare_by_expected_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel()))) / expected_chisquare
262        if not silent:
263            print('chisquare/expected_chisquare:',
264                  output.chisquare_by_expected_chisquare)
265
266    fitp = out.beta
267    try:
268        hess = jacobian(jacobian(odr_chisquare))(np.concatenate((fitp, out.xplus.ravel())))
269    except TypeError:
270        raise Exception("It is required to use autograd.numpy instead of numpy within fit functions, see the documentation for details.") from None
271    condn = np.linalg.cond(hess)
272    if condn > 1e8:
273        warnings.warn("Hessian matrix might be ill-conditioned ({0:1.2e}), error propagation might be unreliable.\n \
274                       Maybe try rescaling the problem such that all parameters are of O(1).".format(condn), RuntimeWarning)
275    try:
276        hess_inv = np.linalg.inv(hess)
277    except np.linalg.LinAlgError:
278        raise Exception("Cannot invert hessian matrix.")
279    except Exception:
280        raise Exception("Unkown error in connection with Hessian inverse.")
281
282    def odr_chisquare_compact_x(d):
283        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
284        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)
285        return chisq
286
287    jac_jac_x = jacobian(jacobian(odr_chisquare_compact_x))(np.concatenate((fitp, out.xplus.ravel(), x_f.ravel())))
288
289    deriv_x = -hess_inv @ jac_jac_x[:n_parms + m, n_parms + m:]
290
291    def odr_chisquare_compact_y(d):
292        model = func(d[:n_parms], d[n_parms:n_parms + m].reshape(x_shape))
293        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)
294        return chisq
295
296    jac_jac_y = jacobian(jacobian(odr_chisquare_compact_y))(np.concatenate((fitp, out.xplus.ravel(), y_f)))
297
298    deriv_y = -hess_inv @ jac_jac_y[:n_parms + m, n_parms + m:]
299
300    result = []
301    for i in range(n_parms):
302        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])))
303
304    output.fit_parameters = result
305
306    output.odr_chisquare = odr_chisquare(np.concatenate((out.beta, out.xplus.ravel())))
307    output.dof = x.shape[-1] - n_parms
308    output.p_value = 1 - chi2.cdf(output.odr_chisquare, output.dof)
309
310    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).
Notes

Based on the orthogonal distance regression module of scipy

def fit_lin(x, y, **kwargs)
610def fit_lin(x, y, **kwargs):
611    """Performs a linear fit to y = n + m * x and returns two Obs n, m.
612
613    Parameters
614    ----------
615    x : list
616        Can either be a list of floats in which case no xerror is assumed, or
617        a list of Obs, where the dvalues of the Obs are used as xerror for the fit.
618    y : list
619        List of Obs, the dvalues of the Obs are used as yerror for the fit.
620    """
621
622    def f(a, x):
623        y = a[0] + a[1] * x
624        return y
625
626    if all(isinstance(n, Obs) for n in x):
627        out = total_least_squares(x, y, f, **kwargs)
628        return out.fit_parameters
629    elif all(isinstance(n, float) or isinstance(n, int) for n in x) or isinstance(x, np.ndarray):
630        out = least_squares(x, y, f, **kwargs)
631        return out.fit_parameters
632    else:
633        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.
def qqplot(x, o_y, func, p)
636def qqplot(x, o_y, func, p):
637    """Generates a quantile-quantile plot of the fit result which can be used to
638       check if the residuals of the fit are gaussian distributed.
639    """
640
641    residuals = []
642    for i_x, i_y in zip(x, o_y):
643        residuals.append((i_y - func(p, i_x)) / i_y.dvalue)
644    residuals = sorted(residuals)
645    my_y = [o.value for o in residuals]
646    probplot = scipy.stats.probplot(my_y)
647    my_x = probplot[0][0]
648    plt.figure(figsize=(8, 8 / 1.618))
649    plt.errorbar(my_x, my_y, fmt='o')
650    fit_start = my_x[0]
651    fit_stop = my_x[-1]
652    samples = np.arange(fit_start, fit_stop, 0.01)
653    plt.plot(samples, samples, 'k--', zorder=11, label='Standard normal distribution')
654    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='-')
655
656    plt.xlabel('Theoretical quantiles')
657    plt.ylabel('Ordered Values')
658    plt.legend()
659    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.

def residual_plot(x, y, func, fit_res)
662def residual_plot(x, y, func, fit_res):
663    """ Generates a plot which compares the fit to the data and displays the corresponding residuals"""
664    sorted_x = sorted(x)
665    xstart = sorted_x[0] - 0.5 * (sorted_x[1] - sorted_x[0])
666    xstop = sorted_x[-1] + 0.5 * (sorted_x[-1] - sorted_x[-2])
667    x_samples = np.arange(xstart, xstop + 0.01, 0.01)
668
669    plt.figure(figsize=(8, 8 / 1.618))
670    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], wspace=0.0, hspace=0.0)
671    ax0 = plt.subplot(gs[0])
672    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')
673    ax0.plot(x_samples, func([o.value for o in fit_res], x_samples), label='Fit', zorder=10, ls='-', ms=0)
674    ax0.set_xticklabels([])
675    ax0.set_xlim([xstart, xstop])
676    ax0.set_xticklabels([])
677    ax0.legend()
678
679    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])
680    ax1 = plt.subplot(gs[1])
681    ax1.plot(x, residuals, 'ko', ls='none', markersize=5)
682    ax1.tick_params(direction='out')
683    ax1.tick_params(axis="x", bottom=True, top=True, labelbottom=True)
684    ax1.axhline(y=0.0, ls='--', color='k', marker=" ")
685    ax1.fill_between(x_samples, -1.0, 1.0, alpha=0.1, facecolor='k')
686    ax1.set_xlim([xstart, xstop])
687    ax1.set_ylabel('Residuals')
688    plt.subplots_adjust(wspace=None, hspace=None)
689    plt.draw()

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

def error_band(x, func, beta)
692def error_band(x, func, beta):
693    """Returns the error band for an array of sample values x, for given fit function func with optimized parameters beta."""
694    cov = covariance(beta)
695    if np.any(np.abs(cov - cov.T) > 1000 * np.finfo(np.float64).eps):
696        warnings.warn("Covariance matrix is not symmetric within floating point precision", RuntimeWarning)
697
698    deriv = []
699    for i, item in enumerate(x):
700        deriv.append(np.array(egrad(func)([o.value for o in beta], item)))
701
702    err = []
703    for i, item in enumerate(x):
704        err.append(np.sqrt(deriv[i] @ cov @ deriv[i]))
705    err = np.array(err)
706
707    return err

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

def ks_test(objects=None)
710def ks_test(objects=None):
711    """Performs a Kolmogorov–Smirnov test for the p-values of all fit object.
712
713    Parameters
714    ----------
715    objects : list
716        List of fit results to include in the analysis (optional).
717    """
718
719    if objects is None:
720        obs_list = []
721        for obj in gc.get_objects():
722            if isinstance(obj, Fit_result):
723                obs_list.append(obj)
724    else:
725        obs_list = objects
726
727    p_values = [o.p_value for o in obs_list]
728
729    bins = len(p_values)
730    x = np.arange(0, 1.001, 0.001)
731    plt.plot(x, x, 'k', zorder=1)
732    plt.xlim(0, 1)
733    plt.ylim(0, 1)
734    plt.xlabel('p-value')
735    plt.ylabel('Cumulative probability')
736    plt.title(str(bins) + ' p-values')
737
738    n = np.arange(1, bins + 1) / np.float64(bins)
739    Xs = np.sort(p_values)
740    plt.step(Xs, n)
741    diffs = n - Xs
742    loc_max_diff = np.argmax(np.abs(diffs))
743    loc = Xs[loc_max_diff]
744    plt.annotate('', xy=(loc, loc), xytext=(loc, loc + diffs[loc_max_diff]), arrowprops=dict(arrowstyle='<->', shrinkA=0, shrinkB=0))
745    plt.draw()
746
747    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).