diff --git a/docs/pyerrors/obs.html b/docs/pyerrors/obs.html index ce26f1c8..75d0f266 100644 --- a/docs/pyerrors/obs.html +++ b/docs/pyerrors/obs.html @@ -315,1662 +315,1672 @@
1import warnings - 2import pickle - 3from math import gcd - 4from functools import reduce - 5import numpy as np - 6import autograd.numpy as anp # Thinly-wrapped numpy - 7from autograd import jacobian - 8import matplotlib.pyplot as plt - 9from scipy.stats import skew, skewtest, kurtosis, kurtosistest - 10import numdifftools as nd - 11from itertools import groupby - 12from .covobs import Covobs - 13 + 2import hashlib + 3import pickle + 4from math import gcd + 5from functools import reduce + 6import numpy as np + 7import autograd.numpy as anp # Thinly-wrapped numpy + 8from autograd import jacobian + 9import matplotlib.pyplot as plt + 10from scipy.stats import skew, skewtest, kurtosis, kurtosistest + 11import numdifftools as nd + 12from itertools import groupby + 13from .covobs import Covobs 14 - 15class Obs: - 16 """Class for a general observable. - 17 - 18 Instances of Obs are the basic objects of a pyerrors error analysis. - 19 They are initialized with a list which contains arrays of samples for - 20 different ensembles/replica and another list of same length which contains - 21 the names of the ensembles/replica. Mathematical operations can be - 22 performed on instances. The result is another instance of Obs. The error of - 23 an instance can be computed with the gamma_method. Also contains additional - 24 methods for output and visualization of the error calculation. - 25 - 26 Attributes - 27 ---------- - 28 S_global : float - 29 Standard value for S (default 2.0) - 30 S_dict : dict - 31 Dictionary for S values. If an entry for a given ensemble - 32 exists this overwrites the standard value for that ensemble. - 33 tau_exp_global : float - 34 Standard value for tau_exp (default 0.0) - 35 tau_exp_dict : dict - 36 Dictionary for tau_exp values. If an entry for a given ensemble exists - 37 this overwrites the standard value for that ensemble. - 38 N_sigma_global : float - 39 Standard value for N_sigma (default 1.0) - 40 N_sigma_dict : dict - 41 Dictionary for N_sigma values. If an entry for a given ensemble exists - 42 this overwrites the standard value for that ensemble. - 43 """ - 44 __slots__ = ['names', 'shape', 'r_values', 'deltas', 'N', '_value', '_dvalue', - 45 'ddvalue', 'reweighted', 'S', 'tau_exp', 'N_sigma', - 46 'e_dvalue', 'e_ddvalue', 'e_tauint', 'e_dtauint', - 47 'e_windowsize', 'e_rho', 'e_drho', 'e_n_tauint', 'e_n_dtauint', - 48 'idl', 'is_merged', 'tag', '_covobs', '__dict__'] - 49 - 50 S_global = 2.0 - 51 S_dict = {} - 52 tau_exp_global = 0.0 - 53 tau_exp_dict = {} - 54 N_sigma_global = 1.0 - 55 N_sigma_dict = {} - 56 filter_eps = 1e-10 - 57 - 58 def __init__(self, samples, names, idl=None, **kwargs): - 59 """ Initialize Obs object. - 60 - 61 Parameters - 62 ---------- - 63 samples : list - 64 list of numpy arrays containing the Monte Carlo samples - 65 names : list - 66 list of strings labeling the individual samples - 67 idl : list, optional - 68 list of ranges or lists on which the samples are defined - 69 """ - 70 - 71 if kwargs.get("means") is None and len(samples): - 72 if len(samples) != len(names): - 73 raise Exception('Length of samples and names incompatible.') - 74 if idl is not None: - 75 if len(idl) != len(names): - 76 raise Exception('Length of idl incompatible with samples and names.') - 77 name_length = len(names) - 78 if name_length > 1: - 79 if name_length != len(set(names)): - 80 raise Exception('names are not unique.') - 81 if not all(isinstance(x, str) for x in names): - 82 raise TypeError('All names have to be strings.') - 83 else: - 84 if not isinstance(names[0], str): - 85 raise TypeError('All names have to be strings.') - 86 if min(len(x) for x in samples) <= 4: - 87 raise Exception('Samples have to have at least 5 entries.') - 88 - 89 self.names = sorted(names) - 90 self.shape = {} - 91 self.r_values = {} - 92 self.deltas = {} - 93 self._covobs = {} - 94 - 95 self._value = 0 - 96 self.N = 0 - 97 self.is_merged = {} - 98 self.idl = {} - 99 if idl is not None: - 100 for name, idx in sorted(zip(names, idl)): - 101 if isinstance(idx, range): - 102 self.idl[name] = idx - 103 elif isinstance(idx, (list, np.ndarray)): - 104 dc = np.unique(np.diff(idx)) - 105 if np.any(dc < 0): - 106 raise Exception("Unsorted idx for idl[%s]" % (name)) - 107 if len(dc) == 1: - 108 self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0]) - 109 else: - 110 self.idl[name] = list(idx) - 111 else: - 112 raise Exception('incompatible type for idl[%s].' % (name)) - 113 else: - 114 for name, sample in sorted(zip(names, samples)): - 115 self.idl[name] = range(1, len(sample) + 1) - 116 - 117 if kwargs.get("means") is not None: - 118 for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))): - 119 self.shape[name] = len(self.idl[name]) - 120 self.N += self.shape[name] - 121 self.r_values[name] = mean - 122 self.deltas[name] = sample - 123 else: - 124 for name, sample in sorted(zip(names, samples)): - 125 self.shape[name] = len(self.idl[name]) - 126 self.N += self.shape[name] - 127 if len(sample) != self.shape[name]: - 128 raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name])) - 129 self.r_values[name] = np.mean(sample) - 130 self.deltas[name] = sample - self.r_values[name] - 131 self._value += self.shape[name] * self.r_values[name] - 132 self._value /= self.N - 133 - 134 self._dvalue = 0.0 - 135 self.ddvalue = 0.0 - 136 self.reweighted = False - 137 - 138 self.tag = None - 139 - 140 @property - 141 def value(self): - 142 return self._value - 143 - 144 @property - 145 def dvalue(self): - 146 return self._dvalue - 147 - 148 @property - 149 def e_names(self): - 150 return sorted(set([o.split('|')[0] for o in self.names])) - 151 - 152 @property - 153 def cov_names(self): - 154 return sorted(set([o for o in self.covobs.keys()])) - 155 - 156 @property - 157 def mc_names(self): - 158 return sorted(set([o.split('|')[0] for o in self.names if o not in self.cov_names])) - 159 - 160 @property - 161 def e_content(self): - 162 res = {} - 163 for e, e_name in enumerate(self.e_names): - 164 res[e_name] = sorted(filter(lambda x: x.startswith(e_name + '|'), self.names)) - 165 if e_name in self.names: - 166 res[e_name].append(e_name) - 167 return res - 168 - 169 @property - 170 def covobs(self): - 171 return self._covobs - 172 - 173 def gamma_method(self, **kwargs): - 174 """Estimate the error and related properties of the Obs. - 175 - 176 Parameters - 177 ---------- - 178 S : float - 179 specifies a custom value for the parameter S (default 2.0). - 180 If set to 0 it is assumed that the data exhibits no - 181 autocorrelation. In this case the error estimates coincides - 182 with the sample standard error. - 183 tau_exp : float - 184 positive value triggers the critical slowing down analysis - 185 (default 0.0). - 186 N_sigma : float - 187 number of standard deviations from zero until the tail is - 188 attached to the autocorrelation function (default 1). - 189 fft : bool - 190 determines whether the fft algorithm is used for the computation - 191 of the autocorrelation function (default True) - 192 """ - 193 - 194 e_content = self.e_content - 195 self.e_dvalue = {} - 196 self.e_ddvalue = {} - 197 self.e_tauint = {} - 198 self.e_dtauint = {} - 199 self.e_windowsize = {} - 200 self.e_n_tauint = {} - 201 self.e_n_dtauint = {} - 202 e_gamma = {} - 203 self.e_rho = {} - 204 self.e_drho = {} - 205 self._dvalue = 0 - 206 self.ddvalue = 0 - 207 - 208 self.S = {} - 209 self.tau_exp = {} - 210 self.N_sigma = {} - 211 - 212 if kwargs.get('fft') is False: - 213 fft = False - 214 else: - 215 fft = True - 216 - 217 def _parse_kwarg(kwarg_name): - 218 if kwarg_name in kwargs: - 219 tmp = kwargs.get(kwarg_name) - 220 if isinstance(tmp, (int, float)): - 221 if tmp < 0: - 222 raise Exception(kwarg_name + ' has to be larger or equal to 0.') - 223 for e, e_name in enumerate(self.e_names): - 224 getattr(self, kwarg_name)[e_name] = tmp - 225 else: - 226 raise TypeError(kwarg_name + ' is not in proper format.') - 227 else: - 228 for e, e_name in enumerate(self.e_names): - 229 if e_name in getattr(Obs, kwarg_name + '_dict'): - 230 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name] - 231 else: - 232 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global') - 233 - 234 _parse_kwarg('S') - 235 _parse_kwarg('tau_exp') - 236 _parse_kwarg('N_sigma') - 237 - 238 for e, e_name in enumerate(self.mc_names): - 239 r_length = [] - 240 for r_name in e_content[e_name]: - 241 if isinstance(self.idl[r_name], range): - 242 r_length.append(len(self.idl[r_name])) - 243 else: - 244 r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1)) - 245 - 246 e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]]) - 247 w_max = max(r_length) // 2 - 248 e_gamma[e_name] = np.zeros(w_max) - 249 self.e_rho[e_name] = np.zeros(w_max) - 250 self.e_drho[e_name] = np.zeros(w_max) - 251 - 252 for r_name in e_content[e_name]: - 253 e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft) - 254 - 255 gamma_div = np.zeros(w_max) - 256 for r_name in e_content[e_name]: - 257 gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft) - 258 gamma_div[gamma_div < 1] = 1.0 - 259 e_gamma[e_name] /= gamma_div[:w_max] - 260 - 261 if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny: # Prevent division by zero - 262 self.e_tauint[e_name] = 0.5 - 263 self.e_dtauint[e_name] = 0.0 - 264 self.e_dvalue[e_name] = 0.0 - 265 self.e_ddvalue[e_name] = 0.0 - 266 self.e_windowsize[e_name] = 0 - 267 continue - 268 - 269 self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0] - 270 self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:]))) - 271 # Make sure no entry of tauint is smaller than 0.5 - 272 self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps - 273 # hep-lat/0306017 eq. (42) - 274 self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) + 0.5 - self.e_n_tauint[e_name]) / e_N) - 275 self.e_n_dtauint[e_name][0] = 0.0 - 276 - 277 def _compute_drho(i): - 278 tmp = self.e_rho[e_name][i + 1:w_max] + np.concatenate([self.e_rho[e_name][i - 1::-1], self.e_rho[e_name][1:w_max - 2 * i]]) - 2 * self.e_rho[e_name][i] * self.e_rho[e_name][1:w_max - i] - 279 self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N) - 280 - 281 _compute_drho(1) - 282 if self.tau_exp[e_name] > 0: - 283 texp = self.tau_exp[e_name] - 284 # Critical slowing down analysis - 285 if w_max // 2 <= 1: - 286 raise Exception("Need at least 8 samples for tau_exp error analysis") - 287 for n in range(1, w_max // 2): - 288 _compute_drho(n + 1) - 289 if (self.e_rho[e_name][n] - self.N_sigma[e_name] * self.e_drho[e_name][n]) < 0 or n >= w_max // 2 - 2: - 290 # Bias correction hep-lat/0306017 eq. (49) included - 291 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) + texp * np.abs(self.e_rho[e_name][n + 1]) # The absolute makes sure, that the tail contribution is always positive - 292 self.e_dtauint[e_name] = np.sqrt(self.e_n_dtauint[e_name][n] ** 2 + texp ** 2 * self.e_drho[e_name][n + 1] ** 2) - 293 # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2 - 294 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) - 295 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) - 296 self.e_windowsize[e_name] = n - 297 break - 298 else: - 299 if self.S[e_name] == 0.0: - 300 self.e_tauint[e_name] = 0.5 - 301 self.e_dtauint[e_name] = 0.0 - 302 self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1)) - 303 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N) - 304 self.e_windowsize[e_name] = 0 - 305 else: - 306 # Standard automatic windowing procedure - 307 tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][1:] + 1) / (2 * self.e_n_tauint[e_name][1:] - 1)) - 308 g_w = np.exp(- np.arange(1, w_max) / tau) - tau / np.sqrt(np.arange(1, w_max) * e_N) - 309 for n in range(1, w_max): - 310 if n < w_max // 2 - 2: - 311 _compute_drho(n + 1) - 312 if g_w[n - 1] < 0 or n >= w_max - 1: - 313 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) # Bias correction hep-lat/0306017 eq. (49) - 314 self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n] - 315 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) - 316 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) - 317 self.e_windowsize[e_name] = n - 318 break - 319 - 320 self._dvalue += self.e_dvalue[e_name] ** 2 - 321 self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2 - 322 - 323 for e_name in self.cov_names: - 324 self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq()) - 325 self.e_ddvalue[e_name] = 0 - 326 self._dvalue += self.e_dvalue[e_name]**2 - 327 - 328 self._dvalue = np.sqrt(self._dvalue) - 329 if self._dvalue == 0.0: - 330 self.ddvalue = 0.0 - 331 else: - 332 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue - 333 return - 334 - 335 def _calc_gamma(self, deltas, idx, shape, w_max, fft): - 336 """Calculate Gamma_{AA} from the deltas, which are defined on idx. - 337 idx is assumed to be a contiguous range (possibly with a stepsize != 1) - 338 - 339 Parameters - 340 ---------- - 341 deltas : list - 342 List of fluctuations - 343 idx : list - 344 List or range of configurations on which the deltas are defined. - 345 shape : int - 346 Number of configurations in idx. - 347 w_max : int - 348 Upper bound for the summation window. - 349 fft : bool - 350 determines whether the fft algorithm is used for the computation - 351 of the autocorrelation function. - 352 """ - 353 gamma = np.zeros(w_max) - 354 deltas = _expand_deltas(deltas, idx, shape) - 355 new_shape = len(deltas) - 356 if fft: - 357 max_gamma = min(new_shape, w_max) - 358 # The padding for the fft has to be even - 359 padding = new_shape + max_gamma + (new_shape + max_gamma) % 2 - 360 gamma[:max_gamma] += np.fft.irfft(np.abs(np.fft.rfft(deltas, padding)) ** 2)[:max_gamma] - 361 else: - 362 for n in range(w_max): - 363 if new_shape - n >= 0: - 364 gamma[n] += deltas[0:new_shape - n].dot(deltas[n:new_shape]) - 365 - 366 return gamma - 367 - 368 def details(self, ens_content=True): - 369 """Output detailed properties of the Obs. - 370 - 371 Parameters - 372 ---------- - 373 ens_content : bool - 374 print details about the ensembles and replica if true. - 375 """ - 376 if self.tag is not None: - 377 print("Description:", self.tag) - 378 if not hasattr(self, 'e_dvalue'): - 379 print('Result\t %3.8e' % (self.value)) - 380 else: - 381 if self.value == 0.0: - 382 percentage = np.nan - 383 else: - 384 percentage = np.abs(self._dvalue / self.value) * 100 - 385 print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage)) - 386 if len(self.e_names) > 1: - 387 print(' Ensemble errors:') - 388 for e_name in self.mc_names: - 389 if len(self.e_names) > 1: - 390 print('', e_name, '\t %3.8e +/- %3.8e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name])) - 391 if self.tau_exp[e_name] > 0: - 392 print(' t_int\t %3.8e +/- %3.8e tau_exp = %3.2f, N_sigma = %1.0i' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.tau_exp[e_name], self.N_sigma[e_name])) - 393 else: - 394 print(' t_int\t %3.8e +/- %3.8e S = %3.2f' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.S[e_name])) - 395 for e_name in self.cov_names: - 396 print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name])) - 397 if ens_content is True: - 398 if len(self.e_names) == 1: - 399 print(self.N, 'samples in', len(self.e_names), 'ensemble:') - 400 else: - 401 print(self.N, 'samples in', len(self.e_names), 'ensembles:') - 402 my_string_list = [] - 403 for key, value in sorted(self.e_content.items()): - 404 if key not in self.covobs: - 405 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " - 406 if len(value) == 1: - 407 my_string += f': {self.shape[value[0]]} configurations' - 408 if isinstance(self.idl[value[0]], range): - 409 my_string += f' (from {self.idl[value[0]].start} to {self.idl[value[0]][-1]}' + int(self.idl[value[0]].step != 1) * f' in steps of {self.idl[value[0]].step}' + ')' - 410 else: - 411 my_string += ' (irregular range)' - 412 else: - 413 sublist = [] - 414 for v in value: - 415 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " - 416 my_substring += f': {self.shape[v]} configurations' - 417 if isinstance(self.idl[v], range): - 418 my_substring += f' (from {self.idl[v].start} to {self.idl[v][-1]}' + int(self.idl[v].step != 1) * f' in steps of {self.idl[v].step}' + ')' - 419 else: - 420 my_substring += ' (irregular range)' - 421 sublist.append(my_substring) - 422 - 423 my_string += '\n' + '\n'.join(sublist) - 424 else: - 425 my_string = ' ' + "\u00B7 Covobs '" + key + "' " - 426 my_string_list.append(my_string) - 427 print('\n'.join(my_string_list)) - 428 - 429 def reweight(self, weight): - 430 """Reweight the obs with given rewighting factors. - 431 - 432 Parameters - 433 ---------- - 434 weight : Obs - 435 Reweighting factor. An Observable that has to be defined on a superset of the - 436 configurations in obs[i].idl for all i. - 437 all_configs : bool - 438 if True, the reweighted observables are normalized by the average of - 439 the reweighting factor on all configurations in weight.idl and not - 440 on the configurations in obs[i].idl. Default False. - 441 """ - 442 return reweight(weight, [self])[0] - 443 - 444 def is_zero_within_error(self, sigma=1): - 445 """Checks whether the observable is zero within 'sigma' standard errors. - 446 - 447 Parameters - 448 ---------- - 449 sigma : int - 450 Number of standard errors used for the check. - 451 - 452 Works only properly when the gamma method was run. - 453 """ - 454 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue - 455 - 456 def is_zero(self, atol=1e-10): - 457 """Checks whether the observable is zero within a given tolerance. - 458 - 459 Parameters - 460 ---------- - 461 atol : float - 462 Absolute tolerance (for details see numpy documentation). - 463 """ - 464 return np.isclose(0.0, self.value, 1e-14, atol) and all(np.allclose(0.0, delta, 1e-14, atol) for delta in self.deltas.values()) and all(np.allclose(0.0, delta.errsq(), 1e-14, atol) for delta in self.covobs.values()) - 465 - 466 def plot_tauint(self, save=None): - 467 """Plot integrated autocorrelation time for each ensemble. - 468 - 469 Parameters - 470 ---------- - 471 save : str - 472 saves the figure to a file named 'save' if. - 473 """ - 474 if not hasattr(self, 'e_dvalue'): - 475 raise Exception('Run the gamma method first.') - 476 - 477 for e, e_name in enumerate(self.mc_names): - 478 fig = plt.figure() - 479 plt.xlabel(r'$W$') - 480 plt.ylabel(r'$\tau_\mathrm{int}$') - 481 length = int(len(self.e_n_tauint[e_name])) - 482 if self.tau_exp[e_name] > 0: - 483 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] - 484 x_help = np.arange(2 * self.tau_exp[e_name]) - 485 y_help = (x_help + 1) * np.abs(self.e_rho[e_name][self.e_windowsize[e_name] + 1]) * (1 - x_help / (2 * (2 * self.tau_exp[e_name] - 1))) + base - 486 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) - 487 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') - 488 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], - 489 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) - 490 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 - 491 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) - 492 else: - 493 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) - 494 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) - 495 - 496 plt.errorbar(np.arange(length)[:int(xmax) + 1], self.e_n_tauint[e_name][:int(xmax) + 1], yerr=self.e_n_dtauint[e_name][:int(xmax) + 1], linewidth=1, capsize=2, label=label) - 497 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') - 498 plt.legend() - 499 plt.xlim(-0.5, xmax) - 500 ylim = plt.ylim() - 501 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) - 502 plt.draw() - 503 if save: - 504 fig.savefig(save + "_" + str(e)) - 505 - 506 def plot_rho(self, save=None): - 507 """Plot normalized autocorrelation function time for each ensemble. - 508 - 509 Parameters - 510 ---------- - 511 save : str - 512 saves the figure to a file named 'save' if. - 513 """ - 514 if not hasattr(self, 'e_dvalue'): - 515 raise Exception('Run the gamma method first.') - 516 for e, e_name in enumerate(self.mc_names): - 517 fig = plt.figure() - 518 plt.xlabel('W') - 519 plt.ylabel('rho') - 520 length = int(len(self.e_drho[e_name])) - 521 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) - 522 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') - 523 if self.tau_exp[e_name] > 0: - 524 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], - 525 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) - 526 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 - 527 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) - 528 else: - 529 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) - 530 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) - 531 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) - 532 plt.xlim(-0.5, xmax) - 533 plt.draw() - 534 if save: - 535 fig.savefig(save + "_" + str(e)) - 536 - 537 def plot_rep_dist(self): - 538 """Plot replica distribution for each ensemble with more than one replicum.""" - 539 if not hasattr(self, 'e_dvalue'): - 540 raise Exception('Run the gamma method first.') - 541 for e, e_name in enumerate(self.mc_names): - 542 if len(self.e_content[e_name]) == 1: - 543 print('No replica distribution for a single replicum (', e_name, ')') - 544 continue - 545 r_length = [] - 546 sub_r_mean = 0 - 547 for r, r_name in enumerate(self.e_content[e_name]): - 548 r_length.append(len(self.deltas[r_name])) - 549 sub_r_mean += self.shape[r_name] * self.r_values[r_name] - 550 e_N = np.sum(r_length) - 551 sub_r_mean /= e_N - 552 arr = np.zeros(len(self.e_content[e_name])) - 553 for r, r_name in enumerate(self.e_content[e_name]): - 554 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) - 555 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) - 556 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') - 557 plt.draw() - 558 - 559 def plot_history(self, expand=True): - 560 """Plot derived Monte Carlo history for each ensemble - 561 - 562 Parameters - 563 ---------- - 564 expand : bool - 565 show expanded history for irregular Monte Carlo chains (default: True). - 566 """ - 567 for e, e_name in enumerate(self.mc_names): - 568 plt.figure() - 569 r_length = [] - 570 tmp = [] - 571 tmp_expanded = [] - 572 for r, r_name in enumerate(self.e_content[e_name]): - 573 tmp.append(self.deltas[r_name] + self.r_values[r_name]) - 574 if expand: - 575 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name]) - 576 r_length.append(len(tmp_expanded[-1])) - 577 else: - 578 r_length.append(len(tmp[-1])) - 579 e_N = np.sum(r_length) - 580 x = np.arange(e_N) - 581 y_test = np.concatenate(tmp, axis=0) - 582 if expand: - 583 y = np.concatenate(tmp_expanded, axis=0) - 584 else: - 585 y = y_test - 586 plt.errorbar(x, y, fmt='.', markersize=3) - 587 plt.xlim(-0.5, e_N - 0.5) - 588 plt.title(e_name + f'\nskew: {skew(y_test):.3f} (p={skewtest(y_test).pvalue:.3f}), kurtosis: {kurtosis(y_test):.3f} (p={kurtosistest(y_test).pvalue:.3f})') - 589 plt.draw() - 590 - 591 def plot_piechart(self, save=None): - 592 """Plot piechart which shows the fractional contribution of each - 593 ensemble to the error and returns a dictionary containing the fractions. - 594 - 595 Parameters - 596 ---------- - 597 save : str - 598 saves the figure to a file named 'save' if. - 599 """ - 600 if not hasattr(self, 'e_dvalue'): - 601 raise Exception('Run the gamma method first.') - 602 if np.isclose(0.0, self._dvalue, atol=1e-15): - 603 raise Exception('Error is 0.0') - 604 labels = self.e_names - 605 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 - 606 fig1, ax1 = plt.subplots() - 607 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) - 608 ax1.axis('equal') - 609 plt.draw() - 610 if save: - 611 fig1.savefig(save) - 612 - 613 return dict(zip(self.e_names, sizes)) - 614 - 615 def dump(self, filename, datatype="json.gz", description="", **kwargs): - 616 """Dump the Obs to a file 'name' of chosen format. - 617 - 618 Parameters - 619 ---------- - 620 filename : str - 621 name of the file to be saved. - 622 datatype : str - 623 Format of the exported file. Supported formats include - 624 "json.gz" and "pickle" - 625 description : str - 626 Description for output file, only relevant for json.gz format. - 627 path : str - 628 specifies a custom path for the file (default '.') - 629 """ - 630 if 'path' in kwargs: - 631 file_name = kwargs.get('path') + '/' + filename - 632 else: - 633 file_name = filename - 634 - 635 if datatype == "json.gz": - 636 from .input.json import dump_to_json - 637 dump_to_json([self], file_name, description=description) - 638 elif datatype == "pickle": - 639 with open(file_name + '.p', 'wb') as fb: - 640 pickle.dump(self, fb) - 641 else: - 642 raise Exception("Unknown datatype " + str(datatype)) - 643 - 644 def export_jackknife(self): - 645 """Export jackknife samples from the Obs - 646 - 647 Returns - 648 ------- - 649 numpy.ndarray - 650 Returns a numpy array of length N + 1 where N is the number of samples - 651 for the given ensemble and replicum. The zeroth entry of the array contains - 652 the mean value of the Obs, entries 1 to N contain the N jackknife samples - 653 derived from the Obs. The current implementation only works for observables - 654 defined on exactly one ensemble and replicum. The derived jackknife samples - 655 should agree with samples from a full jackknife analysis up to O(1/N). - 656 """ - 657 - 658 if len(self.names) != 1: - 659 raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") - 660 - 661 name = self.names[0] - 662 full_data = self.deltas[name] + self.r_values[name] - 663 n = full_data.size - 664 mean = self.value - 665 tmp_jacks = np.zeros(n + 1) - 666 tmp_jacks[0] = mean - 667 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) - 668 return tmp_jacks - 669 - 670 def __float__(self): - 671 return float(self.value) - 672 - 673 def __repr__(self): - 674 return 'Obs[' + str(self) + ']' - 675 - 676 def __str__(self): - 677 if self._dvalue == 0.0: - 678 return str(self.value) - 679 fexp = np.floor(np.log10(self._dvalue)) - 680 if fexp < 0.0: - 681 return '{:{form}}({:2.0f})'.format(self.value, self._dvalue * 10 ** (-fexp + 1), form='.' + str(-int(fexp) + 1) + 'f') - 682 elif fexp == 0.0: - 683 return '{:.1f}({:1.1f})'.format(self.value, self._dvalue) - 684 else: - 685 return '{:.0f}({:2.0f})'.format(self.value, self._dvalue) - 686 - 687 # Overload comparisons - 688 def __lt__(self, other): - 689 return self.value < other - 690 - 691 def __le__(self, other): - 692 return self.value <= other - 693 - 694 def __gt__(self, other): - 695 return self.value > other + 15 + 16class Obs: + 17 """Class for a general observable. + 18 + 19 Instances of Obs are the basic objects of a pyerrors error analysis. + 20 They are initialized with a list which contains arrays of samples for + 21 different ensembles/replica and another list of same length which contains + 22 the names of the ensembles/replica. Mathematical operations can be + 23 performed on instances. The result is another instance of Obs. The error of + 24 an instance can be computed with the gamma_method. Also contains additional + 25 methods for output and visualization of the error calculation. + 26 + 27 Attributes + 28 ---------- + 29 S_global : float + 30 Standard value for S (default 2.0) + 31 S_dict : dict + 32 Dictionary for S values. If an entry for a given ensemble + 33 exists this overwrites the standard value for that ensemble. + 34 tau_exp_global : float + 35 Standard value for tau_exp (default 0.0) + 36 tau_exp_dict : dict + 37 Dictionary for tau_exp values. If an entry for a given ensemble exists + 38 this overwrites the standard value for that ensemble. + 39 N_sigma_global : float + 40 Standard value for N_sigma (default 1.0) + 41 N_sigma_dict : dict + 42 Dictionary for N_sigma values. If an entry for a given ensemble exists + 43 this overwrites the standard value for that ensemble. + 44 """ + 45 __slots__ = ['names', 'shape', 'r_values', 'deltas', 'N', '_value', '_dvalue', + 46 'ddvalue', 'reweighted', 'S', 'tau_exp', 'N_sigma', + 47 'e_dvalue', 'e_ddvalue', 'e_tauint', 'e_dtauint', + 48 'e_windowsize', 'e_rho', 'e_drho', 'e_n_tauint', 'e_n_dtauint', + 49 'idl', 'is_merged', 'tag', '_covobs', '__dict__'] + 50 + 51 S_global = 2.0 + 52 S_dict = {} + 53 tau_exp_global = 0.0 + 54 tau_exp_dict = {} + 55 N_sigma_global = 1.0 + 56 N_sigma_dict = {} + 57 filter_eps = 1e-10 + 58 + 59 def __init__(self, samples, names, idl=None, **kwargs): + 60 """ Initialize Obs object. + 61 + 62 Parameters + 63 ---------- + 64 samples : list + 65 list of numpy arrays containing the Monte Carlo samples + 66 names : list + 67 list of strings labeling the individual samples + 68 idl : list, optional + 69 list of ranges or lists on which the samples are defined + 70 """ + 71 + 72 if kwargs.get("means") is None and len(samples): + 73 if len(samples) != len(names): + 74 raise Exception('Length of samples and names incompatible.') + 75 if idl is not None: + 76 if len(idl) != len(names): + 77 raise Exception('Length of idl incompatible with samples and names.') + 78 name_length = len(names) + 79 if name_length > 1: + 80 if name_length != len(set(names)): + 81 raise Exception('names are not unique.') + 82 if not all(isinstance(x, str) for x in names): + 83 raise TypeError('All names have to be strings.') + 84 else: + 85 if not isinstance(names[0], str): + 86 raise TypeError('All names have to be strings.') + 87 if min(len(x) for x in samples) <= 4: + 88 raise Exception('Samples have to have at least 5 entries.') + 89 + 90 self.names = sorted(names) + 91 self.shape = {} + 92 self.r_values = {} + 93 self.deltas = {} + 94 self._covobs = {} + 95 + 96 self._value = 0 + 97 self.N = 0 + 98 self.is_merged = {} + 99 self.idl = {} + 100 if idl is not None: + 101 for name, idx in sorted(zip(names, idl)): + 102 if isinstance(idx, range): + 103 self.idl[name] = idx + 104 elif isinstance(idx, (list, np.ndarray)): + 105 dc = np.unique(np.diff(idx)) + 106 if np.any(dc < 0): + 107 raise Exception("Unsorted idx for idl[%s]" % (name)) + 108 if len(dc) == 1: + 109 self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0]) + 110 else: + 111 self.idl[name] = list(idx) + 112 else: + 113 raise Exception('incompatible type for idl[%s].' % (name)) + 114 else: + 115 for name, sample in sorted(zip(names, samples)): + 116 self.idl[name] = range(1, len(sample) + 1) + 117 + 118 if kwargs.get("means") is not None: + 119 for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))): + 120 self.shape[name] = len(self.idl[name]) + 121 self.N += self.shape[name] + 122 self.r_values[name] = mean + 123 self.deltas[name] = sample + 124 else: + 125 for name, sample in sorted(zip(names, samples)): + 126 self.shape[name] = len(self.idl[name]) + 127 self.N += self.shape[name] + 128 if len(sample) != self.shape[name]: + 129 raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name])) + 130 self.r_values[name] = np.mean(sample) + 131 self.deltas[name] = sample - self.r_values[name] + 132 self._value += self.shape[name] * self.r_values[name] + 133 self._value /= self.N + 134 + 135 self._dvalue = 0.0 + 136 self.ddvalue = 0.0 + 137 self.reweighted = False + 138 + 139 self.tag = None + 140 + 141 @property + 142 def value(self): + 143 return self._value + 144 + 145 @property + 146 def dvalue(self): + 147 return self._dvalue + 148 + 149 @property + 150 def e_names(self): + 151 return sorted(set([o.split('|')[0] for o in self.names])) + 152 + 153 @property + 154 def cov_names(self): + 155 return sorted(set([o for o in self.covobs.keys()])) + 156 + 157 @property + 158 def mc_names(self): + 159 return sorted(set([o.split('|')[0] for o in self.names if o not in self.cov_names])) + 160 + 161 @property + 162 def e_content(self): + 163 res = {} + 164 for e, e_name in enumerate(self.e_names): + 165 res[e_name] = sorted(filter(lambda x: x.startswith(e_name + '|'), self.names)) + 166 if e_name in self.names: + 167 res[e_name].append(e_name) + 168 return res + 169 + 170 @property + 171 def covobs(self): + 172 return self._covobs + 173 + 174 def gamma_method(self, **kwargs): + 175 """Estimate the error and related properties of the Obs. + 176 + 177 Parameters + 178 ---------- + 179 S : float + 180 specifies a custom value for the parameter S (default 2.0). + 181 If set to 0 it is assumed that the data exhibits no + 182 autocorrelation. In this case the error estimates coincides + 183 with the sample standard error. + 184 tau_exp : float + 185 positive value triggers the critical slowing down analysis + 186 (default 0.0). + 187 N_sigma : float + 188 number of standard deviations from zero until the tail is + 189 attached to the autocorrelation function (default 1). + 190 fft : bool + 191 determines whether the fft algorithm is used for the computation + 192 of the autocorrelation function (default True) + 193 """ + 194 + 195 e_content = self.e_content + 196 self.e_dvalue = {} + 197 self.e_ddvalue = {} + 198 self.e_tauint = {} + 199 self.e_dtauint = {} + 200 self.e_windowsize = {} + 201 self.e_n_tauint = {} + 202 self.e_n_dtauint = {} + 203 e_gamma = {} + 204 self.e_rho = {} + 205 self.e_drho = {} + 206 self._dvalue = 0 + 207 self.ddvalue = 0 + 208 + 209 self.S = {} + 210 self.tau_exp = {} + 211 self.N_sigma = {} + 212 + 213 if kwargs.get('fft') is False: + 214 fft = False + 215 else: + 216 fft = True + 217 + 218 def _parse_kwarg(kwarg_name): + 219 if kwarg_name in kwargs: + 220 tmp = kwargs.get(kwarg_name) + 221 if isinstance(tmp, (int, float)): + 222 if tmp < 0: + 223 raise Exception(kwarg_name + ' has to be larger or equal to 0.') + 224 for e, e_name in enumerate(self.e_names): + 225 getattr(self, kwarg_name)[e_name] = tmp + 226 else: + 227 raise TypeError(kwarg_name + ' is not in proper format.') + 228 else: + 229 for e, e_name in enumerate(self.e_names): + 230 if e_name in getattr(Obs, kwarg_name + '_dict'): + 231 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name] + 232 else: + 233 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global') + 234 + 235 _parse_kwarg('S') + 236 _parse_kwarg('tau_exp') + 237 _parse_kwarg('N_sigma') + 238 + 239 for e, e_name in enumerate(self.mc_names): + 240 r_length = [] + 241 for r_name in e_content[e_name]: + 242 if isinstance(self.idl[r_name], range): + 243 r_length.append(len(self.idl[r_name])) + 244 else: + 245 r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1)) + 246 + 247 e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]]) + 248 w_max = max(r_length) // 2 + 249 e_gamma[e_name] = np.zeros(w_max) + 250 self.e_rho[e_name] = np.zeros(w_max) + 251 self.e_drho[e_name] = np.zeros(w_max) + 252 + 253 for r_name in e_content[e_name]: + 254 e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft) + 255 + 256 gamma_div = np.zeros(w_max) + 257 for r_name in e_content[e_name]: + 258 gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft) + 259 gamma_div[gamma_div < 1] = 1.0 + 260 e_gamma[e_name] /= gamma_div[:w_max] + 261 + 262 if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny: # Prevent division by zero + 263 self.e_tauint[e_name] = 0.5 + 264 self.e_dtauint[e_name] = 0.0 + 265 self.e_dvalue[e_name] = 0.0 + 266 self.e_ddvalue[e_name] = 0.0 + 267 self.e_windowsize[e_name] = 0 + 268 continue + 269 + 270 self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0] + 271 self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:]))) + 272 # Make sure no entry of tauint is smaller than 0.5 + 273 self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps + 274 # hep-lat/0306017 eq. (42) + 275 self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) + 0.5 - self.e_n_tauint[e_name]) / e_N) + 276 self.e_n_dtauint[e_name][0] = 0.0 + 277 + 278 def _compute_drho(i): + 279 tmp = self.e_rho[e_name][i + 1:w_max] + np.concatenate([self.e_rho[e_name][i - 1::-1], self.e_rho[e_name][1:w_max - 2 * i]]) - 2 * self.e_rho[e_name][i] * self.e_rho[e_name][1:w_max - i] + 280 self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N) + 281 + 282 _compute_drho(1) + 283 if self.tau_exp[e_name] > 0: + 284 texp = self.tau_exp[e_name] + 285 # Critical slowing down analysis + 286 if w_max // 2 <= 1: + 287 raise Exception("Need at least 8 samples for tau_exp error analysis") + 288 for n in range(1, w_max // 2): + 289 _compute_drho(n + 1) + 290 if (self.e_rho[e_name][n] - self.N_sigma[e_name] * self.e_drho[e_name][n]) < 0 or n >= w_max // 2 - 2: + 291 # Bias correction hep-lat/0306017 eq. (49) included + 292 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) + texp * np.abs(self.e_rho[e_name][n + 1]) # The absolute makes sure, that the tail contribution is always positive + 293 self.e_dtauint[e_name] = np.sqrt(self.e_n_dtauint[e_name][n] ** 2 + texp ** 2 * self.e_drho[e_name][n + 1] ** 2) + 294 # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2 + 295 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) + 296 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) + 297 self.e_windowsize[e_name] = n + 298 break + 299 else: + 300 if self.S[e_name] == 0.0: + 301 self.e_tauint[e_name] = 0.5 + 302 self.e_dtauint[e_name] = 0.0 + 303 self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1)) + 304 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N) + 305 self.e_windowsize[e_name] = 0 + 306 else: + 307 # Standard automatic windowing procedure + 308 tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][1:] + 1) / (2 * self.e_n_tauint[e_name][1:] - 1)) + 309 g_w = np.exp(- np.arange(1, w_max) / tau) - tau / np.sqrt(np.arange(1, w_max) * e_N) + 310 for n in range(1, w_max): + 311 if n < w_max // 2 - 2: + 312 _compute_drho(n + 1) + 313 if g_w[n - 1] < 0 or n >= w_max - 1: + 314 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) # Bias correction hep-lat/0306017 eq. (49) + 315 self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n] + 316 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) + 317 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) + 318 self.e_windowsize[e_name] = n + 319 break + 320 + 321 self._dvalue += self.e_dvalue[e_name] ** 2 + 322 self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2 + 323 + 324 for e_name in self.cov_names: + 325 self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq()) + 326 self.e_ddvalue[e_name] = 0 + 327 self._dvalue += self.e_dvalue[e_name]**2 + 328 + 329 self._dvalue = np.sqrt(self._dvalue) + 330 if self._dvalue == 0.0: + 331 self.ddvalue = 0.0 + 332 else: + 333 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue + 334 return + 335 + 336 def _calc_gamma(self, deltas, idx, shape, w_max, fft): + 337 """Calculate Gamma_{AA} from the deltas, which are defined on idx. + 338 idx is assumed to be a contiguous range (possibly with a stepsize != 1) + 339 + 340 Parameters + 341 ---------- + 342 deltas : list + 343 List of fluctuations + 344 idx : list + 345 List or range of configurations on which the deltas are defined. + 346 shape : int + 347 Number of configurations in idx. + 348 w_max : int + 349 Upper bound for the summation window. + 350 fft : bool + 351 determines whether the fft algorithm is used for the computation + 352 of the autocorrelation function. + 353 """ + 354 gamma = np.zeros(w_max) + 355 deltas = _expand_deltas(deltas, idx, shape) + 356 new_shape = len(deltas) + 357 if fft: + 358 max_gamma = min(new_shape, w_max) + 359 # The padding for the fft has to be even + 360 padding = new_shape + max_gamma + (new_shape + max_gamma) % 2 + 361 gamma[:max_gamma] += np.fft.irfft(np.abs(np.fft.rfft(deltas, padding)) ** 2)[:max_gamma] + 362 else: + 363 for n in range(w_max): + 364 if new_shape - n >= 0: + 365 gamma[n] += deltas[0:new_shape - n].dot(deltas[n:new_shape]) + 366 + 367 return gamma + 368 + 369 def details(self, ens_content=True): + 370 """Output detailed properties of the Obs. + 371 + 372 Parameters + 373 ---------- + 374 ens_content : bool + 375 print details about the ensembles and replica if true. + 376 """ + 377 if self.tag is not None: + 378 print("Description:", self.tag) + 379 if not hasattr(self, 'e_dvalue'): + 380 print('Result\t %3.8e' % (self.value)) + 381 else: + 382 if self.value == 0.0: + 383 percentage = np.nan + 384 else: + 385 percentage = np.abs(self._dvalue / self.value) * 100 + 386 print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage)) + 387 if len(self.e_names) > 1: + 388 print(' Ensemble errors:') + 389 for e_name in self.mc_names: + 390 if len(self.e_names) > 1: + 391 print('', e_name, '\t %3.8e +/- %3.8e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name])) + 392 if self.tau_exp[e_name] > 0: + 393 print(' t_int\t %3.8e +/- %3.8e tau_exp = %3.2f, N_sigma = %1.0i' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.tau_exp[e_name], self.N_sigma[e_name])) + 394 else: + 395 print(' t_int\t %3.8e +/- %3.8e S = %3.2f' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.S[e_name])) + 396 for e_name in self.cov_names: + 397 print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name])) + 398 if ens_content is True: + 399 if len(self.e_names) == 1: + 400 print(self.N, 'samples in', len(self.e_names), 'ensemble:') + 401 else: + 402 print(self.N, 'samples in', len(self.e_names), 'ensembles:') + 403 my_string_list = [] + 404 for key, value in sorted(self.e_content.items()): + 405 if key not in self.covobs: + 406 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " + 407 if len(value) == 1: + 408 my_string += f': {self.shape[value[0]]} configurations' + 409 if isinstance(self.idl[value[0]], range): + 410 my_string += f' (from {self.idl[value[0]].start} to {self.idl[value[0]][-1]}' + int(self.idl[value[0]].step != 1) * f' in steps of {self.idl[value[0]].step}' + ')' + 411 else: + 412 my_string += ' (irregular range)' + 413 else: + 414 sublist = [] + 415 for v in value: + 416 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " + 417 my_substring += f': {self.shape[v]} configurations' + 418 if isinstance(self.idl[v], range): + 419 my_substring += f' (from {self.idl[v].start} to {self.idl[v][-1]}' + int(self.idl[v].step != 1) * f' in steps of {self.idl[v].step}' + ')' + 420 else: + 421 my_substring += ' (irregular range)' + 422 sublist.append(my_substring) + 423 + 424 my_string += '\n' + '\n'.join(sublist) + 425 else: + 426 my_string = ' ' + "\u00B7 Covobs '" + key + "' " + 427 my_string_list.append(my_string) + 428 print('\n'.join(my_string_list)) + 429 + 430 def reweight(self, weight): + 431 """Reweight the obs with given rewighting factors. + 432 + 433 Parameters + 434 ---------- + 435 weight : Obs + 436 Reweighting factor. An Observable that has to be defined on a superset of the + 437 configurations in obs[i].idl for all i. + 438 all_configs : bool + 439 if True, the reweighted observables are normalized by the average of + 440 the reweighting factor on all configurations in weight.idl and not + 441 on the configurations in obs[i].idl. Default False. + 442 """ + 443 return reweight(weight, [self])[0] + 444 + 445 def is_zero_within_error(self, sigma=1): + 446 """Checks whether the observable is zero within 'sigma' standard errors. + 447 + 448 Parameters + 449 ---------- + 450 sigma : int + 451 Number of standard errors used for the check. + 452 + 453 Works only properly when the gamma method was run. + 454 """ + 455 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue + 456 + 457 def is_zero(self, atol=1e-10): + 458 """Checks whether the observable is zero within a given tolerance. + 459 + 460 Parameters + 461 ---------- + 462 atol : float + 463 Absolute tolerance (for details see numpy documentation). + 464 """ + 465 return np.isclose(0.0, self.value, 1e-14, atol) and all(np.allclose(0.0, delta, 1e-14, atol) for delta in self.deltas.values()) and all(np.allclose(0.0, delta.errsq(), 1e-14, atol) for delta in self.covobs.values()) + 466 + 467 def plot_tauint(self, save=None): + 468 """Plot integrated autocorrelation time for each ensemble. + 469 + 470 Parameters + 471 ---------- + 472 save : str + 473 saves the figure to a file named 'save' if. + 474 """ + 475 if not hasattr(self, 'e_dvalue'): + 476 raise Exception('Run the gamma method first.') + 477 + 478 for e, e_name in enumerate(self.mc_names): + 479 fig = plt.figure() + 480 plt.xlabel(r'$W$') + 481 plt.ylabel(r'$\tau_\mathrm{int}$') + 482 length = int(len(self.e_n_tauint[e_name])) + 483 if self.tau_exp[e_name] > 0: + 484 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] + 485 x_help = np.arange(2 * self.tau_exp[e_name]) + 486 y_help = (x_help + 1) * np.abs(self.e_rho[e_name][self.e_windowsize[e_name] + 1]) * (1 - x_help / (2 * (2 * self.tau_exp[e_name] - 1))) + base + 487 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) + 488 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') + 489 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], + 490 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) + 491 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 + 492 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) + 493 else: + 494 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) + 495 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) + 496 + 497 plt.errorbar(np.arange(length)[:int(xmax) + 1], self.e_n_tauint[e_name][:int(xmax) + 1], yerr=self.e_n_dtauint[e_name][:int(xmax) + 1], linewidth=1, capsize=2, label=label) + 498 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') + 499 plt.legend() + 500 plt.xlim(-0.5, xmax) + 501 ylim = plt.ylim() + 502 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) + 503 plt.draw() + 504 if save: + 505 fig.savefig(save + "_" + str(e)) + 506 + 507 def plot_rho(self, save=None): + 508 """Plot normalized autocorrelation function time for each ensemble. + 509 + 510 Parameters + 511 ---------- + 512 save : str + 513 saves the figure to a file named 'save' if. + 514 """ + 515 if not hasattr(self, 'e_dvalue'): + 516 raise Exception('Run the gamma method first.') + 517 for e, e_name in enumerate(self.mc_names): + 518 fig = plt.figure() + 519 plt.xlabel('W') + 520 plt.ylabel('rho') + 521 length = int(len(self.e_drho[e_name])) + 522 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) + 523 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') + 524 if self.tau_exp[e_name] > 0: + 525 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], + 526 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) + 527 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 + 528 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) + 529 else: + 530 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) + 531 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) + 532 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) + 533 plt.xlim(-0.5, xmax) + 534 plt.draw() + 535 if save: + 536 fig.savefig(save + "_" + str(e)) + 537 + 538 def plot_rep_dist(self): + 539 """Plot replica distribution for each ensemble with more than one replicum.""" + 540 if not hasattr(self, 'e_dvalue'): + 541 raise Exception('Run the gamma method first.') + 542 for e, e_name in enumerate(self.mc_names): + 543 if len(self.e_content[e_name]) == 1: + 544 print('No replica distribution for a single replicum (', e_name, ')') + 545 continue + 546 r_length = [] + 547 sub_r_mean = 0 + 548 for r, r_name in enumerate(self.e_content[e_name]): + 549 r_length.append(len(self.deltas[r_name])) + 550 sub_r_mean += self.shape[r_name] * self.r_values[r_name] + 551 e_N = np.sum(r_length) + 552 sub_r_mean /= e_N + 553 arr = np.zeros(len(self.e_content[e_name])) + 554 for r, r_name in enumerate(self.e_content[e_name]): + 555 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) + 556 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) + 557 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') + 558 plt.draw() + 559 + 560 def plot_history(self, expand=True): + 561 """Plot derived Monte Carlo history for each ensemble + 562 + 563 Parameters + 564 ---------- + 565 expand : bool + 566 show expanded history for irregular Monte Carlo chains (default: True). + 567 """ + 568 for e, e_name in enumerate(self.mc_names): + 569 plt.figure() + 570 r_length = [] + 571 tmp = [] + 572 tmp_expanded = [] + 573 for r, r_name in enumerate(self.e_content[e_name]): + 574 tmp.append(self.deltas[r_name] + self.r_values[r_name]) + 575 if expand: + 576 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name]) + 577 r_length.append(len(tmp_expanded[-1])) + 578 else: + 579 r_length.append(len(tmp[-1])) + 580 e_N = np.sum(r_length) + 581 x = np.arange(e_N) + 582 y_test = np.concatenate(tmp, axis=0) + 583 if expand: + 584 y = np.concatenate(tmp_expanded, axis=0) + 585 else: + 586 y = y_test + 587 plt.errorbar(x, y, fmt='.', markersize=3) + 588 plt.xlim(-0.5, e_N - 0.5) + 589 plt.title(e_name + f'\nskew: {skew(y_test):.3f} (p={skewtest(y_test).pvalue:.3f}), kurtosis: {kurtosis(y_test):.3f} (p={kurtosistest(y_test).pvalue:.3f})') + 590 plt.draw() + 591 + 592 def plot_piechart(self, save=None): + 593 """Plot piechart which shows the fractional contribution of each + 594 ensemble to the error and returns a dictionary containing the fractions. + 595 + 596 Parameters + 597 ---------- + 598 save : str + 599 saves the figure to a file named 'save' if. + 600 """ + 601 if not hasattr(self, 'e_dvalue'): + 602 raise Exception('Run the gamma method first.') + 603 if np.isclose(0.0, self._dvalue, atol=1e-15): + 604 raise Exception('Error is 0.0') + 605 labels = self.e_names + 606 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 + 607 fig1, ax1 = plt.subplots() + 608 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) + 609 ax1.axis('equal') + 610 plt.draw() + 611 if save: + 612 fig1.savefig(save) + 613 + 614 return dict(zip(self.e_names, sizes)) + 615 + 616 def dump(self, filename, datatype="json.gz", description="", **kwargs): + 617 """Dump the Obs to a file 'name' of chosen format. + 618 + 619 Parameters + 620 ---------- + 621 filename : str + 622 name of the file to be saved. + 623 datatype : str + 624 Format of the exported file. Supported formats include + 625 "json.gz" and "pickle" + 626 description : str + 627 Description for output file, only relevant for json.gz format. + 628 path : str + 629 specifies a custom path for the file (default '.') + 630 """ + 631 if 'path' in kwargs: + 632 file_name = kwargs.get('path') + '/' + filename + 633 else: + 634 file_name = filename + 635 + 636 if datatype == "json.gz": + 637 from .input.json import dump_to_json + 638 dump_to_json([self], file_name, description=description) + 639 elif datatype == "pickle": + 640 with open(file_name + '.p', 'wb') as fb: + 641 pickle.dump(self, fb) + 642 else: + 643 raise Exception("Unknown datatype " + str(datatype)) + 644 + 645 def export_jackknife(self): + 646 """Export jackknife samples from the Obs + 647 + 648 Returns + 649 ------- + 650 numpy.ndarray + 651 Returns a numpy array of length N + 1 where N is the number of samples + 652 for the given ensemble and replicum. The zeroth entry of the array contains + 653 the mean value of the Obs, entries 1 to N contain the N jackknife samples + 654 derived from the Obs. The current implementation only works for observables + 655 defined on exactly one ensemble and replicum. The derived jackknife samples + 656 should agree with samples from a full jackknife analysis up to O(1/N). + 657 """ + 658 + 659 if len(self.names) != 1: + 660 raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") + 661 + 662 name = self.names[0] + 663 full_data = self.deltas[name] + self.r_values[name] + 664 n = full_data.size + 665 mean = self.value + 666 tmp_jacks = np.zeros(n + 1) + 667 tmp_jacks[0] = mean + 668 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) + 669 return tmp_jacks + 670 + 671 def __float__(self): + 672 return float(self.value) + 673 + 674 def __repr__(self): + 675 return 'Obs[' + str(self) + ']' + 676 + 677 def __str__(self): + 678 if self._dvalue == 0.0: + 679 return str(self.value) + 680 fexp = np.floor(np.log10(self._dvalue)) + 681 if fexp < 0.0: + 682 return '{:{form}}({:2.0f})'.format(self.value, self._dvalue * 10 ** (-fexp + 1), form='.' + str(-int(fexp) + 1) + 'f') + 683 elif fexp == 0.0: + 684 return '{:.1f}({:1.1f})'.format(self.value, self._dvalue) + 685 else: + 686 return '{:.0f}({:2.0f})'.format(self.value, self._dvalue) + 687 + 688 def __hash__(self): + 689 hash_tuple = (np.array([self.value]).astype(np.float32).data.tobytes(),) + 690 hash_tuple += tuple([o.astype(np.float32).data.tobytes() for o in self.deltas.values()]) + 691 hash_tuple += tuple([np.array([o.errsq()]).astype(np.float32).data.tobytes() for o in self.covobs.values()]) + 692 hash_tuple += tuple([o.encode() for o in self.names]) + 693 m = hashlib.md5() + 694 [m.update(o) for o in hash_tuple] + 695 return int(m.hexdigest(), 16) & 0xFFFFFFFF 696 - 697 def __ge__(self, other): - 698 return self.value >= other - 699 - 700 def __eq__(self, other): - 701 return (self - other).is_zero() - 702 - 703 def __ne__(self, other): - 704 return not (self - other).is_zero() - 705 - 706 # Overload math operations - 707 def __add__(self, y): - 708 if isinstance(y, Obs): - 709 return derived_observable(lambda x, **kwargs: x[0] + x[1], [self, y], man_grad=[1, 1]) - 710 else: - 711 if isinstance(y, np.ndarray): - 712 return np.array([self + o for o in y]) - 713 elif y.__class__.__name__ in ['Corr', 'CObs']: - 714 return NotImplemented - 715 else: - 716 return derived_observable(lambda x, **kwargs: x[0] + y, [self], man_grad=[1]) - 717 - 718 def __radd__(self, y): - 719 return self + y - 720 - 721 def __mul__(self, y): - 722 if isinstance(y, Obs): - 723 return derived_observable(lambda x, **kwargs: x[0] * x[1], [self, y], man_grad=[y.value, self.value]) - 724 else: - 725 if isinstance(y, np.ndarray): - 726 return np.array([self * o for o in y]) - 727 elif isinstance(y, complex): - 728 return CObs(self * y.real, self * y.imag) - 729 elif y.__class__.__name__ in ['Corr', 'CObs']: - 730 return NotImplemented - 731 else: - 732 return derived_observable(lambda x, **kwargs: x[0] * y, [self], man_grad=[y]) - 733 - 734 def __rmul__(self, y): - 735 return self * y - 736 - 737 def __sub__(self, y): - 738 if isinstance(y, Obs): - 739 return derived_observable(lambda x, **kwargs: x[0] - x[1], [self, y], man_grad=[1, -1]) - 740 else: - 741 if isinstance(y, np.ndarray): - 742 return np.array([self - o for o in y]) - 743 elif y.__class__.__name__ in ['Corr', 'CObs']: - 744 return NotImplemented - 745 else: - 746 return derived_observable(lambda x, **kwargs: x[0] - y, [self], man_grad=[1]) - 747 - 748 def __rsub__(self, y): - 749 return -1 * (self - y) - 750 - 751 def __pos__(self): - 752 return self - 753 - 754 def __neg__(self): - 755 return -1 * self - 756 - 757 def __truediv__(self, y): - 758 if isinstance(y, Obs): - 759 return derived_observable(lambda x, **kwargs: x[0] / x[1], [self, y], man_grad=[1 / y.value, - self.value / y.value ** 2]) - 760 else: - 761 if isinstance(y, np.ndarray): - 762 return np.array([self / o for o in y]) - 763 elif y.__class__.__name__ in ['Corr', 'CObs']: - 764 return NotImplemented - 765 else: - 766 return derived_observable(lambda x, **kwargs: x[0] / y, [self], man_grad=[1 / y]) - 767 - 768 def __rtruediv__(self, y): - 769 if isinstance(y, Obs): - 770 return derived_observable(lambda x, **kwargs: x[0] / x[1], [y, self], man_grad=[1 / self.value, - y.value / self.value ** 2]) - 771 else: - 772 if isinstance(y, np.ndarray): - 773 return np.array([o / self for o in y]) - 774 elif y.__class__.__name__ in ['Corr', 'CObs']: - 775 return NotImplemented - 776 else: - 777 return derived_observable(lambda x, **kwargs: y / x[0], [self], man_grad=[-y / self.value ** 2]) - 778 - 779 def __pow__(self, y): - 780 if isinstance(y, Obs): - 781 return derived_observable(lambda x: x[0] ** x[1], [self, y]) - 782 else: - 783 return derived_observable(lambda x: x[0] ** y, [self]) - 784 - 785 def __rpow__(self, y): - 786 if isinstance(y, Obs): - 787 return derived_observable(lambda x: x[0] ** x[1], [y, self]) - 788 else: - 789 return derived_observable(lambda x: y ** x[0], [self]) - 790 - 791 def __abs__(self): - 792 return derived_observable(lambda x: anp.abs(x[0]), [self]) - 793 - 794 # Overload numpy functions - 795 def sqrt(self): - 796 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) - 797 - 798 def log(self): - 799 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) + 697 # Overload comparisons + 698 def __lt__(self, other): + 699 return self.value < other + 700 + 701 def __le__(self, other): + 702 return self.value <= other + 703 + 704 def __gt__(self, other): + 705 return self.value > other + 706 + 707 def __ge__(self, other): + 708 return self.value >= other + 709 + 710 def __eq__(self, other): + 711 return (self - other).is_zero() + 712 + 713 def __ne__(self, other): + 714 return not (self - other).is_zero() + 715 + 716 # Overload math operations + 717 def __add__(self, y): + 718 if isinstance(y, Obs): + 719 return derived_observable(lambda x, **kwargs: x[0] + x[1], [self, y], man_grad=[1, 1]) + 720 else: + 721 if isinstance(y, np.ndarray): + 722 return np.array([self + o for o in y]) + 723 elif y.__class__.__name__ in ['Corr', 'CObs']: + 724 return NotImplemented + 725 else: + 726 return derived_observable(lambda x, **kwargs: x[0] + y, [self], man_grad=[1]) + 727 + 728 def __radd__(self, y): + 729 return self + y + 730 + 731 def __mul__(self, y): + 732 if isinstance(y, Obs): + 733 return derived_observable(lambda x, **kwargs: x[0] * x[1], [self, y], man_grad=[y.value, self.value]) + 734 else: + 735 if isinstance(y, np.ndarray): + 736 return np.array([self * o for o in y]) + 737 elif isinstance(y, complex): + 738 return CObs(self * y.real, self * y.imag) + 739 elif y.__class__.__name__ in ['Corr', 'CObs']: + 740 return NotImplemented + 741 else: + 742 return derived_observable(lambda x, **kwargs: x[0] * y, [self], man_grad=[y]) + 743 + 744 def __rmul__(self, y): + 745 return self * y + 746 + 747 def __sub__(self, y): + 748 if isinstance(y, Obs): + 749 return derived_observable(lambda x, **kwargs: x[0] - x[1], [self, y], man_grad=[1, -1]) + 750 else: + 751 if isinstance(y, np.ndarray): + 752 return np.array([self - o for o in y]) + 753 elif y.__class__.__name__ in ['Corr', 'CObs']: + 754 return NotImplemented + 755 else: + 756 return derived_observable(lambda x, **kwargs: x[0] - y, [self], man_grad=[1]) + 757 + 758 def __rsub__(self, y): + 759 return -1 * (self - y) + 760 + 761 def __pos__(self): + 762 return self + 763 + 764 def __neg__(self): + 765 return -1 * self + 766 + 767 def __truediv__(self, y): + 768 if isinstance(y, Obs): + 769 return derived_observable(lambda x, **kwargs: x[0] / x[1], [self, y], man_grad=[1 / y.value, - self.value / y.value ** 2]) + 770 else: + 771 if isinstance(y, np.ndarray): + 772 return np.array([self / o for o in y]) + 773 elif y.__class__.__name__ in ['Corr', 'CObs']: + 774 return NotImplemented + 775 else: + 776 return derived_observable(lambda x, **kwargs: x[0] / y, [self], man_grad=[1 / y]) + 777 + 778 def __rtruediv__(self, y): + 779 if isinstance(y, Obs): + 780 return derived_observable(lambda x, **kwargs: x[0] / x[1], [y, self], man_grad=[1 / self.value, - y.value / self.value ** 2]) + 781 else: + 782 if isinstance(y, np.ndarray): + 783 return np.array([o / self for o in y]) + 784 elif y.__class__.__name__ in ['Corr', 'CObs']: + 785 return NotImplemented + 786 else: + 787 return derived_observable(lambda x, **kwargs: y / x[0], [self], man_grad=[-y / self.value ** 2]) + 788 + 789 def __pow__(self, y): + 790 if isinstance(y, Obs): + 791 return derived_observable(lambda x: x[0] ** x[1], [self, y]) + 792 else: + 793 return derived_observable(lambda x: x[0] ** y, [self]) + 794 + 795 def __rpow__(self, y): + 796 if isinstance(y, Obs): + 797 return derived_observable(lambda x: x[0] ** x[1], [y, self]) + 798 else: + 799 return derived_observable(lambda x: y ** x[0], [self]) 800 - 801 def exp(self): - 802 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) + 801 def __abs__(self): + 802 return derived_observable(lambda x: anp.abs(x[0]), [self]) 803 - 804 def sin(self): - 805 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) - 806 - 807 def cos(self): - 808 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) - 809 - 810 def tan(self): - 811 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) - 812 - 813 def arcsin(self): - 814 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) - 815 - 816 def arccos(self): - 817 return derived_observable(lambda x: anp.arccos(x[0]), [self]) - 818 - 819 def arctan(self): - 820 return derived_observable(lambda x: anp.arctan(x[0]), [self]) - 821 - 822 def sinh(self): - 823 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) - 824 - 825 def cosh(self): - 826 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) - 827 - 828 def tanh(self): - 829 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) - 830 - 831 def arcsinh(self): - 832 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) - 833 - 834 def arccosh(self): - 835 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) - 836 - 837 def arctanh(self): - 838 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) - 839 + 804 # Overload numpy functions + 805 def sqrt(self): + 806 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) + 807 + 808 def log(self): + 809 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) + 810 + 811 def exp(self): + 812 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) + 813 + 814 def sin(self): + 815 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) + 816 + 817 def cos(self): + 818 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) + 819 + 820 def tan(self): + 821 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) + 822 + 823 def arcsin(self): + 824 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) + 825 + 826 def arccos(self): + 827 return derived_observable(lambda x: anp.arccos(x[0]), [self]) + 828 + 829 def arctan(self): + 830 return derived_observable(lambda x: anp.arctan(x[0]), [self]) + 831 + 832 def sinh(self): + 833 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) + 834 + 835 def cosh(self): + 836 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) + 837 + 838 def tanh(self): + 839 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) 840 - 841class CObs: - 842 """Class for a complex valued observable.""" - 843 __slots__ = ['_real', '_imag', 'tag'] - 844 - 845 def __init__(self, real, imag=0.0): - 846 self._real = real - 847 self._imag = imag - 848 self.tag = None + 841 def arcsinh(self): + 842 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) + 843 + 844 def arccosh(self): + 845 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) + 846 + 847 def arctanh(self): + 848 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) 849 - 850 @property - 851 def real(self): - 852 return self._real - 853 - 854 @property - 855 def imag(self): - 856 return self._imag - 857 - 858 def gamma_method(self, **kwargs): - 859 """Executes the gamma_method for the real and the imaginary part.""" - 860 if isinstance(self.real, Obs): - 861 self.real.gamma_method(**kwargs) - 862 if isinstance(self.imag, Obs): - 863 self.imag.gamma_method(**kwargs) - 864 - 865 def is_zero(self): - 866 """Checks whether both real and imaginary part are zero within machine precision.""" - 867 return self.real == 0.0 and self.imag == 0.0 - 868 - 869 def conjugate(self): - 870 return CObs(self.real, -self.imag) - 871 - 872 def __add__(self, other): - 873 if isinstance(other, np.ndarray): - 874 return other + self - 875 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 876 return CObs(self.real + other.real, - 877 self.imag + other.imag) - 878 else: - 879 return CObs(self.real + other, self.imag) - 880 - 881 def __radd__(self, y): - 882 return self + y - 883 - 884 def __sub__(self, other): - 885 if isinstance(other, np.ndarray): - 886 return -1 * (other - self) - 887 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 888 return CObs(self.real - other.real, self.imag - other.imag) - 889 else: - 890 return CObs(self.real - other, self.imag) - 891 - 892 def __rsub__(self, other): - 893 return -1 * (self - other) - 894 - 895 def __mul__(self, other): - 896 if isinstance(other, np.ndarray): - 897 return other * self - 898 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 899 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): - 900 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], - 901 [self.real, other.real, self.imag, other.imag], - 902 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), - 903 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], - 904 [self.real, other.real, self.imag, other.imag], - 905 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) - 906 elif getattr(other, 'imag', 0) != 0: - 907 return CObs(self.real * other.real - self.imag * other.imag, - 908 self.imag * other.real + self.real * other.imag) - 909 else: - 910 return CObs(self.real * other.real, self.imag * other.real) - 911 else: - 912 return CObs(self.real * other, self.imag * other) - 913 - 914 def __rmul__(self, other): - 915 return self * other - 916 - 917 def __truediv__(self, other): - 918 if isinstance(other, np.ndarray): - 919 return 1 / (other / self) - 920 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 921 r = other.real ** 2 + other.imag ** 2 - 922 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) - 923 else: - 924 return CObs(self.real / other, self.imag / other) - 925 - 926 def __rtruediv__(self, other): - 927 r = self.real ** 2 + self.imag ** 2 - 928 if hasattr(other, 'real') and hasattr(other, 'imag'): - 929 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) - 930 else: - 931 return CObs(self.real * other / r, -self.imag * other / r) - 932 - 933 def __abs__(self): - 934 return np.sqrt(self.real**2 + self.imag**2) + 850 + 851class CObs: + 852 """Class for a complex valued observable.""" + 853 __slots__ = ['_real', '_imag', 'tag'] + 854 + 855 def __init__(self, real, imag=0.0): + 856 self._real = real + 857 self._imag = imag + 858 self.tag = None + 859 + 860 @property + 861 def real(self): + 862 return self._real + 863 + 864 @property + 865 def imag(self): + 866 return self._imag + 867 + 868 def gamma_method(self, **kwargs): + 869 """Executes the gamma_method for the real and the imaginary part.""" + 870 if isinstance(self.real, Obs): + 871 self.real.gamma_method(**kwargs) + 872 if isinstance(self.imag, Obs): + 873 self.imag.gamma_method(**kwargs) + 874 + 875 def is_zero(self): + 876 """Checks whether both real and imaginary part are zero within machine precision.""" + 877 return self.real == 0.0 and self.imag == 0.0 + 878 + 879 def conjugate(self): + 880 return CObs(self.real, -self.imag) + 881 + 882 def __add__(self, other): + 883 if isinstance(other, np.ndarray): + 884 return other + self + 885 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 886 return CObs(self.real + other.real, + 887 self.imag + other.imag) + 888 else: + 889 return CObs(self.real + other, self.imag) + 890 + 891 def __radd__(self, y): + 892 return self + y + 893 + 894 def __sub__(self, other): + 895 if isinstance(other, np.ndarray): + 896 return -1 * (other - self) + 897 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 898 return CObs(self.real - other.real, self.imag - other.imag) + 899 else: + 900 return CObs(self.real - other, self.imag) + 901 + 902 def __rsub__(self, other): + 903 return -1 * (self - other) + 904 + 905 def __mul__(self, other): + 906 if isinstance(other, np.ndarray): + 907 return other * self + 908 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 909 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): + 910 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], + 911 [self.real, other.real, self.imag, other.imag], + 912 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), + 913 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], + 914 [self.real, other.real, self.imag, other.imag], + 915 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) + 916 elif getattr(other, 'imag', 0) != 0: + 917 return CObs(self.real * other.real - self.imag * other.imag, + 918 self.imag * other.real + self.real * other.imag) + 919 else: + 920 return CObs(self.real * other.real, self.imag * other.real) + 921 else: + 922 return CObs(self.real * other, self.imag * other) + 923 + 924 def __rmul__(self, other): + 925 return self * other + 926 + 927 def __truediv__(self, other): + 928 if isinstance(other, np.ndarray): + 929 return 1 / (other / self) + 930 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 931 r = other.real ** 2 + other.imag ** 2 + 932 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) + 933 else: + 934 return CObs(self.real / other, self.imag / other) 935 - 936 def __pos__(self): - 937 return self - 938 - 939 def __neg__(self): - 940 return -1 * self - 941 - 942 def __eq__(self, other): - 943 return self.real == other.real and self.imag == other.imag - 944 - 945 def __str__(self): - 946 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' - 947 - 948 def __repr__(self): - 949 return 'CObs[' + str(self) + ']' - 950 + 936 def __rtruediv__(self, other): + 937 r = self.real ** 2 + self.imag ** 2 + 938 if hasattr(other, 'real') and hasattr(other, 'imag'): + 939 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) + 940 else: + 941 return CObs(self.real * other / r, -self.imag * other / r) + 942 + 943 def __abs__(self): + 944 return np.sqrt(self.real**2 + self.imag**2) + 945 + 946 def __pos__(self): + 947 return self + 948 + 949 def __neg__(self): + 950 return -1 * self 951 - 952def _expand_deltas(deltas, idx, shape): - 953 """Expand deltas defined on idx to a regular, contiguous range, where holes are filled by 0. - 954 If idx is of type range, the deltas are not changed - 955 - 956 Parameters - 957 ---------- - 958 deltas : list - 959 List of fluctuations - 960 idx : list - 961 List or range of configs on which the deltas are defined, has to be sorted in ascending order. - 962 shape : int - 963 Number of configs in idx. - 964 """ - 965 if isinstance(idx, range): - 966 return deltas - 967 else: - 968 ret = np.zeros(idx[-1] - idx[0] + 1) - 969 for i in range(shape): - 970 ret[idx[i] - idx[0]] = deltas[i] - 971 return ret - 972 - 973 - 974def _merge_idx(idl): - 975 """Returns the union of all lists in idl as sorted list - 976 - 977 Parameters - 978 ---------- - 979 idl : list - 980 List of lists or ranges. - 981 """ + 952 def __eq__(self, other): + 953 return self.real == other.real and self.imag == other.imag + 954 + 955 def __str__(self): + 956 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' + 957 + 958 def __repr__(self): + 959 return 'CObs[' + str(self) + ']' + 960 + 961 + 962def _expand_deltas(deltas, idx, shape): + 963 """Expand deltas defined on idx to a regular, contiguous range, where holes are filled by 0. + 964 If idx is of type range, the deltas are not changed + 965 + 966 Parameters + 967 ---------- + 968 deltas : list + 969 List of fluctuations + 970 idx : list + 971 List or range of configs on which the deltas are defined, has to be sorted in ascending order. + 972 shape : int + 973 Number of configs in idx. + 974 """ + 975 if isinstance(idx, range): + 976 return deltas + 977 else: + 978 ret = np.zeros(idx[-1] - idx[0] + 1) + 979 for i in range(shape): + 980 ret[idx[i] - idx[0]] = deltas[i] + 981 return ret 982 - 983 # Use groupby to efficiently check whether all elements of idl are identical - 984 try: - 985 g = groupby(idl) - 986 if next(g, True) and not next(g, False): - 987 return idl[0] - 988 except Exception: - 989 pass - 990 - 991 if np.all([type(idx) is range for idx in idl]): - 992 if len(set([idx[0] for idx in idl])) == 1: - 993 idstart = min([idx.start for idx in idl]) - 994 idstop = max([idx.stop for idx in idl]) - 995 idstep = min([idx.step for idx in idl]) - 996 return range(idstart, idstop, idstep) - 997 - 998 return sorted(set().union(*idl)) - 999 + 983 + 984def _merge_idx(idl): + 985 """Returns the union of all lists in idl as sorted list + 986 + 987 Parameters + 988 ---------- + 989 idl : list + 990 List of lists or ranges. + 991 """ + 992 + 993 # Use groupby to efficiently check whether all elements of idl are identical + 994 try: + 995 g = groupby(idl) + 996 if next(g, True) and not next(g, False): + 997 return idl[0] + 998 except Exception: + 999 pass 1000 -1001def _intersection_idx(idl): -1002 """Returns the intersection of all lists in idl as sorted list -1003 -1004 Parameters -1005 ---------- -1006 idl : list -1007 List of lists or ranges. -1008 """ +1001 if np.all([type(idx) is range for idx in idl]): +1002 if len(set([idx[0] for idx in idl])) == 1: +1003 idstart = min([idx.start for idx in idl]) +1004 idstop = max([idx.stop for idx in idl]) +1005 idstep = min([idx.step for idx in idl]) +1006 return range(idstart, idstop, idstep) +1007 +1008 return sorted(set().union(*idl)) 1009 -1010 def _lcm(*args): -1011 """Returns the lowest common multiple of args. -1012 -1013 From python 3.9 onwards the math library contains an lcm function.""" -1014 return reduce(lambda a, b: a * b // gcd(a, b), args) -1015 -1016 # Use groupby to efficiently check whether all elements of idl are identical -1017 try: -1018 g = groupby(idl) -1019 if next(g, True) and not next(g, False): -1020 return idl[0] -1021 except Exception: -1022 pass -1023 -1024 if np.all([type(idx) is range for idx in idl]): -1025 if len(set([idx[0] for idx in idl])) == 1: -1026 idstart = max([idx.start for idx in idl]) -1027 idstop = min([idx.stop for idx in idl]) -1028 idstep = _lcm(*[idx.step for idx in idl]) -1029 return range(idstart, idstop, idstep) -1030 -1031 return sorted(set.intersection(*[set(o) for o in idl])) -1032 +1010 +1011def _intersection_idx(idl): +1012 """Returns the intersection of all lists in idl as sorted list +1013 +1014 Parameters +1015 ---------- +1016 idl : list +1017 List of lists or ranges. +1018 """ +1019 +1020 def _lcm(*args): +1021 """Returns the lowest common multiple of args. +1022 +1023 From python 3.9 onwards the math library contains an lcm function.""" +1024 return reduce(lambda a, b: a * b // gcd(a, b), args) +1025 +1026 # Use groupby to efficiently check whether all elements of idl are identical +1027 try: +1028 g = groupby(idl) +1029 if next(g, True) and not next(g, False): +1030 return idl[0] +1031 except Exception: +1032 pass 1033 -1034def _expand_deltas_for_merge(deltas, idx, shape, new_idx): -1035 """Expand deltas defined on idx to the list of configs that is defined by new_idx. -1036 New, empty entries are filled by 0. If idx and new_idx are of type range, the smallest -1037 common divisor of the step sizes is used as new step size. -1038 -1039 Parameters -1040 ---------- -1041 deltas : list -1042 List of fluctuations -1043 idx : list -1044 List or range of configs on which the deltas are defined. -1045 Has to be a subset of new_idx and has to be sorted in ascending order. -1046 shape : list -1047 Number of configs in idx. -1048 new_idx : list -1049 List of configs that defines the new range, has to be sorted in ascending order. -1050 """ -1051 -1052 if type(idx) is range and type(new_idx) is range: -1053 if idx == new_idx: -1054 return deltas -1055 ret = np.zeros(new_idx[-1] - new_idx[0] + 1) -1056 for i in range(shape): -1057 ret[idx[i] - new_idx[0]] = deltas[i] -1058 return np.array([ret[new_idx[i] - new_idx[0]] for i in range(len(new_idx))]) -1059 -1060 -1061def _collapse_deltas_for_merge(deltas, idx, shape, new_idx): -1062 """Collapse deltas defined on idx to the list of configs that is defined by new_idx. -1063 If idx and new_idx are of type range, the smallest -1064 common divisor of the step sizes is used as new step size. -1065 -1066 Parameters -1067 ---------- -1068 deltas : list -1069 List of fluctuations -1070 idx : list -1071 List or range of configs on which the deltas are defined. -1072 Has to be a subset of new_idx and has to be sorted in ascending order. -1073 shape : list -1074 Number of configs in idx. -1075 new_idx : list -1076 List of configs that defines the new range, has to be sorted in ascending order. -1077 """ -1078 -1079 if type(idx) is range and type(new_idx) is range: -1080 if idx == new_idx: -1081 return deltas -1082 ret = np.zeros(new_idx[-1] - new_idx[0] + 1) -1083 for i in range(shape): -1084 if idx[i] in new_idx: -1085 ret[idx[i] - new_idx[0]] = deltas[i] -1086 return np.array([ret[new_idx[i] - new_idx[0]] for i in range(len(new_idx))]) -1087 +1034 if np.all([type(idx) is range for idx in idl]): +1035 if len(set([idx[0] for idx in idl])) == 1: +1036 idstart = max([idx.start for idx in idl]) +1037 idstop = min([idx.stop for idx in idl]) +1038 idstep = _lcm(*[idx.step for idx in idl]) +1039 return range(idstart, idstop, idstep) +1040 +1041 return sorted(set.intersection(*[set(o) for o in idl])) +1042 +1043 +1044def _expand_deltas_for_merge(deltas, idx, shape, new_idx): +1045 """Expand deltas defined on idx to the list of configs that is defined by new_idx. +1046 New, empty entries are filled by 0. If idx and new_idx are of type range, the smallest +1047 common divisor of the step sizes is used as new step size. +1048 +1049 Parameters +1050 ---------- +1051 deltas : list +1052 List of fluctuations +1053 idx : list +1054 List or range of configs on which the deltas are defined. +1055 Has to be a subset of new_idx and has to be sorted in ascending order. +1056 shape : list +1057 Number of configs in idx. +1058 new_idx : list +1059 List of configs that defines the new range, has to be sorted in ascending order. +1060 """ +1061 +1062 if type(idx) is range and type(new_idx) is range: +1063 if idx == new_idx: +1064 return deltas +1065 ret = np.zeros(new_idx[-1] - new_idx[0] + 1) +1066 for i in range(shape): +1067 ret[idx[i] - new_idx[0]] = deltas[i] +1068 return np.array([ret[new_idx[i] - new_idx[0]] for i in range(len(new_idx))]) +1069 +1070 +1071def _collapse_deltas_for_merge(deltas, idx, shape, new_idx): +1072 """Collapse deltas defined on idx to the list of configs that is defined by new_idx. +1073 If idx and new_idx are of type range, the smallest +1074 common divisor of the step sizes is used as new step size. +1075 +1076 Parameters +1077 ---------- +1078 deltas : list +1079 List of fluctuations +1080 idx : list +1081 List or range of configs on which the deltas are defined. +1082 Has to be a subset of new_idx and has to be sorted in ascending order. +1083 shape : list +1084 Number of configs in idx. +1085 new_idx : list +1086 List of configs that defines the new range, has to be sorted in ascending order. +1087 """ 1088 -1089def _filter_zeroes(deltas, idx, eps=Obs.filter_eps): -1090 """Filter out all configurations with vanishing fluctuation such that they do not -1091 contribute to the error estimate anymore. Returns the new deltas and -1092 idx according to the filtering. -1093 A fluctuation is considered to be vanishing, if it is smaller than eps times -1094 the mean of the absolute values of all deltas in one list. -1095 -1096 Parameters -1097 ---------- -1098 deltas : list -1099 List of fluctuations -1100 idx : list -1101 List or ranges of configs on which the deltas are defined. -1102 eps : float -1103 Prefactor that enters the filter criterion. -1104 """ -1105 new_deltas = [] -1106 new_idx = [] -1107 maxd = np.mean(np.fabs(deltas)) -1108 for i in range(len(deltas)): -1109 if abs(deltas[i]) > eps * maxd: -1110 new_deltas.append(deltas[i]) -1111 new_idx.append(idx[i]) -1112 if new_idx: -1113 return np.array(new_deltas), new_idx -1114 else: -1115 return deltas, idx -1116 -1117 -1118def derived_observable(func, data, array_mode=False, **kwargs): -1119 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. -1120 -1121 Parameters -1122 ---------- -1123 func : object -1124 arbitrary function of the form func(data, **kwargs). For the -1125 automatic differentiation to work, all numpy functions have to have -1126 the autograd wrapper (use 'import autograd.numpy as anp'). -1127 data : list -1128 list of Obs, e.g. [obs1, obs2, obs3]. -1129 num_grad : bool -1130 if True, numerical derivatives are used instead of autograd -1131 (default False). To control the numerical differentiation the -1132 kwargs of numdifftools.step_generators.MaxStepGenerator -1133 can be used. -1134 man_grad : list -1135 manually supply a list or an array which contains the jacobian -1136 of func. Use cautiously, supplying the wrong derivative will -1137 not be intercepted. -1138 -1139 Notes -1140 ----- -1141 For simple mathematical operations it can be practical to use anonymous -1142 functions. For the ratio of two observables one can e.g. use -1143 -1144 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) -1145 """ -1146 -1147 data = np.asarray(data) -1148 raveled_data = data.ravel() -1149 -1150 # Workaround for matrix operations containing non Obs data -1151 if not all(isinstance(x, Obs) for x in raveled_data): -1152 for i in range(len(raveled_data)): -1153 if isinstance(raveled_data[i], (int, float)): -1154 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") -1155 -1156 allcov = {} -1157 for o in raveled_data: -1158 for name in o.cov_names: -1159 if name in allcov: -1160 if not np.allclose(allcov[name], o.covobs[name].cov): -1161 raise Exception('Inconsistent covariance matrices for %s!' % (name)) -1162 else: -1163 allcov[name] = o.covobs[name].cov -1164 -1165 n_obs = len(raveled_data) -1166 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) -1167 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) -1168 new_sample_names = sorted(set(new_names) - set(new_cov_names)) -1169 -1170 is_merged = {name: (len(list(filter(lambda o: o.is_merged.get(name, False) is True, raveled_data))) > 0) for name in new_sample_names} -1171 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 -1172 -1173 if data.ndim == 1: -1174 values = np.array([o.value for o in data]) -1175 else: -1176 values = np.vectorize(lambda x: x.value)(data) -1177 -1178 new_values = func(values, **kwargs) +1089 if type(idx) is range and type(new_idx) is range: +1090 if idx == new_idx: +1091 return deltas +1092 ret = np.zeros(new_idx[-1] - new_idx[0] + 1) +1093 for i in range(shape): +1094 if idx[i] in new_idx: +1095 ret[idx[i] - new_idx[0]] = deltas[i] +1096 return np.array([ret[new_idx[i] - new_idx[0]] for i in range(len(new_idx))]) +1097 +1098 +1099def _filter_zeroes(deltas, idx, eps=Obs.filter_eps): +1100 """Filter out all configurations with vanishing fluctuation such that they do not +1101 contribute to the error estimate anymore. Returns the new deltas and +1102 idx according to the filtering. +1103 A fluctuation is considered to be vanishing, if it is smaller than eps times +1104 the mean of the absolute values of all deltas in one list. +1105 +1106 Parameters +1107 ---------- +1108 deltas : list +1109 List of fluctuations +1110 idx : list +1111 List or ranges of configs on which the deltas are defined. +1112 eps : float +1113 Prefactor that enters the filter criterion. +1114 """ +1115 new_deltas = [] +1116 new_idx = [] +1117 maxd = np.mean(np.fabs(deltas)) +1118 for i in range(len(deltas)): +1119 if abs(deltas[i]) > eps * maxd: +1120 new_deltas.append(deltas[i]) +1121 new_idx.append(idx[i]) +1122 if new_idx: +1123 return np.array(new_deltas), new_idx +1124 else: +1125 return deltas, idx +1126 +1127 +1128def derived_observable(func, data, array_mode=False, **kwargs): +1129 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. +1130 +1131 Parameters +1132 ---------- +1133 func : object +1134 arbitrary function of the form func(data, **kwargs). For the +1135 automatic differentiation to work, all numpy functions have to have +1136 the autograd wrapper (use 'import autograd.numpy as anp'). +1137 data : list +1138 list of Obs, e.g. [obs1, obs2, obs3]. +1139 num_grad : bool +1140 if True, numerical derivatives are used instead of autograd +1141 (default False). To control the numerical differentiation the +1142 kwargs of numdifftools.step_generators.MaxStepGenerator +1143 can be used. +1144 man_grad : list +1145 manually supply a list or an array which contains the jacobian +1146 of func. Use cautiously, supplying the wrong derivative will +1147 not be intercepted. +1148 +1149 Notes +1150 ----- +1151 For simple mathematical operations it can be practical to use anonymous +1152 functions. For the ratio of two observables one can e.g. use +1153 +1154 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) +1155 """ +1156 +1157 data = np.asarray(data) +1158 raveled_data = data.ravel() +1159 +1160 # Workaround for matrix operations containing non Obs data +1161 if not all(isinstance(x, Obs) for x in raveled_data): +1162 for i in range(len(raveled_data)): +1163 if isinstance(raveled_data[i], (int, float)): +1164 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") +1165 +1166 allcov = {} +1167 for o in raveled_data: +1168 for name in o.cov_names: +1169 if name in allcov: +1170 if not np.allclose(allcov[name], o.covobs[name].cov): +1171 raise Exception('Inconsistent covariance matrices for %s!' % (name)) +1172 else: +1173 allcov[name] = o.covobs[name].cov +1174 +1175 n_obs = len(raveled_data) +1176 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) +1177 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) +1178 new_sample_names = sorted(set(new_names) - set(new_cov_names)) 1179 -1180 multi = int(isinstance(new_values, np.ndarray)) -1181 -1182 new_r_values = {} -1183 new_idl_d = {} -1184 for name in new_sample_names: -1185 idl = [] -1186 tmp_values = np.zeros(n_obs) -1187 for i, item in enumerate(raveled_data): -1188 tmp_values[i] = item.r_values.get(name, item.value) -1189 tmp_idl = item.idl.get(name) -1190 if tmp_idl is not None: -1191 idl.append(tmp_idl) -1192 if multi > 0: -1193 tmp_values = np.array(tmp_values).reshape(data.shape) -1194 new_r_values[name] = func(tmp_values, **kwargs) -1195 new_idl_d[name] = _merge_idx(idl) -1196 if not is_merged[name]: -1197 is_merged[name] = (1 != len(set([len(idx) for idx in [*idl, new_idl_d[name]]]))) -1198 -1199 if 'man_grad' in kwargs: -1200 deriv = np.asarray(kwargs.get('man_grad')) -1201 if new_values.shape + data.shape != deriv.shape: -1202 raise Exception('Manual derivative does not have correct shape.') -1203 elif kwargs.get('num_grad') is True: -1204 if multi > 0: -1205 raise Exception('Multi mode currently not supported for numerical derivative') -1206 options = { -1207 'base_step': 0.1, -1208 'step_ratio': 2.5} -1209 for key in options.keys(): -1210 kwarg = kwargs.get(key) -1211 if kwarg is not None: -1212 options[key] = kwarg -1213 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) -1214 if tmp_df.size == 1: -1215 deriv = np.array([tmp_df.real]) -1216 else: -1217 deriv = tmp_df.real -1218 else: -1219 deriv = jacobian(func)(values, **kwargs) -1220 -1221 final_result = np.zeros(new_values.shape, dtype=object) -1222 -1223 if array_mode is True: -1224 -1225 class _Zero_grad(): -1226 def __init__(self, N): -1227 self.grad = np.zeros((N, 1)) -1228 -1229 new_covobs_lengths = dict(set([y for x in [[(n, o.covobs[n].N) for n in o.cov_names] for o in raveled_data] for y in x])) -1230 d_extracted = {} -1231 g_extracted = {} -1232 for name in new_sample_names: -1233 d_extracted[name] = [] -1234 ens_length = len(new_idl_d[name]) -1235 for i_dat, dat in enumerate(data): -1236 d_extracted[name].append(np.array([_expand_deltas_for_merge(o.deltas.get(name, np.zeros(ens_length)), o.idl.get(name, new_idl_d[name]), o.shape.get(name, ens_length), new_idl_d[name]) for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (ens_length, ))) -1237 for name in new_cov_names: -1238 g_extracted[name] = [] -1239 zero_grad = _Zero_grad(new_covobs_lengths[name]) -1240 for i_dat, dat in enumerate(data): -1241 g_extracted[name].append(np.array([o.covobs.get(name, zero_grad).grad for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (new_covobs_lengths[name], 1))) -1242 -1243 for i_val, new_val in np.ndenumerate(new_values): -1244 new_deltas = {} -1245 new_grad = {} -1246 if array_mode is True: -1247 for name in new_sample_names: -1248 ens_length = d_extracted[name][0].shape[-1] -1249 new_deltas[name] = np.zeros(ens_length) -1250 for i_dat, dat in enumerate(d_extracted[name]): -1251 new_deltas[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) -1252 for name in new_cov_names: -1253 new_grad[name] = 0 -1254 for i_dat, dat in enumerate(g_extracted[name]): -1255 new_grad[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) -1256 else: -1257 for j_obs, obs in np.ndenumerate(data): -1258 for name in obs.names: -1259 if name in obs.cov_names: -1260 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad -1261 else: -1262 new_deltas[name] = new_deltas.get(name, 0) + deriv[i_val + j_obs] * _expand_deltas_for_merge(obs.deltas[name], obs.idl[name], obs.shape[name], new_idl_d[name]) -1263 -1264 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} -1265 -1266 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): -1267 raise Exception('The same name has been used for deltas and covobs!') -1268 new_samples = [] -1269 new_means = [] -1270 new_idl = [] -1271 new_names_obs = [] -1272 for name in new_names: -1273 if name not in new_covobs: -1274 if is_merged[name]: -1275 filtered_deltas, filtered_idl_d = _filter_zeroes(new_deltas[name], new_idl_d[name]) -1276 else: -1277 filtered_deltas = new_deltas[name] -1278 filtered_idl_d = new_idl_d[name] -1279 -1280 new_samples.append(filtered_deltas) -1281 new_idl.append(filtered_idl_d) -1282 new_means.append(new_r_values[name][i_val]) -1283 new_names_obs.append(name) -1284 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) -1285 for name in new_covobs: -1286 final_result[i_val].names.append(name) -1287 final_result[i_val]._covobs = new_covobs -1288 final_result[i_val]._value = new_val -1289 final_result[i_val].is_merged = is_merged -1290 final_result[i_val].reweighted = reweighted -1291 -1292 if multi == 0: -1293 final_result = final_result.item() -1294 -1295 return final_result -1296 -1297 -1298def _reduce_deltas(deltas, idx_old, idx_new): -1299 """Extract deltas defined on idx_old on all configs of idx_new. -1300 -1301 Assumes, that idx_old and idx_new are correctly defined idl, i.e., they -1302 are ordered in an ascending order. -1303 -1304 Parameters -1305 ---------- -1306 deltas : list -1307 List of fluctuations -1308 idx_old : list -1309 List or range of configs on which the deltas are defined -1310 idx_new : list -1311 List of configs for which we want to extract the deltas. -1312 Has to be a subset of idx_old. -1313 """ -1314 if not len(deltas) == len(idx_old): -1315 raise Exception('Length of deltas and idx_old have to be the same: %d != %d' % (len(deltas), len(idx_old))) -1316 if type(idx_old) is range and type(idx_new) is range: -1317 if idx_old == idx_new: -1318 return deltas -1319 shape = len(idx_new) -1320 ret = np.zeros(shape) -1321 oldpos = 0 -1322 for i in range(shape): -1323 pos = -1 -1324 for j in range(oldpos, len(idx_old)): -1325 if idx_old[j] == idx_new[i]: -1326 pos = j -1327 break -1328 if pos < 0: -1329 raise Exception('Error in _reduce_deltas: Config %d not in idx_old' % (idx_new[i])) -1330 ret[i] = deltas[pos] -1331 oldpos = pos -1332 return np.array(ret) -1333 -1334 -1335def reweight(weight, obs, **kwargs): -1336 """Reweight a list of observables. -1337 -1338 Parameters -1339 ---------- -1340 weight : Obs -1341 Reweighting factor. An Observable that has to be defined on a superset of the -1342 configurations in obs[i].idl for all i. -1343 obs : list -1344 list of Obs, e.g. [obs1, obs2, obs3]. -1345 all_configs : bool -1346 if True, the reweighted observables are normalized by the average of -1347 the reweighting factor on all configurations in weight.idl and not -1348 on the configurations in obs[i].idl. Default False. -1349 """ -1350 result = [] -1351 for i in range(len(obs)): -1352 if len(obs[i].cov_names): -1353 raise Exception('Error: Not possible to reweight an Obs that contains covobs!') -1354 if not set(obs[i].names).issubset(weight.names): -1355 raise Exception('Error: Ensembles do not fit') -1356 for name in obs[i].names: -1357 if not set(obs[i].idl[name]).issubset(weight.idl[name]): -1358 raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name)) -1359 new_samples = [] -1360 w_deltas = {} -1361 for name in sorted(obs[i].names): -1362 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) -1363 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) -1364 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) -1365 -1366 if kwargs.get('all_configs'): -1367 new_weight = weight -1368 else: -1369 new_weight = Obs([w_deltas[name] + weight.r_values[name] for name in sorted(obs[i].names)], sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) -1370 -1371 result.append(tmp_obs / new_weight) -1372 result[-1].reweighted = True -1373 result[-1].is_merged = obs[i].is_merged -1374 -1375 return result -1376 -1377 -1378def correlate(obs_a, obs_b): -1379 """Correlate two observables. +1180 is_merged = {name: (len(list(filter(lambda o: o.is_merged.get(name, False) is True, raveled_data))) > 0) for name in new_sample_names} +1181 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 +1182 +1183 if data.ndim == 1: +1184 values = np.array([o.value for o in data]) +1185 else: +1186 values = np.vectorize(lambda x: x.value)(data) +1187 +1188 new_values = func(values, **kwargs) +1189 +1190 multi = int(isinstance(new_values, np.ndarray)) +1191 +1192 new_r_values = {} +1193 new_idl_d = {} +1194 for name in new_sample_names: +1195 idl = [] +1196 tmp_values = np.zeros(n_obs) +1197 for i, item in enumerate(raveled_data): +1198 tmp_values[i] = item.r_values.get(name, item.value) +1199 tmp_idl = item.idl.get(name) +1200 if tmp_idl is not None: +1201 idl.append(tmp_idl) +1202 if multi > 0: +1203 tmp_values = np.array(tmp_values).reshape(data.shape) +1204 new_r_values[name] = func(tmp_values, **kwargs) +1205 new_idl_d[name] = _merge_idx(idl) +1206 if not is_merged[name]: +1207 is_merged[name] = (1 != len(set([len(idx) for idx in [*idl, new_idl_d[name]]]))) +1208 +1209 if 'man_grad' in kwargs: +1210 deriv = np.asarray(kwargs.get('man_grad')) +1211 if new_values.shape + data.shape != deriv.shape: +1212 raise Exception('Manual derivative does not have correct shape.') +1213 elif kwargs.get('num_grad') is True: +1214 if multi > 0: +1215 raise Exception('Multi mode currently not supported for numerical derivative') +1216 options = { +1217 'base_step': 0.1, +1218 'step_ratio': 2.5} +1219 for key in options.keys(): +1220 kwarg = kwargs.get(key) +1221 if kwarg is not None: +1222 options[key] = kwarg +1223 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) +1224 if tmp_df.size == 1: +1225 deriv = np.array([tmp_df.real]) +1226 else: +1227 deriv = tmp_df.real +1228 else: +1229 deriv = jacobian(func)(values, **kwargs) +1230 +1231 final_result = np.zeros(new_values.shape, dtype=object) +1232 +1233 if array_mode is True: +1234 +1235 class _Zero_grad(): +1236 def __init__(self, N): +1237 self.grad = np.zeros((N, 1)) +1238 +1239 new_covobs_lengths = dict(set([y for x in [[(n, o.covobs[n].N) for n in o.cov_names] for o in raveled_data] for y in x])) +1240 d_extracted = {} +1241 g_extracted = {} +1242 for name in new_sample_names: +1243 d_extracted[name] = [] +1244 ens_length = len(new_idl_d[name]) +1245 for i_dat, dat in enumerate(data): +1246 d_extracted[name].append(np.array([_expand_deltas_for_merge(o.deltas.get(name, np.zeros(ens_length)), o.idl.get(name, new_idl_d[name]), o.shape.get(name, ens_length), new_idl_d[name]) for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (ens_length, ))) +1247 for name in new_cov_names: +1248 g_extracted[name] = [] +1249 zero_grad = _Zero_grad(new_covobs_lengths[name]) +1250 for i_dat, dat in enumerate(data): +1251 g_extracted[name].append(np.array([o.covobs.get(name, zero_grad).grad for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (new_covobs_lengths[name], 1))) +1252 +1253 for i_val, new_val in np.ndenumerate(new_values): +1254 new_deltas = {} +1255 new_grad = {} +1256 if array_mode is True: +1257 for name in new_sample_names: +1258 ens_length = d_extracted[name][0].shape[-1] +1259 new_deltas[name] = np.zeros(ens_length) +1260 for i_dat, dat in enumerate(d_extracted[name]): +1261 new_deltas[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) +1262 for name in new_cov_names: +1263 new_grad[name] = 0 +1264 for i_dat, dat in enumerate(g_extracted[name]): +1265 new_grad[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) +1266 else: +1267 for j_obs, obs in np.ndenumerate(data): +1268 for name in obs.names: +1269 if name in obs.cov_names: +1270 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad +1271 else: +1272 new_deltas[name] = new_deltas.get(name, 0) + deriv[i_val + j_obs] * _expand_deltas_for_merge(obs.deltas[name], obs.idl[name], obs.shape[name], new_idl_d[name]) +1273 +1274 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} +1275 +1276 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): +1277 raise Exception('The same name has been used for deltas and covobs!') +1278 new_samples = [] +1279 new_means = [] +1280 new_idl = [] +1281 new_names_obs = [] +1282 for name in new_names: +1283 if name not in new_covobs: +1284 if is_merged[name]: +1285 filtered_deltas, filtered_idl_d = _filter_zeroes(new_deltas[name], new_idl_d[name]) +1286 else: +1287 filtered_deltas = new_deltas[name] +1288 filtered_idl_d = new_idl_d[name] +1289 +1290 new_samples.append(filtered_deltas) +1291 new_idl.append(filtered_idl_d) +1292 new_means.append(new_r_values[name][i_val]) +1293 new_names_obs.append(name) +1294 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) +1295 for name in new_covobs: +1296 final_result[i_val].names.append(name) +1297 final_result[i_val]._covobs = new_covobs +1298 final_result[i_val]._value = new_val +1299 final_result[i_val].is_merged = is_merged +1300 final_result[i_val].reweighted = reweighted +1301 +1302 if multi == 0: +1303 final_result = final_result.item() +1304 +1305 return final_result +1306 +1307 +1308def _reduce_deltas(deltas, idx_old, idx_new): +1309 """Extract deltas defined on idx_old on all configs of idx_new. +1310 +1311 Assumes, that idx_old and idx_new are correctly defined idl, i.e., they +1312 are ordered in an ascending order. +1313 +1314 Parameters +1315 ---------- +1316 deltas : list +1317 List of fluctuations +1318 idx_old : list +1319 List or range of configs on which the deltas are defined +1320 idx_new : list +1321 List of configs for which we want to extract the deltas. +1322 Has to be a subset of idx_old. +1323 """ +1324 if not len(deltas) == len(idx_old): +1325 raise Exception('Length of deltas and idx_old have to be the same: %d != %d' % (len(deltas), len(idx_old))) +1326 if type(idx_old) is range and type(idx_new) is range: +1327 if idx_old == idx_new: +1328 return deltas +1329 shape = len(idx_new) +1330 ret = np.zeros(shape) +1331 oldpos = 0 +1332 for i in range(shape): +1333 pos = -1 +1334 for j in range(oldpos, len(idx_old)): +1335 if idx_old[j] == idx_new[i]: +1336 pos = j +1337 break +1338 if pos < 0: +1339 raise Exception('Error in _reduce_deltas: Config %d not in idx_old' % (idx_new[i])) +1340 ret[i] = deltas[pos] +1341 oldpos = pos +1342 return np.array(ret) +1343 +1344 +1345def reweight(weight, obs, **kwargs): +1346 """Reweight a list of observables. +1347 +1348 Parameters +1349 ---------- +1350 weight : Obs +1351 Reweighting factor. An Observable that has to be defined on a superset of the +1352 configurations in obs[i].idl for all i. +1353 obs : list +1354 list of Obs, e.g. [obs1, obs2, obs3]. +1355 all_configs : bool +1356 if True, the reweighted observables are normalized by the average of +1357 the reweighting factor on all configurations in weight.idl and not +1358 on the configurations in obs[i].idl. Default False. +1359 """ +1360 result = [] +1361 for i in range(len(obs)): +1362 if len(obs[i].cov_names): +1363 raise Exception('Error: Not possible to reweight an Obs that contains covobs!') +1364 if not set(obs[i].names).issubset(weight.names): +1365 raise Exception('Error: Ensembles do not fit') +1366 for name in obs[i].names: +1367 if not set(obs[i].idl[name]).issubset(weight.idl[name]): +1368 raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name)) +1369 new_samples = [] +1370 w_deltas = {} +1371 for name in sorted(obs[i].names): +1372 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) +1373 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) +1374 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) +1375 +1376 if kwargs.get('all_configs'): +1377 new_weight = weight +1378 else: +1379 new_weight = Obs([w_deltas[name] + weight.r_values[name] for name in sorted(obs[i].names)], sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) 1380 -1381 Parameters -1382 ---------- -1383 obs_a : Obs -1384 First observable -1385 obs_b : Obs -1386 Second observable +1381 result.append(tmp_obs / new_weight) +1382 result[-1].reweighted = True +1383 result[-1].is_merged = obs[i].is_merged +1384 +1385 return result +1386 1387 -1388 Notes -1389 ----- -1390 Keep in mind to only correlate primary observables which have not been reweighted -1391 yet. The reweighting has to be applied after correlating the observables. -1392 Currently only works if ensembles are identical (this is not strictly necessary). -1393 """ -1394 -1395 if sorted(obs_a.names) != sorted(obs_b.names): -1396 raise Exception('Ensembles do not fit') -1397 if len(obs_a.cov_names) or len(obs_b.cov_names): -1398 raise Exception('Error: Not possible to correlate Obs that contain covobs!') -1399 for name in obs_a.names: -1400 if obs_a.shape[name] != obs_b.shape[name]: -1401 raise Exception('Shapes of ensemble', name, 'do not fit') -1402 if obs_a.idl[name] != obs_b.idl[name]: -1403 raise Exception('idl of ensemble', name, 'do not fit') +1388def correlate(obs_a, obs_b): +1389 """Correlate two observables. +1390 +1391 Parameters +1392 ---------- +1393 obs_a : Obs +1394 First observable +1395 obs_b : Obs +1396 Second observable +1397 +1398 Notes +1399 ----- +1400 Keep in mind to only correlate primary observables which have not been reweighted +1401 yet. The reweighting has to be applied after correlating the observables. +1402 Currently only works if ensembles are identical (this is not strictly necessary). +1403 """ 1404 -1405 if obs_a.reweighted is True: -1406 warnings.warn("The first observable is already reweighted.", RuntimeWarning) -1407 if obs_b.reweighted is True: -1408 warnings.warn("The second observable is already reweighted.", RuntimeWarning) -1409 -1410 new_samples = [] -1411 new_idl = [] -1412 for name in sorted(obs_a.names): -1413 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) -1414 new_idl.append(obs_a.idl[name]) -1415 -1416 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) -1417 o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names} -1418 o.reweighted = obs_a.reweighted or obs_b.reweighted -1419 return o -1420 -1421 -1422def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): -1423 r'''Calculates the error covariance matrix of a set of observables. -1424 -1425 The gamma method has to be applied first to all observables. -1426 -1427 Parameters -1428 ---------- -1429 obs : list or numpy.ndarray -1430 List or one dimensional array of Obs -1431 visualize : bool -1432 If True plots the corresponding normalized correlation matrix (default False). -1433 correlation : bool -1434 If True the correlation matrix instead of the error covariance matrix is returned (default False). -1435 smooth : None or int -1436 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue -1437 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the -1438 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely -1439 small ones. -1440 -1441 Notes -1442 ----- -1443 The error covariance is defined such that it agrees with the squared standard error for two identical observables -1444 $$\operatorname{cov}(a,a)=\sum_{s=1}^N\delta_a^s\delta_a^s/N^2=\Gamma_{aa}(0)/N=\operatorname{var}(a)/N=\sigma_a^2$$ -1445 in the absence of autocorrelation. -1446 The error covariance is estimated by calculating the correlation matrix assuming no autocorrelation and then rescaling the correlation matrix by the full errors including the previous gamma method estimate for the autocorrelation of the observables. The covariance at windowsize 0 is guaranteed to be positive semi-definite -1447 $$\sum_{i,j}v_i\Gamma_{ij}(0)v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i,j}v_i\delta_i^s\delta_j^s v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i}|v_i\delta_i^s|^2\geq 0\,,$$ for every $v\in\mathbb{R}^M$, while such an identity does not hold for larger windows/lags. -1448 For observables defined on a single ensemble our approximation is equivalent to assuming that the integrated autocorrelation time of an off-diagonal element is equal to the geometric mean of the integrated autocorrelation times of the corresponding diagonal elements. -1449 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ -1450 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). -1451 ''' -1452 -1453 length = len(obs) -1454 -1455 max_samples = np.max([o.N for o in obs]) -1456 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: -1457 warnings.warn(f"The dimension of the covariance matrix ({length}) is larger or equal to the number of samples ({max_samples}). This will result in a rank deficient matrix.", RuntimeWarning) -1458 -1459 cov = np.zeros((length, length)) -1460 for i in range(length): -1461 for j in range(i, length): -1462 cov[i, j] = _covariance_element(obs[i], obs[j]) -1463 cov = cov + cov.T - np.diag(np.diag(cov)) +1405 if sorted(obs_a.names) != sorted(obs_b.names): +1406 raise Exception('Ensembles do not fit') +1407 if len(obs_a.cov_names) or len(obs_b.cov_names): +1408 raise Exception('Error: Not possible to correlate Obs that contain covobs!') +1409 for name in obs_a.names: +1410 if obs_a.shape[name] != obs_b.shape[name]: +1411 raise Exception('Shapes of ensemble', name, 'do not fit') +1412 if obs_a.idl[name] != obs_b.idl[name]: +1413 raise Exception('idl of ensemble', name, 'do not fit') +1414 +1415 if obs_a.reweighted is True: +1416 warnings.warn("The first observable is already reweighted.", RuntimeWarning) +1417 if obs_b.reweighted is True: +1418 warnings.warn("The second observable is already reweighted.", RuntimeWarning) +1419 +1420 new_samples = [] +1421 new_idl = [] +1422 for name in sorted(obs_a.names): +1423 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) +1424 new_idl.append(obs_a.idl[name]) +1425 +1426 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) +1427 o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names} +1428 o.reweighted = obs_a.reweighted or obs_b.reweighted +1429 return o +1430 +1431 +1432def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): +1433 r'''Calculates the error covariance matrix of a set of observables. +1434 +1435 The gamma method has to be applied first to all observables. +1436 +1437 Parameters +1438 ---------- +1439 obs : list or numpy.ndarray +1440 List or one dimensional array of Obs +1441 visualize : bool +1442 If True plots the corresponding normalized correlation matrix (default False). +1443 correlation : bool +1444 If True the correlation matrix instead of the error covariance matrix is returned (default False). +1445 smooth : None or int +1446 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue +1447 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the +1448 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely +1449 small ones. +1450 +1451 Notes +1452 ----- +1453 The error covariance is defined such that it agrees with the squared standard error for two identical observables +1454 $$\operatorname{cov}(a,a)=\sum_{s=1}^N\delta_a^s\delta_a^s/N^2=\Gamma_{aa}(0)/N=\operatorname{var}(a)/N=\sigma_a^2$$ +1455 in the absence of autocorrelation. +1456 The error covariance is estimated by calculating the correlation matrix assuming no autocorrelation and then rescaling the correlation matrix by the full errors including the previous gamma method estimate for the autocorrelation of the observables. The covariance at windowsize 0 is guaranteed to be positive semi-definite +1457 $$\sum_{i,j}v_i\Gamma_{ij}(0)v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i,j}v_i\delta_i^s\delta_j^s v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i}|v_i\delta_i^s|^2\geq 0\,,$$ for every $v\in\mathbb{R}^M$, while such an identity does not hold for larger windows/lags. +1458 For observables defined on a single ensemble our approximation is equivalent to assuming that the integrated autocorrelation time of an off-diagonal element is equal to the geometric mean of the integrated autocorrelation times of the corresponding diagonal elements. +1459 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ +1460 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). +1461 ''' +1462 +1463 length = len(obs) 1464 -1465 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) -1466 -1467 if isinstance(smooth, int): -1468 corr = _smooth_eigenvalues(corr, smooth) -1469 -1470 if visualize: -1471 plt.matshow(corr, vmin=-1, vmax=1) -1472 plt.set_cmap('RdBu') -1473 plt.colorbar() -1474 plt.draw() -1475 -1476 if correlation is True: -1477 return corr -1478 -1479 errors = [o.dvalue for o in obs] -1480 cov = np.diag(errors) @ corr @ np.diag(errors) -1481 -1482 eigenvalues = np.linalg.eigh(cov)[0] -1483 if not np.all(eigenvalues >= 0): -1484 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning) +1465 max_samples = np.max([o.N for o in obs]) +1466 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: +1467 warnings.warn(f"The dimension of the covariance matrix ({length}) is larger or equal to the number of samples ({max_samples}). This will result in a rank deficient matrix.", RuntimeWarning) +1468 +1469 cov = np.zeros((length, length)) +1470 for i in range(length): +1471 for j in range(i, length): +1472 cov[i, j] = _covariance_element(obs[i], obs[j]) +1473 cov = cov + cov.T - np.diag(np.diag(cov)) +1474 +1475 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) +1476 +1477 if isinstance(smooth, int): +1478 corr = _smooth_eigenvalues(corr, smooth) +1479 +1480 if visualize: +1481 plt.matshow(corr, vmin=-1, vmax=1) +1482 plt.set_cmap('RdBu') +1483 plt.colorbar() +1484 plt.draw() 1485 -1486 return cov -1487 +1486 if correlation is True: +1487 return corr 1488 -1489def _smooth_eigenvalues(corr, E): -1490 """Eigenvalue smoothing as described in hep-lat/9412087 +1489 errors = [o.dvalue for o in obs] +1490 cov = np.diag(errors) @ corr @ np.diag(errors) 1491 -1492 corr : np.ndarray -1493 correlation matrix -1494 E : integer -1495 Number of eigenvalues to be left substantially unchanged -1496 """ -1497 if not (2 < E < corr.shape[0] - 1): -1498 raise Exception(f"'E' has to be between 2 and the dimension of the correlation matrix minus 1 ({corr.shape[0] - 1}).") -1499 vals, vec = np.linalg.eigh(corr) -1500 lambda_min = np.mean(vals[:-E]) -1501 vals[vals < lambda_min] = lambda_min -1502 vals /= np.mean(vals) -1503 return vec @ np.diag(vals) @ vec.T -1504 -1505 -1506def _covariance_element(obs1, obs2): -1507 """Estimates the covariance of two Obs objects, neglecting autocorrelations.""" -1508 -1509 def calc_gamma(deltas1, deltas2, idx1, idx2, new_idx): -1510 deltas1 = _collapse_deltas_for_merge(deltas1, idx1, len(idx1), new_idx) -1511 deltas2 = _collapse_deltas_for_merge(deltas2, idx2, len(idx2), new_idx) -1512 return np.sum(deltas1 * deltas2) -1513 -1514 if set(obs1.names).isdisjoint(set(obs2.names)): -1515 return 0.0 -1516 -1517 if not hasattr(obs1, 'e_dvalue') or not hasattr(obs2, 'e_dvalue'): -1518 raise Exception('The gamma method has to be applied to both Obs first.') -1519 -1520 dvalue = 0.0 -1521 -1522 for e_name in obs1.mc_names: +1492 eigenvalues = np.linalg.eigh(cov)[0] +1493 if not np.all(eigenvalues >= 0): +1494 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning) +1495 +1496 return cov +1497 +1498 +1499def _smooth_eigenvalues(corr, E): +1500 """Eigenvalue smoothing as described in hep-lat/9412087 +1501 +1502 corr : np.ndarray +1503 correlation matrix +1504 E : integer +1505 Number of eigenvalues to be left substantially unchanged +1506 """ +1507 if not (2 < E < corr.shape[0] - 1): +1508 raise Exception(f"'E' has to be between 2 and the dimension of the correlation matrix minus 1 ({corr.shape[0] - 1}).") +1509 vals, vec = np.linalg.eigh(corr) +1510 lambda_min = np.mean(vals[:-E]) +1511 vals[vals < lambda_min] = lambda_min +1512 vals /= np.mean(vals) +1513 return vec @ np.diag(vals) @ vec.T +1514 +1515 +1516def _covariance_element(obs1, obs2): +1517 """Estimates the covariance of two Obs objects, neglecting autocorrelations.""" +1518 +1519 def calc_gamma(deltas1, deltas2, idx1, idx2, new_idx): +1520 deltas1 = _collapse_deltas_for_merge(deltas1, idx1, len(idx1), new_idx) +1521 deltas2 = _collapse_deltas_for_merge(deltas2, idx2, len(idx2), new_idx) +1522 return np.sum(deltas1 * deltas2) 1523 -1524 if e_name not in obs2.mc_names: -1525 continue +1524 if set(obs1.names).isdisjoint(set(obs2.names)): +1525 return 0.0 1526 -1527 idl_d = {} -1528 for r_name in obs1.e_content[e_name]: -1529 if r_name not in obs2.e_content[e_name]: -1530 continue -1531 idl_d[r_name] = _intersection_idx([obs1.idl[r_name], obs2.idl[r_name]]) -1532 -1533 gamma = 0.0 -1534 -1535 for r_name in obs1.e_content[e_name]: -1536 if r_name not in obs2.e_content[e_name]: -1537 continue -1538 if len(idl_d[r_name]) == 0: -1539 continue -1540 gamma += calc_gamma(obs1.deltas[r_name], obs2.deltas[r_name], obs1.idl[r_name], obs2.idl[r_name], idl_d[r_name]) -1541 -1542 if gamma == 0.0: -1543 continue +1527 if not hasattr(obs1, 'e_dvalue') or not hasattr(obs2, 'e_dvalue'): +1528 raise Exception('The gamma method has to be applied to both Obs first.') +1529 +1530 dvalue = 0.0 +1531 +1532 for e_name in obs1.mc_names: +1533 +1534 if e_name not in obs2.mc_names: +1535 continue +1536 +1537 idl_d = {} +1538 for r_name in obs1.e_content[e_name]: +1539 if r_name not in obs2.e_content[e_name]: +1540 continue +1541 idl_d[r_name] = _intersection_idx([obs1.idl[r_name], obs2.idl[r_name]]) +1542 +1543 gamma = 0.0 1544 -1545 gamma_div = 0.0 -1546 for r_name in obs1.e_content[e_name]: -1547 if r_name not in obs2.e_content[e_name]: -1548 continue -1549 if len(idl_d[r_name]) == 0: -1550 continue -1551 gamma_div += np.sqrt(calc_gamma(obs1.deltas[r_name], obs1.deltas[r_name], obs1.idl[r_name], obs1.idl[r_name], idl_d[r_name]) * calc_gamma(obs2.deltas[r_name], obs2.deltas[r_name], obs2.idl[r_name], obs2.idl[r_name], idl_d[r_name])) -1552 gamma /= gamma_div -1553 -1554 dvalue += gamma -1555 -1556 for e_name in obs1.cov_names: -1557 -1558 if e_name not in obs2.cov_names: -1559 continue -1560 -1561 dvalue += float(np.dot(np.transpose(obs1.covobs[e_name].grad), np.dot(obs1.covobs[e_name].cov, obs2.covobs[e_name].grad))) -1562 -1563 return dvalue -1564 +1545 for r_name in obs1.e_content[e_name]: +1546 if r_name not in obs2.e_content[e_name]: +1547 continue +1548 if len(idl_d[r_name]) == 0: +1549 continue +1550 gamma += calc_gamma(obs1.deltas[r_name], obs2.deltas[r_name], obs1.idl[r_name], obs2.idl[r_name], idl_d[r_name]) +1551 +1552 if gamma == 0.0: +1553 continue +1554 +1555 gamma_div = 0.0 +1556 for r_name in obs1.e_content[e_name]: +1557 if r_name not in obs2.e_content[e_name]: +1558 continue +1559 if len(idl_d[r_name]) == 0: +1560 continue +1561 gamma_div += np.sqrt(calc_gamma(obs1.deltas[r_name], obs1.deltas[r_name], obs1.idl[r_name], obs1.idl[r_name], idl_d[r_name]) * calc_gamma(obs2.deltas[r_name], obs2.deltas[r_name], obs2.idl[r_name], obs2.idl[r_name], idl_d[r_name])) +1562 gamma /= gamma_div +1563 +1564 dvalue += gamma 1565 -1566def import_jackknife(jacks, name, idl=None): -1567 """Imports jackknife samples and returns an Obs -1568 -1569 Parameters -1570 ---------- -1571 jacks : numpy.ndarray -1572 numpy array containing the mean value as zeroth entry and -1573 the N jackknife samples as first to Nth entry. -1574 name : str -1575 name of the ensemble the samples are defined on. -1576 """ -1577 length = len(jacks) - 1 -1578 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) -1579 samples = jacks[1:] @ prj -1580 mean = np.mean(samples) -1581 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) -1582 new_obs._value = jacks[0] -1583 return new_obs -1584 -1585 -1586def merge_obs(list_of_obs): -1587 """Combine all observables in list_of_obs into one new observable -1588 -1589 Parameters -1590 ---------- -1591 list_of_obs : list -1592 list of the Obs object to be combined -1593 -1594 Notes -1595 ----- -1596 It is not possible to combine obs which are based on the same replicum -1597 """ -1598 replist = [item for obs in list_of_obs for item in obs.names] -1599 if (len(replist) == len(set(replist))) is False: -1600 raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist))) -1601 if any([len(o.cov_names) for o in list_of_obs]): -1602 raise Exception('Not possible to merge data that contains covobs!') -1603 new_dict = {} -1604 idl_dict = {} -1605 for o in list_of_obs: -1606 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) -1607 for key in set(o.deltas) | set(o.r_values)}) -1608 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) -1609 -1610 names = sorted(new_dict.keys()) -1611 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) -1612 o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names} -1613 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) -1614 return o -1615 -1616 -1617def cov_Obs(means, cov, name, grad=None): -1618 """Create an Obs based on mean(s) and a covariance matrix +1566 for e_name in obs1.cov_names: +1567 +1568 if e_name not in obs2.cov_names: +1569 continue +1570 +1571 dvalue += float(np.dot(np.transpose(obs1.covobs[e_name].grad), np.dot(obs1.covobs[e_name].cov, obs2.covobs[e_name].grad))) +1572 +1573 return dvalue +1574 +1575 +1576def import_jackknife(jacks, name, idl=None): +1577 """Imports jackknife samples and returns an Obs +1578 +1579 Parameters +1580 ---------- +1581 jacks : numpy.ndarray +1582 numpy array containing the mean value as zeroth entry and +1583 the N jackknife samples as first to Nth entry. +1584 name : str +1585 name of the ensemble the samples are defined on. +1586 """ +1587 length = len(jacks) - 1 +1588 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) +1589 samples = jacks[1:] @ prj +1590 mean = np.mean(samples) +1591 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) +1592 new_obs._value = jacks[0] +1593 return new_obs +1594 +1595 +1596def merge_obs(list_of_obs): +1597 """Combine all observables in list_of_obs into one new observable +1598 +1599 Parameters +1600 ---------- +1601 list_of_obs : list +1602 list of the Obs object to be combined +1603 +1604 Notes +1605 ----- +1606 It is not possible to combine obs which are based on the same replicum +1607 """ +1608 replist = [item for obs in list_of_obs for item in obs.names] +1609 if (len(replist) == len(set(replist))) is False: +1610 raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist))) +1611 if any([len(o.cov_names) for o in list_of_obs]): +1612 raise Exception('Not possible to merge data that contains covobs!') +1613 new_dict = {} +1614 idl_dict = {} +1615 for o in list_of_obs: +1616 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) +1617 for key in set(o.deltas) | set(o.r_values)}) +1618 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) 1619 -1620 Parameters -1621 ---------- -1622 mean : list of floats or float -1623 N mean value(s) of the new Obs -1624 cov : list or array -1625 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance -1626 name : str -1627 identifier for the covariance matrix -1628 grad : list or array -1629 Gradient of the Covobs wrt. the means belonging to cov. -1630 """ -1631 -1632 def covobs_to_obs(co): -1633 """Make an Obs out of a Covobs -1634 -1635 Parameters -1636 ---------- -1637 co : Covobs -1638 Covobs to be embedded into the Obs -1639 """ -1640 o = Obs([], [], means=[]) -1641 o._value = co.value -1642 o.names.append(co.name) -1643 o._covobs[co.name] = co -1644 o._dvalue = np.sqrt(co.errsq()) -1645 return o -1646 -1647 ol = [] -1648 if isinstance(means, (float, int)): -1649 means = [means] -1650 -1651 for i in range(len(means)): -1652 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) -1653 if ol[0].covobs[name].N != len(means): -1654 raise Exception('You have to provide %d mean values!' % (ol[0].N)) -1655 if len(ol) == 1: -1656 return ol[0] -1657 return ol +1620 names = sorted(new_dict.keys()) +1621 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) +1622 o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names} +1623 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) +1624 return o +1625 +1626 +1627def cov_Obs(means, cov, name, grad=None): +1628 """Create an Obs based on mean(s) and a covariance matrix +1629 +1630 Parameters +1631 ---------- +1632 mean : list of floats or float +1633 N mean value(s) of the new Obs +1634 cov : list or array +1635 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance +1636 name : str +1637 identifier for the covariance matrix +1638 grad : list or array +1639 Gradient of the Covobs wrt. the means belonging to cov. +1640 """ +1641 +1642 def covobs_to_obs(co): +1643 """Make an Obs out of a Covobs +1644 +1645 Parameters +1646 ---------- +1647 co : Covobs +1648 Covobs to be embedded into the Obs +1649 """ +1650 o = Obs([], [], means=[]) +1651 o._value = co.value +1652 o.names.append(co.name) +1653 o._covobs[co.name] = co +1654 o._dvalue = np.sqrt(co.errsq()) +1655 return o +1656 +1657 ol = [] +1658 if isinstance(means, (float, int)): +1659 means = [means] +1660 +1661 for i in range(len(means)): +1662 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) +1663 if ol[0].covobs[name].N != len(means): +1664 raise Exception('You have to provide %d mean values!' % (ol[0].N)) +1665 if len(ol) == 1: +1666 return ol[0] +1667 return ol
16class Obs: - 17 """Class for a general observable. - 18 - 19 Instances of Obs are the basic objects of a pyerrors error analysis. - 20 They are initialized with a list which contains arrays of samples for - 21 different ensembles/replica and another list of same length which contains - 22 the names of the ensembles/replica. Mathematical operations can be - 23 performed on instances. The result is another instance of Obs. The error of - 24 an instance can be computed with the gamma_method. Also contains additional - 25 methods for output and visualization of the error calculation. - 26 - 27 Attributes - 28 ---------- - 29 S_global : float - 30 Standard value for S (default 2.0) - 31 S_dict : dict - 32 Dictionary for S values. If an entry for a given ensemble - 33 exists this overwrites the standard value for that ensemble. - 34 tau_exp_global : float - 35 Standard value for tau_exp (default 0.0) - 36 tau_exp_dict : dict - 37 Dictionary for tau_exp values. If an entry for a given ensemble exists - 38 this overwrites the standard value for that ensemble. - 39 N_sigma_global : float - 40 Standard value for N_sigma (default 1.0) - 41 N_sigma_dict : dict - 42 Dictionary for N_sigma values. If an entry for a given ensemble exists - 43 this overwrites the standard value for that ensemble. - 44 """ - 45 __slots__ = ['names', 'shape', 'r_values', 'deltas', 'N', '_value', '_dvalue', - 46 'ddvalue', 'reweighted', 'S', 'tau_exp', 'N_sigma', - 47 'e_dvalue', 'e_ddvalue', 'e_tauint', 'e_dtauint', - 48 'e_windowsize', 'e_rho', 'e_drho', 'e_n_tauint', 'e_n_dtauint', - 49 'idl', 'is_merged', 'tag', '_covobs', '__dict__'] - 50 - 51 S_global = 2.0 - 52 S_dict = {} - 53 tau_exp_global = 0.0 - 54 tau_exp_dict = {} - 55 N_sigma_global = 1.0 - 56 N_sigma_dict = {} - 57 filter_eps = 1e-10 - 58 - 59 def __init__(self, samples, names, idl=None, **kwargs): - 60 """ Initialize Obs object. - 61 - 62 Parameters - 63 ---------- - 64 samples : list - 65 list of numpy arrays containing the Monte Carlo samples - 66 names : list - 67 list of strings labeling the individual samples - 68 idl : list, optional - 69 list of ranges or lists on which the samples are defined - 70 """ - 71 - 72 if kwargs.get("means") is None and len(samples): - 73 if len(samples) != len(names): - 74 raise Exception('Length of samples and names incompatible.') - 75 if idl is not None: - 76 if len(idl) != len(names): - 77 raise Exception('Length of idl incompatible with samples and names.') - 78 name_length = len(names) - 79 if name_length > 1: - 80 if name_length != len(set(names)): - 81 raise Exception('names are not unique.') - 82 if not all(isinstance(x, str) for x in names): - 83 raise TypeError('All names have to be strings.') - 84 else: - 85 if not isinstance(names[0], str): - 86 raise TypeError('All names have to be strings.') - 87 if min(len(x) for x in samples) <= 4: - 88 raise Exception('Samples have to have at least 5 entries.') - 89 - 90 self.names = sorted(names) - 91 self.shape = {} - 92 self.r_values = {} - 93 self.deltas = {} - 94 self._covobs = {} - 95 - 96 self._value = 0 - 97 self.N = 0 - 98 self.is_merged = {} - 99 self.idl = {} -100 if idl is not None: -101 for name, idx in sorted(zip(names, idl)): -102 if isinstance(idx, range): -103 self.idl[name] = idx -104 elif isinstance(idx, (list, np.ndarray)): -105 dc = np.unique(np.diff(idx)) -106 if np.any(dc < 0): -107 raise Exception("Unsorted idx for idl[%s]" % (name)) -108 if len(dc) == 1: -109 self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0]) -110 else: -111 self.idl[name] = list(idx) -112 else: -113 raise Exception('incompatible type for idl[%s].' % (name)) -114 else: -115 for name, sample in sorted(zip(names, samples)): -116 self.idl[name] = range(1, len(sample) + 1) -117 -118 if kwargs.get("means") is not None: -119 for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))): -120 self.shape[name] = len(self.idl[name]) -121 self.N += self.shape[name] -122 self.r_values[name] = mean -123 self.deltas[name] = sample -124 else: -125 for name, sample in sorted(zip(names, samples)): -126 self.shape[name] = len(self.idl[name]) -127 self.N += self.shape[name] -128 if len(sample) != self.shape[name]: -129 raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name])) -130 self.r_values[name] = np.mean(sample) -131 self.deltas[name] = sample - self.r_values[name] -132 self._value += self.shape[name] * self.r_values[name] -133 self._value /= self.N -134 -135 self._dvalue = 0.0 -136 self.ddvalue = 0.0 -137 self.reweighted = False -138 -139 self.tag = None -140 -141 @property -142 def value(self): -143 return self._value -144 -145 @property -146 def dvalue(self): -147 return self._dvalue -148 -149 @property -150 def e_names(self): -151 return sorted(set([o.split('|')[0] for o in self.names])) -152 -153 @property -154 def cov_names(self): -155 return sorted(set([o for o in self.covobs.keys()])) -156 -157 @property -158 def mc_names(self): -159 return sorted(set([o.split('|')[0] for o in self.names if o not in self.cov_names])) -160 -161 @property -162 def e_content(self): -163 res = {} -164 for e, e_name in enumerate(self.e_names): -165 res[e_name] = sorted(filter(lambda x: x.startswith(e_name + '|'), self.names)) -166 if e_name in self.names: -167 res[e_name].append(e_name) -168 return res -169 -170 @property -171 def covobs(self): -172 return self._covobs -173 -174 def gamma_method(self, **kwargs): -175 """Estimate the error and related properties of the Obs. -176 -177 Parameters -178 ---------- -179 S : float -180 specifies a custom value for the parameter S (default 2.0). -181 If set to 0 it is assumed that the data exhibits no -182 autocorrelation. In this case the error estimates coincides -183 with the sample standard error. -184 tau_exp : float -185 positive value triggers the critical slowing down analysis -186 (default 0.0). -187 N_sigma : float -188 number of standard deviations from zero until the tail is -189 attached to the autocorrelation function (default 1). -190 fft : bool -191 determines whether the fft algorithm is used for the computation -192 of the autocorrelation function (default True) -193 """ -194 -195 e_content = self.e_content -196 self.e_dvalue = {} -197 self.e_ddvalue = {} -198 self.e_tauint = {} -199 self.e_dtauint = {} -200 self.e_windowsize = {} -201 self.e_n_tauint = {} -202 self.e_n_dtauint = {} -203 e_gamma = {} -204 self.e_rho = {} -205 self.e_drho = {} -206 self._dvalue = 0 -207 self.ddvalue = 0 -208 -209 self.S = {} -210 self.tau_exp = {} -211 self.N_sigma = {} -212 -213 if kwargs.get('fft') is False: -214 fft = False -215 else: -216 fft = True -217 -218 def _parse_kwarg(kwarg_name): -219 if kwarg_name in kwargs: -220 tmp = kwargs.get(kwarg_name) -221 if isinstance(tmp, (int, float)): -222 if tmp < 0: -223 raise Exception(kwarg_name + ' has to be larger or equal to 0.') -224 for e, e_name in enumerate(self.e_names): -225 getattr(self, kwarg_name)[e_name] = tmp -226 else: -227 raise TypeError(kwarg_name + ' is not in proper format.') -228 else: -229 for e, e_name in enumerate(self.e_names): -230 if e_name in getattr(Obs, kwarg_name + '_dict'): -231 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name] -232 else: -233 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global') -234 -235 _parse_kwarg('S') -236 _parse_kwarg('tau_exp') -237 _parse_kwarg('N_sigma') -238 -239 for e, e_name in enumerate(self.mc_names): -240 r_length = [] -241 for r_name in e_content[e_name]: -242 if isinstance(self.idl[r_name], range): -243 r_length.append(len(self.idl[r_name])) -244 else: -245 r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1)) -246 -247 e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]]) -248 w_max = max(r_length) // 2 -249 e_gamma[e_name] = np.zeros(w_max) -250 self.e_rho[e_name] = np.zeros(w_max) -251 self.e_drho[e_name] = np.zeros(w_max) -252 -253 for r_name in e_content[e_name]: -254 e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft) -255 -256 gamma_div = np.zeros(w_max) -257 for r_name in e_content[e_name]: -258 gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft) -259 gamma_div[gamma_div < 1] = 1.0 -260 e_gamma[e_name] /= gamma_div[:w_max] -261 -262 if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny: # Prevent division by zero -263 self.e_tauint[e_name] = 0.5 -264 self.e_dtauint[e_name] = 0.0 -265 self.e_dvalue[e_name] = 0.0 -266 self.e_ddvalue[e_name] = 0.0 -267 self.e_windowsize[e_name] = 0 -268 continue -269 -270 self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0] -271 self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:]))) -272 # Make sure no entry of tauint is smaller than 0.5 -273 self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps -274 # hep-lat/0306017 eq. (42) -275 self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) + 0.5 - self.e_n_tauint[e_name]) / e_N) -276 self.e_n_dtauint[e_name][0] = 0.0 -277 -278 def _compute_drho(i): -279 tmp = self.e_rho[e_name][i + 1:w_max] + np.concatenate([self.e_rho[e_name][i - 1::-1], self.e_rho[e_name][1:w_max - 2 * i]]) - 2 * self.e_rho[e_name][i] * self.e_rho[e_name][1:w_max - i] -280 self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N) -281 -282 _compute_drho(1) -283 if self.tau_exp[e_name] > 0: -284 texp = self.tau_exp[e_name] -285 # Critical slowing down analysis -286 if w_max // 2 <= 1: -287 raise Exception("Need at least 8 samples for tau_exp error analysis") -288 for n in range(1, w_max // 2): -289 _compute_drho(n + 1) -290 if (self.e_rho[e_name][n] - self.N_sigma[e_name] * self.e_drho[e_name][n]) < 0 or n >= w_max // 2 - 2: -291 # Bias correction hep-lat/0306017 eq. (49) included -292 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) + texp * np.abs(self.e_rho[e_name][n + 1]) # The absolute makes sure, that the tail contribution is always positive -293 self.e_dtauint[e_name] = np.sqrt(self.e_n_dtauint[e_name][n] ** 2 + texp ** 2 * self.e_drho[e_name][n + 1] ** 2) -294 # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2 -295 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) -296 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) -297 self.e_windowsize[e_name] = n -298 break -299 else: -300 if self.S[e_name] == 0.0: -301 self.e_tauint[e_name] = 0.5 -302 self.e_dtauint[e_name] = 0.0 -303 self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1)) -304 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N) -305 self.e_windowsize[e_name] = 0 -306 else: -307 # Standard automatic windowing procedure -308 tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][1:] + 1) / (2 * self.e_n_tauint[e_name][1:] - 1)) -309 g_w = np.exp(- np.arange(1, w_max) / tau) - tau / np.sqrt(np.arange(1, w_max) * e_N) -310 for n in range(1, w_max): -311 if n < w_max // 2 - 2: -312 _compute_drho(n + 1) -313 if g_w[n - 1] < 0 or n >= w_max - 1: -314 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) # Bias correction hep-lat/0306017 eq. (49) -315 self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n] -316 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) -317 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) -318 self.e_windowsize[e_name] = n -319 break -320 -321 self._dvalue += self.e_dvalue[e_name] ** 2 -322 self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2 -323 -324 for e_name in self.cov_names: -325 self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq()) -326 self.e_ddvalue[e_name] = 0 -327 self._dvalue += self.e_dvalue[e_name]**2 -328 -329 self._dvalue = np.sqrt(self._dvalue) -330 if self._dvalue == 0.0: -331 self.ddvalue = 0.0 -332 else: -333 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue -334 return -335 -336 def _calc_gamma(self, deltas, idx, shape, w_max, fft): -337 """Calculate Gamma_{AA} from the deltas, which are defined on idx. -338 idx is assumed to be a contiguous range (possibly with a stepsize != 1) -339 -340 Parameters -341 ---------- -342 deltas : list -343 List of fluctuations -344 idx : list -345 List or range of configurations on which the deltas are defined. -346 shape : int -347 Number of configurations in idx. -348 w_max : int -349 Upper bound for the summation window. -350 fft : bool -351 determines whether the fft algorithm is used for the computation -352 of the autocorrelation function. -353 """ -354 gamma = np.zeros(w_max) -355 deltas = _expand_deltas(deltas, idx, shape) -356 new_shape = len(deltas) -357 if fft: -358 max_gamma = min(new_shape, w_max) -359 # The padding for the fft has to be even -360 padding = new_shape + max_gamma + (new_shape + max_gamma) % 2 -361 gamma[:max_gamma] += np.fft.irfft(np.abs(np.fft.rfft(deltas, padding)) ** 2)[:max_gamma] -362 else: -363 for n in range(w_max): -364 if new_shape - n >= 0: -365 gamma[n] += deltas[0:new_shape - n].dot(deltas[n:new_shape]) -366 -367 return gamma -368 -369 def details(self, ens_content=True): -370 """Output detailed properties of the Obs. -371 -372 Parameters -373 ---------- -374 ens_content : bool -375 print details about the ensembles and replica if true. -376 """ -377 if self.tag is not None: -378 print("Description:", self.tag) -379 if not hasattr(self, 'e_dvalue'): -380 print('Result\t %3.8e' % (self.value)) -381 else: -382 if self.value == 0.0: -383 percentage = np.nan -384 else: -385 percentage = np.abs(self._dvalue / self.value) * 100 -386 print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage)) -387 if len(self.e_names) > 1: -388 print(' Ensemble errors:') -389 for e_name in self.mc_names: -390 if len(self.e_names) > 1: -391 print('', e_name, '\t %3.8e +/- %3.8e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name])) -392 if self.tau_exp[e_name] > 0: -393 print(' t_int\t %3.8e +/- %3.8e tau_exp = %3.2f, N_sigma = %1.0i' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.tau_exp[e_name], self.N_sigma[e_name])) -394 else: -395 print(' t_int\t %3.8e +/- %3.8e S = %3.2f' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.S[e_name])) -396 for e_name in self.cov_names: -397 print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name])) -398 if ens_content is True: -399 if len(self.e_names) == 1: -400 print(self.N, 'samples in', len(self.e_names), 'ensemble:') -401 else: -402 print(self.N, 'samples in', len(self.e_names), 'ensembles:') -403 my_string_list = [] -404 for key, value in sorted(self.e_content.items()): -405 if key not in self.covobs: -406 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " -407 if len(value) == 1: -408 my_string += f': {self.shape[value[0]]} configurations' -409 if isinstance(self.idl[value[0]], range): -410 my_string += f' (from {self.idl[value[0]].start} to {self.idl[value[0]][-1]}' + int(self.idl[value[0]].step != 1) * f' in steps of {self.idl[value[0]].step}' + ')' -411 else: -412 my_string += ' (irregular range)' -413 else: -414 sublist = [] -415 for v in value: -416 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " -417 my_substring += f': {self.shape[v]} configurations' -418 if isinstance(self.idl[v], range): -419 my_substring += f' (from {self.idl[v].start} to {self.idl[v][-1]}' + int(self.idl[v].step != 1) * f' in steps of {self.idl[v].step}' + ')' -420 else: -421 my_substring += ' (irregular range)' -422 sublist.append(my_substring) -423 -424 my_string += '\n' + '\n'.join(sublist) -425 else: -426 my_string = ' ' + "\u00B7 Covobs '" + key + "' " -427 my_string_list.append(my_string) -428 print('\n'.join(my_string_list)) -429 -430 def reweight(self, weight): -431 """Reweight the obs with given rewighting factors. -432 -433 Parameters -434 ---------- -435 weight : Obs -436 Reweighting factor. An Observable that has to be defined on a superset of the -437 configurations in obs[i].idl for all i. -438 all_configs : bool -439 if True, the reweighted observables are normalized by the average of -440 the reweighting factor on all configurations in weight.idl and not -441 on the configurations in obs[i].idl. Default False. -442 """ -443 return reweight(weight, [self])[0] -444 -445 def is_zero_within_error(self, sigma=1): -446 """Checks whether the observable is zero within 'sigma' standard errors. -447 -448 Parameters -449 ---------- -450 sigma : int -451 Number of standard errors used for the check. -452 -453 Works only properly when the gamma method was run. -454 """ -455 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue -456 -457 def is_zero(self, atol=1e-10): -458 """Checks whether the observable is zero within a given tolerance. -459 -460 Parameters -461 ---------- -462 atol : float -463 Absolute tolerance (for details see numpy documentation). -464 """ -465 return np.isclose(0.0, self.value, 1e-14, atol) and all(np.allclose(0.0, delta, 1e-14, atol) for delta in self.deltas.values()) and all(np.allclose(0.0, delta.errsq(), 1e-14, atol) for delta in self.covobs.values()) -466 -467 def plot_tauint(self, save=None): -468 """Plot integrated autocorrelation time for each ensemble. -469 -470 Parameters -471 ---------- -472 save : str -473 saves the figure to a file named 'save' if. -474 """ -475 if not hasattr(self, 'e_dvalue'): -476 raise Exception('Run the gamma method first.') -477 -478 for e, e_name in enumerate(self.mc_names): -479 fig = plt.figure() -480 plt.xlabel(r'$W$') -481 plt.ylabel(r'$\tau_\mathrm{int}$') -482 length = int(len(self.e_n_tauint[e_name])) -483 if self.tau_exp[e_name] > 0: -484 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] -485 x_help = np.arange(2 * self.tau_exp[e_name]) -486 y_help = (x_help + 1) * np.abs(self.e_rho[e_name][self.e_windowsize[e_name] + 1]) * (1 - x_help / (2 * (2 * self.tau_exp[e_name] - 1))) + base -487 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) -488 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') -489 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], -490 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) -491 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 -492 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) -493 else: -494 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) -495 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) -496 -497 plt.errorbar(np.arange(length)[:int(xmax) + 1], self.e_n_tauint[e_name][:int(xmax) + 1], yerr=self.e_n_dtauint[e_name][:int(xmax) + 1], linewidth=1, capsize=2, label=label) -498 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') -499 plt.legend() -500 plt.xlim(-0.5, xmax) -501 ylim = plt.ylim() -502 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) -503 plt.draw() -504 if save: -505 fig.savefig(save + "_" + str(e)) -506 -507 def plot_rho(self, save=None): -508 """Plot normalized autocorrelation function time for each ensemble. -509 -510 Parameters -511 ---------- -512 save : str -513 saves the figure to a file named 'save' if. -514 """ -515 if not hasattr(self, 'e_dvalue'): -516 raise Exception('Run the gamma method first.') -517 for e, e_name in enumerate(self.mc_names): -518 fig = plt.figure() -519 plt.xlabel('W') -520 plt.ylabel('rho') -521 length = int(len(self.e_drho[e_name])) -522 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) -523 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') -524 if self.tau_exp[e_name] > 0: -525 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], -526 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) -527 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 -528 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) -529 else: -530 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) -531 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) -532 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) -533 plt.xlim(-0.5, xmax) -534 plt.draw() -535 if save: -536 fig.savefig(save + "_" + str(e)) -537 -538 def plot_rep_dist(self): -539 """Plot replica distribution for each ensemble with more than one replicum.""" -540 if not hasattr(self, 'e_dvalue'): -541 raise Exception('Run the gamma method first.') -542 for e, e_name in enumerate(self.mc_names): -543 if len(self.e_content[e_name]) == 1: -544 print('No replica distribution for a single replicum (', e_name, ')') -545 continue -546 r_length = [] -547 sub_r_mean = 0 -548 for r, r_name in enumerate(self.e_content[e_name]): -549 r_length.append(len(self.deltas[r_name])) -550 sub_r_mean += self.shape[r_name] * self.r_values[r_name] -551 e_N = np.sum(r_length) -552 sub_r_mean /= e_N -553 arr = np.zeros(len(self.e_content[e_name])) -554 for r, r_name in enumerate(self.e_content[e_name]): -555 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) -556 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) -557 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') -558 plt.draw() -559 -560 def plot_history(self, expand=True): -561 """Plot derived Monte Carlo history for each ensemble -562 -563 Parameters -564 ---------- -565 expand : bool -566 show expanded history for irregular Monte Carlo chains (default: True). -567 """ -568 for e, e_name in enumerate(self.mc_names): -569 plt.figure() -570 r_length = [] -571 tmp = [] -572 tmp_expanded = [] -573 for r, r_name in enumerate(self.e_content[e_name]): -574 tmp.append(self.deltas[r_name] + self.r_values[r_name]) -575 if expand: -576 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name]) -577 r_length.append(len(tmp_expanded[-1])) -578 else: -579 r_length.append(len(tmp[-1])) -580 e_N = np.sum(r_length) -581 x = np.arange(e_N) -582 y_test = np.concatenate(tmp, axis=0) -583 if expand: -584 y = np.concatenate(tmp_expanded, axis=0) -585 else: -586 y = y_test -587 plt.errorbar(x, y, fmt='.', markersize=3) -588 plt.xlim(-0.5, e_N - 0.5) -589 plt.title(e_name + f'\nskew: {skew(y_test):.3f} (p={skewtest(y_test).pvalue:.3f}), kurtosis: {kurtosis(y_test):.3f} (p={kurtosistest(y_test).pvalue:.3f})') -590 plt.draw() -591 -592 def plot_piechart(self, save=None): -593 """Plot piechart which shows the fractional contribution of each -594 ensemble to the error and returns a dictionary containing the fractions. -595 -596 Parameters -597 ---------- -598 save : str -599 saves the figure to a file named 'save' if. -600 """ -601 if not hasattr(self, 'e_dvalue'): -602 raise Exception('Run the gamma method first.') -603 if np.isclose(0.0, self._dvalue, atol=1e-15): -604 raise Exception('Error is 0.0') -605 labels = self.e_names -606 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 -607 fig1, ax1 = plt.subplots() -608 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) -609 ax1.axis('equal') -610 plt.draw() -611 if save: -612 fig1.savefig(save) -613 -614 return dict(zip(self.e_names, sizes)) -615 -616 def dump(self, filename, datatype="json.gz", description="", **kwargs): -617 """Dump the Obs to a file 'name' of chosen format. -618 -619 Parameters -620 ---------- -621 filename : str -622 name of the file to be saved. -623 datatype : str -624 Format of the exported file. Supported formats include -625 "json.gz" and "pickle" -626 description : str -627 Description for output file, only relevant for json.gz format. -628 path : str -629 specifies a custom path for the file (default '.') -630 """ -631 if 'path' in kwargs: -632 file_name = kwargs.get('path') + '/' + filename -633 else: -634 file_name = filename -635 -636 if datatype == "json.gz": -637 from .input.json import dump_to_json -638 dump_to_json([self], file_name, description=description) -639 elif datatype == "pickle": -640 with open(file_name + '.p', 'wb') as fb: -641 pickle.dump(self, fb) -642 else: -643 raise Exception("Unknown datatype " + str(datatype)) -644 -645 def export_jackknife(self): -646 """Export jackknife samples from the Obs -647 -648 Returns -649 ------- -650 numpy.ndarray -651 Returns a numpy array of length N + 1 where N is the number of samples -652 for the given ensemble and replicum. The zeroth entry of the array contains -653 the mean value of the Obs, entries 1 to N contain the N jackknife samples -654 derived from the Obs. The current implementation only works for observables -655 defined on exactly one ensemble and replicum. The derived jackknife samples -656 should agree with samples from a full jackknife analysis up to O(1/N). -657 """ -658 -659 if len(self.names) != 1: -660 raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") -661 -662 name = self.names[0] -663 full_data = self.deltas[name] + self.r_values[name] -664 n = full_data.size -665 mean = self.value -666 tmp_jacks = np.zeros(n + 1) -667 tmp_jacks[0] = mean -668 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) -669 return tmp_jacks -670 -671 def __float__(self): -672 return float(self.value) -673 -674 def __repr__(self): -675 return 'Obs[' + str(self) + ']' -676 -677 def __str__(self): -678 if self._dvalue == 0.0: -679 return str(self.value) -680 fexp = np.floor(np.log10(self._dvalue)) -681 if fexp < 0.0: -682 return '{:{form}}({:2.0f})'.format(self.value, self._dvalue * 10 ** (-fexp + 1), form='.' + str(-int(fexp) + 1) + 'f') -683 elif fexp == 0.0: -684 return '{:.1f}({:1.1f})'.format(self.value, self._dvalue) -685 else: -686 return '{:.0f}({:2.0f})'.format(self.value, self._dvalue) -687 -688 # Overload comparisons -689 def __lt__(self, other): -690 return self.value < other -691 -692 def __le__(self, other): -693 return self.value <= other -694 -695 def __gt__(self, other): -696 return self.value > other +@@ -2855,87 +2874,87 @@ this overwrites the standard value for that ensemble.17class Obs: + 18 """Class for a general observable. + 19 + 20 Instances of Obs are the basic objects of a pyerrors error analysis. + 21 They are initialized with a list which contains arrays of samples for + 22 different ensembles/replica and another list of same length which contains + 23 the names of the ensembles/replica. Mathematical operations can be + 24 performed on instances. The result is another instance of Obs. The error of + 25 an instance can be computed with the gamma_method. Also contains additional + 26 methods for output and visualization of the error calculation. + 27 + 28 Attributes + 29 ---------- + 30 S_global : float + 31 Standard value for S (default 2.0) + 32 S_dict : dict + 33 Dictionary for S values. If an entry for a given ensemble + 34 exists this overwrites the standard value for that ensemble. + 35 tau_exp_global : float + 36 Standard value for tau_exp (default 0.0) + 37 tau_exp_dict : dict + 38 Dictionary for tau_exp values. If an entry for a given ensemble exists + 39 this overwrites the standard value for that ensemble. + 40 N_sigma_global : float + 41 Standard value for N_sigma (default 1.0) + 42 N_sigma_dict : dict + 43 Dictionary for N_sigma values. If an entry for a given ensemble exists + 44 this overwrites the standard value for that ensemble. + 45 """ + 46 __slots__ = ['names', 'shape', 'r_values', 'deltas', 'N', '_value', '_dvalue', + 47 'ddvalue', 'reweighted', 'S', 'tau_exp', 'N_sigma', + 48 'e_dvalue', 'e_ddvalue', 'e_tauint', 'e_dtauint', + 49 'e_windowsize', 'e_rho', 'e_drho', 'e_n_tauint', 'e_n_dtauint', + 50 'idl', 'is_merged', 'tag', '_covobs', '__dict__'] + 51 + 52 S_global = 2.0 + 53 S_dict = {} + 54 tau_exp_global = 0.0 + 55 tau_exp_dict = {} + 56 N_sigma_global = 1.0 + 57 N_sigma_dict = {} + 58 filter_eps = 1e-10 + 59 + 60 def __init__(self, samples, names, idl=None, **kwargs): + 61 """ Initialize Obs object. + 62 + 63 Parameters + 64 ---------- + 65 samples : list + 66 list of numpy arrays containing the Monte Carlo samples + 67 names : list + 68 list of strings labeling the individual samples + 69 idl : list, optional + 70 list of ranges or lists on which the samples are defined + 71 """ + 72 + 73 if kwargs.get("means") is None and len(samples): + 74 if len(samples) != len(names): + 75 raise Exception('Length of samples and names incompatible.') + 76 if idl is not None: + 77 if len(idl) != len(names): + 78 raise Exception('Length of idl incompatible with samples and names.') + 79 name_length = len(names) + 80 if name_length > 1: + 81 if name_length != len(set(names)): + 82 raise Exception('names are not unique.') + 83 if not all(isinstance(x, str) for x in names): + 84 raise TypeError('All names have to be strings.') + 85 else: + 86 if not isinstance(names[0], str): + 87 raise TypeError('All names have to be strings.') + 88 if min(len(x) for x in samples) <= 4: + 89 raise Exception('Samples have to have at least 5 entries.') + 90 + 91 self.names = sorted(names) + 92 self.shape = {} + 93 self.r_values = {} + 94 self.deltas = {} + 95 self._covobs = {} + 96 + 97 self._value = 0 + 98 self.N = 0 + 99 self.is_merged = {} +100 self.idl = {} +101 if idl is not None: +102 for name, idx in sorted(zip(names, idl)): +103 if isinstance(idx, range): +104 self.idl[name] = idx +105 elif isinstance(idx, (list, np.ndarray)): +106 dc = np.unique(np.diff(idx)) +107 if np.any(dc < 0): +108 raise Exception("Unsorted idx for idl[%s]" % (name)) +109 if len(dc) == 1: +110 self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0]) +111 else: +112 self.idl[name] = list(idx) +113 else: +114 raise Exception('incompatible type for idl[%s].' % (name)) +115 else: +116 for name, sample in sorted(zip(names, samples)): +117 self.idl[name] = range(1, len(sample) + 1) +118 +119 if kwargs.get("means") is not None: +120 for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))): +121 self.shape[name] = len(self.idl[name]) +122 self.N += self.shape[name] +123 self.r_values[name] = mean +124 self.deltas[name] = sample +125 else: +126 for name, sample in sorted(zip(names, samples)): +127 self.shape[name] = len(self.idl[name]) +128 self.N += self.shape[name] +129 if len(sample) != self.shape[name]: +130 raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name])) +131 self.r_values[name] = np.mean(sample) +132 self.deltas[name] = sample - self.r_values[name] +133 self._value += self.shape[name] * self.r_values[name] +134 self._value /= self.N +135 +136 self._dvalue = 0.0 +137 self.ddvalue = 0.0 +138 self.reweighted = False +139 +140 self.tag = None +141 +142 @property +143 def value(self): +144 return self._value +145 +146 @property +147 def dvalue(self): +148 return self._dvalue +149 +150 @property +151 def e_names(self): +152 return sorted(set([o.split('|')[0] for o in self.names])) +153 +154 @property +155 def cov_names(self): +156 return sorted(set([o for o in self.covobs.keys()])) +157 +158 @property +159 def mc_names(self): +160 return sorted(set([o.split('|')[0] for o in self.names if o not in self.cov_names])) +161 +162 @property +163 def e_content(self): +164 res = {} +165 for e, e_name in enumerate(self.e_names): +166 res[e_name] = sorted(filter(lambda x: x.startswith(e_name + '|'), self.names)) +167 if e_name in self.names: +168 res[e_name].append(e_name) +169 return res +170 +171 @property +172 def covobs(self): +173 return self._covobs +174 +175 def gamma_method(self, **kwargs): +176 """Estimate the error and related properties of the Obs. +177 +178 Parameters +179 ---------- +180 S : float +181 specifies a custom value for the parameter S (default 2.0). +182 If set to 0 it is assumed that the data exhibits no +183 autocorrelation. In this case the error estimates coincides +184 with the sample standard error. +185 tau_exp : float +186 positive value triggers the critical slowing down analysis +187 (default 0.0). +188 N_sigma : float +189 number of standard deviations from zero until the tail is +190 attached to the autocorrelation function (default 1). +191 fft : bool +192 determines whether the fft algorithm is used for the computation +193 of the autocorrelation function (default True) +194 """ +195 +196 e_content = self.e_content +197 self.e_dvalue = {} +198 self.e_ddvalue = {} +199 self.e_tauint = {} +200 self.e_dtauint = {} +201 self.e_windowsize = {} +202 self.e_n_tauint = {} +203 self.e_n_dtauint = {} +204 e_gamma = {} +205 self.e_rho = {} +206 self.e_drho = {} +207 self._dvalue = 0 +208 self.ddvalue = 0 +209 +210 self.S = {} +211 self.tau_exp = {} +212 self.N_sigma = {} +213 +214 if kwargs.get('fft') is False: +215 fft = False +216 else: +217 fft = True +218 +219 def _parse_kwarg(kwarg_name): +220 if kwarg_name in kwargs: +221 tmp = kwargs.get(kwarg_name) +222 if isinstance(tmp, (int, float)): +223 if tmp < 0: +224 raise Exception(kwarg_name + ' has to be larger or equal to 0.') +225 for e, e_name in enumerate(self.e_names): +226 getattr(self, kwarg_name)[e_name] = tmp +227 else: +228 raise TypeError(kwarg_name + ' is not in proper format.') +229 else: +230 for e, e_name in enumerate(self.e_names): +231 if e_name in getattr(Obs, kwarg_name + '_dict'): +232 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name] +233 else: +234 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global') +235 +236 _parse_kwarg('S') +237 _parse_kwarg('tau_exp') +238 _parse_kwarg('N_sigma') +239 +240 for e, e_name in enumerate(self.mc_names): +241 r_length = [] +242 for r_name in e_content[e_name]: +243 if isinstance(self.idl[r_name], range): +244 r_length.append(len(self.idl[r_name])) +245 else: +246 r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1)) +247 +248 e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]]) +249 w_max = max(r_length) // 2 +250 e_gamma[e_name] = np.zeros(w_max) +251 self.e_rho[e_name] = np.zeros(w_max) +252 self.e_drho[e_name] = np.zeros(w_max) +253 +254 for r_name in e_content[e_name]: +255 e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft) +256 +257 gamma_div = np.zeros(w_max) +258 for r_name in e_content[e_name]: +259 gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft) +260 gamma_div[gamma_div < 1] = 1.0 +261 e_gamma[e_name] /= gamma_div[:w_max] +262 +263 if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny: # Prevent division by zero +264 self.e_tauint[e_name] = 0.5 +265 self.e_dtauint[e_name] = 0.0 +266 self.e_dvalue[e_name] = 0.0 +267 self.e_ddvalue[e_name] = 0.0 +268 self.e_windowsize[e_name] = 0 +269 continue +270 +271 self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0] +272 self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:]))) +273 # Make sure no entry of tauint is smaller than 0.5 +274 self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps +275 # hep-lat/0306017 eq. (42) +276 self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) + 0.5 - self.e_n_tauint[e_name]) / e_N) +277 self.e_n_dtauint[e_name][0] = 0.0 +278 +279 def _compute_drho(i): +280 tmp = self.e_rho[e_name][i + 1:w_max] + np.concatenate([self.e_rho[e_name][i - 1::-1], self.e_rho[e_name][1:w_max - 2 * i]]) - 2 * self.e_rho[e_name][i] * self.e_rho[e_name][1:w_max - i] +281 self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N) +282 +283 _compute_drho(1) +284 if self.tau_exp[e_name] > 0: +285 texp = self.tau_exp[e_name] +286 # Critical slowing down analysis +287 if w_max // 2 <= 1: +288 raise Exception("Need at least 8 samples for tau_exp error analysis") +289 for n in range(1, w_max // 2): +290 _compute_drho(n + 1) +291 if (self.e_rho[e_name][n] - self.N_sigma[e_name] * self.e_drho[e_name][n]) < 0 or n >= w_max // 2 - 2: +292 # Bias correction hep-lat/0306017 eq. (49) included +293 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) + texp * np.abs(self.e_rho[e_name][n + 1]) # The absolute makes sure, that the tail contribution is always positive +294 self.e_dtauint[e_name] = np.sqrt(self.e_n_dtauint[e_name][n] ** 2 + texp ** 2 * self.e_drho[e_name][n + 1] ** 2) +295 # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2 +296 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) +297 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) +298 self.e_windowsize[e_name] = n +299 break +300 else: +301 if self.S[e_name] == 0.0: +302 self.e_tauint[e_name] = 0.5 +303 self.e_dtauint[e_name] = 0.0 +304 self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1)) +305 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N) +306 self.e_windowsize[e_name] = 0 +307 else: +308 # Standard automatic windowing procedure +309 tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][1:] + 1) / (2 * self.e_n_tauint[e_name][1:] - 1)) +310 g_w = np.exp(- np.arange(1, w_max) / tau) - tau / np.sqrt(np.arange(1, w_max) * e_N) +311 for n in range(1, w_max): +312 if n < w_max // 2 - 2: +313 _compute_drho(n + 1) +314 if g_w[n - 1] < 0 or n >= w_max - 1: +315 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) # Bias correction hep-lat/0306017 eq. (49) +316 self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n] +317 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) +318 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) +319 self.e_windowsize[e_name] = n +320 break +321 +322 self._dvalue += self.e_dvalue[e_name] ** 2 +323 self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2 +324 +325 for e_name in self.cov_names: +326 self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq()) +327 self.e_ddvalue[e_name] = 0 +328 self._dvalue += self.e_dvalue[e_name]**2 +329 +330 self._dvalue = np.sqrt(self._dvalue) +331 if self._dvalue == 0.0: +332 self.ddvalue = 0.0 +333 else: +334 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue +335 return +336 +337 def _calc_gamma(self, deltas, idx, shape, w_max, fft): +338 """Calculate Gamma_{AA} from the deltas, which are defined on idx. +339 idx is assumed to be a contiguous range (possibly with a stepsize != 1) +340 +341 Parameters +342 ---------- +343 deltas : list +344 List of fluctuations +345 idx : list +346 List or range of configurations on which the deltas are defined. +347 shape : int +348 Number of configurations in idx. +349 w_max : int +350 Upper bound for the summation window. +351 fft : bool +352 determines whether the fft algorithm is used for the computation +353 of the autocorrelation function. +354 """ +355 gamma = np.zeros(w_max) +356 deltas = _expand_deltas(deltas, idx, shape) +357 new_shape = len(deltas) +358 if fft: +359 max_gamma = min(new_shape, w_max) +360 # The padding for the fft has to be even +361 padding = new_shape + max_gamma + (new_shape + max_gamma) % 2 +362 gamma[:max_gamma] += np.fft.irfft(np.abs(np.fft.rfft(deltas, padding)) ** 2)[:max_gamma] +363 else: +364 for n in range(w_max): +365 if new_shape - n >= 0: +366 gamma[n] += deltas[0:new_shape - n].dot(deltas[n:new_shape]) +367 +368 return gamma +369 +370 def details(self, ens_content=True): +371 """Output detailed properties of the Obs. +372 +373 Parameters +374 ---------- +375 ens_content : bool +376 print details about the ensembles and replica if true. +377 """ +378 if self.tag is not None: +379 print("Description:", self.tag) +380 if not hasattr(self, 'e_dvalue'): +381 print('Result\t %3.8e' % (self.value)) +382 else: +383 if self.value == 0.0: +384 percentage = np.nan +385 else: +386 percentage = np.abs(self._dvalue / self.value) * 100 +387 print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage)) +388 if len(self.e_names) > 1: +389 print(' Ensemble errors:') +390 for e_name in self.mc_names: +391 if len(self.e_names) > 1: +392 print('', e_name, '\t %3.8e +/- %3.8e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name])) +393 if self.tau_exp[e_name] > 0: +394 print(' t_int\t %3.8e +/- %3.8e tau_exp = %3.2f, N_sigma = %1.0i' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.tau_exp[e_name], self.N_sigma[e_name])) +395 else: +396 print(' t_int\t %3.8e +/- %3.8e S = %3.2f' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.S[e_name])) +397 for e_name in self.cov_names: +398 print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name])) +399 if ens_content is True: +400 if len(self.e_names) == 1: +401 print(self.N, 'samples in', len(self.e_names), 'ensemble:') +402 else: +403 print(self.N, 'samples in', len(self.e_names), 'ensembles:') +404 my_string_list = [] +405 for key, value in sorted(self.e_content.items()): +406 if key not in self.covobs: +407 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " +408 if len(value) == 1: +409 my_string += f': {self.shape[value[0]]} configurations' +410 if isinstance(self.idl[value[0]], range): +411 my_string += f' (from {self.idl[value[0]].start} to {self.idl[value[0]][-1]}' + int(self.idl[value[0]].step != 1) * f' in steps of {self.idl[value[0]].step}' + ')' +412 else: +413 my_string += ' (irregular range)' +414 else: +415 sublist = [] +416 for v in value: +417 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " +418 my_substring += f': {self.shape[v]} configurations' +419 if isinstance(self.idl[v], range): +420 my_substring += f' (from {self.idl[v].start} to {self.idl[v][-1]}' + int(self.idl[v].step != 1) * f' in steps of {self.idl[v].step}' + ')' +421 else: +422 my_substring += ' (irregular range)' +423 sublist.append(my_substring) +424 +425 my_string += '\n' + '\n'.join(sublist) +426 else: +427 my_string = ' ' + "\u00B7 Covobs '" + key + "' " +428 my_string_list.append(my_string) +429 print('\n'.join(my_string_list)) +430 +431 def reweight(self, weight): +432 """Reweight the obs with given rewighting factors. +433 +434 Parameters +435 ---------- +436 weight : Obs +437 Reweighting factor. An Observable that has to be defined on a superset of the +438 configurations in obs[i].idl for all i. +439 all_configs : bool +440 if True, the reweighted observables are normalized by the average of +441 the reweighting factor on all configurations in weight.idl and not +442 on the configurations in obs[i].idl. Default False. +443 """ +444 return reweight(weight, [self])[0] +445 +446 def is_zero_within_error(self, sigma=1): +447 """Checks whether the observable is zero within 'sigma' standard errors. +448 +449 Parameters +450 ---------- +451 sigma : int +452 Number of standard errors used for the check. +453 +454 Works only properly when the gamma method was run. +455 """ +456 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue +457 +458 def is_zero(self, atol=1e-10): +459 """Checks whether the observable is zero within a given tolerance. +460 +461 Parameters +462 ---------- +463 atol : float +464 Absolute tolerance (for details see numpy documentation). +465 """ +466 return np.isclose(0.0, self.value, 1e-14, atol) and all(np.allclose(0.0, delta, 1e-14, atol) for delta in self.deltas.values()) and all(np.allclose(0.0, delta.errsq(), 1e-14, atol) for delta in self.covobs.values()) +467 +468 def plot_tauint(self, save=None): +469 """Plot integrated autocorrelation time for each ensemble. +470 +471 Parameters +472 ---------- +473 save : str +474 saves the figure to a file named 'save' if. +475 """ +476 if not hasattr(self, 'e_dvalue'): +477 raise Exception('Run the gamma method first.') +478 +479 for e, e_name in enumerate(self.mc_names): +480 fig = plt.figure() +481 plt.xlabel(r'$W$') +482 plt.ylabel(r'$\tau_\mathrm{int}$') +483 length = int(len(self.e_n_tauint[e_name])) +484 if self.tau_exp[e_name] > 0: +485 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] +486 x_help = np.arange(2 * self.tau_exp[e_name]) +487 y_help = (x_help + 1) * np.abs(self.e_rho[e_name][self.e_windowsize[e_name] + 1]) * (1 - x_help / (2 * (2 * self.tau_exp[e_name] - 1))) + base +488 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) +489 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') +490 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], +491 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) +492 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 +493 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) +494 else: +495 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) +496 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) +497 +498 plt.errorbar(np.arange(length)[:int(xmax) + 1], self.e_n_tauint[e_name][:int(xmax) + 1], yerr=self.e_n_dtauint[e_name][:int(xmax) + 1], linewidth=1, capsize=2, label=label) +499 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') +500 plt.legend() +501 plt.xlim(-0.5, xmax) +502 ylim = plt.ylim() +503 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) +504 plt.draw() +505 if save: +506 fig.savefig(save + "_" + str(e)) +507 +508 def plot_rho(self, save=None): +509 """Plot normalized autocorrelation function time for each ensemble. +510 +511 Parameters +512 ---------- +513 save : str +514 saves the figure to a file named 'save' if. +515 """ +516 if not hasattr(self, 'e_dvalue'): +517 raise Exception('Run the gamma method first.') +518 for e, e_name in enumerate(self.mc_names): +519 fig = plt.figure() +520 plt.xlabel('W') +521 plt.ylabel('rho') +522 length = int(len(self.e_drho[e_name])) +523 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) +524 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') +525 if self.tau_exp[e_name] > 0: +526 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], +527 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) +528 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 +529 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) +530 else: +531 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) +532 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) +533 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) +534 plt.xlim(-0.5, xmax) +535 plt.draw() +536 if save: +537 fig.savefig(save + "_" + str(e)) +538 +539 def plot_rep_dist(self): +540 """Plot replica distribution for each ensemble with more than one replicum.""" +541 if not hasattr(self, 'e_dvalue'): +542 raise Exception('Run the gamma method first.') +543 for e, e_name in enumerate(self.mc_names): +544 if len(self.e_content[e_name]) == 1: +545 print('No replica distribution for a single replicum (', e_name, ')') +546 continue +547 r_length = [] +548 sub_r_mean = 0 +549 for r, r_name in enumerate(self.e_content[e_name]): +550 r_length.append(len(self.deltas[r_name])) +551 sub_r_mean += self.shape[r_name] * self.r_values[r_name] +552 e_N = np.sum(r_length) +553 sub_r_mean /= e_N +554 arr = np.zeros(len(self.e_content[e_name])) +555 for r, r_name in enumerate(self.e_content[e_name]): +556 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) +557 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) +558 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') +559 plt.draw() +560 +561 def plot_history(self, expand=True): +562 """Plot derived Monte Carlo history for each ensemble +563 +564 Parameters +565 ---------- +566 expand : bool +567 show expanded history for irregular Monte Carlo chains (default: True). +568 """ +569 for e, e_name in enumerate(self.mc_names): +570 plt.figure() +571 r_length = [] +572 tmp = [] +573 tmp_expanded = [] +574 for r, r_name in enumerate(self.e_content[e_name]): +575 tmp.append(self.deltas[r_name] + self.r_values[r_name]) +576 if expand: +577 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name]) +578 r_length.append(len(tmp_expanded[-1])) +579 else: +580 r_length.append(len(tmp[-1])) +581 e_N = np.sum(r_length) +582 x = np.arange(e_N) +583 y_test = np.concatenate(tmp, axis=0) +584 if expand: +585 y = np.concatenate(tmp_expanded, axis=0) +586 else: +587 y = y_test +588 plt.errorbar(x, y, fmt='.', markersize=3) +589 plt.xlim(-0.5, e_N - 0.5) +590 plt.title(e_name + f'\nskew: {skew(y_test):.3f} (p={skewtest(y_test).pvalue:.3f}), kurtosis: {kurtosis(y_test):.3f} (p={kurtosistest(y_test).pvalue:.3f})') +591 plt.draw() +592 +593 def plot_piechart(self, save=None): +594 """Plot piechart which shows the fractional contribution of each +595 ensemble to the error and returns a dictionary containing the fractions. +596 +597 Parameters +598 ---------- +599 save : str +600 saves the figure to a file named 'save' if. +601 """ +602 if not hasattr(self, 'e_dvalue'): +603 raise Exception('Run the gamma method first.') +604 if np.isclose(0.0, self._dvalue, atol=1e-15): +605 raise Exception('Error is 0.0') +606 labels = self.e_names +607 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 +608 fig1, ax1 = plt.subplots() +609 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) +610 ax1.axis('equal') +611 plt.draw() +612 if save: +613 fig1.savefig(save) +614 +615 return dict(zip(self.e_names, sizes)) +616 +617 def dump(self, filename, datatype="json.gz", description="", **kwargs): +618 """Dump the Obs to a file 'name' of chosen format. +619 +620 Parameters +621 ---------- +622 filename : str +623 name of the file to be saved. +624 datatype : str +625 Format of the exported file. Supported formats include +626 "json.gz" and "pickle" +627 description : str +628 Description for output file, only relevant for json.gz format. +629 path : str +630 specifies a custom path for the file (default '.') +631 """ +632 if 'path' in kwargs: +633 file_name = kwargs.get('path') + '/' + filename +634 else: +635 file_name = filename +636 +637 if datatype == "json.gz": +638 from .input.json import dump_to_json +639 dump_to_json([self], file_name, description=description) +640 elif datatype == "pickle": +641 with open(file_name + '.p', 'wb') as fb: +642 pickle.dump(self, fb) +643 else: +644 raise Exception("Unknown datatype " + str(datatype)) +645 +646 def export_jackknife(self): +647 """Export jackknife samples from the Obs +648 +649 Returns +650 ------- +651 numpy.ndarray +652 Returns a numpy array of length N + 1 where N is the number of samples +653 for the given ensemble and replicum. The zeroth entry of the array contains +654 the mean value of the Obs, entries 1 to N contain the N jackknife samples +655 derived from the Obs. The current implementation only works for observables +656 defined on exactly one ensemble and replicum. The derived jackknife samples +657 should agree with samples from a full jackknife analysis up to O(1/N). +658 """ +659 +660 if len(self.names) != 1: +661 raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") +662 +663 name = self.names[0] +664 full_data = self.deltas[name] + self.r_values[name] +665 n = full_data.size +666 mean = self.value +667 tmp_jacks = np.zeros(n + 1) +668 tmp_jacks[0] = mean +669 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) +670 return tmp_jacks +671 +672 def __float__(self): +673 return float(self.value) +674 +675 def __repr__(self): +676 return 'Obs[' + str(self) + ']' +677 +678 def __str__(self): +679 if self._dvalue == 0.0: +680 return str(self.value) +681 fexp = np.floor(np.log10(self._dvalue)) +682 if fexp < 0.0: +683 return '{:{form}}({:2.0f})'.format(self.value, self._dvalue * 10 ** (-fexp + 1), form='.' + str(-int(fexp) + 1) + 'f') +684 elif fexp == 0.0: +685 return '{:.1f}({:1.1f})'.format(self.value, self._dvalue) +686 else: +687 return '{:.0f}({:2.0f})'.format(self.value, self._dvalue) +688 +689 def __hash__(self): +690 hash_tuple = (np.array([self.value]).astype(np.float32).data.tobytes(),) +691 hash_tuple += tuple([o.astype(np.float32).data.tobytes() for o in self.deltas.values()]) +692 hash_tuple += tuple([np.array([o.errsq()]).astype(np.float32).data.tobytes() for o in self.covobs.values()]) +693 hash_tuple += tuple([o.encode() for o in self.names]) +694 m = hashlib.md5() +695 [m.update(o) for o in hash_tuple] +696 return int(m.hexdigest(), 16) & 0xFFFFFFFF 697 -698 def __ge__(self, other): -699 return self.value >= other -700 -701 def __eq__(self, other): -702 return (self - other).is_zero() -703 -704 def __ne__(self, other): -705 return not (self - other).is_zero() -706 -707 # Overload math operations -708 def __add__(self, y): -709 if isinstance(y, Obs): -710 return derived_observable(lambda x, **kwargs: x[0] + x[1], [self, y], man_grad=[1, 1]) -711 else: -712 if isinstance(y, np.ndarray): -713 return np.array([self + o for o in y]) -714 elif y.__class__.__name__ in ['Corr', 'CObs']: -715 return NotImplemented -716 else: -717 return derived_observable(lambda x, **kwargs: x[0] + y, [self], man_grad=[1]) -718 -719 def __radd__(self, y): -720 return self + y -721 -722 def __mul__(self, y): -723 if isinstance(y, Obs): -724 return derived_observable(lambda x, **kwargs: x[0] * x[1], [self, y], man_grad=[y.value, self.value]) -725 else: -726 if isinstance(y, np.ndarray): -727 return np.array([self * o for o in y]) -728 elif isinstance(y, complex): -729 return CObs(self * y.real, self * y.imag) -730 elif y.__class__.__name__ in ['Corr', 'CObs']: -731 return NotImplemented -732 else: -733 return derived_observable(lambda x, **kwargs: x[0] * y, [self], man_grad=[y]) -734 -735 def __rmul__(self, y): -736 return self * y -737 -738 def __sub__(self, y): -739 if isinstance(y, Obs): -740 return derived_observable(lambda x, **kwargs: x[0] - x[1], [self, y], man_grad=[1, -1]) -741 else: -742 if isinstance(y, np.ndarray): -743 return np.array([self - o for o in y]) -744 elif y.__class__.__name__ in ['Corr', 'CObs']: -745 return NotImplemented -746 else: -747 return derived_observable(lambda x, **kwargs: x[0] - y, [self], man_grad=[1]) -748 -749 def __rsub__(self, y): -750 return -1 * (self - y) -751 -752 def __pos__(self): -753 return self -754 -755 def __neg__(self): -756 return -1 * self -757 -758 def __truediv__(self, y): -759 if isinstance(y, Obs): -760 return derived_observable(lambda x, **kwargs: x[0] / x[1], [self, y], man_grad=[1 / y.value, - self.value / y.value ** 2]) -761 else: -762 if isinstance(y, np.ndarray): -763 return np.array([self / o for o in y]) -764 elif y.__class__.__name__ in ['Corr', 'CObs']: -765 return NotImplemented -766 else: -767 return derived_observable(lambda x, **kwargs: x[0] / y, [self], man_grad=[1 / y]) -768 -769 def __rtruediv__(self, y): -770 if isinstance(y, Obs): -771 return derived_observable(lambda x, **kwargs: x[0] / x[1], [y, self], man_grad=[1 / self.value, - y.value / self.value ** 2]) -772 else: -773 if isinstance(y, np.ndarray): -774 return np.array([o / self for o in y]) -775 elif y.__class__.__name__ in ['Corr', 'CObs']: -776 return NotImplemented -777 else: -778 return derived_observable(lambda x, **kwargs: y / x[0], [self], man_grad=[-y / self.value ** 2]) -779 -780 def __pow__(self, y): -781 if isinstance(y, Obs): -782 return derived_observable(lambda x: x[0] ** x[1], [self, y]) -783 else: -784 return derived_observable(lambda x: x[0] ** y, [self]) -785 -786 def __rpow__(self, y): -787 if isinstance(y, Obs): -788 return derived_observable(lambda x: x[0] ** x[1], [y, self]) -789 else: -790 return derived_observable(lambda x: y ** x[0], [self]) -791 -792 def __abs__(self): -793 return derived_observable(lambda x: anp.abs(x[0]), [self]) -794 -795 # Overload numpy functions -796 def sqrt(self): -797 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) -798 -799 def log(self): -800 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) +698 # Overload comparisons +699 def __lt__(self, other): +700 return self.value < other +701 +702 def __le__(self, other): +703 return self.value <= other +704 +705 def __gt__(self, other): +706 return self.value > other +707 +708 def __ge__(self, other): +709 return self.value >= other +710 +711 def __eq__(self, other): +712 return (self - other).is_zero() +713 +714 def __ne__(self, other): +715 return not (self - other).is_zero() +716 +717 # Overload math operations +718 def __add__(self, y): +719 if isinstance(y, Obs): +720 return derived_observable(lambda x, **kwargs: x[0] + x[1], [self, y], man_grad=[1, 1]) +721 else: +722 if isinstance(y, np.ndarray): +723 return np.array([self + o for o in y]) +724 elif y.__class__.__name__ in ['Corr', 'CObs']: +725 return NotImplemented +726 else: +727 return derived_observable(lambda x, **kwargs: x[0] + y, [self], man_grad=[1]) +728 +729 def __radd__(self, y): +730 return self + y +731 +732 def __mul__(self, y): +733 if isinstance(y, Obs): +734 return derived_observable(lambda x, **kwargs: x[0] * x[1], [self, y], man_grad=[y.value, self.value]) +735 else: +736 if isinstance(y, np.ndarray): +737 return np.array([self * o for o in y]) +738 elif isinstance(y, complex): +739 return CObs(self * y.real, self * y.imag) +740 elif y.__class__.__name__ in ['Corr', 'CObs']: +741 return NotImplemented +742 else: +743 return derived_observable(lambda x, **kwargs: x[0] * y, [self], man_grad=[y]) +744 +745 def __rmul__(self, y): +746 return self * y +747 +748 def __sub__(self, y): +749 if isinstance(y, Obs): +750 return derived_observable(lambda x, **kwargs: x[0] - x[1], [self, y], man_grad=[1, -1]) +751 else: +752 if isinstance(y, np.ndarray): +753 return np.array([self - o for o in y]) +754 elif y.__class__.__name__ in ['Corr', 'CObs']: +755 return NotImplemented +756 else: +757 return derived_observable(lambda x, **kwargs: x[0] - y, [self], man_grad=[1]) +758 +759 def __rsub__(self, y): +760 return -1 * (self - y) +761 +762 def __pos__(self): +763 return self +764 +765 def __neg__(self): +766 return -1 * self +767 +768 def __truediv__(self, y): +769 if isinstance(y, Obs): +770 return derived_observable(lambda x, **kwargs: x[0] / x[1], [self, y], man_grad=[1 / y.value, - self.value / y.value ** 2]) +771 else: +772 if isinstance(y, np.ndarray): +773 return np.array([self / o for o in y]) +774 elif y.__class__.__name__ in ['Corr', 'CObs']: +775 return NotImplemented +776 else: +777 return derived_observable(lambda x, **kwargs: x[0] / y, [self], man_grad=[1 / y]) +778 +779 def __rtruediv__(self, y): +780 if isinstance(y, Obs): +781 return derived_observable(lambda x, **kwargs: x[0] / x[1], [y, self], man_grad=[1 / self.value, - y.value / self.value ** 2]) +782 else: +783 if isinstance(y, np.ndarray): +784 return np.array([o / self for o in y]) +785 elif y.__class__.__name__ in ['Corr', 'CObs']: +786 return NotImplemented +787 else: +788 return derived_observable(lambda x, **kwargs: y / x[0], [self], man_grad=[-y / self.value ** 2]) +789 +790 def __pow__(self, y): +791 if isinstance(y, Obs): +792 return derived_observable(lambda x: x[0] ** x[1], [self, y]) +793 else: +794 return derived_observable(lambda x: x[0] ** y, [self]) +795 +796 def __rpow__(self, y): +797 if isinstance(y, Obs): +798 return derived_observable(lambda x: x[0] ** x[1], [y, self]) +799 else: +800 return derived_observable(lambda x: y ** x[0], [self]) 801 -802 def exp(self): -803 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) +802 def __abs__(self): +803 return derived_observable(lambda x: anp.abs(x[0]), [self]) 804 -805 def sin(self): -806 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) -807 -808 def cos(self): -809 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) -810 -811 def tan(self): -812 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) -813 -814 def arcsin(self): -815 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) -816 -817 def arccos(self): -818 return derived_observable(lambda x: anp.arccos(x[0]), [self]) -819 -820 def arctan(self): -821 return derived_observable(lambda x: anp.arctan(x[0]), [self]) -822 -823 def sinh(self): -824 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) -825 -826 def cosh(self): -827 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) -828 -829 def tanh(self): -830 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) -831 -832 def arcsinh(self): -833 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) -834 -835 def arccosh(self): -836 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) -837 -838 def arctanh(self): -839 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) +805 # Overload numpy functions +806 def sqrt(self): +807 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) +808 +809 def log(self): +810 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) +811 +812 def exp(self): +813 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) +814 +815 def sin(self): +816 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) +817 +818 def cos(self): +819 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) +820 +821 def tan(self): +822 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) +823 +824 def arcsin(self): +825 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) +826 +827 def arccos(self): +828 return derived_observable(lambda x: anp.arccos(x[0]), [self]) +829 +830 def arctan(self): +831 return derived_observable(lambda x: anp.arctan(x[0]), [self]) +832 +833 def sinh(self): +834 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) +835 +836 def cosh(self): +837 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) +838 +839 def tanh(self): +840 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) +841 +842 def arcsinh(self): +843 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) +844 +845 def arccosh(self): +846 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) +847 +848 def arctanh(self): +849 return derived_observable(lambda x: anp.arctanh(x[0]), [self])
59 def __init__(self, samples, names, idl=None, **kwargs): - 60 """ Initialize Obs object. - 61 - 62 Parameters - 63 ---------- - 64 samples : list - 65 list of numpy arrays containing the Monte Carlo samples - 66 names : list - 67 list of strings labeling the individual samples - 68 idl : list, optional - 69 list of ranges or lists on which the samples are defined - 70 """ - 71 - 72 if kwargs.get("means") is None and len(samples): - 73 if len(samples) != len(names): - 74 raise Exception('Length of samples and names incompatible.') - 75 if idl is not None: - 76 if len(idl) != len(names): - 77 raise Exception('Length of idl incompatible with samples and names.') - 78 name_length = len(names) - 79 if name_length > 1: - 80 if name_length != len(set(names)): - 81 raise Exception('names are not unique.') - 82 if not all(isinstance(x, str) for x in names): - 83 raise TypeError('All names have to be strings.') - 84 else: - 85 if not isinstance(names[0], str): - 86 raise TypeError('All names have to be strings.') - 87 if min(len(x) for x in samples) <= 4: - 88 raise Exception('Samples have to have at least 5 entries.') - 89 - 90 self.names = sorted(names) - 91 self.shape = {} - 92 self.r_values = {} - 93 self.deltas = {} - 94 self._covobs = {} - 95 - 96 self._value = 0 - 97 self.N = 0 - 98 self.is_merged = {} - 99 self.idl = {} -100 if idl is not None: -101 for name, idx in sorted(zip(names, idl)): -102 if isinstance(idx, range): -103 self.idl[name] = idx -104 elif isinstance(idx, (list, np.ndarray)): -105 dc = np.unique(np.diff(idx)) -106 if np.any(dc < 0): -107 raise Exception("Unsorted idx for idl[%s]" % (name)) -108 if len(dc) == 1: -109 self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0]) -110 else: -111 self.idl[name] = list(idx) -112 else: -113 raise Exception('incompatible type for idl[%s].' % (name)) -114 else: -115 for name, sample in sorted(zip(names, samples)): -116 self.idl[name] = range(1, len(sample) + 1) -117 -118 if kwargs.get("means") is not None: -119 for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))): -120 self.shape[name] = len(self.idl[name]) -121 self.N += self.shape[name] -122 self.r_values[name] = mean -123 self.deltas[name] = sample -124 else: -125 for name, sample in sorted(zip(names, samples)): -126 self.shape[name] = len(self.idl[name]) -127 self.N += self.shape[name] -128 if len(sample) != self.shape[name]: -129 raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name])) -130 self.r_values[name] = np.mean(sample) -131 self.deltas[name] = sample - self.r_values[name] -132 self._value += self.shape[name] * self.r_values[name] -133 self._value /= self.N -134 -135 self._dvalue = 0.0 -136 self.ddvalue = 0.0 -137 self.reweighted = False -138 -139 self.tag = None +@@ -3230,167 +3249,167 @@ list of ranges or lists on which the samples are defined60 def __init__(self, samples, names, idl=None, **kwargs): + 61 """ Initialize Obs object. + 62 + 63 Parameters + 64 ---------- + 65 samples : list + 66 list of numpy arrays containing the Monte Carlo samples + 67 names : list + 68 list of strings labeling the individual samples + 69 idl : list, optional + 70 list of ranges or lists on which the samples are defined + 71 """ + 72 + 73 if kwargs.get("means") is None and len(samples): + 74 if len(samples) != len(names): + 75 raise Exception('Length of samples and names incompatible.') + 76 if idl is not None: + 77 if len(idl) != len(names): + 78 raise Exception('Length of idl incompatible with samples and names.') + 79 name_length = len(names) + 80 if name_length > 1: + 81 if name_length != len(set(names)): + 82 raise Exception('names are not unique.') + 83 if not all(isinstance(x, str) for x in names): + 84 raise TypeError('All names have to be strings.') + 85 else: + 86 if not isinstance(names[0], str): + 87 raise TypeError('All names have to be strings.') + 88 if min(len(x) for x in samples) <= 4: + 89 raise Exception('Samples have to have at least 5 entries.') + 90 + 91 self.names = sorted(names) + 92 self.shape = {} + 93 self.r_values = {} + 94 self.deltas = {} + 95 self._covobs = {} + 96 + 97 self._value = 0 + 98 self.N = 0 + 99 self.is_merged = {} +100 self.idl = {} +101 if idl is not None: +102 for name, idx in sorted(zip(names, idl)): +103 if isinstance(idx, range): +104 self.idl[name] = idx +105 elif isinstance(idx, (list, np.ndarray)): +106 dc = np.unique(np.diff(idx)) +107 if np.any(dc < 0): +108 raise Exception("Unsorted idx for idl[%s]" % (name)) +109 if len(dc) == 1: +110 self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0]) +111 else: +112 self.idl[name] = list(idx) +113 else: +114 raise Exception('incompatible type for idl[%s].' % (name)) +115 else: +116 for name, sample in sorted(zip(names, samples)): +117 self.idl[name] = range(1, len(sample) + 1) +118 +119 if kwargs.get("means") is not None: +120 for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))): +121 self.shape[name] = len(self.idl[name]) +122 self.N += self.shape[name] +123 self.r_values[name] = mean +124 self.deltas[name] = sample +125 else: +126 for name, sample in sorted(zip(names, samples)): +127 self.shape[name] = len(self.idl[name]) +128 self.N += self.shape[name] +129 if len(sample) != self.shape[name]: +130 raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name])) +131 self.r_values[name] = np.mean(sample) +132 self.deltas[name] = sample - self.r_values[name] +133 self._value += self.shape[name] * self.r_values[name] +134 self._value /= self.N +135 +136 self._dvalue = 0.0 +137 self.ddvalue = 0.0 +138 self.reweighted = False +139 +140 self.tag = None
174 def gamma_method(self, **kwargs): -175 """Estimate the error and related properties of the Obs. -176 -177 Parameters -178 ---------- -179 S : float -180 specifies a custom value for the parameter S (default 2.0). -181 If set to 0 it is assumed that the data exhibits no -182 autocorrelation. In this case the error estimates coincides -183 with the sample standard error. -184 tau_exp : float -185 positive value triggers the critical slowing down analysis -186 (default 0.0). -187 N_sigma : float -188 number of standard deviations from zero until the tail is -189 attached to the autocorrelation function (default 1). -190 fft : bool -191 determines whether the fft algorithm is used for the computation -192 of the autocorrelation function (default True) -193 """ -194 -195 e_content = self.e_content -196 self.e_dvalue = {} -197 self.e_ddvalue = {} -198 self.e_tauint = {} -199 self.e_dtauint = {} -200 self.e_windowsize = {} -201 self.e_n_tauint = {} -202 self.e_n_dtauint = {} -203 e_gamma = {} -204 self.e_rho = {} -205 self.e_drho = {} -206 self._dvalue = 0 -207 self.ddvalue = 0 -208 -209 self.S = {} -210 self.tau_exp = {} -211 self.N_sigma = {} -212 -213 if kwargs.get('fft') is False: -214 fft = False -215 else: -216 fft = True -217 -218 def _parse_kwarg(kwarg_name): -219 if kwarg_name in kwargs: -220 tmp = kwargs.get(kwarg_name) -221 if isinstance(tmp, (int, float)): -222 if tmp < 0: -223 raise Exception(kwarg_name + ' has to be larger or equal to 0.') -224 for e, e_name in enumerate(self.e_names): -225 getattr(self, kwarg_name)[e_name] = tmp -226 else: -227 raise TypeError(kwarg_name + ' is not in proper format.') -228 else: -229 for e, e_name in enumerate(self.e_names): -230 if e_name in getattr(Obs, kwarg_name + '_dict'): -231 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name] -232 else: -233 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global') -234 -235 _parse_kwarg('S') -236 _parse_kwarg('tau_exp') -237 _parse_kwarg('N_sigma') -238 -239 for e, e_name in enumerate(self.mc_names): -240 r_length = [] -241 for r_name in e_content[e_name]: -242 if isinstance(self.idl[r_name], range): -243 r_length.append(len(self.idl[r_name])) -244 else: -245 r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1)) -246 -247 e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]]) -248 w_max = max(r_length) // 2 -249 e_gamma[e_name] = np.zeros(w_max) -250 self.e_rho[e_name] = np.zeros(w_max) -251 self.e_drho[e_name] = np.zeros(w_max) -252 -253 for r_name in e_content[e_name]: -254 e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft) -255 -256 gamma_div = np.zeros(w_max) -257 for r_name in e_content[e_name]: -258 gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft) -259 gamma_div[gamma_div < 1] = 1.0 -260 e_gamma[e_name] /= gamma_div[:w_max] -261 -262 if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny: # Prevent division by zero -263 self.e_tauint[e_name] = 0.5 -264 self.e_dtauint[e_name] = 0.0 -265 self.e_dvalue[e_name] = 0.0 -266 self.e_ddvalue[e_name] = 0.0 -267 self.e_windowsize[e_name] = 0 -268 continue -269 -270 self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0] -271 self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:]))) -272 # Make sure no entry of tauint is smaller than 0.5 -273 self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps -274 # hep-lat/0306017 eq. (42) -275 self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) + 0.5 - self.e_n_tauint[e_name]) / e_N) -276 self.e_n_dtauint[e_name][0] = 0.0 -277 -278 def _compute_drho(i): -279 tmp = self.e_rho[e_name][i + 1:w_max] + np.concatenate([self.e_rho[e_name][i - 1::-1], self.e_rho[e_name][1:w_max - 2 * i]]) - 2 * self.e_rho[e_name][i] * self.e_rho[e_name][1:w_max - i] -280 self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N) -281 -282 _compute_drho(1) -283 if self.tau_exp[e_name] > 0: -284 texp = self.tau_exp[e_name] -285 # Critical slowing down analysis -286 if w_max // 2 <= 1: -287 raise Exception("Need at least 8 samples for tau_exp error analysis") -288 for n in range(1, w_max // 2): -289 _compute_drho(n + 1) -290 if (self.e_rho[e_name][n] - self.N_sigma[e_name] * self.e_drho[e_name][n]) < 0 or n >= w_max // 2 - 2: -291 # Bias correction hep-lat/0306017 eq. (49) included -292 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) + texp * np.abs(self.e_rho[e_name][n + 1]) # The absolute makes sure, that the tail contribution is always positive -293 self.e_dtauint[e_name] = np.sqrt(self.e_n_dtauint[e_name][n] ** 2 + texp ** 2 * self.e_drho[e_name][n + 1] ** 2) -294 # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2 -295 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) -296 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) -297 self.e_windowsize[e_name] = n -298 break -299 else: -300 if self.S[e_name] == 0.0: -301 self.e_tauint[e_name] = 0.5 -302 self.e_dtauint[e_name] = 0.0 -303 self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1)) -304 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N) -305 self.e_windowsize[e_name] = 0 -306 else: -307 # Standard automatic windowing procedure -308 tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][1:] + 1) / (2 * self.e_n_tauint[e_name][1:] - 1)) -309 g_w = np.exp(- np.arange(1, w_max) / tau) - tau / np.sqrt(np.arange(1, w_max) * e_N) -310 for n in range(1, w_max): -311 if n < w_max // 2 - 2: -312 _compute_drho(n + 1) -313 if g_w[n - 1] < 0 or n >= w_max - 1: -314 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) # Bias correction hep-lat/0306017 eq. (49) -315 self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n] -316 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) -317 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) -318 self.e_windowsize[e_name] = n -319 break -320 -321 self._dvalue += self.e_dvalue[e_name] ** 2 -322 self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2 -323 -324 for e_name in self.cov_names: -325 self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq()) -326 self.e_ddvalue[e_name] = 0 -327 self._dvalue += self.e_dvalue[e_name]**2 -328 -329 self._dvalue = np.sqrt(self._dvalue) -330 if self._dvalue == 0.0: -331 self.ddvalue = 0.0 -332 else: -333 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue -334 return +@@ -3429,66 +3448,66 @@ of the autocorrelation function (default True)175 def gamma_method(self, **kwargs): +176 """Estimate the error and related properties of the Obs. +177 +178 Parameters +179 ---------- +180 S : float +181 specifies a custom value for the parameter S (default 2.0). +182 If set to 0 it is assumed that the data exhibits no +183 autocorrelation. In this case the error estimates coincides +184 with the sample standard error. +185 tau_exp : float +186 positive value triggers the critical slowing down analysis +187 (default 0.0). +188 N_sigma : float +189 number of standard deviations from zero until the tail is +190 attached to the autocorrelation function (default 1). +191 fft : bool +192 determines whether the fft algorithm is used for the computation +193 of the autocorrelation function (default True) +194 """ +195 +196 e_content = self.e_content +197 self.e_dvalue = {} +198 self.e_ddvalue = {} +199 self.e_tauint = {} +200 self.e_dtauint = {} +201 self.e_windowsize = {} +202 self.e_n_tauint = {} +203 self.e_n_dtauint = {} +204 e_gamma = {} +205 self.e_rho = {} +206 self.e_drho = {} +207 self._dvalue = 0 +208 self.ddvalue = 0 +209 +210 self.S = {} +211 self.tau_exp = {} +212 self.N_sigma = {} +213 +214 if kwargs.get('fft') is False: +215 fft = False +216 else: +217 fft = True +218 +219 def _parse_kwarg(kwarg_name): +220 if kwarg_name in kwargs: +221 tmp = kwargs.get(kwarg_name) +222 if isinstance(tmp, (int, float)): +223 if tmp < 0: +224 raise Exception(kwarg_name + ' has to be larger or equal to 0.') +225 for e, e_name in enumerate(self.e_names): +226 getattr(self, kwarg_name)[e_name] = tmp +227 else: +228 raise TypeError(kwarg_name + ' is not in proper format.') +229 else: +230 for e, e_name in enumerate(self.e_names): +231 if e_name in getattr(Obs, kwarg_name + '_dict'): +232 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name] +233 else: +234 getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global') +235 +236 _parse_kwarg('S') +237 _parse_kwarg('tau_exp') +238 _parse_kwarg('N_sigma') +239 +240 for e, e_name in enumerate(self.mc_names): +241 r_length = [] +242 for r_name in e_content[e_name]: +243 if isinstance(self.idl[r_name], range): +244 r_length.append(len(self.idl[r_name])) +245 else: +246 r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1)) +247 +248 e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]]) +249 w_max = max(r_length) // 2 +250 e_gamma[e_name] = np.zeros(w_max) +251 self.e_rho[e_name] = np.zeros(w_max) +252 self.e_drho[e_name] = np.zeros(w_max) +253 +254 for r_name in e_content[e_name]: +255 e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft) +256 +257 gamma_div = np.zeros(w_max) +258 for r_name in e_content[e_name]: +259 gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft) +260 gamma_div[gamma_div < 1] = 1.0 +261 e_gamma[e_name] /= gamma_div[:w_max] +262 +263 if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny: # Prevent division by zero +264 self.e_tauint[e_name] = 0.5 +265 self.e_dtauint[e_name] = 0.0 +266 self.e_dvalue[e_name] = 0.0 +267 self.e_ddvalue[e_name] = 0.0 +268 self.e_windowsize[e_name] = 0 +269 continue +270 +271 self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0] +272 self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:]))) +273 # Make sure no entry of tauint is smaller than 0.5 +274 self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps +275 # hep-lat/0306017 eq. (42) +276 self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) + 0.5 - self.e_n_tauint[e_name]) / e_N) +277 self.e_n_dtauint[e_name][0] = 0.0 +278 +279 def _compute_drho(i): +280 tmp = self.e_rho[e_name][i + 1:w_max] + np.concatenate([self.e_rho[e_name][i - 1::-1], self.e_rho[e_name][1:w_max - 2 * i]]) - 2 * self.e_rho[e_name][i] * self.e_rho[e_name][1:w_max - i] +281 self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N) +282 +283 _compute_drho(1) +284 if self.tau_exp[e_name] > 0: +285 texp = self.tau_exp[e_name] +286 # Critical slowing down analysis +287 if w_max // 2 <= 1: +288 raise Exception("Need at least 8 samples for tau_exp error analysis") +289 for n in range(1, w_max // 2): +290 _compute_drho(n + 1) +291 if (self.e_rho[e_name][n] - self.N_sigma[e_name] * self.e_drho[e_name][n]) < 0 or n >= w_max // 2 - 2: +292 # Bias correction hep-lat/0306017 eq. (49) included +293 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) + texp * np.abs(self.e_rho[e_name][n + 1]) # The absolute makes sure, that the tail contribution is always positive +294 self.e_dtauint[e_name] = np.sqrt(self.e_n_dtauint[e_name][n] ** 2 + texp ** 2 * self.e_drho[e_name][n + 1] ** 2) +295 # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2 +296 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) +297 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) +298 self.e_windowsize[e_name] = n +299 break +300 else: +301 if self.S[e_name] == 0.0: +302 self.e_tauint[e_name] = 0.5 +303 self.e_dtauint[e_name] = 0.0 +304 self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1)) +305 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N) +306 self.e_windowsize[e_name] = 0 +307 else: +308 # Standard automatic windowing procedure +309 tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][1:] + 1) / (2 * self.e_n_tauint[e_name][1:] - 1)) +310 g_w = np.exp(- np.arange(1, w_max) / tau) - tau / np.sqrt(np.arange(1, w_max) * e_N) +311 for n in range(1, w_max): +312 if n < w_max // 2 - 2: +313 _compute_drho(n + 1) +314 if g_w[n - 1] < 0 or n >= w_max - 1: +315 self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n + 1) / e_N) / (1 + 1 / e_N) # Bias correction hep-lat/0306017 eq. (49) +316 self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n] +317 self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N) +318 self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n + 0.5) / e_N) +319 self.e_windowsize[e_name] = n +320 break +321 +322 self._dvalue += self.e_dvalue[e_name] ** 2 +323 self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2 +324 +325 for e_name in self.cov_names: +326 self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq()) +327 self.e_ddvalue[e_name] = 0 +328 self._dvalue += self.e_dvalue[e_name]**2 +329 +330 self._dvalue = np.sqrt(self._dvalue) +331 if self._dvalue == 0.0: +332 self.ddvalue = 0.0 +333 else: +334 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue +335 return
369 def details(self, ens_content=True): -370 """Output detailed properties of the Obs. -371 -372 Parameters -373 ---------- -374 ens_content : bool -375 print details about the ensembles and replica if true. -376 """ -377 if self.tag is not None: -378 print("Description:", self.tag) -379 if not hasattr(self, 'e_dvalue'): -380 print('Result\t %3.8e' % (self.value)) -381 else: -382 if self.value == 0.0: -383 percentage = np.nan -384 else: -385 percentage = np.abs(self._dvalue / self.value) * 100 -386 print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage)) -387 if len(self.e_names) > 1: -388 print(' Ensemble errors:') -389 for e_name in self.mc_names: -390 if len(self.e_names) > 1: -391 print('', e_name, '\t %3.8e +/- %3.8e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name])) -392 if self.tau_exp[e_name] > 0: -393 print(' t_int\t %3.8e +/- %3.8e tau_exp = %3.2f, N_sigma = %1.0i' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.tau_exp[e_name], self.N_sigma[e_name])) -394 else: -395 print(' t_int\t %3.8e +/- %3.8e S = %3.2f' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.S[e_name])) -396 for e_name in self.cov_names: -397 print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name])) -398 if ens_content is True: -399 if len(self.e_names) == 1: -400 print(self.N, 'samples in', len(self.e_names), 'ensemble:') -401 else: -402 print(self.N, 'samples in', len(self.e_names), 'ensembles:') -403 my_string_list = [] -404 for key, value in sorted(self.e_content.items()): -405 if key not in self.covobs: -406 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " -407 if len(value) == 1: -408 my_string += f': {self.shape[value[0]]} configurations' -409 if isinstance(self.idl[value[0]], range): -410 my_string += f' (from {self.idl[value[0]].start} to {self.idl[value[0]][-1]}' + int(self.idl[value[0]].step != 1) * f' in steps of {self.idl[value[0]].step}' + ')' -411 else: -412 my_string += ' (irregular range)' -413 else: -414 sublist = [] -415 for v in value: -416 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " -417 my_substring += f': {self.shape[v]} configurations' -418 if isinstance(self.idl[v], range): -419 my_substring += f' (from {self.idl[v].start} to {self.idl[v][-1]}' + int(self.idl[v].step != 1) * f' in steps of {self.idl[v].step}' + ')' -420 else: -421 my_substring += ' (irregular range)' -422 sublist.append(my_substring) -423 -424 my_string += '\n' + '\n'.join(sublist) -425 else: -426 my_string = ' ' + "\u00B7 Covobs '" + key + "' " -427 my_string_list.append(my_string) -428 print('\n'.join(my_string_list)) +@@ -3515,20 +3534,20 @@ print details about the ensembles and replica if true.370 def details(self, ens_content=True): +371 """Output detailed properties of the Obs. +372 +373 Parameters +374 ---------- +375 ens_content : bool +376 print details about the ensembles and replica if true. +377 """ +378 if self.tag is not None: +379 print("Description:", self.tag) +380 if not hasattr(self, 'e_dvalue'): +381 print('Result\t %3.8e' % (self.value)) +382 else: +383 if self.value == 0.0: +384 percentage = np.nan +385 else: +386 percentage = np.abs(self._dvalue / self.value) * 100 +387 print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage)) +388 if len(self.e_names) > 1: +389 print(' Ensemble errors:') +390 for e_name in self.mc_names: +391 if len(self.e_names) > 1: +392 print('', e_name, '\t %3.8e +/- %3.8e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name])) +393 if self.tau_exp[e_name] > 0: +394 print(' t_int\t %3.8e +/- %3.8e tau_exp = %3.2f, N_sigma = %1.0i' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.tau_exp[e_name], self.N_sigma[e_name])) +395 else: +396 print(' t_int\t %3.8e +/- %3.8e S = %3.2f' % (self.e_tauint[e_name], self.e_dtauint[e_name], self.S[e_name])) +397 for e_name in self.cov_names: +398 print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name])) +399 if ens_content is True: +400 if len(self.e_names) == 1: +401 print(self.N, 'samples in', len(self.e_names), 'ensemble:') +402 else: +403 print(self.N, 'samples in', len(self.e_names), 'ensembles:') +404 my_string_list = [] +405 for key, value in sorted(self.e_content.items()): +406 if key not in self.covobs: +407 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " +408 if len(value) == 1: +409 my_string += f': {self.shape[value[0]]} configurations' +410 if isinstance(self.idl[value[0]], range): +411 my_string += f' (from {self.idl[value[0]].start} to {self.idl[value[0]][-1]}' + int(self.idl[value[0]].step != 1) * f' in steps of {self.idl[value[0]].step}' + ')' +412 else: +413 my_string += ' (irregular range)' +414 else: +415 sublist = [] +416 for v in value: +417 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " +418 my_substring += f': {self.shape[v]} configurations' +419 if isinstance(self.idl[v], range): +420 my_substring += f' (from {self.idl[v].start} to {self.idl[v][-1]}' + int(self.idl[v].step != 1) * f' in steps of {self.idl[v].step}' + ')' +421 else: +422 my_substring += ' (irregular range)' +423 sublist.append(my_substring) +424 +425 my_string += '\n' + '\n'.join(sublist) +426 else: +427 my_string = ' ' + "\u00B7 Covobs '" + key + "' " +428 my_string_list.append(my_string) +429 print('\n'.join(my_string_list))
430 def reweight(self, weight): -431 """Reweight the obs with given rewighting factors. -432 -433 Parameters -434 ---------- -435 weight : Obs -436 Reweighting factor. An Observable that has to be defined on a superset of the -437 configurations in obs[i].idl for all i. -438 all_configs : bool -439 if True, the reweighted observables are normalized by the average of -440 the reweighting factor on all configurations in weight.idl and not -441 on the configurations in obs[i].idl. Default False. -442 """ -443 return reweight(weight, [self])[0] +@@ -3560,17 +3579,17 @@ on the configurations in obs[i].idl. Default False.431 def reweight(self, weight): +432 """Reweight the obs with given rewighting factors. +433 +434 Parameters +435 ---------- +436 weight : Obs +437 Reweighting factor. An Observable that has to be defined on a superset of the +438 configurations in obs[i].idl for all i. +439 all_configs : bool +440 if True, the reweighted observables are normalized by the average of +441 the reweighting factor on all configurations in weight.idl and not +442 on the configurations in obs[i].idl. Default False. +443 """ +444 return reweight(weight, [self])[0]
445 def is_zero_within_error(self, sigma=1): -446 """Checks whether the observable is zero within 'sigma' standard errors. -447 -448 Parameters -449 ---------- -450 sigma : int -451 Number of standard errors used for the check. -452 -453 Works only properly when the gamma method was run. -454 """ -455 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue +@@ -3598,15 +3617,15 @@ Number of standard errors used for the check.446 def is_zero_within_error(self, sigma=1): +447 """Checks whether the observable is zero within 'sigma' standard errors. +448 +449 Parameters +450 ---------- +451 sigma : int +452 Number of standard errors used for the check. +453 +454 Works only properly when the gamma method was run. +455 """ +456 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue
457 def is_zero(self, atol=1e-10): -458 """Checks whether the observable is zero within a given tolerance. -459 -460 Parameters -461 ---------- -462 atol : float -463 Absolute tolerance (for details see numpy documentation). -464 """ -465 return np.isclose(0.0, self.value, 1e-14, atol) and all(np.allclose(0.0, delta, 1e-14, atol) for delta in self.deltas.values()) and all(np.allclose(0.0, delta.errsq(), 1e-14, atol) for delta in self.covobs.values()) +@@ -3633,45 +3652,45 @@ Absolute tolerance (for details see numpy documentation).458 def is_zero(self, atol=1e-10): +459 """Checks whether the observable is zero within a given tolerance. +460 +461 Parameters +462 ---------- +463 atol : float +464 Absolute tolerance (for details see numpy documentation). +465 """ +466 return np.isclose(0.0, self.value, 1e-14, atol) and all(np.allclose(0.0, delta, 1e-14, atol) for delta in self.deltas.values()) and all(np.allclose(0.0, delta.errsq(), 1e-14, atol) for delta in self.covobs.values())
467 def plot_tauint(self, save=None): -468 """Plot integrated autocorrelation time for each ensemble. -469 -470 Parameters -471 ---------- -472 save : str -473 saves the figure to a file named 'save' if. -474 """ -475 if not hasattr(self, 'e_dvalue'): -476 raise Exception('Run the gamma method first.') -477 -478 for e, e_name in enumerate(self.mc_names): -479 fig = plt.figure() -480 plt.xlabel(r'$W$') -481 plt.ylabel(r'$\tau_\mathrm{int}$') -482 length = int(len(self.e_n_tauint[e_name])) -483 if self.tau_exp[e_name] > 0: -484 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] -485 x_help = np.arange(2 * self.tau_exp[e_name]) -486 y_help = (x_help + 1) * np.abs(self.e_rho[e_name][self.e_windowsize[e_name] + 1]) * (1 - x_help / (2 * (2 * self.tau_exp[e_name] - 1))) + base -487 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) -488 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') -489 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], -490 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) -491 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 -492 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) -493 else: -494 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) -495 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) -496 -497 plt.errorbar(np.arange(length)[:int(xmax) + 1], self.e_n_tauint[e_name][:int(xmax) + 1], yerr=self.e_n_dtauint[e_name][:int(xmax) + 1], linewidth=1, capsize=2, label=label) -498 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') -499 plt.legend() -500 plt.xlim(-0.5, xmax) -501 ylim = plt.ylim() -502 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) -503 plt.draw() -504 if save: -505 fig.savefig(save + "_" + str(e)) +@@ -3698,36 +3717,36 @@ saves the figure to a file named 'save' if.468 def plot_tauint(self, save=None): +469 """Plot integrated autocorrelation time for each ensemble. +470 +471 Parameters +472 ---------- +473 save : str +474 saves the figure to a file named 'save' if. +475 """ +476 if not hasattr(self, 'e_dvalue'): +477 raise Exception('Run the gamma method first.') +478 +479 for e, e_name in enumerate(self.mc_names): +480 fig = plt.figure() +481 plt.xlabel(r'$W$') +482 plt.ylabel(r'$\tau_\mathrm{int}$') +483 length = int(len(self.e_n_tauint[e_name])) +484 if self.tau_exp[e_name] > 0: +485 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] +486 x_help = np.arange(2 * self.tau_exp[e_name]) +487 y_help = (x_help + 1) * np.abs(self.e_rho[e_name][self.e_windowsize[e_name] + 1]) * (1 - x_help / (2 * (2 * self.tau_exp[e_name] - 1))) + base +488 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) +489 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') +490 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], +491 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) +492 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 +493 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) +494 else: +495 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) +496 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) +497 +498 plt.errorbar(np.arange(length)[:int(xmax) + 1], self.e_n_tauint[e_name][:int(xmax) + 1], yerr=self.e_n_dtauint[e_name][:int(xmax) + 1], linewidth=1, capsize=2, label=label) +499 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') +500 plt.legend() +501 plt.xlim(-0.5, xmax) +502 ylim = plt.ylim() +503 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) +504 plt.draw() +505 if save: +506 fig.savefig(save + "_" + str(e))
507 def plot_rho(self, save=None): -508 """Plot normalized autocorrelation function time for each ensemble. -509 -510 Parameters -511 ---------- -512 save : str -513 saves the figure to a file named 'save' if. -514 """ -515 if not hasattr(self, 'e_dvalue'): -516 raise Exception('Run the gamma method first.') -517 for e, e_name in enumerate(self.mc_names): -518 fig = plt.figure() -519 plt.xlabel('W') -520 plt.ylabel('rho') -521 length = int(len(self.e_drho[e_name])) -522 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) -523 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') -524 if self.tau_exp[e_name] > 0: -525 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], -526 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) -527 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 -528 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) -529 else: -530 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) -531 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) -532 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) -533 plt.xlim(-0.5, xmax) -534 plt.draw() -535 if save: -536 fig.savefig(save + "_" + str(e)) +@@ -3754,27 +3773,27 @@ saves the figure to a file named 'save' if.508 def plot_rho(self, save=None): +509 """Plot normalized autocorrelation function time for each ensemble. +510 +511 Parameters +512 ---------- +513 save : str +514 saves the figure to a file named 'save' if. +515 """ +516 if not hasattr(self, 'e_dvalue'): +517 raise Exception('Run the gamma method first.') +518 for e, e_name in enumerate(self.mc_names): +519 fig = plt.figure() +520 plt.xlabel('W') +521 plt.ylabel('rho') +522 length = int(len(self.e_drho[e_name])) +523 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) +524 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') +525 if self.tau_exp[e_name] > 0: +526 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], +527 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) +528 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 +529 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) +530 else: +531 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) +532 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) +533 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) +534 plt.xlim(-0.5, xmax) +535 plt.draw() +536 if save: +537 fig.savefig(save + "_" + str(e))
538 def plot_rep_dist(self): -539 """Plot replica distribution for each ensemble with more than one replicum.""" -540 if not hasattr(self, 'e_dvalue'): -541 raise Exception('Run the gamma method first.') -542 for e, e_name in enumerate(self.mc_names): -543 if len(self.e_content[e_name]) == 1: -544 print('No replica distribution for a single replicum (', e_name, ')') -545 continue -546 r_length = [] -547 sub_r_mean = 0 -548 for r, r_name in enumerate(self.e_content[e_name]): -549 r_length.append(len(self.deltas[r_name])) -550 sub_r_mean += self.shape[r_name] * self.r_values[r_name] -551 e_N = np.sum(r_length) -552 sub_r_mean /= e_N -553 arr = np.zeros(len(self.e_content[e_name])) -554 for r, r_name in enumerate(self.e_content[e_name]): -555 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) -556 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) -557 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') -558 plt.draw() +@@ -3794,37 +3813,37 @@ saves the figure to a file named 'save' if.539 def plot_rep_dist(self): +540 """Plot replica distribution for each ensemble with more than one replicum.""" +541 if not hasattr(self, 'e_dvalue'): +542 raise Exception('Run the gamma method first.') +543 for e, e_name in enumerate(self.mc_names): +544 if len(self.e_content[e_name]) == 1: +545 print('No replica distribution for a single replicum (', e_name, ')') +546 continue +547 r_length = [] +548 sub_r_mean = 0 +549 for r, r_name in enumerate(self.e_content[e_name]): +550 r_length.append(len(self.deltas[r_name])) +551 sub_r_mean += self.shape[r_name] * self.r_values[r_name] +552 e_N = np.sum(r_length) +553 sub_r_mean /= e_N +554 arr = np.zeros(len(self.e_content[e_name])) +555 for r, r_name in enumerate(self.e_content[e_name]): +556 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) +557 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) +558 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') +559 plt.draw()
560 def plot_history(self, expand=True): -561 """Plot derived Monte Carlo history for each ensemble -562 -563 Parameters -564 ---------- -565 expand : bool -566 show expanded history for irregular Monte Carlo chains (default: True). -567 """ -568 for e, e_name in enumerate(self.mc_names): -569 plt.figure() -570 r_length = [] -571 tmp = [] -572 tmp_expanded = [] -573 for r, r_name in enumerate(self.e_content[e_name]): -574 tmp.append(self.deltas[r_name] + self.r_values[r_name]) -575 if expand: -576 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name]) -577 r_length.append(len(tmp_expanded[-1])) -578 else: -579 r_length.append(len(tmp[-1])) -580 e_N = np.sum(r_length) -581 x = np.arange(e_N) -582 y_test = np.concatenate(tmp, axis=0) -583 if expand: -584 y = np.concatenate(tmp_expanded, axis=0) -585 else: -586 y = y_test -587 plt.errorbar(x, y, fmt='.', markersize=3) -588 plt.xlim(-0.5, e_N - 0.5) -589 plt.title(e_name + f'\nskew: {skew(y_test):.3f} (p={skewtest(y_test).pvalue:.3f}), kurtosis: {kurtosis(y_test):.3f} (p={kurtosistest(y_test).pvalue:.3f})') -590 plt.draw() +@@ -3851,29 +3870,29 @@ show expanded history for irregular Monte Carlo chains (default: True).561 def plot_history(self, expand=True): +562 """Plot derived Monte Carlo history for each ensemble +563 +564 Parameters +565 ---------- +566 expand : bool +567 show expanded history for irregular Monte Carlo chains (default: True). +568 """ +569 for e, e_name in enumerate(self.mc_names): +570 plt.figure() +571 r_length = [] +572 tmp = [] +573 tmp_expanded = [] +574 for r, r_name in enumerate(self.e_content[e_name]): +575 tmp.append(self.deltas[r_name] + self.r_values[r_name]) +576 if expand: +577 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name]) +578 r_length.append(len(tmp_expanded[-1])) +579 else: +580 r_length.append(len(tmp[-1])) +581 e_N = np.sum(r_length) +582 x = np.arange(e_N) +583 y_test = np.concatenate(tmp, axis=0) +584 if expand: +585 y = np.concatenate(tmp_expanded, axis=0) +586 else: +587 y = y_test +588 plt.errorbar(x, y, fmt='.', markersize=3) +589 plt.xlim(-0.5, e_N - 0.5) +590 plt.title(e_name + f'\nskew: {skew(y_test):.3f} (p={skewtest(y_test).pvalue:.3f}), kurtosis: {kurtosis(y_test):.3f} (p={kurtosistest(y_test).pvalue:.3f})') +591 plt.draw()
592 def plot_piechart(self, save=None): -593 """Plot piechart which shows the fractional contribution of each -594 ensemble to the error and returns a dictionary containing the fractions. -595 -596 Parameters -597 ---------- -598 save : str -599 saves the figure to a file named 'save' if. -600 """ -601 if not hasattr(self, 'e_dvalue'): -602 raise Exception('Run the gamma method first.') -603 if np.isclose(0.0, self._dvalue, atol=1e-15): -604 raise Exception('Error is 0.0') -605 labels = self.e_names -606 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 -607 fig1, ax1 = plt.subplots() -608 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) -609 ax1.axis('equal') -610 plt.draw() -611 if save: -612 fig1.savefig(save) -613 -614 return dict(zip(self.e_names, sizes)) +@@ -3901,34 +3920,34 @@ saves the figure to a file named 'save' if.593 def plot_piechart(self, save=None): +594 """Plot piechart which shows the fractional contribution of each +595 ensemble to the error and returns a dictionary containing the fractions. +596 +597 Parameters +598 ---------- +599 save : str +600 saves the figure to a file named 'save' if. +601 """ +602 if not hasattr(self, 'e_dvalue'): +603 raise Exception('Run the gamma method first.') +604 if np.isclose(0.0, self._dvalue, atol=1e-15): +605 raise Exception('Error is 0.0') +606 labels = self.e_names +607 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 +608 fig1, ax1 = plt.subplots() +609 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) +610 ax1.axis('equal') +611 plt.draw() +612 if save: +613 fig1.savefig(save) +614 +615 return dict(zip(self.e_names, sizes))
616 def dump(self, filename, datatype="json.gz", description="", **kwargs): -617 """Dump the Obs to a file 'name' of chosen format. -618 -619 Parameters -620 ---------- -621 filename : str -622 name of the file to be saved. -623 datatype : str -624 Format of the exported file. Supported formats include -625 "json.gz" and "pickle" -626 description : str -627 Description for output file, only relevant for json.gz format. -628 path : str -629 specifies a custom path for the file (default '.') -630 """ -631 if 'path' in kwargs: -632 file_name = kwargs.get('path') + '/' + filename -633 else: -634 file_name = filename -635 -636 if datatype == "json.gz": -637 from .input.json import dump_to_json -638 dump_to_json([self], file_name, description=description) -639 elif datatype == "pickle": -640 with open(file_name + '.p', 'wb') as fb: -641 pickle.dump(self, fb) -642 else: -643 raise Exception("Unknown datatype " + str(datatype)) +@@ -3962,31 +3981,31 @@ specifies a custom path for the file (default '.')617 def dump(self, filename, datatype="json.gz", description="", **kwargs): +618 """Dump the Obs to a file 'name' of chosen format. +619 +620 Parameters +621 ---------- +622 filename : str +623 name of the file to be saved. +624 datatype : str +625 Format of the exported file. Supported formats include +626 "json.gz" and "pickle" +627 description : str +628 Description for output file, only relevant for json.gz format. +629 path : str +630 specifies a custom path for the file (default '.') +631 """ +632 if 'path' in kwargs: +633 file_name = kwargs.get('path') + '/' + filename +634 else: +635 file_name = filename +636 +637 if datatype == "json.gz": +638 from .input.json import dump_to_json +639 dump_to_json([self], file_name, description=description) +640 elif datatype == "pickle": +641 with open(file_name + '.p', 'wb') as fb: +642 pickle.dump(self, fb) +643 else: +644 raise Exception("Unknown datatype " + str(datatype))
645 def export_jackknife(self): -646 """Export jackknife samples from the Obs -647 -648 Returns -649 ------- -650 numpy.ndarray -651 Returns a numpy array of length N + 1 where N is the number of samples -652 for the given ensemble and replicum. The zeroth entry of the array contains -653 the mean value of the Obs, entries 1 to N contain the N jackknife samples -654 derived from the Obs. The current implementation only works for observables -655 defined on exactly one ensemble and replicum. The derived jackknife samples -656 should agree with samples from a full jackknife analysis up to O(1/N). -657 """ -658 -659 if len(self.names) != 1: -660 raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") -661 -662 name = self.names[0] -663 full_data = self.deltas[name] + self.r_values[name] -664 n = full_data.size -665 mean = self.value -666 tmp_jacks = np.zeros(n + 1) -667 tmp_jacks[0] = mean -668 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) -669 return tmp_jacks +@@ -4017,8 +4036,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).646 def export_jackknife(self): +647 """Export jackknife samples from the Obs +648 +649 Returns +650 ------- +651 numpy.ndarray +652 Returns a numpy array of length N + 1 where N is the number of samples +653 for the given ensemble and replicum. The zeroth entry of the array contains +654 the mean value of the Obs, entries 1 to N contain the N jackknife samples +655 derived from the Obs. The current implementation only works for observables +656 defined on exactly one ensemble and replicum. The derived jackknife samples +657 should agree with samples from a full jackknife analysis up to O(1/N). +658 """ +659 +660 if len(self.names) != 1: +661 raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") +662 +663 name = self.names[0] +664 full_data = self.deltas[name] + self.r_values[name] +665 n = full_data.size +666 mean = self.value +667 tmp_jacks = np.zeros(n + 1) +668 tmp_jacks[0] = mean +669 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) +670 return tmp_jacks
796 def sqrt(self): -797 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) + @@ -4036,8 +4055,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
799 def log(self): -800 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) + @@ -4055,8 +4074,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
802 def exp(self): -803 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) + @@ -4074,8 +4093,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
805 def sin(self): -806 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) + @@ -4093,8 +4112,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
808 def cos(self): -809 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) + @@ -4112,8 +4131,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
811 def tan(self): -812 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) + @@ -4131,8 +4150,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
814 def arcsin(self): -815 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) + @@ -4150,8 +4169,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
817 def arccos(self): -818 return derived_observable(lambda x: anp.arccos(x[0]), [self]) + @@ -4169,8 +4188,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
820 def arctan(self): -821 return derived_observable(lambda x: anp.arctan(x[0]), [self]) + @@ -4188,8 +4207,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
823 def sinh(self): -824 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) + @@ -4207,8 +4226,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
826 def cosh(self): -827 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) + @@ -4226,8 +4245,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
829 def tanh(self): -830 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) + @@ -4245,8 +4264,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
832 def arcsinh(self): -833 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) + @@ -4264,8 +4283,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
835 def arccosh(self): -836 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) + @@ -4283,8 +4302,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
838 def arctanh(self): -839 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) + @@ -4435,115 +4454,115 @@ should agree with samples from a full jackknife analysis up to O(1/N).
842class CObs: -843 """Class for a complex valued observable.""" -844 __slots__ = ['_real', '_imag', 'tag'] -845 -846 def __init__(self, real, imag=0.0): -847 self._real = real -848 self._imag = imag -849 self.tag = None -850 -851 @property -852 def real(self): -853 return self._real -854 -855 @property -856 def imag(self): -857 return self._imag -858 -859 def gamma_method(self, **kwargs): -860 """Executes the gamma_method for the real and the imaginary part.""" -861 if isinstance(self.real, Obs): -862 self.real.gamma_method(**kwargs) -863 if isinstance(self.imag, Obs): -864 self.imag.gamma_method(**kwargs) -865 -866 def is_zero(self): -867 """Checks whether both real and imaginary part are zero within machine precision.""" -868 return self.real == 0.0 and self.imag == 0.0 -869 -870 def conjugate(self): -871 return CObs(self.real, -self.imag) -872 -873 def __add__(self, other): -874 if isinstance(other, np.ndarray): -875 return other + self -876 elif hasattr(other, 'real') and hasattr(other, 'imag'): -877 return CObs(self.real + other.real, -878 self.imag + other.imag) -879 else: -880 return CObs(self.real + other, self.imag) -881 -882 def __radd__(self, y): -883 return self + y -884 -885 def __sub__(self, other): -886 if isinstance(other, np.ndarray): -887 return -1 * (other - self) -888 elif hasattr(other, 'real') and hasattr(other, 'imag'): -889 return CObs(self.real - other.real, self.imag - other.imag) -890 else: -891 return CObs(self.real - other, self.imag) -892 -893 def __rsub__(self, other): -894 return -1 * (self - other) -895 -896 def __mul__(self, other): -897 if isinstance(other, np.ndarray): -898 return other * self -899 elif hasattr(other, 'real') and hasattr(other, 'imag'): -900 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): -901 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], -902 [self.real, other.real, self.imag, other.imag], -903 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), -904 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], -905 [self.real, other.real, self.imag, other.imag], -906 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) -907 elif getattr(other, 'imag', 0) != 0: -908 return CObs(self.real * other.real - self.imag * other.imag, -909 self.imag * other.real + self.real * other.imag) -910 else: -911 return CObs(self.real * other.real, self.imag * other.real) -912 else: -913 return CObs(self.real * other, self.imag * other) -914 -915 def __rmul__(self, other): -916 return self * other -917 -918 def __truediv__(self, other): -919 if isinstance(other, np.ndarray): -920 return 1 / (other / self) -921 elif hasattr(other, 'real') and hasattr(other, 'imag'): -922 r = other.real ** 2 + other.imag ** 2 -923 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) -924 else: -925 return CObs(self.real / other, self.imag / other) -926 -927 def __rtruediv__(self, other): -928 r = self.real ** 2 + self.imag ** 2 -929 if hasattr(other, 'real') and hasattr(other, 'imag'): -930 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) -931 else: -932 return CObs(self.real * other / r, -self.imag * other / r) -933 -934 def __abs__(self): -935 return np.sqrt(self.real**2 + self.imag**2) +@@ -4561,10 +4580,10 @@ should agree with samples from a full jackknife analysis up to O(1/N).852class CObs: +853 """Class for a complex valued observable.""" +854 __slots__ = ['_real', '_imag', 'tag'] +855 +856 def __init__(self, real, imag=0.0): +857 self._real = real +858 self._imag = imag +859 self.tag = None +860 +861 @property +862 def real(self): +863 return self._real +864 +865 @property +866 def imag(self): +867 return self._imag +868 +869 def gamma_method(self, **kwargs): +870 """Executes the gamma_method for the real and the imaginary part.""" +871 if isinstance(self.real, Obs): +872 self.real.gamma_method(**kwargs) +873 if isinstance(self.imag, Obs): +874 self.imag.gamma_method(**kwargs) +875 +876 def is_zero(self): +877 """Checks whether both real and imaginary part are zero within machine precision.""" +878 return self.real == 0.0 and self.imag == 0.0 +879 +880 def conjugate(self): +881 return CObs(self.real, -self.imag) +882 +883 def __add__(self, other): +884 if isinstance(other, np.ndarray): +885 return other + self +886 elif hasattr(other, 'real') and hasattr(other, 'imag'): +887 return CObs(self.real + other.real, +888 self.imag + other.imag) +889 else: +890 return CObs(self.real + other, self.imag) +891 +892 def __radd__(self, y): +893 return self + y +894 +895 def __sub__(self, other): +896 if isinstance(other, np.ndarray): +897 return -1 * (other - self) +898 elif hasattr(other, 'real') and hasattr(other, 'imag'): +899 return CObs(self.real - other.real, self.imag - other.imag) +900 else: +901 return CObs(self.real - other, self.imag) +902 +903 def __rsub__(self, other): +904 return -1 * (self - other) +905 +906 def __mul__(self, other): +907 if isinstance(other, np.ndarray): +908 return other * self +909 elif hasattr(other, 'real') and hasattr(other, 'imag'): +910 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): +911 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], +912 [self.real, other.real, self.imag, other.imag], +913 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), +914 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], +915 [self.real, other.real, self.imag, other.imag], +916 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) +917 elif getattr(other, 'imag', 0) != 0: +918 return CObs(self.real * other.real - self.imag * other.imag, +919 self.imag * other.real + self.real * other.imag) +920 else: +921 return CObs(self.real * other.real, self.imag * other.real) +922 else: +923 return CObs(self.real * other, self.imag * other) +924 +925 def __rmul__(self, other): +926 return self * other +927 +928 def __truediv__(self, other): +929 if isinstance(other, np.ndarray): +930 return 1 / (other / self) +931 elif hasattr(other, 'real') and hasattr(other, 'imag'): +932 r = other.real ** 2 + other.imag ** 2 +933 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) +934 else: +935 return CObs(self.real / other, self.imag / other) 936 -937 def __pos__(self): -938 return self -939 -940 def __neg__(self): -941 return -1 * self -942 -943 def __eq__(self, other): -944 return self.real == other.real and self.imag == other.imag -945 -946 def __str__(self): -947 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' -948 -949 def __repr__(self): -950 return 'CObs[' + str(self) + ']' +937 def __rtruediv__(self, other): +938 r = self.real ** 2 + self.imag ** 2 +939 if hasattr(other, 'real') and hasattr(other, 'imag'): +940 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) +941 else: +942 return CObs(self.real * other / r, -self.imag * other / r) +943 +944 def __abs__(self): +945 return np.sqrt(self.real**2 + self.imag**2) +946 +947 def __pos__(self): +948 return self +949 +950 def __neg__(self): +951 return -1 * self +952 +953 def __eq__(self, other): +954 return self.real == other.real and self.imag == other.imag +955 +956 def __str__(self): +957 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' +958 +959 def __repr__(self): +960 return 'CObs[' + str(self) + ']'
846 def __init__(self, real, imag=0.0): -847 self._real = real -848 self._imag = imag -849 self.tag = None + @@ -4615,12 +4634,12 @@ should agree with samples from a full jackknife analysis up to O(1/N).
859 def gamma_method(self, **kwargs): -860 """Executes the gamma_method for the real and the imaginary part.""" -861 if isinstance(self.real, Obs): -862 self.real.gamma_method(**kwargs) -863 if isinstance(self.imag, Obs): -864 self.imag.gamma_method(**kwargs) + @@ -4640,9 +4659,9 @@ should agree with samples from a full jackknife analysis up to O(1/N).
866 def is_zero(self): -867 """Checks whether both real and imaginary part are zero within machine precision.""" -868 return self.real == 0.0 and self.imag == 0.0 + @@ -4662,8 +4681,8 @@ should agree with samples from a full jackknife analysis up to O(1/N).
870 def conjugate(self): -871 return CObs(self.real, -self.imag) + @@ -4682,184 +4701,184 @@ should agree with samples from a full jackknife analysis up to O(1/N).
1119def derived_observable(func, data, array_mode=False, **kwargs): -1120 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. -1121 -1122 Parameters -1123 ---------- -1124 func : object -1125 arbitrary function of the form func(data, **kwargs). For the -1126 automatic differentiation to work, all numpy functions have to have -1127 the autograd wrapper (use 'import autograd.numpy as anp'). -1128 data : list -1129 list of Obs, e.g. [obs1, obs2, obs3]. -1130 num_grad : bool -1131 if True, numerical derivatives are used instead of autograd -1132 (default False). To control the numerical differentiation the -1133 kwargs of numdifftools.step_generators.MaxStepGenerator -1134 can be used. -1135 man_grad : list -1136 manually supply a list or an array which contains the jacobian -1137 of func. Use cautiously, supplying the wrong derivative will -1138 not be intercepted. -1139 -1140 Notes -1141 ----- -1142 For simple mathematical operations it can be practical to use anonymous -1143 functions. For the ratio of two observables one can e.g. use -1144 -1145 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) -1146 """ -1147 -1148 data = np.asarray(data) -1149 raveled_data = data.ravel() -1150 -1151 # Workaround for matrix operations containing non Obs data -1152 if not all(isinstance(x, Obs) for x in raveled_data): -1153 for i in range(len(raveled_data)): -1154 if isinstance(raveled_data[i], (int, float)): -1155 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") -1156 -1157 allcov = {} -1158 for o in raveled_data: -1159 for name in o.cov_names: -1160 if name in allcov: -1161 if not np.allclose(allcov[name], o.covobs[name].cov): -1162 raise Exception('Inconsistent covariance matrices for %s!' % (name)) -1163 else: -1164 allcov[name] = o.covobs[name].cov -1165 -1166 n_obs = len(raveled_data) -1167 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) -1168 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) -1169 new_sample_names = sorted(set(new_names) - set(new_cov_names)) -1170 -1171 is_merged = {name: (len(list(filter(lambda o: o.is_merged.get(name, False) is True, raveled_data))) > 0) for name in new_sample_names} -1172 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 -1173 -1174 if data.ndim == 1: -1175 values = np.array([o.value for o in data]) -1176 else: -1177 values = np.vectorize(lambda x: x.value)(data) -1178 -1179 new_values = func(values, **kwargs) +@@ -4906,47 +4925,47 @@ functions. For the ratio of two observables one can e.g. use1129def derived_observable(func, data, array_mode=False, **kwargs): +1130 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. +1131 +1132 Parameters +1133 ---------- +1134 func : object +1135 arbitrary function of the form func(data, **kwargs). For the +1136 automatic differentiation to work, all numpy functions have to have +1137 the autograd wrapper (use 'import autograd.numpy as anp'). +1138 data : list +1139 list of Obs, e.g. [obs1, obs2, obs3]. +1140 num_grad : bool +1141 if True, numerical derivatives are used instead of autograd +1142 (default False). To control the numerical differentiation the +1143 kwargs of numdifftools.step_generators.MaxStepGenerator +1144 can be used. +1145 man_grad : list +1146 manually supply a list or an array which contains the jacobian +1147 of func. Use cautiously, supplying the wrong derivative will +1148 not be intercepted. +1149 +1150 Notes +1151 ----- +1152 For simple mathematical operations it can be practical to use anonymous +1153 functions. For the ratio of two observables one can e.g. use +1154 +1155 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) +1156 """ +1157 +1158 data = np.asarray(data) +1159 raveled_data = data.ravel() +1160 +1161 # Workaround for matrix operations containing non Obs data +1162 if not all(isinstance(x, Obs) for x in raveled_data): +1163 for i in range(len(raveled_data)): +1164 if isinstance(raveled_data[i], (int, float)): +1165 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") +1166 +1167 allcov = {} +1168 for o in raveled_data: +1169 for name in o.cov_names: +1170 if name in allcov: +1171 if not np.allclose(allcov[name], o.covobs[name].cov): +1172 raise Exception('Inconsistent covariance matrices for %s!' % (name)) +1173 else: +1174 allcov[name] = o.covobs[name].cov +1175 +1176 n_obs = len(raveled_data) +1177 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) +1178 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) +1179 new_sample_names = sorted(set(new_names) - set(new_cov_names)) 1180 -1181 multi = int(isinstance(new_values, np.ndarray)) -1182 -1183 new_r_values = {} -1184 new_idl_d = {} -1185 for name in new_sample_names: -1186 idl = [] -1187 tmp_values = np.zeros(n_obs) -1188 for i, item in enumerate(raveled_data): -1189 tmp_values[i] = item.r_values.get(name, item.value) -1190 tmp_idl = item.idl.get(name) -1191 if tmp_idl is not None: -1192 idl.append(tmp_idl) -1193 if multi > 0: -1194 tmp_values = np.array(tmp_values).reshape(data.shape) -1195 new_r_values[name] = func(tmp_values, **kwargs) -1196 new_idl_d[name] = _merge_idx(idl) -1197 if not is_merged[name]: -1198 is_merged[name] = (1 != len(set([len(idx) for idx in [*idl, new_idl_d[name]]]))) -1199 -1200 if 'man_grad' in kwargs: -1201 deriv = np.asarray(kwargs.get('man_grad')) -1202 if new_values.shape + data.shape != deriv.shape: -1203 raise Exception('Manual derivative does not have correct shape.') -1204 elif kwargs.get('num_grad') is True: -1205 if multi > 0: -1206 raise Exception('Multi mode currently not supported for numerical derivative') -1207 options = { -1208 'base_step': 0.1, -1209 'step_ratio': 2.5} -1210 for key in options.keys(): -1211 kwarg = kwargs.get(key) -1212 if kwarg is not None: -1213 options[key] = kwarg -1214 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) -1215 if tmp_df.size == 1: -1216 deriv = np.array([tmp_df.real]) -1217 else: -1218 deriv = tmp_df.real -1219 else: -1220 deriv = jacobian(func)(values, **kwargs) -1221 -1222 final_result = np.zeros(new_values.shape, dtype=object) -1223 -1224 if array_mode is True: -1225 -1226 class _Zero_grad(): -1227 def __init__(self, N): -1228 self.grad = np.zeros((N, 1)) -1229 -1230 new_covobs_lengths = dict(set([y for x in [[(n, o.covobs[n].N) for n in o.cov_names] for o in raveled_data] for y in x])) -1231 d_extracted = {} -1232 g_extracted = {} -1233 for name in new_sample_names: -1234 d_extracted[name] = [] -1235 ens_length = len(new_idl_d[name]) -1236 for i_dat, dat in enumerate(data): -1237 d_extracted[name].append(np.array([_expand_deltas_for_merge(o.deltas.get(name, np.zeros(ens_length)), o.idl.get(name, new_idl_d[name]), o.shape.get(name, ens_length), new_idl_d[name]) for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (ens_length, ))) -1238 for name in new_cov_names: -1239 g_extracted[name] = [] -1240 zero_grad = _Zero_grad(new_covobs_lengths[name]) -1241 for i_dat, dat in enumerate(data): -1242 g_extracted[name].append(np.array([o.covobs.get(name, zero_grad).grad for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (new_covobs_lengths[name], 1))) -1243 -1244 for i_val, new_val in np.ndenumerate(new_values): -1245 new_deltas = {} -1246 new_grad = {} -1247 if array_mode is True: -1248 for name in new_sample_names: -1249 ens_length = d_extracted[name][0].shape[-1] -1250 new_deltas[name] = np.zeros(ens_length) -1251 for i_dat, dat in enumerate(d_extracted[name]): -1252 new_deltas[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) -1253 for name in new_cov_names: -1254 new_grad[name] = 0 -1255 for i_dat, dat in enumerate(g_extracted[name]): -1256 new_grad[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) -1257 else: -1258 for j_obs, obs in np.ndenumerate(data): -1259 for name in obs.names: -1260 if name in obs.cov_names: -1261 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad -1262 else: -1263 new_deltas[name] = new_deltas.get(name, 0) + deriv[i_val + j_obs] * _expand_deltas_for_merge(obs.deltas[name], obs.idl[name], obs.shape[name], new_idl_d[name]) -1264 -1265 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} -1266 -1267 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): -1268 raise Exception('The same name has been used for deltas and covobs!') -1269 new_samples = [] -1270 new_means = [] -1271 new_idl = [] -1272 new_names_obs = [] -1273 for name in new_names: -1274 if name not in new_covobs: -1275 if is_merged[name]: -1276 filtered_deltas, filtered_idl_d = _filter_zeroes(new_deltas[name], new_idl_d[name]) -1277 else: -1278 filtered_deltas = new_deltas[name] -1279 filtered_idl_d = new_idl_d[name] -1280 -1281 new_samples.append(filtered_deltas) -1282 new_idl.append(filtered_idl_d) -1283 new_means.append(new_r_values[name][i_val]) -1284 new_names_obs.append(name) -1285 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) -1286 for name in new_covobs: -1287 final_result[i_val].names.append(name) -1288 final_result[i_val]._covobs = new_covobs -1289 final_result[i_val]._value = new_val -1290 final_result[i_val].is_merged = is_merged -1291 final_result[i_val].reweighted = reweighted -1292 -1293 if multi == 0: -1294 final_result = final_result.item() -1295 -1296 return final_result +1181 is_merged = {name: (len(list(filter(lambda o: o.is_merged.get(name, False) is True, raveled_data))) > 0) for name in new_sample_names} +1182 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 +1183 +1184 if data.ndim == 1: +1185 values = np.array([o.value for o in data]) +1186 else: +1187 values = np.vectorize(lambda x: x.value)(data) +1188 +1189 new_values = func(values, **kwargs) +1190 +1191 multi = int(isinstance(new_values, np.ndarray)) +1192 +1193 new_r_values = {} +1194 new_idl_d = {} +1195 for name in new_sample_names: +1196 idl = [] +1197 tmp_values = np.zeros(n_obs) +1198 for i, item in enumerate(raveled_data): +1199 tmp_values[i] = item.r_values.get(name, item.value) +1200 tmp_idl = item.idl.get(name) +1201 if tmp_idl is not None: +1202 idl.append(tmp_idl) +1203 if multi > 0: +1204 tmp_values = np.array(tmp_values).reshape(data.shape) +1205 new_r_values[name] = func(tmp_values, **kwargs) +1206 new_idl_d[name] = _merge_idx(idl) +1207 if not is_merged[name]: +1208 is_merged[name] = (1 != len(set([len(idx) for idx in [*idl, new_idl_d[name]]]))) +1209 +1210 if 'man_grad' in kwargs: +1211 deriv = np.asarray(kwargs.get('man_grad')) +1212 if new_values.shape + data.shape != deriv.shape: +1213 raise Exception('Manual derivative does not have correct shape.') +1214 elif kwargs.get('num_grad') is True: +1215 if multi > 0: +1216 raise Exception('Multi mode currently not supported for numerical derivative') +1217 options = { +1218 'base_step': 0.1, +1219 'step_ratio': 2.5} +1220 for key in options.keys(): +1221 kwarg = kwargs.get(key) +1222 if kwarg is not None: +1223 options[key] = kwarg +1224 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) +1225 if tmp_df.size == 1: +1226 deriv = np.array([tmp_df.real]) +1227 else: +1228 deriv = tmp_df.real +1229 else: +1230 deriv = jacobian(func)(values, **kwargs) +1231 +1232 final_result = np.zeros(new_values.shape, dtype=object) +1233 +1234 if array_mode is True: +1235 +1236 class _Zero_grad(): +1237 def __init__(self, N): +1238 self.grad = np.zeros((N, 1)) +1239 +1240 new_covobs_lengths = dict(set([y for x in [[(n, o.covobs[n].N) for n in o.cov_names] for o in raveled_data] for y in x])) +1241 d_extracted = {} +1242 g_extracted = {} +1243 for name in new_sample_names: +1244 d_extracted[name] = [] +1245 ens_length = len(new_idl_d[name]) +1246 for i_dat, dat in enumerate(data): +1247 d_extracted[name].append(np.array([_expand_deltas_for_merge(o.deltas.get(name, np.zeros(ens_length)), o.idl.get(name, new_idl_d[name]), o.shape.get(name, ens_length), new_idl_d[name]) for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (ens_length, ))) +1248 for name in new_cov_names: +1249 g_extracted[name] = [] +1250 zero_grad = _Zero_grad(new_covobs_lengths[name]) +1251 for i_dat, dat in enumerate(data): +1252 g_extracted[name].append(np.array([o.covobs.get(name, zero_grad).grad for o in dat.reshape(np.prod(dat.shape))]).reshape(dat.shape + (new_covobs_lengths[name], 1))) +1253 +1254 for i_val, new_val in np.ndenumerate(new_values): +1255 new_deltas = {} +1256 new_grad = {} +1257 if array_mode is True: +1258 for name in new_sample_names: +1259 ens_length = d_extracted[name][0].shape[-1] +1260 new_deltas[name] = np.zeros(ens_length) +1261 for i_dat, dat in enumerate(d_extracted[name]): +1262 new_deltas[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) +1263 for name in new_cov_names: +1264 new_grad[name] = 0 +1265 for i_dat, dat in enumerate(g_extracted[name]): +1266 new_grad[name] += np.tensordot(deriv[i_val + (i_dat, )], dat) +1267 else: +1268 for j_obs, obs in np.ndenumerate(data): +1269 for name in obs.names: +1270 if name in obs.cov_names: +1271 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad +1272 else: +1273 new_deltas[name] = new_deltas.get(name, 0) + deriv[i_val + j_obs] * _expand_deltas_for_merge(obs.deltas[name], obs.idl[name], obs.shape[name], new_idl_d[name]) +1274 +1275 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} +1276 +1277 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): +1278 raise Exception('The same name has been used for deltas and covobs!') +1279 new_samples = [] +1280 new_means = [] +1281 new_idl = [] +1282 new_names_obs = [] +1283 for name in new_names: +1284 if name not in new_covobs: +1285 if is_merged[name]: +1286 filtered_deltas, filtered_idl_d = _filter_zeroes(new_deltas[name], new_idl_d[name]) +1287 else: +1288 filtered_deltas = new_deltas[name] +1289 filtered_idl_d = new_idl_d[name] +1290 +1291 new_samples.append(filtered_deltas) +1292 new_idl.append(filtered_idl_d) +1293 new_means.append(new_r_values[name][i_val]) +1294 new_names_obs.append(name) +1295 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) +1296 for name in new_covobs: +1297 final_result[i_val].names.append(name) +1298 final_result[i_val]._covobs = new_covobs +1299 final_result[i_val]._value = new_val +1300 final_result[i_val].is_merged = is_merged +1301 final_result[i_val].reweighted = reweighted +1302 +1303 if multi == 0: +1304 final_result = final_result.item() +1305 +1306 return final_result
1336def reweight(weight, obs, **kwargs): -1337 """Reweight a list of observables. -1338 -1339 Parameters -1340 ---------- -1341 weight : Obs -1342 Reweighting factor. An Observable that has to be defined on a superset of the -1343 configurations in obs[i].idl for all i. -1344 obs : list -1345 list of Obs, e.g. [obs1, obs2, obs3]. -1346 all_configs : bool -1347 if True, the reweighted observables are normalized by the average of -1348 the reweighting factor on all configurations in weight.idl and not -1349 on the configurations in obs[i].idl. Default False. -1350 """ -1351 result = [] -1352 for i in range(len(obs)): -1353 if len(obs[i].cov_names): -1354 raise Exception('Error: Not possible to reweight an Obs that contains covobs!') -1355 if not set(obs[i].names).issubset(weight.names): -1356 raise Exception('Error: Ensembles do not fit') -1357 for name in obs[i].names: -1358 if not set(obs[i].idl[name]).issubset(weight.idl[name]): -1359 raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name)) -1360 new_samples = [] -1361 w_deltas = {} -1362 for name in sorted(obs[i].names): -1363 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) -1364 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) -1365 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) -1366 -1367 if kwargs.get('all_configs'): -1368 new_weight = weight -1369 else: -1370 new_weight = Obs([w_deltas[name] + weight.r_values[name] for name in sorted(obs[i].names)], sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) -1371 -1372 result.append(tmp_obs / new_weight) -1373 result[-1].reweighted = True -1374 result[-1].is_merged = obs[i].is_merged -1375 -1376 return result +@@ -4980,48 +4999,48 @@ on the configurations in obs[i].idl. Default False.1346def reweight(weight, obs, **kwargs): +1347 """Reweight a list of observables. +1348 +1349 Parameters +1350 ---------- +1351 weight : Obs +1352 Reweighting factor. An Observable that has to be defined on a superset of the +1353 configurations in obs[i].idl for all i. +1354 obs : list +1355 list of Obs, e.g. [obs1, obs2, obs3]. +1356 all_configs : bool +1357 if True, the reweighted observables are normalized by the average of +1358 the reweighting factor on all configurations in weight.idl and not +1359 on the configurations in obs[i].idl. Default False. +1360 """ +1361 result = [] +1362 for i in range(len(obs)): +1363 if len(obs[i].cov_names): +1364 raise Exception('Error: Not possible to reweight an Obs that contains covobs!') +1365 if not set(obs[i].names).issubset(weight.names): +1366 raise Exception('Error: Ensembles do not fit') +1367 for name in obs[i].names: +1368 if not set(obs[i].idl[name]).issubset(weight.idl[name]): +1369 raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name)) +1370 new_samples = [] +1371 w_deltas = {} +1372 for name in sorted(obs[i].names): +1373 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) +1374 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) +1375 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) +1376 +1377 if kwargs.get('all_configs'): +1378 new_weight = weight +1379 else: +1380 new_weight = Obs([w_deltas[name] + weight.r_values[name] for name in sorted(obs[i].names)], sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) +1381 +1382 result.append(tmp_obs / new_weight) +1383 result[-1].reweighted = True +1384 result[-1].is_merged = obs[i].is_merged +1385 +1386 return result
1379def correlate(obs_a, obs_b): -1380 """Correlate two observables. -1381 -1382 Parameters -1383 ---------- -1384 obs_a : Obs -1385 First observable -1386 obs_b : Obs -1387 Second observable -1388 -1389 Notes -1390 ----- -1391 Keep in mind to only correlate primary observables which have not been reweighted -1392 yet. The reweighting has to be applied after correlating the observables. -1393 Currently only works if ensembles are identical (this is not strictly necessary). -1394 """ -1395 -1396 if sorted(obs_a.names) != sorted(obs_b.names): -1397 raise Exception('Ensembles do not fit') -1398 if len(obs_a.cov_names) or len(obs_b.cov_names): -1399 raise Exception('Error: Not possible to correlate Obs that contain covobs!') -1400 for name in obs_a.names: -1401 if obs_a.shape[name] != obs_b.shape[name]: -1402 raise Exception('Shapes of ensemble', name, 'do not fit') -1403 if obs_a.idl[name] != obs_b.idl[name]: -1404 raise Exception('idl of ensemble', name, 'do not fit') +@@ -5056,71 +5075,71 @@ Currently only works if ensembles are identical (this is not strictly necessary)1389def correlate(obs_a, obs_b): +1390 """Correlate two observables. +1391 +1392 Parameters +1393 ---------- +1394 obs_a : Obs +1395 First observable +1396 obs_b : Obs +1397 Second observable +1398 +1399 Notes +1400 ----- +1401 Keep in mind to only correlate primary observables which have not been reweighted +1402 yet. The reweighting has to be applied after correlating the observables. +1403 Currently only works if ensembles are identical (this is not strictly necessary). +1404 """ 1405 -1406 if obs_a.reweighted is True: -1407 warnings.warn("The first observable is already reweighted.", RuntimeWarning) -1408 if obs_b.reweighted is True: -1409 warnings.warn("The second observable is already reweighted.", RuntimeWarning) -1410 -1411 new_samples = [] -1412 new_idl = [] -1413 for name in sorted(obs_a.names): -1414 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) -1415 new_idl.append(obs_a.idl[name]) -1416 -1417 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) -1418 o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names} -1419 o.reweighted = obs_a.reweighted or obs_b.reweighted -1420 return o +1406 if sorted(obs_a.names) != sorted(obs_b.names): +1407 raise Exception('Ensembles do not fit') +1408 if len(obs_a.cov_names) or len(obs_b.cov_names): +1409 raise Exception('Error: Not possible to correlate Obs that contain covobs!') +1410 for name in obs_a.names: +1411 if obs_a.shape[name] != obs_b.shape[name]: +1412 raise Exception('Shapes of ensemble', name, 'do not fit') +1413 if obs_a.idl[name] != obs_b.idl[name]: +1414 raise Exception('idl of ensemble', name, 'do not fit') +1415 +1416 if obs_a.reweighted is True: +1417 warnings.warn("The first observable is already reweighted.", RuntimeWarning) +1418 if obs_b.reweighted is True: +1419 warnings.warn("The second observable is already reweighted.", RuntimeWarning) +1420 +1421 new_samples = [] +1422 new_idl = [] +1423 for name in sorted(obs_a.names): +1424 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) +1425 new_idl.append(obs_a.idl[name]) +1426 +1427 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) +1428 o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names} +1429 o.reweighted = obs_a.reweighted or obs_b.reweighted +1430 return o
1423def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): -1424 r'''Calculates the error covariance matrix of a set of observables. -1425 -1426 The gamma method has to be applied first to all observables. -1427 -1428 Parameters -1429 ---------- -1430 obs : list or numpy.ndarray -1431 List or one dimensional array of Obs -1432 visualize : bool -1433 If True plots the corresponding normalized correlation matrix (default False). -1434 correlation : bool -1435 If True the correlation matrix instead of the error covariance matrix is returned (default False). -1436 smooth : None or int -1437 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue -1438 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the -1439 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely -1440 small ones. -1441 -1442 Notes -1443 ----- -1444 The error covariance is defined such that it agrees with the squared standard error for two identical observables -1445 $$\operatorname{cov}(a,a)=\sum_{s=1}^N\delta_a^s\delta_a^s/N^2=\Gamma_{aa}(0)/N=\operatorname{var}(a)/N=\sigma_a^2$$ -1446 in the absence of autocorrelation. -1447 The error covariance is estimated by calculating the correlation matrix assuming no autocorrelation and then rescaling the correlation matrix by the full errors including the previous gamma method estimate for the autocorrelation of the observables. The covariance at windowsize 0 is guaranteed to be positive semi-definite -1448 $$\sum_{i,j}v_i\Gamma_{ij}(0)v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i,j}v_i\delta_i^s\delta_j^s v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i}|v_i\delta_i^s|^2\geq 0\,,$$ for every $v\in\mathbb{R}^M$, while such an identity does not hold for larger windows/lags. -1449 For observables defined on a single ensemble our approximation is equivalent to assuming that the integrated autocorrelation time of an off-diagonal element is equal to the geometric mean of the integrated autocorrelation times of the corresponding diagonal elements. -1450 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ -1451 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). -1452 ''' -1453 -1454 length = len(obs) -1455 -1456 max_samples = np.max([o.N for o in obs]) -1457 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: -1458 warnings.warn(f"The dimension of the covariance matrix ({length}) is larger or equal to the number of samples ({max_samples}). This will result in a rank deficient matrix.", RuntimeWarning) -1459 -1460 cov = np.zeros((length, length)) -1461 for i in range(length): -1462 for j in range(i, length): -1463 cov[i, j] = _covariance_element(obs[i], obs[j]) -1464 cov = cov + cov.T - np.diag(np.diag(cov)) +@@ -5169,24 +5188,24 @@ This construction ensures that the estimated covariance matrix is positive semi-1433def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): +1434 r'''Calculates the error covariance matrix of a set of observables. +1435 +1436 The gamma method has to be applied first to all observables. +1437 +1438 Parameters +1439 ---------- +1440 obs : list or numpy.ndarray +1441 List or one dimensional array of Obs +1442 visualize : bool +1443 If True plots the corresponding normalized correlation matrix (default False). +1444 correlation : bool +1445 If True the correlation matrix instead of the error covariance matrix is returned (default False). +1446 smooth : None or int +1447 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue +1448 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the +1449 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely +1450 small ones. +1451 +1452 Notes +1453 ----- +1454 The error covariance is defined such that it agrees with the squared standard error for two identical observables +1455 $$\operatorname{cov}(a,a)=\sum_{s=1}^N\delta_a^s\delta_a^s/N^2=\Gamma_{aa}(0)/N=\operatorname{var}(a)/N=\sigma_a^2$$ +1456 in the absence of autocorrelation. +1457 The error covariance is estimated by calculating the correlation matrix assuming no autocorrelation and then rescaling the correlation matrix by the full errors including the previous gamma method estimate for the autocorrelation of the observables. The covariance at windowsize 0 is guaranteed to be positive semi-definite +1458 $$\sum_{i,j}v_i\Gamma_{ij}(0)v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i,j}v_i\delta_i^s\delta_j^s v_j=\frac{1}{N}\sum_{s=1}^N\sum_{i}|v_i\delta_i^s|^2\geq 0\,,$$ for every $v\in\mathbb{R}^M$, while such an identity does not hold for larger windows/lags. +1459 For observables defined on a single ensemble our approximation is equivalent to assuming that the integrated autocorrelation time of an off-diagonal element is equal to the geometric mean of the integrated autocorrelation times of the corresponding diagonal elements. +1460 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ +1461 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). +1462 ''' +1463 +1464 length = len(obs) 1465 -1466 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) -1467 -1468 if isinstance(smooth, int): -1469 corr = _smooth_eigenvalues(corr, smooth) -1470 -1471 if visualize: -1472 plt.matshow(corr, vmin=-1, vmax=1) -1473 plt.set_cmap('RdBu') -1474 plt.colorbar() -1475 plt.draw() -1476 -1477 if correlation is True: -1478 return corr -1479 -1480 errors = [o.dvalue for o in obs] -1481 cov = np.diag(errors) @ corr @ np.diag(errors) -1482 -1483 eigenvalues = np.linalg.eigh(cov)[0] -1484 if not np.all(eigenvalues >= 0): -1485 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning) +1466 max_samples = np.max([o.N for o in obs]) +1467 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: +1468 warnings.warn(f"The dimension of the covariance matrix ({length}) is larger or equal to the number of samples ({max_samples}). This will result in a rank deficient matrix.", RuntimeWarning) +1469 +1470 cov = np.zeros((length, length)) +1471 for i in range(length): +1472 for j in range(i, length): +1473 cov[i, j] = _covariance_element(obs[i], obs[j]) +1474 cov = cov + cov.T - np.diag(np.diag(cov)) +1475 +1476 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) +1477 +1478 if isinstance(smooth, int): +1479 corr = _smooth_eigenvalues(corr, smooth) +1480 +1481 if visualize: +1482 plt.matshow(corr, vmin=-1, vmax=1) +1483 plt.set_cmap('RdBu') +1484 plt.colorbar() +1485 plt.draw() 1486 -1487 return cov +1487 if correlation is True: +1488 return corr +1489 +1490 errors = [o.dvalue for o in obs] +1491 cov = np.diag(errors) @ corr @ np.diag(errors) +1492 +1493 eigenvalues = np.linalg.eigh(cov)[0] +1494 if not np.all(eigenvalues >= 0): +1495 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning) +1496 +1497 return cov
1567def import_jackknife(jacks, name, idl=None): -1568 """Imports jackknife samples and returns an Obs -1569 -1570 Parameters -1571 ---------- -1572 jacks : numpy.ndarray -1573 numpy array containing the mean value as zeroth entry and -1574 the N jackknife samples as first to Nth entry. -1575 name : str -1576 name of the ensemble the samples are defined on. -1577 """ -1578 length = len(jacks) - 1 -1579 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) -1580 samples = jacks[1:] @ prj -1581 mean = np.mean(samples) -1582 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) -1583 new_obs._value = jacks[0] -1584 return new_obs +@@ -5216,35 +5235,35 @@ name of the ensemble the samples are defined on.1577def import_jackknife(jacks, name, idl=None): +1578 """Imports jackknife samples and returns an Obs +1579 +1580 Parameters +1581 ---------- +1582 jacks : numpy.ndarray +1583 numpy array containing the mean value as zeroth entry and +1584 the N jackknife samples as first to Nth entry. +1585 name : str +1586 name of the ensemble the samples are defined on. +1587 """ +1588 length = len(jacks) - 1 +1589 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) +1590 samples = jacks[1:] @ prj +1591 mean = np.mean(samples) +1592 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) +1593 new_obs._value = jacks[0] +1594 return new_obs
1587def merge_obs(list_of_obs): -1588 """Combine all observables in list_of_obs into one new observable -1589 -1590 Parameters -1591 ---------- -1592 list_of_obs : list -1593 list of the Obs object to be combined -1594 -1595 Notes -1596 ----- -1597 It is not possible to combine obs which are based on the same replicum -1598 """ -1599 replist = [item for obs in list_of_obs for item in obs.names] -1600 if (len(replist) == len(set(replist))) is False: -1601 raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist))) -1602 if any([len(o.cov_names) for o in list_of_obs]): -1603 raise Exception('Not possible to merge data that contains covobs!') -1604 new_dict = {} -1605 idl_dict = {} -1606 for o in list_of_obs: -1607 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) -1608 for key in set(o.deltas) | set(o.r_values)}) -1609 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) -1610 -1611 names = sorted(new_dict.keys()) -1612 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) -1613 o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names} -1614 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) -1615 return o +@@ -5275,47 +5294,47 @@ list of the Obs object to be combined1597def merge_obs(list_of_obs): +1598 """Combine all observables in list_of_obs into one new observable +1599 +1600 Parameters +1601 ---------- +1602 list_of_obs : list +1603 list of the Obs object to be combined +1604 +1605 Notes +1606 ----- +1607 It is not possible to combine obs which are based on the same replicum +1608 """ +1609 replist = [item for obs in list_of_obs for item in obs.names] +1610 if (len(replist) == len(set(replist))) is False: +1611 raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist))) +1612 if any([len(o.cov_names) for o in list_of_obs]): +1613 raise Exception('Not possible to merge data that contains covobs!') +1614 new_dict = {} +1615 idl_dict = {} +1616 for o in list_of_obs: +1617 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) +1618 for key in set(o.deltas) | set(o.r_values)}) +1619 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) +1620 +1621 names = sorted(new_dict.keys()) +1622 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) +1623 o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names} +1624 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) +1625 return o
1618def cov_Obs(means, cov, name, grad=None): -1619 """Create an Obs based on mean(s) and a covariance matrix -1620 -1621 Parameters -1622 ---------- -1623 mean : list of floats or float -1624 N mean value(s) of the new Obs -1625 cov : list or array -1626 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance -1627 name : str -1628 identifier for the covariance matrix -1629 grad : list or array -1630 Gradient of the Covobs wrt. the means belonging to cov. -1631 """ -1632 -1633 def covobs_to_obs(co): -1634 """Make an Obs out of a Covobs -1635 -1636 Parameters -1637 ---------- -1638 co : Covobs -1639 Covobs to be embedded into the Obs -1640 """ -1641 o = Obs([], [], means=[]) -1642 o._value = co.value -1643 o.names.append(co.name) -1644 o._covobs[co.name] = co -1645 o._dvalue = np.sqrt(co.errsq()) -1646 return o -1647 -1648 ol = [] -1649 if isinstance(means, (float, int)): -1650 means = [means] -1651 -1652 for i in range(len(means)): -1653 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) -1654 if ol[0].covobs[name].N != len(means): -1655 raise Exception('You have to provide %d mean values!' % (ol[0].N)) -1656 if len(ol) == 1: -1657 return ol[0] -1658 return ol +1628def cov_Obs(means, cov, name, grad=None): +1629 """Create an Obs based on mean(s) and a covariance matrix +1630 +1631 Parameters +1632 ---------- +1633 mean : list of floats or float +1634 N mean value(s) of the new Obs +1635 cov : list or array +1636 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance +1637 name : str +1638 identifier for the covariance matrix +1639 grad : list or array +1640 Gradient of the Covobs wrt. the means belonging to cov. +1641 """ +1642 +1643 def covobs_to_obs(co): +1644 """Make an Obs out of a Covobs +1645 +1646 Parameters +1647 ---------- +1648 co : Covobs +1649 Covobs to be embedded into the Obs +1650 """ +1651 o = Obs([], [], means=[]) +1652 o._value = co.value +1653 o.names.append(co.name) +1654 o._covobs[co.name] = co +1655 o._dvalue = np.sqrt(co.errsq()) +1656 return o +1657 +1658 ol = [] +1659 if isinstance(means, (float, int)): +1660 means = [means] +1661 +1662 for i in range(len(means)): +1663 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) +1664 if ol[0].covobs[name].N != len(means): +1665 raise Exception('You have to provide %d mean values!' % (ol[0].N)) +1666 if len(ol) == 1: +1667 return ol[0] +1668 return ol