diff --git a/docs/pyerrors/correlators.html b/docs/pyerrors/correlators.html index aa01d708..700e56d6 100644 --- a/docs/pyerrors/correlators.html +++ b/docs/pyerrors/correlators.html @@ -970,800 +970,795 @@ 727 if variant == 'log': 728 newcontent = [] 729 for t in range(self.T - 1): - 730 if ((self.content[t] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0): + 730 if ((self.content[t] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0) or self.content[t][0].value / self.content[t + 1][0].value < 0: 731 newcontent.append(None) - 732 elif self.content[t][0].value / self.content[t + 1][0].value < 0: - 733 newcontent.append(None) - 734 else: - 735 newcontent.append(self.content[t] / self.content[t + 1]) - 736 if (all([x is None for x in newcontent])): - 737 raise ValueError('m_eff is undefined at all timeslices') + 732 else: + 733 newcontent.append(self.content[t] / self.content[t + 1]) + 734 if (all([x is None for x in newcontent])): + 735 raise ValueError('m_eff is undefined at all timeslices') + 736 + 737 return np.log(Corr(newcontent, padding=[0, 1])) 738 - 739 return np.log(Corr(newcontent, padding=[0, 1])) - 740 - 741 elif variant == 'logsym': - 742 newcontent = [] - 743 for t in range(1, self.T - 1): - 744 if ((self.content[t - 1] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0): - 745 newcontent.append(None) - 746 elif self.content[t - 1][0].value / self.content[t + 1][0].value < 0: - 747 newcontent.append(None) - 748 else: - 749 newcontent.append(self.content[t - 1] / self.content[t + 1]) - 750 if (all([x is None for x in newcontent])): - 751 raise ValueError('m_eff is undefined at all timeslices') - 752 - 753 return np.log(Corr(newcontent, padding=[1, 1])) / 2 - 754 - 755 elif variant in ['periodic', 'cosh', 'sinh']: - 756 if variant in ['periodic', 'cosh']: - 757 func = anp.cosh - 758 else: - 759 func = anp.sinh - 760 - 761 def root_function(x, d): - 762 return func(x * (t - self.T / 2)) / func(x * (t + 1 - self.T / 2)) - d - 763 - 764 newcontent = [] - 765 for t in range(self.T - 1): - 766 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t + 1][0].value == 0): - 767 newcontent.append(None) - 768 # Fill the two timeslices in the middle of the lattice with their predecessors - 769 elif variant == 'sinh' and t in [self.T / 2, self.T / 2 - 1]: - 770 newcontent.append(newcontent[-1]) - 771 elif self.content[t][0].value / self.content[t + 1][0].value < 0: - 772 newcontent.append(None) - 773 else: - 774 newcontent.append(np.abs(find_root(self.content[t][0] / self.content[t + 1][0], root_function, guess=guess))) - 775 if (all([x is None for x in newcontent])): - 776 raise ValueError('m_eff is undefined at all timeslices') - 777 - 778 return Corr(newcontent, padding=[0, 1]) - 779 - 780 elif variant == 'arccosh': - 781 newcontent = [] - 782 for t in range(1, self.T - 1): - 783 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t - 1] is None) or (self.content[t][0].value == 0): - 784 newcontent.append(None) - 785 else: - 786 newcontent.append((self.content[t + 1] + self.content[t - 1]) / (2 * self.content[t])) - 787 if (all([x is None for x in newcontent])): - 788 raise ValueError("m_eff is undefined at all timeslices") - 789 return np.arccosh(Corr(newcontent, padding=[1, 1])) - 790 - 791 else: - 792 raise ValueError('Unknown variant.') - 793 - 794 def fit(self, function, fitrange=None, silent=False, **kwargs): - 795 r'''Fits function to the data - 796 - 797 Parameters - 798 ---------- - 799 function : obj - 800 function to fit to the data. See fits.least_squares for details. - 801 fitrange : list - 802 Two element list containing the timeslices on which the fit is supposed to start and stop. - 803 Caution: This range is inclusive as opposed to standard python indexing. - 804 `fitrange=[4, 6]` corresponds to the three entries 4, 5 and 6. - 805 If not specified, self.prange or all timeslices are used. - 806 silent : bool - 807 Decides whether output is printed to the standard output. - 808 ''' - 809 if self.N != 1: - 810 raise ValueError("Correlator must be projected before fitting") - 811 - 812 if fitrange is None: - 813 if self.prange: - 814 fitrange = self.prange - 815 else: - 816 fitrange = [0, self.T - 1] - 817 else: - 818 if not isinstance(fitrange, list): - 819 raise TypeError("fitrange has to be a list with two elements") - 820 if len(fitrange) != 2: - 821 raise ValueError("fitrange has to have exactly two elements [fit_start, fit_stop]") - 822 - 823 xs = np.array([x for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) - 824 ys = np.array([self.content[x][0] for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) - 825 result = least_squares(xs, ys, function, silent=silent, **kwargs) - 826 return result - 827 - 828 def plateau(self, plateau_range=None, method="fit", auto_gamma=False): - 829 """ Extract a plateau value from a Corr object - 830 - 831 Parameters - 832 ---------- - 833 plateau_range : list - 834 list with two entries, indicating the first and the last timeslice - 835 of the plateau region. - 836 method : str - 837 method to extract the plateau. - 838 'fit' fits a constant to the plateau region - 839 'avg', 'average' or 'mean' just average over the given timeslices. - 840 auto_gamma : bool - 841 apply gamma_method with default parameters to the Corr. Defaults to None - 842 """ - 843 if not plateau_range: - 844 if self.prange: - 845 plateau_range = self.prange - 846 else: - 847 raise ValueError("no plateau range provided") - 848 if self.N != 1: - 849 raise ValueError("Correlator must be projected before getting a plateau.") - 850 if (all([self.content[t] is None for t in range(plateau_range[0], plateau_range[1] + 1)])): - 851 raise ValueError("plateau is undefined at all timeslices in plateaurange.") - 852 if auto_gamma: - 853 self.gamma_method() - 854 if method == "fit": - 855 def const_func(a, t): - 856 return a[0] - 857 return self.fit(const_func, plateau_range)[0] - 858 elif method in ["avg", "average", "mean"]: - 859 returnvalue = np.mean([item[0] for item in self.content[plateau_range[0]:plateau_range[1] + 1] if item is not None]) - 860 return returnvalue - 861 - 862 else: - 863 raise ValueError("Unsupported plateau method: " + method) - 864 - 865 def set_prange(self, prange): - 866 """Sets the attribute prange of the Corr object.""" - 867 if not len(prange) == 2: - 868 raise ValueError("prange must be a list or array with two values") - 869 if not ((isinstance(prange[0], int)) and (isinstance(prange[1], int))): - 870 raise TypeError("Start and end point must be integers") - 871 if not (0 <= prange[0] <= self.T and 0 <= prange[1] <= self.T and prange[0] <= prange[1]): - 872 raise ValueError("Start and end point must define a range in the interval 0,T") - 873 - 874 self.prange = prange - 875 return - 876 - 877 def show(self, x_range=None, comp=None, y_range=None, logscale=False, plateau=None, fit_res=None, fit_key=None, ylabel=None, save=None, auto_gamma=False, hide_sigma=None, references=None, title=None): - 878 """Plots the correlator using the tag of the correlator as label if available. - 879 - 880 Parameters - 881 ---------- - 882 x_range : list - 883 list of two values, determining the range of the x-axis e.g. [4, 8]. - 884 comp : Corr or list of Corr - 885 Correlator or list of correlators which are plotted for comparison. - 886 The tags of these correlators are used as labels if available. - 887 logscale : bool - 888 Sets y-axis to logscale. - 889 plateau : Obs - 890 Plateau value to be visualized in the figure. - 891 fit_res : Fit_result - 892 Fit_result object to be visualized. - 893 fit_key : str - 894 Key for the fit function in Fit_result.fit_function (for combined fits). - 895 ylabel : str - 896 Label for the y-axis. - 897 save : str - 898 path to file in which the figure should be saved. - 899 auto_gamma : bool - 900 Apply the gamma method with standard parameters to all correlators and plateau values before plotting. - 901 hide_sigma : float - 902 Hides data points from the first value on which is consistent with zero within 'hide_sigma' standard errors. - 903 references : list - 904 List of floating point values that are displayed as horizontal lines for reference. - 905 title : string - 906 Optional title of the figure. - 907 """ - 908 if self.N != 1: - 909 raise ValueError("Correlator must be projected before plotting") - 910 - 911 if auto_gamma: - 912 self.gamma_method() - 913 - 914 if x_range is None: - 915 x_range = [0, self.T - 1] - 916 - 917 fig = plt.figure() - 918 ax1 = fig.add_subplot(111) - 919 - 920 x, y, y_err = self.plottable() - 921 if hide_sigma: - 922 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 + 739 elif variant == 'logsym': + 740 newcontent = [] + 741 for t in range(1, self.T - 1): + 742 if ((self.content[t - 1] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0) or self.content[t - 1][0].value / self.content[t + 1][0].value < 0: + 743 newcontent.append(None) + 744 else: + 745 newcontent.append(self.content[t - 1] / self.content[t + 1]) + 746 if (all([x is None for x in newcontent])): + 747 raise ValueError('m_eff is undefined at all timeslices') + 748 + 749 return np.log(Corr(newcontent, padding=[1, 1])) / 2 + 750 + 751 elif variant in ['periodic', 'cosh', 'sinh']: + 752 if variant in ['periodic', 'cosh']: + 753 func = anp.cosh + 754 else: + 755 func = anp.sinh + 756 + 757 def root_function(x, d): + 758 return func(x * (t - self.T / 2)) / func(x * (t + 1 - self.T / 2)) - d + 759 + 760 newcontent = [] + 761 for t in range(self.T - 1): + 762 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t + 1][0].value == 0): + 763 newcontent.append(None) + 764 # Fill the two timeslices in the middle of the lattice with their predecessors + 765 elif variant == 'sinh' and t in [self.T / 2, self.T / 2 - 1]: + 766 newcontent.append(newcontent[-1]) + 767 elif self.content[t][0].value / self.content[t + 1][0].value < 0: + 768 newcontent.append(None) + 769 else: + 770 newcontent.append(np.abs(find_root(self.content[t][0] / self.content[t + 1][0], root_function, guess=guess))) + 771 if (all([x is None for x in newcontent])): + 772 raise ValueError('m_eff is undefined at all timeslices') + 773 + 774 return Corr(newcontent, padding=[0, 1]) + 775 + 776 elif variant == 'arccosh': + 777 newcontent = [] + 778 for t in range(1, self.T - 1): + 779 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t - 1] is None) or (self.content[t][0].value == 0): + 780 newcontent.append(None) + 781 else: + 782 newcontent.append((self.content[t + 1] + self.content[t - 1]) / (2 * self.content[t])) + 783 if (all([x is None for x in newcontent])): + 784 raise ValueError("m_eff is undefined at all timeslices") + 785 return np.arccosh(Corr(newcontent, padding=[1, 1])) + 786 + 787 else: + 788 raise ValueError('Unknown variant.') + 789 + 790 def fit(self, function, fitrange=None, silent=False, **kwargs): + 791 r'''Fits function to the data + 792 + 793 Parameters + 794 ---------- + 795 function : obj + 796 function to fit to the data. See fits.least_squares for details. + 797 fitrange : list + 798 Two element list containing the timeslices on which the fit is supposed to start and stop. + 799 Caution: This range is inclusive as opposed to standard python indexing. + 800 `fitrange=[4, 6]` corresponds to the three entries 4, 5 and 6. + 801 If not specified, self.prange or all timeslices are used. + 802 silent : bool + 803 Decides whether output is printed to the standard output. + 804 ''' + 805 if self.N != 1: + 806 raise ValueError("Correlator must be projected before fitting") + 807 + 808 if fitrange is None: + 809 if self.prange: + 810 fitrange = self.prange + 811 else: + 812 fitrange = [0, self.T - 1] + 813 else: + 814 if not isinstance(fitrange, list): + 815 raise TypeError("fitrange has to be a list with two elements") + 816 if len(fitrange) != 2: + 817 raise ValueError("fitrange has to have exactly two elements [fit_start, fit_stop]") + 818 + 819 xs = np.array([x for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) + 820 ys = np.array([self.content[x][0] for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) + 821 result = least_squares(xs, ys, function, silent=silent, **kwargs) + 822 return result + 823 + 824 def plateau(self, plateau_range=None, method="fit", auto_gamma=False): + 825 """ Extract a plateau value from a Corr object + 826 + 827 Parameters + 828 ---------- + 829 plateau_range : list + 830 list with two entries, indicating the first and the last timeslice + 831 of the plateau region. + 832 method : str + 833 method to extract the plateau. + 834 'fit' fits a constant to the plateau region + 835 'avg', 'average' or 'mean' just average over the given timeslices. + 836 auto_gamma : bool + 837 apply gamma_method with default parameters to the Corr. Defaults to None + 838 """ + 839 if not plateau_range: + 840 if self.prange: + 841 plateau_range = self.prange + 842 else: + 843 raise ValueError("no plateau range provided") + 844 if self.N != 1: + 845 raise ValueError("Correlator must be projected before getting a plateau.") + 846 if (all([self.content[t] is None for t in range(plateau_range[0], plateau_range[1] + 1)])): + 847 raise ValueError("plateau is undefined at all timeslices in plateaurange.") + 848 if auto_gamma: + 849 self.gamma_method() + 850 if method == "fit": + 851 def const_func(a, t): + 852 return a[0] + 853 return self.fit(const_func, plateau_range)[0] + 854 elif method in ["avg", "average", "mean"]: + 855 returnvalue = np.mean([item[0] for item in self.content[plateau_range[0]:plateau_range[1] + 1] if item is not None]) + 856 return returnvalue + 857 + 858 else: + 859 raise ValueError("Unsupported plateau method: " + method) + 860 + 861 def set_prange(self, prange): + 862 """Sets the attribute prange of the Corr object.""" + 863 if not len(prange) == 2: + 864 raise ValueError("prange must be a list or array with two values") + 865 if not ((isinstance(prange[0], int)) and (isinstance(prange[1], int))): + 866 raise TypeError("Start and end point must be integers") + 867 if not (0 <= prange[0] <= self.T and 0 <= prange[1] <= self.T and prange[0] <= prange[1]): + 868 raise ValueError("Start and end point must define a range in the interval 0,T") + 869 + 870 self.prange = prange + 871 + 872 def show(self, x_range=None, comp=None, y_range=None, logscale=False, plateau=None, fit_res=None, fit_key=None, ylabel=None, save=None, auto_gamma=False, hide_sigma=None, references=None, title=None): + 873 """Plots the correlator using the tag of the correlator as label if available. + 874 + 875 Parameters + 876 ---------- + 877 x_range : list + 878 list of two values, determining the range of the x-axis e.g. [4, 8]. + 879 comp : Corr or list of Corr + 880 Correlator or list of correlators which are plotted for comparison. + 881 The tags of these correlators are used as labels if available. + 882 logscale : bool + 883 Sets y-axis to logscale. + 884 plateau : Obs + 885 Plateau value to be visualized in the figure. + 886 fit_res : Fit_result + 887 Fit_result object to be visualized. + 888 fit_key : str + 889 Key for the fit function in Fit_result.fit_function (for combined fits). + 890 ylabel : str + 891 Label for the y-axis. + 892 save : str + 893 path to file in which the figure should be saved. + 894 auto_gamma : bool + 895 Apply the gamma method with standard parameters to all correlators and plateau values before plotting. + 896 hide_sigma : float + 897 Hides data points from the first value on which is consistent with zero within 'hide_sigma' standard errors. + 898 references : list + 899 List of floating point values that are displayed as horizontal lines for reference. + 900 title : string + 901 Optional title of the figure. + 902 """ + 903 if self.N != 1: + 904 raise ValueError("Correlator must be projected before plotting") + 905 + 906 if auto_gamma: + 907 self.gamma_method() + 908 + 909 if x_range is None: + 910 x_range = [0, self.T - 1] + 911 + 912 fig = plt.figure() + 913 ax1 = fig.add_subplot(111) + 914 + 915 x, y, y_err = self.plottable() + 916 if hide_sigma: + 917 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 + 918 else: + 919 hide_from = None + 920 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=self.tag) + 921 if logscale: + 922 ax1.set_yscale('log') 923 else: - 924 hide_from = None - 925 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=self.tag) - 926 if logscale: - 927 ax1.set_yscale('log') - 928 else: - 929 if y_range is None: - 930 try: - 931 y_min = min([(x[0].value - x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) - 932 y_max = max([(x[0].value + x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) - 933 ax1.set_ylim([y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min)]) - 934 except Exception: - 935 pass - 936 else: - 937 ax1.set_ylim(y_range) - 938 if comp: - 939 if isinstance(comp, (Corr, list)): - 940 for corr in comp if isinstance(comp, list) else [comp]: - 941 if auto_gamma: - 942 corr.gamma_method() - 943 x, y, y_err = corr.plottable() - 944 if hide_sigma: - 945 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 - 946 else: - 947 hide_from = None - 948 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=corr.tag, mfc=plt.rcParams['axes.facecolor']) - 949 else: - 950 raise TypeError("'comp' must be a correlator or a list of correlators.") - 951 - 952 if plateau: - 953 if isinstance(plateau, Obs): - 954 if auto_gamma: - 955 plateau.gamma_method() - 956 ax1.axhline(y=plateau.value, linewidth=2, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--', label=str(plateau)) - 957 ax1.axhspan(plateau.value - plateau.dvalue, plateau.value + plateau.dvalue, alpha=0.25, color=plt.rcParams['text.color'], ls='-') - 958 else: - 959 raise TypeError("'plateau' must be an Obs") - 960 - 961 if references: - 962 if isinstance(references, list): - 963 for ref in references: - 964 ax1.axhline(y=ref, linewidth=1, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--') - 965 else: - 966 raise TypeError("'references' must be a list of floating pint values.") - 967 - 968 if self.prange: - 969 ax1.axvline(self.prange[0], 0, 1, ls='-', marker=',', color="black", zorder=0) - 970 ax1.axvline(self.prange[1], 0, 1, ls='-', marker=',', color="black", zorder=0) - 971 - 972 if fit_res: - 973 x_samples = np.arange(x_range[0], x_range[1] + 1, 0.05) - 974 if isinstance(fit_res.fit_function, dict): - 975 if fit_key: - 976 ax1.plot(x_samples, fit_res.fit_function[fit_key]([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) - 977 else: - 978 raise ValueError("Please provide a 'fit_key' for visualizing combined fits.") - 979 else: - 980 ax1.plot(x_samples, fit_res.fit_function([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) + 924 if y_range is None: + 925 try: + 926 y_min = min([(x[0].value - x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) + 927 y_max = max([(x[0].value + x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) + 928 ax1.set_ylim([y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min)]) + 929 except Exception: + 930 pass + 931 else: + 932 ax1.set_ylim(y_range) + 933 if comp: + 934 if isinstance(comp, (Corr, list)): + 935 for corr in comp if isinstance(comp, list) else [comp]: + 936 if auto_gamma: + 937 corr.gamma_method() + 938 x, y, y_err = corr.plottable() + 939 if hide_sigma: + 940 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 + 941 else: + 942 hide_from = None + 943 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=corr.tag, mfc=plt.rcParams['axes.facecolor']) + 944 else: + 945 raise TypeError("'comp' must be a correlator or a list of correlators.") + 946 + 947 if plateau: + 948 if isinstance(plateau, Obs): + 949 if auto_gamma: + 950 plateau.gamma_method() + 951 ax1.axhline(y=plateau.value, linewidth=2, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--', label=str(plateau)) + 952 ax1.axhspan(plateau.value - plateau.dvalue, plateau.value + plateau.dvalue, alpha=0.25, color=plt.rcParams['text.color'], ls='-') + 953 else: + 954 raise TypeError("'plateau' must be an Obs") + 955 + 956 if references: + 957 if isinstance(references, list): + 958 for ref in references: + 959 ax1.axhline(y=ref, linewidth=1, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--') + 960 else: + 961 raise TypeError("'references' must be a list of floating pint values.") + 962 + 963 if self.prange: + 964 ax1.axvline(self.prange[0], 0, 1, ls='-', marker=',', color="black", zorder=0) + 965 ax1.axvline(self.prange[1], 0, 1, ls='-', marker=',', color="black", zorder=0) + 966 + 967 if fit_res: + 968 x_samples = np.arange(x_range[0], x_range[1] + 1, 0.05) + 969 if isinstance(fit_res.fit_function, dict): + 970 if fit_key: + 971 ax1.plot(x_samples, fit_res.fit_function[fit_key]([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) + 972 else: + 973 raise ValueError("Please provide a 'fit_key' for visualizing combined fits.") + 974 else: + 975 ax1.plot(x_samples, fit_res.fit_function([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) + 976 + 977 ax1.set_xlabel(r'$x_0 / a$') + 978 if ylabel: + 979 ax1.set_ylabel(ylabel) + 980 ax1.set_xlim([x_range[0] - 0.5, x_range[1] + 0.5]) 981 - 982 ax1.set_xlabel(r'$x_0 / a$') - 983 if ylabel: - 984 ax1.set_ylabel(ylabel) - 985 ax1.set_xlim([x_range[0] - 0.5, x_range[1] + 0.5]) - 986 - 987 _handles, labels = ax1.get_legend_handles_labels() - 988 if labels: - 989 ax1.legend() + 982 _handles, labels = ax1.get_legend_handles_labels() + 983 if labels: + 984 ax1.legend() + 985 + 986 if title: + 987 plt.title(title) + 988 + 989 plt.draw() 990 - 991 if title: - 992 plt.title(title) - 993 - 994 plt.draw() - 995 - 996 if save: - 997 if isinstance(save, str): - 998 fig.savefig(save, bbox_inches='tight') - 999 else: -1000 raise TypeError("'save' has to be a string.") -1001 -1002 def spaghetti_plot(self, logscale=True): -1003 """Produces a spaghetti plot of the correlator suited to monitor exceptional configurations. -1004 -1005 Parameters -1006 ---------- -1007 logscale : bool -1008 Determines whether the scale of the y-axis is logarithmic or standard. -1009 """ -1010 if self.N != 1: -1011 raise ValueError("Correlator needs to be projected first.") -1012 -1013 mc_names = list(set([item for sublist in [list(itertools.chain.from_iterable(map(o[0].e_content.get, o[0].mc_names))) for o in self.content if o is not None] for item in sublist])) -1014 x0_vals = [n for (n, o) in zip(np.arange(self.T), self.content, strict=True) if o is not None] -1015 -1016 for name in mc_names: -1017 data = np.array([o[0].deltas[name] + o[0].r_values[name] for o in self.content if o is not None]).T + 991 if save: + 992 if isinstance(save, str): + 993 fig.savefig(save, bbox_inches='tight') + 994 else: + 995 raise TypeError("'save' has to be a string.") + 996 + 997 def spaghetti_plot(self, logscale=True): + 998 """Produces a spaghetti plot of the correlator suited to monitor exceptional configurations. + 999 +1000 Parameters +1001 ---------- +1002 logscale : bool +1003 Determines whether the scale of the y-axis is logarithmic or standard. +1004 """ +1005 if self.N != 1: +1006 raise ValueError("Correlator needs to be projected first.") +1007 +1008 mc_names = list(set([item for sublist in [list(itertools.chain.from_iterable(map(o[0].e_content.get, o[0].mc_names))) for o in self.content if o is not None] for item in sublist])) +1009 x0_vals = [n for (n, o) in zip(np.arange(self.T), self.content, strict=True) if o is not None] +1010 +1011 for name in mc_names: +1012 data = np.array([o[0].deltas[name] + o[0].r_values[name] for o in self.content if o is not None]).T +1013 +1014 fig = plt.figure() +1015 ax = fig.add_subplot(111) +1016 for dat in data: +1017 ax.plot(x0_vals, dat, ls='-', marker='') 1018 -1019 fig = plt.figure() -1020 ax = fig.add_subplot(111) -1021 for dat in data: -1022 ax.plot(x0_vals, dat, ls='-', marker='') -1023 -1024 if logscale is True: -1025 ax.set_yscale('log') -1026 -1027 ax.set_xlabel(r'$x_0 / a$') -1028 plt.title(name) -1029 plt.draw() -1030 -1031 def dump(self, filename, datatype="json.gz", **kwargs): -1032 """Dumps the Corr into a file of chosen type -1033 Parameters -1034 ---------- -1035 filename : str -1036 Name of the file to be saved. -1037 datatype : str -1038 Format of the exported file. Supported formats include -1039 "json.gz" and "pickle" -1040 path : str -1041 specifies a custom path for the file (default '.') -1042 """ -1043 if datatype == "json.gz": -1044 from .input.json import dump_to_json -1045 if 'path' in kwargs: -1046 file_name = kwargs.get('path') + '/' + filename -1047 else: -1048 file_name = filename -1049 dump_to_json(self, file_name) -1050 elif datatype == "pickle": -1051 dump_object(self, filename, **kwargs) -1052 else: -1053 raise ValueError("Unknown datatype " + str(datatype)) -1054 -1055 def print(self, print_range=None): -1056 print(self.__repr__(print_range)) -1057 -1058 def __repr__(self, print_range=None): -1059 if print_range is None: -1060 print_range = [0, None] -1061 -1062 content_string = "" -1063 content_string += "Corr T=" + str(self.T) + " N=" + str(self.N) + "\n" # +" filled with"+ str(type(self.content[0][0])) there should be a good solution here +1019 if logscale is True: +1020 ax.set_yscale('log') +1021 +1022 ax.set_xlabel(r'$x_0 / a$') +1023 plt.title(name) +1024 plt.draw() +1025 +1026 def dump(self, filename, datatype="json.gz", **kwargs): +1027 """Dumps the Corr into a file of chosen type +1028 Parameters +1029 ---------- +1030 filename : str +1031 Name of the file to be saved. +1032 datatype : str +1033 Format of the exported file. Supported formats include +1034 "json.gz" and "pickle" +1035 path : str +1036 specifies a custom path for the file (default '.') +1037 """ +1038 if datatype == "json.gz": +1039 from .input.json import dump_to_json +1040 if 'path' in kwargs: +1041 file_name = kwargs.get('path') + '/' + filename +1042 else: +1043 file_name = filename +1044 dump_to_json(self, file_name) +1045 elif datatype == "pickle": +1046 dump_object(self, filename, **kwargs) +1047 else: +1048 raise ValueError("Unknown datatype " + str(datatype)) +1049 +1050 def print(self, print_range=None): +1051 print(self.__repr__(print_range)) +1052 +1053 def __repr__(self, print_range=None): +1054 if print_range is None: +1055 print_range = [0, None] +1056 +1057 content_string = "" +1058 content_string += "Corr T=" + str(self.T) + " N=" + str(self.N) + "\n" # +" filled with"+ str(type(self.content[0][0])) there should be a good solution here +1059 +1060 if self.tag is not None: +1061 content_string += "Description: " + self.tag + "\n" +1062 if self.N != 1: +1063 return content_string 1064 -1065 if self.tag is not None: -1066 content_string += "Description: " + self.tag + "\n" -1067 if self.N != 1: -1068 return content_string -1069 -1070 if print_range[1]: -1071 print_range[1] += 1 -1072 content_string += 'x0/a\tCorr(x0/a)\n------------------\n' -1073 for i, sub_corr in enumerate(self.content[print_range[0]:print_range[1]]): -1074 if sub_corr is None: -1075 content_string += str(i + print_range[0]) + '\n' -1076 else: -1077 content_string += str(i + print_range[0]) -1078 for element in sub_corr: -1079 content_string += f"\t{element:+2}" -1080 content_string += '\n' -1081 return content_string -1082 -1083 def __str__(self): -1084 return self.__repr__() +1065 if print_range[1]: +1066 print_range[1] += 1 +1067 content_string += 'x0/a\tCorr(x0/a)\n------------------\n' +1068 for i, sub_corr in enumerate(self.content[print_range[0]:print_range[1]]): +1069 if sub_corr is None: +1070 content_string += str(i + print_range[0]) + '\n' +1071 else: +1072 content_string += str(i + print_range[0]) +1073 for element in sub_corr: +1074 content_string += f"\t{element:+2}" +1075 content_string += '\n' +1076 return content_string +1077 +1078 def __str__(self): +1079 return self.__repr__() +1080 +1081 # We define the basic operations, that can be performed with correlators. +1082 # While */+- get defined here, they only work for Corr*Obs and not Obs*Corr. +1083 # This is because Obs*Corr checks Obs.__mul__ first and does not catch an exception. +1084 # One could try and tell Obs to check if the y in __mul__ is a Corr and 1085 -1086 # We define the basic operations, that can be performed with correlators. -1087 # While */+- get defined here, they only work for Corr*Obs and not Obs*Corr. -1088 # This is because Obs*Corr checks Obs.__mul__ first and does not catch an exception. -1089 # One could try and tell Obs to check if the y in __mul__ is a Corr and -1090 -1091 __array_priority__ = 10000 -1092 -1093 def __eq__(self, y): -1094 if isinstance(y, Corr): -1095 comp = np.asarray(y.content, dtype=object) -1096 else: -1097 comp = np.asarray(y) -1098 return np.asarray(self.content, dtype=object) == comp -1099 -1100 __hash__ = None -1101 -1102 def __add__(self, y): -1103 if isinstance(y, Corr): -1104 if ((self.N != y.N) or (self.T != y.T)): -1105 raise ValueError("Addition of Corrs with different shape") -1106 newcontent = [] -1107 for t in range(self.T): -1108 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1109 newcontent.append(None) -1110 else: -1111 newcontent.append(self.content[t] + y.content[t]) -1112 return Corr(newcontent) -1113 -1114 elif isinstance(y, (Obs, int, float, CObs, complex)): -1115 newcontent = [] -1116 for t in range(self.T): -1117 if _check_for_none(self, self.content[t]): -1118 newcontent.append(None) -1119 else: -1120 newcontent.append(self.content[t] + y) -1121 return Corr(newcontent, prange=self.prange) -1122 elif isinstance(y, np.ndarray): -1123 if y.shape == (self.T,): -1124 return Corr(list((np.array(self.content).T + y).T)) -1125 else: -1126 raise ValueError("operands could not be broadcast together") -1127 else: -1128 raise TypeError("Corr + wrong type") -1129 -1130 def __mul__(self, y): -1131 if isinstance(y, Corr): -1132 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): -1133 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") -1134 newcontent = [] -1135 for t in range(self.T): -1136 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1137 newcontent.append(None) -1138 else: -1139 newcontent.append(self.content[t] * y.content[t]) -1140 return Corr(newcontent) -1141 -1142 elif isinstance(y, (Obs, int, float, CObs, complex)): -1143 newcontent = [] -1144 for t in range(self.T): -1145 if _check_for_none(self, self.content[t]): -1146 newcontent.append(None) -1147 else: -1148 newcontent.append(self.content[t] * y) -1149 return Corr(newcontent, prange=self.prange) -1150 elif isinstance(y, np.ndarray): -1151 if y.shape == (self.T,): -1152 return Corr(list((np.array(self.content).T * y).T)) -1153 else: -1154 raise ValueError("operands could not be broadcast together") -1155 else: -1156 raise TypeError("Corr * wrong type") -1157 -1158 def __matmul__(self, y): -1159 if isinstance(y, np.ndarray): -1160 if y.ndim != 2 or y.shape[0] != y.shape[1]: -1161 raise ValueError("Can only multiply correlators by square matrices.") -1162 if not self.N == y.shape[0]: -1163 raise ValueError("matmul: mismatch of matrix dimensions") -1164 newcontent = [] -1165 for t in range(self.T): -1166 if _check_for_none(self, self.content[t]): -1167 newcontent.append(None) -1168 else: -1169 newcontent.append(self.content[t] @ y) -1170 return Corr(newcontent) -1171 elif isinstance(y, Corr): -1172 if not self.N == y.N: -1173 raise ValueError("matmul: mismatch of matrix dimensions") -1174 newcontent = [] -1175 for t in range(self.T): -1176 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1177 newcontent.append(None) -1178 else: -1179 newcontent.append(self.content[t] @ y.content[t]) -1180 return Corr(newcontent) -1181 -1182 else: -1183 return NotImplemented -1184 -1185 def __rmatmul__(self, y): -1186 if isinstance(y, np.ndarray): -1187 if y.ndim != 2 or y.shape[0] != y.shape[1]: -1188 raise ValueError("Can only multiply correlators by square matrices.") -1189 if not self.N == y.shape[0]: -1190 raise ValueError("matmul: mismatch of matrix dimensions") -1191 newcontent = [] -1192 for t in range(self.T): -1193 if _check_for_none(self, self.content[t]): -1194 newcontent.append(None) -1195 else: -1196 newcontent.append(y @ self.content[t]) -1197 return Corr(newcontent) -1198 else: -1199 return NotImplemented -1200 -1201 def __truediv__(self, y): -1202 if isinstance(y, Corr): -1203 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): -1204 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") -1205 newcontent = [] +1086 __array_priority__ = 10000 +1087 +1088 def __eq__(self, y): +1089 if isinstance(y, Corr): +1090 comp = np.asarray(y.content, dtype=object) +1091 else: +1092 comp = np.asarray(y) +1093 return np.asarray(self.content, dtype=object) == comp +1094 +1095 __hash__ = None +1096 +1097 def __add__(self, y): +1098 if isinstance(y, Corr): +1099 if ((self.N != y.N) or (self.T != y.T)): +1100 raise ValueError("Addition of Corrs with different shape") +1101 newcontent = [] +1102 for t in range(self.T): +1103 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1104 newcontent.append(None) +1105 else: +1106 newcontent.append(self.content[t] + y.content[t]) +1107 return Corr(newcontent) +1108 +1109 elif isinstance(y, (Obs, int, float, CObs, complex)): +1110 newcontent = [] +1111 for t in range(self.T): +1112 if _check_for_none(self, self.content[t]): +1113 newcontent.append(None) +1114 else: +1115 newcontent.append(self.content[t] + y) +1116 return Corr(newcontent, prange=self.prange) +1117 elif isinstance(y, np.ndarray): +1118 if y.shape == (self.T,): +1119 return Corr(list((np.array(self.content).T + y).T)) +1120 else: +1121 raise ValueError("operands could not be broadcast together") +1122 else: +1123 raise TypeError("Corr + wrong type") +1124 +1125 def __mul__(self, y): +1126 if isinstance(y, Corr): +1127 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): +1128 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") +1129 newcontent = [] +1130 for t in range(self.T): +1131 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1132 newcontent.append(None) +1133 else: +1134 newcontent.append(self.content[t] * y.content[t]) +1135 return Corr(newcontent) +1136 +1137 elif isinstance(y, (Obs, int, float, CObs, complex)): +1138 newcontent = [] +1139 for t in range(self.T): +1140 if _check_for_none(self, self.content[t]): +1141 newcontent.append(None) +1142 else: +1143 newcontent.append(self.content[t] * y) +1144 return Corr(newcontent, prange=self.prange) +1145 elif isinstance(y, np.ndarray): +1146 if y.shape == (self.T,): +1147 return Corr(list((np.array(self.content).T * y).T)) +1148 else: +1149 raise ValueError("operands could not be broadcast together") +1150 else: +1151 raise TypeError("Corr * wrong type") +1152 +1153 def __matmul__(self, y): +1154 if isinstance(y, np.ndarray): +1155 if y.ndim != 2 or y.shape[0] != y.shape[1]: +1156 raise ValueError("Can only multiply correlators by square matrices.") +1157 if not self.N == y.shape[0]: +1158 raise ValueError("matmul: mismatch of matrix dimensions") +1159 newcontent = [] +1160 for t in range(self.T): +1161 if _check_for_none(self, self.content[t]): +1162 newcontent.append(None) +1163 else: +1164 newcontent.append(self.content[t] @ y) +1165 return Corr(newcontent) +1166 elif isinstance(y, Corr): +1167 if not self.N == y.N: +1168 raise ValueError("matmul: mismatch of matrix dimensions") +1169 newcontent = [] +1170 for t in range(self.T): +1171 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1172 newcontent.append(None) +1173 else: +1174 newcontent.append(self.content[t] @ y.content[t]) +1175 return Corr(newcontent) +1176 +1177 else: +1178 return NotImplemented +1179 +1180 def __rmatmul__(self, y): +1181 if isinstance(y, np.ndarray): +1182 if y.ndim != 2 or y.shape[0] != y.shape[1]: +1183 raise ValueError("Can only multiply correlators by square matrices.") +1184 if not self.N == y.shape[0]: +1185 raise ValueError("matmul: mismatch of matrix dimensions") +1186 newcontent = [] +1187 for t in range(self.T): +1188 if _check_for_none(self, self.content[t]): +1189 newcontent.append(None) +1190 else: +1191 newcontent.append(y @ self.content[t]) +1192 return Corr(newcontent) +1193 else: +1194 return NotImplemented +1195 +1196 def __truediv__(self, y): +1197 if isinstance(y, Corr): +1198 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): +1199 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") +1200 newcontent = [] +1201 for t in range(self.T): +1202 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1203 newcontent.append(None) +1204 else: +1205 newcontent.append(self.content[t] / y.content[t]) 1206 for t in range(self.T): -1207 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1208 newcontent.append(None) -1209 else: -1210 newcontent.append(self.content[t] / y.content[t]) -1211 for t in range(self.T): -1212 if _check_for_none(self, newcontent[t]): -1213 continue -1214 if np.isnan(np.sum(newcontent[t]).value): -1215 newcontent[t] = None -1216 -1217 if all([item is None for item in newcontent]): -1218 raise ValueError("Division returns completely undefined correlator") -1219 return Corr(newcontent) -1220 -1221 elif isinstance(y, (Obs, CObs)): -1222 if isinstance(y, Obs): -1223 if y.value == 0: -1224 raise ValueError('Division by zero will return undefined correlator') -1225 if isinstance(y, CObs): -1226 if y.is_zero(): -1227 raise ValueError('Division by zero will return undefined correlator') -1228 -1229 newcontent = [] -1230 for t in range(self.T): -1231 if _check_for_none(self, self.content[t]): -1232 newcontent.append(None) -1233 else: -1234 newcontent.append(self.content[t] / y) -1235 return Corr(newcontent, prange=self.prange) -1236 -1237 elif isinstance(y, (int, float)): -1238 if y == 0: -1239 raise ValueError('Division by zero will return undefined correlator') -1240 newcontent = [] -1241 for t in range(self.T): -1242 if _check_for_none(self, self.content[t]): -1243 newcontent.append(None) -1244 else: -1245 newcontent.append(self.content[t] / y) -1246 return Corr(newcontent, prange=self.prange) -1247 elif isinstance(y, np.ndarray): -1248 if y.shape == (self.T,): -1249 return Corr(list((np.array(self.content).T / y).T)) -1250 else: -1251 raise ValueError("operands could not be broadcast together") -1252 else: -1253 raise TypeError('Corr / wrong type') -1254 -1255 def __neg__(self): -1256 newcontent = [None if _check_for_none(self, item) else -1. * item for item in self.content] -1257 return Corr(newcontent, prange=self.prange) -1258 -1259 def __sub__(self, y): -1260 return self + (-y) -1261 -1262 def __pow__(self, y): -1263 if isinstance(y, (Obs, int, float, CObs)): -1264 newcontent = [None if _check_for_none(self, item) else item**y for item in self.content] -1265 return Corr(newcontent, prange=self.prange) -1266 else: -1267 raise TypeError('Type of exponent not supported') -1268 -1269 def __abs__(self): -1270 newcontent = [None if _check_for_none(self, item) else np.abs(item) for item in self.content] -1271 return Corr(newcontent, prange=self.prange) -1272 -1273 # The numpy functions: -1274 def sqrt(self): -1275 return self ** 0.5 -1276 -1277 def log(self): -1278 newcontent = [None if _check_for_none(self, item) else np.log(item) for item in self.content] -1279 return Corr(newcontent, prange=self.prange) -1280 -1281 def exp(self): -1282 newcontent = [None if _check_for_none(self, item) else np.exp(item) for item in self.content] -1283 return Corr(newcontent, prange=self.prange) -1284 -1285 def _apply_func_to_corr(self, func): -1286 newcontent = [None if _check_for_none(self, item) else func(item) for item in self.content] -1287 for t in range(self.T): -1288 if _check_for_none(self, newcontent[t]): -1289 continue -1290 tmp_sum = np.sum(newcontent[t]) -1291 if hasattr(tmp_sum, "value"): -1292 if np.isnan(tmp_sum.value): -1293 newcontent[t] = None -1294 if all([item is None for item in newcontent]): -1295 raise ValueError('Operation returns undefined correlator') -1296 return Corr(newcontent) -1297 -1298 def sin(self): -1299 return self._apply_func_to_corr(np.sin) -1300 -1301 def cos(self): -1302 return self._apply_func_to_corr(np.cos) -1303 -1304 def tan(self): -1305 return self._apply_func_to_corr(np.tan) -1306 -1307 def sinh(self): -1308 return self._apply_func_to_corr(np.sinh) -1309 -1310 def cosh(self): -1311 return self._apply_func_to_corr(np.cosh) -1312 -1313 def tanh(self): -1314 return self._apply_func_to_corr(np.tanh) -1315 -1316 def arcsin(self): -1317 return self._apply_func_to_corr(np.arcsin) -1318 -1319 def arccos(self): -1320 return self._apply_func_to_corr(np.arccos) -1321 -1322 def arctan(self): -1323 return self._apply_func_to_corr(np.arctan) -1324 -1325 def arcsinh(self): -1326 return self._apply_func_to_corr(np.arcsinh) -1327 -1328 def arccosh(self): -1329 return self._apply_func_to_corr(np.arccosh) -1330 -1331 def arctanh(self): -1332 return self._apply_func_to_corr(np.arctanh) -1333 -1334 # Right hand side operations (require tweak in main module to work) -1335 def __radd__(self, y): -1336 return self + y -1337 -1338 def __rsub__(self, y): -1339 return -self + y -1340 -1341 def __rmul__(self, y): -1342 return self * y -1343 -1344 def __rtruediv__(self, y): -1345 return (self / y) ** (-1) -1346 -1347 @property -1348 def real(self): -1349 def return_real(obs_OR_cobs): -1350 if isinstance(obs_OR_cobs.flatten()[0], CObs): -1351 return np.vectorize(lambda x: x.real)(obs_OR_cobs) -1352 else: -1353 return obs_OR_cobs -1354 -1355 return self._apply_func_to_corr(return_real) -1356 -1357 @property -1358 def imag(self): -1359 def return_imag(obs_OR_cobs): -1360 if isinstance(obs_OR_cobs.flatten()[0], CObs): -1361 return np.vectorize(lambda x: x.imag)(obs_OR_cobs) -1362 else: -1363 return obs_OR_cobs * 0 # So it stays the right type +1207 if _check_for_none(self, newcontent[t]): +1208 continue +1209 if np.isnan(np.sum(newcontent[t]).value): +1210 newcontent[t] = None +1211 +1212 if all([item is None for item in newcontent]): +1213 raise ValueError("Division returns completely undefined correlator") +1214 return Corr(newcontent) +1215 +1216 elif isinstance(y, (Obs, CObs)): +1217 if isinstance(y, Obs): +1218 if y.value == 0: +1219 raise ValueError('Division by zero will return undefined correlator') +1220 if isinstance(y, CObs): +1221 if y.is_zero(): +1222 raise ValueError('Division by zero will return undefined correlator') +1223 +1224 newcontent = [] +1225 for t in range(self.T): +1226 if _check_for_none(self, self.content[t]): +1227 newcontent.append(None) +1228 else: +1229 newcontent.append(self.content[t] / y) +1230 return Corr(newcontent, prange=self.prange) +1231 +1232 elif isinstance(y, (int, float)): +1233 if y == 0: +1234 raise ValueError('Division by zero will return undefined correlator') +1235 newcontent = [] +1236 for t in range(self.T): +1237 if _check_for_none(self, self.content[t]): +1238 newcontent.append(None) +1239 else: +1240 newcontent.append(self.content[t] / y) +1241 return Corr(newcontent, prange=self.prange) +1242 elif isinstance(y, np.ndarray): +1243 if y.shape == (self.T,): +1244 return Corr(list((np.array(self.content).T / y).T)) +1245 else: +1246 raise ValueError("operands could not be broadcast together") +1247 else: +1248 raise TypeError('Corr / wrong type') +1249 +1250 def __neg__(self): +1251 newcontent = [None if _check_for_none(self, item) else -1. * item for item in self.content] +1252 return Corr(newcontent, prange=self.prange) +1253 +1254 def __sub__(self, y): +1255 return self + (-y) +1256 +1257 def __pow__(self, y): +1258 if isinstance(y, (Obs, int, float, CObs)): +1259 newcontent = [None if _check_for_none(self, item) else item**y for item in self.content] +1260 return Corr(newcontent, prange=self.prange) +1261 else: +1262 raise TypeError('Type of exponent not supported') +1263 +1264 def __abs__(self): +1265 newcontent = [None if _check_for_none(self, item) else np.abs(item) for item in self.content] +1266 return Corr(newcontent, prange=self.prange) +1267 +1268 # The numpy functions: +1269 def sqrt(self): +1270 return self ** 0.5 +1271 +1272 def log(self): +1273 newcontent = [None if _check_for_none(self, item) else np.log(item) for item in self.content] +1274 return Corr(newcontent, prange=self.prange) +1275 +1276 def exp(self): +1277 newcontent = [None if _check_for_none(self, item) else np.exp(item) for item in self.content] +1278 return Corr(newcontent, prange=self.prange) +1279 +1280 def _apply_func_to_corr(self, func): +1281 newcontent = [None if _check_for_none(self, item) else func(item) for item in self.content] +1282 for t in range(self.T): +1283 if _check_for_none(self, newcontent[t]): +1284 continue +1285 tmp_sum = np.sum(newcontent[t]) +1286 if hasattr(tmp_sum, "value"): +1287 if np.isnan(tmp_sum.value): +1288 newcontent[t] = None +1289 if all([item is None for item in newcontent]): +1290 raise ValueError('Operation returns undefined correlator') +1291 return Corr(newcontent) +1292 +1293 def sin(self): +1294 return self._apply_func_to_corr(np.sin) +1295 +1296 def cos(self): +1297 return self._apply_func_to_corr(np.cos) +1298 +1299 def tan(self): +1300 return self._apply_func_to_corr(np.tan) +1301 +1302 def sinh(self): +1303 return self._apply_func_to_corr(np.sinh) +1304 +1305 def cosh(self): +1306 return self._apply_func_to_corr(np.cosh) +1307 +1308 def tanh(self): +1309 return self._apply_func_to_corr(np.tanh) +1310 +1311 def arcsin(self): +1312 return self._apply_func_to_corr(np.arcsin) +1313 +1314 def arccos(self): +1315 return self._apply_func_to_corr(np.arccos) +1316 +1317 def arctan(self): +1318 return self._apply_func_to_corr(np.arctan) +1319 +1320 def arcsinh(self): +1321 return self._apply_func_to_corr(np.arcsinh) +1322 +1323 def arccosh(self): +1324 return self._apply_func_to_corr(np.arccosh) +1325 +1326 def arctanh(self): +1327 return self._apply_func_to_corr(np.arctanh) +1328 +1329 # Right hand side operations (require tweak in main module to work) +1330 def __radd__(self, y): +1331 return self + y +1332 +1333 def __rsub__(self, y): +1334 return -self + y +1335 +1336 def __rmul__(self, y): +1337 return self * y +1338 +1339 def __rtruediv__(self, y): +1340 return (self / y) ** (-1) +1341 +1342 @property +1343 def real(self): +1344 def return_real(obs_OR_cobs): +1345 if isinstance(obs_OR_cobs.flatten()[0], CObs): +1346 return np.vectorize(lambda x: x.real)(obs_OR_cobs) +1347 else: +1348 return obs_OR_cobs +1349 +1350 return self._apply_func_to_corr(return_real) +1351 +1352 @property +1353 def imag(self): +1354 def return_imag(obs_OR_cobs): +1355 if isinstance(obs_OR_cobs.flatten()[0], CObs): +1356 return np.vectorize(lambda x: x.imag)(obs_OR_cobs) +1357 else: +1358 return obs_OR_cobs * 0 # So it stays the right type +1359 +1360 return self._apply_func_to_corr(return_imag) +1361 +1362 def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None): +1363 r''' Project large correlation matrix to lowest states 1364 -1365 return self._apply_func_to_corr(return_imag) -1366 -1367 def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None): -1368 r''' Project large correlation matrix to lowest states -1369 -1370 This method can be used to reduce the size of an (N x N) correlation matrix -1371 to (Ntrunc x Ntrunc) by solving a GEVP at very early times where the noise -1372 is still small. -1373 -1374 Parameters -1375 ---------- -1376 Ntrunc: int -1377 Rank of the target matrix. -1378 tproj: int -1379 Time where the eigenvectors are evaluated, corresponds to ts in the GEVP method. -1380 The default value is 3. -1381 t0proj: int -1382 Time where the correlation matrix is inverted. Choosing t0proj=1 is strongly -1383 discouraged for O(a) improved theories, since the correctness of the procedure -1384 cannot be granted in this case. The default value is 2. -1385 basematrix : Corr -1386 Correlation matrix that is used to determine the eigenvectors of the -1387 lowest states based on a GEVP. basematrix is taken to be the Corr itself if -1388 is is not specified. -1389 -1390 Notes -1391 ----- -1392 We have the basematrix $C(t)$ and the target matrix $G(t)$. We start by solving -1393 the GEVP $$C(t) v_n(t, t_0) = \lambda_n(t, t_0) C(t_0) v_n(t, t_0)$$ where $t \equiv t_\mathrm{proj}$ -1394 and $t_0 \equiv t_{0, \mathrm{proj}}$. The target matrix is projected onto the subspace of the -1395 resulting eigenvectors $v_n, n=1,\dots,N_\mathrm{trunc}$ via -1396 $$G^\prime_{i, j}(t) = (v_i, G(t) v_j)$$. This allows to reduce the size of a large -1397 correlation matrix and to remove some noise that is added by irrelevant operators. -1398 This may allow to use the GEVP on $G(t)$ at late times such that the theoretically motivated -1399 bound $t_0 \leq t/2$ holds, since the condition number of $G(t)$ is decreased, compared to $C(t)$. -1400 ''' -1401 -1402 if self.N == 1: -1403 raise ValueError('Method cannot be applied to one-dimensional correlators.') -1404 if basematrix is None: -1405 basematrix = self -1406 if Ntrunc >= basematrix.N: -1407 raise ValueError(f'Cannot truncate using Ntrunc >= {basematrix.N}') -1408 if basematrix.N != self.N: -1409 raise ValueError('basematrix and targetmatrix have to be of the same size.') -1410 -1411 evecs = basematrix.GEVP(t0proj, tproj, sort=None)[:Ntrunc] -1412 -1413 tmpmat = np.empty((Ntrunc, Ntrunc), dtype=object) -1414 rmat = [] -1415 for t in range(basematrix.T): -1416 if self.content[t] is None: -1417 rmat.append(None) -1418 else: -1419 for i in range(Ntrunc): -1420 for j in range(Ntrunc): -1421 tmpmat[i][j] = evecs[i].T @ self[t] @ evecs[j] -1422 rmat.append(np.copy(tmpmat)) -1423 -1424 return Corr(rmat) -1425 -1426 -1427def _sort_vectors(vec_set_in, ts): -1428 """Helper function used to find a set of Eigenvectors consistent over all timeslices""" -1429 -1430 if isinstance(vec_set_in[ts][0][0], Obs): -1431 vec_set = [anp.vectorize(float)(vi) if vi is not None else vi for vi in vec_set_in] -1432 else: -1433 vec_set = vec_set_in -1434 reference_sorting = np.array(vec_set[ts]) -1435 N = reference_sorting.shape[0] -1436 sorted_vec_set = [] -1437 for t in range(len(vec_set)): -1438 if vec_set[t] is None: -1439 sorted_vec_set.append(None) -1440 elif not t == ts: -1441 perms = [list(o) for o in permutations([i for i in range(N)], N)] -1442 best_score = 0 -1443 for perm in perms: -1444 current_score = 1 -1445 for k in range(N): -1446 new_sorting = reference_sorting.copy() -1447 new_sorting[perm[k], :] = vec_set[t][k] -1448 current_score *= abs(np.linalg.det(new_sorting)) -1449 if current_score > best_score: -1450 best_score = current_score -1451 best_perm = perm -1452 sorted_vec_set.append([vec_set_in[t][k] for k in best_perm]) -1453 else: -1454 sorted_vec_set.append(vec_set_in[t]) -1455 -1456 return sorted_vec_set +1365 This method can be used to reduce the size of an (N x N) correlation matrix +1366 to (Ntrunc x Ntrunc) by solving a GEVP at very early times where the noise +1367 is still small. +1368 +1369 Parameters +1370 ---------- +1371 Ntrunc: int +1372 Rank of the target matrix. +1373 tproj: int +1374 Time where the eigenvectors are evaluated, corresponds to ts in the GEVP method. +1375 The default value is 3. +1376 t0proj: int +1377 Time where the correlation matrix is inverted. Choosing t0proj=1 is strongly +1378 discouraged for O(a) improved theories, since the correctness of the procedure +1379 cannot be granted in this case. The default value is 2. +1380 basematrix : Corr +1381 Correlation matrix that is used to determine the eigenvectors of the +1382 lowest states based on a GEVP. basematrix is taken to be the Corr itself if +1383 is is not specified. +1384 +1385 Notes +1386 ----- +1387 We have the basematrix $C(t)$ and the target matrix $G(t)$. We start by solving +1388 the GEVP $$C(t) v_n(t, t_0) = \lambda_n(t, t_0) C(t_0) v_n(t, t_0)$$ where $t \equiv t_\mathrm{proj}$ +1389 and $t_0 \equiv t_{0, \mathrm{proj}}$. The target matrix is projected onto the subspace of the +1390 resulting eigenvectors $v_n, n=1,\dots,N_\mathrm{trunc}$ via +1391 $$G^\prime_{i, j}(t) = (v_i, G(t) v_j)$$. This allows to reduce the size of a large +1392 correlation matrix and to remove some noise that is added by irrelevant operators. +1393 This may allow to use the GEVP on $G(t)$ at late times such that the theoretically motivated +1394 bound $t_0 \leq t/2$ holds, since the condition number of $G(t)$ is decreased, compared to $C(t)$. +1395 ''' +1396 +1397 if self.N == 1: +1398 raise ValueError('Method cannot be applied to one-dimensional correlators.') +1399 if basematrix is None: +1400 basematrix = self +1401 if Ntrunc >= basematrix.N: +1402 raise ValueError(f'Cannot truncate using Ntrunc >= {basematrix.N}') +1403 if basematrix.N != self.N: +1404 raise ValueError('basematrix and targetmatrix have to be of the same size.') +1405 +1406 evecs = basematrix.GEVP(t0proj, tproj, sort=None)[:Ntrunc] +1407 +1408 tmpmat = np.empty((Ntrunc, Ntrunc), dtype=object) +1409 rmat = [] +1410 for t in range(basematrix.T): +1411 if self.content[t] is None: +1412 rmat.append(None) +1413 else: +1414 for i in range(Ntrunc): +1415 for j in range(Ntrunc): +1416 tmpmat[i][j] = evecs[i].T @ self[t] @ evecs[j] +1417 rmat.append(np.copy(tmpmat)) +1418 +1419 return Corr(rmat) +1420 +1421 +1422def _sort_vectors(vec_set_in, ts): +1423 """Helper function used to find a set of Eigenvectors consistent over all timeslices""" +1424 +1425 if isinstance(vec_set_in[ts][0][0], Obs): +1426 vec_set = [anp.vectorize(float)(vi) if vi is not None else vi for vi in vec_set_in] +1427 else: +1428 vec_set = vec_set_in +1429 reference_sorting = np.array(vec_set[ts]) +1430 N = reference_sorting.shape[0] +1431 sorted_vec_set = [] +1432 for t in range(len(vec_set)): +1433 if vec_set[t] is None: +1434 sorted_vec_set.append(None) +1435 elif not t == ts: +1436 perms = [list(o) for o in permutations([i for i in range(N)], N)] +1437 best_score = 0 +1438 for perm in perms: +1439 current_score = 1 +1440 for k in range(N): +1441 new_sorting = reference_sorting.copy() +1442 new_sorting[perm[k], :] = vec_set[t][k] +1443 current_score *= abs(np.linalg.det(new_sorting)) +1444 if current_score > best_score: +1445 best_score = current_score +1446 best_perm = perm +1447 sorted_vec_set.append([vec_set_in[t][k] for k in best_perm]) +1448 else: +1449 sorted_vec_set.append(vec_set_in[t]) +1450 +1451 return sorted_vec_set +1452 +1453 +1454def _check_for_none(corr, entry): +1455 """Checks if entry for correlator corr is None""" +1456 return len(list(filter(None, np.asarray(entry).flatten()))) < corr.N ** 2 1457 1458 -1459def _check_for_none(corr, entry): -1460 """Checks if entry for correlator corr is None""" -1461 return len(list(filter(None, np.asarray(entry).flatten()))) < corr.N ** 2 -1462 +1459def _GEVP_solver(Gt, G0, method='eigh', chol_inv=None): +1460 r"""Helper function for solving the GEVP and sorting the eigenvectors. +1461 +1462 Solves $G(t)v_i=\lambda_i G(t_0)v_i$ and returns the eigenvectors v_i 1463 -1464def _GEVP_solver(Gt, G0, method='eigh', chol_inv=None): -1465 r"""Helper function for solving the GEVP and sorting the eigenvectors. -1466 -1467 Solves $G(t)v_i=\lambda_i G(t_0)v_i$ and returns the eigenvectors v_i -1468 -1469 The helper function assumes that both provided matrices are symmetric and -1470 only processes the lower triangular part of both matrices. In case the matrices -1471 are not symmetric the upper triangular parts are effectively discarded. -1472 -1473 Parameters -1474 ---------- -1475 Gt : array -1476 The correlator at time t for the left hand side of the GEVP -1477 G0 : array -1478 The correlator at time t0 for the right hand side of the GEVP -1479 Method used to solve the GEVP. -1480 - "eigh": Use scipy.linalg.eigh to solve the GEVP. -1481 - "cholesky": Use manually implemented solution via the Cholesky decomposition. -1482 chol_inv : array, optional -1483 Inverse of the Cholesky decomposition of G0. May be provided to -1484 speed up the computation in the case of method=='cholesky' -1485 -1486 """ -1487 if isinstance(G0[0][0], Obs): -1488 vector_obs = True -1489 else: -1490 vector_obs = False -1491 -1492 if method == 'cholesky': -1493 if vector_obs: -1494 cholesky = linalg.cholesky -1495 inv = linalg.inv -1496 eigv = linalg.eigv -1497 matmul = linalg.matmul -1498 else: -1499 cholesky = np.linalg.cholesky -1500 inv = np.linalg.inv -1501 -1502 def eigv(x, **kwargs): -1503 return np.linalg.eigh(x)[1] -1504 -1505 def matmul(*operands): -1506 return np.linalg.multi_dot(operands) -1507 N = Gt.shape[0] -1508 output = [[] for j in range(N)] -1509 if chol_inv is None: -1510 chol = cholesky(G0) # This will automatically report if the matrix is not pos-def -1511 chol_inv = inv(chol) -1512 -1513 try: -1514 new_matrix = matmul(chol_inv, Gt, chol_inv.T) -1515 ev = eigv(new_matrix) -1516 ev = matmul(chol_inv.T, ev) -1517 output = np.flip(ev, axis=1).T -1518 except (np.linalg.LinAlgError, TypeError, ValueError): # The above code can fail because of linalg-errors or because the entry of the corr is None -1519 for s in range(N): -1520 output[s] = None -1521 return output -1522 elif method == 'eigh': -1523 return scipy.linalg.eigh(Gt, G0, lower=True)[1].T[::-1] +1464 The helper function assumes that both provided matrices are symmetric and +1465 only processes the lower triangular part of both matrices. In case the matrices +1466 are not symmetric the upper triangular parts are effectively discarded. +1467 +1468 Parameters +1469 ---------- +1470 Gt : array +1471 The correlator at time t for the left hand side of the GEVP +1472 G0 : array +1473 The correlator at time t0 for the right hand side of the GEVP +1474 Method used to solve the GEVP. +1475 - "eigh": Use scipy.linalg.eigh to solve the GEVP. +1476 - "cholesky": Use manually implemented solution via the Cholesky decomposition. +1477 chol_inv : array, optional +1478 Inverse of the Cholesky decomposition of G0. May be provided to +1479 speed up the computation in the case of method=='cholesky' +1480 +1481 """ +1482 if isinstance(G0[0][0], Obs): +1483 vector_obs = True +1484 else: +1485 vector_obs = False +1486 +1487 if method == 'cholesky': +1488 if vector_obs: +1489 cholesky = linalg.cholesky +1490 inv = linalg.inv +1491 eigv = linalg.eigv +1492 matmul = linalg.matmul +1493 else: +1494 cholesky = np.linalg.cholesky +1495 inv = np.linalg.inv +1496 +1497 def eigv(x, **kwargs): +1498 return np.linalg.eigh(x)[1] +1499 +1500 def matmul(*operands): +1501 return np.linalg.multi_dot(operands) +1502 N = Gt.shape[0] +1503 output = [[] for j in range(N)] +1504 if chol_inv is None: +1505 chol = cholesky(G0) # This will automatically report if the matrix is not pos-def +1506 chol_inv = inv(chol) +1507 +1508 try: +1509 new_matrix = matmul(chol_inv, Gt, chol_inv.T) +1510 ev = eigv(new_matrix) +1511 ev = matmul(chol_inv.T, ev) +1512 output = np.flip(ev, axis=1).T +1513 except (np.linalg.LinAlgError, TypeError, ValueError): # The above code can fail because of linalg-errors or because the entry of the corr is None +1514 for s in range(N): +1515 output[s] = None +1516 return output +1517 elif method == 'eigh': +1518 return scipy.linalg.eigh(Gt, G0, lower=True)[1].T[::-1] @@ -2492,701 +2487,696 @@ 728 if variant == 'log': 729 newcontent = [] 730 for t in range(self.T - 1): - 731 if ((self.content[t] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0): + 731 if ((self.content[t] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0) or self.content[t][0].value / self.content[t + 1][0].value < 0: 732 newcontent.append(None) - 733 elif self.content[t][0].value / self.content[t + 1][0].value < 0: - 734 newcontent.append(None) - 735 else: - 736 newcontent.append(self.content[t] / self.content[t + 1]) - 737 if (all([x is None for x in newcontent])): - 738 raise ValueError('m_eff is undefined at all timeslices') + 733 else: + 734 newcontent.append(self.content[t] / self.content[t + 1]) + 735 if (all([x is None for x in newcontent])): + 736 raise ValueError('m_eff is undefined at all timeslices') + 737 + 738 return np.log(Corr(newcontent, padding=[0, 1])) 739 - 740 return np.log(Corr(newcontent, padding=[0, 1])) - 741 - 742 elif variant == 'logsym': - 743 newcontent = [] - 744 for t in range(1, self.T - 1): - 745 if ((self.content[t - 1] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0): - 746 newcontent.append(None) - 747 elif self.content[t - 1][0].value / self.content[t + 1][0].value < 0: - 748 newcontent.append(None) - 749 else: - 750 newcontent.append(self.content[t - 1] / self.content[t + 1]) - 751 if (all([x is None for x in newcontent])): - 752 raise ValueError('m_eff is undefined at all timeslices') - 753 - 754 return np.log(Corr(newcontent, padding=[1, 1])) / 2 - 755 - 756 elif variant in ['periodic', 'cosh', 'sinh']: - 757 if variant in ['periodic', 'cosh']: - 758 func = anp.cosh - 759 else: - 760 func = anp.sinh - 761 - 762 def root_function(x, d): - 763 return func(x * (t - self.T / 2)) / func(x * (t + 1 - self.T / 2)) - d - 764 - 765 newcontent = [] - 766 for t in range(self.T - 1): - 767 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t + 1][0].value == 0): - 768 newcontent.append(None) - 769 # Fill the two timeslices in the middle of the lattice with their predecessors - 770 elif variant == 'sinh' and t in [self.T / 2, self.T / 2 - 1]: - 771 newcontent.append(newcontent[-1]) - 772 elif self.content[t][0].value / self.content[t + 1][0].value < 0: - 773 newcontent.append(None) - 774 else: - 775 newcontent.append(np.abs(find_root(self.content[t][0] / self.content[t + 1][0], root_function, guess=guess))) - 776 if (all([x is None for x in newcontent])): - 777 raise ValueError('m_eff is undefined at all timeslices') - 778 - 779 return Corr(newcontent, padding=[0, 1]) - 780 - 781 elif variant == 'arccosh': - 782 newcontent = [] - 783 for t in range(1, self.T - 1): - 784 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t - 1] is None) or (self.content[t][0].value == 0): - 785 newcontent.append(None) - 786 else: - 787 newcontent.append((self.content[t + 1] + self.content[t - 1]) / (2 * self.content[t])) - 788 if (all([x is None for x in newcontent])): - 789 raise ValueError("m_eff is undefined at all timeslices") - 790 return np.arccosh(Corr(newcontent, padding=[1, 1])) - 791 - 792 else: - 793 raise ValueError('Unknown variant.') - 794 - 795 def fit(self, function, fitrange=None, silent=False, **kwargs): - 796 r'''Fits function to the data - 797 - 798 Parameters - 799 ---------- - 800 function : obj - 801 function to fit to the data. See fits.least_squares for details. - 802 fitrange : list - 803 Two element list containing the timeslices on which the fit is supposed to start and stop. - 804 Caution: This range is inclusive as opposed to standard python indexing. - 805 `fitrange=[4, 6]` corresponds to the three entries 4, 5 and 6. - 806 If not specified, self.prange or all timeslices are used. - 807 silent : bool - 808 Decides whether output is printed to the standard output. - 809 ''' - 810 if self.N != 1: - 811 raise ValueError("Correlator must be projected before fitting") - 812 - 813 if fitrange is None: - 814 if self.prange: - 815 fitrange = self.prange - 816 else: - 817 fitrange = [0, self.T - 1] - 818 else: - 819 if not isinstance(fitrange, list): - 820 raise TypeError("fitrange has to be a list with two elements") - 821 if len(fitrange) != 2: - 822 raise ValueError("fitrange has to have exactly two elements [fit_start, fit_stop]") - 823 - 824 xs = np.array([x for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) - 825 ys = np.array([self.content[x][0] for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) - 826 result = least_squares(xs, ys, function, silent=silent, **kwargs) - 827 return result - 828 - 829 def plateau(self, plateau_range=None, method="fit", auto_gamma=False): - 830 """ Extract a plateau value from a Corr object - 831 - 832 Parameters - 833 ---------- - 834 plateau_range : list - 835 list with two entries, indicating the first and the last timeslice - 836 of the plateau region. - 837 method : str - 838 method to extract the plateau. - 839 'fit' fits a constant to the plateau region - 840 'avg', 'average' or 'mean' just average over the given timeslices. - 841 auto_gamma : bool - 842 apply gamma_method with default parameters to the Corr. Defaults to None - 843 """ - 844 if not plateau_range: - 845 if self.prange: - 846 plateau_range = self.prange - 847 else: - 848 raise ValueError("no plateau range provided") - 849 if self.N != 1: - 850 raise ValueError("Correlator must be projected before getting a plateau.") - 851 if (all([self.content[t] is None for t in range(plateau_range[0], plateau_range[1] + 1)])): - 852 raise ValueError("plateau is undefined at all timeslices in plateaurange.") - 853 if auto_gamma: - 854 self.gamma_method() - 855 if method == "fit": - 856 def const_func(a, t): - 857 return a[0] - 858 return self.fit(const_func, plateau_range)[0] - 859 elif method in ["avg", "average", "mean"]: - 860 returnvalue = np.mean([item[0] for item in self.content[plateau_range[0]:plateau_range[1] + 1] if item is not None]) - 861 return returnvalue - 862 - 863 else: - 864 raise ValueError("Unsupported plateau method: " + method) - 865 - 866 def set_prange(self, prange): - 867 """Sets the attribute prange of the Corr object.""" - 868 if not len(prange) == 2: - 869 raise ValueError("prange must be a list or array with two values") - 870 if not ((isinstance(prange[0], int)) and (isinstance(prange[1], int))): - 871 raise TypeError("Start and end point must be integers") - 872 if not (0 <= prange[0] <= self.T and 0 <= prange[1] <= self.T and prange[0] <= prange[1]): - 873 raise ValueError("Start and end point must define a range in the interval 0,T") - 874 - 875 self.prange = prange - 876 return - 877 - 878 def show(self, x_range=None, comp=None, y_range=None, logscale=False, plateau=None, fit_res=None, fit_key=None, ylabel=None, save=None, auto_gamma=False, hide_sigma=None, references=None, title=None): - 879 """Plots the correlator using the tag of the correlator as label if available. - 880 - 881 Parameters - 882 ---------- - 883 x_range : list - 884 list of two values, determining the range of the x-axis e.g. [4, 8]. - 885 comp : Corr or list of Corr - 886 Correlator or list of correlators which are plotted for comparison. - 887 The tags of these correlators are used as labels if available. - 888 logscale : bool - 889 Sets y-axis to logscale. - 890 plateau : Obs - 891 Plateau value to be visualized in the figure. - 892 fit_res : Fit_result - 893 Fit_result object to be visualized. - 894 fit_key : str - 895 Key for the fit function in Fit_result.fit_function (for combined fits). - 896 ylabel : str - 897 Label for the y-axis. - 898 save : str - 899 path to file in which the figure should be saved. - 900 auto_gamma : bool - 901 Apply the gamma method with standard parameters to all correlators and plateau values before plotting. - 902 hide_sigma : float - 903 Hides data points from the first value on which is consistent with zero within 'hide_sigma' standard errors. - 904 references : list - 905 List of floating point values that are displayed as horizontal lines for reference. - 906 title : string - 907 Optional title of the figure. - 908 """ - 909 if self.N != 1: - 910 raise ValueError("Correlator must be projected before plotting") - 911 - 912 if auto_gamma: - 913 self.gamma_method() - 914 - 915 if x_range is None: - 916 x_range = [0, self.T - 1] - 917 - 918 fig = plt.figure() - 919 ax1 = fig.add_subplot(111) - 920 - 921 x, y, y_err = self.plottable() - 922 if hide_sigma: - 923 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 + 740 elif variant == 'logsym': + 741 newcontent = [] + 742 for t in range(1, self.T - 1): + 743 if ((self.content[t - 1] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0) or self.content[t - 1][0].value / self.content[t + 1][0].value < 0: + 744 newcontent.append(None) + 745 else: + 746 newcontent.append(self.content[t - 1] / self.content[t + 1]) + 747 if (all([x is None for x in newcontent])): + 748 raise ValueError('m_eff is undefined at all timeslices') + 749 + 750 return np.log(Corr(newcontent, padding=[1, 1])) / 2 + 751 + 752 elif variant in ['periodic', 'cosh', 'sinh']: + 753 if variant in ['periodic', 'cosh']: + 754 func = anp.cosh + 755 else: + 756 func = anp.sinh + 757 + 758 def root_function(x, d): + 759 return func(x * (t - self.T / 2)) / func(x * (t + 1 - self.T / 2)) - d + 760 + 761 newcontent = [] + 762 for t in range(self.T - 1): + 763 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t + 1][0].value == 0): + 764 newcontent.append(None) + 765 # Fill the two timeslices in the middle of the lattice with their predecessors + 766 elif variant == 'sinh' and t in [self.T / 2, self.T / 2 - 1]: + 767 newcontent.append(newcontent[-1]) + 768 elif self.content[t][0].value / self.content[t + 1][0].value < 0: + 769 newcontent.append(None) + 770 else: + 771 newcontent.append(np.abs(find_root(self.content[t][0] / self.content[t + 1][0], root_function, guess=guess))) + 772 if (all([x is None for x in newcontent])): + 773 raise ValueError('m_eff is undefined at all timeslices') + 774 + 775 return Corr(newcontent, padding=[0, 1]) + 776 + 777 elif variant == 'arccosh': + 778 newcontent = [] + 779 for t in range(1, self.T - 1): + 780 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t - 1] is None) or (self.content[t][0].value == 0): + 781 newcontent.append(None) + 782 else: + 783 newcontent.append((self.content[t + 1] + self.content[t - 1]) / (2 * self.content[t])) + 784 if (all([x is None for x in newcontent])): + 785 raise ValueError("m_eff is undefined at all timeslices") + 786 return np.arccosh(Corr(newcontent, padding=[1, 1])) + 787 + 788 else: + 789 raise ValueError('Unknown variant.') + 790 + 791 def fit(self, function, fitrange=None, silent=False, **kwargs): + 792 r'''Fits function to the data + 793 + 794 Parameters + 795 ---------- + 796 function : obj + 797 function to fit to the data. See fits.least_squares for details. + 798 fitrange : list + 799 Two element list containing the timeslices on which the fit is supposed to start and stop. + 800 Caution: This range is inclusive as opposed to standard python indexing. + 801 `fitrange=[4, 6]` corresponds to the three entries 4, 5 and 6. + 802 If not specified, self.prange or all timeslices are used. + 803 silent : bool + 804 Decides whether output is printed to the standard output. + 805 ''' + 806 if self.N != 1: + 807 raise ValueError("Correlator must be projected before fitting") + 808 + 809 if fitrange is None: + 810 if self.prange: + 811 fitrange = self.prange + 812 else: + 813 fitrange = [0, self.T - 1] + 814 else: + 815 if not isinstance(fitrange, list): + 816 raise TypeError("fitrange has to be a list with two elements") + 817 if len(fitrange) != 2: + 818 raise ValueError("fitrange has to have exactly two elements [fit_start, fit_stop]") + 819 + 820 xs = np.array([x for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) + 821 ys = np.array([self.content[x][0] for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) + 822 result = least_squares(xs, ys, function, silent=silent, **kwargs) + 823 return result + 824 + 825 def plateau(self, plateau_range=None, method="fit", auto_gamma=False): + 826 """ Extract a plateau value from a Corr object + 827 + 828 Parameters + 829 ---------- + 830 plateau_range : list + 831 list with two entries, indicating the first and the last timeslice + 832 of the plateau region. + 833 method : str + 834 method to extract the plateau. + 835 'fit' fits a constant to the plateau region + 836 'avg', 'average' or 'mean' just average over the given timeslices. + 837 auto_gamma : bool + 838 apply gamma_method with default parameters to the Corr. Defaults to None + 839 """ + 840 if not plateau_range: + 841 if self.prange: + 842 plateau_range = self.prange + 843 else: + 844 raise ValueError("no plateau range provided") + 845 if self.N != 1: + 846 raise ValueError("Correlator must be projected before getting a plateau.") + 847 if (all([self.content[t] is None for t in range(plateau_range[0], plateau_range[1] + 1)])): + 848 raise ValueError("plateau is undefined at all timeslices in plateaurange.") + 849 if auto_gamma: + 850 self.gamma_method() + 851 if method == "fit": + 852 def const_func(a, t): + 853 return a[0] + 854 return self.fit(const_func, plateau_range)[0] + 855 elif method in ["avg", "average", "mean"]: + 856 returnvalue = np.mean([item[0] for item in self.content[plateau_range[0]:plateau_range[1] + 1] if item is not None]) + 857 return returnvalue + 858 + 859 else: + 860 raise ValueError("Unsupported plateau method: " + method) + 861 + 862 def set_prange(self, prange): + 863 """Sets the attribute prange of the Corr object.""" + 864 if not len(prange) == 2: + 865 raise ValueError("prange must be a list or array with two values") + 866 if not ((isinstance(prange[0], int)) and (isinstance(prange[1], int))): + 867 raise TypeError("Start and end point must be integers") + 868 if not (0 <= prange[0] <= self.T and 0 <= prange[1] <= self.T and prange[0] <= prange[1]): + 869 raise ValueError("Start and end point must define a range in the interval 0,T") + 870 + 871 self.prange = prange + 872 + 873 def show(self, x_range=None, comp=None, y_range=None, logscale=False, plateau=None, fit_res=None, fit_key=None, ylabel=None, save=None, auto_gamma=False, hide_sigma=None, references=None, title=None): + 874 """Plots the correlator using the tag of the correlator as label if available. + 875 + 876 Parameters + 877 ---------- + 878 x_range : list + 879 list of two values, determining the range of the x-axis e.g. [4, 8]. + 880 comp : Corr or list of Corr + 881 Correlator or list of correlators which are plotted for comparison. + 882 The tags of these correlators are used as labels if available. + 883 logscale : bool + 884 Sets y-axis to logscale. + 885 plateau : Obs + 886 Plateau value to be visualized in the figure. + 887 fit_res : Fit_result + 888 Fit_result object to be visualized. + 889 fit_key : str + 890 Key for the fit function in Fit_result.fit_function (for combined fits). + 891 ylabel : str + 892 Label for the y-axis. + 893 save : str + 894 path to file in which the figure should be saved. + 895 auto_gamma : bool + 896 Apply the gamma method with standard parameters to all correlators and plateau values before plotting. + 897 hide_sigma : float + 898 Hides data points from the first value on which is consistent with zero within 'hide_sigma' standard errors. + 899 references : list + 900 List of floating point values that are displayed as horizontal lines for reference. + 901 title : string + 902 Optional title of the figure. + 903 """ + 904 if self.N != 1: + 905 raise ValueError("Correlator must be projected before plotting") + 906 + 907 if auto_gamma: + 908 self.gamma_method() + 909 + 910 if x_range is None: + 911 x_range = [0, self.T - 1] + 912 + 913 fig = plt.figure() + 914 ax1 = fig.add_subplot(111) + 915 + 916 x, y, y_err = self.plottable() + 917 if hide_sigma: + 918 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 + 919 else: + 920 hide_from = None + 921 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=self.tag) + 922 if logscale: + 923 ax1.set_yscale('log') 924 else: - 925 hide_from = None - 926 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=self.tag) - 927 if logscale: - 928 ax1.set_yscale('log') - 929 else: - 930 if y_range is None: - 931 try: - 932 y_min = min([(x[0].value - x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) - 933 y_max = max([(x[0].value + x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) - 934 ax1.set_ylim([y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min)]) - 935 except Exception: - 936 pass - 937 else: - 938 ax1.set_ylim(y_range) - 939 if comp: - 940 if isinstance(comp, (Corr, list)): - 941 for corr in comp if isinstance(comp, list) else [comp]: - 942 if auto_gamma: - 943 corr.gamma_method() - 944 x, y, y_err = corr.plottable() - 945 if hide_sigma: - 946 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 - 947 else: - 948 hide_from = None - 949 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=corr.tag, mfc=plt.rcParams['axes.facecolor']) - 950 else: - 951 raise TypeError("'comp' must be a correlator or a list of correlators.") - 952 - 953 if plateau: - 954 if isinstance(plateau, Obs): - 955 if auto_gamma: - 956 plateau.gamma_method() - 957 ax1.axhline(y=plateau.value, linewidth=2, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--', label=str(plateau)) - 958 ax1.axhspan(plateau.value - plateau.dvalue, plateau.value + plateau.dvalue, alpha=0.25, color=plt.rcParams['text.color'], ls='-') - 959 else: - 960 raise TypeError("'plateau' must be an Obs") - 961 - 962 if references: - 963 if isinstance(references, list): - 964 for ref in references: - 965 ax1.axhline(y=ref, linewidth=1, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--') - 966 else: - 967 raise TypeError("'references' must be a list of floating pint values.") - 968 - 969 if self.prange: - 970 ax1.axvline(self.prange[0], 0, 1, ls='-', marker=',', color="black", zorder=0) - 971 ax1.axvline(self.prange[1], 0, 1, ls='-', marker=',', color="black", zorder=0) - 972 - 973 if fit_res: - 974 x_samples = np.arange(x_range[0], x_range[1] + 1, 0.05) - 975 if isinstance(fit_res.fit_function, dict): - 976 if fit_key: - 977 ax1.plot(x_samples, fit_res.fit_function[fit_key]([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) - 978 else: - 979 raise ValueError("Please provide a 'fit_key' for visualizing combined fits.") - 980 else: - 981 ax1.plot(x_samples, fit_res.fit_function([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) + 925 if y_range is None: + 926 try: + 927 y_min = min([(x[0].value - x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) + 928 y_max = max([(x[0].value + x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) + 929 ax1.set_ylim([y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min)]) + 930 except Exception: + 931 pass + 932 else: + 933 ax1.set_ylim(y_range) + 934 if comp: + 935 if isinstance(comp, (Corr, list)): + 936 for corr in comp if isinstance(comp, list) else [comp]: + 937 if auto_gamma: + 938 corr.gamma_method() + 939 x, y, y_err = corr.plottable() + 940 if hide_sigma: + 941 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 + 942 else: + 943 hide_from = None + 944 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=corr.tag, mfc=plt.rcParams['axes.facecolor']) + 945 else: + 946 raise TypeError("'comp' must be a correlator or a list of correlators.") + 947 + 948 if plateau: + 949 if isinstance(plateau, Obs): + 950 if auto_gamma: + 951 plateau.gamma_method() + 952 ax1.axhline(y=plateau.value, linewidth=2, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--', label=str(plateau)) + 953 ax1.axhspan(plateau.value - plateau.dvalue, plateau.value + plateau.dvalue, alpha=0.25, color=plt.rcParams['text.color'], ls='-') + 954 else: + 955 raise TypeError("'plateau' must be an Obs") + 956 + 957 if references: + 958 if isinstance(references, list): + 959 for ref in references: + 960 ax1.axhline(y=ref, linewidth=1, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--') + 961 else: + 962 raise TypeError("'references' must be a list of floating pint values.") + 963 + 964 if self.prange: + 965 ax1.axvline(self.prange[0], 0, 1, ls='-', marker=',', color="black", zorder=0) + 966 ax1.axvline(self.prange[1], 0, 1, ls='-', marker=',', color="black", zorder=0) + 967 + 968 if fit_res: + 969 x_samples = np.arange(x_range[0], x_range[1] + 1, 0.05) + 970 if isinstance(fit_res.fit_function, dict): + 971 if fit_key: + 972 ax1.plot(x_samples, fit_res.fit_function[fit_key]([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) + 973 else: + 974 raise ValueError("Please provide a 'fit_key' for visualizing combined fits.") + 975 else: + 976 ax1.plot(x_samples, fit_res.fit_function([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) + 977 + 978 ax1.set_xlabel(r'$x_0 / a$') + 979 if ylabel: + 980 ax1.set_ylabel(ylabel) + 981 ax1.set_xlim([x_range[0] - 0.5, x_range[1] + 0.5]) 982 - 983 ax1.set_xlabel(r'$x_0 / a$') - 984 if ylabel: - 985 ax1.set_ylabel(ylabel) - 986 ax1.set_xlim([x_range[0] - 0.5, x_range[1] + 0.5]) - 987 - 988 _handles, labels = ax1.get_legend_handles_labels() - 989 if labels: - 990 ax1.legend() + 983 _handles, labels = ax1.get_legend_handles_labels() + 984 if labels: + 985 ax1.legend() + 986 + 987 if title: + 988 plt.title(title) + 989 + 990 plt.draw() 991 - 992 if title: - 993 plt.title(title) - 994 - 995 plt.draw() - 996 - 997 if save: - 998 if isinstance(save, str): - 999 fig.savefig(save, bbox_inches='tight') -1000 else: -1001 raise TypeError("'save' has to be a string.") -1002 -1003 def spaghetti_plot(self, logscale=True): -1004 """Produces a spaghetti plot of the correlator suited to monitor exceptional configurations. -1005 -1006 Parameters -1007 ---------- -1008 logscale : bool -1009 Determines whether the scale of the y-axis is logarithmic or standard. -1010 """ -1011 if self.N != 1: -1012 raise ValueError("Correlator needs to be projected first.") -1013 -1014 mc_names = list(set([item for sublist in [list(itertools.chain.from_iterable(map(o[0].e_content.get, o[0].mc_names))) for o in self.content if o is not None] for item in sublist])) -1015 x0_vals = [n for (n, o) in zip(np.arange(self.T), self.content, strict=True) if o is not None] -1016 -1017 for name in mc_names: -1018 data = np.array([o[0].deltas[name] + o[0].r_values[name] for o in self.content if o is not None]).T + 992 if save: + 993 if isinstance(save, str): + 994 fig.savefig(save, bbox_inches='tight') + 995 else: + 996 raise TypeError("'save' has to be a string.") + 997 + 998 def spaghetti_plot(self, logscale=True): + 999 """Produces a spaghetti plot of the correlator suited to monitor exceptional configurations. +1000 +1001 Parameters +1002 ---------- +1003 logscale : bool +1004 Determines whether the scale of the y-axis is logarithmic or standard. +1005 """ +1006 if self.N != 1: +1007 raise ValueError("Correlator needs to be projected first.") +1008 +1009 mc_names = list(set([item for sublist in [list(itertools.chain.from_iterable(map(o[0].e_content.get, o[0].mc_names))) for o in self.content if o is not None] for item in sublist])) +1010 x0_vals = [n for (n, o) in zip(np.arange(self.T), self.content, strict=True) if o is not None] +1011 +1012 for name in mc_names: +1013 data = np.array([o[0].deltas[name] + o[0].r_values[name] for o in self.content if o is not None]).T +1014 +1015 fig = plt.figure() +1016 ax = fig.add_subplot(111) +1017 for dat in data: +1018 ax.plot(x0_vals, dat, ls='-', marker='') 1019 -1020 fig = plt.figure() -1021 ax = fig.add_subplot(111) -1022 for dat in data: -1023 ax.plot(x0_vals, dat, ls='-', marker='') -1024 -1025 if logscale is True: -1026 ax.set_yscale('log') -1027 -1028 ax.set_xlabel(r'$x_0 / a$') -1029 plt.title(name) -1030 plt.draw() -1031 -1032 def dump(self, filename, datatype="json.gz", **kwargs): -1033 """Dumps the Corr into a file of chosen type -1034 Parameters -1035 ---------- -1036 filename : str -1037 Name of the file to be saved. -1038 datatype : str -1039 Format of the exported file. Supported formats include -1040 "json.gz" and "pickle" -1041 path : str -1042 specifies a custom path for the file (default '.') -1043 """ -1044 if datatype == "json.gz": -1045 from .input.json import dump_to_json -1046 if 'path' in kwargs: -1047 file_name = kwargs.get('path') + '/' + filename -1048 else: -1049 file_name = filename -1050 dump_to_json(self, file_name) -1051 elif datatype == "pickle": -1052 dump_object(self, filename, **kwargs) -1053 else: -1054 raise ValueError("Unknown datatype " + str(datatype)) -1055 -1056 def print(self, print_range=None): -1057 print(self.__repr__(print_range)) -1058 -1059 def __repr__(self, print_range=None): -1060 if print_range is None: -1061 print_range = [0, None] -1062 -1063 content_string = "" -1064 content_string += "Corr T=" + str(self.T) + " N=" + str(self.N) + "\n" # +" filled with"+ str(type(self.content[0][0])) there should be a good solution here +1020 if logscale is True: +1021 ax.set_yscale('log') +1022 +1023 ax.set_xlabel(r'$x_0 / a$') +1024 plt.title(name) +1025 plt.draw() +1026 +1027 def dump(self, filename, datatype="json.gz", **kwargs): +1028 """Dumps the Corr into a file of chosen type +1029 Parameters +1030 ---------- +1031 filename : str +1032 Name of the file to be saved. +1033 datatype : str +1034 Format of the exported file. Supported formats include +1035 "json.gz" and "pickle" +1036 path : str +1037 specifies a custom path for the file (default '.') +1038 """ +1039 if datatype == "json.gz": +1040 from .input.json import dump_to_json +1041 if 'path' in kwargs: +1042 file_name = kwargs.get('path') + '/' + filename +1043 else: +1044 file_name = filename +1045 dump_to_json(self, file_name) +1046 elif datatype == "pickle": +1047 dump_object(self, filename, **kwargs) +1048 else: +1049 raise ValueError("Unknown datatype " + str(datatype)) +1050 +1051 def print(self, print_range=None): +1052 print(self.__repr__(print_range)) +1053 +1054 def __repr__(self, print_range=None): +1055 if print_range is None: +1056 print_range = [0, None] +1057 +1058 content_string = "" +1059 content_string += "Corr T=" + str(self.T) + " N=" + str(self.N) + "\n" # +" filled with"+ str(type(self.content[0][0])) there should be a good solution here +1060 +1061 if self.tag is not None: +1062 content_string += "Description: " + self.tag + "\n" +1063 if self.N != 1: +1064 return content_string 1065 -1066 if self.tag is not None: -1067 content_string += "Description: " + self.tag + "\n" -1068 if self.N != 1: -1069 return content_string -1070 -1071 if print_range[1]: -1072 print_range[1] += 1 -1073 content_string += 'x0/a\tCorr(x0/a)\n------------------\n' -1074 for i, sub_corr in enumerate(self.content[print_range[0]:print_range[1]]): -1075 if sub_corr is None: -1076 content_string += str(i + print_range[0]) + '\n' -1077 else: -1078 content_string += str(i + print_range[0]) -1079 for element in sub_corr: -1080 content_string += f"\t{element:+2}" -1081 content_string += '\n' -1082 return content_string -1083 -1084 def __str__(self): -1085 return self.__repr__() +1066 if print_range[1]: +1067 print_range[1] += 1 +1068 content_string += 'x0/a\tCorr(x0/a)\n------------------\n' +1069 for i, sub_corr in enumerate(self.content[print_range[0]:print_range[1]]): +1070 if sub_corr is None: +1071 content_string += str(i + print_range[0]) + '\n' +1072 else: +1073 content_string += str(i + print_range[0]) +1074 for element in sub_corr: +1075 content_string += f"\t{element:+2}" +1076 content_string += '\n' +1077 return content_string +1078 +1079 def __str__(self): +1080 return self.__repr__() +1081 +1082 # We define the basic operations, that can be performed with correlators. +1083 # While */+- get defined here, they only work for Corr*Obs and not Obs*Corr. +1084 # This is because Obs*Corr checks Obs.__mul__ first and does not catch an exception. +1085 # One could try and tell Obs to check if the y in __mul__ is a Corr and 1086 -1087 # We define the basic operations, that can be performed with correlators. -1088 # While */+- get defined here, they only work for Corr*Obs and not Obs*Corr. -1089 # This is because Obs*Corr checks Obs.__mul__ first and does not catch an exception. -1090 # One could try and tell Obs to check if the y in __mul__ is a Corr and -1091 -1092 __array_priority__ = 10000 -1093 -1094 def __eq__(self, y): -1095 if isinstance(y, Corr): -1096 comp = np.asarray(y.content, dtype=object) -1097 else: -1098 comp = np.asarray(y) -1099 return np.asarray(self.content, dtype=object) == comp -1100 -1101 __hash__ = None -1102 -1103 def __add__(self, y): -1104 if isinstance(y, Corr): -1105 if ((self.N != y.N) or (self.T != y.T)): -1106 raise ValueError("Addition of Corrs with different shape") -1107 newcontent = [] -1108 for t in range(self.T): -1109 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1110 newcontent.append(None) -1111 else: -1112 newcontent.append(self.content[t] + y.content[t]) -1113 return Corr(newcontent) -1114 -1115 elif isinstance(y, (Obs, int, float, CObs, complex)): -1116 newcontent = [] -1117 for t in range(self.T): -1118 if _check_for_none(self, self.content[t]): -1119 newcontent.append(None) -1120 else: -1121 newcontent.append(self.content[t] + y) -1122 return Corr(newcontent, prange=self.prange) -1123 elif isinstance(y, np.ndarray): -1124 if y.shape == (self.T,): -1125 return Corr(list((np.array(self.content).T + y).T)) -1126 else: -1127 raise ValueError("operands could not be broadcast together") -1128 else: -1129 raise TypeError("Corr + wrong type") -1130 -1131 def __mul__(self, y): -1132 if isinstance(y, Corr): -1133 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): -1134 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") -1135 newcontent = [] -1136 for t in range(self.T): -1137 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1138 newcontent.append(None) -1139 else: -1140 newcontent.append(self.content[t] * y.content[t]) -1141 return Corr(newcontent) -1142 -1143 elif isinstance(y, (Obs, int, float, CObs, complex)): -1144 newcontent = [] -1145 for t in range(self.T): -1146 if _check_for_none(self, self.content[t]): -1147 newcontent.append(None) -1148 else: -1149 newcontent.append(self.content[t] * y) -1150 return Corr(newcontent, prange=self.prange) -1151 elif isinstance(y, np.ndarray): -1152 if y.shape == (self.T,): -1153 return Corr(list((np.array(self.content).T * y).T)) -1154 else: -1155 raise ValueError("operands could not be broadcast together") -1156 else: -1157 raise TypeError("Corr * wrong type") -1158 -1159 def __matmul__(self, y): -1160 if isinstance(y, np.ndarray): -1161 if y.ndim != 2 or y.shape[0] != y.shape[1]: -1162 raise ValueError("Can only multiply correlators by square matrices.") -1163 if not self.N == y.shape[0]: -1164 raise ValueError("matmul: mismatch of matrix dimensions") -1165 newcontent = [] -1166 for t in range(self.T): -1167 if _check_for_none(self, self.content[t]): -1168 newcontent.append(None) -1169 else: -1170 newcontent.append(self.content[t] @ y) -1171 return Corr(newcontent) -1172 elif isinstance(y, Corr): -1173 if not self.N == y.N: -1174 raise ValueError("matmul: mismatch of matrix dimensions") -1175 newcontent = [] -1176 for t in range(self.T): -1177 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1178 newcontent.append(None) -1179 else: -1180 newcontent.append(self.content[t] @ y.content[t]) -1181 return Corr(newcontent) -1182 -1183 else: -1184 return NotImplemented -1185 -1186 def __rmatmul__(self, y): -1187 if isinstance(y, np.ndarray): -1188 if y.ndim != 2 or y.shape[0] != y.shape[1]: -1189 raise ValueError("Can only multiply correlators by square matrices.") -1190 if not self.N == y.shape[0]: -1191 raise ValueError("matmul: mismatch of matrix dimensions") -1192 newcontent = [] -1193 for t in range(self.T): -1194 if _check_for_none(self, self.content[t]): -1195 newcontent.append(None) -1196 else: -1197 newcontent.append(y @ self.content[t]) -1198 return Corr(newcontent) -1199 else: -1200 return NotImplemented -1201 -1202 def __truediv__(self, y): -1203 if isinstance(y, Corr): -1204 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): -1205 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") -1206 newcontent = [] +1087 __array_priority__ = 10000 +1088 +1089 def __eq__(self, y): +1090 if isinstance(y, Corr): +1091 comp = np.asarray(y.content, dtype=object) +1092 else: +1093 comp = np.asarray(y) +1094 return np.asarray(self.content, dtype=object) == comp +1095 +1096 __hash__ = None +1097 +1098 def __add__(self, y): +1099 if isinstance(y, Corr): +1100 if ((self.N != y.N) or (self.T != y.T)): +1101 raise ValueError("Addition of Corrs with different shape") +1102 newcontent = [] +1103 for t in range(self.T): +1104 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1105 newcontent.append(None) +1106 else: +1107 newcontent.append(self.content[t] + y.content[t]) +1108 return Corr(newcontent) +1109 +1110 elif isinstance(y, (Obs, int, float, CObs, complex)): +1111 newcontent = [] +1112 for t in range(self.T): +1113 if _check_for_none(self, self.content[t]): +1114 newcontent.append(None) +1115 else: +1116 newcontent.append(self.content[t] + y) +1117 return Corr(newcontent, prange=self.prange) +1118 elif isinstance(y, np.ndarray): +1119 if y.shape == (self.T,): +1120 return Corr(list((np.array(self.content).T + y).T)) +1121 else: +1122 raise ValueError("operands could not be broadcast together") +1123 else: +1124 raise TypeError("Corr + wrong type") +1125 +1126 def __mul__(self, y): +1127 if isinstance(y, Corr): +1128 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): +1129 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") +1130 newcontent = [] +1131 for t in range(self.T): +1132 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1133 newcontent.append(None) +1134 else: +1135 newcontent.append(self.content[t] * y.content[t]) +1136 return Corr(newcontent) +1137 +1138 elif isinstance(y, (Obs, int, float, CObs, complex)): +1139 newcontent = [] +1140 for t in range(self.T): +1141 if _check_for_none(self, self.content[t]): +1142 newcontent.append(None) +1143 else: +1144 newcontent.append(self.content[t] * y) +1145 return Corr(newcontent, prange=self.prange) +1146 elif isinstance(y, np.ndarray): +1147 if y.shape == (self.T,): +1148 return Corr(list((np.array(self.content).T * y).T)) +1149 else: +1150 raise ValueError("operands could not be broadcast together") +1151 else: +1152 raise TypeError("Corr * wrong type") +1153 +1154 def __matmul__(self, y): +1155 if isinstance(y, np.ndarray): +1156 if y.ndim != 2 or y.shape[0] != y.shape[1]: +1157 raise ValueError("Can only multiply correlators by square matrices.") +1158 if not self.N == y.shape[0]: +1159 raise ValueError("matmul: mismatch of matrix dimensions") +1160 newcontent = [] +1161 for t in range(self.T): +1162 if _check_for_none(self, self.content[t]): +1163 newcontent.append(None) +1164 else: +1165 newcontent.append(self.content[t] @ y) +1166 return Corr(newcontent) +1167 elif isinstance(y, Corr): +1168 if not self.N == y.N: +1169 raise ValueError("matmul: mismatch of matrix dimensions") +1170 newcontent = [] +1171 for t in range(self.T): +1172 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1173 newcontent.append(None) +1174 else: +1175 newcontent.append(self.content[t] @ y.content[t]) +1176 return Corr(newcontent) +1177 +1178 else: +1179 return NotImplemented +1180 +1181 def __rmatmul__(self, y): +1182 if isinstance(y, np.ndarray): +1183 if y.ndim != 2 or y.shape[0] != y.shape[1]: +1184 raise ValueError("Can only multiply correlators by square matrices.") +1185 if not self.N == y.shape[0]: +1186 raise ValueError("matmul: mismatch of matrix dimensions") +1187 newcontent = [] +1188 for t in range(self.T): +1189 if _check_for_none(self, self.content[t]): +1190 newcontent.append(None) +1191 else: +1192 newcontent.append(y @ self.content[t]) +1193 return Corr(newcontent) +1194 else: +1195 return NotImplemented +1196 +1197 def __truediv__(self, y): +1198 if isinstance(y, Corr): +1199 if not ((self.N == 1 or y.N == 1 or self.N == y.N) and self.T == y.T): +1200 raise ValueError("Multiplication of Corr object requires N=N or N=1 and T=T") +1201 newcontent = [] +1202 for t in range(self.T): +1203 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): +1204 newcontent.append(None) +1205 else: +1206 newcontent.append(self.content[t] / y.content[t]) 1207 for t in range(self.T): -1208 if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]): -1209 newcontent.append(None) -1210 else: -1211 newcontent.append(self.content[t] / y.content[t]) -1212 for t in range(self.T): -1213 if _check_for_none(self, newcontent[t]): -1214 continue -1215 if np.isnan(np.sum(newcontent[t]).value): -1216 newcontent[t] = None -1217 -1218 if all([item is None for item in newcontent]): -1219 raise ValueError("Division returns completely undefined correlator") -1220 return Corr(newcontent) -1221 -1222 elif isinstance(y, (Obs, CObs)): -1223 if isinstance(y, Obs): -1224 if y.value == 0: -1225 raise ValueError('Division by zero will return undefined correlator') -1226 if isinstance(y, CObs): -1227 if y.is_zero(): -1228 raise ValueError('Division by zero will return undefined correlator') -1229 -1230 newcontent = [] -1231 for t in range(self.T): -1232 if _check_for_none(self, self.content[t]): -1233 newcontent.append(None) -1234 else: -1235 newcontent.append(self.content[t] / y) -1236 return Corr(newcontent, prange=self.prange) -1237 -1238 elif isinstance(y, (int, float)): -1239 if y == 0: -1240 raise ValueError('Division by zero will return undefined correlator') -1241 newcontent = [] -1242 for t in range(self.T): -1243 if _check_for_none(self, self.content[t]): -1244 newcontent.append(None) -1245 else: -1246 newcontent.append(self.content[t] / y) -1247 return Corr(newcontent, prange=self.prange) -1248 elif isinstance(y, np.ndarray): -1249 if y.shape == (self.T,): -1250 return Corr(list((np.array(self.content).T / y).T)) -1251 else: -1252 raise ValueError("operands could not be broadcast together") -1253 else: -1254 raise TypeError('Corr / wrong type') -1255 -1256 def __neg__(self): -1257 newcontent = [None if _check_for_none(self, item) else -1. * item for item in self.content] -1258 return Corr(newcontent, prange=self.prange) -1259 -1260 def __sub__(self, y): -1261 return self + (-y) -1262 -1263 def __pow__(self, y): -1264 if isinstance(y, (Obs, int, float, CObs)): -1265 newcontent = [None if _check_for_none(self, item) else item**y for item in self.content] -1266 return Corr(newcontent, prange=self.prange) -1267 else: -1268 raise TypeError('Type of exponent not supported') -1269 -1270 def __abs__(self): -1271 newcontent = [None if _check_for_none(self, item) else np.abs(item) for item in self.content] -1272 return Corr(newcontent, prange=self.prange) -1273 -1274 # The numpy functions: -1275 def sqrt(self): -1276 return self ** 0.5 -1277 -1278 def log(self): -1279 newcontent = [None if _check_for_none(self, item) else np.log(item) for item in self.content] -1280 return Corr(newcontent, prange=self.prange) -1281 -1282 def exp(self): -1283 newcontent = [None if _check_for_none(self, item) else np.exp(item) for item in self.content] -1284 return Corr(newcontent, prange=self.prange) -1285 -1286 def _apply_func_to_corr(self, func): -1287 newcontent = [None if _check_for_none(self, item) else func(item) for item in self.content] -1288 for t in range(self.T): -1289 if _check_for_none(self, newcontent[t]): -1290 continue -1291 tmp_sum = np.sum(newcontent[t]) -1292 if hasattr(tmp_sum, "value"): -1293 if np.isnan(tmp_sum.value): -1294 newcontent[t] = None -1295 if all([item is None for item in newcontent]): -1296 raise ValueError('Operation returns undefined correlator') -1297 return Corr(newcontent) -1298 -1299 def sin(self): -1300 return self._apply_func_to_corr(np.sin) -1301 -1302 def cos(self): -1303 return self._apply_func_to_corr(np.cos) -1304 -1305 def tan(self): -1306 return self._apply_func_to_corr(np.tan) -1307 -1308 def sinh(self): -1309 return self._apply_func_to_corr(np.sinh) -1310 -1311 def cosh(self): -1312 return self._apply_func_to_corr(np.cosh) -1313 -1314 def tanh(self): -1315 return self._apply_func_to_corr(np.tanh) -1316 -1317 def arcsin(self): -1318 return self._apply_func_to_corr(np.arcsin) -1319 -1320 def arccos(self): -1321 return self._apply_func_to_corr(np.arccos) -1322 -1323 def arctan(self): -1324 return self._apply_func_to_corr(np.arctan) -1325 -1326 def arcsinh(self): -1327 return self._apply_func_to_corr(np.arcsinh) -1328 -1329 def arccosh(self): -1330 return self._apply_func_to_corr(np.arccosh) -1331 -1332 def arctanh(self): -1333 return self._apply_func_to_corr(np.arctanh) -1334 -1335 # Right hand side operations (require tweak in main module to work) -1336 def __radd__(self, y): -1337 return self + y -1338 -1339 def __rsub__(self, y): -1340 return -self + y -1341 -1342 def __rmul__(self, y): -1343 return self * y -1344 -1345 def __rtruediv__(self, y): -1346 return (self / y) ** (-1) -1347 -1348 @property -1349 def real(self): -1350 def return_real(obs_OR_cobs): -1351 if isinstance(obs_OR_cobs.flatten()[0], CObs): -1352 return np.vectorize(lambda x: x.real)(obs_OR_cobs) -1353 else: -1354 return obs_OR_cobs -1355 -1356 return self._apply_func_to_corr(return_real) -1357 -1358 @property -1359 def imag(self): -1360 def return_imag(obs_OR_cobs): -1361 if isinstance(obs_OR_cobs.flatten()[0], CObs): -1362 return np.vectorize(lambda x: x.imag)(obs_OR_cobs) -1363 else: -1364 return obs_OR_cobs * 0 # So it stays the right type +1208 if _check_for_none(self, newcontent[t]): +1209 continue +1210 if np.isnan(np.sum(newcontent[t]).value): +1211 newcontent[t] = None +1212 +1213 if all([item is None for item in newcontent]): +1214 raise ValueError("Division returns completely undefined correlator") +1215 return Corr(newcontent) +1216 +1217 elif isinstance(y, (Obs, CObs)): +1218 if isinstance(y, Obs): +1219 if y.value == 0: +1220 raise ValueError('Division by zero will return undefined correlator') +1221 if isinstance(y, CObs): +1222 if y.is_zero(): +1223 raise ValueError('Division by zero will return undefined correlator') +1224 +1225 newcontent = [] +1226 for t in range(self.T): +1227 if _check_for_none(self, self.content[t]): +1228 newcontent.append(None) +1229 else: +1230 newcontent.append(self.content[t] / y) +1231 return Corr(newcontent, prange=self.prange) +1232 +1233 elif isinstance(y, (int, float)): +1234 if y == 0: +1235 raise ValueError('Division by zero will return undefined correlator') +1236 newcontent = [] +1237 for t in range(self.T): +1238 if _check_for_none(self, self.content[t]): +1239 newcontent.append(None) +1240 else: +1241 newcontent.append(self.content[t] / y) +1242 return Corr(newcontent, prange=self.prange) +1243 elif isinstance(y, np.ndarray): +1244 if y.shape == (self.T,): +1245 return Corr(list((np.array(self.content).T / y).T)) +1246 else: +1247 raise ValueError("operands could not be broadcast together") +1248 else: +1249 raise TypeError('Corr / wrong type') +1250 +1251 def __neg__(self): +1252 newcontent = [None if _check_for_none(self, item) else -1. * item for item in self.content] +1253 return Corr(newcontent, prange=self.prange) +1254 +1255 def __sub__(self, y): +1256 return self + (-y) +1257 +1258 def __pow__(self, y): +1259 if isinstance(y, (Obs, int, float, CObs)): +1260 newcontent = [None if _check_for_none(self, item) else item**y for item in self.content] +1261 return Corr(newcontent, prange=self.prange) +1262 else: +1263 raise TypeError('Type of exponent not supported') +1264 +1265 def __abs__(self): +1266 newcontent = [None if _check_for_none(self, item) else np.abs(item) for item in self.content] +1267 return Corr(newcontent, prange=self.prange) +1268 +1269 # The numpy functions: +1270 def sqrt(self): +1271 return self ** 0.5 +1272 +1273 def log(self): +1274 newcontent = [None if _check_for_none(self, item) else np.log(item) for item in self.content] +1275 return Corr(newcontent, prange=self.prange) +1276 +1277 def exp(self): +1278 newcontent = [None if _check_for_none(self, item) else np.exp(item) for item in self.content] +1279 return Corr(newcontent, prange=self.prange) +1280 +1281 def _apply_func_to_corr(self, func): +1282 newcontent = [None if _check_for_none(self, item) else func(item) for item in self.content] +1283 for t in range(self.T): +1284 if _check_for_none(self, newcontent[t]): +1285 continue +1286 tmp_sum = np.sum(newcontent[t]) +1287 if hasattr(tmp_sum, "value"): +1288 if np.isnan(tmp_sum.value): +1289 newcontent[t] = None +1290 if all([item is None for item in newcontent]): +1291 raise ValueError('Operation returns undefined correlator') +1292 return Corr(newcontent) +1293 +1294 def sin(self): +1295 return self._apply_func_to_corr(np.sin) +1296 +1297 def cos(self): +1298 return self._apply_func_to_corr(np.cos) +1299 +1300 def tan(self): +1301 return self._apply_func_to_corr(np.tan) +1302 +1303 def sinh(self): +1304 return self._apply_func_to_corr(np.sinh) +1305 +1306 def cosh(self): +1307 return self._apply_func_to_corr(np.cosh) +1308 +1309 def tanh(self): +1310 return self._apply_func_to_corr(np.tanh) +1311 +1312 def arcsin(self): +1313 return self._apply_func_to_corr(np.arcsin) +1314 +1315 def arccos(self): +1316 return self._apply_func_to_corr(np.arccos) +1317 +1318 def arctan(self): +1319 return self._apply_func_to_corr(np.arctan) +1320 +1321 def arcsinh(self): +1322 return self._apply_func_to_corr(np.arcsinh) +1323 +1324 def arccosh(self): +1325 return self._apply_func_to_corr(np.arccosh) +1326 +1327 def arctanh(self): +1328 return self._apply_func_to_corr(np.arctanh) +1329 +1330 # Right hand side operations (require tweak in main module to work) +1331 def __radd__(self, y): +1332 return self + y +1333 +1334 def __rsub__(self, y): +1335 return -self + y +1336 +1337 def __rmul__(self, y): +1338 return self * y +1339 +1340 def __rtruediv__(self, y): +1341 return (self / y) ** (-1) +1342 +1343 @property +1344 def real(self): +1345 def return_real(obs_OR_cobs): +1346 if isinstance(obs_OR_cobs.flatten()[0], CObs): +1347 return np.vectorize(lambda x: x.real)(obs_OR_cobs) +1348 else: +1349 return obs_OR_cobs +1350 +1351 return self._apply_func_to_corr(return_real) +1352 +1353 @property +1354 def imag(self): +1355 def return_imag(obs_OR_cobs): +1356 if isinstance(obs_OR_cobs.flatten()[0], CObs): +1357 return np.vectorize(lambda x: x.imag)(obs_OR_cobs) +1358 else: +1359 return obs_OR_cobs * 0 # So it stays the right type +1360 +1361 return self._apply_func_to_corr(return_imag) +1362 +1363 def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None): +1364 r''' Project large correlation matrix to lowest states 1365 -1366 return self._apply_func_to_corr(return_imag) -1367 -1368 def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None): -1369 r''' Project large correlation matrix to lowest states -1370 -1371 This method can be used to reduce the size of an (N x N) correlation matrix -1372 to (Ntrunc x Ntrunc) by solving a GEVP at very early times where the noise -1373 is still small. -1374 -1375 Parameters -1376 ---------- -1377 Ntrunc: int -1378 Rank of the target matrix. -1379 tproj: int -1380 Time where the eigenvectors are evaluated, corresponds to ts in the GEVP method. -1381 The default value is 3. -1382 t0proj: int -1383 Time where the correlation matrix is inverted. Choosing t0proj=1 is strongly -1384 discouraged for O(a) improved theories, since the correctness of the procedure -1385 cannot be granted in this case. The default value is 2. -1386 basematrix : Corr -1387 Correlation matrix that is used to determine the eigenvectors of the -1388 lowest states based on a GEVP. basematrix is taken to be the Corr itself if -1389 is is not specified. -1390 -1391 Notes -1392 ----- -1393 We have the basematrix $C(t)$ and the target matrix $G(t)$. We start by solving -1394 the GEVP $$C(t) v_n(t, t_0) = \lambda_n(t, t_0) C(t_0) v_n(t, t_0)$$ where $t \equiv t_\mathrm{proj}$ -1395 and $t_0 \equiv t_{0, \mathrm{proj}}$. The target matrix is projected onto the subspace of the -1396 resulting eigenvectors $v_n, n=1,\dots,N_\mathrm{trunc}$ via -1397 $$G^\prime_{i, j}(t) = (v_i, G(t) v_j)$$. This allows to reduce the size of a large -1398 correlation matrix and to remove some noise that is added by irrelevant operators. -1399 This may allow to use the GEVP on $G(t)$ at late times such that the theoretically motivated -1400 bound $t_0 \leq t/2$ holds, since the condition number of $G(t)$ is decreased, compared to $C(t)$. -1401 ''' -1402 -1403 if self.N == 1: -1404 raise ValueError('Method cannot be applied to one-dimensional correlators.') -1405 if basematrix is None: -1406 basematrix = self -1407 if Ntrunc >= basematrix.N: -1408 raise ValueError(f'Cannot truncate using Ntrunc >= {basematrix.N}') -1409 if basematrix.N != self.N: -1410 raise ValueError('basematrix and targetmatrix have to be of the same size.') -1411 -1412 evecs = basematrix.GEVP(t0proj, tproj, sort=None)[:Ntrunc] -1413 -1414 tmpmat = np.empty((Ntrunc, Ntrunc), dtype=object) -1415 rmat = [] -1416 for t in range(basematrix.T): -1417 if self.content[t] is None: -1418 rmat.append(None) -1419 else: -1420 for i in range(Ntrunc): -1421 for j in range(Ntrunc): -1422 tmpmat[i][j] = evecs[i].T @ self[t] @ evecs[j] -1423 rmat.append(np.copy(tmpmat)) -1424 -1425 return Corr(rmat) +1366 This method can be used to reduce the size of an (N x N) correlation matrix +1367 to (Ntrunc x Ntrunc) by solving a GEVP at very early times where the noise +1368 is still small. +1369 +1370 Parameters +1371 ---------- +1372 Ntrunc: int +1373 Rank of the target matrix. +1374 tproj: int +1375 Time where the eigenvectors are evaluated, corresponds to ts in the GEVP method. +1376 The default value is 3. +1377 t0proj: int +1378 Time where the correlation matrix is inverted. Choosing t0proj=1 is strongly +1379 discouraged for O(a) improved theories, since the correctness of the procedure +1380 cannot be granted in this case. The default value is 2. +1381 basematrix : Corr +1382 Correlation matrix that is used to determine the eigenvectors of the +1383 lowest states based on a GEVP. basematrix is taken to be the Corr itself if +1384 is is not specified. +1385 +1386 Notes +1387 ----- +1388 We have the basematrix $C(t)$ and the target matrix $G(t)$. We start by solving +1389 the GEVP $$C(t) v_n(t, t_0) = \lambda_n(t, t_0) C(t_0) v_n(t, t_0)$$ where $t \equiv t_\mathrm{proj}$ +1390 and $t_0 \equiv t_{0, \mathrm{proj}}$. The target matrix is projected onto the subspace of the +1391 resulting eigenvectors $v_n, n=1,\dots,N_\mathrm{trunc}$ via +1392 $$G^\prime_{i, j}(t) = (v_i, G(t) v_j)$$. This allows to reduce the size of a large +1393 correlation matrix and to remove some noise that is added by irrelevant operators. +1394 This may allow to use the GEVP on $G(t)$ at late times such that the theoretically motivated +1395 bound $t_0 \leq t/2$ holds, since the condition number of $G(t)$ is decreased, compared to $C(t)$. +1396 ''' +1397 +1398 if self.N == 1: +1399 raise ValueError('Method cannot be applied to one-dimensional correlators.') +1400 if basematrix is None: +1401 basematrix = self +1402 if Ntrunc >= basematrix.N: +1403 raise ValueError(f'Cannot truncate using Ntrunc >= {basematrix.N}') +1404 if basematrix.N != self.N: +1405 raise ValueError('basematrix and targetmatrix have to be of the same size.') +1406 +1407 evecs = basematrix.GEVP(t0proj, tproj, sort=None)[:Ntrunc] +1408 +1409 tmpmat = np.empty((Ntrunc, Ntrunc), dtype=object) +1410 rmat = [] +1411 for t in range(basematrix.T): +1412 if self.content[t] is None: +1413 rmat.append(None) +1414 else: +1415 for i in range(Ntrunc): +1416 for j in range(Ntrunc): +1417 tmpmat[i][j] = evecs[i].T @ self[t] @ evecs[j] +1418 rmat.append(np.copy(tmpmat)) +1419 +1420 return Corr(rmat) @@ -4535,69 +4525,65 @@ Available choice: 728 if variant == 'log': 729 newcontent = [] 730 for t in range(self.T - 1): -731 if ((self.content[t] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0): +731 if ((self.content[t] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0) or self.content[t][0].value / self.content[t + 1][0].value < 0: 732 newcontent.append(None) -733 elif self.content[t][0].value / self.content[t + 1][0].value < 0: -734 newcontent.append(None) -735 else: -736 newcontent.append(self.content[t] / self.content[t + 1]) -737 if (all([x is None for x in newcontent])): -738 raise ValueError('m_eff is undefined at all timeslices') +733 else: +734 newcontent.append(self.content[t] / self.content[t + 1]) +735 if (all([x is None for x in newcontent])): +736 raise ValueError('m_eff is undefined at all timeslices') +737 +738 return np.log(Corr(newcontent, padding=[0, 1])) 739 -740 return np.log(Corr(newcontent, padding=[0, 1])) -741 -742 elif variant == 'logsym': -743 newcontent = [] -744 for t in range(1, self.T - 1): -745 if ((self.content[t - 1] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0): -746 newcontent.append(None) -747 elif self.content[t - 1][0].value / self.content[t + 1][0].value < 0: -748 newcontent.append(None) -749 else: -750 newcontent.append(self.content[t - 1] / self.content[t + 1]) -751 if (all([x is None for x in newcontent])): -752 raise ValueError('m_eff is undefined at all timeslices') -753 -754 return np.log(Corr(newcontent, padding=[1, 1])) / 2 -755 -756 elif variant in ['periodic', 'cosh', 'sinh']: -757 if variant in ['periodic', 'cosh']: -758 func = anp.cosh -759 else: -760 func = anp.sinh -761 -762 def root_function(x, d): -763 return func(x * (t - self.T / 2)) / func(x * (t + 1 - self.T / 2)) - d -764 -765 newcontent = [] -766 for t in range(self.T - 1): -767 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t + 1][0].value == 0): -768 newcontent.append(None) -769 # Fill the two timeslices in the middle of the lattice with their predecessors -770 elif variant == 'sinh' and t in [self.T / 2, self.T / 2 - 1]: -771 newcontent.append(newcontent[-1]) -772 elif self.content[t][0].value / self.content[t + 1][0].value < 0: -773 newcontent.append(None) -774 else: -775 newcontent.append(np.abs(find_root(self.content[t][0] / self.content[t + 1][0], root_function, guess=guess))) -776 if (all([x is None for x in newcontent])): -777 raise ValueError('m_eff is undefined at all timeslices') -778 -779 return Corr(newcontent, padding=[0, 1]) -780 -781 elif variant == 'arccosh': -782 newcontent = [] -783 for t in range(1, self.T - 1): -784 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t - 1] is None) or (self.content[t][0].value == 0): -785 newcontent.append(None) -786 else: -787 newcontent.append((self.content[t + 1] + self.content[t - 1]) / (2 * self.content[t])) -788 if (all([x is None for x in newcontent])): -789 raise ValueError("m_eff is undefined at all timeslices") -790 return np.arccosh(Corr(newcontent, padding=[1, 1])) -791 -792 else: -793 raise ValueError('Unknown variant.') +740 elif variant == 'logsym': +741 newcontent = [] +742 for t in range(1, self.T - 1): +743 if ((self.content[t - 1] is None) or (self.content[t + 1] is None)) or (self.content[t + 1][0].value == 0) or self.content[t - 1][0].value / self.content[t + 1][0].value < 0: +744 newcontent.append(None) +745 else: +746 newcontent.append(self.content[t - 1] / self.content[t + 1]) +747 if (all([x is None for x in newcontent])): +748 raise ValueError('m_eff is undefined at all timeslices') +749 +750 return np.log(Corr(newcontent, padding=[1, 1])) / 2 +751 +752 elif variant in ['periodic', 'cosh', 'sinh']: +753 if variant in ['periodic', 'cosh']: +754 func = anp.cosh +755 else: +756 func = anp.sinh +757 +758 def root_function(x, d): +759 return func(x * (t - self.T / 2)) / func(x * (t + 1 - self.T / 2)) - d +760 +761 newcontent = [] +762 for t in range(self.T - 1): +763 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t + 1][0].value == 0): +764 newcontent.append(None) +765 # Fill the two timeslices in the middle of the lattice with their predecessors +766 elif variant == 'sinh' and t in [self.T / 2, self.T / 2 - 1]: +767 newcontent.append(newcontent[-1]) +768 elif self.content[t][0].value / self.content[t + 1][0].value < 0: +769 newcontent.append(None) +770 else: +771 newcontent.append(np.abs(find_root(self.content[t][0] / self.content[t + 1][0], root_function, guess=guess))) +772 if (all([x is None for x in newcontent])): +773 raise ValueError('m_eff is undefined at all timeslices') +774 +775 return Corr(newcontent, padding=[0, 1]) +776 +777 elif variant == 'arccosh': +778 newcontent = [] +779 for t in range(1, self.T - 1): +780 if (self.content[t] is None) or (self.content[t + 1] is None) or (self.content[t - 1] is None) or (self.content[t][0].value == 0): +781 newcontent.append(None) +782 else: +783 newcontent.append((self.content[t + 1] + self.content[t - 1]) / (2 * self.content[t])) +784 if (all([x is None for x in newcontent])): +785 raise ValueError("m_eff is undefined at all timeslices") +786 return np.arccosh(Corr(newcontent, padding=[1, 1])) +787 +788 else: +789 raise ValueError('Unknown variant.') @@ -4631,39 +4617,39 @@ guess for the root finder, only relevant for the root variant -
795 def fit(self, function, fitrange=None, silent=False, **kwargs): -796 r'''Fits function to the data -797 -798 Parameters -799 ---------- -800 function : obj -801 function to fit to the data. See fits.least_squares for details. -802 fitrange : list -803 Two element list containing the timeslices on which the fit is supposed to start and stop. -804 Caution: This range is inclusive as opposed to standard python indexing. -805 `fitrange=[4, 6]` corresponds to the three entries 4, 5 and 6. -806 If not specified, self.prange or all timeslices are used. -807 silent : bool -808 Decides whether output is printed to the standard output. -809 ''' -810 if self.N != 1: -811 raise ValueError("Correlator must be projected before fitting") -812 -813 if fitrange is None: -814 if self.prange: -815 fitrange = self.prange -816 else: -817 fitrange = [0, self.T - 1] -818 else: -819 if not isinstance(fitrange, list): -820 raise TypeError("fitrange has to be a list with two elements") -821 if len(fitrange) != 2: -822 raise ValueError("fitrange has to have exactly two elements [fit_start, fit_stop]") -823 -824 xs = np.array([x for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) -825 ys = np.array([self.content[x][0] for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) -826 result = least_squares(xs, ys, function, silent=silent, **kwargs) -827 return result +@@ -4697,42 +4683,42 @@ Decides whether output is printed to the standard output.791 def fit(self, function, fitrange=None, silent=False, **kwargs): +792 r'''Fits function to the data +793 +794 Parameters +795 ---------- +796 function : obj +797 function to fit to the data. See fits.least_squares for details. +798 fitrange : list +799 Two element list containing the timeslices on which the fit is supposed to start and stop. +800 Caution: This range is inclusive as opposed to standard python indexing. +801 `fitrange=[4, 6]` corresponds to the three entries 4, 5 and 6. +802 If not specified, self.prange or all timeslices are used. +803 silent : bool +804 Decides whether output is printed to the standard output. +805 ''' +806 if self.N != 1: +807 raise ValueError("Correlator must be projected before fitting") +808 +809 if fitrange is None: +810 if self.prange: +811 fitrange = self.prange +812 else: +813 fitrange = [0, self.T - 1] +814 else: +815 if not isinstance(fitrange, list): +816 raise TypeError("fitrange has to be a list with two elements") +817 if len(fitrange) != 2: +818 raise ValueError("fitrange has to have exactly two elements [fit_start, fit_stop]") +819 +820 xs = np.array([x for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) +821 ys = np.array([self.content[x][0] for x in range(fitrange[0], fitrange[1] + 1) if self.content[x] is not None]) +822 result = least_squares(xs, ys, function, silent=silent, **kwargs) +823 return result
829 def plateau(self, plateau_range=None, method="fit", auto_gamma=False): -830 """ Extract a plateau value from a Corr object -831 -832 Parameters -833 ---------- -834 plateau_range : list -835 list with two entries, indicating the first and the last timeslice -836 of the plateau region. -837 method : str -838 method to extract the plateau. -839 'fit' fits a constant to the plateau region -840 'avg', 'average' or 'mean' just average over the given timeslices. -841 auto_gamma : bool -842 apply gamma_method with default parameters to the Corr. Defaults to None -843 """ -844 if not plateau_range: -845 if self.prange: -846 plateau_range = self.prange -847 else: -848 raise ValueError("no plateau range provided") -849 if self.N != 1: -850 raise ValueError("Correlator must be projected before getting a plateau.") -851 if (all([self.content[t] is None for t in range(plateau_range[0], plateau_range[1] + 1)])): -852 raise ValueError("plateau is undefined at all timeslices in plateaurange.") -853 if auto_gamma: -854 self.gamma_method() -855 if method == "fit": -856 def const_func(a, t): -857 return a[0] -858 return self.fit(const_func, plateau_range)[0] -859 elif method in ["avg", "average", "mean"]: -860 returnvalue = np.mean([item[0] for item in self.content[plateau_range[0]:plateau_range[1] + 1] if item is not None]) -861 return returnvalue -862 -863 else: -864 raise ValueError("Unsupported plateau method: " + method) +@@ -4766,17 +4752,16 @@ apply gamma_method with default parameters to the Corr. Defaults to None825 def plateau(self, plateau_range=None, method="fit", auto_gamma=False): +826 """ Extract a plateau value from a Corr object +827 +828 Parameters +829 ---------- +830 plateau_range : list +831 list with two entries, indicating the first and the last timeslice +832 of the plateau region. +833 method : str +834 method to extract the plateau. +835 'fit' fits a constant to the plateau region +836 'avg', 'average' or 'mean' just average over the given timeslices. +837 auto_gamma : bool +838 apply gamma_method with default parameters to the Corr. Defaults to None +839 """ +840 if not plateau_range: +841 if self.prange: +842 plateau_range = self.prange +843 else: +844 raise ValueError("no plateau range provided") +845 if self.N != 1: +846 raise ValueError("Correlator must be projected before getting a plateau.") +847 if (all([self.content[t] is None for t in range(plateau_range[0], plateau_range[1] + 1)])): +848 raise ValueError("plateau is undefined at all timeslices in plateaurange.") +849 if auto_gamma: +850 self.gamma_method() +851 if method == "fit": +852 def const_func(a, t): +853 return a[0] +854 return self.fit(const_func, plateau_range)[0] +855 elif method in ["avg", "average", "mean"]: +856 returnvalue = np.mean([item[0] for item in self.content[plateau_range[0]:plateau_range[1] + 1] if item is not None]) +857 return returnvalue +858 +859 else: +860 raise ValueError("Unsupported plateau method: " + method)
866 def set_prange(self, prange): -867 """Sets the attribute prange of the Corr object.""" -868 if not len(prange) == 2: -869 raise ValueError("prange must be a list or array with two values") -870 if not ((isinstance(prange[0], int)) and (isinstance(prange[1], int))): -871 raise TypeError("Start and end point must be integers") -872 if not (0 <= prange[0] <= self.T and 0 <= prange[1] <= self.T and prange[0] <= prange[1]): -873 raise ValueError("Start and end point must define a range in the interval 0,T") -874 -875 self.prange = prange -876 return +@@ -4796,130 +4781,130 @@ apply gamma_method with default parameters to the Corr. Defaults to None862 def set_prange(self, prange): +863 """Sets the attribute prange of the Corr object.""" +864 if not len(prange) == 2: +865 raise ValueError("prange must be a list or array with two values") +866 if not ((isinstance(prange[0], int)) and (isinstance(prange[1], int))): +867 raise TypeError("Start and end point must be integers") +868 if not (0 <= prange[0] <= self.T and 0 <= prange[1] <= self.T and prange[0] <= prange[1]): +869 raise ValueError("Start and end point must define a range in the interval 0,T") +870 +871 self.prange = prange
878 def show(self, x_range=None, comp=None, y_range=None, logscale=False, plateau=None, fit_res=None, fit_key=None, ylabel=None, save=None, auto_gamma=False, hide_sigma=None, references=None, title=None): - 879 """Plots the correlator using the tag of the correlator as label if available. - 880 - 881 Parameters - 882 ---------- - 883 x_range : list - 884 list of two values, determining the range of the x-axis e.g. [4, 8]. - 885 comp : Corr or list of Corr - 886 Correlator or list of correlators which are plotted for comparison. - 887 The tags of these correlators are used as labels if available. - 888 logscale : bool - 889 Sets y-axis to logscale. - 890 plateau : Obs - 891 Plateau value to be visualized in the figure. - 892 fit_res : Fit_result - 893 Fit_result object to be visualized. - 894 fit_key : str - 895 Key for the fit function in Fit_result.fit_function (for combined fits). - 896 ylabel : str - 897 Label for the y-axis. - 898 save : str - 899 path to file in which the figure should be saved. - 900 auto_gamma : bool - 901 Apply the gamma method with standard parameters to all correlators and plateau values before plotting. - 902 hide_sigma : float - 903 Hides data points from the first value on which is consistent with zero within 'hide_sigma' standard errors. - 904 references : list - 905 List of floating point values that are displayed as horizontal lines for reference. - 906 title : string - 907 Optional title of the figure. - 908 """ - 909 if self.N != 1: - 910 raise ValueError("Correlator must be projected before plotting") - 911 - 912 if auto_gamma: - 913 self.gamma_method() - 914 - 915 if x_range is None: - 916 x_range = [0, self.T - 1] - 917 - 918 fig = plt.figure() - 919 ax1 = fig.add_subplot(111) - 920 - 921 x, y, y_err = self.plottable() - 922 if hide_sigma: - 923 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 - 924 else: - 925 hide_from = None - 926 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=self.tag) - 927 if logscale: - 928 ax1.set_yscale('log') - 929 else: - 930 if y_range is None: - 931 try: - 932 y_min = min([(x[0].value - x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) - 933 y_max = max([(x[0].value + x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) - 934 ax1.set_ylim([y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min)]) - 935 except Exception: - 936 pass - 937 else: - 938 ax1.set_ylim(y_range) - 939 if comp: - 940 if isinstance(comp, (Corr, list)): - 941 for corr in comp if isinstance(comp, list) else [comp]: - 942 if auto_gamma: - 943 corr.gamma_method() - 944 x, y, y_err = corr.plottable() - 945 if hide_sigma: - 946 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 - 947 else: - 948 hide_from = None - 949 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=corr.tag, mfc=plt.rcParams['axes.facecolor']) - 950 else: - 951 raise TypeError("'comp' must be a correlator or a list of correlators.") - 952 - 953 if plateau: - 954 if isinstance(plateau, Obs): - 955 if auto_gamma: - 956 plateau.gamma_method() - 957 ax1.axhline(y=plateau.value, linewidth=2, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--', label=str(plateau)) - 958 ax1.axhspan(plateau.value - plateau.dvalue, plateau.value + plateau.dvalue, alpha=0.25, color=plt.rcParams['text.color'], ls='-') - 959 else: - 960 raise TypeError("'plateau' must be an Obs") - 961 - 962 if references: - 963 if isinstance(references, list): - 964 for ref in references: - 965 ax1.axhline(y=ref, linewidth=1, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--') - 966 else: - 967 raise TypeError("'references' must be a list of floating pint values.") - 968 - 969 if self.prange: - 970 ax1.axvline(self.prange[0], 0, 1, ls='-', marker=',', color="black", zorder=0) - 971 ax1.axvline(self.prange[1], 0, 1, ls='-', marker=',', color="black", zorder=0) - 972 - 973 if fit_res: - 974 x_samples = np.arange(x_range[0], x_range[1] + 1, 0.05) - 975 if isinstance(fit_res.fit_function, dict): - 976 if fit_key: - 977 ax1.plot(x_samples, fit_res.fit_function[fit_key]([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) - 978 else: - 979 raise ValueError("Please provide a 'fit_key' for visualizing combined fits.") - 980 else: - 981 ax1.plot(x_samples, fit_res.fit_function([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) - 982 - 983 ax1.set_xlabel(r'$x_0 / a$') - 984 if ylabel: - 985 ax1.set_ylabel(ylabel) - 986 ax1.set_xlim([x_range[0] - 0.5, x_range[1] + 0.5]) - 987 - 988 _handles, labels = ax1.get_legend_handles_labels() - 989 if labels: - 990 ax1.legend() - 991 - 992 if title: - 993 plt.title(title) - 994 - 995 plt.draw() - 996 - 997 if save: - 998 if isinstance(save, str): - 999 fig.savefig(save, bbox_inches='tight') -1000 else: -1001 raise TypeError("'save' has to be a string.") +@@ -4969,34 +4954,34 @@ Optional title of the figure.873 def show(self, x_range=None, comp=None, y_range=None, logscale=False, plateau=None, fit_res=None, fit_key=None, ylabel=None, save=None, auto_gamma=False, hide_sigma=None, references=None, title=None): +874 """Plots the correlator using the tag of the correlator as label if available. +875 +876 Parameters +877 ---------- +878 x_range : list +879 list of two values, determining the range of the x-axis e.g. [4, 8]. +880 comp : Corr or list of Corr +881 Correlator or list of correlators which are plotted for comparison. +882 The tags of these correlators are used as labels if available. +883 logscale : bool +884 Sets y-axis to logscale. +885 plateau : Obs +886 Plateau value to be visualized in the figure. +887 fit_res : Fit_result +888 Fit_result object to be visualized. +889 fit_key : str +890 Key for the fit function in Fit_result.fit_function (for combined fits). +891 ylabel : str +892 Label for the y-axis. +893 save : str +894 path to file in which the figure should be saved. +895 auto_gamma : bool +896 Apply the gamma method with standard parameters to all correlators and plateau values before plotting. +897 hide_sigma : float +898 Hides data points from the first value on which is consistent with zero within 'hide_sigma' standard errors. +899 references : list +900 List of floating point values that are displayed as horizontal lines for reference. +901 title : string +902 Optional title of the figure. +903 """ +904 if self.N != 1: +905 raise ValueError("Correlator must be projected before plotting") +906 +907 if auto_gamma: +908 self.gamma_method() +909 +910 if x_range is None: +911 x_range = [0, self.T - 1] +912 +913 fig = plt.figure() +914 ax1 = fig.add_subplot(111) +915 +916 x, y, y_err = self.plottable() +917 if hide_sigma: +918 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 +919 else: +920 hide_from = None +921 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=self.tag) +922 if logscale: +923 ax1.set_yscale('log') +924 else: +925 if y_range is None: +926 try: +927 y_min = min([(x[0].value - x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) +928 y_max = max([(x[0].value + x[0].dvalue) for x in self.content[x_range[0]: x_range[1] + 1] if (x is not None) and x[0].dvalue < 2 * np.abs(x[0].value)]) +929 ax1.set_ylim([y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min)]) +930 except Exception: +931 pass +932 else: +933 ax1.set_ylim(y_range) +934 if comp: +935 if isinstance(comp, (Corr, list)): +936 for corr in comp if isinstance(comp, list) else [comp]: +937 if auto_gamma: +938 corr.gamma_method() +939 x, y, y_err = corr.plottable() +940 if hide_sigma: +941 hide_from = np.argmax((hide_sigma * np.array(y_err[1:])) > np.abs(y[1:])) - 1 +942 else: +943 hide_from = None +944 ax1.errorbar(x[:hide_from], y[:hide_from], y_err[:hide_from], label=corr.tag, mfc=plt.rcParams['axes.facecolor']) +945 else: +946 raise TypeError("'comp' must be a correlator or a list of correlators.") +947 +948 if plateau: +949 if isinstance(plateau, Obs): +950 if auto_gamma: +951 plateau.gamma_method() +952 ax1.axhline(y=plateau.value, linewidth=2, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--', label=str(plateau)) +953 ax1.axhspan(plateau.value - plateau.dvalue, plateau.value + plateau.dvalue, alpha=0.25, color=plt.rcParams['text.color'], ls='-') +954 else: +955 raise TypeError("'plateau' must be an Obs") +956 +957 if references: +958 if isinstance(references, list): +959 for ref in references: +960 ax1.axhline(y=ref, linewidth=1, color=plt.rcParams['text.color'], alpha=0.6, marker=',', ls='--') +961 else: +962 raise TypeError("'references' must be a list of floating pint values.") +963 +964 if self.prange: +965 ax1.axvline(self.prange[0], 0, 1, ls='-', marker=',', color="black", zorder=0) +966 ax1.axvline(self.prange[1], 0, 1, ls='-', marker=',', color="black", zorder=0) +967 +968 if fit_res: +969 x_samples = np.arange(x_range[0], x_range[1] + 1, 0.05) +970 if isinstance(fit_res.fit_function, dict): +971 if fit_key: +972 ax1.plot(x_samples, fit_res.fit_function[fit_key]([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) +973 else: +974 raise ValueError("Please provide a 'fit_key' for visualizing combined fits.") +975 else: +976 ax1.plot(x_samples, fit_res.fit_function([o.value for o in fit_res.fit_parameters], x_samples), ls='-', marker=',', lw=2) +977 +978 ax1.set_xlabel(r'$x_0 / a$') +979 if ylabel: +980 ax1.set_ylabel(ylabel) +981 ax1.set_xlim([x_range[0] - 0.5, x_range[1] + 0.5]) +982 +983 _handles, labels = ax1.get_legend_handles_labels() +984 if labels: +985 ax1.legend() +986 +987 if title: +988 plt.title(title) +989 +990 plt.draw() +991 +992 if save: +993 if isinstance(save, str): +994 fig.savefig(save, bbox_inches='tight') +995 else: +996 raise TypeError("'save' has to be a string.")
1003 def spaghetti_plot(self, logscale=True): -1004 """Produces a spaghetti plot of the correlator suited to monitor exceptional configurations. -1005 -1006 Parameters -1007 ---------- -1008 logscale : bool -1009 Determines whether the scale of the y-axis is logarithmic or standard. -1010 """ -1011 if self.N != 1: -1012 raise ValueError("Correlator needs to be projected first.") -1013 -1014 mc_names = list(set([item for sublist in [list(itertools.chain.from_iterable(map(o[0].e_content.get, o[0].mc_names))) for o in self.content if o is not None] for item in sublist])) -1015 x0_vals = [n for (n, o) in zip(np.arange(self.T), self.content, strict=True) if o is not None] -1016 -1017 for name in mc_names: -1018 data = np.array([o[0].deltas[name] + o[0].r_values[name] for o in self.content if o is not None]).T +@@ -5023,29 +5008,29 @@ Determines whether the scale of the y-axis is logarithmic or standard.998 def spaghetti_plot(self, logscale=True): + 999 """Produces a spaghetti plot of the correlator suited to monitor exceptional configurations. +1000 +1001 Parameters +1002 ---------- +1003 logscale : bool +1004 Determines whether the scale of the y-axis is logarithmic or standard. +1005 """ +1006 if self.N != 1: +1007 raise ValueError("Correlator needs to be projected first.") +1008 +1009 mc_names = list(set([item for sublist in [list(itertools.chain.from_iterable(map(o[0].e_content.get, o[0].mc_names))) for o in self.content if o is not None] for item in sublist])) +1010 x0_vals = [n for (n, o) in zip(np.arange(self.T), self.content, strict=True) if o is not None] +1011 +1012 for name in mc_names: +1013 data = np.array([o[0].deltas[name] + o[0].r_values[name] for o in self.content if o is not None]).T +1014 +1015 fig = plt.figure() +1016 ax = fig.add_subplot(111) +1017 for dat in data: +1018 ax.plot(x0_vals, dat, ls='-', marker='') 1019 -1020 fig = plt.figure() -1021 ax = fig.add_subplot(111) -1022 for dat in data: -1023 ax.plot(x0_vals, dat, ls='-', marker='') -1024 -1025 if logscale is True: -1026 ax.set_yscale('log') -1027 -1028 ax.set_xlabel(r'$x_0 / a$') -1029 plt.title(name) -1030 plt.draw() +1020 if logscale is True: +1021 ax.set_yscale('log') +1022 +1023 ax.set_xlabel(r'$x_0 / a$') +1024 plt.title(name) +1025 plt.draw()
1032 def dump(self, filename, datatype="json.gz", **kwargs): -1033 """Dumps the Corr into a file of chosen type -1034 Parameters -1035 ---------- -1036 filename : str -1037 Name of the file to be saved. -1038 datatype : str -1039 Format of the exported file. Supported formats include -1040 "json.gz" and "pickle" -1041 path : str -1042 specifies a custom path for the file (default '.') -1043 """ -1044 if datatype == "json.gz": -1045 from .input.json import dump_to_json -1046 if 'path' in kwargs: -1047 file_name = kwargs.get('path') + '/' + filename -1048 else: -1049 file_name = filename -1050 dump_to_json(self, file_name) -1051 elif datatype == "pickle": -1052 dump_object(self, filename, **kwargs) -1053 else: -1054 raise ValueError("Unknown datatype " + str(datatype)) +@@ -5077,8 +5062,8 @@ specifies a custom path for the file (default '.')1027 def dump(self, filename, datatype="json.gz", **kwargs): +1028 """Dumps the Corr into a file of chosen type +1029 Parameters +1030 ---------- +1031 filename : str +1032 Name of the file to be saved. +1033 datatype : str +1034 Format of the exported file. Supported formats include +1035 "json.gz" and "pickle" +1036 path : str +1037 specifies a custom path for the file (default '.') +1038 """ +1039 if datatype == "json.gz": +1040 from .input.json import dump_to_json +1041 if 'path' in kwargs: +1042 file_name = kwargs.get('path') + '/' + filename +1043 else: +1044 file_name = filename +1045 dump_to_json(self, file_name) +1046 elif datatype == "pickle": +1047 dump_object(self, filename, **kwargs) +1048 else: +1049 raise ValueError("Unknown datatype " + str(datatype))
1056 def print(self, print_range=None): -1057 print(self.__repr__(print_range)) + @@ -5096,8 +5081,8 @@ specifies a custom path for the file (default '.')
1275 def sqrt(self): -1276 return self ** 0.5 + @@ -5115,9 +5100,9 @@ specifies a custom path for the file (default '.')
1278 def log(self): -1279 newcontent = [None if _check_for_none(self, item) else np.log(item) for item in self.content] -1280 return Corr(newcontent, prange=self.prange) + @@ -5135,9 +5120,9 @@ specifies a custom path for the file (default '.')
1282 def exp(self): -1283 newcontent = [None if _check_for_none(self, item) else np.exp(item) for item in self.content] -1284 return Corr(newcontent, prange=self.prange) + @@ -5155,8 +5140,8 @@ specifies a custom path for the file (default '.')
1299 def sin(self): -1300 return self._apply_func_to_corr(np.sin) + @@ -5174,8 +5159,8 @@ specifies a custom path for the file (default '.')
1302 def cos(self): -1303 return self._apply_func_to_corr(np.cos) + @@ -5193,8 +5178,8 @@ specifies a custom path for the file (default '.')
1305 def tan(self): -1306 return self._apply_func_to_corr(np.tan) + @@ -5212,8 +5197,8 @@ specifies a custom path for the file (default '.')
1308 def sinh(self): -1309 return self._apply_func_to_corr(np.sinh) + @@ -5231,8 +5216,8 @@ specifies a custom path for the file (default '.')
1311 def cosh(self): -1312 return self._apply_func_to_corr(np.cosh) + @@ -5250,8 +5235,8 @@ specifies a custom path for the file (default '.')
1314 def tanh(self): -1315 return self._apply_func_to_corr(np.tanh) + @@ -5269,8 +5254,8 @@ specifies a custom path for the file (default '.')
1317 def arcsin(self): -1318 return self._apply_func_to_corr(np.arcsin) + @@ -5288,8 +5273,8 @@ specifies a custom path for the file (default '.')
1320 def arccos(self): -1321 return self._apply_func_to_corr(np.arccos) + @@ -5307,8 +5292,8 @@ specifies a custom path for the file (default '.')
1323 def arctan(self): -1324 return self._apply_func_to_corr(np.arctan) + @@ -5326,8 +5311,8 @@ specifies a custom path for the file (default '.')
1326 def arcsinh(self): -1327 return self._apply_func_to_corr(np.arcsinh) + @@ -5345,8 +5330,8 @@ specifies a custom path for the file (default '.')
1329 def arccosh(self): -1330 return self._apply_func_to_corr(np.arccosh) + @@ -5364,8 +5349,8 @@ specifies a custom path for the file (default '.')
1332 def arctanh(self): -1333 return self._apply_func_to_corr(np.arctanh) + @@ -5381,15 +5366,15 @@ specifies a custom path for the file (default '.')
1348 @property -1349 def real(self): -1350 def return_real(obs_OR_cobs): -1351 if isinstance(obs_OR_cobs.flatten()[0], CObs): -1352 return np.vectorize(lambda x: x.real)(obs_OR_cobs) -1353 else: -1354 return obs_OR_cobs -1355 -1356 return self._apply_func_to_corr(return_real) + @@ -5405,15 +5390,15 @@ specifies a custom path for the file (default '.')
1358 @property -1359 def imag(self): -1360 def return_imag(obs_OR_cobs): -1361 if isinstance(obs_OR_cobs.flatten()[0], CObs): -1362 return np.vectorize(lambda x: x.imag)(obs_OR_cobs) -1363 else: -1364 return obs_OR_cobs * 0 # So it stays the right type -1365 -1366 return self._apply_func_to_corr(return_imag) +@@ -5431,64 +5416,64 @@ specifies a custom path for the file (default '.')1353 @property +1354 def imag(self): +1355 def return_imag(obs_OR_cobs): +1356 if isinstance(obs_OR_cobs.flatten()[0], CObs): +1357 return np.vectorize(lambda x: x.imag)(obs_OR_cobs) +1358 else: +1359 return obs_OR_cobs * 0 # So it stays the right type +1360 +1361 return self._apply_func_to_corr(return_imag)
1368 def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None): -1369 r''' Project large correlation matrix to lowest states -1370 -1371 This method can be used to reduce the size of an (N x N) correlation matrix -1372 to (Ntrunc x Ntrunc) by solving a GEVP at very early times where the noise -1373 is still small. -1374 -1375 Parameters -1376 ---------- -1377 Ntrunc: int -1378 Rank of the target matrix. -1379 tproj: int -1380 Time where the eigenvectors are evaluated, corresponds to ts in the GEVP method. -1381 The default value is 3. -1382 t0proj: int -1383 Time where the correlation matrix is inverted. Choosing t0proj=1 is strongly -1384 discouraged for O(a) improved theories, since the correctness of the procedure -1385 cannot be granted in this case. The default value is 2. -1386 basematrix : Corr -1387 Correlation matrix that is used to determine the eigenvectors of the -1388 lowest states based on a GEVP. basematrix is taken to be the Corr itself if -1389 is is not specified. -1390 -1391 Notes -1392 ----- -1393 We have the basematrix $C(t)$ and the target matrix $G(t)$. We start by solving -1394 the GEVP $$C(t) v_n(t, t_0) = \lambda_n(t, t_0) C(t_0) v_n(t, t_0)$$ where $t \equiv t_\mathrm{proj}$ -1395 and $t_0 \equiv t_{0, \mathrm{proj}}$. The target matrix is projected onto the subspace of the -1396 resulting eigenvectors $v_n, n=1,\dots,N_\mathrm{trunc}$ via -1397 $$G^\prime_{i, j}(t) = (v_i, G(t) v_j)$$. This allows to reduce the size of a large -1398 correlation matrix and to remove some noise that is added by irrelevant operators. -1399 This may allow to use the GEVP on $G(t)$ at late times such that the theoretically motivated -1400 bound $t_0 \leq t/2$ holds, since the condition number of $G(t)$ is decreased, compared to $C(t)$. -1401 ''' -1402 -1403 if self.N == 1: -1404 raise ValueError('Method cannot be applied to one-dimensional correlators.') -1405 if basematrix is None: -1406 basematrix = self -1407 if Ntrunc >= basematrix.N: -1408 raise ValueError(f'Cannot truncate using Ntrunc >= {basematrix.N}') -1409 if basematrix.N != self.N: -1410 raise ValueError('basematrix and targetmatrix have to be of the same size.') -1411 -1412 evecs = basematrix.GEVP(t0proj, tproj, sort=None)[:Ntrunc] -1413 -1414 tmpmat = np.empty((Ntrunc, Ntrunc), dtype=object) -1415 rmat = [] -1416 for t in range(basematrix.T): -1417 if self.content[t] is None: -1418 rmat.append(None) -1419 else: -1420 for i in range(Ntrunc): -1421 for j in range(Ntrunc): -1422 tmpmat[i][j] = evecs[i].T @ self[t] @ evecs[j] -1423 rmat.append(np.copy(tmpmat)) -1424 -1425 return Corr(rmat) +diff --git a/docs/pyerrors/obs.html b/docs/pyerrors/obs.html index df71bb85..c2694443 100644 --- a/docs/pyerrors/obs.html +++ b/docs/pyerrors/obs.html @@ -700,1535 +700,1534 @@ 364 self.ddvalue = 0.0 365 else: 366 self.ddvalue = np.sqrt(self.ddvalue) / self._dvalue - 367 return - 368 - 369 gm = gamma_method - 370 - 371 def _calc_gamma(self, deltas, idx, shape, w_max, fft, gapsize): - 372 """Calculate Gamma_{AA} from the deltas, which are defined on idx. - 373 idx is assumed to be a contiguous range (possibly with a stepsize != 1) - 374 - 375 Parameters - 376 ---------- - 377 deltas : list - 378 List of fluctuations - 379 idx : list - 380 List or range of configurations on which the deltas are defined. - 381 shape : int - 382 Number of configurations in idx. - 383 w_max : int - 384 Upper bound for the summation window. - 385 fft : bool - 386 determines whether the fft algorithm is used for the computation - 387 of the autocorrelation function. - 388 gapsize : int - 389 The target distance between two configurations. If longer distances - 390 are found in idx, the data is expanded. - 391 """ - 392 gamma = np.zeros(w_max) - 393 deltas = _expand_deltas(deltas, idx, shape, gapsize) - 394 new_shape = len(deltas) - 395 if fft: - 396 max_gamma = min(new_shape, w_max) - 397 # The padding for the fft has to be even - 398 padding = new_shape + max_gamma + (new_shape + max_gamma) % 2 - 399 gamma[:max_gamma] += np.fft.irfft(np.abs(np.fft.rfft(deltas, padding)) ** 2)[:max_gamma] - 400 else: - 401 for n in range(w_max): - 402 if new_shape - n >= 0: - 403 gamma[n] += deltas[0:new_shape - n].dot(deltas[n:new_shape]) - 404 - 405 return gamma - 406 - 407 def details(self, ens_content=True): - 408 """Output detailed properties of the Obs. - 409 - 410 Parameters - 411 ---------- - 412 ens_content : bool - 413 print details about the ensembles and replica if true. - 414 """ - 415 if self.tag is not None: - 416 print("Description:", self.tag) - 417 if not hasattr(self, 'e_dvalue'): - 418 print(f'Result\t {self.value:3.8e}') - 419 else: - 420 if self.value == 0.0: - 421 percentage = np.nan - 422 else: - 423 percentage = np.abs(self._dvalue / self.value) * 100 - 424 print(f'Result\t {self.value:3.8e} +/- {self._dvalue:3.8e} +/- {self.ddvalue:3.8e} ({percentage:3.3f}%)') - 425 if len(self.e_names) > 1: - 426 print(' Ensemble errors:') - 427 e_content = self.e_content - 428 for e_name in self.mc_names: - 429 gap = _determine_gap(self, e_content, e_name) - 430 - 431 if len(self.e_names) > 1: - 432 print('', e_name, f'\t {self.e_dvalue[e_name]:3.6e} +/- {self.e_ddvalue[e_name]:3.6e}') - 433 tau_string = " \N{GREEK SMALL LETTER TAU}_int\t " + _format_uncertainty(self.e_tauint[e_name], self.e_dtauint[e_name]) - 434 tau_string += f" in units of {gap} config" - 435 if gap > 1: - 436 tau_string += "s" - 437 if self.tau_exp[e_name] > 0: - 438 tau_string = f"{tau_string: <45}" + f'\t(\N{GREEK SMALL LETTER TAU}_exp={self.tau_exp[e_name]:3.2f}, N_\N{GREEK SMALL LETTER SIGMA}={self.N_sigma[e_name]:g})' - 439 else: - 440 tau_string = f"{tau_string: <45}" + f'\t(S={self.S[e_name]:3.2f})' - 441 print(tau_string) - 442 for e_name in self.cov_names: - 443 print('', e_name, f'\t {self.e_dvalue[e_name]:3.8e}') - 444 if ens_content is True: - 445 if len(self.e_names) == 1: - 446 print(self.N, 'samples in', len(self.e_names), 'ensemble:') - 447 else: - 448 print(self.N, 'samples in', len(self.e_names), 'ensembles:') - 449 my_string_list = [] - 450 for key, value in sorted(self.e_content.items()): - 451 if key not in self.covobs: - 452 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " - 453 if len(value) == 1: - 454 my_string += f': {self.shape[value[0]]} configurations' - 455 if isinstance(self.idl[value[0]], range): - 456 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}' + ')' - 457 else: - 458 my_string += f' (irregular range from {self.idl[value[0]][0]} to {self.idl[value[0]][-1]})' - 459 else: - 460 sublist = [] - 461 for v in value: - 462 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " - 463 my_substring += f': {self.shape[v]} configurations' - 464 if isinstance(self.idl[v], range): - 465 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}' + ')' - 466 else: - 467 my_substring += f' (irregular range from {self.idl[v][0]} to {self.idl[v][-1]})' - 468 sublist.append(my_substring) - 469 - 470 my_string += '\n' + '\n'.join(sublist) - 471 else: - 472 my_string = ' ' + "\u00B7 Covobs '" + key + "' " - 473 my_string_list.append(my_string) - 474 print('\n'.join(my_string_list)) - 475 - 476 def reweight(self, weight): - 477 """Reweight the obs with given rewighting factors. - 478 - 479 Parameters - 480 ---------- - 481 weight : Obs - 482 Reweighting factor. An Observable that has to be defined on a superset of the - 483 configurations in obs[i].idl for all i. - 484 all_configs : bool - 485 if True, the reweighted observables are normalized by the average of - 486 the reweighting factor on all configurations in weight.idl and not - 487 on the configurations in obs[i].idl. Default False. - 488 """ - 489 return reweight(weight, [self])[0] - 490 - 491 def is_zero_within_error(self, sigma=1): - 492 """Checks whether the observable is zero within 'sigma' standard errors. - 493 - 494 Parameters - 495 ---------- - 496 sigma : int - 497 Number of standard errors used for the check. - 498 - 499 Works only properly when the gamma method was run. - 500 """ - 501 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue - 502 - 503 def is_zero(self, atol=1e-10): - 504 """Checks whether the observable is zero within a given tolerance. - 505 - 506 Parameters - 507 ---------- - 508 atol : float - 509 Absolute tolerance (for details see numpy documentation). - 510 """ - 511 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()) - 512 - 513 def plot_tauint(self, save=None): - 514 """Plot integrated autocorrelation time for each ensemble. - 515 - 516 Parameters - 517 ---------- - 518 save : str - 519 saves the figure to a file named 'save' if. - 520 """ - 521 if not hasattr(self, 'e_dvalue'): - 522 raise Exception('Run the gamma method first.') - 523 - 524 for e, e_name in enumerate(self.mc_names): - 525 fig = plt.figure() - 526 plt.xlabel(r'$W$') - 527 plt.ylabel(r'$\tau_\mathrm{int}$') - 528 length = len(self.e_n_tauint[e_name]) - 529 if self.tau_exp[e_name] > 0: - 530 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] - 531 x_help = np.arange(2 * self.tau_exp[e_name]) - 532 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 - 533 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) - 534 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') - 535 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], - 536 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) - 537 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 - 538 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) - 539 else: - 540 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) - 541 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) - 542 - 543 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) - 544 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') - 545 plt.legend() - 546 plt.xlim(-0.5, xmax) - 547 ylim = plt.ylim() - 548 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) - 549 plt.draw() - 550 if save: - 551 fig.savefig(save + "_" + str(e)) - 552 - 553 def plot_rho(self, save=None): - 554 """Plot normalized autocorrelation function time for each ensemble. - 555 - 556 Parameters - 557 ---------- - 558 save : str - 559 saves the figure to a file named 'save' if. - 560 """ - 561 if not hasattr(self, 'e_dvalue'): - 562 raise Exception('Run the gamma method first.') - 563 for e, e_name in enumerate(self.mc_names): - 564 fig = plt.figure() - 565 plt.xlabel('W') - 566 plt.ylabel('rho') - 567 length = len(self.e_drho[e_name]) - 568 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) - 569 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') - 570 if self.tau_exp[e_name] > 0: - 571 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], - 572 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) - 573 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 - 574 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) - 575 else: - 576 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) - 577 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) - 578 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) - 579 plt.xlim(-0.5, xmax) - 580 plt.draw() - 581 if save: - 582 fig.savefig(save + "_" + str(e)) - 583 - 584 def plot_rep_dist(self): - 585 """Plot replica distribution for each ensemble with more than one replicum.""" - 586 if not hasattr(self, 'e_dvalue'): - 587 raise Exception('Run the gamma method first.') - 588 for _e, e_name in enumerate(self.mc_names): - 589 if len(self.e_content[e_name]) == 1: - 590 print('No replica distribution for a single replicum (', e_name, ')') - 591 continue - 592 r_length = [] - 593 sub_r_mean = 0 - 594 for r_name in self.e_content[e_name]: - 595 r_length.append(len(self.deltas[r_name])) - 596 sub_r_mean += self.shape[r_name] * self.r_values[r_name] - 597 e_N = np.sum(r_length) - 598 sub_r_mean /= e_N - 599 arr = np.zeros(len(self.e_content[e_name])) - 600 for r, r_name in enumerate(self.e_content[e_name]): - 601 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) - 602 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) - 603 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') - 604 plt.draw() - 605 - 606 def plot_history(self, expand=True): - 607 """Plot derived Monte Carlo history for each ensemble - 608 - 609 Parameters - 610 ---------- - 611 expand : bool - 612 show expanded history for irregular Monte Carlo chains (default: True). - 613 """ - 614 for _e, e_name in enumerate(self.mc_names): - 615 plt.figure() - 616 r_length = [] - 617 tmp = [] - 618 tmp_expanded = [] - 619 for _r, r_name in enumerate(self.e_content[e_name]): - 620 tmp.append(self.deltas[r_name] + self.r_values[r_name]) - 621 if expand: - 622 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name], 1) + self.r_values[r_name]) - 623 r_length.append(len(tmp_expanded[-1])) - 624 else: - 625 r_length.append(len(tmp[-1])) - 626 e_N = np.sum(r_length) - 627 x = np.arange(e_N) - 628 y_test = np.concatenate(tmp, axis=0) - 629 if expand: - 630 y = np.concatenate(tmp_expanded, axis=0) - 631 else: - 632 y = y_test - 633 plt.errorbar(x, y, fmt='.', markersize=3) - 634 plt.xlim(-0.5, e_N - 0.5) - 635 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})') - 636 plt.draw() - 637 - 638 def plot_piechart(self, save=None): - 639 """Plot piechart which shows the fractional contribution of each - 640 ensemble to the error and returns a dictionary containing the fractions. - 641 - 642 Parameters - 643 ---------- - 644 save : str - 645 saves the figure to a file named 'save' if. - 646 """ - 647 if not hasattr(self, 'e_dvalue'): - 648 raise Exception('Run the gamma method first.') - 649 if np.isclose(0.0, self._dvalue, atol=1e-15): - 650 raise ValueError('Error is 0.0') - 651 labels = self.e_names - 652 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 - 653 fig1, ax1 = plt.subplots() - 654 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) - 655 ax1.axis('equal') - 656 plt.draw() - 657 if save: - 658 fig1.savefig(save) - 659 - 660 return dict(zip(labels, sizes, strict=True)) - 661 - 662 def dump(self, filename, datatype="json.gz", description="", **kwargs): - 663 """Dump the Obs to a file 'name' of chosen format. - 664 - 665 Parameters - 666 ---------- - 667 filename : str - 668 name of the file to be saved. - 669 datatype : str - 670 Format of the exported file. Supported formats include - 671 "json.gz" and "pickle" - 672 description : str - 673 Description for output file, only relevant for json.gz format. - 674 path : str - 675 specifies a custom path for the file (default '.') - 676 """ - 677 if 'path' in kwargs: - 678 file_name = kwargs.get('path') + '/' + filename - 679 else: - 680 file_name = filename - 681 - 682 if datatype == "json.gz": - 683 from .input.json import dump_to_json - 684 dump_to_json([self], file_name, description=description) - 685 elif datatype == "pickle": - 686 with open(file_name + '.p', 'wb') as fb: - 687 pickle.dump(self, fb) - 688 else: - 689 raise TypeError("Unknown datatype " + str(datatype)) - 690 - 691 def export_jackknife(self): - 692 """Export jackknife samples from the Obs - 693 - 694 Returns - 695 ------- - 696 numpy.ndarray - 697 Returns a numpy array of length N + 1 where N is the number of samples - 698 for the given ensemble and replicum. The zeroth entry of the array contains - 699 the mean value of the Obs, entries 1 to N contain the N jackknife samples - 700 derived from the Obs. The current implementation only works for observables - 701 defined on exactly one ensemble and replicum. The derived jackknife samples - 702 should agree with samples from a full jackknife analysis up to O(1/N). - 703 """ - 704 - 705 if len(self.names) != 1: - 706 raise ValueError("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") - 707 - 708 name = self.names[0] - 709 full_data = self.deltas[name] + self.r_values[name] - 710 n = full_data.size - 711 mean = self.value - 712 tmp_jacks = np.zeros(n + 1) - 713 tmp_jacks[0] = mean - 714 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) - 715 return tmp_jacks - 716 - 717 def export_bootstrap(self, samples=500, random_numbers=None, save_rng=None): - 718 """Export bootstrap samples from the Obs - 719 - 720 Parameters - 721 ---------- - 722 samples : int - 723 Number of bootstrap samples to generate. - 724 random_numbers : np.ndarray - 725 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples. - 726 If not provided the bootstrap samples are generated bashed on the md5 hash of the enesmble name. - 727 save_rng : str - 728 Save the random numbers to a file if a path is specified. - 729 - 730 Returns - 731 ------- - 732 numpy.ndarray - 733 Returns a numpy array of length N + 1 where N is the number of samples - 734 for the given ensemble and replicum. The zeroth entry of the array contains - 735 the mean value of the Obs, entries 1 to N contain the N import_bootstrap samples - 736 derived from the Obs. The current implementation only works for observables - 737 defined on exactly one ensemble and replicum. The derived bootstrap samples - 738 should agree with samples from a full bootstrap analysis up to O(1/N). - 739 """ - 740 if len(self.names) != 1: - 741 raise ValueError("'export_boostrap' is only implemented for Obs defined on one ensemble and replicum.") - 742 - 743 name = self.names[0] - 744 length = self.N - 745 - 746 if random_numbers is None: - 747 seed = int(hashlib.md5(name.encode()).hexdigest(), 16) & 0xFFFFFFFF - 748 rng = np.random.default_rng(seed) - 749 random_numbers = rng.integers(0, length, size=(samples, length)) - 750 - 751 if save_rng is not None: - 752 np.savetxt(save_rng, random_numbers, fmt='%i') - 753 - 754 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length - 755 ret = np.zeros(samples + 1) - 756 ret[0] = self.value - 757 ret[1:] = proj @ (self.deltas[name] + self.r_values[name]) - 758 return ret - 759 - 760 def __float__(self): - 761 return float(self.value) - 762 - 763 def __repr__(self): - 764 return 'Obs[' + str(self) + ']' - 765 - 766 def __str__(self): - 767 return _format_uncertainty(self.value, self._dvalue) - 768 - 769 def __format__(self, format_type): - 770 if format_type == "": - 771 significance = 2 - 772 else: - 773 significance = int(float(format_type.replace("+", "").replace("-", ""))) - 774 my_str = _format_uncertainty(self.value, self._dvalue, - 775 significance=significance) - 776 for char in ["+", " "]: - 777 if format_type.startswith(char): - 778 if my_str[0] != "-": - 779 my_str = char + my_str - 780 return my_str - 781 - 782 def __hash__(self): - 783 hash_tuple = (np.array([self.value]).astype(np.float32).data.tobytes(),) - 784 hash_tuple += tuple([o.astype(np.float32).data.tobytes() for o in self.deltas.values()]) - 785 hash_tuple += tuple([np.array([o.errsq()]).astype(np.float32).data.tobytes() for o in self.covobs.values()]) - 786 hash_tuple += tuple([o.encode() for o in self.names]) - 787 m = hashlib.md5() - 788 [m.update(o) for o in hash_tuple] - 789 return int(m.hexdigest(), 16) & 0xFFFFFFFF - 790 - 791 # Overload comparisons - 792 def __lt__(self, other): - 793 return self.value < other - 794 - 795 def __le__(self, other): - 796 return self.value <= other - 797 - 798 def __gt__(self, other): - 799 return self.value > other - 800 - 801 def __ge__(self, other): - 802 return self.value >= other - 803 - 804 def __eq__(self, other): - 805 if other is None: - 806 return False - 807 return (self - other).is_zero() - 808 - 809 # Overload math operations - 810 def __add__(self, y): - 811 if isinstance(y, Obs): - 812 return derived_observable(lambda x, **kwargs: x[0] + x[1], [self, y], man_grad=[1, 1]) - 813 else: - 814 if isinstance(y, np.ndarray): - 815 return np.array([self + o for o in y]) - 816 elif isinstance(y, complex): - 817 return CObs(self, 0) + y - 818 elif y.__class__.__name__ in ['Corr', 'CObs']: - 819 return NotImplemented - 820 else: - 821 return derived_observable(lambda x, **kwargs: x[0] + y, [self], man_grad=[1]) - 822 - 823 def __radd__(self, y): - 824 return self + y - 825 - 826 def __mul__(self, y): - 827 if isinstance(y, Obs): - 828 return derived_observable(lambda x, **kwargs: x[0] * x[1], [self, y], man_grad=[y.value, self.value]) - 829 else: - 830 if isinstance(y, np.ndarray): - 831 return np.array([self * o for o in y]) - 832 elif isinstance(y, complex): - 833 return CObs(self * y.real, self * y.imag) - 834 elif y.__class__.__name__ in ['Corr', 'CObs']: - 835 return NotImplemented - 836 else: - 837 return derived_observable(lambda x, **kwargs: x[0] * y, [self], man_grad=[y]) - 838 - 839 def __rmul__(self, y): - 840 return self * y - 841 - 842 def __sub__(self, y): - 843 if isinstance(y, Obs): - 844 return derived_observable(lambda x, **kwargs: x[0] - x[1], [self, y], man_grad=[1, -1]) - 845 else: - 846 if isinstance(y, np.ndarray): - 847 return np.array([self - o for o in y]) - 848 elif y.__class__.__name__ in ['Corr', 'CObs']: - 849 return NotImplemented - 850 else: - 851 return derived_observable(lambda x, **kwargs: x[0] - y, [self], man_grad=[1]) - 852 - 853 def __rsub__(self, y): - 854 return -1 * (self - y) - 855 - 856 def __pos__(self): - 857 return self - 858 - 859 def __neg__(self): - 860 return -1 * self - 861 - 862 def __truediv__(self, y): - 863 if isinstance(y, Obs): - 864 return derived_observable(lambda x, **kwargs: x[0] / x[1], [self, y], man_grad=[1 / y.value, - self.value / y.value ** 2]) - 865 else: - 866 if isinstance(y, np.ndarray): - 867 return np.array([self / o for o in y]) - 868 elif y.__class__.__name__ in ['Corr', 'CObs']: - 869 return NotImplemented - 870 else: - 871 return derived_observable(lambda x, **kwargs: x[0] / y, [self], man_grad=[1 / y]) - 872 - 873 def __rtruediv__(self, y): - 874 if isinstance(y, Obs): - 875 return derived_observable(lambda x, **kwargs: x[0] / x[1], [y, self], man_grad=[1 / self.value, - y.value / self.value ** 2]) - 876 else: - 877 if isinstance(y, np.ndarray): - 878 return np.array([o / self for o in y]) - 879 elif y.__class__.__name__ in ['Corr', 'CObs']: - 880 return NotImplemented - 881 else: - 882 return derived_observable(lambda x, **kwargs: y / x[0], [self], man_grad=[-y / self.value ** 2]) - 883 - 884 def __pow__(self, y): - 885 if isinstance(y, Obs): - 886 return derived_observable(lambda x, **kwargs: x[0] ** x[1], [self, y], man_grad=[y.value * self.value ** (y.value - 1), self.value ** y.value * np.log(self.value)]) - 887 else: - 888 return derived_observable(lambda x, **kwargs: x[0] ** y, [self], man_grad=[y * self.value ** (y - 1)]) - 889 - 890 def __rpow__(self, y): - 891 return derived_observable(lambda x, **kwargs: y ** x[0], [self], man_grad=[y ** self.value * np.log(y)]) - 892 - 893 def __abs__(self): - 894 return derived_observable(lambda x: anp.abs(x[0]), [self]) - 895 - 896 # Overload numpy functions - 897 def sqrt(self): - 898 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) - 899 - 900 def log(self): - 901 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) - 902 - 903 def exp(self): - 904 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) - 905 - 906 def sin(self): - 907 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) - 908 - 909 def cos(self): - 910 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) - 911 - 912 def tan(self): - 913 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) - 914 - 915 def arcsin(self): - 916 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) - 917 - 918 def arccos(self): - 919 return derived_observable(lambda x: anp.arccos(x[0]), [self]) - 920 - 921 def arctan(self): - 922 return derived_observable(lambda x: anp.arctan(x[0]), [self]) - 923 - 924 def sinh(self): - 925 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) - 926 - 927 def cosh(self): - 928 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) - 929 - 930 def tanh(self): - 931 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) - 932 - 933 def arcsinh(self): - 934 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) - 935 - 936 def arccosh(self): - 937 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) - 938 - 939 def arctanh(self): - 940 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) + 367 + 368 gm = gamma_method + 369 + 370 def _calc_gamma(self, deltas, idx, shape, w_max, fft, gapsize): + 371 """Calculate Gamma_{AA} from the deltas, which are defined on idx. + 372 idx is assumed to be a contiguous range (possibly with a stepsize != 1) + 373 + 374 Parameters + 375 ---------- + 376 deltas : list + 377 List of fluctuations + 378 idx : list + 379 List or range of configurations on which the deltas are defined. + 380 shape : int + 381 Number of configurations in idx. + 382 w_max : int + 383 Upper bound for the summation window. + 384 fft : bool + 385 determines whether the fft algorithm is used for the computation + 386 of the autocorrelation function. + 387 gapsize : int + 388 The target distance between two configurations. If longer distances + 389 are found in idx, the data is expanded. + 390 """ + 391 gamma = np.zeros(w_max) + 392 deltas = _expand_deltas(deltas, idx, shape, gapsize) + 393 new_shape = len(deltas) + 394 if fft: + 395 max_gamma = min(new_shape, w_max) + 396 # The padding for the fft has to be even + 397 padding = new_shape + max_gamma + (new_shape + max_gamma) % 2 + 398 gamma[:max_gamma] += np.fft.irfft(np.abs(np.fft.rfft(deltas, padding)) ** 2)[:max_gamma] + 399 else: + 400 for n in range(w_max): + 401 if new_shape - n >= 0: + 402 gamma[n] += deltas[0:new_shape - n].dot(deltas[n:new_shape]) + 403 + 404 return gamma + 405 + 406 def details(self, ens_content=True): + 407 """Output detailed properties of the Obs. + 408 + 409 Parameters + 410 ---------- + 411 ens_content : bool + 412 print details about the ensembles and replica if true. + 413 """ + 414 if self.tag is not None: + 415 print("Description:", self.tag) + 416 if not hasattr(self, 'e_dvalue'): + 417 print(f'Result\t {self.value:3.8e}') + 418 else: + 419 if self.value == 0.0: + 420 percentage = np.nan + 421 else: + 422 percentage = np.abs(self._dvalue / self.value) * 100 + 423 print(f'Result\t {self.value:3.8e} +/- {self._dvalue:3.8e} +/- {self.ddvalue:3.8e} ({percentage:3.3f}%)') + 424 if len(self.e_names) > 1: + 425 print(' Ensemble errors:') + 426 e_content = self.e_content + 427 for e_name in self.mc_names: + 428 gap = _determine_gap(self, e_content, e_name) + 429 + 430 if len(self.e_names) > 1: + 431 print('', e_name, f'\t {self.e_dvalue[e_name]:3.6e} +/- {self.e_ddvalue[e_name]:3.6e}') + 432 tau_string = " \N{GREEK SMALL LETTER TAU}_int\t " + _format_uncertainty(self.e_tauint[e_name], self.e_dtauint[e_name]) + 433 tau_string += f" in units of {gap} config" + 434 if gap > 1: + 435 tau_string += "s" + 436 if self.tau_exp[e_name] > 0: + 437 tau_string = f"{tau_string: <45}" + f'\t(\N{GREEK SMALL LETTER TAU}_exp={self.tau_exp[e_name]:3.2f}, N_\N{GREEK SMALL LETTER SIGMA}={self.N_sigma[e_name]:g})' + 438 else: + 439 tau_string = f"{tau_string: <45}" + f'\t(S={self.S[e_name]:3.2f})' + 440 print(tau_string) + 441 for e_name in self.cov_names: + 442 print('', e_name, f'\t {self.e_dvalue[e_name]:3.8e}') + 443 if ens_content is True: + 444 if len(self.e_names) == 1: + 445 print(self.N, 'samples in', len(self.e_names), 'ensemble:') + 446 else: + 447 print(self.N, 'samples in', len(self.e_names), 'ensembles:') + 448 my_string_list = [] + 449 for key, value in sorted(self.e_content.items()): + 450 if key not in self.covobs: + 451 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " + 452 if len(value) == 1: + 453 my_string += f': {self.shape[value[0]]} configurations' + 454 if isinstance(self.idl[value[0]], range): + 455 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}' + ')' + 456 else: + 457 my_string += f' (irregular range from {self.idl[value[0]][0]} to {self.idl[value[0]][-1]})' + 458 else: + 459 sublist = [] + 460 for v in value: + 461 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " + 462 my_substring += f': {self.shape[v]} configurations' + 463 if isinstance(self.idl[v], range): + 464 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}' + ')' + 465 else: + 466 my_substring += f' (irregular range from {self.idl[v][0]} to {self.idl[v][-1]})' + 467 sublist.append(my_substring) + 468 + 469 my_string += '\n' + '\n'.join(sublist) + 470 else: + 471 my_string = ' ' + "\u00B7 Covobs '" + key + "' " + 472 my_string_list.append(my_string) + 473 print('\n'.join(my_string_list)) + 474 + 475 def reweight(self, weight): + 476 """Reweight the obs with given rewighting factors. + 477 + 478 Parameters + 479 ---------- + 480 weight : Obs + 481 Reweighting factor. An Observable that has to be defined on a superset of the + 482 configurations in obs[i].idl for all i. + 483 all_configs : bool + 484 if True, the reweighted observables are normalized by the average of + 485 the reweighting factor on all configurations in weight.idl and not + 486 on the configurations in obs[i].idl. Default False. + 487 """ + 488 return reweight(weight, [self])[0] + 489 + 490 def is_zero_within_error(self, sigma=1): + 491 """Checks whether the observable is zero within 'sigma' standard errors. + 492 + 493 Parameters + 494 ---------- + 495 sigma : int + 496 Number of standard errors used for the check. + 497 + 498 Works only properly when the gamma method was run. + 499 """ + 500 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue + 501 + 502 def is_zero(self, atol=1e-10): + 503 """Checks whether the observable is zero within a given tolerance. + 504 + 505 Parameters + 506 ---------- + 507 atol : float + 508 Absolute tolerance (for details see numpy documentation). + 509 """ + 510 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()) + 511 + 512 def plot_tauint(self, save=None): + 513 """Plot integrated autocorrelation time for each ensemble. + 514 + 515 Parameters + 516 ---------- + 517 save : str + 518 saves the figure to a file named 'save' if. + 519 """ + 520 if not hasattr(self, 'e_dvalue'): + 521 raise Exception('Run the gamma method first.') + 522 + 523 for e, e_name in enumerate(self.mc_names): + 524 fig = plt.figure() + 525 plt.xlabel(r'$W$') + 526 plt.ylabel(r'$\tau_\mathrm{int}$') + 527 length = len(self.e_n_tauint[e_name]) + 528 if self.tau_exp[e_name] > 0: + 529 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] + 530 x_help = np.arange(2 * self.tau_exp[e_name]) + 531 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 + 532 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) + 533 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') + 534 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], + 535 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) + 536 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 + 537 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) + 538 else: + 539 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) + 540 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) + 541 + 542 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) + 543 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') + 544 plt.legend() + 545 plt.xlim(-0.5, xmax) + 546 ylim = plt.ylim() + 547 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) + 548 plt.draw() + 549 if save: + 550 fig.savefig(save + "_" + str(e)) + 551 + 552 def plot_rho(self, save=None): + 553 """Plot normalized autocorrelation function time for each ensemble. + 554 + 555 Parameters + 556 ---------- + 557 save : str + 558 saves the figure to a file named 'save' if. + 559 """ + 560 if not hasattr(self, 'e_dvalue'): + 561 raise Exception('Run the gamma method first.') + 562 for e, e_name in enumerate(self.mc_names): + 563 fig = plt.figure() + 564 plt.xlabel('W') + 565 plt.ylabel('rho') + 566 length = len(self.e_drho[e_name]) + 567 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) + 568 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') + 569 if self.tau_exp[e_name] > 0: + 570 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], + 571 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) + 572 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 + 573 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) + 574 else: + 575 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) + 576 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) + 577 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) + 578 plt.xlim(-0.5, xmax) + 579 plt.draw() + 580 if save: + 581 fig.savefig(save + "_" + str(e)) + 582 + 583 def plot_rep_dist(self): + 584 """Plot replica distribution for each ensemble with more than one replicum.""" + 585 if not hasattr(self, 'e_dvalue'): + 586 raise Exception('Run the gamma method first.') + 587 for _e, e_name in enumerate(self.mc_names): + 588 if len(self.e_content[e_name]) == 1: + 589 print('No replica distribution for a single replicum (', e_name, ')') + 590 continue + 591 r_length = [] + 592 sub_r_mean = 0 + 593 for r_name in self.e_content[e_name]: + 594 r_length.append(len(self.deltas[r_name])) + 595 sub_r_mean += self.shape[r_name] * self.r_values[r_name] + 596 e_N = np.sum(r_length) + 597 sub_r_mean /= e_N + 598 arr = np.zeros(len(self.e_content[e_name])) + 599 for r, r_name in enumerate(self.e_content[e_name]): + 600 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) + 601 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) + 602 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') + 603 plt.draw() + 604 + 605 def plot_history(self, expand=True): + 606 """Plot derived Monte Carlo history for each ensemble + 607 + 608 Parameters + 609 ---------- + 610 expand : bool + 611 show expanded history for irregular Monte Carlo chains (default: True). + 612 """ + 613 for _e, e_name in enumerate(self.mc_names): + 614 plt.figure() + 615 r_length = [] + 616 tmp = [] + 617 tmp_expanded = [] + 618 for _r, r_name in enumerate(self.e_content[e_name]): + 619 tmp.append(self.deltas[r_name] + self.r_values[r_name]) + 620 if expand: + 621 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name], 1) + self.r_values[r_name]) + 622 r_length.append(len(tmp_expanded[-1])) + 623 else: + 624 r_length.append(len(tmp[-1])) + 625 e_N = np.sum(r_length) + 626 x = np.arange(e_N) + 627 y_test = np.concatenate(tmp, axis=0) + 628 if expand: + 629 y = np.concatenate(tmp_expanded, axis=0) + 630 else: + 631 y = y_test + 632 plt.errorbar(x, y, fmt='.', markersize=3) + 633 plt.xlim(-0.5, e_N - 0.5) + 634 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})') + 635 plt.draw() + 636 + 637 def plot_piechart(self, save=None): + 638 """Plot piechart which shows the fractional contribution of each + 639 ensemble to the error and returns a dictionary containing the fractions. + 640 + 641 Parameters + 642 ---------- + 643 save : str + 644 saves the figure to a file named 'save' if. + 645 """ + 646 if not hasattr(self, 'e_dvalue'): + 647 raise Exception('Run the gamma method first.') + 648 if np.isclose(0.0, self._dvalue, atol=1e-15): + 649 raise ValueError('Error is 0.0') + 650 labels = self.e_names + 651 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 + 652 fig1, ax1 = plt.subplots() + 653 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) + 654 ax1.axis('equal') + 655 plt.draw() + 656 if save: + 657 fig1.savefig(save) + 658 + 659 return dict(zip(labels, sizes, strict=True)) + 660 + 661 def dump(self, filename, datatype="json.gz", description="", **kwargs): + 662 """Dump the Obs to a file 'name' of chosen format. + 663 + 664 Parameters + 665 ---------- + 666 filename : str + 667 name of the file to be saved. + 668 datatype : str + 669 Format of the exported file. Supported formats include + 670 "json.gz" and "pickle" + 671 description : str + 672 Description for output file, only relevant for json.gz format. + 673 path : str + 674 specifies a custom path for the file (default '.') + 675 """ + 676 if 'path' in kwargs: + 677 file_name = kwargs.get('path') + '/' + filename + 678 else: + 679 file_name = filename + 680 + 681 if datatype == "json.gz": + 682 from .input.json import dump_to_json + 683 dump_to_json([self], file_name, description=description) + 684 elif datatype == "pickle": + 685 with open(file_name + '.p', 'wb') as fb: + 686 pickle.dump(self, fb) + 687 else: + 688 raise TypeError("Unknown datatype " + str(datatype)) + 689 + 690 def export_jackknife(self): + 691 """Export jackknife samples from the Obs + 692 + 693 Returns + 694 ------- + 695 numpy.ndarray + 696 Returns a numpy array of length N + 1 where N is the number of samples + 697 for the given ensemble and replicum. The zeroth entry of the array contains + 698 the mean value of the Obs, entries 1 to N contain the N jackknife samples + 699 derived from the Obs. The current implementation only works for observables + 700 defined on exactly one ensemble and replicum. The derived jackknife samples + 701 should agree with samples from a full jackknife analysis up to O(1/N). + 702 """ + 703 + 704 if len(self.names) != 1: + 705 raise ValueError("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") + 706 + 707 name = self.names[0] + 708 full_data = self.deltas[name] + self.r_values[name] + 709 n = full_data.size + 710 mean = self.value + 711 tmp_jacks = np.zeros(n + 1) + 712 tmp_jacks[0] = mean + 713 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) + 714 return tmp_jacks + 715 + 716 def export_bootstrap(self, samples=500, random_numbers=None, save_rng=None): + 717 """Export bootstrap samples from the Obs + 718 + 719 Parameters + 720 ---------- + 721 samples : int + 722 Number of bootstrap samples to generate. + 723 random_numbers : np.ndarray + 724 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples. + 725 If not provided the bootstrap samples are generated bashed on the md5 hash of the enesmble name. + 726 save_rng : str + 727 Save the random numbers to a file if a path is specified. + 728 + 729 Returns + 730 ------- + 731 numpy.ndarray + 732 Returns a numpy array of length N + 1 where N is the number of samples + 733 for the given ensemble and replicum. The zeroth entry of the array contains + 734 the mean value of the Obs, entries 1 to N contain the N import_bootstrap samples + 735 derived from the Obs. The current implementation only works for observables + 736 defined on exactly one ensemble and replicum. The derived bootstrap samples + 737 should agree with samples from a full bootstrap analysis up to O(1/N). + 738 """ + 739 if len(self.names) != 1: + 740 raise ValueError("'export_boostrap' is only implemented for Obs defined on one ensemble and replicum.") + 741 + 742 name = self.names[0] + 743 length = self.N + 744 + 745 if random_numbers is None: + 746 seed = int(hashlib.md5(name.encode()).hexdigest(), 16) & 0xFFFFFFFF + 747 rng = np.random.default_rng(seed) + 748 random_numbers = rng.integers(0, length, size=(samples, length)) + 749 + 750 if save_rng is not None: + 751 np.savetxt(save_rng, random_numbers, fmt='%i') + 752 + 753 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length + 754 ret = np.zeros(samples + 1) + 755 ret[0] = self.value + 756 ret[1:] = proj @ (self.deltas[name] + self.r_values[name]) + 757 return ret + 758 + 759 def __float__(self): + 760 return float(self.value) + 761 + 762 def __repr__(self): + 763 return 'Obs[' + str(self) + ']' + 764 + 765 def __str__(self): + 766 return _format_uncertainty(self.value, self._dvalue) + 767 + 768 def __format__(self, format_type): + 769 if format_type == "": + 770 significance = 2 + 771 else: + 772 significance = int(float(format_type.replace("+", "").replace("-", ""))) + 773 my_str = _format_uncertainty(self.value, self._dvalue, + 774 significance=significance) + 775 for char in ["+", " "]: + 776 if format_type.startswith(char): + 777 if my_str[0] != "-": + 778 my_str = char + my_str + 779 return my_str + 780 + 781 def __hash__(self): + 782 hash_tuple = (np.array([self.value]).astype(np.float32).data.tobytes(),) + 783 hash_tuple += tuple([o.astype(np.float32).data.tobytes() for o in self.deltas.values()]) + 784 hash_tuple += tuple([np.array([o.errsq()]).astype(np.float32).data.tobytes() for o in self.covobs.values()]) + 785 hash_tuple += tuple([o.encode() for o in self.names]) + 786 m = hashlib.md5() + 787 [m.update(o) for o in hash_tuple] + 788 return int(m.hexdigest(), 16) & 0xFFFFFFFF + 789 + 790 # Overload comparisons + 791 def __lt__(self, other): + 792 return self.value < other + 793 + 794 def __le__(self, other): + 795 return self.value <= other + 796 + 797 def __gt__(self, other): + 798 return self.value > other + 799 + 800 def __ge__(self, other): + 801 return self.value >= other + 802 + 803 def __eq__(self, other): + 804 if other is None: + 805 return False + 806 return (self - other).is_zero() + 807 + 808 # Overload math operations + 809 def __add__(self, y): + 810 if isinstance(y, Obs): + 811 return derived_observable(lambda x, **kwargs: x[0] + x[1], [self, y], man_grad=[1, 1]) + 812 else: + 813 if isinstance(y, np.ndarray): + 814 return np.array([self + o for o in y]) + 815 elif isinstance(y, complex): + 816 return CObs(self, 0) + y + 817 elif y.__class__.__name__ in ['Corr', 'CObs']: + 818 return NotImplemented + 819 else: + 820 return derived_observable(lambda x, **kwargs: x[0] + y, [self], man_grad=[1]) + 821 + 822 def __radd__(self, y): + 823 return self + y + 824 + 825 def __mul__(self, y): + 826 if isinstance(y, Obs): + 827 return derived_observable(lambda x, **kwargs: x[0] * x[1], [self, y], man_grad=[y.value, self.value]) + 828 else: + 829 if isinstance(y, np.ndarray): + 830 return np.array([self * o for o in y]) + 831 elif isinstance(y, complex): + 832 return CObs(self * y.real, self * y.imag) + 833 elif y.__class__.__name__ in ['Corr', 'CObs']: + 834 return NotImplemented + 835 else: + 836 return derived_observable(lambda x, **kwargs: x[0] * y, [self], man_grad=[y]) + 837 + 838 def __rmul__(self, y): + 839 return self * y + 840 + 841 def __sub__(self, y): + 842 if isinstance(y, Obs): + 843 return derived_observable(lambda x, **kwargs: x[0] - x[1], [self, y], man_grad=[1, -1]) + 844 else: + 845 if isinstance(y, np.ndarray): + 846 return np.array([self - o for o in y]) + 847 elif y.__class__.__name__ in ['Corr', 'CObs']: + 848 return NotImplemented + 849 else: + 850 return derived_observable(lambda x, **kwargs: x[0] - y, [self], man_grad=[1]) + 851 + 852 def __rsub__(self, y): + 853 return -1 * (self - y) + 854 + 855 def __pos__(self): + 856 return self + 857 + 858 def __neg__(self): + 859 return -1 * self + 860 + 861 def __truediv__(self, y): + 862 if isinstance(y, Obs): + 863 return derived_observable(lambda x, **kwargs: x[0] / x[1], [self, y], man_grad=[1 / y.value, - self.value / y.value ** 2]) + 864 else: + 865 if isinstance(y, np.ndarray): + 866 return np.array([self / o for o in y]) + 867 elif y.__class__.__name__ in ['Corr', 'CObs']: + 868 return NotImplemented + 869 else: + 870 return derived_observable(lambda x, **kwargs: x[0] / y, [self], man_grad=[1 / y]) + 871 + 872 def __rtruediv__(self, y): + 873 if isinstance(y, Obs): + 874 return derived_observable(lambda x, **kwargs: x[0] / x[1], [y, self], man_grad=[1 / self.value, - y.value / self.value ** 2]) + 875 else: + 876 if isinstance(y, np.ndarray): + 877 return np.array([o / self for o in y]) + 878 elif y.__class__.__name__ in ['Corr', 'CObs']: + 879 return NotImplemented + 880 else: + 881 return derived_observable(lambda x, **kwargs: y / x[0], [self], man_grad=[-y / self.value ** 2]) + 882 + 883 def __pow__(self, y): + 884 if isinstance(y, Obs): + 885 return derived_observable(lambda x, **kwargs: x[0] ** x[1], [self, y], man_grad=[y.value * self.value ** (y.value - 1), self.value ** y.value * np.log(self.value)]) + 886 else: + 887 return derived_observable(lambda x, **kwargs: x[0] ** y, [self], man_grad=[y * self.value ** (y - 1)]) + 888 + 889 def __rpow__(self, y): + 890 return derived_observable(lambda x, **kwargs: y ** x[0], [self], man_grad=[y ** self.value * np.log(y)]) + 891 + 892 def __abs__(self): + 893 return derived_observable(lambda x: anp.abs(x[0]), [self]) + 894 + 895 # Overload numpy functions + 896 def sqrt(self): + 897 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) + 898 + 899 def log(self): + 900 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) + 901 + 902 def exp(self): + 903 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) + 904 + 905 def sin(self): + 906 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) + 907 + 908 def cos(self): + 909 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) + 910 + 911 def tan(self): + 912 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) + 913 + 914 def arcsin(self): + 915 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) + 916 + 917 def arccos(self): + 918 return derived_observable(lambda x: anp.arccos(x[0]), [self]) + 919 + 920 def arctan(self): + 921 return derived_observable(lambda x: anp.arctan(x[0]), [self]) + 922 + 923 def sinh(self): + 924 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) + 925 + 926 def cosh(self): + 927 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) + 928 + 929 def tanh(self): + 930 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) + 931 + 932 def arcsinh(self): + 933 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) + 934 + 935 def arccosh(self): + 936 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) + 937 + 938 def arctanh(self): + 939 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) + 940 941 - 942 - 943class CObs: - 944 """Class for a complex valued observable.""" - 945 __slots__ = ['_imag', '_real', 'tag'] - 946 - 947 def __init__(self, real, imag=0.0): - 948 self._real = real - 949 self._imag = imag - 950 self.tag = None - 951 - 952 @property - 953 def real(self): - 954 return self._real - 955 - 956 @property - 957 def imag(self): - 958 return self._imag - 959 - 960 def gamma_method(self, **kwargs): - 961 """Executes the gamma_method for the real and the imaginary part.""" - 962 if isinstance(self.real, Obs): - 963 self.real.gamma_method(**kwargs) - 964 if isinstance(self.imag, Obs): - 965 self.imag.gamma_method(**kwargs) - 966 - 967 def is_zero(self): - 968 """Checks whether both real and imaginary part are zero within machine precision.""" - 969 return self.real == 0.0 and self.imag == 0.0 - 970 - 971 def conjugate(self): - 972 return CObs(self.real, -self.imag) - 973 - 974 def __add__(self, other): - 975 if isinstance(other, np.ndarray): - 976 return other + self - 977 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 978 return CObs(self.real + other.real, - 979 self.imag + other.imag) - 980 else: - 981 return CObs(self.real + other, self.imag) - 982 - 983 def __radd__(self, y): - 984 return self + y - 985 - 986 def __sub__(self, other): - 987 if isinstance(other, np.ndarray): - 988 return -1 * (other - self) - 989 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 990 return CObs(self.real - other.real, self.imag - other.imag) - 991 else: - 992 return CObs(self.real - other, self.imag) - 993 - 994 def __rsub__(self, other): - 995 return -1 * (self - other) - 996 - 997 def __mul__(self, other): - 998 if isinstance(other, np.ndarray): - 999 return other * self -1000 elif hasattr(other, 'real') and hasattr(other, 'imag'): -1001 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): -1002 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], -1003 [self.real, other.real, self.imag, other.imag], -1004 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), -1005 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], -1006 [self.real, other.real, self.imag, other.imag], -1007 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) -1008 elif getattr(other, 'imag', 0) != 0: -1009 return CObs(self.real * other.real - self.imag * other.imag, -1010 self.imag * other.real + self.real * other.imag) -1011 else: -1012 return CObs(self.real * other.real, self.imag * other.real) -1013 else: -1014 return CObs(self.real * other, self.imag * other) -1015 -1016 def __rmul__(self, other): -1017 return self * other -1018 -1019 def __truediv__(self, other): -1020 if isinstance(other, np.ndarray): -1021 return 1 / (other / self) -1022 elif hasattr(other, 'real') and hasattr(other, 'imag'): -1023 r = other.real ** 2 + other.imag ** 2 -1024 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) -1025 else: -1026 return CObs(self.real / other, self.imag / other) -1027 -1028 def __rtruediv__(self, other): -1029 r = self.real ** 2 + self.imag ** 2 -1030 if hasattr(other, 'real') and hasattr(other, 'imag'): -1031 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) -1032 else: -1033 return CObs(self.real * other / r, -self.imag * other / r) -1034 -1035 def __abs__(self): -1036 return np.sqrt(self.real**2 + self.imag**2) -1037 -1038 def __pos__(self): -1039 return self -1040 -1041 def __neg__(self): -1042 return -1 * self -1043 -1044 def __eq__(self, other): -1045 return self.real == other.real and self.imag == other.imag -1046 -1047 __hash__ = None -1048 -1049 def __str__(self): -1050 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' -1051 -1052 def __repr__(self): -1053 return 'CObs[' + str(self) + ']' -1054 -1055 def __format__(self, format_type): -1056 if format_type == "": -1057 significance = 2 -1058 format_type = "2" -1059 else: -1060 significance = int(float(format_type.replace("+", "").replace("-", ""))) -1061 return f"({self.real:{format_type}}{self.imag:+{significance}}j)" + 942class CObs: + 943 """Class for a complex valued observable.""" + 944 __slots__ = ['_imag', '_real', 'tag'] + 945 + 946 def __init__(self, real, imag=0.0): + 947 self._real = real + 948 self._imag = imag + 949 self.tag = None + 950 + 951 @property + 952 def real(self): + 953 return self._real + 954 + 955 @property + 956 def imag(self): + 957 return self._imag + 958 + 959 def gamma_method(self, **kwargs): + 960 """Executes the gamma_method for the real and the imaginary part.""" + 961 if isinstance(self.real, Obs): + 962 self.real.gamma_method(**kwargs) + 963 if isinstance(self.imag, Obs): + 964 self.imag.gamma_method(**kwargs) + 965 + 966 def is_zero(self): + 967 """Checks whether both real and imaginary part are zero within machine precision.""" + 968 return self.real == 0.0 and self.imag == 0.0 + 969 + 970 def conjugate(self): + 971 return CObs(self.real, -self.imag) + 972 + 973 def __add__(self, other): + 974 if isinstance(other, np.ndarray): + 975 return other + self + 976 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 977 return CObs(self.real + other.real, + 978 self.imag + other.imag) + 979 else: + 980 return CObs(self.real + other, self.imag) + 981 + 982 def __radd__(self, y): + 983 return self + y + 984 + 985 def __sub__(self, other): + 986 if isinstance(other, np.ndarray): + 987 return -1 * (other - self) + 988 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 989 return CObs(self.real - other.real, self.imag - other.imag) + 990 else: + 991 return CObs(self.real - other, self.imag) + 992 + 993 def __rsub__(self, other): + 994 return -1 * (self - other) + 995 + 996 def __mul__(self, other): + 997 if isinstance(other, np.ndarray): + 998 return other * self + 999 elif hasattr(other, 'real') and hasattr(other, 'imag'): +1000 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): +1001 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], +1002 [self.real, other.real, self.imag, other.imag], +1003 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), +1004 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], +1005 [self.real, other.real, self.imag, other.imag], +1006 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) +1007 elif getattr(other, 'imag', 0) != 0: +1008 return CObs(self.real * other.real - self.imag * other.imag, +1009 self.imag * other.real + self.real * other.imag) +1010 else: +1011 return CObs(self.real * other.real, self.imag * other.real) +1012 else: +1013 return CObs(self.real * other, self.imag * other) +1014 +1015 def __rmul__(self, other): +1016 return self * other +1017 +1018 def __truediv__(self, other): +1019 if isinstance(other, np.ndarray): +1020 return 1 / (other / self) +1021 elif hasattr(other, 'real') and hasattr(other, 'imag'): +1022 r = other.real ** 2 + other.imag ** 2 +1023 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) +1024 else: +1025 return CObs(self.real / other, self.imag / other) +1026 +1027 def __rtruediv__(self, other): +1028 r = self.real ** 2 + self.imag ** 2 +1029 if hasattr(other, 'real') and hasattr(other, 'imag'): +1030 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) +1031 else: +1032 return CObs(self.real * other / r, -self.imag * other / r) +1033 +1034 def __abs__(self): +1035 return np.sqrt(self.real**2 + self.imag**2) +1036 +1037 def __pos__(self): +1038 return self +1039 +1040 def __neg__(self): +1041 return -1 * self +1042 +1043 def __eq__(self, other): +1044 return self.real == other.real and self.imag == other.imag +1045 +1046 __hash__ = None +1047 +1048 def __str__(self): +1049 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' +1050 +1051 def __repr__(self): +1052 return 'CObs[' + str(self) + ']' +1053 +1054 def __format__(self, format_type): +1055 if format_type == "": +1056 significance = 2 +1057 format_type = "2" +1058 else: +1059 significance = int(float(format_type.replace("+", "").replace("-", ""))) +1060 return f"({self.real:{format_type}}{self.imag:+{significance}}j)" +1061 1062 -1063 -1064def gamma_method(x, **kwargs): -1065 """Vectorized version of the gamma_method applicable to lists or arrays of Obs. -1066 -1067 See docstring of pe.Obs.gamma_method for details. -1068 """ -1069 return np.vectorize(lambda o: o.gm(**kwargs))(x) +1063def gamma_method(x, **kwargs): +1064 """Vectorized version of the gamma_method applicable to lists or arrays of Obs. +1065 +1066 See docstring of pe.Obs.gamma_method for details. +1067 """ +1068 return np.vectorize(lambda o: o.gm(**kwargs))(x) +1069 1070 -1071 -1072gm = gamma_method +1071gm = gamma_method +1072 1073 -1074 -1075def _format_uncertainty(value, dvalue, significance=2): -1076 """Creates a string of a value and its error in paranthesis notation, e.g., 13.02(45)""" -1077 if dvalue == 0.0 or (not np.isfinite(dvalue)): -1078 return str(value) -1079 if not isinstance(significance, int): -1080 raise TypeError("significance needs to be an integer.") -1081 if significance < 1: -1082 raise ValueError("significance needs to be larger than zero.") -1083 fexp = np.floor(np.log10(dvalue)) -1084 if fexp < 0.0: -1085 return '{:{form}}({:1.0f})'.format(value, dvalue * 10 ** (-fexp + significance - 1), form='.' + str(-int(fexp) + significance - 1) + 'f') -1086 elif fexp == 0.0: -1087 return f"{value:.{significance - 1}f}({dvalue:1.{significance - 1}f})" -1088 else: -1089 return f"{value:.{max(0, int(significance - fexp - 1))}f}({dvalue:2.{max(0, int(significance - fexp - 1))}f})" +1074def _format_uncertainty(value, dvalue, significance=2): +1075 """Creates a string of a value and its error in paranthesis notation, e.g., 13.02(45)""" +1076 if dvalue == 0.0 or (not np.isfinite(dvalue)): +1077 return str(value) +1078 if not isinstance(significance, int): +1079 raise TypeError("significance needs to be an integer.") +1080 if significance < 1: +1081 raise ValueError("significance needs to be larger than zero.") +1082 fexp = np.floor(np.log10(dvalue)) +1083 if fexp < 0.0: +1084 return '{:{form}}({:1.0f})'.format(value, dvalue * 10 ** (-fexp + significance - 1), form='.' + str(-int(fexp) + significance - 1) + 'f') +1085 elif fexp == 0.0: +1086 return f"{value:.{significance - 1}f}({dvalue:1.{significance - 1}f})" +1087 else: +1088 return f"{value:.{max(0, int(significance - fexp - 1))}f}({dvalue:2.{max(0, int(significance - fexp - 1))}f})" +1089 1090 -1091 -1092def _expand_deltas(deltas, idx, shape, gapsize): -1093 """Expand deltas defined on idx to a regular range with spacing gapsize between two -1094 configurations and where holes are filled by 0. -1095 If idx is of type range, the deltas are not changed if the idx.step == gapsize. -1096 -1097 Parameters -1098 ---------- -1099 deltas : list -1100 List of fluctuations -1101 idx : list -1102 List or range of configs on which the deltas are defined, has to be sorted in ascending order. -1103 shape : int -1104 Number of configs in idx. -1105 gapsize : int -1106 The target distance between two configurations. If longer distances -1107 are found in idx, the data is expanded. -1108 """ -1109 if isinstance(idx, range): -1110 if (idx.step == gapsize): -1111 return deltas -1112 ret = np.zeros((idx[-1] - idx[0] + gapsize) // gapsize) -1113 for i in range(shape): -1114 ret[(idx[i] - idx[0]) // gapsize] = deltas[i] -1115 return ret +1091def _expand_deltas(deltas, idx, shape, gapsize): +1092 """Expand deltas defined on idx to a regular range with spacing gapsize between two +1093 configurations and where holes are filled by 0. +1094 If idx is of type range, the deltas are not changed if the idx.step == gapsize. +1095 +1096 Parameters +1097 ---------- +1098 deltas : list +1099 List of fluctuations +1100 idx : list +1101 List or range of configs on which the deltas are defined, has to be sorted in ascending order. +1102 shape : int +1103 Number of configs in idx. +1104 gapsize : int +1105 The target distance between two configurations. If longer distances +1106 are found in idx, the data is expanded. +1107 """ +1108 if isinstance(idx, range): +1109 if (idx.step == gapsize): +1110 return deltas +1111 ret = np.zeros((idx[-1] - idx[0] + gapsize) // gapsize) +1112 for i in range(shape): +1113 ret[(idx[i] - idx[0]) // gapsize] = deltas[i] +1114 return ret +1115 1116 -1117 -1118def _merge_idx(idl): -1119 """Returns the union of all lists in idl as range or sorted list -1120 -1121 Parameters -1122 ---------- -1123 idl : list -1124 List of lists or ranges. -1125 """ -1126 -1127 if _check_lists_equal(idl): -1128 return idl[0] -1129 -1130 idunion = sorted(set().union(*idl)) -1131 -1132 # Check whether idunion can be expressed as range -1133 idrange = range(idunion[0], idunion[-1] + 1, idunion[1] - idunion[0]) -1134 idtest = [list(idrange), idunion] -1135 if _check_lists_equal(idtest): -1136 return idrange -1137 -1138 return idunion +1117def _merge_idx(idl): +1118 """Returns the union of all lists in idl as range or sorted list +1119 +1120 Parameters +1121 ---------- +1122 idl : list +1123 List of lists or ranges. +1124 """ +1125 +1126 if _check_lists_equal(idl): +1127 return idl[0] +1128 +1129 idunion = sorted(set().union(*idl)) +1130 +1131 # Check whether idunion can be expressed as range +1132 idrange = range(idunion[0], idunion[-1] + 1, idunion[1] - idunion[0]) +1133 idtest = [list(idrange), idunion] +1134 if _check_lists_equal(idtest): +1135 return idrange +1136 +1137 return idunion +1138 1139 -1140 -1141def _intersection_idx(idl): -1142 """Returns the intersection of all lists in idl as range or sorted list -1143 -1144 Parameters -1145 ---------- -1146 idl : list -1147 List of lists or ranges. -1148 """ -1149 -1150 if _check_lists_equal(idl): -1151 return idl[0] -1152 -1153 idinter = sorted(set.intersection(*[set(o) for o in idl])) -1154 -1155 # Check whether idinter can be expressed as range -1156 try: -1157 idrange = range(idinter[0], idinter[-1] + 1, idinter[1] - idinter[0]) -1158 idtest = [list(idrange), idinter] -1159 if _check_lists_equal(idtest): -1160 return idrange -1161 except IndexError: -1162 pass -1163 -1164 return idinter +1140def _intersection_idx(idl): +1141 """Returns the intersection of all lists in idl as range or sorted list +1142 +1143 Parameters +1144 ---------- +1145 idl : list +1146 List of lists or ranges. +1147 """ +1148 +1149 if _check_lists_equal(idl): +1150 return idl[0] +1151 +1152 idinter = sorted(set.intersection(*[set(o) for o in idl])) +1153 +1154 # Check whether idinter can be expressed as range +1155 try: +1156 idrange = range(idinter[0], idinter[-1] + 1, idinter[1] - idinter[0]) +1157 idtest = [list(idrange), idinter] +1158 if _check_lists_equal(idtest): +1159 return idrange +1160 except IndexError: +1161 pass +1162 +1163 return idinter +1164 1165 -1166 -1167def _expand_deltas_for_merge(deltas, idx, shape, new_idx, scalefactor): -1168 """Expand deltas defined on idx to the list of configs that is defined by new_idx. -1169 New, empty entries are filled by 0. If idx and new_idx are of type range, the smallest -1170 common divisor of the step sizes is used as new step size. -1171 -1172 Parameters -1173 ---------- -1174 deltas : list -1175 List of fluctuations -1176 idx : list -1177 List or range of configs on which the deltas are defined. -1178 Has to be a subset of new_idx and has to be sorted in ascending order. -1179 shape : list -1180 Number of configs in idx. -1181 new_idx : list -1182 List of configs that defines the new range, has to be sorted in ascending order. -1183 scalefactor : float -1184 An additional scaling factor that can be applied to scale the fluctuations, -1185 e.g., when Obs with differing numbers of replica are merged. -1186 """ -1187 if type(idx) is range and type(new_idx) is range: -1188 if idx == new_idx: -1189 if scalefactor == 1: -1190 return deltas -1191 else: -1192 return deltas * scalefactor -1193 ret = np.zeros(new_idx[-1] - new_idx[0] + 1) -1194 for i in range(shape): -1195 ret[idx[i] - new_idx[0]] = deltas[i] -1196 return np.array([ret[new_idx[i] - new_idx[0]] for i in range(len(new_idx))]) * len(new_idx) / len(idx) * scalefactor +1166def _expand_deltas_for_merge(deltas, idx, shape, new_idx, scalefactor): +1167 """Expand deltas defined on idx to the list of configs that is defined by new_idx. +1168 New, empty entries are filled by 0. If idx and new_idx are of type range, the smallest +1169 common divisor of the step sizes is used as new step size. +1170 +1171 Parameters +1172 ---------- +1173 deltas : list +1174 List of fluctuations +1175 idx : list +1176 List or range of configs on which the deltas are defined. +1177 Has to be a subset of new_idx and has to be sorted in ascending order. +1178 shape : list +1179 Number of configs in idx. +1180 new_idx : list +1181 List of configs that defines the new range, has to be sorted in ascending order. +1182 scalefactor : float +1183 An additional scaling factor that can be applied to scale the fluctuations, +1184 e.g., when Obs with differing numbers of replica are merged. +1185 """ +1186 if type(idx) is range and type(new_idx) is range: +1187 if idx == new_idx: +1188 if scalefactor == 1: +1189 return deltas +1190 else: +1191 return deltas * scalefactor +1192 ret = np.zeros(new_idx[-1] - new_idx[0] + 1) +1193 for i in range(shape): +1194 ret[idx[i] - new_idx[0]] = deltas[i] +1195 return np.array([ret[new_idx[i] - new_idx[0]] for i in range(len(new_idx))]) * len(new_idx) / len(idx) * scalefactor +1196 1197 -1198 -1199def derived_observable(func, data, array_mode=False, **kwargs): -1200 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. -1201 -1202 Parameters -1203 ---------- -1204 func : object -1205 arbitrary function of the form func(data, **kwargs). For the -1206 automatic differentiation to work, all numpy functions have to have -1207 the autograd wrapper (use 'import autograd.numpy as anp'). -1208 data : list -1209 list of Obs, e.g. [obs1, obs2, obs3]. -1210 num_grad : bool -1211 if True, numerical derivatives are used instead of autograd -1212 (default False). To control the numerical differentiation the -1213 kwargs of numdifftools.step_generators.MaxStepGenerator -1214 can be used. -1215 man_grad : list -1216 manually supply a list or an array which contains the jacobian -1217 of func. Use cautiously, supplying the wrong derivative will -1218 not be intercepted. -1219 -1220 Notes -1221 ----- -1222 For simple mathematical operations it can be practical to use anonymous -1223 functions. For the ratio of two observables one can e.g. use -1224 -1225 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) -1226 """ -1227 -1228 data = np.asarray(data) -1229 raveled_data = data.ravel() -1230 -1231 # Workaround for matrix operations containing non Obs data -1232 if not all(isinstance(x, Obs) for x in raveled_data): -1233 for i in range(len(raveled_data)): -1234 if isinstance(raveled_data[i], (int, float)): -1235 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") -1236 -1237 allcov = {} -1238 for o in raveled_data: -1239 for name in o.cov_names: -1240 if name in allcov: -1241 if not np.allclose(allcov[name], o.covobs[name].cov): -1242 raise Exception(f'Inconsistent covariance matrices for {name}!') -1243 else: -1244 allcov[name] = o.covobs[name].cov -1245 -1246 n_obs = len(raveled_data) -1247 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) -1248 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) -1249 new_sample_names = sorted(set(new_names) - set(new_cov_names)) -1250 -1251 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 -1252 -1253 if data.ndim == 1: -1254 values = np.array([o.value for o in data]) -1255 else: -1256 values = np.vectorize(lambda x: x.value)(data) -1257 -1258 new_values = func(values, **kwargs) -1259 -1260 multi = int(isinstance(new_values, np.ndarray)) -1261 -1262 new_r_values = {} -1263 new_idl_d = {} -1264 for name in new_sample_names: -1265 idl = [] -1266 tmp_values = np.zeros(n_obs) -1267 for i, item in enumerate(raveled_data): -1268 tmp_values[i] = item.r_values.get(name, item.value) -1269 tmp_idl = item.idl.get(name) -1270 if tmp_idl is not None: -1271 idl.append(tmp_idl) -1272 if multi > 0: -1273 tmp_values = np.array(tmp_values).reshape(data.shape) -1274 new_r_values[name] = func(tmp_values, **kwargs) -1275 new_idl_d[name] = _merge_idx(idl) -1276 -1277 def _compute_scalefactor_missing_rep(obs): -1278 """ -1279 Computes the scale factor that is to be multiplied with the deltas -1280 in the case where Obs with different subsets of replica are merged. -1281 Returns a dictionary with the scale factor for each Monte Carlo name. -1282 -1283 Parameters -1284 ---------- -1285 obs : Obs -1286 The observable corresponding to the deltas that are to be scaled -1287 """ -1288 scalef_d = {} -1289 for mc_name in obs.mc_names: -1290 mc_idl_d = [name for name in obs.idl if name.startswith(mc_name + '|')] -1291 new_mc_idl_d = [name for name in new_idl_d if name.startswith(mc_name + '|')] -1292 if len(mc_idl_d) > 0 and len(mc_idl_d) < len(new_mc_idl_d): -1293 scalef_d[mc_name] = sum([len(new_idl_d[name]) for name in new_mc_idl_d]) / sum([len(new_idl_d[name]) for name in mc_idl_d]) -1294 return scalef_d -1295 -1296 if 'man_grad' in kwargs: -1297 deriv = np.asarray(kwargs.get('man_grad')) -1298 if new_values.shape + data.shape != deriv.shape: -1299 raise ValueError('Manual derivative does not have correct shape.') -1300 elif kwargs.get('num_grad') is True: -1301 if multi > 0: -1302 raise NotImplementedError('Multi mode currently not supported for numerical derivative') -1303 options = { -1304 'base_step': 0.1, -1305 'step_ratio': 2.5} -1306 for key in options.keys(): -1307 kwarg = kwargs.get(key) -1308 if kwarg is not None: -1309 options[key] = kwarg -1310 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) -1311 if tmp_df.size == 1: -1312 deriv = np.array([tmp_df.real]) -1313 else: -1314 deriv = tmp_df.real -1315 else: -1316 deriv = jacobian(func)(values, **kwargs) -1317 -1318 final_result = np.zeros(new_values.shape, dtype=object) -1319 -1320 if array_mode is True: -1321 -1322 class _Zero_grad: -1323 def __init__(self, N): -1324 self.grad = np.zeros((N, 1)) -1325 -1326 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])) -1327 d_extracted = {} -1328 g_extracted = {} -1329 for name in new_sample_names: -1330 d_extracted[name] = [] -1331 ens_length = len(new_idl_d[name]) -1332 for dat in data: -1333 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], _compute_scalefactor_missing_rep(o).get(name.split('|')[0], 1)) for o in dat.reshape(np.prod(dat.shape))]).reshape((*dat.shape, ens_length))) -1334 for name in new_cov_names: -1335 g_extracted[name] = [] -1336 zero_grad = _Zero_grad(new_covobs_lengths[name]) -1337 for dat in data: -1338 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))) -1339 -1340 for i_val, new_val in np.ndenumerate(new_values): -1341 new_deltas = {} -1342 new_grad = {} -1343 if array_mode is True: -1344 for name in new_sample_names: -1345 ens_length = d_extracted[name][0].shape[-1] -1346 new_deltas[name] = np.zeros(ens_length) -1347 for i_dat, dat in enumerate(d_extracted[name]): -1348 new_deltas[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) -1349 for name in new_cov_names: -1350 new_grad[name] = 0 -1351 for i_dat, dat in enumerate(g_extracted[name]): -1352 new_grad[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) -1353 else: -1354 for j_obs, obs in np.ndenumerate(data): -1355 scalef_d = _compute_scalefactor_missing_rep(obs) -1356 for name in obs.names: -1357 if name in obs.cov_names: -1358 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad -1359 else: -1360 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], scalef_d.get(name.split('|')[0], 1)) -1361 -1362 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} -1363 -1364 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): -1365 raise ValueError('The same name has been used for deltas and covobs!') -1366 new_samples = [] -1367 new_means = [] -1368 new_idl = [] -1369 new_names_obs = [] -1370 for name in new_names: -1371 if name not in new_covobs: -1372 new_samples.append(new_deltas[name]) -1373 new_idl.append(new_idl_d[name]) -1374 new_means.append(new_r_values[name][i_val]) -1375 new_names_obs.append(name) -1376 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) -1377 for name in new_covobs: -1378 final_result[i_val].names.append(name) -1379 final_result[i_val]._covobs = new_covobs -1380 final_result[i_val]._value = new_val -1381 final_result[i_val].reweighted = reweighted -1382 -1383 if multi == 0: -1384 final_result = final_result.item() -1385 -1386 return final_result +1198def derived_observable(func, data, array_mode=False, **kwargs): +1199 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. +1200 +1201 Parameters +1202 ---------- +1203 func : object +1204 arbitrary function of the form func(data, **kwargs). For the +1205 automatic differentiation to work, all numpy functions have to have +1206 the autograd wrapper (use 'import autograd.numpy as anp'). +1207 data : list +1208 list of Obs, e.g. [obs1, obs2, obs3]. +1209 num_grad : bool +1210 if True, numerical derivatives are used instead of autograd +1211 (default False). To control the numerical differentiation the +1212 kwargs of numdifftools.step_generators.MaxStepGenerator +1213 can be used. +1214 man_grad : list +1215 manually supply a list or an array which contains the jacobian +1216 of func. Use cautiously, supplying the wrong derivative will +1217 not be intercepted. +1218 +1219 Notes +1220 ----- +1221 For simple mathematical operations it can be practical to use anonymous +1222 functions. For the ratio of two observables one can e.g. use +1223 +1224 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) +1225 """ +1226 +1227 data = np.asarray(data) +1228 raveled_data = data.ravel() +1229 +1230 # Workaround for matrix operations containing non Obs data +1231 if not all(isinstance(x, Obs) for x in raveled_data): +1232 for i in range(len(raveled_data)): +1233 if isinstance(raveled_data[i], (int, float)): +1234 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") +1235 +1236 allcov = {} +1237 for o in raveled_data: +1238 for name in o.cov_names: +1239 if name in allcov: +1240 if not np.allclose(allcov[name], o.covobs[name].cov): +1241 raise Exception(f'Inconsistent covariance matrices for {name}!') +1242 else: +1243 allcov[name] = o.covobs[name].cov +1244 +1245 n_obs = len(raveled_data) +1246 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) +1247 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) +1248 new_sample_names = sorted(set(new_names) - set(new_cov_names)) +1249 +1250 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 +1251 +1252 if data.ndim == 1: +1253 values = np.array([o.value for o in data]) +1254 else: +1255 values = np.vectorize(lambda x: x.value)(data) +1256 +1257 new_values = func(values, **kwargs) +1258 +1259 multi = int(isinstance(new_values, np.ndarray)) +1260 +1261 new_r_values = {} +1262 new_idl_d = {} +1263 for name in new_sample_names: +1264 idl = [] +1265 tmp_values = np.zeros(n_obs) +1266 for i, item in enumerate(raveled_data): +1267 tmp_values[i] = item.r_values.get(name, item.value) +1268 tmp_idl = item.idl.get(name) +1269 if tmp_idl is not None: +1270 idl.append(tmp_idl) +1271 if multi > 0: +1272 tmp_values = np.array(tmp_values).reshape(data.shape) +1273 new_r_values[name] = func(tmp_values, **kwargs) +1274 new_idl_d[name] = _merge_idx(idl) +1275 +1276 def _compute_scalefactor_missing_rep(obs): +1277 """ +1278 Computes the scale factor that is to be multiplied with the deltas +1279 in the case where Obs with different subsets of replica are merged. +1280 Returns a dictionary with the scale factor for each Monte Carlo name. +1281 +1282 Parameters +1283 ---------- +1284 obs : Obs +1285 The observable corresponding to the deltas that are to be scaled +1286 """ +1287 scalef_d = {} +1288 for mc_name in obs.mc_names: +1289 mc_idl_d = [name for name in obs.idl if name.startswith(mc_name + '|')] +1290 new_mc_idl_d = [name for name in new_idl_d if name.startswith(mc_name + '|')] +1291 if len(mc_idl_d) > 0 and len(mc_idl_d) < len(new_mc_idl_d): +1292 scalef_d[mc_name] = sum([len(new_idl_d[name]) for name in new_mc_idl_d]) / sum([len(new_idl_d[name]) for name in mc_idl_d]) +1293 return scalef_d +1294 +1295 if 'man_grad' in kwargs: +1296 deriv = np.asarray(kwargs.get('man_grad')) +1297 if new_values.shape + data.shape != deriv.shape: +1298 raise ValueError('Manual derivative does not have correct shape.') +1299 elif kwargs.get('num_grad') is True: +1300 if multi > 0: +1301 raise NotImplementedError('Multi mode currently not supported for numerical derivative') +1302 options = { +1303 'base_step': 0.1, +1304 'step_ratio': 2.5} +1305 for key in options: +1306 kwarg = kwargs.get(key) +1307 if kwarg is not None: +1308 options[key] = kwarg +1309 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) +1310 if tmp_df.size == 1: +1311 deriv = np.array([tmp_df.real]) +1312 else: +1313 deriv = tmp_df.real +1314 else: +1315 deriv = jacobian(func)(values, **kwargs) +1316 +1317 final_result = np.zeros(new_values.shape, dtype=object) +1318 +1319 if array_mode is True: +1320 +1321 class _Zero_grad: +1322 def __init__(self, N): +1323 self.grad = np.zeros((N, 1)) +1324 +1325 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])) +1326 d_extracted = {} +1327 g_extracted = {} +1328 for name in new_sample_names: +1329 d_extracted[name] = [] +1330 ens_length = len(new_idl_d[name]) +1331 for dat in data: +1332 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], _compute_scalefactor_missing_rep(o).get(name.split('|')[0], 1)) for o in dat.reshape(np.prod(dat.shape))]).reshape((*dat.shape, ens_length))) +1333 for name in new_cov_names: +1334 g_extracted[name] = [] +1335 zero_grad = _Zero_grad(new_covobs_lengths[name]) +1336 for dat in data: +1337 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))) +1338 +1339 for i_val, new_val in np.ndenumerate(new_values): +1340 new_deltas = {} +1341 new_grad = {} +1342 if array_mode is True: +1343 for name in new_sample_names: +1344 ens_length = d_extracted[name][0].shape[-1] +1345 new_deltas[name] = np.zeros(ens_length) +1346 for i_dat, dat in enumerate(d_extracted[name]): +1347 new_deltas[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) +1348 for name in new_cov_names: +1349 new_grad[name] = 0 +1350 for i_dat, dat in enumerate(g_extracted[name]): +1351 new_grad[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) +1352 else: +1353 for j_obs, obs in np.ndenumerate(data): +1354 scalef_d = _compute_scalefactor_missing_rep(obs) +1355 for name in obs.names: +1356 if name in obs.cov_names: +1357 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad +1358 else: +1359 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], scalef_d.get(name.split('|')[0], 1)) +1360 +1361 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} +1362 +1363 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): +1364 raise ValueError('The same name has been used for deltas and covobs!') +1365 new_samples = [] +1366 new_means = [] +1367 new_idl = [] +1368 new_names_obs = [] +1369 for name in new_names: +1370 if name not in new_covobs: +1371 new_samples.append(new_deltas[name]) +1372 new_idl.append(new_idl_d[name]) +1373 new_means.append(new_r_values[name][i_val]) +1374 new_names_obs.append(name) +1375 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) +1376 for name in new_covobs: +1377 final_result[i_val].names.append(name) +1378 final_result[i_val]._covobs = new_covobs +1379 final_result[i_val]._value = new_val +1380 final_result[i_val].reweighted = reweighted +1381 +1382 if multi == 0: +1383 final_result = final_result.item() +1384 +1385 return final_result +1386 1387 -1388 -1389def _reduce_deltas(deltas, idx_old, idx_new): -1390 """Extract deltas defined on idx_old on all configs of idx_new. -1391 -1392 Assumes, that idx_old and idx_new are correctly defined idl, i.e., they -1393 are ordered in an ascending order. -1394 -1395 Parameters -1396 ---------- -1397 deltas : list -1398 List of fluctuations -1399 idx_old : list -1400 List or range of configs on which the deltas are defined -1401 idx_new : list -1402 List of configs for which we want to extract the deltas. -1403 Has to be a subset of idx_old. -1404 """ -1405 if not len(deltas) == len(idx_old): -1406 raise ValueError(f'Length of deltas and idx_old have to be the same: {len(deltas)} != {len(idx_old)}') -1407 if type(idx_old) is range and type(idx_new) is range: -1408 if idx_old == idx_new: -1409 return deltas -1410 if _check_lists_equal([idx_old, idx_new]): -1411 return deltas -1412 indices = np.intersect1d(idx_old, idx_new, assume_unique=True, return_indices=True)[1] -1413 if len(indices) < len(idx_new): -1414 raise ValueError('Error in _reduce_deltas: Config of idx_new not in idx_old') -1415 return np.array(deltas)[indices] +1388def _reduce_deltas(deltas, idx_old, idx_new): +1389 """Extract deltas defined on idx_old on all configs of idx_new. +1390 +1391 Assumes, that idx_old and idx_new are correctly defined idl, i.e., they +1392 are ordered in an ascending order. +1393 +1394 Parameters +1395 ---------- +1396 deltas : list +1397 List of fluctuations +1398 idx_old : list +1399 List or range of configs on which the deltas are defined +1400 idx_new : list +1401 List of configs for which we want to extract the deltas. +1402 Has to be a subset of idx_old. +1403 """ +1404 if not len(deltas) == len(idx_old): +1405 raise ValueError(f'Length of deltas and idx_old have to be the same: {len(deltas)} != {len(idx_old)}') +1406 if type(idx_old) is range and type(idx_new) is range: +1407 if idx_old == idx_new: +1408 return deltas +1409 if _check_lists_equal([idx_old, idx_new]): +1410 return deltas +1411 indices = np.intersect1d(idx_old, idx_new, assume_unique=True, return_indices=True)[1] +1412 if len(indices) < len(idx_new): +1413 raise ValueError('Error in _reduce_deltas: Config of idx_new not in idx_old') +1414 return np.array(deltas)[indices] +1415 1416 -1417 -1418def reweight(weight, obs, **kwargs): -1419 """Reweight a list of observables. -1420 -1421 Parameters -1422 ---------- -1423 weight : Obs -1424 Reweighting factor. An Observable that has to be defined on a superset of the -1425 configurations in obs[i].idl for all i. -1426 obs : list -1427 list of Obs, e.g. [obs1, obs2, obs3]. -1428 all_configs : bool -1429 if True, the reweighted observables are normalized by the average of -1430 the reweighting factor on all configurations in weight.idl and not -1431 on the configurations in obs[i].idl. Default False. -1432 """ -1433 result = [] -1434 for i in range(len(obs)): -1435 if len(obs[i].cov_names): -1436 raise ValueError('Error: Not possible to reweight an Obs that contains covobs!') -1437 if not set(obs[i].names).issubset(weight.names): -1438 raise ValueError('Error: Ensembles do not fit') -1439 if len(obs[i].mc_names) > 1 or len(weight.mc_names) > 1: -1440 raise ValueError('Error: Cannot reweight an Obs that contains multiple ensembles.') -1441 for name in obs[i].names: -1442 if not set(obs[i].idl[name]).issubset(weight.idl[name]): -1443 raise ValueError(f'obs[{i}] has to be defined on a subset of the configs in weight.idl[{name}]!') -1444 new_samples = [] -1445 w_deltas = {} -1446 for name in sorted(obs[i].names): -1447 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) -1448 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) -1449 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) -1450 -1451 if kwargs.get('all_configs'): -1452 new_weight = weight -1453 else: -1454 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)]) -1455 -1456 result.append(tmp_obs / new_weight) -1457 result[-1].reweighted = True -1458 -1459 return result +1417def reweight(weight, obs, **kwargs): +1418 """Reweight a list of observables. +1419 +1420 Parameters +1421 ---------- +1422 weight : Obs +1423 Reweighting factor. An Observable that has to be defined on a superset of the +1424 configurations in obs[i].idl for all i. +1425 obs : list +1426 list of Obs, e.g. [obs1, obs2, obs3]. +1427 all_configs : bool +1428 if True, the reweighted observables are normalized by the average of +1429 the reweighting factor on all configurations in weight.idl and not +1430 on the configurations in obs[i].idl. Default False. +1431 """ +1432 result = [] +1433 for i in range(len(obs)): +1434 if len(obs[i].cov_names): +1435 raise ValueError('Error: Not possible to reweight an Obs that contains covobs!') +1436 if not set(obs[i].names).issubset(weight.names): +1437 raise ValueError('Error: Ensembles do not fit') +1438 if len(obs[i].mc_names) > 1 or len(weight.mc_names) > 1: +1439 raise ValueError('Error: Cannot reweight an Obs that contains multiple ensembles.') +1440 for name in obs[i].names: +1441 if not set(obs[i].idl[name]).issubset(weight.idl[name]): +1442 raise ValueError(f'obs[{i}] has to be defined on a subset of the configs in weight.idl[{name}]!') +1443 new_samples = [] +1444 w_deltas = {} +1445 for name in sorted(obs[i].names): +1446 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) +1447 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) +1448 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) +1449 +1450 if kwargs.get('all_configs'): +1451 new_weight = weight +1452 else: +1453 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)]) +1454 +1455 result.append(tmp_obs / new_weight) +1456 result[-1].reweighted = True +1457 +1458 return result +1459 1460 -1461 -1462def correlate(obs_a, obs_b): -1463 """Correlate two observables. -1464 -1465 Parameters -1466 ---------- -1467 obs_a : Obs -1468 First observable -1469 obs_b : Obs -1470 Second observable -1471 -1472 Notes -1473 ----- -1474 Keep in mind to only correlate primary observables which have not been reweighted -1475 yet. The reweighting has to be applied after correlating the observables. -1476 Only works if a single ensemble is present in the Obs. -1477 Currently only works if ensemble content is identical (this is not strictly necessary). -1478 """ -1479 -1480 if len(obs_a.mc_names) > 1 or len(obs_b.mc_names) > 1: -1481 raise ValueError('Error: Cannot correlate Obs that contain multiple ensembles.') -1482 if sorted(obs_a.names) != sorted(obs_b.names): -1483 raise ValueError(f"Ensembles do not fit {set(sorted(obs_a.names)) ^ set(sorted(obs_b.names))}") -1484 if len(obs_a.cov_names) or len(obs_b.cov_names): -1485 raise ValueError('Error: Not possible to correlate Obs that contain covobs!') -1486 for name in obs_a.names: -1487 if obs_a.shape[name] != obs_b.shape[name]: -1488 raise ValueError('Shapes of ensemble', name, 'do not fit') -1489 if obs_a.idl[name] != obs_b.idl[name]: -1490 raise ValueError('idl of ensemble', name, 'do not fit') -1491 -1492 if obs_a.reweighted is True: -1493 warnings.warn("The first observable is already reweighted.", RuntimeWarning, stacklevel=2) -1494 if obs_b.reweighted is True: -1495 warnings.warn("The second observable is already reweighted.", RuntimeWarning, stacklevel=2) -1496 -1497 new_samples = [] -1498 new_idl = [] -1499 for name in sorted(obs_a.names): -1500 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) -1501 new_idl.append(obs_a.idl[name]) -1502 -1503 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) -1504 o.reweighted = obs_a.reweighted or obs_b.reweighted -1505 return o +1461def correlate(obs_a, obs_b): +1462 """Correlate two observables. +1463 +1464 Parameters +1465 ---------- +1466 obs_a : Obs +1467 First observable +1468 obs_b : Obs +1469 Second observable +1470 +1471 Notes +1472 ----- +1473 Keep in mind to only correlate primary observables which have not been reweighted +1474 yet. The reweighting has to be applied after correlating the observables. +1475 Only works if a single ensemble is present in the Obs. +1476 Currently only works if ensemble content is identical (this is not strictly necessary). +1477 """ +1478 +1479 if len(obs_a.mc_names) > 1 or len(obs_b.mc_names) > 1: +1480 raise ValueError('Error: Cannot correlate Obs that contain multiple ensembles.') +1481 if sorted(obs_a.names) != sorted(obs_b.names): +1482 raise ValueError(f"Ensembles do not fit {set(sorted(obs_a.names)) ^ set(sorted(obs_b.names))}") +1483 if len(obs_a.cov_names) or len(obs_b.cov_names): +1484 raise ValueError('Error: Not possible to correlate Obs that contain covobs!') +1485 for name in obs_a.names: +1486 if obs_a.shape[name] != obs_b.shape[name]: +1487 raise ValueError('Shapes of ensemble', name, 'do not fit') +1488 if obs_a.idl[name] != obs_b.idl[name]: +1489 raise ValueError('idl of ensemble', name, 'do not fit') +1490 +1491 if obs_a.reweighted is True: +1492 warnings.warn("The first observable is already reweighted.", RuntimeWarning, stacklevel=2) +1493 if obs_b.reweighted is True: +1494 warnings.warn("The second observable is already reweighted.", RuntimeWarning, stacklevel=2) +1495 +1496 new_samples = [] +1497 new_idl = [] +1498 for name in sorted(obs_a.names): +1499 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) +1500 new_idl.append(obs_a.idl[name]) +1501 +1502 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) +1503 o.reweighted = obs_a.reweighted or obs_b.reweighted +1504 return o +1505 1506 -1507 -1508def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): -1509 r'''Calculates the error covariance matrix of a set of observables. -1510 -1511 WARNING: This function should be used with care, especially for observables with support on multiple -1512 ensembles with differing autocorrelations. See the notes below for details. -1513 -1514 The gamma method has to be applied first to all observables. -1515 -1516 Parameters -1517 ---------- -1518 obs : list or numpy.ndarray -1519 List or one dimensional array of Obs -1520 visualize : bool -1521 If True plots the corresponding normalized correlation matrix (default False). -1522 correlation : bool -1523 If True the correlation matrix instead of the error covariance matrix is returned (default False). -1524 smooth : None or int -1525 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue -1526 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the -1527 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely -1528 small ones. -1529 -1530 Notes -1531 ----- -1532 The error covariance is defined such that it agrees with the squared standard error for two identical observables -1533 $$\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$$ -1534 in the absence of autocorrelation. -1535 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 -1536 $$\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. -1537 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. -1538 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ -1539 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). -1540 ''' -1541 -1542 length = len(obs) -1543 -1544 max_samples = np.max([o.N for o in obs]) -1545 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: -1546 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, stacklevel=2) -1547 -1548 cov = np.zeros((length, length)) -1549 for i in range(length): -1550 for j in range(i, length): -1551 cov[i, j] = _covariance_element(obs[i], obs[j]) -1552 cov = cov + cov.T - np.diag(np.diag(cov)) -1553 -1554 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) -1555 -1556 if isinstance(smooth, int): -1557 corr = _smooth_eigenvalues(corr, smooth) -1558 -1559 if visualize: -1560 plt.matshow(corr, vmin=-1, vmax=1) -1561 plt.set_cmap('RdBu') -1562 plt.colorbar() -1563 plt.draw() -1564 -1565 if correlation is True: -1566 return corr -1567 -1568 errors = [o.dvalue for o in obs] -1569 cov = np.diag(errors) @ corr @ np.diag(errors) -1570 -1571 eigenvalues = np.linalg.eigh(cov)[0] -1572 if not np.all(eigenvalues >= 0): -1573 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning, stacklevel=2) -1574 -1575 return cov +1507def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): +1508 r'''Calculates the error covariance matrix of a set of observables. +1509 +1510 WARNING: This function should be used with care, especially for observables with support on multiple +1511 ensembles with differing autocorrelations. See the notes below for details. +1512 +1513 The gamma method has to be applied first to all observables. +1514 +1515 Parameters +1516 ---------- +1517 obs : list or numpy.ndarray +1518 List or one dimensional array of Obs +1519 visualize : bool +1520 If True plots the corresponding normalized correlation matrix (default False). +1521 correlation : bool +1522 If True the correlation matrix instead of the error covariance matrix is returned (default False). +1523 smooth : None or int +1524 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue +1525 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the +1526 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely +1527 small ones. +1528 +1529 Notes +1530 ----- +1531 The error covariance is defined such that it agrees with the squared standard error for two identical observables +1532 $$\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$$ +1533 in the absence of autocorrelation. +1534 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 +1535 $$\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. +1536 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. +1537 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ +1538 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). +1539 ''' +1540 +1541 length = len(obs) +1542 +1543 max_samples = np.max([o.N for o in obs]) +1544 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: +1545 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, stacklevel=2) +1546 +1547 cov = np.zeros((length, length)) +1548 for i in range(length): +1549 for j in range(i, length): +1550 cov[i, j] = _covariance_element(obs[i], obs[j]) +1551 cov = cov + cov.T - np.diag(np.diag(cov)) +1552 +1553 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) +1554 +1555 if isinstance(smooth, int): +1556 corr = _smooth_eigenvalues(corr, smooth) +1557 +1558 if visualize: +1559 plt.matshow(corr, vmin=-1, vmax=1) +1560 plt.set_cmap('RdBu') +1561 plt.colorbar() +1562 plt.draw() +1563 +1564 if correlation is True: +1565 return corr +1566 +1567 errors = [o.dvalue for o in obs] +1568 cov = np.diag(errors) @ corr @ np.diag(errors) +1569 +1570 eigenvalues = np.linalg.eigh(cov)[0] +1571 if not np.all(eigenvalues >= 0): +1572 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning, stacklevel=2) +1573 +1574 return cov +1575 1576 -1577 -1578def invert_corr_cov_cholesky(corr, inverrdiag): -1579 """Constructs a lower triangular matrix `chol` via the Cholesky decomposition of the correlation matrix `corr` -1580 and then returns the inverse covariance matrix `chol_inv` as a lower triangular matrix by solving `chol * x = inverrdiag`. -1581 -1582 Parameters -1583 ---------- -1584 corr : np.ndarray -1585 correlation matrix -1586 inverrdiag : np.ndarray -1587 diagonal matrix, the entries are the inverse errors of the data points considered -1588 """ -1589 -1590 condn = np.linalg.cond(corr) -1591 if condn > 0.1 / np.finfo(float).eps: -1592 raise ValueError(f"Cannot invert correlation matrix as its condition number exceeds machine precision ({condn:1.2e})") -1593 if condn > 1e13: -1594 warnings.warn(f"Correlation matrix may be ill-conditioned, condition number: {{{condn:1.2e}}}", RuntimeWarning, stacklevel=2) -1595 chol = np.linalg.cholesky(corr) -1596 chol_inv = scipy.linalg.solve_triangular(chol, inverrdiag, lower=True) -1597 -1598 return chol_inv +1577def invert_corr_cov_cholesky(corr, inverrdiag): +1578 """Constructs a lower triangular matrix `chol` via the Cholesky decomposition of the correlation matrix `corr` +1579 and then returns the inverse covariance matrix `chol_inv` as a lower triangular matrix by solving `chol * x = inverrdiag`. +1580 +1581 Parameters +1582 ---------- +1583 corr : np.ndarray +1584 correlation matrix +1585 inverrdiag : np.ndarray +1586 diagonal matrix, the entries are the inverse errors of the data points considered +1587 """ +1588 +1589 condn = np.linalg.cond(corr) +1590 if condn > 0.1 / np.finfo(float).eps: +1591 raise ValueError(f"Cannot invert correlation matrix as its condition number exceeds machine precision ({condn:1.2e})") +1592 if condn > 1e13: +1593 warnings.warn(f"Correlation matrix may be ill-conditioned, condition number: {{{condn:1.2e}}}", RuntimeWarning, stacklevel=2) +1594 chol = np.linalg.cholesky(corr) +1595 chol_inv = scipy.linalg.solve_triangular(chol, inverrdiag, lower=True) +1596 +1597 return chol_inv +1598 1599 -1600 -1601def sort_corr(corr, kl, yd): -1602 """ Reorders a correlation matrix to match the alphabetical order of its underlying y data. -1603 -1604 The ordering of the input correlation matrix `corr` is given by the list of keys `kl`. -1605 The input dictionary `yd` (with the same keys `kl`) must contain the corresponding y data -1606 that the correlation matrix is based on. -1607 This function sorts the list of keys `kl` alphabetically and sorts the matrix `corr` -1608 according to this alphabetical order such that the sorted matrix `corr_sorted` corresponds -1609 to the y data `yd` when arranged in an alphabetical order by its keys. -1610 -1611 Parameters -1612 ---------- -1613 corr : np.ndarray -1614 A square correlation matrix constructed using the order of the y data specified by `kl`. -1615 The dimensions of `corr` should match the total number of y data points in `yd` combined. -1616 kl : list of str -1617 A list of keys that denotes the order in which the y data from `yd` was used to build the -1618 input correlation matrix `corr`. -1619 yd : dict of list -1620 A dictionary where each key corresponds to a unique identifier, and its value is a list of -1621 y data points. The total number of y data points across all keys must match the dimensions -1622 of `corr`. The lists in the dictionary can be lists of Obs. -1623 -1624 Returns -1625 ------- -1626 np.ndarray -1627 A new, sorted correlation matrix that corresponds to the y data from `yd` when arranged alphabetically by its keys. -1628 -1629 Example -1630 ------- -1631 >>> import numpy as np -1632 >>> import pyerrors as pe -1633 >>> corr = np.array([[1, 0.2, 0.3], [0.2, 1, 0.4], [0.3, 0.4, 1]]) -1634 >>> kl = ['b', 'a'] -1635 >>> yd = {'a': [1, 2], 'b': [3]} -1636 >>> sorted_corr = pe.obs.sort_corr(corr, kl, yd) -1637 >>> print(sorted_corr) -1638 array([[1. , 0.3, 0.4], -1639 [0.3, 1. , 0.2], -1640 [0.4, 0.2, 1. ]]) -1641 -1642 """ -1643 kl_sorted = sorted(kl) -1644 -1645 posd = {} -1646 ofs = 0 -1647 for _ki, k in enumerate(kl): -1648 posd[k] = [i + ofs for i in range(len(yd[k]))] -1649 ofs += len(posd[k]) -1650 -1651 mapping = [] -1652 for k in kl_sorted: -1653 for i in range(len(yd[k])): -1654 mapping.append(posd[k][i]) -1655 -1656 corr_sorted = np.zeros_like(corr) -1657 for i in range(corr.shape[0]): -1658 for j in range(corr.shape[0]): -1659 corr_sorted[i][j] = corr[mapping[i]][mapping[j]] -1660 -1661 return corr_sorted +1600def sort_corr(corr, kl, yd): +1601 """ Reorders a correlation matrix to match the alphabetical order of its underlying y data. +1602 +1603 The ordering of the input correlation matrix `corr` is given by the list of keys `kl`. +1604 The input dictionary `yd` (with the same keys `kl`) must contain the corresponding y data +1605 that the correlation matrix is based on. +1606 This function sorts the list of keys `kl` alphabetically and sorts the matrix `corr` +1607 according to this alphabetical order such that the sorted matrix `corr_sorted` corresponds +1608 to the y data `yd` when arranged in an alphabetical order by its keys. +1609 +1610 Parameters +1611 ---------- +1612 corr : np.ndarray +1613 A square correlation matrix constructed using the order of the y data specified by `kl`. +1614 The dimensions of `corr` should match the total number of y data points in `yd` combined. +1615 kl : list of str +1616 A list of keys that denotes the order in which the y data from `yd` was used to build the +1617 input correlation matrix `corr`. +1618 yd : dict of list +1619 A dictionary where each key corresponds to a unique identifier, and its value is a list of +1620 y data points. The total number of y data points across all keys must match the dimensions +1621 of `corr`. The lists in the dictionary can be lists of Obs. +1622 +1623 Returns +1624 ------- +1625 np.ndarray +1626 A new, sorted correlation matrix that corresponds to the y data from `yd` when arranged alphabetically by its keys. +1627 +1628 Example +1629 ------- +1630 >>> import numpy as np +1631 >>> import pyerrors as pe +1632 >>> corr = np.array([[1, 0.2, 0.3], [0.2, 1, 0.4], [0.3, 0.4, 1]]) +1633 >>> kl = ['b', 'a'] +1634 >>> yd = {'a': [1, 2], 'b': [3]} +1635 >>> sorted_corr = pe.obs.sort_corr(corr, kl, yd) +1636 >>> print(sorted_corr) +1637 array([[1. , 0.3, 0.4], +1638 [0.3, 1. , 0.2], +1639 [0.4, 0.2, 1. ]]) +1640 +1641 """ +1642 kl_sorted = sorted(kl) +1643 +1644 posd = {} +1645 ofs = 0 +1646 for _ki, k in enumerate(kl): +1647 posd[k] = [i + ofs for i in range(len(yd[k]))] +1648 ofs += len(posd[k]) +1649 +1650 mapping = [] +1651 for k in kl_sorted: +1652 for i in range(len(yd[k])): +1653 mapping.append(posd[k][i]) +1654 +1655 corr_sorted = np.zeros_like(corr) +1656 for i in range(corr.shape[0]): +1657 for j in range(corr.shape[0]): +1658 corr_sorted[i][j] = corr[mapping[i]][mapping[j]] +1659 +1660 return corr_sorted +1661 1662 -1663 -1664def _smooth_eigenvalues(corr, E): -1665 """Eigenvalue smoothing as described in hep-lat/9412087 -1666 -1667 corr : np.ndarray -1668 correlation matrix -1669 E : integer -1670 Number of eigenvalues to be left substantially unchanged -1671 """ -1672 if not (2 < E < corr.shape[0] - 1): -1673 raise ValueError(f"'E' has to be between 2 and the dimension of the correlation matrix minus 1 ({corr.shape[0] - 1}).") -1674 vals, vec = np.linalg.eigh(corr) -1675 lambda_min = np.mean(vals[:-E]) -1676 vals[vals < lambda_min] = lambda_min -1677 vals /= np.mean(vals) -1678 return vec @ np.diag(vals) @ vec.T +1663def _smooth_eigenvalues(corr, E): +1664 """Eigenvalue smoothing as described in hep-lat/9412087 +1665 +1666 corr : np.ndarray +1667 correlation matrix +1668 E : integer +1669 Number of eigenvalues to be left substantially unchanged +1670 """ +1671 if not (2 < E < corr.shape[0] - 1): +1672 raise ValueError(f"'E' has to be between 2 and the dimension of the correlation matrix minus 1 ({corr.shape[0] - 1}).") +1673 vals, vec = np.linalg.eigh(corr) +1674 lambda_min = np.mean(vals[:-E]) +1675 vals[vals < lambda_min] = lambda_min +1676 vals /= np.mean(vals) +1677 return vec @ np.diag(vals) @ vec.T +1678 1679 -1680 -1681def _covariance_element(obs1, obs2): -1682 """Estimates the covariance of two Obs objects, neglecting autocorrelations.""" -1683 -1684 def calc_gamma(deltas1, deltas2, idx1, idx2, new_idx): -1685 deltas1 = _reduce_deltas(deltas1, idx1, new_idx) -1686 deltas2 = _reduce_deltas(deltas2, idx2, new_idx) -1687 return np.sum(deltas1 * deltas2) -1688 -1689 if set(obs1.names).isdisjoint(set(obs2.names)): -1690 return 0.0 -1691 -1692 if not hasattr(obs1, 'e_dvalue') or not hasattr(obs2, 'e_dvalue'): -1693 raise Exception('The gamma method has to be applied to both Obs first.') -1694 -1695 dvalue = 0.0 -1696 -1697 for e_name in obs1.mc_names: -1698 -1699 if e_name not in obs2.mc_names: -1700 continue -1701 -1702 idl_d = {} -1703 for r_name in obs1.e_content[e_name]: -1704 if r_name not in obs2.e_content[e_name]: -1705 continue -1706 idl_d[r_name] = _intersection_idx([obs1.idl[r_name], obs2.idl[r_name]]) -1707 -1708 gamma = 0.0 -1709 -1710 for r_name in obs1.e_content[e_name]: -1711 if r_name not in obs2.e_content[e_name]: -1712 continue -1713 if len(idl_d[r_name]) == 0: -1714 continue -1715 gamma += calc_gamma(obs1.deltas[r_name], obs2.deltas[r_name], obs1.idl[r_name], obs2.idl[r_name], idl_d[r_name]) -1716 -1717 if gamma == 0.0: -1718 continue -1719 -1720 gamma_div = 0.0 -1721 for r_name in obs1.e_content[e_name]: -1722 if r_name not in obs2.e_content[e_name]: -1723 continue -1724 if len(idl_d[r_name]) == 0: -1725 continue -1726 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])) -1727 gamma /= gamma_div -1728 -1729 dvalue += gamma -1730 -1731 for e_name in obs1.cov_names: -1732 -1733 if e_name not in obs2.cov_names: -1734 continue -1735 -1736 dvalue += np.dot(np.transpose(obs1.covobs[e_name].grad), np.dot(obs1.covobs[e_name].cov, obs2.covobs[e_name].grad)).item() -1737 -1738 return dvalue +1680def _covariance_element(obs1, obs2): +1681 """Estimates the covariance of two Obs objects, neglecting autocorrelations.""" +1682 +1683 def calc_gamma(deltas1, deltas2, idx1, idx2, new_idx): +1684 deltas1 = _reduce_deltas(deltas1, idx1, new_idx) +1685 deltas2 = _reduce_deltas(deltas2, idx2, new_idx) +1686 return np.sum(deltas1 * deltas2) +1687 +1688 if set(obs1.names).isdisjoint(set(obs2.names)): +1689 return 0.0 +1690 +1691 if not hasattr(obs1, 'e_dvalue') or not hasattr(obs2, 'e_dvalue'): +1692 raise Exception('The gamma method has to be applied to both Obs first.') +1693 +1694 dvalue = 0.0 +1695 +1696 for e_name in obs1.mc_names: +1697 +1698 if e_name not in obs2.mc_names: +1699 continue +1700 +1701 idl_d = {} +1702 for r_name in obs1.e_content[e_name]: +1703 if r_name not in obs2.e_content[e_name]: +1704 continue +1705 idl_d[r_name] = _intersection_idx([obs1.idl[r_name], obs2.idl[r_name]]) +1706 +1707 gamma = 0.0 +1708 +1709 for r_name in obs1.e_content[e_name]: +1710 if r_name not in obs2.e_content[e_name]: +1711 continue +1712 if len(idl_d[r_name]) == 0: +1713 continue +1714 gamma += calc_gamma(obs1.deltas[r_name], obs2.deltas[r_name], obs1.idl[r_name], obs2.idl[r_name], idl_d[r_name]) +1715 +1716 if gamma == 0.0: +1717 continue +1718 +1719 gamma_div = 0.0 +1720 for r_name in obs1.e_content[e_name]: +1721 if r_name not in obs2.e_content[e_name]: +1722 continue +1723 if len(idl_d[r_name]) == 0: +1724 continue +1725 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])) +1726 gamma /= gamma_div +1727 +1728 dvalue += gamma +1729 +1730 for e_name in obs1.cov_names: +1731 +1732 if e_name not in obs2.cov_names: +1733 continue +1734 +1735 dvalue += np.dot(np.transpose(obs1.covobs[e_name].grad), np.dot(obs1.covobs[e_name].cov, obs2.covobs[e_name].grad)).item() +1736 +1737 return dvalue +1738 1739 -1740 -1741def import_jackknife(jacks, name, idl=None): -1742 """Imports jackknife samples and returns an Obs -1743 -1744 Parameters -1745 ---------- -1746 jacks : numpy.ndarray -1747 numpy array containing the mean value as zeroth entry and -1748 the N jackknife samples as first to Nth entry. -1749 name : str -1750 name of the ensemble the samples are defined on. -1751 """ -1752 length = len(jacks) - 1 -1753 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) -1754 samples = jacks[1:] @ prj -1755 mean = np.mean(samples) -1756 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) -1757 new_obs._value = jacks[0] -1758 return new_obs +1740def import_jackknife(jacks, name, idl=None): +1741 """Imports jackknife samples and returns an Obs +1742 +1743 Parameters +1744 ---------- +1745 jacks : numpy.ndarray +1746 numpy array containing the mean value as zeroth entry and +1747 the N jackknife samples as first to Nth entry. +1748 name : str +1749 name of the ensemble the samples are defined on. +1750 """ +1751 length = len(jacks) - 1 +1752 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) +1753 samples = jacks[1:] @ prj +1754 mean = np.mean(samples) +1755 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) +1756 new_obs._value = jacks[0] +1757 return new_obs +1758 1759 -1760 -1761def import_bootstrap(boots, name, random_numbers): -1762 """Imports bootstrap samples and returns an Obs -1763 -1764 Parameters -1765 ---------- -1766 boots : numpy.ndarray -1767 numpy array containing the mean value as zeroth entry and -1768 the N bootstrap samples as first to Nth entry. -1769 name : str -1770 name of the ensemble the samples are defined on. -1771 random_numbers : np.ndarray -1772 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples, -1773 where samples is the number of bootstrap samples and length is the length of the original Monte Carlo -1774 chain to be reconstructed. -1775 """ -1776 samples, length = random_numbers.shape -1777 if samples != len(boots) - 1: -1778 raise ValueError("Random numbers do not have the correct shape.") -1779 -1780 if samples < length: -1781 raise ValueError("Obs can't be reconstructed if there are fewer bootstrap samples than Monte Carlo data points.") -1782 -1783 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length -1784 -1785 samples = scipy.linalg.lstsq(proj, boots[1:])[0] -1786 ret = Obs([samples], [name]) -1787 ret._value = boots[0] -1788 return ret +1760def import_bootstrap(boots, name, random_numbers): +1761 """Imports bootstrap samples and returns an Obs +1762 +1763 Parameters +1764 ---------- +1765 boots : numpy.ndarray +1766 numpy array containing the mean value as zeroth entry and +1767 the N bootstrap samples as first to Nth entry. +1768 name : str +1769 name of the ensemble the samples are defined on. +1770 random_numbers : np.ndarray +1771 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples, +1772 where samples is the number of bootstrap samples and length is the length of the original Monte Carlo +1773 chain to be reconstructed. +1774 """ +1775 samples, length = random_numbers.shape +1776 if samples != len(boots) - 1: +1777 raise ValueError("Random numbers do not have the correct shape.") +1778 +1779 if samples < length: +1780 raise ValueError("Obs can't be reconstructed if there are fewer bootstrap samples than Monte Carlo data points.") +1781 +1782 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length +1783 +1784 samples = scipy.linalg.lstsq(proj, boots[1:])[0] +1785 ret = Obs([samples], [name]) +1786 ret._value = boots[0] +1787 return ret +1788 1789 -1790 -1791def merge_obs(list_of_obs): -1792 """Combine all observables in list_of_obs into one new observable. -1793 This allows to merge Obs that have been computed on multiple replica -1794 of the same ensemble. -1795 If you like to merge Obs that are based on several ensembles, please -1796 average them yourself. -1797 -1798 Parameters -1799 ---------- -1800 list_of_obs : list -1801 list of the Obs object to be combined -1802 -1803 Notes -1804 ----- -1805 It is not possible to combine obs which are based on the same replicum -1806 """ -1807 replist = [item for obs in list_of_obs for item in obs.names] -1808 if (len(replist) == len(set(replist))) is False: -1809 raise ValueError(f'list_of_obs contains duplicate replica: {replist!s}') -1810 if any([len(o.cov_names) for o in list_of_obs]): -1811 raise ValueError('Not possible to merge data that contains covobs!') -1812 new_dict = {} -1813 idl_dict = {} -1814 for o in list_of_obs: -1815 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) -1816 for key in set(o.deltas) | set(o.r_values)}) -1817 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) -1818 -1819 names = sorted(new_dict.keys()) -1820 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) -1821 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) -1822 return o +1790def merge_obs(list_of_obs): +1791 """Combine all observables in list_of_obs into one new observable. +1792 This allows to merge Obs that have been computed on multiple replica +1793 of the same ensemble. +1794 If you like to merge Obs that are based on several ensembles, please +1795 average them yourself. +1796 +1797 Parameters +1798 ---------- +1799 list_of_obs : list +1800 list of the Obs object to be combined +1801 +1802 Notes +1803 ----- +1804 It is not possible to combine obs which are based on the same replicum +1805 """ +1806 replist = [item for obs in list_of_obs for item in obs.names] +1807 if (len(replist) == len(set(replist))) is False: +1808 raise ValueError(f'list_of_obs contains duplicate replica: {replist!s}') +1809 if any([len(o.cov_names) for o in list_of_obs]): +1810 raise ValueError('Not possible to merge data that contains covobs!') +1811 new_dict = {} +1812 idl_dict = {} +1813 for o in list_of_obs: +1814 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) +1815 for key in set(o.deltas) | set(o.r_values)}) +1816 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) +1817 +1818 names = sorted(new_dict.keys()) +1819 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) +1820 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) +1821 return o +1822 1823 -1824 -1825def cov_Obs(means, cov, name, grad=None): -1826 """Create an Obs based on mean(s) and a covariance matrix -1827 -1828 Parameters -1829 ---------- -1830 mean : list of floats or float -1831 N mean value(s) of the new Obs -1832 cov : list or array -1833 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance -1834 name : str -1835 identifier for the covariance matrix -1836 grad : list or array -1837 Gradient of the Covobs wrt. the means belonging to cov. -1838 """ -1839 -1840 def covobs_to_obs(co): -1841 """Make an Obs out of a Covobs -1842 -1843 Parameters -1844 ---------- -1845 co : Covobs -1846 Covobs to be embedded into the Obs -1847 """ -1848 o = Obs([], [], means=[]) -1849 o._value = co.value -1850 o.names.append(co.name) -1851 o._covobs[co.name] = co -1852 o._dvalue = np.sqrt(co.errsq()) -1853 return o -1854 -1855 ol = [] -1856 if isinstance(means, (float, int)): -1857 means = [means] -1858 -1859 for i in range(len(means)): -1860 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) -1861 if ol[0].covobs[name].N != len(means): -1862 raise ValueError(f'You have to provide {ol[0].N} mean values!') -1863 if len(ol) == 1: -1864 return ol[0] -1865 return ol +1824def cov_Obs(means, cov, name, grad=None): +1825 """Create an Obs based on mean(s) and a covariance matrix +1826 +1827 Parameters +1828 ---------- +1829 mean : list of floats or float +1830 N mean value(s) of the new Obs +1831 cov : list or array +1832 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance +1833 name : str +1834 identifier for the covariance matrix +1835 grad : list or array +1836 Gradient of the Covobs wrt. the means belonging to cov. +1837 """ +1838 +1839 def covobs_to_obs(co): +1840 """Make an Obs out of a Covobs +1841 +1842 Parameters +1843 ---------- +1844 co : Covobs +1845 Covobs to be embedded into the Obs +1846 """ +1847 o = Obs([], [], means=[]) +1848 o._value = co.value +1849 o.names.append(co.name) +1850 o._covobs[co.name] = co +1851 o._dvalue = np.sqrt(co.errsq()) +1852 return o +1853 +1854 ol = [] +1855 if isinstance(means, (float, int)): +1856 means = [means] +1857 +1858 for i in range(len(means)): +1859 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) +1860 if ol[0].covobs[name].N != len(means): +1861 raise ValueError(f'You have to provide {ol[0].N} mean values!') +1862 if len(ol) == 1: +1863 return ol[0] +1864 return ol +1865 1866 -1867 -1868def _determine_gap(o, e_content, e_name): -1869 gaps = [] -1870 for r_name in e_content[e_name]: -1871 if isinstance(o.idl[r_name], range): -1872 gaps.append(o.idl[r_name].step) -1873 else: -1874 gaps.append(np.min(np.diff(o.idl[r_name]))) -1875 -1876 gap = min(gaps) -1877 if not np.all([gi % gap == 0 for gi in gaps]): -1878 raise ValueError(f"Replica for ensemble {e_name} do not have a common spacing.", gaps) -1879 -1880 return gap +1867def _determine_gap(o, e_content, e_name): +1868 gaps = [] +1869 for r_name in e_content[e_name]: +1870 if isinstance(o.idl[r_name], range): +1871 gaps.append(o.idl[r_name].step) +1872 else: +1873 gaps.append(np.min(np.diff(o.idl[r_name]))) +1874 +1875 gap = min(gaps) +1876 if not np.all([gi % gap == 0 for gi in gaps]): +1877 raise ValueError(f"Replica for ensemble {e_name} do not have a common spacing.", gaps) +1878 +1879 return gap +1880 1881 -1882 -1883def _check_lists_equal(idl): -1884 ''' -1885 Use groupby to efficiently check whether all elements of idl are identical. -1886 Returns True if all elements are equal, otherwise False. -1887 -1888 Parameters -1889 ---------- -1890 idl : list of lists, ranges or np.ndarrays -1891 ''' -1892 g = groupby([np.nditer(el) if isinstance(el, np.ndarray) else el for el in idl]) -1893 if next(g, True) and not next(g, False): -1894 return True -1895 return False +1882def _check_lists_equal(idl): +1883 ''' +1884 Use groupby to efficiently check whether all elements of idl are identical. +1885 Returns True if all elements are equal, otherwise False. +1886 +1887 Parameters +1888 ---------- +1889 idl : list of lists, ranges or np.ndarrays +1890 ''' +1891 g = groupby([np.nditer(el) if isinstance(el, np.ndarray) else el for el in idl]) +1892 if next(g, True) and not next(g, False): +1893 return True +1894 return False1363 def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None): +1364 r''' Project large correlation matrix to lowest states +1365 +1366 This method can be used to reduce the size of an (N x N) correlation matrix +1367 to (Ntrunc x Ntrunc) by solving a GEVP at very early times where the noise +1368 is still small. +1369 +1370 Parameters +1371 ---------- +1372 Ntrunc: int +1373 Rank of the target matrix. +1374 tproj: int +1375 Time where the eigenvectors are evaluated, corresponds to ts in the GEVP method. +1376 The default value is 3. +1377 t0proj: int +1378 Time where the correlation matrix is inverted. Choosing t0proj=1 is strongly +1379 discouraged for O(a) improved theories, since the correctness of the procedure +1380 cannot be granted in this case. The default value is 2. +1381 basematrix : Corr +1382 Correlation matrix that is used to determine the eigenvectors of the +1383 lowest states based on a GEVP. basematrix is taken to be the Corr itself if +1384 is is not specified. +1385 +1386 Notes +1387 ----- +1388 We have the basematrix $C(t)$ and the target matrix $G(t)$. We start by solving +1389 the GEVP $$C(t) v_n(t, t_0) = \lambda_n(t, t_0) C(t_0) v_n(t, t_0)$$ where $t \equiv t_\mathrm{proj}$ +1390 and $t_0 \equiv t_{0, \mathrm{proj}}$. The target matrix is projected onto the subspace of the +1391 resulting eigenvectors $v_n, n=1,\dots,N_\mathrm{trunc}$ via +1392 $$G^\prime_{i, j}(t) = (v_i, G(t) v_j)$$. This allows to reduce the size of a large +1393 correlation matrix and to remove some noise that is added by irrelevant operators. +1394 This may allow to use the GEVP on $G(t)$ at late times such that the theoretically motivated +1395 bound $t_0 \leq t/2$ holds, since the condition number of $G(t)$ is decreased, compared to $C(t)$. +1396 ''' +1397 +1398 if self.N == 1: +1399 raise ValueError('Method cannot be applied to one-dimensional correlators.') +1400 if basematrix is None: +1401 basematrix = self +1402 if Ntrunc >= basematrix.N: +1403 raise ValueError(f'Cannot truncate using Ntrunc >= {basematrix.N}') +1404 if basematrix.N != self.N: +1405 raise ValueError('basematrix and targetmatrix have to be of the same size.') +1406 +1407 evecs = basematrix.GEVP(t0proj, tproj, sort=None)[:Ntrunc] +1408 +1409 tmpmat = np.empty((Ntrunc, Ntrunc), dtype=object) +1410 rmat = [] +1411 for t in range(basematrix.T): +1412 if self.content[t] is None: +1413 rmat.append(None) +1414 else: +1415 for i in range(Ntrunc): +1416 for j in range(Ntrunc): +1417 tmpmat[i][j] = evecs[i].T @ self[t] @ evecs[j] +1418 rmat.append(np.copy(tmpmat)) +1419 +1420 return Corr(rmat)
408 def details(self, ens_content=True): -409 """Output detailed properties of the Obs. -410 -411 Parameters -412 ---------- -413 ens_content : bool -414 print details about the ensembles and replica if true. -415 """ -416 if self.tag is not None: -417 print("Description:", self.tag) -418 if not hasattr(self, 'e_dvalue'): -419 print(f'Result\t {self.value:3.8e}') -420 else: -421 if self.value == 0.0: -422 percentage = np.nan -423 else: -424 percentage = np.abs(self._dvalue / self.value) * 100 -425 print(f'Result\t {self.value:3.8e} +/- {self._dvalue:3.8e} +/- {self.ddvalue:3.8e} ({percentage:3.3f}%)') -426 if len(self.e_names) > 1: -427 print(' Ensemble errors:') -428 e_content = self.e_content -429 for e_name in self.mc_names: -430 gap = _determine_gap(self, e_content, e_name) -431 -432 if len(self.e_names) > 1: -433 print('', e_name, f'\t {self.e_dvalue[e_name]:3.6e} +/- {self.e_ddvalue[e_name]:3.6e}') -434 tau_string = " \N{GREEK SMALL LETTER TAU}_int\t " + _format_uncertainty(self.e_tauint[e_name], self.e_dtauint[e_name]) -435 tau_string += f" in units of {gap} config" -436 if gap > 1: -437 tau_string += "s" -438 if self.tau_exp[e_name] > 0: -439 tau_string = f"{tau_string: <45}" + f'\t(\N{GREEK SMALL LETTER TAU}_exp={self.tau_exp[e_name]:3.2f}, N_\N{GREEK SMALL LETTER SIGMA}={self.N_sigma[e_name]:g})' -440 else: -441 tau_string = f"{tau_string: <45}" + f'\t(S={self.S[e_name]:3.2f})' -442 print(tau_string) -443 for e_name in self.cov_names: -444 print('', e_name, f'\t {self.e_dvalue[e_name]:3.8e}') -445 if ens_content is True: -446 if len(self.e_names) == 1: -447 print(self.N, 'samples in', len(self.e_names), 'ensemble:') -448 else: -449 print(self.N, 'samples in', len(self.e_names), 'ensembles:') -450 my_string_list = [] -451 for key, value in sorted(self.e_content.items()): -452 if key not in self.covobs: -453 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " -454 if len(value) == 1: -455 my_string += f': {self.shape[value[0]]} configurations' -456 if isinstance(self.idl[value[0]], range): -457 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}' + ')' -458 else: -459 my_string += f' (irregular range from {self.idl[value[0]][0]} to {self.idl[value[0]][-1]})' -460 else: -461 sublist = [] -462 for v in value: -463 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " -464 my_substring += f': {self.shape[v]} configurations' -465 if isinstance(self.idl[v], range): -466 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}' + ')' -467 else: -468 my_substring += f' (irregular range from {self.idl[v][0]} to {self.idl[v][-1]})' -469 sublist.append(my_substring) -470 -471 my_string += '\n' + '\n'.join(sublist) -472 else: -473 my_string = ' ' + "\u00B7 Covobs '" + key + "' " -474 my_string_list.append(my_string) -475 print('\n'.join(my_string_list)) +@@ -4125,20 +4121,20 @@ print details about the ensembles and replica if true.407 def details(self, ens_content=True): +408 """Output detailed properties of the Obs. +409 +410 Parameters +411 ---------- +412 ens_content : bool +413 print details about the ensembles and replica if true. +414 """ +415 if self.tag is not None: +416 print("Description:", self.tag) +417 if not hasattr(self, 'e_dvalue'): +418 print(f'Result\t {self.value:3.8e}') +419 else: +420 if self.value == 0.0: +421 percentage = np.nan +422 else: +423 percentage = np.abs(self._dvalue / self.value) * 100 +424 print(f'Result\t {self.value:3.8e} +/- {self._dvalue:3.8e} +/- {self.ddvalue:3.8e} ({percentage:3.3f}%)') +425 if len(self.e_names) > 1: +426 print(' Ensemble errors:') +427 e_content = self.e_content +428 for e_name in self.mc_names: +429 gap = _determine_gap(self, e_content, e_name) +430 +431 if len(self.e_names) > 1: +432 print('', e_name, f'\t {self.e_dvalue[e_name]:3.6e} +/- {self.e_ddvalue[e_name]:3.6e}') +433 tau_string = " \N{GREEK SMALL LETTER TAU}_int\t " + _format_uncertainty(self.e_tauint[e_name], self.e_dtauint[e_name]) +434 tau_string += f" in units of {gap} config" +435 if gap > 1: +436 tau_string += "s" +437 if self.tau_exp[e_name] > 0: +438 tau_string = f"{tau_string: <45}" + f'\t(\N{GREEK SMALL LETTER TAU}_exp={self.tau_exp[e_name]:3.2f}, N_\N{GREEK SMALL LETTER SIGMA}={self.N_sigma[e_name]:g})' +439 else: +440 tau_string = f"{tau_string: <45}" + f'\t(S={self.S[e_name]:3.2f})' +441 print(tau_string) +442 for e_name in self.cov_names: +443 print('', e_name, f'\t {self.e_dvalue[e_name]:3.8e}') +444 if ens_content is True: +445 if len(self.e_names) == 1: +446 print(self.N, 'samples in', len(self.e_names), 'ensemble:') +447 else: +448 print(self.N, 'samples in', len(self.e_names), 'ensembles:') +449 my_string_list = [] +450 for key, value in sorted(self.e_content.items()): +451 if key not in self.covobs: +452 my_string = ' ' + "\u00B7 Ensemble '" + key + "' " +453 if len(value) == 1: +454 my_string += f': {self.shape[value[0]]} configurations' +455 if isinstance(self.idl[value[0]], range): +456 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}' + ')' +457 else: +458 my_string += f' (irregular range from {self.idl[value[0]][0]} to {self.idl[value[0]][-1]})' +459 else: +460 sublist = [] +461 for v in value: +462 my_substring = ' ' + "\u00B7 Replicum '" + v[len(key) + 1:] + "' " +463 my_substring += f': {self.shape[v]} configurations' +464 if isinstance(self.idl[v], range): +465 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}' + ')' +466 else: +467 my_substring += f' (irregular range from {self.idl[v][0]} to {self.idl[v][-1]})' +468 sublist.append(my_substring) +469 +470 my_string += '\n' + '\n'.join(sublist) +471 else: +472 my_string = ' ' + "\u00B7 Covobs '" + key + "' " +473 my_string_list.append(my_string) +474 print('\n'.join(my_string_list))
477 def reweight(self, weight): -478 """Reweight the obs with given rewighting factors. -479 -480 Parameters -481 ---------- -482 weight : Obs -483 Reweighting factor. An Observable that has to be defined on a superset of the -484 configurations in obs[i].idl for all i. -485 all_configs : bool -486 if True, the reweighted observables are normalized by the average of -487 the reweighting factor on all configurations in weight.idl and not -488 on the configurations in obs[i].idl. Default False. -489 """ -490 return reweight(weight, [self])[0] +@@ -4170,17 +4166,17 @@ on the configurations in obs[i].idl. Default False.476 def reweight(self, weight): +477 """Reweight the obs with given rewighting factors. +478 +479 Parameters +480 ---------- +481 weight : Obs +482 Reweighting factor. An Observable that has to be defined on a superset of the +483 configurations in obs[i].idl for all i. +484 all_configs : bool +485 if True, the reweighted observables are normalized by the average of +486 the reweighting factor on all configurations in weight.idl and not +487 on the configurations in obs[i].idl. Default False. +488 """ +489 return reweight(weight, [self])[0]
492 def is_zero_within_error(self, sigma=1): -493 """Checks whether the observable is zero within 'sigma' standard errors. -494 -495 Parameters -496 ---------- -497 sigma : int -498 Number of standard errors used for the check. -499 -500 Works only properly when the gamma method was run. -501 """ -502 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue +@@ -4208,15 +4204,15 @@ Number of standard errors used for the check.491 def is_zero_within_error(self, sigma=1): +492 """Checks whether the observable is zero within 'sigma' standard errors. +493 +494 Parameters +495 ---------- +496 sigma : int +497 Number of standard errors used for the check. +498 +499 Works only properly when the gamma method was run. +500 """ +501 return self.is_zero() or np.abs(self.value) <= sigma * self._dvalue
504 def is_zero(self, atol=1e-10): -505 """Checks whether the observable is zero within a given tolerance. -506 -507 Parameters -508 ---------- -509 atol : float -510 Absolute tolerance (for details see numpy documentation). -511 """ -512 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()) +@@ -4243,45 +4239,45 @@ Absolute tolerance (for details see numpy documentation).503 def is_zero(self, atol=1e-10): +504 """Checks whether the observable is zero within a given tolerance. +505 +506 Parameters +507 ---------- +508 atol : float +509 Absolute tolerance (for details see numpy documentation). +510 """ +511 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())
514 def plot_tauint(self, save=None): -515 """Plot integrated autocorrelation time for each ensemble. -516 -517 Parameters -518 ---------- -519 save : str -520 saves the figure to a file named 'save' if. -521 """ -522 if not hasattr(self, 'e_dvalue'): -523 raise Exception('Run the gamma method first.') -524 -525 for e, e_name in enumerate(self.mc_names): -526 fig = plt.figure() -527 plt.xlabel(r'$W$') -528 plt.ylabel(r'$\tau_\mathrm{int}$') -529 length = len(self.e_n_tauint[e_name]) -530 if self.tau_exp[e_name] > 0: -531 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] -532 x_help = np.arange(2 * self.tau_exp[e_name]) -533 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 -534 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) -535 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') -536 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], -537 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) -538 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 -539 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) -540 else: -541 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) -542 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) -543 -544 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) -545 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') -546 plt.legend() -547 plt.xlim(-0.5, xmax) -548 ylim = plt.ylim() -549 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) -550 plt.draw() -551 if save: -552 fig.savefig(save + "_" + str(e)) +@@ -4308,36 +4304,36 @@ saves the figure to a file named 'save' if.513 def plot_tauint(self, save=None): +514 """Plot integrated autocorrelation time for each ensemble. +515 +516 Parameters +517 ---------- +518 save : str +519 saves the figure to a file named 'save' if. +520 """ +521 if not hasattr(self, 'e_dvalue'): +522 raise Exception('Run the gamma method first.') +523 +524 for e, e_name in enumerate(self.mc_names): +525 fig = plt.figure() +526 plt.xlabel(r'$W$') +527 plt.ylabel(r'$\tau_\mathrm{int}$') +528 length = len(self.e_n_tauint[e_name]) +529 if self.tau_exp[e_name] > 0: +530 base = self.e_n_tauint[e_name][self.e_windowsize[e_name]] +531 x_help = np.arange(2 * self.tau_exp[e_name]) +532 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 +533 x_arr = np.arange(self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]) +534 plt.plot(x_arr, y_help, 'C' + str(e), linewidth=1, ls='--', marker=',') +535 plt.errorbar([self.e_windowsize[e_name] + 2 * self.tau_exp[e_name]], [self.e_tauint[e_name]], +536 yerr=[self.e_dtauint[e_name]], fmt='C' + str(e), linewidth=1, capsize=2, marker='o', mfc=plt.rcParams['axes.facecolor']) +537 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 +538 label = e_name + r', $\tau_\mathrm{exp}$=' + str(np.around(self.tau_exp[e_name], decimals=2)) +539 else: +540 label = e_name + ', S=' + str(np.around(self.S[e_name], decimals=2)) +541 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) +542 +543 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) +544 plt.axvline(x=self.e_windowsize[e_name], color='C' + str(e), alpha=0.5, marker=',', ls='--') +545 plt.legend() +546 plt.xlim(-0.5, xmax) +547 ylim = plt.ylim() +548 plt.ylim(bottom=0.0, top=max(1.0, ylim[1])) +549 plt.draw() +550 if save: +551 fig.savefig(save + "_" + str(e))
554 def plot_rho(self, save=None): -555 """Plot normalized autocorrelation function time for each ensemble. -556 -557 Parameters -558 ---------- -559 save : str -560 saves the figure to a file named 'save' if. -561 """ -562 if not hasattr(self, 'e_dvalue'): -563 raise Exception('Run the gamma method first.') -564 for e, e_name in enumerate(self.mc_names): -565 fig = plt.figure() -566 plt.xlabel('W') -567 plt.ylabel('rho') -568 length = len(self.e_drho[e_name]) -569 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) -570 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') -571 if self.tau_exp[e_name] > 0: -572 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], -573 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) -574 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 -575 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) -576 else: -577 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) -578 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) -579 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) -580 plt.xlim(-0.5, xmax) -581 plt.draw() -582 if save: -583 fig.savefig(save + "_" + str(e)) +@@ -4364,27 +4360,27 @@ saves the figure to a file named 'save' if.553 def plot_rho(self, save=None): +554 """Plot normalized autocorrelation function time for each ensemble. +555 +556 Parameters +557 ---------- +558 save : str +559 saves the figure to a file named 'save' if. +560 """ +561 if not hasattr(self, 'e_dvalue'): +562 raise Exception('Run the gamma method first.') +563 for e, e_name in enumerate(self.mc_names): +564 fig = plt.figure() +565 plt.xlabel('W') +566 plt.ylabel('rho') +567 length = len(self.e_drho[e_name]) +568 plt.errorbar(np.arange(length), self.e_rho[e_name][:length], yerr=self.e_drho[e_name][:], linewidth=1, capsize=2) +569 plt.axvline(x=self.e_windowsize[e_name], color='r', alpha=0.25, ls='--', marker=',') +570 if self.tau_exp[e_name] > 0: +571 plt.plot([self.e_windowsize[e_name] + 1, self.e_windowsize[e_name] + 1 + 2 * self.tau_exp[e_name]], +572 [self.e_rho[e_name][self.e_windowsize[e_name] + 1], 0], 'k-', lw=1) +573 xmax = self.e_windowsize[e_name] + 2 * self.tau_exp[e_name] + 1.5 +574 plt.title('Rho ' + e_name + r', tau\_exp=' + str(np.around(self.tau_exp[e_name], decimals=2))) +575 else: +576 xmax = max(10.5, 2 * self.e_windowsize[e_name] - 0.5) +577 plt.title('Rho ' + e_name + ', S=' + str(np.around(self.S[e_name], decimals=2))) +578 plt.plot([-0.5, xmax], [0, 0], 'k--', lw=1) +579 plt.xlim(-0.5, xmax) +580 plt.draw() +581 if save: +582 fig.savefig(save + "_" + str(e))
585 def plot_rep_dist(self): -586 """Plot replica distribution for each ensemble with more than one replicum.""" -587 if not hasattr(self, 'e_dvalue'): -588 raise Exception('Run the gamma method first.') -589 for _e, e_name in enumerate(self.mc_names): -590 if len(self.e_content[e_name]) == 1: -591 print('No replica distribution for a single replicum (', e_name, ')') -592 continue -593 r_length = [] -594 sub_r_mean = 0 -595 for r_name in self.e_content[e_name]: -596 r_length.append(len(self.deltas[r_name])) -597 sub_r_mean += self.shape[r_name] * self.r_values[r_name] -598 e_N = np.sum(r_length) -599 sub_r_mean /= e_N -600 arr = np.zeros(len(self.e_content[e_name])) -601 for r, r_name in enumerate(self.e_content[e_name]): -602 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) -603 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) -604 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') -605 plt.draw() +@@ -4404,37 +4400,37 @@ saves the figure to a file named 'save' if.584 def plot_rep_dist(self): +585 """Plot replica distribution for each ensemble with more than one replicum.""" +586 if not hasattr(self, 'e_dvalue'): +587 raise Exception('Run the gamma method first.') +588 for _e, e_name in enumerate(self.mc_names): +589 if len(self.e_content[e_name]) == 1: +590 print('No replica distribution for a single replicum (', e_name, ')') +591 continue +592 r_length = [] +593 sub_r_mean = 0 +594 for r_name in self.e_content[e_name]: +595 r_length.append(len(self.deltas[r_name])) +596 sub_r_mean += self.shape[r_name] * self.r_values[r_name] +597 e_N = np.sum(r_length) +598 sub_r_mean /= e_N +599 arr = np.zeros(len(self.e_content[e_name])) +600 for r, r_name in enumerate(self.e_content[e_name]): +601 arr[r] = (self.r_values[r_name] - sub_r_mean) / (self.e_dvalue[e_name] * np.sqrt(e_N / self.shape[r_name] - 1)) +602 plt.hist(arr, rwidth=0.8, bins=len(self.e_content[e_name])) +603 plt.title('Replica distribution' + e_name + ' (mean=0, var=1)') +604 plt.draw()
607 def plot_history(self, expand=True): -608 """Plot derived Monte Carlo history for each ensemble -609 -610 Parameters -611 ---------- -612 expand : bool -613 show expanded history for irregular Monte Carlo chains (default: True). -614 """ -615 for _e, e_name in enumerate(self.mc_names): -616 plt.figure() -617 r_length = [] -618 tmp = [] -619 tmp_expanded = [] -620 for _r, r_name in enumerate(self.e_content[e_name]): -621 tmp.append(self.deltas[r_name] + self.r_values[r_name]) -622 if expand: -623 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name], 1) + self.r_values[r_name]) -624 r_length.append(len(tmp_expanded[-1])) -625 else: -626 r_length.append(len(tmp[-1])) -627 e_N = np.sum(r_length) -628 x = np.arange(e_N) -629 y_test = np.concatenate(tmp, axis=0) -630 if expand: -631 y = np.concatenate(tmp_expanded, axis=0) -632 else: -633 y = y_test -634 plt.errorbar(x, y, fmt='.', markersize=3) -635 plt.xlim(-0.5, e_N - 0.5) -636 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})') -637 plt.draw() +@@ -4461,29 +4457,29 @@ show expanded history for irregular Monte Carlo chains (default: True).606 def plot_history(self, expand=True): +607 """Plot derived Monte Carlo history for each ensemble +608 +609 Parameters +610 ---------- +611 expand : bool +612 show expanded history for irregular Monte Carlo chains (default: True). +613 """ +614 for _e, e_name in enumerate(self.mc_names): +615 plt.figure() +616 r_length = [] +617 tmp = [] +618 tmp_expanded = [] +619 for _r, r_name in enumerate(self.e_content[e_name]): +620 tmp.append(self.deltas[r_name] + self.r_values[r_name]) +621 if expand: +622 tmp_expanded.append(_expand_deltas(self.deltas[r_name], list(self.idl[r_name]), self.shape[r_name], 1) + self.r_values[r_name]) +623 r_length.append(len(tmp_expanded[-1])) +624 else: +625 r_length.append(len(tmp[-1])) +626 e_N = np.sum(r_length) +627 x = np.arange(e_N) +628 y_test = np.concatenate(tmp, axis=0) +629 if expand: +630 y = np.concatenate(tmp_expanded, axis=0) +631 else: +632 y = y_test +633 plt.errorbar(x, y, fmt='.', markersize=3) +634 plt.xlim(-0.5, e_N - 0.5) +635 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})') +636 plt.draw()
639 def plot_piechart(self, save=None): -640 """Plot piechart which shows the fractional contribution of each -641 ensemble to the error and returns a dictionary containing the fractions. -642 -643 Parameters -644 ---------- -645 save : str -646 saves the figure to a file named 'save' if. -647 """ -648 if not hasattr(self, 'e_dvalue'): -649 raise Exception('Run the gamma method first.') -650 if np.isclose(0.0, self._dvalue, atol=1e-15): -651 raise ValueError('Error is 0.0') -652 labels = self.e_names -653 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 -654 fig1, ax1 = plt.subplots() -655 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) -656 ax1.axis('equal') -657 plt.draw() -658 if save: -659 fig1.savefig(save) -660 -661 return dict(zip(labels, sizes, strict=True)) +@@ -4511,34 +4507,34 @@ saves the figure to a file named 'save' if.638 def plot_piechart(self, save=None): +639 """Plot piechart which shows the fractional contribution of each +640 ensemble to the error and returns a dictionary containing the fractions. +641 +642 Parameters +643 ---------- +644 save : str +645 saves the figure to a file named 'save' if. +646 """ +647 if not hasattr(self, 'e_dvalue'): +648 raise Exception('Run the gamma method first.') +649 if np.isclose(0.0, self._dvalue, atol=1e-15): +650 raise ValueError('Error is 0.0') +651 labels = self.e_names +652 sizes = [self.e_dvalue[name] ** 2 for name in labels] / self._dvalue ** 2 +653 fig1, ax1 = plt.subplots() +654 ax1.pie(sizes, labels=labels, startangle=90, normalize=True) +655 ax1.axis('equal') +656 plt.draw() +657 if save: +658 fig1.savefig(save) +659 +660 return dict(zip(labels, sizes, strict=True))
663 def dump(self, filename, datatype="json.gz", description="", **kwargs): -664 """Dump the Obs to a file 'name' of chosen format. -665 -666 Parameters -667 ---------- -668 filename : str -669 name of the file to be saved. -670 datatype : str -671 Format of the exported file. Supported formats include -672 "json.gz" and "pickle" -673 description : str -674 Description for output file, only relevant for json.gz format. -675 path : str -676 specifies a custom path for the file (default '.') -677 """ -678 if 'path' in kwargs: -679 file_name = kwargs.get('path') + '/' + filename -680 else: -681 file_name = filename -682 -683 if datatype == "json.gz": -684 from .input.json import dump_to_json -685 dump_to_json([self], file_name, description=description) -686 elif datatype == "pickle": -687 with open(file_name + '.p', 'wb') as fb: -688 pickle.dump(self, fb) -689 else: -690 raise TypeError("Unknown datatype " + str(datatype)) +@@ -4572,31 +4568,31 @@ specifies a custom path for the file (default '.')662 def dump(self, filename, datatype="json.gz", description="", **kwargs): +663 """Dump the Obs to a file 'name' of chosen format. +664 +665 Parameters +666 ---------- +667 filename : str +668 name of the file to be saved. +669 datatype : str +670 Format of the exported file. Supported formats include +671 "json.gz" and "pickle" +672 description : str +673 Description for output file, only relevant for json.gz format. +674 path : str +675 specifies a custom path for the file (default '.') +676 """ +677 if 'path' in kwargs: +678 file_name = kwargs.get('path') + '/' + filename +679 else: +680 file_name = filename +681 +682 if datatype == "json.gz": +683 from .input.json import dump_to_json +684 dump_to_json([self], file_name, description=description) +685 elif datatype == "pickle": +686 with open(file_name + '.p', 'wb') as fb: +687 pickle.dump(self, fb) +688 else: +689 raise TypeError("Unknown datatype " + str(datatype))
692 def export_jackknife(self): -693 """Export jackknife samples from the Obs -694 -695 Returns -696 ------- -697 numpy.ndarray -698 Returns a numpy array of length N + 1 where N is the number of samples -699 for the given ensemble and replicum. The zeroth entry of the array contains -700 the mean value of the Obs, entries 1 to N contain the N jackknife samples -701 derived from the Obs. The current implementation only works for observables -702 defined on exactly one ensemble and replicum. The derived jackknife samples -703 should agree with samples from a full jackknife analysis up to O(1/N). -704 """ -705 -706 if len(self.names) != 1: -707 raise ValueError("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") -708 -709 name = self.names[0] -710 full_data = self.deltas[name] + self.r_values[name] -711 n = full_data.size -712 mean = self.value -713 tmp_jacks = np.zeros(n + 1) -714 tmp_jacks[0] = mean -715 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) -716 return tmp_jacks +@@ -4627,48 +4623,48 @@ should agree with samples from a full jackknife analysis up to O(1/N).691 def export_jackknife(self): +692 """Export jackknife samples from the Obs +693 +694 Returns +695 ------- +696 numpy.ndarray +697 Returns a numpy array of length N + 1 where N is the number of samples +698 for the given ensemble and replicum. The zeroth entry of the array contains +699 the mean value of the Obs, entries 1 to N contain the N jackknife samples +700 derived from the Obs. The current implementation only works for observables +701 defined on exactly one ensemble and replicum. The derived jackknife samples +702 should agree with samples from a full jackknife analysis up to O(1/N). +703 """ +704 +705 if len(self.names) != 1: +706 raise ValueError("'export_jackknife' is only implemented for Obs defined on one ensemble and replicum.") +707 +708 name = self.names[0] +709 full_data = self.deltas[name] + self.r_values[name] +710 n = full_data.size +711 mean = self.value +712 tmp_jacks = np.zeros(n + 1) +713 tmp_jacks[0] = mean +714 tmp_jacks[1:] = (n * mean - full_data) / (n - 1) +715 return tmp_jacks
718 def export_bootstrap(self, samples=500, random_numbers=None, save_rng=None): -719 """Export bootstrap samples from the Obs -720 -721 Parameters -722 ---------- -723 samples : int -724 Number of bootstrap samples to generate. -725 random_numbers : np.ndarray -726 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples. -727 If not provided the bootstrap samples are generated bashed on the md5 hash of the enesmble name. -728 save_rng : str -729 Save the random numbers to a file if a path is specified. -730 -731 Returns -732 ------- -733 numpy.ndarray -734 Returns a numpy array of length N + 1 where N is the number of samples -735 for the given ensemble and replicum. The zeroth entry of the array contains -736 the mean value of the Obs, entries 1 to N contain the N import_bootstrap samples -737 derived from the Obs. The current implementation only works for observables -738 defined on exactly one ensemble and replicum. The derived bootstrap samples -739 should agree with samples from a full bootstrap analysis up to O(1/N). -740 """ -741 if len(self.names) != 1: -742 raise ValueError("'export_boostrap' is only implemented for Obs defined on one ensemble and replicum.") -743 -744 name = self.names[0] -745 length = self.N -746 -747 if random_numbers is None: -748 seed = int(hashlib.md5(name.encode()).hexdigest(), 16) & 0xFFFFFFFF -749 rng = np.random.default_rng(seed) -750 random_numbers = rng.integers(0, length, size=(samples, length)) -751 -752 if save_rng is not None: -753 np.savetxt(save_rng, random_numbers, fmt='%i') -754 -755 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length -756 ret = np.zeros(samples + 1) -757 ret[0] = self.value -758 ret[1:] = proj @ (self.deltas[name] + self.r_values[name]) -759 return ret +@@ -4711,8 +4707,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).717 def export_bootstrap(self, samples=500, random_numbers=None, save_rng=None): +718 """Export bootstrap samples from the Obs +719 +720 Parameters +721 ---------- +722 samples : int +723 Number of bootstrap samples to generate. +724 random_numbers : np.ndarray +725 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples. +726 If not provided the bootstrap samples are generated bashed on the md5 hash of the enesmble name. +727 save_rng : str +728 Save the random numbers to a file if a path is specified. +729 +730 Returns +731 ------- +732 numpy.ndarray +733 Returns a numpy array of length N + 1 where N is the number of samples +734 for the given ensemble and replicum. The zeroth entry of the array contains +735 the mean value of the Obs, entries 1 to N contain the N import_bootstrap samples +736 derived from the Obs. The current implementation only works for observables +737 defined on exactly one ensemble and replicum. The derived bootstrap samples +738 should agree with samples from a full bootstrap analysis up to O(1/N). +739 """ +740 if len(self.names) != 1: +741 raise ValueError("'export_boostrap' is only implemented for Obs defined on one ensemble and replicum.") +742 +743 name = self.names[0] +744 length = self.N +745 +746 if random_numbers is None: +747 seed = int(hashlib.md5(name.encode()).hexdigest(), 16) & 0xFFFFFFFF +748 rng = np.random.default_rng(seed) +749 random_numbers = rng.integers(0, length, size=(samples, length)) +750 +751 if save_rng is not None: +752 np.savetxt(save_rng, random_numbers, fmt='%i') +753 +754 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length +755 ret = np.zeros(samples + 1) +756 ret[0] = self.value +757 ret[1:] = proj @ (self.deltas[name] + self.r_values[name]) +758 return ret
898 def sqrt(self): -899 return derived_observable(lambda x, **kwargs: np.sqrt(x[0]), [self], man_grad=[1 / 2 / np.sqrt(self.value)]) + @@ -4730,8 +4726,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
901 def log(self): -902 return derived_observable(lambda x, **kwargs: np.log(x[0]), [self], man_grad=[1 / self.value]) + @@ -4749,8 +4745,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
904 def exp(self): -905 return derived_observable(lambda x, **kwargs: np.exp(x[0]), [self], man_grad=[np.exp(self.value)]) + @@ -4768,8 +4764,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
907 def sin(self): -908 return derived_observable(lambda x, **kwargs: np.sin(x[0]), [self], man_grad=[np.cos(self.value)]) + @@ -4787,8 +4783,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
910 def cos(self): -911 return derived_observable(lambda x, **kwargs: np.cos(x[0]), [self], man_grad=[-np.sin(self.value)]) + @@ -4806,8 +4802,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
913 def tan(self): -914 return derived_observable(lambda x, **kwargs: np.tan(x[0]), [self], man_grad=[1 / np.cos(self.value) ** 2]) + @@ -4825,8 +4821,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
916 def arcsin(self): -917 return derived_observable(lambda x: anp.arcsin(x[0]), [self]) + @@ -4844,8 +4840,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
919 def arccos(self): -920 return derived_observable(lambda x: anp.arccos(x[0]), [self]) + @@ -4863,8 +4859,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
922 def arctan(self): -923 return derived_observable(lambda x: anp.arctan(x[0]), [self]) + @@ -4882,8 +4878,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
925 def sinh(self): -926 return derived_observable(lambda x, **kwargs: np.sinh(x[0]), [self], man_grad=[np.cosh(self.value)]) + @@ -4901,8 +4897,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
928 def cosh(self): -929 return derived_observable(lambda x, **kwargs: np.cosh(x[0]), [self], man_grad=[np.sinh(self.value)]) + @@ -4920,8 +4916,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
931 def tanh(self): -932 return derived_observable(lambda x, **kwargs: np.tanh(x[0]), [self], man_grad=[1 / np.cosh(self.value) ** 2]) + @@ -4939,8 +4935,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
934 def arcsinh(self): -935 return derived_observable(lambda x: anp.arcsinh(x[0]), [self]) + @@ -4958,8 +4954,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
937 def arccosh(self): -938 return derived_observable(lambda x: anp.arccosh(x[0]), [self]) + @@ -4977,8 +4973,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
940 def arctanh(self): -941 return derived_observable(lambda x: anp.arctanh(x[0]), [self]) + @@ -5129,125 +5125,125 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
944class CObs: - 945 """Class for a complex valued observable.""" - 946 __slots__ = ['_imag', '_real', 'tag'] - 947 - 948 def __init__(self, real, imag=0.0): - 949 self._real = real - 950 self._imag = imag - 951 self.tag = None - 952 - 953 @property - 954 def real(self): - 955 return self._real - 956 - 957 @property - 958 def imag(self): - 959 return self._imag - 960 - 961 def gamma_method(self, **kwargs): - 962 """Executes the gamma_method for the real and the imaginary part.""" - 963 if isinstance(self.real, Obs): - 964 self.real.gamma_method(**kwargs) - 965 if isinstance(self.imag, Obs): - 966 self.imag.gamma_method(**kwargs) - 967 - 968 def is_zero(self): - 969 """Checks whether both real and imaginary part are zero within machine precision.""" - 970 return self.real == 0.0 and self.imag == 0.0 - 971 - 972 def conjugate(self): - 973 return CObs(self.real, -self.imag) - 974 - 975 def __add__(self, other): - 976 if isinstance(other, np.ndarray): - 977 return other + self - 978 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 979 return CObs(self.real + other.real, - 980 self.imag + other.imag) - 981 else: - 982 return CObs(self.real + other, self.imag) - 983 - 984 def __radd__(self, y): - 985 return self + y - 986 - 987 def __sub__(self, other): - 988 if isinstance(other, np.ndarray): - 989 return -1 * (other - self) - 990 elif hasattr(other, 'real') and hasattr(other, 'imag'): - 991 return CObs(self.real - other.real, self.imag - other.imag) - 992 else: - 993 return CObs(self.real - other, self.imag) - 994 - 995 def __rsub__(self, other): - 996 return -1 * (self - other) - 997 - 998 def __mul__(self, other): - 999 if isinstance(other, np.ndarray): -1000 return other * self -1001 elif hasattr(other, 'real') and hasattr(other, 'imag'): -1002 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): -1003 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], -1004 [self.real, other.real, self.imag, other.imag], -1005 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), -1006 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], -1007 [self.real, other.real, self.imag, other.imag], -1008 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) -1009 elif getattr(other, 'imag', 0) != 0: -1010 return CObs(self.real * other.real - self.imag * other.imag, -1011 self.imag * other.real + self.real * other.imag) -1012 else: -1013 return CObs(self.real * other.real, self.imag * other.real) -1014 else: -1015 return CObs(self.real * other, self.imag * other) -1016 -1017 def __rmul__(self, other): -1018 return self * other -1019 -1020 def __truediv__(self, other): -1021 if isinstance(other, np.ndarray): -1022 return 1 / (other / self) -1023 elif hasattr(other, 'real') and hasattr(other, 'imag'): -1024 r = other.real ** 2 + other.imag ** 2 -1025 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) -1026 else: -1027 return CObs(self.real / other, self.imag / other) -1028 -1029 def __rtruediv__(self, other): -1030 r = self.real ** 2 + self.imag ** 2 -1031 if hasattr(other, 'real') and hasattr(other, 'imag'): -1032 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) -1033 else: -1034 return CObs(self.real * other / r, -self.imag * other / r) -1035 -1036 def __abs__(self): -1037 return np.sqrt(self.real**2 + self.imag**2) -1038 -1039 def __pos__(self): -1040 return self -1041 -1042 def __neg__(self): -1043 return -1 * self -1044 -1045 def __eq__(self, other): -1046 return self.real == other.real and self.imag == other.imag -1047 -1048 __hash__ = None -1049 -1050 def __str__(self): -1051 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' -1052 -1053 def __repr__(self): -1054 return 'CObs[' + str(self) + ']' -1055 -1056 def __format__(self, format_type): -1057 if format_type == "": -1058 significance = 2 -1059 format_type = "2" -1060 else: -1061 significance = int(float(format_type.replace("+", "").replace("-", ""))) -1062 return f"({self.real:{format_type}}{self.imag:+{significance}}j)" +@@ -5265,10 +5261,10 @@ should agree with samples from a full bootstrap analysis up to O(1/N).943class CObs: + 944 """Class for a complex valued observable.""" + 945 __slots__ = ['_imag', '_real', 'tag'] + 946 + 947 def __init__(self, real, imag=0.0): + 948 self._real = real + 949 self._imag = imag + 950 self.tag = None + 951 + 952 @property + 953 def real(self): + 954 return self._real + 955 + 956 @property + 957 def imag(self): + 958 return self._imag + 959 + 960 def gamma_method(self, **kwargs): + 961 """Executes the gamma_method for the real and the imaginary part.""" + 962 if isinstance(self.real, Obs): + 963 self.real.gamma_method(**kwargs) + 964 if isinstance(self.imag, Obs): + 965 self.imag.gamma_method(**kwargs) + 966 + 967 def is_zero(self): + 968 """Checks whether both real and imaginary part are zero within machine precision.""" + 969 return self.real == 0.0 and self.imag == 0.0 + 970 + 971 def conjugate(self): + 972 return CObs(self.real, -self.imag) + 973 + 974 def __add__(self, other): + 975 if isinstance(other, np.ndarray): + 976 return other + self + 977 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 978 return CObs(self.real + other.real, + 979 self.imag + other.imag) + 980 else: + 981 return CObs(self.real + other, self.imag) + 982 + 983 def __radd__(self, y): + 984 return self + y + 985 + 986 def __sub__(self, other): + 987 if isinstance(other, np.ndarray): + 988 return -1 * (other - self) + 989 elif hasattr(other, 'real') and hasattr(other, 'imag'): + 990 return CObs(self.real - other.real, self.imag - other.imag) + 991 else: + 992 return CObs(self.real - other, self.imag) + 993 + 994 def __rsub__(self, other): + 995 return -1 * (self - other) + 996 + 997 def __mul__(self, other): + 998 if isinstance(other, np.ndarray): + 999 return other * self +1000 elif hasattr(other, 'real') and hasattr(other, 'imag'): +1001 if all(isinstance(i, Obs) for i in [self.real, self.imag, other.real, other.imag]): +1002 return CObs(derived_observable(lambda x, **kwargs: x[0] * x[1] - x[2] * x[3], +1003 [self.real, other.real, self.imag, other.imag], +1004 man_grad=[other.real.value, self.real.value, -other.imag.value, -self.imag.value]), +1005 derived_observable(lambda x, **kwargs: x[2] * x[1] + x[0] * x[3], +1006 [self.real, other.real, self.imag, other.imag], +1007 man_grad=[other.imag.value, self.imag.value, other.real.value, self.real.value])) +1008 elif getattr(other, 'imag', 0) != 0: +1009 return CObs(self.real * other.real - self.imag * other.imag, +1010 self.imag * other.real + self.real * other.imag) +1011 else: +1012 return CObs(self.real * other.real, self.imag * other.real) +1013 else: +1014 return CObs(self.real * other, self.imag * other) +1015 +1016 def __rmul__(self, other): +1017 return self * other +1018 +1019 def __truediv__(self, other): +1020 if isinstance(other, np.ndarray): +1021 return 1 / (other / self) +1022 elif hasattr(other, 'real') and hasattr(other, 'imag'): +1023 r = other.real ** 2 + other.imag ** 2 +1024 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.imag * other.real - self.real * other.imag) / r) +1025 else: +1026 return CObs(self.real / other, self.imag / other) +1027 +1028 def __rtruediv__(self, other): +1029 r = self.real ** 2 + self.imag ** 2 +1030 if hasattr(other, 'real') and hasattr(other, 'imag'): +1031 return CObs((self.real * other.real + self.imag * other.imag) / r, (self.real * other.imag - self.imag * other.real) / r) +1032 else: +1033 return CObs(self.real * other / r, -self.imag * other / r) +1034 +1035 def __abs__(self): +1036 return np.sqrt(self.real**2 + self.imag**2) +1037 +1038 def __pos__(self): +1039 return self +1040 +1041 def __neg__(self): +1042 return -1 * self +1043 +1044 def __eq__(self, other): +1045 return self.real == other.real and self.imag == other.imag +1046 +1047 __hash__ = None +1048 +1049 def __str__(self): +1050 return '(' + str(self.real) + int(self.imag >= 0.0) * '+' + str(self.imag) + 'j)' +1051 +1052 def __repr__(self): +1053 return 'CObs[' + str(self) + ']' +1054 +1055 def __format__(self, format_type): +1056 if format_type == "": +1057 significance = 2 +1058 format_type = "2" +1059 else: +1060 significance = int(float(format_type.replace("+", "").replace("-", ""))) +1061 return f"({self.real:{format_type}}{self.imag:+{significance}}j)"
948 def __init__(self, real, imag=0.0): -949 self._real = real -950 self._imag = imag -951 self.tag = None + @@ -5295,9 +5291,9 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
953 @property -954 def real(self): -955 return self._real + @@ -5313,9 +5309,9 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
957 @property -958 def imag(self): -959 return self._imag + @@ -5333,12 +5329,12 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
961 def gamma_method(self, **kwargs): -962 """Executes the gamma_method for the real and the imaginary part.""" -963 if isinstance(self.real, Obs): -964 self.real.gamma_method(**kwargs) -965 if isinstance(self.imag, Obs): -966 self.imag.gamma_method(**kwargs) + @@ -5358,9 +5354,9 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
968 def is_zero(self): -969 """Checks whether both real and imaginary part are zero within machine precision.""" -970 return self.real == 0.0 and self.imag == 0.0 + @@ -5380,8 +5376,8 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
972 def conjugate(self): -973 return CObs(self.real, -self.imag) + @@ -5400,12 +5396,12 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
1065def gamma_method(x, **kwargs): -1066 """Vectorized version of the gamma_method applicable to lists or arrays of Obs. -1067 -1068 See docstring of pe.Obs.gamma_method for details. -1069 """ -1070 return np.vectorize(lambda o: o.gm(**kwargs))(x) + @@ -5427,12 +5423,12 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
1065def gamma_method(x, **kwargs): -1066 """Vectorized version of the gamma_method applicable to lists or arrays of Obs. -1067 -1068 See docstring of pe.Obs.gamma_method for details. -1069 """ -1070 return np.vectorize(lambda o: o.gm(**kwargs))(x) + @@ -5454,194 +5450,194 @@ should agree with samples from a full bootstrap analysis up to O(1/N).
1200def derived_observable(func, data, array_mode=False, **kwargs): -1201 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. -1202 -1203 Parameters -1204 ---------- -1205 func : object -1206 arbitrary function of the form func(data, **kwargs). For the -1207 automatic differentiation to work, all numpy functions have to have -1208 the autograd wrapper (use 'import autograd.numpy as anp'). -1209 data : list -1210 list of Obs, e.g. [obs1, obs2, obs3]. -1211 num_grad : bool -1212 if True, numerical derivatives are used instead of autograd -1213 (default False). To control the numerical differentiation the -1214 kwargs of numdifftools.step_generators.MaxStepGenerator -1215 can be used. -1216 man_grad : list -1217 manually supply a list or an array which contains the jacobian -1218 of func. Use cautiously, supplying the wrong derivative will -1219 not be intercepted. -1220 -1221 Notes -1222 ----- -1223 For simple mathematical operations it can be practical to use anonymous -1224 functions. For the ratio of two observables one can e.g. use -1225 -1226 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) -1227 """ -1228 -1229 data = np.asarray(data) -1230 raveled_data = data.ravel() -1231 -1232 # Workaround for matrix operations containing non Obs data -1233 if not all(isinstance(x, Obs) for x in raveled_data): -1234 for i in range(len(raveled_data)): -1235 if isinstance(raveled_data[i], (int, float)): -1236 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") -1237 -1238 allcov = {} -1239 for o in raveled_data: -1240 for name in o.cov_names: -1241 if name in allcov: -1242 if not np.allclose(allcov[name], o.covobs[name].cov): -1243 raise Exception(f'Inconsistent covariance matrices for {name}!') -1244 else: -1245 allcov[name] = o.covobs[name].cov -1246 -1247 n_obs = len(raveled_data) -1248 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) -1249 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) -1250 new_sample_names = sorted(set(new_names) - set(new_cov_names)) -1251 -1252 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 -1253 -1254 if data.ndim == 1: -1255 values = np.array([o.value for o in data]) -1256 else: -1257 values = np.vectorize(lambda x: x.value)(data) -1258 -1259 new_values = func(values, **kwargs) -1260 -1261 multi = int(isinstance(new_values, np.ndarray)) -1262 -1263 new_r_values = {} -1264 new_idl_d = {} -1265 for name in new_sample_names: -1266 idl = [] -1267 tmp_values = np.zeros(n_obs) -1268 for i, item in enumerate(raveled_data): -1269 tmp_values[i] = item.r_values.get(name, item.value) -1270 tmp_idl = item.idl.get(name) -1271 if tmp_idl is not None: -1272 idl.append(tmp_idl) -1273 if multi > 0: -1274 tmp_values = np.array(tmp_values).reshape(data.shape) -1275 new_r_values[name] = func(tmp_values, **kwargs) -1276 new_idl_d[name] = _merge_idx(idl) -1277 -1278 def _compute_scalefactor_missing_rep(obs): -1279 """ -1280 Computes the scale factor that is to be multiplied with the deltas -1281 in the case where Obs with different subsets of replica are merged. -1282 Returns a dictionary with the scale factor for each Monte Carlo name. -1283 -1284 Parameters -1285 ---------- -1286 obs : Obs -1287 The observable corresponding to the deltas that are to be scaled -1288 """ -1289 scalef_d = {} -1290 for mc_name in obs.mc_names: -1291 mc_idl_d = [name for name in obs.idl if name.startswith(mc_name + '|')] -1292 new_mc_idl_d = [name for name in new_idl_d if name.startswith(mc_name + '|')] -1293 if len(mc_idl_d) > 0 and len(mc_idl_d) < len(new_mc_idl_d): -1294 scalef_d[mc_name] = sum([len(new_idl_d[name]) for name in new_mc_idl_d]) / sum([len(new_idl_d[name]) for name in mc_idl_d]) -1295 return scalef_d -1296 -1297 if 'man_grad' in kwargs: -1298 deriv = np.asarray(kwargs.get('man_grad')) -1299 if new_values.shape + data.shape != deriv.shape: -1300 raise ValueError('Manual derivative does not have correct shape.') -1301 elif kwargs.get('num_grad') is True: -1302 if multi > 0: -1303 raise NotImplementedError('Multi mode currently not supported for numerical derivative') -1304 options = { -1305 'base_step': 0.1, -1306 'step_ratio': 2.5} -1307 for key in options.keys(): -1308 kwarg = kwargs.get(key) -1309 if kwarg is not None: -1310 options[key] = kwarg -1311 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) -1312 if tmp_df.size == 1: -1313 deriv = np.array([tmp_df.real]) -1314 else: -1315 deriv = tmp_df.real -1316 else: -1317 deriv = jacobian(func)(values, **kwargs) -1318 -1319 final_result = np.zeros(new_values.shape, dtype=object) -1320 -1321 if array_mode is True: -1322 -1323 class _Zero_grad: -1324 def __init__(self, N): -1325 self.grad = np.zeros((N, 1)) -1326 -1327 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])) -1328 d_extracted = {} -1329 g_extracted = {} -1330 for name in new_sample_names: -1331 d_extracted[name] = [] -1332 ens_length = len(new_idl_d[name]) -1333 for dat in data: -1334 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], _compute_scalefactor_missing_rep(o).get(name.split('|')[0], 1)) for o in dat.reshape(np.prod(dat.shape))]).reshape((*dat.shape, ens_length))) -1335 for name in new_cov_names: -1336 g_extracted[name] = [] -1337 zero_grad = _Zero_grad(new_covobs_lengths[name]) -1338 for dat in data: -1339 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))) -1340 -1341 for i_val, new_val in np.ndenumerate(new_values): -1342 new_deltas = {} -1343 new_grad = {} -1344 if array_mode is True: -1345 for name in new_sample_names: -1346 ens_length = d_extracted[name][0].shape[-1] -1347 new_deltas[name] = np.zeros(ens_length) -1348 for i_dat, dat in enumerate(d_extracted[name]): -1349 new_deltas[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) -1350 for name in new_cov_names: -1351 new_grad[name] = 0 -1352 for i_dat, dat in enumerate(g_extracted[name]): -1353 new_grad[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) -1354 else: -1355 for j_obs, obs in np.ndenumerate(data): -1356 scalef_d = _compute_scalefactor_missing_rep(obs) -1357 for name in obs.names: -1358 if name in obs.cov_names: -1359 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad -1360 else: -1361 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], scalef_d.get(name.split('|')[0], 1)) -1362 -1363 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} -1364 -1365 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): -1366 raise ValueError('The same name has been used for deltas and covobs!') -1367 new_samples = [] -1368 new_means = [] -1369 new_idl = [] -1370 new_names_obs = [] -1371 for name in new_names: -1372 if name not in new_covobs: -1373 new_samples.append(new_deltas[name]) -1374 new_idl.append(new_idl_d[name]) -1375 new_means.append(new_r_values[name][i_val]) -1376 new_names_obs.append(name) -1377 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) -1378 for name in new_covobs: -1379 final_result[i_val].names.append(name) -1380 final_result[i_val]._covobs = new_covobs -1381 final_result[i_val]._value = new_val -1382 final_result[i_val].reweighted = reweighted -1383 -1384 if multi == 0: -1385 final_result = final_result.item() -1386 -1387 return final_result +@@ -5688,48 +5684,48 @@ functions. For the ratio of two observables one can e.g. use1199def derived_observable(func, data, array_mode=False, **kwargs): +1200 """Construct a derived Obs according to func(data, **kwargs) using automatic differentiation. +1201 +1202 Parameters +1203 ---------- +1204 func : object +1205 arbitrary function of the form func(data, **kwargs). For the +1206 automatic differentiation to work, all numpy functions have to have +1207 the autograd wrapper (use 'import autograd.numpy as anp'). +1208 data : list +1209 list of Obs, e.g. [obs1, obs2, obs3]. +1210 num_grad : bool +1211 if True, numerical derivatives are used instead of autograd +1212 (default False). To control the numerical differentiation the +1213 kwargs of numdifftools.step_generators.MaxStepGenerator +1214 can be used. +1215 man_grad : list +1216 manually supply a list or an array which contains the jacobian +1217 of func. Use cautiously, supplying the wrong derivative will +1218 not be intercepted. +1219 +1220 Notes +1221 ----- +1222 For simple mathematical operations it can be practical to use anonymous +1223 functions. For the ratio of two observables one can e.g. use +1224 +1225 new_obs = derived_observable(lambda x: x[0] / x[1], [obs1, obs2]) +1226 """ +1227 +1228 data = np.asarray(data) +1229 raveled_data = data.ravel() +1230 +1231 # Workaround for matrix operations containing non Obs data +1232 if not all(isinstance(x, Obs) for x in raveled_data): +1233 for i in range(len(raveled_data)): +1234 if isinstance(raveled_data[i], (int, float)): +1235 raveled_data[i] = cov_Obs(raveled_data[i], 0.0, "###dummy_covobs###") +1236 +1237 allcov = {} +1238 for o in raveled_data: +1239 for name in o.cov_names: +1240 if name in allcov: +1241 if not np.allclose(allcov[name], o.covobs[name].cov): +1242 raise Exception(f'Inconsistent covariance matrices for {name}!') +1243 else: +1244 allcov[name] = o.covobs[name].cov +1245 +1246 n_obs = len(raveled_data) +1247 new_names = sorted(set([y for x in [o.names for o in raveled_data] for y in x])) +1248 new_cov_names = sorted(set([y for x in [o.cov_names for o in raveled_data] for y in x])) +1249 new_sample_names = sorted(set(new_names) - set(new_cov_names)) +1250 +1251 reweighted = len(list(filter(lambda o: o.reweighted is True, raveled_data))) > 0 +1252 +1253 if data.ndim == 1: +1254 values = np.array([o.value for o in data]) +1255 else: +1256 values = np.vectorize(lambda x: x.value)(data) +1257 +1258 new_values = func(values, **kwargs) +1259 +1260 multi = int(isinstance(new_values, np.ndarray)) +1261 +1262 new_r_values = {} +1263 new_idl_d = {} +1264 for name in new_sample_names: +1265 idl = [] +1266 tmp_values = np.zeros(n_obs) +1267 for i, item in enumerate(raveled_data): +1268 tmp_values[i] = item.r_values.get(name, item.value) +1269 tmp_idl = item.idl.get(name) +1270 if tmp_idl is not None: +1271 idl.append(tmp_idl) +1272 if multi > 0: +1273 tmp_values = np.array(tmp_values).reshape(data.shape) +1274 new_r_values[name] = func(tmp_values, **kwargs) +1275 new_idl_d[name] = _merge_idx(idl) +1276 +1277 def _compute_scalefactor_missing_rep(obs): +1278 """ +1279 Computes the scale factor that is to be multiplied with the deltas +1280 in the case where Obs with different subsets of replica are merged. +1281 Returns a dictionary with the scale factor for each Monte Carlo name. +1282 +1283 Parameters +1284 ---------- +1285 obs : Obs +1286 The observable corresponding to the deltas that are to be scaled +1287 """ +1288 scalef_d = {} +1289 for mc_name in obs.mc_names: +1290 mc_idl_d = [name for name in obs.idl if name.startswith(mc_name + '|')] +1291 new_mc_idl_d = [name for name in new_idl_d if name.startswith(mc_name + '|')] +1292 if len(mc_idl_d) > 0 and len(mc_idl_d) < len(new_mc_idl_d): +1293 scalef_d[mc_name] = sum([len(new_idl_d[name]) for name in new_mc_idl_d]) / sum([len(new_idl_d[name]) for name in mc_idl_d]) +1294 return scalef_d +1295 +1296 if 'man_grad' in kwargs: +1297 deriv = np.asarray(kwargs.get('man_grad')) +1298 if new_values.shape + data.shape != deriv.shape: +1299 raise ValueError('Manual derivative does not have correct shape.') +1300 elif kwargs.get('num_grad') is True: +1301 if multi > 0: +1302 raise NotImplementedError('Multi mode currently not supported for numerical derivative') +1303 options = { +1304 'base_step': 0.1, +1305 'step_ratio': 2.5} +1306 for key in options: +1307 kwarg = kwargs.get(key) +1308 if kwarg is not None: +1309 options[key] = kwarg +1310 tmp_df = nd.Gradient(func, order=4, **{k: v for k, v in options.items() if v is not None})(values, **kwargs) +1311 if tmp_df.size == 1: +1312 deriv = np.array([tmp_df.real]) +1313 else: +1314 deriv = tmp_df.real +1315 else: +1316 deriv = jacobian(func)(values, **kwargs) +1317 +1318 final_result = np.zeros(new_values.shape, dtype=object) +1319 +1320 if array_mode is True: +1321 +1322 class _Zero_grad: +1323 def __init__(self, N): +1324 self.grad = np.zeros((N, 1)) +1325 +1326 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])) +1327 d_extracted = {} +1328 g_extracted = {} +1329 for name in new_sample_names: +1330 d_extracted[name] = [] +1331 ens_length = len(new_idl_d[name]) +1332 for dat in data: +1333 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], _compute_scalefactor_missing_rep(o).get(name.split('|')[0], 1)) for o in dat.reshape(np.prod(dat.shape))]).reshape((*dat.shape, ens_length))) +1334 for name in new_cov_names: +1335 g_extracted[name] = [] +1336 zero_grad = _Zero_grad(new_covobs_lengths[name]) +1337 for dat in data: +1338 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))) +1339 +1340 for i_val, new_val in np.ndenumerate(new_values): +1341 new_deltas = {} +1342 new_grad = {} +1343 if array_mode is True: +1344 for name in new_sample_names: +1345 ens_length = d_extracted[name][0].shape[-1] +1346 new_deltas[name] = np.zeros(ens_length) +1347 for i_dat, dat in enumerate(d_extracted[name]): +1348 new_deltas[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) +1349 for name in new_cov_names: +1350 new_grad[name] = 0 +1351 for i_dat, dat in enumerate(g_extracted[name]): +1352 new_grad[name] += np.tensordot(deriv[(*i_val, i_dat)], dat) +1353 else: +1354 for j_obs, obs in np.ndenumerate(data): +1355 scalef_d = _compute_scalefactor_missing_rep(obs) +1356 for name in obs.names: +1357 if name in obs.cov_names: +1358 new_grad[name] = new_grad.get(name, 0) + deriv[i_val + j_obs] * obs.covobs[name].grad +1359 else: +1360 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], scalef_d.get(name.split('|')[0], 1)) +1361 +1362 new_covobs = {name: Covobs(0, allcov[name], name, grad=new_grad[name]) for name in new_grad} +1363 +1364 if not set(new_covobs.keys()).isdisjoint(new_deltas.keys()): +1365 raise ValueError('The same name has been used for deltas and covobs!') +1366 new_samples = [] +1367 new_means = [] +1368 new_idl = [] +1369 new_names_obs = [] +1370 for name in new_names: +1371 if name not in new_covobs: +1372 new_samples.append(new_deltas[name]) +1373 new_idl.append(new_idl_d[name]) +1374 new_means.append(new_r_values[name][i_val]) +1375 new_names_obs.append(name) +1376 final_result[i_val] = Obs(new_samples, new_names_obs, means=new_means, idl=new_idl) +1377 for name in new_covobs: +1378 final_result[i_val].names.append(name) +1379 final_result[i_val]._covobs = new_covobs +1380 final_result[i_val]._value = new_val +1381 final_result[i_val].reweighted = reweighted +1382 +1383 if multi == 0: +1384 final_result = final_result.item() +1385 +1386 return final_result
1419def reweight(weight, obs, **kwargs): -1420 """Reweight a list of observables. -1421 -1422 Parameters -1423 ---------- -1424 weight : Obs -1425 Reweighting factor. An Observable that has to be defined on a superset of the -1426 configurations in obs[i].idl for all i. -1427 obs : list -1428 list of Obs, e.g. [obs1, obs2, obs3]. -1429 all_configs : bool -1430 if True, the reweighted observables are normalized by the average of -1431 the reweighting factor on all configurations in weight.idl and not -1432 on the configurations in obs[i].idl. Default False. -1433 """ -1434 result = [] -1435 for i in range(len(obs)): -1436 if len(obs[i].cov_names): -1437 raise ValueError('Error: Not possible to reweight an Obs that contains covobs!') -1438 if not set(obs[i].names).issubset(weight.names): -1439 raise ValueError('Error: Ensembles do not fit') -1440 if len(obs[i].mc_names) > 1 or len(weight.mc_names) > 1: -1441 raise ValueError('Error: Cannot reweight an Obs that contains multiple ensembles.') -1442 for name in obs[i].names: -1443 if not set(obs[i].idl[name]).issubset(weight.idl[name]): -1444 raise ValueError(f'obs[{i}] has to be defined on a subset of the configs in weight.idl[{name}]!') -1445 new_samples = [] -1446 w_deltas = {} -1447 for name in sorted(obs[i].names): -1448 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) -1449 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) -1450 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) -1451 -1452 if kwargs.get('all_configs'): -1453 new_weight = weight -1454 else: -1455 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)]) -1456 -1457 result.append(tmp_obs / new_weight) -1458 result[-1].reweighted = True -1459 -1460 return result +@@ -5763,50 +5759,50 @@ on the configurations in obs[i].idl. Default False.1418def reweight(weight, obs, **kwargs): +1419 """Reweight a list of observables. +1420 +1421 Parameters +1422 ---------- +1423 weight : Obs +1424 Reweighting factor. An Observable that has to be defined on a superset of the +1425 configurations in obs[i].idl for all i. +1426 obs : list +1427 list of Obs, e.g. [obs1, obs2, obs3]. +1428 all_configs : bool +1429 if True, the reweighted observables are normalized by the average of +1430 the reweighting factor on all configurations in weight.idl and not +1431 on the configurations in obs[i].idl. Default False. +1432 """ +1433 result = [] +1434 for i in range(len(obs)): +1435 if len(obs[i].cov_names): +1436 raise ValueError('Error: Not possible to reweight an Obs that contains covobs!') +1437 if not set(obs[i].names).issubset(weight.names): +1438 raise ValueError('Error: Ensembles do not fit') +1439 if len(obs[i].mc_names) > 1 or len(weight.mc_names) > 1: +1440 raise ValueError('Error: Cannot reweight an Obs that contains multiple ensembles.') +1441 for name in obs[i].names: +1442 if not set(obs[i].idl[name]).issubset(weight.idl[name]): +1443 raise ValueError(f'obs[{i}] has to be defined on a subset of the configs in weight.idl[{name}]!') +1444 new_samples = [] +1445 w_deltas = {} +1446 for name in sorted(obs[i].names): +1447 w_deltas[name] = _reduce_deltas(weight.deltas[name], weight.idl[name], obs[i].idl[name]) +1448 new_samples.append((w_deltas[name] + weight.r_values[name]) * (obs[i].deltas[name] + obs[i].r_values[name])) +1449 tmp_obs = Obs(new_samples, sorted(obs[i].names), idl=[obs[i].idl[name] for name in sorted(obs[i].names)]) +1450 +1451 if kwargs.get('all_configs'): +1452 new_weight = weight +1453 else: +1454 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)]) +1455 +1456 result.append(tmp_obs / new_weight) +1457 result[-1].reweighted = True +1458 +1459 return result
1463def correlate(obs_a, obs_b): -1464 """Correlate two observables. -1465 -1466 Parameters -1467 ---------- -1468 obs_a : Obs -1469 First observable -1470 obs_b : Obs -1471 Second observable -1472 -1473 Notes -1474 ----- -1475 Keep in mind to only correlate primary observables which have not been reweighted -1476 yet. The reweighting has to be applied after correlating the observables. -1477 Only works if a single ensemble is present in the Obs. -1478 Currently only works if ensemble content is identical (this is not strictly necessary). -1479 """ -1480 -1481 if len(obs_a.mc_names) > 1 or len(obs_b.mc_names) > 1: -1482 raise ValueError('Error: Cannot correlate Obs that contain multiple ensembles.') -1483 if sorted(obs_a.names) != sorted(obs_b.names): -1484 raise ValueError(f"Ensembles do not fit {set(sorted(obs_a.names)) ^ set(sorted(obs_b.names))}") -1485 if len(obs_a.cov_names) or len(obs_b.cov_names): -1486 raise ValueError('Error: Not possible to correlate Obs that contain covobs!') -1487 for name in obs_a.names: -1488 if obs_a.shape[name] != obs_b.shape[name]: -1489 raise ValueError('Shapes of ensemble', name, 'do not fit') -1490 if obs_a.idl[name] != obs_b.idl[name]: -1491 raise ValueError('idl of ensemble', name, 'do not fit') -1492 -1493 if obs_a.reweighted is True: -1494 warnings.warn("The first observable is already reweighted.", RuntimeWarning, stacklevel=2) -1495 if obs_b.reweighted is True: -1496 warnings.warn("The second observable is already reweighted.", RuntimeWarning, stacklevel=2) -1497 -1498 new_samples = [] -1499 new_idl = [] -1500 for name in sorted(obs_a.names): -1501 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) -1502 new_idl.append(obs_a.idl[name]) -1503 -1504 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) -1505 o.reweighted = obs_a.reweighted or obs_b.reweighted -1506 return o +@@ -5842,74 +5838,74 @@ Currently only works if ensemble content is identical (this is not strictly nece1462def correlate(obs_a, obs_b): +1463 """Correlate two observables. +1464 +1465 Parameters +1466 ---------- +1467 obs_a : Obs +1468 First observable +1469 obs_b : Obs +1470 Second observable +1471 +1472 Notes +1473 ----- +1474 Keep in mind to only correlate primary observables which have not been reweighted +1475 yet. The reweighting has to be applied after correlating the observables. +1476 Only works if a single ensemble is present in the Obs. +1477 Currently only works if ensemble content is identical (this is not strictly necessary). +1478 """ +1479 +1480 if len(obs_a.mc_names) > 1 or len(obs_b.mc_names) > 1: +1481 raise ValueError('Error: Cannot correlate Obs that contain multiple ensembles.') +1482 if sorted(obs_a.names) != sorted(obs_b.names): +1483 raise ValueError(f"Ensembles do not fit {set(sorted(obs_a.names)) ^ set(sorted(obs_b.names))}") +1484 if len(obs_a.cov_names) or len(obs_b.cov_names): +1485 raise ValueError('Error: Not possible to correlate Obs that contain covobs!') +1486 for name in obs_a.names: +1487 if obs_a.shape[name] != obs_b.shape[name]: +1488 raise ValueError('Shapes of ensemble', name, 'do not fit') +1489 if obs_a.idl[name] != obs_b.idl[name]: +1490 raise ValueError('idl of ensemble', name, 'do not fit') +1491 +1492 if obs_a.reweighted is True: +1493 warnings.warn("The first observable is already reweighted.", RuntimeWarning, stacklevel=2) +1494 if obs_b.reweighted is True: +1495 warnings.warn("The second observable is already reweighted.", RuntimeWarning, stacklevel=2) +1496 +1497 new_samples = [] +1498 new_idl = [] +1499 for name in sorted(obs_a.names): +1500 new_samples.append((obs_a.deltas[name] + obs_a.r_values[name]) * (obs_b.deltas[name] + obs_b.r_values[name])) +1501 new_idl.append(obs_a.idl[name]) +1502 +1503 o = Obs(new_samples, sorted(obs_a.names), idl=new_idl) +1504 o.reweighted = obs_a.reweighted or obs_b.reweighted +1505 return o
1509def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): -1510 r'''Calculates the error covariance matrix of a set of observables. -1511 -1512 WARNING: This function should be used with care, especially for observables with support on multiple -1513 ensembles with differing autocorrelations. See the notes below for details. -1514 -1515 The gamma method has to be applied first to all observables. -1516 -1517 Parameters -1518 ---------- -1519 obs : list or numpy.ndarray -1520 List or one dimensional array of Obs -1521 visualize : bool -1522 If True plots the corresponding normalized correlation matrix (default False). -1523 correlation : bool -1524 If True the correlation matrix instead of the error covariance matrix is returned (default False). -1525 smooth : None or int -1526 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue -1527 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the -1528 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely -1529 small ones. -1530 -1531 Notes -1532 ----- -1533 The error covariance is defined such that it agrees with the squared standard error for two identical observables -1534 $$\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$$ -1535 in the absence of autocorrelation. -1536 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 -1537 $$\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. -1538 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. -1539 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ -1540 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). -1541 ''' -1542 -1543 length = len(obs) -1544 -1545 max_samples = np.max([o.N for o in obs]) -1546 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: -1547 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, stacklevel=2) -1548 -1549 cov = np.zeros((length, length)) -1550 for i in range(length): -1551 for j in range(i, length): -1552 cov[i, j] = _covariance_element(obs[i], obs[j]) -1553 cov = cov + cov.T - np.diag(np.diag(cov)) -1554 -1555 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) -1556 -1557 if isinstance(smooth, int): -1558 corr = _smooth_eigenvalues(corr, smooth) -1559 -1560 if visualize: -1561 plt.matshow(corr, vmin=-1, vmax=1) -1562 plt.set_cmap('RdBu') -1563 plt.colorbar() -1564 plt.draw() -1565 -1566 if correlation is True: -1567 return corr -1568 -1569 errors = [o.dvalue for o in obs] -1570 cov = np.diag(errors) @ corr @ np.diag(errors) -1571 -1572 eigenvalues = np.linalg.eigh(cov)[0] -1573 if not np.all(eigenvalues >= 0): -1574 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning, stacklevel=2) -1575 -1576 return cov +@@ -5961,27 +5957,27 @@ This construction ensures that the estimated covariance matrix is positive semi-1508def covariance(obs, visualize=False, correlation=False, smooth=None, **kwargs): +1509 r'''Calculates the error covariance matrix of a set of observables. +1510 +1511 WARNING: This function should be used with care, especially for observables with support on multiple +1512 ensembles with differing autocorrelations. See the notes below for details. +1513 +1514 The gamma method has to be applied first to all observables. +1515 +1516 Parameters +1517 ---------- +1518 obs : list or numpy.ndarray +1519 List or one dimensional array of Obs +1520 visualize : bool +1521 If True plots the corresponding normalized correlation matrix (default False). +1522 correlation : bool +1523 If True the correlation matrix instead of the error covariance matrix is returned (default False). +1524 smooth : None or int +1525 If smooth is an integer 'E' between 2 and the dimension of the matrix minus 1 the eigenvalue +1526 smoothing procedure of hep-lat/9412087 is applied to the correlation matrix which leaves the +1527 largest E eigenvalues essentially unchanged and smoothes the smaller eigenvalues to avoid extremely +1528 small ones. +1529 +1530 Notes +1531 ----- +1532 The error covariance is defined such that it agrees with the squared standard error for two identical observables +1533 $$\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$$ +1534 in the absence of autocorrelation. +1535 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 +1536 $$\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. +1537 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. +1538 $$\tau_{\mathrm{int}, ij}=\sqrt{\tau_{\mathrm{int}, i}\times \tau_{\mathrm{int}, j}}$$ +1539 This construction ensures that the estimated covariance matrix is positive semi-definite (up to numerical rounding errors). +1540 ''' +1541 +1542 length = len(obs) +1543 +1544 max_samples = np.max([o.N for o in obs]) +1545 if max_samples <= length and not [item for sublist in [o.cov_names for o in obs] for item in sublist]: +1546 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, stacklevel=2) +1547 +1548 cov = np.zeros((length, length)) +1549 for i in range(length): +1550 for j in range(i, length): +1551 cov[i, j] = _covariance_element(obs[i], obs[j]) +1552 cov = cov + cov.T - np.diag(np.diag(cov)) +1553 +1554 corr = np.diag(1 / np.sqrt(np.diag(cov))) @ cov @ np.diag(1 / np.sqrt(np.diag(cov))) +1555 +1556 if isinstance(smooth, int): +1557 corr = _smooth_eigenvalues(corr, smooth) +1558 +1559 if visualize: +1560 plt.matshow(corr, vmin=-1, vmax=1) +1561 plt.set_cmap('RdBu') +1562 plt.colorbar() +1563 plt.draw() +1564 +1565 if correlation is True: +1566 return corr +1567 +1568 errors = [o.dvalue for o in obs] +1569 cov = np.diag(errors) @ corr @ np.diag(errors) +1570 +1571 eigenvalues = np.linalg.eigh(cov)[0] +1572 if not np.all(eigenvalues >= 0): +1573 warnings.warn("Covariance matrix is not positive semi-definite (Eigenvalues: " + str(eigenvalues) + ")", RuntimeWarning, stacklevel=2) +1574 +1575 return cov
1579def invert_corr_cov_cholesky(corr, inverrdiag): -1580 """Constructs a lower triangular matrix `chol` via the Cholesky decomposition of the correlation matrix `corr` -1581 and then returns the inverse covariance matrix `chol_inv` as a lower triangular matrix by solving `chol * x = inverrdiag`. -1582 -1583 Parameters -1584 ---------- -1585 corr : np.ndarray -1586 correlation matrix -1587 inverrdiag : np.ndarray -1588 diagonal matrix, the entries are the inverse errors of the data points considered -1589 """ -1590 -1591 condn = np.linalg.cond(corr) -1592 if condn > 0.1 / np.finfo(float).eps: -1593 raise ValueError(f"Cannot invert correlation matrix as its condition number exceeds machine precision ({condn:1.2e})") -1594 if condn > 1e13: -1595 warnings.warn(f"Correlation matrix may be ill-conditioned, condition number: {{{condn:1.2e}}}", RuntimeWarning, stacklevel=2) -1596 chol = np.linalg.cholesky(corr) -1597 chol_inv = scipy.linalg.solve_triangular(chol, inverrdiag, lower=True) -1598 -1599 return chol_inv +@@ -6011,67 +6007,67 @@ diagonal matrix, the entries are the inverse errors of the data points considere1578def invert_corr_cov_cholesky(corr, inverrdiag): +1579 """Constructs a lower triangular matrix `chol` via the Cholesky decomposition of the correlation matrix `corr` +1580 and then returns the inverse covariance matrix `chol_inv` as a lower triangular matrix by solving `chol * x = inverrdiag`. +1581 +1582 Parameters +1583 ---------- +1584 corr : np.ndarray +1585 correlation matrix +1586 inverrdiag : np.ndarray +1587 diagonal matrix, the entries are the inverse errors of the data points considered +1588 """ +1589 +1590 condn = np.linalg.cond(corr) +1591 if condn > 0.1 / np.finfo(float).eps: +1592 raise ValueError(f"Cannot invert correlation matrix as its condition number exceeds machine precision ({condn:1.2e})") +1593 if condn > 1e13: +1594 warnings.warn(f"Correlation matrix may be ill-conditioned, condition number: {{{condn:1.2e}}}", RuntimeWarning, stacklevel=2) +1595 chol = np.linalg.cholesky(corr) +1596 chol_inv = scipy.linalg.solve_triangular(chol, inverrdiag, lower=True) +1597 +1598 return chol_inv
1602def sort_corr(corr, kl, yd): -1603 """ Reorders a correlation matrix to match the alphabetical order of its underlying y data. -1604 -1605 The ordering of the input correlation matrix `corr` is given by the list of keys `kl`. -1606 The input dictionary `yd` (with the same keys `kl`) must contain the corresponding y data -1607 that the correlation matrix is based on. -1608 This function sorts the list of keys `kl` alphabetically and sorts the matrix `corr` -1609 according to this alphabetical order such that the sorted matrix `corr_sorted` corresponds -1610 to the y data `yd` when arranged in an alphabetical order by its keys. -1611 -1612 Parameters -1613 ---------- -1614 corr : np.ndarray -1615 A square correlation matrix constructed using the order of the y data specified by `kl`. -1616 The dimensions of `corr` should match the total number of y data points in `yd` combined. -1617 kl : list of str -1618 A list of keys that denotes the order in which the y data from `yd` was used to build the -1619 input correlation matrix `corr`. -1620 yd : dict of list -1621 A dictionary where each key corresponds to a unique identifier, and its value is a list of -1622 y data points. The total number of y data points across all keys must match the dimensions -1623 of `corr`. The lists in the dictionary can be lists of Obs. -1624 -1625 Returns -1626 ------- -1627 np.ndarray -1628 A new, sorted correlation matrix that corresponds to the y data from `yd` when arranged alphabetically by its keys. -1629 -1630 Example -1631 ------- -1632 >>> import numpy as np -1633 >>> import pyerrors as pe -1634 >>> corr = np.array([[1, 0.2, 0.3], [0.2, 1, 0.4], [0.3, 0.4, 1]]) -1635 >>> kl = ['b', 'a'] -1636 >>> yd = {'a': [1, 2], 'b': [3]} -1637 >>> sorted_corr = pe.obs.sort_corr(corr, kl, yd) -1638 >>> print(sorted_corr) -1639 array([[1. , 0.3, 0.4], -1640 [0.3, 1. , 0.2], -1641 [0.4, 0.2, 1. ]]) -1642 -1643 """ -1644 kl_sorted = sorted(kl) -1645 -1646 posd = {} -1647 ofs = 0 -1648 for _ki, k in enumerate(kl): -1649 posd[k] = [i + ofs for i in range(len(yd[k]))] -1650 ofs += len(posd[k]) -1651 -1652 mapping = [] -1653 for k in kl_sorted: -1654 for i in range(len(yd[k])): -1655 mapping.append(posd[k][i]) -1656 -1657 corr_sorted = np.zeros_like(corr) -1658 for i in range(corr.shape[0]): -1659 for j in range(corr.shape[0]): -1660 corr_sorted[i][j] = corr[mapping[i]][mapping[j]] -1661 -1662 return corr_sorted +@@ -6135,24 +6131,24 @@ of1601def sort_corr(corr, kl, yd): +1602 """ Reorders a correlation matrix to match the alphabetical order of its underlying y data. +1603 +1604 The ordering of the input correlation matrix `corr` is given by the list of keys `kl`. +1605 The input dictionary `yd` (with the same keys `kl`) must contain the corresponding y data +1606 that the correlation matrix is based on. +1607 This function sorts the list of keys `kl` alphabetically and sorts the matrix `corr` +1608 according to this alphabetical order such that the sorted matrix `corr_sorted` corresponds +1609 to the y data `yd` when arranged in an alphabetical order by its keys. +1610 +1611 Parameters +1612 ---------- +1613 corr : np.ndarray +1614 A square correlation matrix constructed using the order of the y data specified by `kl`. +1615 The dimensions of `corr` should match the total number of y data points in `yd` combined. +1616 kl : list of str +1617 A list of keys that denotes the order in which the y data from `yd` was used to build the +1618 input correlation matrix `corr`. +1619 yd : dict of list +1620 A dictionary where each key corresponds to a unique identifier, and its value is a list of +1621 y data points. The total number of y data points across all keys must match the dimensions +1622 of `corr`. The lists in the dictionary can be lists of Obs. +1623 +1624 Returns +1625 ------- +1626 np.ndarray +1627 A new, sorted correlation matrix that corresponds to the y data from `yd` when arranged alphabetically by its keys. +1628 +1629 Example +1630 ------- +1631 >>> import numpy as np +1632 >>> import pyerrors as pe +1633 >>> corr = np.array([[1, 0.2, 0.3], [0.2, 1, 0.4], [0.3, 0.4, 1]]) +1634 >>> kl = ['b', 'a'] +1635 >>> yd = {'a': [1, 2], 'b': [3]} +1636 >>> sorted_corr = pe.obs.sort_corr(corr, kl, yd) +1637 >>> print(sorted_corr) +1638 array([[1. , 0.3, 0.4], +1639 [0.3, 1. , 0.2], +1640 [0.4, 0.2, 1. ]]) +1641 +1642 """ +1643 kl_sorted = sorted(kl) +1644 +1645 posd = {} +1646 ofs = 0 +1647 for _ki, k in enumerate(kl): +1648 posd[k] = [i + ofs for i in range(len(yd[k]))] +1649 ofs += len(posd[k]) +1650 +1651 mapping = [] +1652 for k in kl_sorted: +1653 for i in range(len(yd[k])): +1654 mapping.append(posd[k][i]) +1655 +1656 corr_sorted = np.zeros_like(corr) +1657 for i in range(corr.shape[0]): +1658 for j in range(corr.shape[0]): +1659 corr_sorted[i][j] = corr[mapping[i]][mapping[j]] +1660 +1661 return corr_sortedcorr. The lists in the dictionary can be lists of Obs.
1742def import_jackknife(jacks, name, idl=None): -1743 """Imports jackknife samples and returns an Obs -1744 -1745 Parameters -1746 ---------- -1747 jacks : numpy.ndarray -1748 numpy array containing the mean value as zeroth entry and -1749 the N jackknife samples as first to Nth entry. -1750 name : str -1751 name of the ensemble the samples are defined on. -1752 """ -1753 length = len(jacks) - 1 -1754 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) -1755 samples = jacks[1:] @ prj -1756 mean = np.mean(samples) -1757 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) -1758 new_obs._value = jacks[0] -1759 return new_obs +@@ -6182,34 +6178,34 @@ name of the ensemble the samples are defined on.1741def import_jackknife(jacks, name, idl=None): +1742 """Imports jackknife samples and returns an Obs +1743 +1744 Parameters +1745 ---------- +1746 jacks : numpy.ndarray +1747 numpy array containing the mean value as zeroth entry and +1748 the N jackknife samples as first to Nth entry. +1749 name : str +1750 name of the ensemble the samples are defined on. +1751 """ +1752 length = len(jacks) - 1 +1753 prj = (np.ones((length, length)) - (length - 1) * np.identity(length)) +1754 samples = jacks[1:] @ prj +1755 mean = np.mean(samples) +1756 new_obs = Obs([samples - mean], [name], idl=idl, means=[mean]) +1757 new_obs._value = jacks[0] +1758 return new_obs
1762def import_bootstrap(boots, name, random_numbers): -1763 """Imports bootstrap samples and returns an Obs -1764 -1765 Parameters -1766 ---------- -1767 boots : numpy.ndarray -1768 numpy array containing the mean value as zeroth entry and -1769 the N bootstrap samples as first to Nth entry. -1770 name : str -1771 name of the ensemble the samples are defined on. -1772 random_numbers : np.ndarray -1773 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples, -1774 where samples is the number of bootstrap samples and length is the length of the original Monte Carlo -1775 chain to be reconstructed. -1776 """ -1777 samples, length = random_numbers.shape -1778 if samples != len(boots) - 1: -1779 raise ValueError("Random numbers do not have the correct shape.") -1780 -1781 if samples < length: -1782 raise ValueError("Obs can't be reconstructed if there are fewer bootstrap samples than Monte Carlo data points.") -1783 -1784 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length -1785 -1786 samples = scipy.linalg.lstsq(proj, boots[1:])[0] -1787 ret = Obs([samples], [name]) -1788 ret._value = boots[0] -1789 return ret +@@ -6243,38 +6239,38 @@ chain to be reconstructed.1761def import_bootstrap(boots, name, random_numbers): +1762 """Imports bootstrap samples and returns an Obs +1763 +1764 Parameters +1765 ---------- +1766 boots : numpy.ndarray +1767 numpy array containing the mean value as zeroth entry and +1768 the N bootstrap samples as first to Nth entry. +1769 name : str +1770 name of the ensemble the samples are defined on. +1771 random_numbers : np.ndarray +1772 Array of shape (samples, length) containing the random numbers to generate the bootstrap samples, +1773 where samples is the number of bootstrap samples and length is the length of the original Monte Carlo +1774 chain to be reconstructed. +1775 """ +1776 samples, length = random_numbers.shape +1777 if samples != len(boots) - 1: +1778 raise ValueError("Random numbers do not have the correct shape.") +1779 +1780 if samples < length: +1781 raise ValueError("Obs can't be reconstructed if there are fewer bootstrap samples than Monte Carlo data points.") +1782 +1783 proj = np.vstack([np.bincount(o, minlength=length) for o in random_numbers]) / length +1784 +1785 samples = scipy.linalg.lstsq(proj, boots[1:])[0] +1786 ret = Obs([samples], [name]) +1787 ret._value = boots[0] +1788 return ret
1792def merge_obs(list_of_obs): -1793 """Combine all observables in list_of_obs into one new observable. -1794 This allows to merge Obs that have been computed on multiple replica -1795 of the same ensemble. -1796 If you like to merge Obs that are based on several ensembles, please -1797 average them yourself. -1798 -1799 Parameters -1800 ---------- -1801 list_of_obs : list -1802 list of the Obs object to be combined -1803 -1804 Notes -1805 ----- -1806 It is not possible to combine obs which are based on the same replicum -1807 """ -1808 replist = [item for obs in list_of_obs for item in obs.names] -1809 if (len(replist) == len(set(replist))) is False: -1810 raise ValueError(f'list_of_obs contains duplicate replica: {replist!s}') -1811 if any([len(o.cov_names) for o in list_of_obs]): -1812 raise ValueError('Not possible to merge data that contains covobs!') -1813 new_dict = {} -1814 idl_dict = {} -1815 for o in list_of_obs: -1816 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) -1817 for key in set(o.deltas) | set(o.r_values)}) -1818 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) -1819 -1820 names = sorted(new_dict.keys()) -1821 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) -1822 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) -1823 return o +@@ -6309,47 +6305,47 @@ list of the Obs object to be combined1791def merge_obs(list_of_obs): +1792 """Combine all observables in list_of_obs into one new observable. +1793 This allows to merge Obs that have been computed on multiple replica +1794 of the same ensemble. +1795 If you like to merge Obs that are based on several ensembles, please +1796 average them yourself. +1797 +1798 Parameters +1799 ---------- +1800 list_of_obs : list +1801 list of the Obs object to be combined +1802 +1803 Notes +1804 ----- +1805 It is not possible to combine obs which are based on the same replicum +1806 """ +1807 replist = [item for obs in list_of_obs for item in obs.names] +1808 if (len(replist) == len(set(replist))) is False: +1809 raise ValueError(f'list_of_obs contains duplicate replica: {replist!s}') +1810 if any([len(o.cov_names) for o in list_of_obs]): +1811 raise ValueError('Not possible to merge data that contains covobs!') +1812 new_dict = {} +1813 idl_dict = {} +1814 for o in list_of_obs: +1815 new_dict.update({key: o.deltas.get(key, 0) + o.r_values.get(key, 0) +1816 for key in set(o.deltas) | set(o.r_values)}) +1817 idl_dict.update({key: o.idl.get(key, 0) for key in set(o.deltas)}) +1818 +1819 names = sorted(new_dict.keys()) +1820 o = Obs([new_dict[name] for name in names], names, idl=[idl_dict[name] for name in names]) +1821 o.reweighted = np.max([oi.reweighted for oi in list_of_obs]) +1822 return o
1826def cov_Obs(means, cov, name, grad=None): -1827 """Create an Obs based on mean(s) and a covariance matrix -1828 -1829 Parameters -1830 ---------- -1831 mean : list of floats or float -1832 N mean value(s) of the new Obs -1833 cov : list or array -1834 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance -1835 name : str -1836 identifier for the covariance matrix -1837 grad : list or array -1838 Gradient of the Covobs wrt. the means belonging to cov. -1839 """ -1840 -1841 def covobs_to_obs(co): -1842 """Make an Obs out of a Covobs -1843 -1844 Parameters -1845 ---------- -1846 co : Covobs -1847 Covobs to be embedded into the Obs -1848 """ -1849 o = Obs([], [], means=[]) -1850 o._value = co.value -1851 o.names.append(co.name) -1852 o._covobs[co.name] = co -1853 o._dvalue = np.sqrt(co.errsq()) -1854 return o -1855 -1856 ol = [] -1857 if isinstance(means, (float, int)): -1858 means = [means] -1859 -1860 for i in range(len(means)): -1861 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) -1862 if ol[0].covobs[name].N != len(means): -1863 raise ValueError(f'You have to provide {ol[0].N} mean values!') -1864 if len(ol) == 1: -1865 return ol[0] -1866 return ol +1825def cov_Obs(means, cov, name, grad=None): +1826 """Create an Obs based on mean(s) and a covariance matrix +1827 +1828 Parameters +1829 ---------- +1830 mean : list of floats or float +1831 N mean value(s) of the new Obs +1832 cov : list or array +1833 2d (NxN) Covariance matrix, 1d diagonal entries or 0d covariance +1834 name : str +1835 identifier for the covariance matrix +1836 grad : list or array +1837 Gradient of the Covobs wrt. the means belonging to cov. +1838 """ +1839 +1840 def covobs_to_obs(co): +1841 """Make an Obs out of a Covobs +1842 +1843 Parameters +1844 ---------- +1845 co : Covobs +1846 Covobs to be embedded into the Obs +1847 """ +1848 o = Obs([], [], means=[]) +1849 o._value = co.value +1850 o.names.append(co.name) +1851 o._covobs[co.name] = co +1852 o._dvalue = np.sqrt(co.errsq()) +1853 return o +1854 +1855 ol = [] +1856 if isinstance(means, (float, int)): +1857 means = [means] +1858 +1859 for i in range(len(means)): +1860 ol.append(covobs_to_obs(Covobs(means[i], cov, name, pos=i, grad=grad))) +1861 if ol[0].covobs[name].N != len(means): +1862 raise ValueError(f'You have to provide {ol[0].N} mean values!') +1863 if len(ol) == 1: +1864 return ol[0] +1865 return ol