pyerrors.obs

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

Class for a general observable.

Instances of Obs are the basic objects of a pyerrors error analysis. They are initialized with a list which contains arrays of samples for different ensembles/replica and another list of same length which contains the names of the ensembles/replica. Mathematical operations can be performed on instances. The result is another instance of Obs. The error of an instance can be computed with the gamma_method. Also contains additional methods for output and visualization of the error calculation.

Attributes
  • S_global (float): Standard value for S (default 2.0)
  • S_dict (dict): Dictionary for S values. If an entry for a given ensemble exists this overwrites the standard value for that ensemble.
  • tau_exp_global (float): Standard value for tau_exp (default 0.0)
  • tau_exp_dict (dict): Dictionary for tau_exp values. If an entry for a given ensemble exists this overwrites the standard value for that ensemble.
  • N_sigma_global (float): Standard value for N_sigma (default 1.0)
  • N_sigma_dict (dict): Dictionary for N_sigma values. If an entry for a given ensemble exists this overwrites the standard value for that ensemble.
Obs(samples, names, idl=None, **kwargs)
 63    def __init__(self, samples, names, idl=None, **kwargs):
 64        """ Initialize Obs object.
 65
 66        Parameters
 67        ----------
 68        samples : list
 69            list of numpy arrays containing the Monte Carlo samples
 70        names : list
 71            list of strings labeling the individual samples
 72        idl : list, optional
 73            list of ranges or lists on which the samples are defined
 74        """
 75
 76        if kwargs.get("means") is None and len(samples):
 77            if len(samples) != len(names):
 78                raise Exception('Length of samples and names incompatible.')
 79            if idl is not None:
 80                if len(idl) != len(names):
 81                    raise Exception('Length of idl incompatible with samples and names.')
 82            name_length = len(names)
 83            if name_length > 1:
 84                if name_length != len(set(names)):
 85                    raise Exception('names are not unique.')
 86                if not all(isinstance(x, str) for x in names):
 87                    raise TypeError('All names have to be strings.')
 88            else:
 89                if not isinstance(names[0], str):
 90                    raise TypeError('All names have to be strings.')
 91            if min(len(x) for x in samples) <= 4:
 92                raise Exception('Samples have to have at least 5 entries.')
 93
 94        self.names = sorted(names)
 95        self.shape = {}
 96        self.r_values = {}
 97        self.deltas = {}
 98        self._covobs = {}
 99
100        self._value = 0
101        self.N = 0
102        self.is_merged = {}
103        self.idl = {}
104        if idl is not None:
105            for name, idx in sorted(zip(names, idl)):
106                if isinstance(idx, range):
107                    self.idl[name] = idx
108                elif isinstance(idx, (list, np.ndarray)):
109                    dc = np.unique(np.diff(idx))
110                    if np.any(dc < 0):
111                        raise Exception("Unsorted idx for idl[%s]" % (name))
112                    if len(dc) == 1:
113                        self.idl[name] = range(idx[0], idx[-1] + dc[0], dc[0])
114                    else:
115                        self.idl[name] = list(idx)
116                else:
117                    raise Exception('incompatible type for idl[%s].' % (name))
118        else:
119            for name, sample in sorted(zip(names, samples)):
120                self.idl[name] = range(1, len(sample) + 1)
121
122        if kwargs.get("means") is not None:
123            for name, sample, mean in sorted(zip(names, samples, kwargs.get("means"))):
124                self.shape[name] = len(self.idl[name])
125                self.N += self.shape[name]
126                self.r_values[name] = mean
127                self.deltas[name] = sample
128        else:
129            for name, sample in sorted(zip(names, samples)):
130                self.shape[name] = len(self.idl[name])
131                self.N += self.shape[name]
132                if len(sample) != self.shape[name]:
133                    raise Exception('Incompatible samples and idx for %s: %d vs. %d' % (name, len(sample), self.shape[name]))
134                self.r_values[name] = np.mean(sample)
135                self.deltas[name] = sample - self.r_values[name]
136                self._value += self.shape[name] * self.r_values[name]
137            self._value /= self.N
138
139        self._dvalue = 0.0
140        self.ddvalue = 0.0
141        self.reweighted = False
142
143        self.tag = None

Initialize Obs object.

Parameters
  • samples (list): list of numpy arrays containing the Monte Carlo samples
  • names (list): list of strings labeling the individual samples
  • idl (list, optional): list of ranges or lists on which the samples are defined
def gamma_method(self, **kwargs):
178    def gamma_method(self, **kwargs):
179        """Estimate the error and related properties of the Obs.
180
181        Parameters
182        ----------
183        S : float
184            specifies a custom value for the parameter S (default 2.0).
185            If set to 0 it is assumed that the data exhibits no
186            autocorrelation. In this case the error estimates coincides
187            with the sample standard error.
188        tau_exp : float
189            positive value triggers the critical slowing down analysis
190            (default 0.0).
191        N_sigma : float
192            number of standard deviations from zero until the tail is
193            attached to the autocorrelation function (default 1).
194        fft : bool
195            determines whether the fft algorithm is used for the computation
196            of the autocorrelation function (default True)
197        """
198
199        e_content = self.e_content
200        self.e_dvalue = {}
201        self.e_ddvalue = {}
202        self.e_tauint = {}
203        self.e_dtauint = {}
204        self.e_windowsize = {}
205        self.e_n_tauint = {}
206        self.e_n_dtauint = {}
207        e_gamma = {}
208        self.e_rho = {}
209        self.e_drho = {}
210        self._dvalue = 0
211        self.ddvalue = 0
212
213        self.S = {}
214        self.tau_exp = {}
215        self.N_sigma = {}
216
217        if kwargs.get('fft') is False:
218            fft = False
219        else:
220            fft = True
221
222        def _parse_kwarg(kwarg_name):
223            if kwarg_name in kwargs:
224                tmp = kwargs.get(kwarg_name)
225                if isinstance(tmp, (int, float)):
226                    if tmp < 0:
227                        raise Exception(kwarg_name + ' has to be larger or equal to 0.')
228                    for e, e_name in enumerate(self.e_names):
229                        getattr(self, kwarg_name)[e_name] = tmp
230                else:
231                    raise TypeError(kwarg_name + ' is not in proper format.')
232            else:
233                for e, e_name in enumerate(self.e_names):
234                    if e_name in getattr(Obs, kwarg_name + '_dict'):
235                        getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_dict')[e_name]
236                    else:
237                        getattr(self, kwarg_name)[e_name] = getattr(Obs, kwarg_name + '_global')
238
239        _parse_kwarg('S')
240        _parse_kwarg('tau_exp')
241        _parse_kwarg('N_sigma')
242
243        for e, e_name in enumerate(self.mc_names):
244            r_length = []
245            for r_name in e_content[e_name]:
246                if isinstance(self.idl[r_name], range):
247                    r_length.append(len(self.idl[r_name]))
248                else:
249                    r_length.append((self.idl[r_name][-1] - self.idl[r_name][0] + 1))
250
251            e_N = np.sum([self.shape[r_name] for r_name in e_content[e_name]])
252            w_max = max(r_length) // 2
253            e_gamma[e_name] = np.zeros(w_max)
254            self.e_rho[e_name] = np.zeros(w_max)
255            self.e_drho[e_name] = np.zeros(w_max)
256
257            for r_name in e_content[e_name]:
258                e_gamma[e_name] += self._calc_gamma(self.deltas[r_name], self.idl[r_name], self.shape[r_name], w_max, fft)
259
260            gamma_div = np.zeros(w_max)
261            for r_name in e_content[e_name]:
262                gamma_div += self._calc_gamma(np.ones((self.shape[r_name])), self.idl[r_name], self.shape[r_name], w_max, fft)
263            gamma_div[gamma_div < 1] = 1.0
264            e_gamma[e_name] /= gamma_div[:w_max]
265
266            if np.abs(e_gamma[e_name][0]) < 10 * np.finfo(float).tiny:  # Prevent division by zero
267                self.e_tauint[e_name] = 0.5
268                self.e_dtauint[e_name] = 0.0
269                self.e_dvalue[e_name] = 0.0
270                self.e_ddvalue[e_name] = 0.0
271                self.e_windowsize[e_name] = 0
272                continue
273
274            gaps = []
275            for r_name in e_content[e_name]:
276                if isinstance(self.idl[r_name], range):
277                    gaps.append(1)
278                else:
279                    gaps.append(np.min(np.diff(self.idl[r_name])))
280
281            if not np.all([gi == gaps[0] for gi in gaps]):
282                raise Exception(f"Replica for ensemble {e_name} are not equally spaced.", gaps)
283            else:
284                gapsize = gaps[0]
285
286            self.e_rho[e_name] = e_gamma[e_name][:w_max] / e_gamma[e_name][0]
287            self.e_n_tauint[e_name] = np.cumsum(np.concatenate(([0.5], self.e_rho[e_name][1:])))
288            # Make sure no entry of tauint is smaller than 0.5
289            self.e_n_tauint[e_name][self.e_n_tauint[e_name] <= 0.5] = 0.5 + np.finfo(np.float64).eps
290            # hep-lat/0306017 eq. (42)
291            self.e_n_dtauint[e_name] = self.e_n_tauint[e_name] * 2 * np.sqrt(np.abs(np.arange(w_max) / gapsize + 0.5 - self.e_n_tauint[e_name]) / e_N)
292            self.e_n_dtauint[e_name][0] = 0.0
293
294            def _compute_drho(i):
295                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]
296                self.e_drho[e_name][i] = np.sqrt(np.sum(tmp ** 2) / e_N)
297
298            _compute_drho(gapsize)
299            if self.tau_exp[e_name] > 0:
300                texp = self.tau_exp[e_name]
301                # Critical slowing down analysis
302                if w_max // 2 <= 1:
303                    raise Exception("Need at least 8 samples for tau_exp error analysis")
304                for n in range(gapsize, w_max // 2, gapsize):
305                    _compute_drho(n + gapsize)
306                    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:
307                        # Bias correction hep-lat/0306017 eq. (49) included
308                        self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n / gapsize + 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
309                        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)
310                        # Error of tau_exp neglected so far, missing term: self.e_rho[e_name][n + 1] ** 2 * d_tau_exp ** 2
311                        self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N)
312                        self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n / gapsize + 0.5) / e_N)
313                        self.e_windowsize[e_name] = n
314                        break
315            else:
316                if self.S[e_name] == 0.0:
317                    self.e_tauint[e_name] = 0.5
318                    self.e_dtauint[e_name] = 0.0
319                    self.e_dvalue[e_name] = np.sqrt(e_gamma[e_name][0] / (e_N - 1))
320                    self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt(0.5 / e_N)
321                    self.e_windowsize[e_name] = 0
322                else:
323                    # Standard automatic windowing procedure
324                    tau = self.S[e_name] / np.log((2 * self.e_n_tauint[e_name][gapsize::gapsize] + 1) / (2 * self.e_n_tauint[e_name][gapsize::gapsize] - 1))
325                    g_w = np.exp(- np.arange(1, len(tau) + 1) / tau) - tau / np.sqrt(np.arange(1, len(tau) + 1) * e_N)
326                    for n in range(1, w_max):
327                        if n < w_max // 2 - 2:
328                            _compute_drho(gapsize * n + gapsize)
329                        if g_w[n - 1] < 0 or n >= w_max - 1:
330                            n *= gapsize
331                            self.e_tauint[e_name] = self.e_n_tauint[e_name][n] * (1 + (2 * n / gapsize + 1) / e_N) / (1 + 1 / e_N)  # Bias correction hep-lat/0306017 eq. (49)
332                            self.e_dtauint[e_name] = self.e_n_dtauint[e_name][n]
333                            self.e_dvalue[e_name] = np.sqrt(2 * self.e_tauint[e_name] * e_gamma[e_name][0] * (1 + 1 / e_N) / e_N)
334                            self.e_ddvalue[e_name] = self.e_dvalue[e_name] * np.sqrt((n / gapsize + 0.5) / e_N)
335                            self.e_windowsize[e_name] = n
336                            break
337
338            self._dvalue += self.e_dvalue[e_name] ** 2
339            self.ddvalue += (self.e_dvalue[e_name] * self.e_ddvalue[e_name]) ** 2
340
341        for e_name in self.cov_names:
342            self.e_dvalue[e_name] = np.sqrt(self.covobs[e_name].errsq())
343            self.e_ddvalue[e_name] = 0
344            self._dvalue += self.e_dvalue[e_name]**2
345
346        self._dvalue = np.sqrt(self._dvalue)
347        if self._dvalue == 0.0:
348            self.ddvalue = 0.0
349        else:
350            self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue
351        return

Estimate the error and related properties of the Obs.

Parameters
  • S (float): specifies a custom value for the parameter S (default 2.0). If set to 0 it is assumed that the data exhibits no autocorrelation. In this case the error estimates coincides with the sample standard error.
  • tau_exp (float): positive value triggers the critical slowing down analysis (default 0.0).
  • N_sigma (float): number of standard deviations from zero until the tail is attached to the autocorrelation function (default 1).
  • fft (bool): determines whether the fft algorithm is used for the computation of the autocorrelation function (default True)
def details(self, ens_content=True):
386    def details(self, ens_content=True):
387        """Output detailed properties of the Obs.
388
389        Parameters
390        ----------
391        ens_content : bool
392            print details about the ensembles and replica if true.
393        """
394        if self.tag is not None:
395            print("Description:", self.tag)
396        if not hasattr(self, 'e_dvalue'):
397            print('Result\t %3.8e' % (self.value))
398        else:
399            if self.value == 0.0:
400                percentage = np.nan
401            else:
402                percentage = np.abs(self._dvalue / self.value) * 100
403            print('Result\t %3.8e +/- %3.8e +/- %3.8e (%3.3f%%)' % (self.value, self._dvalue, self.ddvalue, percentage))
404            if len(self.e_names) > 1:
405                print(' Ensemble errors:')
406            e_content = self.e_content
407            for e_name in self.mc_names:
408                if isinstance(self.idl[e_content[e_name][0]], range):
409                    gap = self.idl[e_content[e_name][0]].step
410                else:
411                    gap = np.min(np.diff(self.idl[e_content[e_name][0]]))
412
413                if len(self.e_names) > 1:
414                    print('', e_name, '\t %3.6e +/- %3.6e' % (self.e_dvalue[e_name], self.e_ddvalue[e_name]))
415                tau_string = " \N{GREEK SMALL LETTER TAU}_int\t " + _format_uncertainty(self.e_tauint[e_name], self.e_dtauint[e_name])
416                tau_string += f" in units of {gap} config"
417                if gap > 1:
418                    tau_string += "s"
419                if self.tau_exp[e_name] > 0:
420                    tau_string = f"{tau_string: <45}" + '\t(\N{GREEK SMALL LETTER TAU}_exp=%3.2f, N_\N{GREEK SMALL LETTER SIGMA}=%1.0i)' % (self.tau_exp[e_name], self.N_sigma[e_name])
421                else:
422                    tau_string = f"{tau_string: <45}" + '\t(S=%3.2f)' % (self.S[e_name])
423                print(tau_string)
424            for e_name in self.cov_names:
425                print('', e_name, '\t %3.8e' % (self.e_dvalue[e_name]))
426        if ens_content is True:
427            if len(self.e_names) == 1:
428                print(self.N, 'samples in', len(self.e_names), 'ensemble:')
429            else:
430                print(self.N, 'samples in', len(self.e_names), 'ensembles:')
431            my_string_list = []
432            for key, value in sorted(self.e_content.items()):
433                if key not in self.covobs:
434                    my_string = '  ' + "\u00B7 Ensemble '" + key + "' "
435                    if len(value) == 1:
436                        my_string += f': {self.shape[value[0]]} configurations'
437                        if isinstance(self.idl[value[0]], range):
438                            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}' + ')'
439                        else:
440                            my_string += f' (irregular range from {self.idl[value[0]][0]} to {self.idl[value[0]][-1]})'
441                    else:
442                        sublist = []
443                        for v in value:
444                            my_substring = '    ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' "
445                            my_substring += f': {self.shape[v]} configurations'
446                            if isinstance(self.idl[v], range):
447                                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}' + ')'
448                            else:
449                                my_substring += f' (irregular range from {self.idl[v][0]} to {self.idl[v][-1]})'
450                            sublist.append(my_substring)
451
452                        my_string += '\n' + '\n'.join(sublist)
453                else:
454                    my_string = '  ' + "\u00B7 Covobs   '" + key + "' "
455                my_string_list.append(my_string)
456            print('\n'.join(my_string_list))

Output detailed properties of the Obs.

Parameters
  • ens_content (bool): print details about the ensembles and replica if true.
def reweight(self, weight):
458    def reweight(self, weight):
459        """Reweight the obs with given rewighting factors.
460
461        Parameters
462        ----------
463        weight : Obs
464            Reweighting factor. An Observable that has to be defined on a superset of the
465            configurations in obs[i].idl for all i.
466        all_configs : bool
467            if True, the reweighted observables are normalized by the average of
468            the reweighting factor on all configurations in weight.idl and not
469            on the configurations in obs[i].idl. Default False.
470        """
471        return reweight(weight, [self])[0]

Reweight the obs with given rewighting factors.

Parameters
  • weight (Obs): Reweighting factor. An Observable that has to be defined on a superset of the configurations in obs[i].idl for all i.
  • all_configs (bool): if True, the reweighted observables are normalized by the average of the reweighting factor on all configurations in weight.idl and not on the configurations in obs[i].idl. Default False.
def is_zero_within_error(self, sigma=1):
473    def is_zero_within_error(self, sigma=1):
474        """Checks whether the observable is zero within 'sigma' standard errors.
475
476        Parameters
477        ----------
478        sigma : int
479            Number of standard errors used for the check.
480
481        Works only properly when the gamma method was run.
482        """
483        return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue

Checks whether the observable is zero within 'sigma' standard errors.

Parameters
  • sigma (int): Number of standard errors used for the check.
  • Works only properly when the gamma method was run.
def is_zero(self, atol=1e-10):
485    def is_zero(self, atol=1e-10):
486        """Checks whether the observable is zero within a given tolerance.
487
488        Parameters
489        ----------
490        atol : float
491            Absolute tolerance (for details see numpy documentation).
492        """
493        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())

Checks whether the observable is zero within a given tolerance.

Parameters
  • atol (float): Absolute tolerance (for details see numpy documentation).
def plot_tauint(self, save=None):
495    def plot_tauint(self, save=None):
496        """Plot integrated autocorrelation time for each ensemble.
497
498        Parameters
499        ----------
500        save : str
501            saves the figure to a file named 'save' if.
502        """
503        if not hasattr(self, 'e_dvalue'):
504            raise Exception('Run the gamma method first.')
505
506        for e, e_name in enumerate(self.mc_names):
507            fig = plt.figure()
508            plt.xlabel(r'$W$')
509            plt.ylabel(r'$\tau_\mathrm{int}$')
510            length = int(len(self.e_n_tauint[e_name]))
511            if self.tau_exp[e_name] > 0:
512                base = self.e_n_tauint[e_name][self.e_windowsize[e_name]]
513                x_help = np.arange(2 * self.tau_exp[e_name])
514                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
515                x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name])
516                plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',')
517                plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]],
518                             yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor'])
519                xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5
520                label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2))
521            else:
522                label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))
523                xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5)
524
525            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)
526            plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--')
527            plt.legend()
528            plt.xlim(-0.5, xmax)
529            ylim = plt.ylim()
530            plt.ylim(bottom=0.0, top=max(1.0, ylim[1]))
531            plt.draw()
532            if save:
533                fig.savefig(save + "_" + str(e))

Plot integrated autocorrelation time for each ensemble.

Parameters
  • save (str): saves the figure to a file named 'save' if.
def plot_rho(self, save=None):
535    def plot_rho(self, save=None):
536        """Plot normalized autocorrelation function time for each ensemble.
537
538        Parameters
539        ----------
540        save : str
541            saves the figure to a file named 'save' if.
542        """
543        if not hasattr(self, 'e_dvalue'):
544            raise Exception('Run the gamma method first.')
545        for e, e_name in enumerate(self.mc_names):
546            fig = plt.figure()
547            plt.xlabel('W')
548            plt.ylabel('rho')
549            length = int(len(self.e_drho[e_name]))
550            plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2)
551            plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',')
552            if self.tau_exp[e_name] > 0:
553                plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]],
554                         [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1)
555                xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5
556                plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2)))
557            else:
558                xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5)
559                plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)))
560            plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1)
561            plt.xlim(-0.5, xmax)
562            plt.draw()
563            if save:
564                fig.savefig(save + "_" + str(e))

Plot normalized autocorrelation function time for each ensemble.

Parameters
  • save (str): saves the figure to a file named 'save' if.
def plot_rep_dist(self):
566    def plot_rep_dist(self):
567        """Plot replica distribution for each ensemble with more than one replicum."""
568        if not hasattr(self, 'e_dvalue'):
569            raise Exception('Run the gamma method first.')
570        for e, e_name in enumerate(self.mc_names):
571            if len(self.e_content[e_name]) == 1:
572                print('No replica distribution for a single replicum (', e_name, ')')
573                continue
574            r_length = []
575            sub_r_mean = 0
576            for r, r_name in enumerate(self.e_content[e_name]):
577                r_length.append(len(self.deltas[r_name]))
578                sub_r_mean += self.shape[r_name] * self.r_values[r_name]
579            e_N = np.sum(r_length)
580            sub_r_mean /= e_N
581            arr = np.zeros(len(self.e_content[e_name]))
582            for r, r_name in enumerate(self.e_content[e_name]):
583                arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1))
584            plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name]))
585            plt.title('Replica distribution' + e_name + ' (mean=0, var=1)')
586            plt.draw()

Plot replica distribution for each ensemble with more than one replicum.

def plot_history(self, expand=True):
588    def plot_history(self, expand=True):
589        """Plot derived Monte Carlo history for each ensemble
590
591        Parameters
592        ----------
593        expand : bool
594            show expanded history for irregular Monte Carlo chains (default: True).
595        """
596        for e, e_name in enumerate(self.mc_names):
597            plt.figure()
598            r_length = []
599            tmp = []
600            tmp_expanded = []
601            for r, r_name in enumerate(self.e_content[e_name]):
602                tmp.append(self.deltas[r_name] + self.r_values[r_name])
603                if expand:
604                    tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name]) + self.r_values[r_name])
605                    r_length.append(len(tmp_expanded[-1]))
606                else:
607                    r_length.append(len(tmp[-1]))
608            e_N = np.sum(r_length)
609            x = np.arange(e_N)
610            y_test = np.concatenate(tmp, axis=0)
611            if expand:
612                y = np.concatenate(tmp_expanded, axis=0)
613            else:
614                y = y_test
615            plt.errorbar(x, y, fmt='.', markersize=3)
616            plt.xlim(-0.5, e_N - 0.5)
617            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})')
618            plt.draw()

Plot derived Monte Carlo history for each ensemble

Parameters
  • expand (bool): show expanded history for irregular Monte Carlo chains (default: True).
def plot_piechart(self, save=None):
620    def plot_piechart(self, save=None):
621        """Plot piechart which shows the fractional contribution of each
622        ensemble to the error and returns a dictionary containing the fractions.
623
624        Parameters
625        ----------
626        save : str
627            saves the figure to a file named 'save' if.
628        """
629        if not hasattr(self, 'e_dvalue'):
630            raise Exception('Run the gamma method first.')
631        if np.isclose(0.0, self._dvalue, atol=1e-15):
632            raise Exception('Error is 0.0')
633        labels = self.e_names
634        sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2
635        fig1, ax1 = plt.subplots()
636        ax1.pie(sizes, labels=labels, startangle=90, normalize=True)
637        ax1.axis('equal')
638        plt.draw()
639        if save:
640            fig1.savefig(save)
641
642        return dict(zip(self.e_names, sizes))

Plot piechart which shows the fractional contribution of each ensemble to the error and returns a dictionary containing the fractions.

Parameters
  • save (str): saves the figure to a file named 'save' if.
def dump(self, filename, datatype='json.gz', description='', **kwargs):
644    def dump(self, filename, datatype="json.gz", description="", **kwargs):
645        """Dump the Obs to a file 'name' of chosen format.
646
647        Parameters
648        ----------
649        filename : str
650            name of the file to be saved.
651        datatype : str
652            Format of the exported file. Supported formats include
653            "json.gz" and "pickle"
654        description : str
655            Description for output file, only relevant for json.gz format.
656        path : str
657            specifies a custom path for the file (default '.')
658        """
659        if 'path' in kwargs:
660            file_name = kwargs.get('path') + '/' + filename
661        else:
662            file_name = filename
663
664        if datatype == "json.gz":
665            from .input.json import dump_to_json
666            dump_to_json([self], file_name, description=description)
667        elif datatype == "pickle":
668            with open(file_name + '.p', 'wb') as fb:
669                pickle.dump(self, fb)
670        else:
671            raise Exception("Unknown datatype " + str(datatype))

Dump the Obs to a file 'name' of chosen format.

Parameters
  • filename (str): name of the file to be saved.
  • datatype (str): Format of the exported file. Supported formats include "json.gz" and "pickle"
  • description (str): Description for output file, only relevant for json.gz format.
  • path (str): specifies a custom path for the file (default '.')
def export_jackknife(self):
673    def export_jackknife(self):
674        """Export jackknife samples from the Obs
675
676        Returns
677        -------
678        numpy.ndarray
679            Returns a numpy array of length N + 1 where N is the number of samples
680            for the given ensemble and replicum. The zeroth entry of the array contains
681            the mean value of the Obs, entries 1 to N contain the N jackknife samples
682            derived from the Obs. The current implementation only works for observables
683            defined on exactly one ensemble and replicum. The derived jackknife samples
684            should agree with samples from a full jackknife analysis up to O(1/N).
685        """
686
687        if len(self.names) != 1:
688            raise Exception("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.")
689
690        name = self.names[0]
691        full_data = self.deltas[name] + self.r_values[name]
692        n = full_data.size
693        mean = self.value
694        tmp_jacks = np.zeros(n + 1)
695        tmp_jacks[0] = mean
696        tmp_jacks[1:] = (n * mean - full_data) / (n - 1)
697        return tmp_jacks

Export jackknife samples from the Obs

Returns
  • numpy.ndarray: Returns a numpy array of length N + 1 where N is the number of samples for the given ensemble and replicum. The zeroth entry of the array contains the mean value of the Obs, entries 1 to N contain the N jackknife samples derived from the Obs. The current implementation only works for observables defined on exactly one ensemble and replicum. The derived jackknife samples should agree with samples from a full jackknife analysis up to O(1/N).
def sqrt(self):
825    def sqrt(self):
826        return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)])
def log(self):
828    def log(self):
829        return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value])
def exp(self):
831    def exp(self):
832        return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)])
def sin(self):
834    def sin(self):
835        return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)])
def cos(self):
837    def cos(self):
838        return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)])
def tan(self):
840    def tan(self):
841        return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2])
def arcsin(self):
843    def arcsin(self):
844        return derived_observable(lambda x: anp.arcsin(x[0]), [self])
def arccos(self):
846    def arccos(self):
847        return derived_observable(lambda x: anp.arccos(x[0]), [self])
def arctan(self):
849    def arctan(self):
850        return derived_observable(lambda x: anp.arctan(x[0]), [self])
def sinh(self):
852    def sinh(self):
853        return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)])
def cosh(self):
855    def cosh(self):
856        return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)])
def tanh(self):
858    def tanh(self):
859        return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2])
def arcsinh(self):
861    def arcsinh(self):
862        return derived_observable(lambda x: anp.arcsinh(x[0]), [self])
def arccosh(self):
864    def arccosh(self):
865        return derived_observable(lambda x: anp.arccosh(x[0]), [self])
def arctanh(self):
867    def arctanh(self):
868        return derived_observable(lambda x: anp.arctanh(x[0]), [self])
class CObs:
871class CObs:
872    """Class for a complex valued observable."""
873    __slots__ = ['_real', '_imag', 'tag']
874
875    def __init__(self, real, imag=0.0):
876        self._real = real
877        self._imag = imag
878        self.tag = None
879
880    @property
881    def real(self):
882        return self._real
883
884    @property
885    def imag(self):
886        return self._imag
887
888    def gamma_method(self, **kwargs):
889        """Executes the gamma_method for the real and the imaginary part."""
890        if isinstance(self.real, Obs):
891            self.real.gamma_method(**kwargs)
892        if isinstance(self.imag, Obs):
893            self.imag.gamma_method(**kwargs)
894
895    def is_zero(self):
896        """Checks whether both real and imaginary part are zero within machine precision."""
897        return self.real == 0.0 and self.imag == 0.0
898
899    def conjugate(self):
900        return CObs(self.real, -self.imag)
901
902    def __add__(self, other):
903        if isinstance(other, np.ndarray):
904            return other + self
905        elif hasattr(other, 'real') and hasattr(other, 'imag'):
906            return CObs(self.real + other.real,
907                        self.imag + other.imag)
908        else:
909            return CObs(self.real + other, self.imag)
910
911    def __radd__(self, y):
912        return self + y
913
914    def __sub__(self, other):
915        if isinstance(other, np.ndarray):
916            return -1 * (other - self)
917        elif hasattr(other, 'real') and hasattr(other, 'imag'):
918            return CObs(self.real - other.real, self.imag - other.imag)
919        else:
920            return CObs(self.real - other, self.imag)
921
922    def __rsub__(self, other):
923        return -1 * (self - other)
924
925    def __mul__(self, other):
926        if isinstance(other, np.ndarray):
927            return other * self
928        elif hasattr(other, 'real') and hasattr(other, 'imag'):
929            if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]):
930                return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3],
931                                               [self.real, other.real, self.imag, other.imag],
932                                               man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]),
933                            derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3],
934                                               [self.real, other.real, self.imag, other.imag],
935                                               man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value]))
936            elif getattr(other, 'imag', 0) != 0:
937                return CObs(self.real * other.real - self.imag * other.imag,
938                            self.imag * other.real + self.real * other.imag)
939            else:
940                return CObs(self.real * other.real, self.imag * other.real)
941        else:
942            return CObs(self.real * other, self.imag * other)
943
944    def __rmul__(self, other):
945        return self * other
946
947    def __truediv__(self, other):
948        if isinstance(other, np.ndarray):
949            return 1 / (other / self)
950        elif hasattr(other, 'real') and hasattr(other, 'imag'):
951            r = other.real ** 2 + other.imag ** 2
952            return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r)
953        else:
954            return CObs(self.real / other, self.imag / other)
955
956    def __rtruediv__(self, other):
957        r = self.real ** 2 + self.imag ** 2
958        if hasattr(other, 'real') and hasattr(other, 'imag'):
959            return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r)
960        else:
961            return CObs(self.real * other / r, -self.imag * other / r)
962
963    def __abs__(self):
964        return np.sqrt(self.real**2 + self.imag**2)
965
966    def __pos__(self):
967        return self
968
969    def __neg__(self):
970        return -1 * self
971
972    def __eq__(self, other):
973        return self.real == other.real and self.imag == other.imag
974
975    def __str__(self):
976        return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)'
977
978    def __repr__(self):
979        return 'CObs[' + str(self) + ']'

Class for a complex valued observable.

CObs(real, imag=0.0)
875    def __init__(self, real, imag=0.0):
876        self._real = real
877        self._imag = imag
878        self.tag = None
def gamma_method(self, **kwargs):
888    def gamma_method(self, **kwargs):
889        """Executes the gamma_method for the real and the imaginary part."""
890        if isinstance(self.real, Obs):
891            self.real.gamma_method(**kwargs)
892        if isinstance(self.imag, Obs):
893            self.imag.gamma_method(**kwargs)

Executes the gamma_method for the real and the imaginary part.

def is_zero(self):
895    def is_zero(self):
896        """Checks whether both real and imaginary part are zero within machine precision."""
897        return self.real == 0.0 and self.imag == 0.0

Checks whether both real and imaginary part are zero within machine precision.

def conjugate(self):
899    def conjugate(self):
900        return CObs(self.real, -self.imag)
def derived_observable(func, data, array_mode=False, **kwargs):
1133def derived_observable(func, data, array_mode=False, **kwargs):
1134    """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation.
1135
1136    Parameters
1137    ----------
1138    func : object
1139        arbitrary function of the form func(data, **kwargs). For the
1140        automatic differentiation to work, all numpy functions have to have
1141        the autograd wrapper (use 'import autograd.numpy as anp').
1142    data : list
1143        list of Obs, e.g. [obs1, obs2, obs3].
1144    num_grad : bool
1145        if True, numerical derivatives are used instead of autograd
1146        (default False). To control the numerical differentiation the
1147        kwargs of numdifftools.step_generators.MaxStepGenerator
1148        can be used.
1149    man_grad : list
1150        manually supply a list or an array which contains the jacobian
1151        of func. Use cautiously, supplying the wrong derivative will
1152        not be intercepted.
1153
1154    Notes
1155    -----
1156    For simple mathematical operations it can be practical to use anonymous
1157    functions. For the ratio of two observables one can e.g. use
1158
1159    new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2])
1160    """
1161
1162    data = np.asarray(data)
1163    raveled_data = data.ravel()
1164
1165    # Workaround for matrix operations containing non Obs data
1166    if not all(isinstance(x, Obs) for x in raveled_data):
1167        for i in range(len(raveled_data)):
1168            if isinstance(raveled_data[i], (int, float)):
1169                raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###")
1170
1171    allcov = {}
1172    for o in raveled_data:
1173        for name in o.cov_names:
1174            if name in allcov:
1175                if not np.allclose(allcov[name], o.covobs[name].cov):
1176                    raise Exception('Inconsistent covariance matrices for %s!' % (name))
1177            else:
1178                allcov[name] = o.covobs[name].cov
1179
1180    n_obs = len(raveled_data)
1181    new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x]))
1182    new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x]))
1183    new_sample_names = sorted(set(new_names) - set(new_cov_names))
1184
1185    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}
1186    reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0
1187
1188    if data.ndim == 1:
1189        values = np.array([o.value for o in data])
1190    else:
1191        values = np.vectorize(lambda x: x.value)(data)
1192
1193    new_values = func(values, **kwargs)
1194
1195    multi = int(isinstance(new_values, np.ndarray))
1196
1197    new_r_values = {}
1198    new_idl_d = {}
1199    for name in new_sample_names:
1200        idl = []
1201        tmp_values = np.zeros(n_obs)
1202        for i, item in enumerate(raveled_data):
1203            tmp_values[i] = item.r_values.get(name, item.value)
1204            tmp_idl = item.idl.get(name)
1205            if tmp_idl is not None:
1206                idl.append(tmp_idl)
1207        if multi > 0:
1208            tmp_values = np.array(tmp_values).reshape(data.shape)
1209        new_r_values[name] = func(tmp_values, **kwargs)
1210        new_idl_d[name] = _merge_idx(idl)
1211        if not is_merged[name]:
1212            is_merged[name] = (1 != len(set([len(idx) for idx in [*idl, new_idl_d[name]]])))
1213
1214    if 'man_grad' in kwargs:
1215        deriv = np.asarray(kwargs.get('man_grad'))
1216        if new_values.shape + data.shape != deriv.shape:
1217            raise Exception('Manual derivative does not have correct shape.')
1218    elif kwargs.get('num_grad') is True:
1219        if multi > 0:
1220            raise Exception('Multi mode currently not supported for numerical derivative')
1221        options = {
1222            'base_step': 0.1,
1223            'step_ratio': 2.5}
1224        for key in options.keys():
1225            kwarg = kwargs.get(key)
1226            if kwarg is not None:
1227                options[key] = kwarg
1228        tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs)
1229        if tmp_df.size == 1:
1230            deriv = np.array([tmp_df.real])
1231        else:
1232            deriv = tmp_df.real
1233    else:
1234        deriv = jacobian(func)(values, **kwargs)
1235
1236    final_result = np.zeros(new_values.shape, dtype=object)
1237
1238    if array_mode is True:
1239
1240        class _Zero_grad():
1241            def __init__(self, N):
1242                self.grad = np.zeros((N, 1))
1243
1244        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]))
1245        d_extracted = {}
1246        g_extracted = {}
1247        for name in new_sample_names:
1248            d_extracted[name] = []
1249            ens_length = len(new_idl_d[name])
1250            for i_dat, dat in enumerate(data):
1251                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, )))
1252        for name in new_cov_names:
1253            g_extracted[name] = []
1254            zero_grad = _Zero_grad(new_covobs_lengths[name])
1255            for i_dat, dat in enumerate(data):
1256                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)))
1257
1258    for i_val, new_val in np.ndenumerate(new_values):
1259        new_deltas = {}
1260        new_grad = {}
1261        if array_mode is True:
1262            for name in new_sample_names:
1263                ens_length = d_extracted[name][0].shape[-1]
1264                new_deltas[name] = np.zeros(ens_length)
1265                for i_dat, dat in enumerate(d_extracted[name]):
1266                    new_deltas[name] += np.tensordot(deriv[i_val + (i_dat, )], dat)
1267            for name in new_cov_names:
1268                new_grad[name] = 0
1269                for i_dat, dat in enumerate(g_extracted[name]):
1270                    new_grad[name] += np.tensordot(deriv[i_val + (i_dat, )], dat)
1271        else:
1272            for j_obs, obs in np.ndenumerate(data):
1273                for name in obs.names:
1274                    if name in obs.cov_names:
1275                        new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad
1276                    else:
1277                        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])
1278
1279        new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad}
1280
1281        if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()):
1282            raise Exception('The same name has been used for deltas and covobs!')
1283        new_samples = []
1284        new_means = []
1285        new_idl = []
1286        new_names_obs = []
1287        for name in new_names:
1288            if name not in new_covobs:
1289                if is_merged[name]:
1290                    filtered_deltas, filtered_idl_d = _filter_zeroes(new_deltas[name], new_idl_d[name])
1291                else:
1292                    filtered_deltas = new_deltas[name]
1293                    filtered_idl_d = new_idl_d[name]
1294
1295                new_samples.append(filtered_deltas)
1296                new_idl.append(filtered_idl_d)
1297                new_means.append(new_r_values[name][i_val])
1298                new_names_obs.append(name)
1299        final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl)
1300        for name in new_covobs:
1301            final_result[i_val].names.append(name)
1302        final_result[i_val]._covobs = new_covobs
1303        final_result[i_val]._value = new_val
1304        final_result[i_val].is_merged = is_merged
1305        final_result[i_val].reweighted = reweighted
1306
1307    if multi == 0:
1308        final_result = final_result.item()
1309
1310    return final_result

Construct a derived Obs according to func(data, **kwargs) using automatic differentiation.

Parameters
  • func (object): arbitrary function of the form func(data, **kwargs). For the automatic differentiation to work, all numpy functions have to have the autograd wrapper (use 'import autograd.numpy as anp').
  • data (list): list of Obs, e.g. [obs1, obs2, obs3].
  • num_grad (bool): if True, numerical derivatives are used instead of autograd (default False). To control the numerical differentiation the kwargs of numdifftools.step_generators.MaxStepGenerator can be used.
  • man_grad (list): manually supply a list or an array which contains the jacobian of func. Use cautiously, supplying the wrong derivative will not be intercepted.
Notes

For simple mathematical operations it can be practical to use anonymous functions. For the ratio of two observables one can e.g. use

new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2])

def reweight(weight, obs, **kwargs):
1347def reweight(weight, obs, **kwargs):
1348    """Reweight a list of observables.
1349
1350    Parameters
1351    ----------
1352    weight : Obs
1353        Reweighting factor. An Observable that has to be defined on a superset of the
1354        configurations in obs[i].idl for all i.
1355    obs : list
1356        list of Obs, e.g. [obs1, obs2, obs3].
1357    all_configs : bool
1358        if True, the reweighted observables are normalized by the average of
1359        the reweighting factor on all configurations in weight.idl and not
1360        on the configurations in obs[i].idl. Default False.
1361    """
1362    result = []
1363    for i in range(len(obs)):
1364        if len(obs[i].cov_names):
1365            raise Exception('Error: Not possible to reweight an Obs that contains covobs!')
1366        if not set(obs[i].names).issubset(weight.names):
1367            raise Exception('Error: Ensembles do not fit')
1368        for name in obs[i].names:
1369            if not set(obs[i].idl[name]).issubset(weight.idl[name]):
1370                raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name))
1371        new_samples = []
1372        w_deltas = {}
1373        for name in sorted(obs[i].names):
1374            w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name])
1375            new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name]))
1376        tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)])
1377
1378        if kwargs.get('all_configs'):
1379            new_weight = weight
1380        else:
1381            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)])
1382
1383        result.append(tmp_obs / new_weight)
1384        result[-1].reweighted = True
1385        result[-1].is_merged = obs[i].is_merged
1386
1387    return result

Reweight a list of observables.

Parameters
  • weight (Obs): Reweighting factor. An Observable that has to be defined on a superset of the configurations in obs[i].idl for all i.
  • obs (list): list of Obs, e.g. [obs1, obs2, obs3].
  • all_configs (bool): if True, the reweighted observables are normalized by the average of the reweighting factor on all configurations in weight.idl and not on the configurations in obs[i].idl. Default False.
def correlate(obs_a, obs_b):
1390def correlate(obs_a, obs_b):
1391    """Correlate two observables.
1392
1393    Parameters
1394    ----------
1395    obs_a : Obs
1396        First observable
1397    obs_b : Obs
1398        Second observable
1399
1400    Notes
1401    -----
1402    Keep in mind to only correlate primary observables which have not been reweighted
1403    yet. The reweighting has to be applied after correlating the observables.
1404    Currently only works if ensembles are identical (this is not strictly necessary).
1405    """
1406
1407    if sorted(obs_a.names) != sorted(obs_b.names):
1408        raise Exception(f"Ensembles do not fit {set(sorted(obs_a.names)) ^ set(sorted(obs_b.names))}")
1409    if len(obs_a.cov_names) or len(obs_b.cov_names):
1410        raise Exception('Error: Not possible to correlate Obs that contain covobs!')
1411    for name in obs_a.names:
1412        if obs_a.shape[name] != obs_b.shape[name]:
1413            raise Exception('Shapes of ensemble', name, 'do not fit')
1414        if obs_a.idl[name] != obs_b.idl[name]:
1415            raise Exception('idl of ensemble', name, 'do not fit')
1416
1417    if obs_a.reweighted is True:
1418        warnings.warn("The first observable is already reweighted.", RuntimeWarning)
1419    if obs_b.reweighted is True:
1420        warnings.warn("The second observable is already reweighted.", RuntimeWarning)
1421
1422    new_samples = []
1423    new_idl = []
1424    for name in sorted(obs_a.names):
1425        new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name]))
1426        new_idl.append(obs_a.idl[name])
1427
1428    o = Obs(new_samples, sorted(obs_a.names), idl=new_idl)
1429    o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names}
1430    o.reweighted = obs_a.reweighted or obs_b.reweighted
1431    return o

Correlate two observables.

Parameters
  • obs_a (Obs): First observable
  • obs_b (Obs): Second observable
Notes

Keep in mind to only correlate primary observables which have not been reweighted yet. The reweighting has to be applied after correlating the observables. Currently only works if ensembles are identical (this is not strictly necessary).

def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs):
1434def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs):
1435    r'''Calculates the error covariance matrix of a set of observables.
1436
1437    WARNING: This function should be used with care, especially for observables with support on multiple
1438             ensembles with differing autocorrelations. See the notes below for details.
1439
1440    The gamma method has to be applied first to all observables.
1441
1442    Parameters
1443    ----------
1444    obs : list or numpy.ndarray
1445        List or one dimensional array of Obs
1446    visualize : bool
1447        If True plots the corresponding normalized correlation matrix (default False).
1448    correlation : bool
1449        If True the correlation matrix instead of the error covariance matrix is returned (default False).
1450    smooth : None or int
1451        If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue
1452        smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the
1453        largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely
1454        small ones.
1455
1456    Notes
1457    -----
1458    The error covariance is defined such that it agrees with the squared standard error for two identical observables
1459    $$\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$$
1460    in the absence of autocorrelation.
1461    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
1462    $$\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.
1463    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.
1464    $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$
1465    This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors).
1466    '''
1467
1468    length = len(obs)
1469
1470    max_samples = np.max([o.N for o in obs])
1471    if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]:
1472        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)
1473
1474    cov = np.zeros((length, length))
1475    for i in range(length):
1476        for j in range(i, length):
1477            cov[i, j] = _covariance_element(obs[i], obs[j])
1478    cov = cov + cov.T - np.diag(np.diag(cov))
1479
1480    corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov)))
1481
1482    if isinstance(smooth, int):
1483        corr = _smooth_eigenvalues(corr, smooth)
1484
1485    if visualize:
1486        plt.matshow(corr, vmin=-1, vmax=1)
1487        plt.set_cmap('RdBu')
1488        plt.colorbar()
1489        plt.draw()
1490
1491    if correlation is True:
1492        return corr
1493
1494    errors = [o.dvalue for o in obs]
1495    cov = np.diag(errors) @ corr @ np.diag(errors)
1496
1497    eigenvalues = np.linalg.eigh(cov)[0]
1498    if not np.all(eigenvalues >= 0):
1499        warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning)
1500
1501    return cov

Calculates the error covariance matrix of a set of observables.

WARNING: This function should be used with care, especially for observables with support on multiple ensembles with differing autocorrelations. See the notes below for details.

The gamma method has to be applied first to all observables.

Parameters
  • obs (list or numpy.ndarray): List or one dimensional array of Obs
  • visualize (bool): If True plots the corresponding normalized correlation matrix (default False).
  • correlation (bool): If True the correlation matrix instead of the error covariance matrix is returned (default False).
  • smooth (None or int): If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely small ones.
Notes

The error covariance is defined such that it agrees with the squared standard error for two identical observables $$\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$$ in the absence of autocorrelation. 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 $$\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. 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. $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors).

def import_jackknife(jacks, name, idl=None):
1581def import_jackknife(jacks, name, idl=None):
1582    """Imports jackknife samples and returns an Obs
1583
1584    Parameters
1585    ----------
1586    jacks : numpy.ndarray
1587        numpy array containing the mean value as zeroth entry and
1588        the N jackknife samples as first to Nth entry.
1589    name : str
1590        name of the ensemble the samples are defined on.
1591    """
1592    length = len(jacks) - 1
1593    prj = (np.ones((length, length)) - (length - 1) * np.identity(length))
1594    samples = jacks[1:] @ prj
1595    mean = np.mean(samples)
1596    new_obs = Obs([samples - mean], [name], idl=idl, means=[mean])
1597    new_obs._value = jacks[0]
1598    return new_obs

Imports jackknife samples and returns an Obs

Parameters
  • jacks (numpy.ndarray): numpy array containing the mean value as zeroth entry and the N jackknife samples as first to Nth entry.
  • name (str): name of the ensemble the samples are defined on.
def merge_obs(list_of_obs):
1601def merge_obs(list_of_obs):
1602    """Combine all observables in list_of_obs into one new observable
1603
1604    Parameters
1605    ----------
1606    list_of_obs : list
1607        list of the Obs object to be combined
1608
1609    Notes
1610    -----
1611    It is not possible to combine obs which are based on the same replicum
1612    """
1613    replist = [item for obs in list_of_obs for item in obs.names]
1614    if (len(replist) == len(set(replist))) is False:
1615        raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist)))
1616    if any([len(o.cov_names) for o in list_of_obs]):
1617        raise Exception('Not possible to merge data that contains covobs!')
1618    new_dict = {}
1619    idl_dict = {}
1620    for o in list_of_obs:
1621        new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0)
1622                        for key in set(o.deltas) | set(o.r_values)})
1623        idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)})
1624
1625    names = sorted(new_dict.keys())
1626    o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names])
1627    o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names}
1628    o.reweighted = np.max([oi.reweighted for oi in list_of_obs])
1629    return o

Combine all observables in list_of_obs into one new observable

Parameters
  • list_of_obs (list): list of the Obs object to be combined
Notes

It is not possible to combine obs which are based on the same replicum

def cov_Obs(means, cov, name, grad=None):
1632def cov_Obs(means, cov, name, grad=None):
1633    """Create an Obs based on mean(s) and a covariance matrix
1634
1635    Parameters
1636    ----------
1637    mean : list of floats or float
1638        N mean value(s) of the new Obs
1639    cov : list or array
1640        2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance
1641    name : str
1642        identifier for the covariance matrix
1643    grad : list or array
1644        Gradient of the Covobs wrt. the means belonging to cov.
1645    """
1646
1647    def covobs_to_obs(co):
1648        """Make an Obs out of a Covobs
1649
1650        Parameters
1651        ----------
1652        co : Covobs
1653            Covobs to be embedded into the Obs
1654        """
1655        o = Obs([], [], means=[])
1656        o._value = co.value
1657        o.names.append(co.name)
1658        o._covobs[co.name] = co
1659        o._dvalue = np.sqrt(co.errsq())
1660        return o
1661
1662    ol = []
1663    if isinstance(means, (float, int)):
1664        means = [means]
1665
1666    for i in range(len(means)):
1667        ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad)))
1668    if ol[0].covobs[name].N != len(means):
1669        raise Exception('You have to provide %d mean values!' % (ol[0].N))
1670    if len(ol) == 1:
1671        return ol[0]
1672    return ol

Create an Obs based on mean(s) and a covariance matrix

Parameters
  • mean (list of floats or float): N mean value(s) of the new Obs
  • cov (list or array): 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance
  • name (str): identifier for the covariance matrix
  • grad (list or array): Gradient of the Covobs wrt. the means belonging to cov.