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

-
1258def reweight(weight, obs, **kwargs):
-1259    """Reweight a list of observables.
-1260
-1261    Parameters
-1262    ----------
-1263    weight : Obs
-1264        Reweighting factor. An Observable that has to be defined on a superset of the
-1265        configurations in obs[i].idl for all i.
-1266    obs : list
-1267        list of Obs, e.g. [obs1, obs2, obs3].
-1268    all_configs : bool
-1269        if True, the reweighted observables are normalized by the average of
-1270        the reweighting factor on all configurations in weight.idl and not
-1271        on the configurations in obs[i].idl.
-1272    """
-1273    result = []
-1274    for i in range(len(obs)):
-1275        if len(obs[i].cov_names):
-1276            raise Exception('Error: Not possible to reweight an Obs that contains covobs!')
-1277        if not set(obs[i].names).issubset(weight.names):
-1278            raise Exception('Error: Ensembles do not fit')
-1279        for name in obs[i].names:
-1280            if not set(obs[i].idl[name]).issubset(weight.idl[name]):
-1281                raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name))
-1282        new_samples = []
-1283        w_deltas = {}
-1284        for name in sorted(obs[i].names):
-1285            w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name])
-1286            new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name]))
-1287        tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)])
-1288
-1289        if kwargs.get('all_configs'):
-1290            new_weight = weight
-1291        else:
-1292            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)])
-1293
-1294        result.append(derived_observable(lambda x, **kwargs: x[0] / x[1], [tmp_obs, new_weight], **kwargs))
-1295        result[-1].reweighted = True
-1296        result[-1].is_merged = obs[i].is_merged
-1297
-1298    return result
+            
1321def reweight(weight, obs, **kwargs):
+1322    """Reweight a list of observables.
+1323
+1324    Parameters
+1325    ----------
+1326    weight : Obs
+1327        Reweighting factor. An Observable that has to be defined on a superset of the
+1328        configurations in obs[i].idl for all i.
+1329    obs : list
+1330        list of Obs, e.g. [obs1, obs2, obs3].
+1331    all_configs : bool
+1332        if True, the reweighted observables are normalized by the average of
+1333        the reweighting factor on all configurations in weight.idl and not
+1334        on the configurations in obs[i].idl.
+1335    """
+1336    result = []
+1337    for i in range(len(obs)):
+1338        if len(obs[i].cov_names):
+1339            raise Exception('Error: Not possible to reweight an Obs that contains covobs!')
+1340        if not set(obs[i].names).issubset(weight.names):
+1341            raise Exception('Error: Ensembles do not fit')
+1342        for name in obs[i].names:
+1343            if not set(obs[i].idl[name]).issubset(weight.idl[name]):
+1344                raise Exception('obs[%d] has to be defined on a subset of the configs in weight.idl[%s]!' % (i, name))
+1345        new_samples = []
+1346        w_deltas = {}
+1347        for name in sorted(obs[i].names):
+1348            w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name])
+1349            new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name]))
+1350        tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)])
+1351
+1352        if kwargs.get('all_configs'):
+1353            new_weight = weight
+1354        else:
+1355            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)])
+1356
+1357        result.append(derived_observable(lambda x, **kwargs: x[0] / x[1], [tmp_obs, new_weight], **kwargs))
+1358        result[-1].reweighted = True
+1359        result[-1].is_merged = obs[i].is_merged
+1360
+1361    return result
 
@@ -4838,48 +4902,48 @@ on the configurations in obs[i].idl.
-
1301def correlate(obs_a, obs_b):
-1302    """Correlate two observables.
-1303
-1304    Parameters
-1305    ----------
-1306    obs_a : Obs
-1307        First observable
-1308    obs_b : Obs
-1309        Second observable
-1310
-1311    Notes
-1312    -----
-1313    Keep in mind to only correlate primary observables which have not been reweighted
-1314    yet. The reweighting has to be applied after correlating the observables.
-1315    Currently only works if ensembles are identical (this is not strictly necessary).
-1316    """
-1317
-1318    if sorted(obs_a.names) != sorted(obs_b.names):
-1319        raise Exception('Ensembles do not fit')
-1320    if len(obs_a.cov_names) or len(obs_b.cov_names):
-1321        raise Exception('Error: Not possible to correlate Obs that contain covobs!')
-1322    for name in obs_a.names:
-1323        if obs_a.shape[name] != obs_b.shape[name]:
-1324            raise Exception('Shapes of ensemble', name, 'do not fit')
-1325        if obs_a.idl[name] != obs_b.idl[name]:
-1326            raise Exception('idl of ensemble', name, 'do not fit')
-1327
-1328    if obs_a.reweighted is True:
-1329        warnings.warn("The first observable is already reweighted.", RuntimeWarning)
-1330    if obs_b.reweighted is True:
-1331        warnings.warn("The second observable is already reweighted.", RuntimeWarning)
-1332
-1333    new_samples = []
-1334    new_idl = []
-1335    for name in sorted(obs_a.names):
-1336        new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name]))
-1337        new_idl.append(obs_a.idl[name])
-1338
-1339    o = Obs(new_samples, sorted(obs_a.names), idl=new_idl)
-1340    o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names}
-1341    o.reweighted = obs_a.reweighted or obs_b.reweighted
-1342    return o
+            
1364def correlate(obs_a, obs_b):
+1365    """Correlate two observables.
+1366
+1367    Parameters
+1368    ----------
+1369    obs_a : Obs
+1370        First observable
+1371    obs_b : Obs
+1372        Second observable
+1373
+1374    Notes
+1375    -----
+1376    Keep in mind to only correlate primary observables which have not been reweighted
+1377    yet. The reweighting has to be applied after correlating the observables.
+1378    Currently only works if ensembles are identical (this is not strictly necessary).
+1379    """
+1380
+1381    if sorted(obs_a.names) != sorted(obs_b.names):
+1382        raise Exception('Ensembles do not fit')
+1383    if len(obs_a.cov_names) or len(obs_b.cov_names):
+1384        raise Exception('Error: Not possible to correlate Obs that contain covobs!')
+1385    for name in obs_a.names:
+1386        if obs_a.shape[name] != obs_b.shape[name]:
+1387            raise Exception('Shapes of ensemble', name, 'do not fit')
+1388        if obs_a.idl[name] != obs_b.idl[name]:
+1389            raise Exception('idl of ensemble', name, 'do not fit')
+1390
+1391    if obs_a.reweighted is True:
+1392        warnings.warn("The first observable is already reweighted.", RuntimeWarning)
+1393    if obs_b.reweighted is True:
+1394        warnings.warn("The second observable is already reweighted.", RuntimeWarning)
+1395
+1396    new_samples = []
+1397    new_idl = []
+1398    for name in sorted(obs_a.names):
+1399        new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name]))
+1400        new_idl.append(obs_a.idl[name])
+1401
+1402    o = Obs(new_samples, sorted(obs_a.names), idl=new_idl)
+1403    o.is_merged = {name: (obs_a.is_merged.get(name, False) or obs_b.is_merged.get(name, False)) for name in o.names}
+1404    o.reweighted = obs_a.reweighted or obs_b.reweighted
+1405    return o
 
@@ -4914,71 +4978,71 @@ Currently only works if ensembles are identical (this is not strictly necessary)
-
1345def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs):
-1346    r'''Calculates the error covariance matrix of a set of observables.
-1347
-1348    The gamma method has to be applied first to all observables.
-1349
-1350    Parameters
-1351    ----------
-1352    obs : list or numpy.ndarray
-1353        List or one dimensional array of Obs
-1354    visualize : bool
-1355        If True plots the corresponding normalized correlation matrix (default False).
-1356    correlation : bool
-1357        If True the correlation matrix instead of the error covariance matrix is returned (default False).
-1358    smooth : None or int
-1359        If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue
-1360        smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the
-1361        largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely
-1362        small ones.
-1363
-1364    Notes
-1365    -----
-1366    The error covariance is defined such that it agrees with the squared standard error for two identical observables
-1367    $$\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$$
-1368    in the absence of autocorrelation.
-1369    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
-1370    $$\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.
-1371    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.
-1372    $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$
-1373    This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors).
-1374    '''
-1375
-1376    length = len(obs)
-1377
-1378    max_samples = np.max([o.N for o in obs])
-1379    if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]:
-1380        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)
-1381
-1382    cov = np.zeros((length, length))
-1383    for i in range(length):
-1384        for j in range(i, length):
-1385            cov[i, j] = _covariance_element(obs[i], obs[j])
-1386    cov = cov + cov.T - np.diag(np.diag(cov))
-1387
-1388    corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov)))
-1389
-1390    if isinstance(smooth, int):
-1391        corr = _smooth_eigenvalues(corr, smooth)
-1392
-1393    errors = [o.dvalue for o in obs]
-1394    cov = np.diag(errors) @ corr @ np.diag(errors)
-1395
-1396    eigenvalues = np.linalg.eigh(cov)[0]
-1397    if not np.all(eigenvalues >= 0):
-1398        warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning)
-1399
-1400    if visualize:
-1401        plt.matshow(corr, vmin=-1, vmax=1)
-1402        plt.set_cmap('RdBu')
-1403        plt.colorbar()
-1404        plt.draw()
-1405
-1406    if correlation is True:
-1407        return corr
-1408    else:
-1409        return cov
+            
1408def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs):
+1409    r'''Calculates the error covariance matrix of a set of observables.
+1410
+1411    The gamma method has to be applied first to all observables.
+1412
+1413    Parameters
+1414    ----------
+1415    obs : list or numpy.ndarray
+1416        List or one dimensional array of Obs
+1417    visualize : bool
+1418        If True plots the corresponding normalized correlation matrix (default False).
+1419    correlation : bool
+1420        If True the correlation matrix instead of the error covariance matrix is returned (default False).
+1421    smooth : None or int
+1422        If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue
+1423        smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the
+1424        largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely
+1425        small ones.
+1426
+1427    Notes
+1428    -----
+1429    The error covariance is defined such that it agrees with the squared standard error for two identical observables
+1430    $$\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$$
+1431    in the absence of autocorrelation.
+1432    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
+1433    $$\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.
+1434    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.
+1435    $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$
+1436    This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors).
+1437    '''
+1438
+1439    length = len(obs)
+1440
+1441    max_samples = np.max([o.N for o in obs])
+1442    if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]:
+1443        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)
+1444
+1445    cov = np.zeros((length, length))
+1446    for i in range(length):
+1447        for j in range(i, length):
+1448            cov[i, j] = _covariance_element(obs[i], obs[j])
+1449    cov = cov + cov.T - np.diag(np.diag(cov))
+1450
+1451    corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov)))
+1452
+1453    if isinstance(smooth, int):
+1454        corr = _smooth_eigenvalues(corr, smooth)
+1455
+1456    errors = [o.dvalue for o in obs]
+1457    cov = np.diag(errors) @ corr @ np.diag(errors)
+1458
+1459    eigenvalues = np.linalg.eigh(cov)[0]
+1460    if not np.all(eigenvalues >= 0):
+1461        warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning)
+1462
+1463    if visualize:
+1464        plt.matshow(corr, vmin=-1, vmax=1)
+1465        plt.set_cmap('RdBu')
+1466        plt.colorbar()
+1467        plt.draw()
+1468
+1469    if correlation is True:
+1470        return corr
+1471    else:
+1472        return cov
 
@@ -5027,24 +5091,24 @@ This construction ensures that the estimated covariance matrix is positive semi-
-
1488def import_jackknife(jacks, name, idl=None):
-1489    """Imports jackknife samples and returns an Obs
-1490
-1491    Parameters
-1492    ----------
-1493    jacks : numpy.ndarray
-1494        numpy array containing the mean value as zeroth entry and
-1495        the N jackknife samples as first to Nth entry.
-1496    name : str
-1497        name of the ensemble the samples are defined on.
-1498    """
-1499    length = len(jacks) - 1
-1500    prj = (np.ones((length, length)) - (length - 1) * np.identity(length))
-1501    samples = jacks[1:] @ prj
-1502    mean = np.mean(samples)
-1503    new_obs = Obs([samples - mean], [name], idl=idl, means=[mean])
-1504    new_obs._value = jacks[0]
-1505    return new_obs
+            
1552def import_jackknife(jacks, name, idl=None):
+1553    """Imports jackknife samples and returns an Obs
+1554
+1555    Parameters
+1556    ----------
+1557    jacks : numpy.ndarray
+1558        numpy array containing the mean value as zeroth entry and
+1559        the N jackknife samples as first to Nth entry.
+1560    name : str
+1561        name of the ensemble the samples are defined on.
+1562    """
+1563    length = len(jacks) - 1
+1564    prj = (np.ones((length, length)) - (length - 1) * np.identity(length))
+1565    samples = jacks[1:] @ prj
+1566    mean = np.mean(samples)
+1567    new_obs = Obs([samples - mean], [name], idl=idl, means=[mean])
+1568    new_obs._value = jacks[0]
+1569    return new_obs
 
@@ -5074,35 +5138,35 @@ name of the ensemble the samples are defined on.
-
1508def merge_obs(list_of_obs):
-1509    """Combine all observables in list_of_obs into one new observable
-1510
-1511    Parameters
-1512    ----------
-1513    list_of_obs : list
-1514        list of the Obs object to be combined
-1515
-1516    Notes
-1517    -----
-1518    It is not possible to combine obs which are based on the same replicum
-1519    """
-1520    replist = [item for obs in list_of_obs for item in obs.names]
-1521    if (len(replist) == len(set(replist))) is False:
-1522        raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist)))
-1523    if any([len(o.cov_names) for o in list_of_obs]):
-1524        raise Exception('Not possible to merge data that contains covobs!')
-1525    new_dict = {}
-1526    idl_dict = {}
-1527    for o in list_of_obs:
-1528        new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0)
-1529                        for key in set(o.deltas) | set(o.r_values)})
-1530        idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)})
-1531
-1532    names = sorted(new_dict.keys())
-1533    o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names])
-1534    o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names}
-1535    o.reweighted = np.max([oi.reweighted for oi in list_of_obs])
-1536    return o
+            
1572def merge_obs(list_of_obs):
+1573    """Combine all observables in list_of_obs into one new observable
+1574
+1575    Parameters
+1576    ----------
+1577    list_of_obs : list
+1578        list of the Obs object to be combined
+1579
+1580    Notes
+1581    -----
+1582    It is not possible to combine obs which are based on the same replicum
+1583    """
+1584    replist = [item for obs in list_of_obs for item in obs.names]
+1585    if (len(replist) == len(set(replist))) is False:
+1586        raise Exception('list_of_obs contains duplicate replica: %s' % (str(replist)))
+1587    if any([len(o.cov_names) for o in list_of_obs]):
+1588        raise Exception('Not possible to merge data that contains covobs!')
+1589    new_dict = {}
+1590    idl_dict = {}
+1591    for o in list_of_obs:
+1592        new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0)
+1593                        for key in set(o.deltas) | set(o.r_values)})
+1594        idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)})
+1595
+1596    names = sorted(new_dict.keys())
+1597    o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names])
+1598    o.is_merged = {name: np.any([oi.is_merged.get(name, False) for oi in list_of_obs]) for name in o.names}
+1599    o.reweighted = np.max([oi.reweighted for oi in list_of_obs])
+1600    return o
 
@@ -5133,47 +5197,47 @@ list of the Obs object to be combined
-
1539def cov_Obs(means, cov, name, grad=None):
-1540    """Create an Obs based on mean(s) and a covariance matrix
-1541
-1542    Parameters
-1543    ----------
-1544    mean : list of floats or float
-1545        N mean value(s) of the new Obs
-1546    cov : list or array
-1547        2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance
-1548    name : str
-1549        identifier for the covariance matrix
-1550    grad : list or array
-1551        Gradient of the Covobs wrt. the means belonging to cov.
-1552    """
-1553
-1554    def covobs_to_obs(co):
-1555        """Make an Obs out of a Covobs
-1556
-1557        Parameters
-1558        ----------
-1559        co : Covobs
-1560            Covobs to be embedded into the Obs
-1561        """
-1562        o = Obs([], [], means=[])
-1563        o._value = co.value
-1564        o.names.append(co.name)
-1565        o._covobs[co.name] = co
-1566        o._dvalue = np.sqrt(co.errsq())
-1567        return o
-1568
-1569    ol = []
-1570    if isinstance(means, (float, int)):
-1571        means = [means]
-1572
-1573    for i in range(len(means)):
-1574        ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad)))
-1575    if ol[0].covobs[name].N != len(means):
-1576        raise Exception('You have to provide %d mean values!' % (ol[0].N))
-1577    if len(ol) == 1:
-1578        return ol[0]
-1579    return ol
+            
1603def cov_Obs(means, cov, name, grad=None):
+1604    """Create an Obs based on mean(s) and a covariance matrix
+1605
+1606    Parameters
+1607    ----------
+1608    mean : list of floats or float
+1609        N mean value(s) of the new Obs
+1610    cov : list or array
+1611        2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance
+1612    name : str
+1613        identifier for the covariance matrix
+1614    grad : list or array
+1615        Gradient of the Covobs wrt. the means belonging to cov.
+1616    """
+1617
+1618    def covobs_to_obs(co):
+1619        """Make an Obs out of a Covobs
+1620
+1621        Parameters
+1622        ----------
+1623        co : Covobs
+1624            Covobs to be embedded into the Obs
+1625        """
+1626        o = Obs([], [], means=[])
+1627        o._value = co.value
+1628        o.names.append(co.name)
+1629        o._covobs[co.name] = co
+1630        o._dvalue = np.sqrt(co.errsq())
+1631        return o
+1632
+1633    ol = []
+1634    if isinstance(means, (float, int)):
+1635        means = [means]
+1636
+1637    for i in range(len(means)):
+1638        ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad)))
+1639    if ol[0].covobs[name].N != len(means):
+1640        raise Exception('You have to provide %d mean values!' % (ol[0].N))
+1641    if len(ol) == 1:
+1642        return ol[0]
+1643    return ol