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