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