Skip to content
Snippets Groups Projects
Select Git revision
  • 2333af3f1f7bc5bdcbc5bc8acea701d015970b05
  • main default protected
2 results

evaluation.cpython-311.pyc

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    PeakOTron.py 94.86 KiB
    from HelperFunctions import GP_gain, GP_lbda, GP_mu, HistMean, HistStd, HistSkew
    from HelperFunctions import LatexFormat, Linear
    from AdditionalPDFs import gpoisson
    from LossFunctions import *
    from Model import DRM, Beta, Beta_Inf
    from iminuit import Minuit
    from scipy.interpolate import interp1d, UnivariateSpline, InterpolatedUnivariateSpline
    from scipy.stats import norm
    from astropy.stats import knuth_bin_width, freedman_bin_width, scott_bin_width
    from matplotlib import cm
    import matplotlib.pyplot as plt
    import matplotlib.gridspec as gridspec
    from matplotlib.ticker import ScalarFormatter
    from itertools import chain
    from copy import deepcopy
    import time
    
    
    
    import matplotlib.pyplot as plt
    
    
    #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    ########################################################################################################
    ## PEAKOTRON
    ########################################################################################################
    #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    
    
    class PeakOTron:
        
        
        ########################################################################################################
        ## __init__
        ########################################################################################################
        ## 
        ## VARIABLES: 
        ## n_call_minuit: number of calls to Minuit for each trial
        ## n_iterations_minuit: number of trial iterations if convergence is not  achieved
        ## n_bootstrap: number of bootstraps for each statistic 
        ## plot_figsize: figure size in inches for prefit figures
        ## plot_fontsize: fontsize for output plots
        ## plot_cmap:  colour map for output plots
        ##
        ## This function defines the basic information stored by PeakOTron and the default fit value dictionary that is updated with the input parameters of     ## the user.
        ##
        ########################################################################################################
        
        def __init__(self,
                     verbose=False,
                     n_call_minuit=10000,
                     n_iterations_minuit = 10,
                     plot_figsize=(10,10),
                     plot_fontsize=25,
                     plot_legendfontsize=18,
                     plot_cmap = 'turbo',
                     eps = 1e-6
                    ):
            
            self._default_hist_data={
                "count":None,
                "density":None,
                "bin_numbers":None,
                "bin_centres":None,
                "counts_bg":None,
                "counts_bgsub":None,
                "truncate_nsigma0_do_min":None,
                "bw":None,
                "peaks":{
                    "peak_position":None,
                    "peak_height":None,
                    "peak_index":None,
                    "peak_mean":None,
                    "peak_mean_error":None,
                    "peak_variance":None,
                    "peak_variance_error":None,
                    "peak_std_deviation":None,
                    "peak_std_deviation_error":None
                }
            }
            
            self._default_fit_dict={
                "Q_0":None,
                "G":None,
                "lbda":None,
                "mu":None,
                "sigma_0":None,
                "sigma_1":None,
                "DCR":None,
                "tau":None,
                "t_gate":None,
                "t_0":None,
                "k_max":None,
                "len_pad":None
            }
            
            self._eps = eps
            self._plot_figsize= plot_figsize
            self._plot_fontsize= plot_fontsize
            self._plot_legendfontsize= plot_legendfontsize
            self._cmap = cm.get_cmap(plot_cmap)
            self._len_pad = int(100)
            self._verbose=verbose
            self._n_call_minuit = n_call_minuit
            self._n_iterations_minuit = n_iterations_minuit
           
            
            self._max_n_peaks = None
            self._max_n_peaks_dcr = 6
            self._min_n_events_peak=100
            
            self._m_DRM = None
            self._time_prefit = None
            self._time_fit = None
            
            self._default_fit_kwargs={
                    "tau":None,
                    "tau_err":None,
                    "t_0":None,
                    "t_0_err":None,
                    "tau_R":None,
                    "tau_R_err":None,
                    "t_gate":None,
                    "t_gate_err":None,
                    "bw":None,
                    "bin_0":None,
                    "truncate_nsigma0_up":None,
                    "truncate_nsigma0_do":None,
                    "truncate_nsigma0_chi2scanlim":None,
                    "min_n_events_peak":None,
                    "prefit_only":False,
                    "bin_method":"knuth"
            }
        
            self.InitKwargs()
            
    
        
    
            
        ########################################################################################################
        ## InitKwargs()
        ########################################################################################################
        ##
        ## This function creates the fit dictionary and initialises the output dictionaries of the class.
        ##
        ########################################################################################################
            
        def InitKwargs(self):
            self._fit_kwargs = self._default_fit_kwargs.copy()
            self._prefit_values = self._default_fit_dict.copy()
            self._prefit_errors = self._default_fit_dict.copy()
            self._fit_values= self._default_fit_dict.copy()
            self._fit_errors = self._default_fit_dict.copy()
            
            self._prefit_values_bin = self._default_fit_dict.copy()
            self._prefit_errors_bin = self._default_fit_dict.copy()
            self._fit_values_bin= self._default_fit_dict.copy()
            self._fit_errors_bin = self._default_fit_dict.copy()
            
            
            self._hist_data = self._default_hist_data.copy()
        
    
            
        ########################################################################################################
        ## SetMaxNPeaks(max_n_peaks)
        ########################################################################################################
        ##
        ## Typically, it is sufficient to determine the appropriate number of peaks to fit from mu. 
        ##  However, to make sure we don't accidentally create a fit with a large number of peaks, 
        ##  the default threshold of 20 peaks may be set with this function
        ##
        ########################################################################################################
        
        def SetMaxNPeaks(self, max_n_peaks):
            max_n_peaks = int(max_n_peaks)
            if(max_n_peaks<4):
                raise Exception("Maximum number of light peaks must be greater than 3.")
            
    
            self._max_n_peaks =  max_n_peaks
            if(self._verbose):
                print("Set maximum number of light peaks to {:d}.".format(self._max_n_peaks))
                 
                
        ########################################################################################################
        ## SetMaxNPeaksDCR(max_n_peaks)
        ########################################################################################################
        ##
        ## Typically, it is sufficient to determine the appropriate number of peaks to fit from mu. 
        ##  However, to make sure we don't accidentally create a fit with a large number of peaks, 
        ##  the default threshold of 20 peaks may be set with this function
        ##
        ########################################################################################################
        
        def SetMaxNPeaksDCR(self, max_n_peaks_dcr):
            max_n_peaks_dcr = int(max_n_peaks_dcr)
            if(max_n_peaks_dcr<4):
                raise Exception("Maximum number of dark peaks must be greater than 3.")
            
    
            self._max_n_peaks_dcr =  max_n_peaks_dcr
            if(self._verbose):
                print("Set maximum number of dark peaks to {:d}.".format(self._max_n_peaks_dcr))
                
    
                    
    
        ########################################################################################################
        ## GetMinuit()
        ########################################################################################################
        ##
        ## Returns Minuit object after fit. Note this object has fit results stored in bins, not in the unit parameter of the input.
        ##
        ########################################################################################################
                    
    
        def GetMinuit(self):
            if(self._m_DRM is None):
                raise Exception ("Fit has not yet been performed. Please first perform a fit.")
                
            else:
                return self._m_DRM
            
            
        ########################################################################################################
        ## GetFitTime()
        ########################################################################################################
        ##
        ## Returns fit time, for prefit and fit.
        ##
        ########################################################################################################
                    
            
            
        def GetFitTime(self, prefit=False):
            
            if(prefit):
                if(self._time_prefit is None):
                    raise Exception ("Fit has not yet been performed. Please first perform a fit.")
                else:
                    return self._time_prefit
                
            else:
                if(self._time_fit is None):
                    raise Exception ("Fit has not yet been performed. Please first perform a fit.")
                else:
                    return self._time_fit
            
        ########################################################################################################
        ## IsValid()
        ########################################################################################################
        ##
        ## Returns whether or not Minuit returned valid.
        ##
        ########################################################################################################
            
        def IsValid(self):
            if(self._m_DRM is None):
                raise Exception ("Fit has not yet been performed. Please first perform a fit.")
                
            else:
                return self._m_DRM.valid
         
        
        ########################################################################################################
        ## GetFitResults()
        ########################################################################################################
        ##
        ## Returns fit results and errors in the input charge unit.
        ##
        ########################################################################################################
        
        
        
        def GetFitResults(self, bin_units=True):
            
    
            
            if(self._m_DRM is None):
                raise Exception ("Fit has not yet been performed. Please first perform a fit.")
            else:
                                
                if(bin_units):
                    temp_values = deepcopy(self._fit_values_bin)
                    temp_errors = deepcopy(self._fit_errors_bin)
                    
                else:
                    temp_values = deepcopy(self._fit_values)
                    temp_errors = deepcopy(self._fit_errors)
    
    
                ## This part rescales the afterpulse to an infinite gate length. 
                ## Please note that the effectiveness of the scaling factor depends on the ability to determine tau_Ap. 
                ## This may be challenging for some spectra where there is insufficient information between the peaks to calculate tau_Ap. 
                ## In that case, it is suggested that an appropriate tau_Ap is assumed (i.e. from overvoltages allowing an accurate assessment of tau_Ap)
                ## so that this factor can be reconstructed. 
    
                p_Ap_inf_sf = Beta_Inf(temp_values["tau_R"], temp_values["tau_Ap"])/Beta(temp_values["tau_R"], temp_values["tau_Ap"], temp_values["t_gate"])
    
    
                temp_values["p_Ap_inf"] = temp_values["p_Ap"]*p_Ap_inf_sf
                temp_errors["p_Ap_inf"] =  temp_errors["p_Ap"]*p_Ap_inf_sf ## TO DO: propogate errors correctly. 
    
    
    
                t_fit = self.GetFitTime(prefit=False)
                t_prefit = self.GetFitTime(prefit=True)
                    
                temp_values["fit_time"] = t_fit
                temp_values["prefit_time"] = t_prefit
     
                return temp_values, temp_errors
            
            
        ########################################################################################################
        ## GetPrefitResults()
        ########################################################################################################
        ##
        ## Returns pre-fit results and errors in the input charge unit.
        ##
        ########################################################################################################
            
        def GetPrefitResults(self, bin_units=True):
            if(bin_units):
                return self._prefit_values_bin, self._prefit_errors_bin
            else:
                return self._prefit_values, self._prefit_errors
            
            
        ########################################################################################################
        ## GetPeakInfo()
        ########################################################################################################
        ##
        ## Returns peak information obtained during the fitting procedure. 
        ##
        ########################################################################################################
            
        def GetPeakInfo(self):
            
            if(self._m_DRM is None):
                raise Exception ("Fit has not yet been performed. Please first perform a fit.")
            else:
                return self._hist_data
    
        
        ########################################################################################################
        ## GetChi2(prefit=False)
        ########################################################################################################
        ##
        ## VARIABLES:
        ## prefit(False): Evaluate the model using the prefit or fitted values
        ## 
        ##
        ##
        ## Calculates the Chi2 of the resultant fit relative to the original density estimate.
        ########################################################################################################
       
        
    
        
        def GetChi2(self, pars=None, prefit=False,  n_sigma_lims=[None, None]):
    
         
            _x_all = self._hist_data["bin_numbers"]
            _y_all = self._hist_data["counts"]
            _y_err_all = self._hist_data["counts_error"]
            
            _Q_0_prefit = self._prefit_values_bin["Q_0"]
            _sigma_0_prefit = self._prefit_values_bin["sigma_0"]
    
    
    
            if(pars is None):
                _y_hat_all = self.GetModel(_x_all, prefit=prefit, bin_units=True)
            else:
                _y_hat_all = DRM(_x_all, **pars)
                
            
            cond_count = (_y_all>1) 
    
          
            if(n_sigma_lims[0] is None):
                if(self._hist_data["truncate_nsigma0_do_min"] is not None):
                    _lim_nsigma0_low = _Q_0_prefit -self._hist_data["truncate_nsigma0_do_min"]*_sigma_0_prefit
                else:
                    _lim_nsigma0_low = -np.inf
            else:
                _lim_nsigma0_low = _Q_0_prefit - n_sigma_lims[0]*_sigma_0_prefit
        
            if(n_sigma_lims[1] is None):
                _lim_nsigma0_hi = np.inf
            else:
                _lim_nsigma0_hi = _Q_0_prefit + n_sigma_lims[1]*_sigma_0_prefit
          
           
            
            cond_nsigma0_low = (_x_all > _lim_nsigma0_low)
            cond_nsigma0_hi = (_x_all < _lim_nsigma0_hi)
                  
            conds = cond_count & cond_nsigma0_low & cond_nsigma0_hi
            
    
            
            _x = _x_all[conds]
            _y = _y_all[conds]
            _y_err = _y_err_all[conds]
            _y_hat = _y_hat_all[conds]
            _y_hat_err =  np.sqrt(_y_hat_all[conds])
            
        
    
            
                        
            chi2 = np.sum(((_y_hat - _y)/_y_hat_err)**2)
            ndof = len(_x)-10
            
           
    
            return chi2, ndof
        
    
        
        
        ########################################################################################################
        ## GetModel(x, prefit=False)
        ########################################################################################################
        ##
        ## VARIABLES:
        ## x: the x values over which to evaluate the model. 
        ## prefit (False): whether or not to use the prefit values in the model evaluation.
        ## 
        ##
        ## Evaluates model according to the fitted parameters and returns the Detector Response Model
        ########################################################################################################
        
    
        def GetModel(self, x, prefit=False, bin_units=False):
            
            if(self._m_DRM is None):
                raise Exception ("Fit has not yet been performed. Please first perform a fit.")
            else: 
                if(prefit): 
                    if(bin_units):
                        return DRM(x, **self._prefit_values_bin)
                    else:
                        return DRM(x, **self._prefit_values)
                else:
                    if(bin_units):
                        return DRM(x, **self._fit_values_bin)
                    else:
                        return DRM(x, **self._fit_values)
            
            
            
        ########################################################################################################
        ## GetBins(data, bw, bin_method)
        ########################################################################################################
        ##
        ## VARIABLES:
        ## data: the input data, either a 1D array of individual discharges or a 2D array of [bin centres, counts]
        ## bw: the bin width. If this is None, then the bin_method will be used to choose the binning if 1D
        ## bin_method: the method for binning 1D data. This can be 'knuth', 'freedman', or 'scott'
        ##
        ## Automated calculation of bins and bin widths for 2D prebinned charge measurements or 1D charge measurements. 
        ## Note: because spectra from scopes have been observed with variable bin widths, the program also rebins 2D histograms using the same bin width as the original spectrum.
        ########################################################################################################
        
        def GetBins(self, data, bw, bin_method, bin_0):
            
            
            if(data.ndim == 1):
                
                if(self._verbose):
                    print("Data is assumed 1D. Assuming list of charges.")
    
                if(bw is None):
                    if(self._verbose):
                        print("Bin width not set. Binning with {:s}".format(bin_method))
                        
                    if(bin_method == "knuth"):
                        bw = knuth_bin_width(data)/20
                    elif(bin_method == "freedman"):
                        bw = freedman_bin_width(data)
                    elif(bin_method == "scott"):
                        bw = scott_bin_width(data)
                    else:
                        raise Exception("Binning method not recognised. Please select bin_method from 'knuth', 'freedman' or 'scott'")
    
                x_min, x_max = data.min(), data.max()
    
            elif(data.ndim == 2):
                
                if(self._verbose):
                    print("Data is assumed 2D. Assuming input is a histogram, with columns [bin_centre, counts].")
                    
                _x, _y = data[:,0], data[:,1]
                data = np.array(list(chain.from_iterable([[__x]*int(__y) for __x,__y in zip(_x,_y)])))
                f_bin = interp1d(np.arange(0, len(_x)), _x)
                if(bw is None):
                    bw = abs(f_bin(1) - f_bin(0))
                idx_nonzero = np.squeeze(np.argwhere((_y>0)))
                x_min, x_max = _x[np.min(idx_nonzero)]-bw/2,  _x[np.max(idx_nonzero)]+bw/2
                
            else:
                raise Exception("Data is neither 1D nor 2D. Please provide the correct dimensions of data to the fit program.")
    
            if(bin_0 is not None):
                if(self._verbose):
                    print("Setting first bin to be {:3.3f} input charge units".format(bin_0))
                
                x_min = bin_0
            
            nbins = np.ceil((x_max - x_min) / bw)
            nbins = max(1, nbins)
            bins = x_min + bw * np.arange(nbins + 1)
            return data, bw, nbins, bins
        
    
    
        
        def PlotHistogram(self, figsize=(10.0,10.0), save_directory=None, label="Histogram", x_label="ADC"):
    
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            counts = self._hist_data["counts"]
            bin_numbers = self._hist_data["bin_numbers"]
            bin_centres = self._hist_data["bin_centres"]
            
            ax.step(bin_numbers, 
                    counts, 
                    #label="{:s} \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts)))),
                    label = "{:s}".format(label),
                    color="C0", 
                    where="mid",
                    lw=5)
            ax.grid(which="both")
            ax.set_ylabel("Counts", fontsize=self._plot_fontsize)
            ax.set_xlabel("Bin", fontsize=self._plot_fontsize)
            ax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", labelsize=self._plot_fontsize)
            ax.set_ylim([1,None])
            ax.set_yscale("log")
            ax.legend(fontsize=self._plot_legendfontsize)
            
            _f = interp1d(bin_numbers, bin_centres, bounds_error=False, fill_value="extrapolate")
            _f_inv = interp1d(bin_centres, bin_centres, bounds_error=False, fill_value="extrapolate")
            
            
    #         secax = ax.secondary_xaxis('top', functions=(_f, _f_inv))
    #         secax.set_xlabel('{:s}'.format(x_label), fontsize=self._plot_fontsize)
    #         secax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            
            
    #         mf = ScalarFormatter(useMathText=True)
    #         mf.set_powerlimits((-2,2))
    #         secax.xaxis.set_major_formatter(mf)
    #         offset = secax.xaxis.get_offset_text()
    #         offset.set_size(int(0.8*self._plot_fontsize))
            
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
            
    
            
            
        
        def PlotFFT(self, figsize=(10.0,10.0), save_directory=None,  label="Histogram"):
            
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            
            fft_freq = self._hist_data["fft_freq"]
            counts = self._hist_data["counts"]
            fft_amplitude = self._hist_data["fft_amplitude"]
            G_fft = self._hist_data["G_fft"]
            inv_G_fft = 1/G_fft
        
            ax.step(fft_freq,
                     fft_amplitude,
                     color="C0",
                     where="mid",
                     #label="{:s} \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts)))),
                     label = "{:s}".format(label),
                     lw=5)
            
            
            ax.axvline(x=inv_G_fft, color="purple", linestyle="--", lw=4, label="$G^{{*}}_{{FFT}}$ = {:3.3f} Bin".format(G_fft))
            
            ax.set_yscale("log")
            ax.set_xscale("log")
            ax.grid(which="both")
            ax.set_xlabel("Bin in Fourier Space", fontsize=25)
            ax.set_ylabel("Power Spectral Density", fontsize=25)
            ax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", labelsize=self._plot_fontsize)
            ax.legend(fontsize=self._plot_legendfontsize)
            
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
            
        def PlotBGSub(self, figsize=(10,10), save_directory=None, plot_bgsub=False, label="Histogram"):
            
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            counts = self._hist_data["counts"]
            counts_bg = self._hist_data["counts_bg"]
            counts_bgsub = self._hist_data["counts_bgsub"]
            bin_numbers = self._hist_data["bin_numbers"]
    
            if(plot_bgsub):
                
                
                ax.step(bin_numbers,
                         counts,
                         #label="{:s} \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts)))),
                         label = "{:s}".format(label),
                         color="C0",
                         where="mid",
                         lw=5)
                
                
                ax.step(bin_numbers,
                         counts_bgsub,
                         #label="{:s} - BG \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts_bgsub)))),
                         label = "{:s} - Background".format(label),
                         color="r",
                         where="mid",
                         lw=5)
                
            else:
                ax.step(bin_numbers,
                         counts,
                         #label="{:s} \n $N_{{events}}$ = {:s}".format(label,LatexFormat(float(np.sum(counts)))),
                         label = "{:s}".format(label),
                         color="C0",
                         where="mid",
                         lw=5)
                
                ax.fill_between(bin_numbers[counts_bg>0],
                         counts_bg[counts_bg>0],
                         #label="{:s}, BG \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts_bg)))),
                         label = "{:s}, Background".format(label),
                         color='none',
                         hatch="///",
                         edgecolor="r",
                         step="mid",
                         lw=5)
                
            ax.grid(which="both")
            ax.set_xlabel("Bin", fontsize=25)
            ax.set_ylabel("Counts", fontsize=self._plot_fontsize)
            ax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", labelsize=self._plot_fontsize)
            ax.set_ylim([1,None])
            ax.set_yscale("log")
            ax.legend(fontsize=self._plot_legendfontsize)
            
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
        
        
        def PlotPeaks(self, figsize=(10.0,10.0), save_directory=None,  label="Histogram"):
            
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            counts = self._hist_data["counts"]
            counts_bgsub = self._hist_data["counts_bgsub"]
            bin_numbers = self._hist_data["bin_numbers"]
            Q_0_est = self._hist_data["Q_0_est"]
            
            peak_position = self._hist_data["peaks"]["peak_position"]
    
            ax.step(bin_numbers, 
                    counts,
                    #label="{:s} \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts)))), 
                    label = "{:s}".format(label),
                    color="C0", 
                    lw=5, 
                    where="mid",
                    zorder=-5)
            
            ax.step(bin_numbers, counts_bgsub,
                    #label="{:s} - BG \n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(counts_bgsub)))),
                    label="{:s} - Background".format(label),
                         color="r",
                         lw=5,
                         where="mid",
                         zorder=-4)
                   
            
            color_vals = [self._cmap(_v) for _v in np.linspace(0,0.75, len(peak_position))]
            
            for peak_i, pp in enumerate(peak_position):
          
                if(peak_i == 0):
                    annotation_text = "$Q_{{0}}$"
                    color = "purple"
                    ls="--"
                
                    ax.axvline(x=pp, 
                          color=color,
                          linestyle=ls,
                          lw=2.5,
                          label = '{:s}'.format(annotation_text)
                         )
                    
                else:
                    annotation_text = "Peak Position {:d}".format(peak_i)
                    color = color_vals[peak_i]
                    ls="-"
                   
                    ax.axvline(x=pp, 
                          color=color,
                          linestyle=ls,
                          lw=2.5
                         )
    #                 plt.text(10.1,0, annotation_text, color=color, fontsize=self._plot_legend,rotation=90)
                    
               
    
            ax.scatter([Q_0_est],
                       [counts[np.argmin(abs(bin_numbers-Q_0_est))]],
                       marker="v",
                       s=200,
                       color="purple",
                       zorder=5,
                       label= "$Q^{est}_{{0}}$")
            
            ax.set_xlabel("Bin", fontsize=self._plot_fontsize)
            ax.set_ylabel("Counts", fontsize=self._plot_fontsize)
            ax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", labelsize=self._plot_fontsize)
            ax.grid(which="both")
            ax.legend(fontsize=self._plot_legendfontsize)
             
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
    
    
        def PlotVarianceFit(self, figsize=(10.0, 10.0), save_directory=None,  y_limits=[None, None], x_limits=[None, None]):
            
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            peak_index = self._hist_data["peaks"]["peak_index"]
            peak_variance = self._hist_data["peaks"]["peak_variance"]
            peak_variance_error = self._hist_data["peaks"]["peak_variance_error"]
            
            
            sigma_0 = self._prefit_values_bin["sigma_0"]
            dsigma_0 = self._prefit_errors_bin["sigma_0"]
            sigma_1 = self._prefit_values_bin["sigma_1"]
            dsigma_1 = self._prefit_errors_bin["sigma_1"]
            
            ax.errorbar( peak_index,
                         peak_variance,
                         yerr=peak_variance_error,
                         fmt="o",
                         lw=3,
                         markersize=10,
                         color="C0",
                         label="Variances of Peaks"
                        )
            
            ax.plot(peak_index,
                     Linear(peak_index,
                            sigma_1**2,
                            sigma_0**2
                            ),
                     color="C0",
                     linestyle="--",
                     lw=3,
                     label = "Linear Fit: \n $\sigma^{{2}}_{{0}}$ = {:s} $\pm$ {:s} Bin$^{{2}}$ \n $\sigma^{{2}}_{{1}}$ = {:s} $\pm$ {:s} Bin$^{{2}}$".format(
                                                                  LatexFormat(sigma_0**2),
                                                                  LatexFormat(2*sigma_0*dsigma_0),
                                                                  LatexFormat(sigma_1**1),
                                                                  LatexFormat(2*sigma_0*dsigma_1),
                    
                     ),
                    )
            
            
            
            ax.set_xlabel("Peak Index", fontsize=self._plot_fontsize)
            ax.set_ylabel("$\sigma^{2}_{peak}$ [Bin$^{2}$]", fontsize=self._plot_fontsize)
            ax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", labelsize=self._plot_fontsize)
            ax.grid(which="both")
            ax.set_ylim(y_limits)
            ax.set_xlim(x_limits)
            ax.legend(fontsize=self._plot_legendfontsize)
    
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
            
            
        def PlotMeanFit(self, figsize=(10.0, 10.0), y_limits=[None, None], x_limits=[None, None], save_directory=None):
            
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            bin_numbers = self._hist_data["bin_numbers"]
            
            peak_index = self._hist_data["peaks"]["peak_index"]
            peak_mean = self._hist_data["peaks"]["peak_mean"]
            peak_mean_error = self._hist_data["peaks"]["peak_mean_error"]
    
            Q_0 = self._prefit_values_bin["Q_0"]
            dQ_0 = self._prefit_errors_bin["Q_0"]
            G = self._hist_data["G_fft"]
            
            ax.errorbar( peak_index,
                         peak_mean,
                         yerr=peak_mean_error,
                         fmt="o",
                         markersize=10,
                         lw=3,
                         color="C0",
                         label="Means of Peaks"
                        )
            
            ax.plot( peak_index,
                     Linear(peak_index,
                            G,
                            Q_0,
                            ),
                     color="C0",
                     linestyle="--",
                     lw=3,
                     markersize=10,
                     label = "Linear Fit: \n $Q_{{0}}$ = {:s} $\pm$ {:s} Bin \n $G^{{*}}_{{FFT}}$ = {:s} Bin".format(
                                                                  LatexFormat(Q_0),
                                                                  LatexFormat(dQ_0),
                                                                  LatexFormat(G),
                    
                     ),
                    )
            
            
            ax.set_xlabel("Peak Index", fontsize=self._plot_fontsize)
            ax.set_ylabel("Peak Position [Bin]", fontsize=self._plot_fontsize)
    #         ax.set_ylabel("$\\langle Q_{{peak}} \\rangle$", fontsize=self._plot_fontsize)
            ax.tick_params(axis="x", labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", labelsize=self._plot_fontsize)
            ax.grid(which="both")
            ax.set_ylim(y_limits)
            ax.set_xlim(x_limits)
            ax.legend(fontsize=self._plot_legendfontsize)
            
            
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
            
            
            
            
            
        def PlotDCR(self, figsize=(10.0, 10.0),  save_directory=None, label = "Histogram"):
            
                    
            fig = plt.figure(figsize=figsize)
            ax = plt.subplot(111)
            
            counts = self._hist_data["counts"]
            bin_numbers = self._hist_data["bin_numbers"]
            bw = self._hist_data["bw"]
            
            
            Q_0 = self._prefit_values_bin["Q_0"]
            G = self._prefit_values_bin["G"]
          
            DCR = self._prefit_values_bin["DCR"]
            dDCR = self._prefit_errors_bin["DCR"]
            
            
            K = (bin_numbers - Q_0)/G
            
            
            cond_NN = (K>=0.45)&(K<=0.55)
            cond_Nc = (K<=0.5)
            cond_DCR = (K<=2.5)
            
            NN_sum = max(np.sum(counts[cond_NN]),1)
            NN = np.mean(counts[cond_NN])
            
            if(NN!=NN):
                NN = float(counts[np.argmin(abs(K-0.5))])
                NN_sum = float(NN)
            
            Nc = max(float(np.sum(counts[cond_Nc])), 1)
    
            
    
            ax.step(K[cond_DCR], 
                    counts[cond_DCR], 
                    label="{:s}".format(label), 
                    color="C0", 
                    where="mid",
                    lw=5)
        
        
    
            ax.fill_between(
                         K[cond_Nc],
                         counts[cond_Nc],
                         color='none',
                         hatch="///",
                         edgecolor="r",
                         lw=3,
                         step="mid",
                         #label = "$N_{{K \leq 0.5}}$={:d} events".format(int(Nc)) )
                         label = "$N_{{K \leq 0.5}}$")
            
            ax.fill_between(
                         K[cond_NN],
                         counts[cond_NN],
                         color='green',
                         edgecolor="g",
                         alpha=0.3,
                         step="mid",
                         #label = "$\\langle N \\rangle_{{0.45 < K \leq 0.55}}$={:3.1f} events".format(NN))
                         label = "$\\langle N \\rangle_{{0.45 < K \leq 0.55}}$")
        
            legend_title="$DCR$={:s} $\pm$ {:s} MHz".format(LatexFormat(DCR*1e3), LatexFormat(dDCR*1e3))
            
            ax.set_xlabel("K [p.e.]", fontsize=self._plot_fontsize)
            ax.grid(which="both")
            
            ax.set_ylabel("Counts", fontsize=self._plot_fontsize)
            ax.tick_params(axis="x", which='both', labelsize=self._plot_fontsize, rotation=45)
            ax.tick_params(axis="y", which='both', labelsize=self._plot_fontsize)
            ax.set_yscale("log")
            ax.legend(title=legend_title, title_fontsize=int(self._plot_legendfontsize), fontsize=int(self._plot_legendfontsize))
                
    
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
                    
    
              
        def PlotFit(self,
                    figsize=(10.0,10.0),
                    labelsize=None,
                    ticksize=None,
                    titlesize=None,
                    legendsize=None,
                    title=None,
                    xlabel = None,
                    scaled=False,
                    save_directory=None,
                    label=None,
                    y_limits = [1, None],
                    residual_limits = [-5, 5],
                    residualticksize=None,
                    linewidth_main = 5,  
                    x_limits = [None, None],
                    display=True,
                    prefit=False,
                    plot_in_bins=False
                   ):
        
        
            if(labelsize is None):
                labelsize=self._plot_fontsize
            
            if(ticksize is None):
                ticksize=self._plot_fontsize
                
            if(titlesize is None):
                titlesize = self._plot_fontsize
                
            if(legendsize is None):
                legendsize = self._plot_legendfontsize
                
            if(residualticksize is None):
                residualticksize =  int(0.8*self._plot_fontsize)
                
    
                
                
                
            if(plot_in_bins):
                x_all = self._hist_data["bin_numbers"]
                bw = 1
            else:
                x_all = self._hist_data["bin_centres"]
                bw = self._hist_data["bw"]
                
                
     
            y_all = self._hist_data["counts"]
            y_err_all = self._hist_data["counts_error"]
            
            Q_0_prefit = self._prefit_values_bin["Q_0"]
            sigma_0_prefit = self._prefit_values_bin["sigma_0"]
            
            
            if(self._hist_data["truncate_nsigma0_do_min"] is not None):
                lim_nsigma0 =  Q_0_prefit - self._hist_data["truncate_nsigma0_do_min"]*sigma_0_prefit
                cond_sel_nsigma0 = (self._hist_data["bin_numbers"]>lim_nsigma0)
              
            else:
                cond_sel_nsigma0 = np.full(len(x_all), True).astype(bool)
                
            x = x_all[cond_sel_nsigma0]
            y = y_all[cond_sel_nsigma0]
            y_err = y_err_all[cond_sel_nsigma0]
            
            y_hat = self.GetModel(x, prefit=prefit, bin_units=plot_in_bins)*bw
            y_hat_err = np.sqrt(y_hat)
           
            chi2, ndf = self.GetChi2(prefit=prefit)
                
    
    
            if(xlabel is None):
                if(plot_in_bins):
                    xlabel="Bin"
                else:
                    xlabel = "Q"
                
            fig = plt.figure(figsize=figsize)
            gs = gridspec.GridSpec(2, 1, height_ratios=[2, 1])
            ax0 = plt.subplot(gs[0])   
            if(label is None):
                label = "Histogram" 
    #         label="{:s},\n $N_{{events}}$ = {:s}".format(label, LatexFormat(float(np.sum(y))))
            label = "{:s}".format(label)
            ax0.step(x_all, y_all, label=label, lw=5, color="C0", where="mid")
            ax0.plot(x, y_hat, linestyle="--", label="Fit", lw=5, color="C1")
            ax0.plot([], [], ' ', label="$\\chi^{{2}}$/NDF = {:s}".format(LatexFormat(chi2/ndf)))
    
            
            ax0.set_ylabel("Counts".format(xlabel, xlabel), fontsize=labelsize)
            ax0.tick_params(axis="y", labelsize=ticksize)
            
    
            
            if(y_limits[0] is None):
                y_limits[0] = np.min(y[y>0])
            ax0.set_ylim(y_limits)
            ax0.set_xlim(x_limits)
            if(title is not None):
                ax0.set_title(title, fontsize=titlesize)
                
            ax0.grid(which="both")
            
            ax0.set_yscale("log")
            ax0.legend(fontsize=legendsize)
           
    
    
            ax1 = plt.subplot(gs[1], sharex = ax0)
            
            ax1.scatter(x, (y - y_hat)/(y_hat_err), color="C1") 
    
            ax1.tick_params(axis="x", labelsize=ticksize, rotation=45)
            ax1.tick_params(axis="y", labelsize=ticksize)
            ax1.set_ylabel("Pull", fontsize=labelsize)
            ax1.set_xlabel(xlabel, fontsize=labelsize)
            ax1.set_ylim(residual_limits)
            ax1.set_yticks(np.arange(int(np.floor(np.min(residual_limits))), 
                                     int(np.ceil(np.max(residual_limits)))))
            ax1.axhline(y=-1.0, color="purple", lw=2.5, linestyle="--")
            ax1.axhline(y=1.0, color="purple", lw=2.5, linestyle="--")
            ax1.tick_params(axis="x", labelsize=ticksize)
            ax1.tick_params(axis="y", labelsize=residualticksize)
            ax1.grid(which="both")
            
    
    
    
            fig.tight_layout()
            
            if(not plot_in_bins):
                mf = ScalarFormatter(useMathText=True)
                mf.set_powerlimits((-2,2))
                plt.gca().xaxis.set_major_formatter(mf)
                offset = ax1.xaxis.get_offset_text()
                offset.set_size(int(0.8*ticksize))
    
    
    
            fig.subplots_adjust(hspace=.0)
            if(save_directory is not None):
                print("Saving figure to {0}...".format(save_directory))
                fig.savefig(save_directory)
            if(display):
                plt.pause(0.01)
                fig.show()
            else:
                plt.close(fig)
            
            
            
            
        ########################################################################################################
        ## Fit(prefit=False)
        ########################################################################################################
        ##
        ## VARIABLES:             
        ## tau:  slow time-constant of SiPM pulse in nanoseconds
        ## tau_err: optional error to allow tau to float
        ## t_0: pre-gate integration window in nanoseconds.
        ## t_0_err: optional error to allow t_0 to float
        ## tau_R: recovery time of SiPM in nanoseconds
        ## tau_R_err: optional error to allow tau_R to float
        ## t_gate: gate length in nanoseconds
        ## t_gate_err: optional error to allow t_gate to float
        ## bw: optional bin width if data is 1D 
        ## truncate_nG: optional parameter to truncate distribution a certain fraction/number of estimated gain units before the estimated pedestal.
        ## peak_dist_nG: required minimum distance between peaks in gain units to register a peaks. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html
        ## peak_rel_height: Chooses the relative height at which the peak width is measured as a percentage of its prominence. 1.0 calculates the width of the peak at its lowest contour line while 0.5 evaluates at half the prominence height. Must be at least 0. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.peak_widths.html#scipy.signal.peak_widths
        ## bin_method: binning method for unbinned data. "knuth", "freedman" or "scott" rules are available. 
        ## alpha_peaks: the maximum percentile of the distribution a found peak is allowed to have. This determines the threshold on the selected peak positions. 
        ## 
        ## Main fit function of PeakOTron, which performs the prefit and main fit using Minuit. 
        ##
        ########################################################################################################
            
            
        def __MinuitFit(self, f_DRM, A):
            
                t_0 = time.time()
                ########################################################################################################
                ## Inititalise Minuit with hypothesis of no dark counts or afterpulsing. 
                ########################################################################################################
    
                m_DRM = Minuit(f_DRM,
                                 Q_0 = self._prefit_values_bin["Q_0"],
                                 G = self._prefit_values_bin["G"],
                                 mu = self._prefit_values_bin["mu"],
                                 lbda = self._prefit_values_bin["lbda"],
                                 sigma_0 = self._prefit_values_bin["sigma_0"],
                                 sigma_1 = self._prefit_values_bin["sigma_1"],
                                 tau_Ap = self._prefit_values_bin["tau_Ap"],
                                 p_Ap = epsilon(),
                                 DCR = epsilon(),
                                 tau = self._prefit_values_bin["tau"],
                                 tau_R = self._prefit_values_bin["tau_R"],
                                 t_gate = self._prefit_values_bin["t_gate"],
                                 t_0  = self._prefit_values_bin["t_0"],
                                 k_max = self._prefit_values_bin["k_max"],
                                 k_max_dcr = self._prefit_values_bin["k_max_dcr"],
                                 len_pad = self._prefit_values_bin["len_pad"],
                                 A = A
                              )
    
    
    
    
    
                m_DRM.errors["Q_0"]  =  self._prefit_errors_bin["Q_0"]
                m_DRM.errors["G"] =   self._prefit_errors_bin["G"]
                m_DRM.errors["mu"]  = self._prefit_errors_bin["mu"]
                m_DRM.errors["lbda"]  = self._prefit_errors_bin["lbda"]
                m_DRM.errors["sigma_0"]  = self._prefit_errors_bin["sigma_0"]
                m_DRM.errors["sigma_1"]  = self._prefit_errors_bin["sigma_1"]
                m_DRM.errors["tau_Ap"]  = self._prefit_errors_bin["tau_Ap"]
                m_DRM.errors["p_Ap"]  = self._prefit_errors_bin["p_Ap"]
                m_DRM.errors["DCR"]  = self._prefit_errors_bin["DCR"]
                m_DRM.errors["A"]  = np.sqrt(A)
    
    
                ########################################################################################################
                ## Set parameter limits
                ## These limits have been chosen to best constrain the possible values for each of the parameters. 
                ## NOTE: we are working in bin widths, so the minimum allowed pedestal position is the first/second bin for Q_0, G, sigma_0, sigma_1
                ########################################################################################################
    
    
            
                m_DRM.limits["G"] =  (1, None)
    
                m_DRM.limits["mu"]  = (0.01, None)
    
                m_DRM.limits["lbda"]  = (1e-6, 1-1e-4)
    
                m_DRM.limits["sigma_0"]  = (0.1, None)
    
                m_DRM.limits["sigma_1"]  = (0.1, None)
    
                m_DRM.limits["tau_Ap"]  = (3, self._prefit_values_bin["t_gate"]/2)
    
                m_DRM.limits["p_Ap"]  = (1e-6, 1-1e-4)
    
                m_DRM.limits["DCR"]  = (1e-6, None)
                
                m_DRM.limits["A"]  = (self._prefit_values_bin["A"] - 3*np.sqrt(self._prefit_values_bin["A"]), 
                                      self._prefit_values_bin["A"] + 3*np.sqrt(self._prefit_values_bin["A"]))
                
                # m_DRM.limits["A"]  = (1,None)
    
    
                ########################################################################################################
                ## Allow input tau, t_gate, tau_R and t_0 to float if errors are known. 
                ########################################################################################################
    
    
                if(self._fit_kwargs["tau_err"] is None):
                    m_DRM.fixed["tau"]=True
                else:
                    m_DRM.fixed["tau"]=False
                    m_DRM.errors["tau"] = self._prefit_errors_bin["tau"]
                    m_DRM.limits["tau"] = (max(epsilon(), self._prefit_values_bin["tau"]-self._prefit_errors_bin["tau"]),
                                           max(epsilon(), self._prefit_values_bin["tau"]+self._prefit_errors_bin["tau"])
                                          )
    
                if(self._fit_kwargs["t_gate_err"] is None):
                    m_DRM.fixed["t_gate"]=True
                else:
    
                    m_DRM.fixed["t_gate"]=False
                    m_DRM.errors["t_gate"] = self._prefit_errors_bin["t_gate"]
                    m_DRM.limits["t_gate"] = (max(epsilon(), self._prefit_values_bin["t_gate"]-self._prefit_errors_bin["t_gate"]),
                                              max(epsilon(), self._prefit_values_bin["t_gate"]+self._prefit_errors_bin["t_gate"])
                                             )
    
    
                if(self._fit_kwargs["t_0_err"] is None):
                    m_DRM.fixed["t_0"]=True
                else:
                    m_DRM.fixed["t_0"]=False
                    m_DRM.errors["t_0"] = self._prefit_errors_bin["t_0"]
                    m_DRM.limits["t_0"] = (max(epsilon(), self._prefit_values_bin["t_0"]-self._prefit_errors_bin["t_0"]),
                                              max(epsilon(), self._prefit_values_bin["t_0"]+self._prefit_errors_bin["t_0"])
                                              )
    
    
    
                if(self._fit_kwargs["tau_R_err"] is None):
                    m_DRM.fixed["tau_R"]=True
                else:
                    m_DRM.fixed["tau_R"]=False
                    m_DRM.errors["tau_R"] =  self._prefit_errors_bin["tau_R"]
                    m_DRM.limits["tau_R"] = (max(epsilon(), self._prefit_values_bin["tau_R"]-self._prefit_errors_bin["tau_R"]),
                                              max(epsilon(), self._prefit_values_bin["tau_R"]+self._prefit_errors_bin["tau_R"])
                                              )
    
    
    
                m_DRM.fixed["k_max"]=True
                m_DRM.fixed["k_max_dcr"]=True
                m_DRM.fixed["len_pad"]=True
    
    
    
                ########################################################################################################
                ##Perform fit under hypothesis there are no dark counts, afterpulses. 
                ########################################################################################################
    
                m_DRM.fixed["Q_0"]  =  False
                m_DRM.fixed["G"] =   False
                m_DRM.fixed["mu"]  = False
                m_DRM.fixed["lbda"]  = False
                m_DRM.fixed["sigma_0"]  = False
                m_DRM.fixed["sigma_1"]  = False
                m_DRM.fixed["p_Ap"]=True
                m_DRM.fixed["tau_Ap"]=True
                m_DRM.fixed["DCR"]=True
                m_DRM.fixed["A"]=False  
                
                
                if(self._verbose):
                    print("Fitting basic light...")
    
                m_DRM.strategy=1
                m_DRM.errordef=m_DRM.LIKELIHOOD
                m_DRM.migrad(ncall = self._n_call_minuit,
                             iterate= self._n_iterations_minuit)
    
                
                if(self._verbose):
                    print("First pass complete...")
                    print(m_DRM)
                    
                if(self._verbose):
                    print("Fixing basic light and fitting noise...")   
                    
                
                m_DRM.fixed["Q_0"]  =  True
                m_DRM.fixed["G"] =   True
                m_DRM.fixed["mu"]  = True
                m_DRM.fixed["lbda"]  = True
                m_DRM.fixed["sigma_0"]  = True
                m_DRM.fixed["sigma_1"]  = True
                m_DRM.fixed["p_Ap"]=False
                m_DRM.fixed["tau_Ap"]=False
                m_DRM.fixed["DCR"]=False
                m_DRM.values["DCR"] = self._prefit_values_bin["DCR"]
                m_DRM.values["p_Ap"] = self._prefit_values_bin["p_Ap"]
                m_DRM.fixed["A"]=False  
              
                m_DRM.strategy=1
                m_DRM.errordef=m_DRM.LIKELIHOOD
                m_DRM.migrad(ncall = self._n_call_minuit,
                             iterate= self._n_iterations_minuit)
                
                if(self._verbose):
                    print("Second pass complete...")
                    print(m_DRM)
    
            
                if(self._verbose):
                    print("Fitting full model...")
                    
          
                m_DRM.fixed["Q_0"]  =  False
                m_DRM.fixed["G"] =   False
                m_DRM.fixed["mu"]  = False
                m_DRM.fixed["lbda"]  = False
                m_DRM.fixed["sigma_0"]  = False
                m_DRM.fixed["sigma_1"]  = False
                m_DRM.fixed["p_Ap"]=False
                m_DRM.fixed["tau_Ap"]=False
                m_DRM.fixed["DCR"]=False
                m_DRM.fixed["A"]=False  
                    
                m_DRM.strategy=1
                m_DRM.migrad(ncall = self._n_call_minuit, 
                             iterate= self._n_iterations_minuit)
                     
                m_DRM.hesse()
                
      
                t_1 = time.time()
      
                
                time_fit = t_1 - t_0
                if(self._verbose):
                    print("Prefit took {:3.3f} s, Fit took {:3.3f} s".format(self._time_prefit, 
                                                                             time_fit))
                    print(m_DRM)
                    
                    
                    
              
                
                return m_DRM, time_fit
        
        
            
            
                
        def Fit(self, data, **kwargs):
            
            ########################################################################################################
            ## Preparation
            ########################################################################################################
            ##
            ## 1. Data dictionaries are initialised if previously filled.
            ## 2. tau, tauR, t_0 and t_gate are checked to ensure they are provided to the program. 
            ## 3. Perform checks of input parameters to ensure no disallowed values 
            ########################################################################################################
            
            self.InitKwargs()
            
            
            if not set(kwargs.keys()).issubset(self._fit_kwargs.keys()):
                wrong_keys = list(set(kwargs.keys()).difference(set(self._fit_kwargs.keys())))
                
                raise Exception ("Arguments {:s} not recognised.".format(",".join(wrong_keys)))
                
    
            if not "tau" in kwargs.keys():
                raise Exception ("Please provide 'tau' (slow component of SiPM pulse in nanoseconds).")
                
            elif(not isinstance(kwargs["tau"], float) or kwargs["tau"]<=0):
                raise Exception ("Please set 'tau' (slow component of SiPM pulse in nanoseconds) to a positive float.")   
                
                
            if not "tau_R" in kwargs.keys():
                raise Exception ("Please provide 'tau_R' (recovery time of SiPM in nanoseconds).")
                
            elif(not isinstance(kwargs["tau_R"], float) or kwargs["tau_R"]<=0):
                raise Exception ("Please set 'tau' (slow component of SiPM pulse in nanoseconds) to a positive float.") 
                
                
            if not "t_0" in kwargs.keys():
                raise Exception ("Please provide 't_0' (integration region before start of SiPM pulse integration gate in nanoseconds).")
                
            elif(kwargs["t_0"]<=0):
                raise Exception ("Please set 't_0' (integration region before start of SiPM pulse integration gate in nanoseconds) to a positive float.")   
                
                
                
            if not "t_gate" in kwargs.keys():
                raise Exception ("Please provide 't_gate' (length of SiPM pulse integration gate) in nanoseconds.")
                
            elif(kwargs["t_0"]<=0):
                raise Exception ("Please set 't_0' (integration region before start of SiPM pulse integration gate in nanoseconds) to a positive float.")    
                
    
            self._fit_kwargs.update(kwargs)
            
                       
            if self._fit_kwargs["min_n_events_peak"] is not None:
                if(not isinstance(self._fit_kwargs["min_n_events_peak"], int)):
                    raise Exception ("Please set max_n_events peak (max events to fit Gaus to peak) as an integer" )
            else:
                self._fit_kwargs["min_n_events_peak"] = self._min_n_events_peak
                
                
            if self._fit_kwargs["truncate_nsigma0_up"] is not None:
                
                if(not isinstance(self._fit_kwargs["truncate_nsigma0_up"], float)):
                    raise Exception ("Please set truncate_nsigma0_up (# of sigma0 before pedestal to for Chi2/NDF to be evaluated) as a positive float" )
                    
                elif(self._fit_kwargs["truncate_nsigma0_up"]<=0):
                    raise Exception ("Please set truncate_nsigma0_up  (# of sigma0 before pedestal to for Chi2/NDF to be evaluated) as a positive float" )
                
                
            if self._fit_kwargs["truncate_nsigma0_do"] is not None:
                
                if(not isinstance(self._fit_kwargs["truncate_nsigma0_do"], float)):
                    raise Exception ("Please set truncate_nsigma0_do (max # of sigma0 before pedestal to for to be scanned) as a positive float between 0.0 and 4.0" )
                    
                elif(self._fit_kwargs["truncate_nsigma0_do"]<=0 or self._fit_kwargs["truncate_nsigma0_do"]>4):
                    raise Exception ("Please set truncate_nsigma0_do  (# of sigma0 before pedestal to for Chi2/NDF to be evaluated) as a positive float between 0.0 and 4.0" )
                  
                
    
            if self._fit_kwargs["truncate_nsigma0_chi2scanlim"] is not None:
                
                if(not isinstance(self._fit_kwargs["truncate_nsigma0_chi2scanlim"], float)):
                    raise Exception ("Please set truncate_nsigma0_chi2scanlim (minimum Chi2/NDF for a fit to be selected in a scan) as a positive float" )
                    
                elif(self._fit_kwargs["truncate_nsigma0_chi2scanlim"]<0):
                    raise Exception ("Please set truncate_nsigma0_chi2scanlim (minimum Chi2/NDF for a fit to be selected in a scan) as a positive float" )
    
                        
            if self._fit_kwargs["bw"] is not None:
                if(not isinstance(self._fit_kwargs["bw"], float)):
                    raise Exception ("Please set bw (bin width) to a positive float." )
                if(self._fit_kwargs["bw"]<=0):
                    raise Exception ("Please set bw (bin width) to a positive float." )
                    
            if self._fit_kwargs["bin_0"] is not None:
                if(not isinstance(self._fit_kwargs["bin_0"], float)):
                    raise Exception ("Please set bin_0 (first bin position) to a float." )
      
    
            if(self._verbose):
                print("Prefitting...")
                
                
            ########################################################################################################
            ## Prefitting routine applied
            ########################################################################################################
        
          
            t_0 = time.time()
            self.GetPreFit(data, **self._fit_kwargs)
            t_1 = time.time()
            
            self._time_prefit = t_1-t_0
            
            if(self._verbose):
                print("Prefitting complete.")
                
            
            _offset = self._hist_data["bc_min"]
            _scale =  self._hist_data["bw"]
                
            if(self._fit_kwargs["prefit_only"]):
                
                _prefit_values_temp = self._prefit_values_bin.copy()
                _prefit_errors_temp = self._prefit_errors_bin.copy()
        
                for key, value in _prefit_values_temp.items():
                    if(key=="Q_0"):
                        _prefit_values_temp[key] = _offset + value*_scale
                    elif(key=="G"): 
                        _prefit_values_temp[key] = value*_scale
                    elif(key=="sigma_0"):
                        _prefit_values_temp[key] = value*_scale
                    elif(key=="sigma_1"):
                        _prefit_values_temp[key] = value*_scale
                    else:
                        _prefit_values_temp[key] = value
    
                for key, value in _prefit_errors_temp.items():
                    if(key=="Q_0"):
                        _prefit_errors_temp[key] = value*_scale
                    elif(key=="G"):
                        _prefit_errors_temp[key] = value*_scale
                    elif(key=="sigma_0"):
                        _prefit_errors_temp[key] = value*_scale
                    elif(key=="sigma_1"):
                        _prefit_errors_temp[key] = value*_scale
                    else:
                        _prefit_errors_temp[key] = value
    
                self._prefit_values.update(_prefit_values_temp)
                self._prefit_errors.update(_prefit_errors_temp)
                
    
    
                
            else:
                
                
                
                ########################################################################################################
                ## Truncate fit if supplied factor.
                ########################################################################################################
            
            
    #                       lim_nsigma0 = self._prefit_values_bin["Q_0"] - self._fit_kwargs["truncate_nsigma0"]*self._prefit_values_bin["sigma_0"]
    #                 if(self._verbose):
    #                     print("Truncating fit, starting from {:3.3f} estimated sigma_0 from estimated pedestal ({:3.3f} bins) ...".format(self._fit_kwargs["truncate_nsigma0"], lim_nsigma0))
            
            
            
                if(self._fit_kwargs["truncate_nsigma0_do"] is not None):
                        
                        nsigma_0_scan_range = np.linspace(4.0, 
                                                          self._fit_kwargs["truncate_nsigma0_do"], 
                                                          np.floor((4-self._fit_kwargs["truncate_nsigma0_do"])/0.5).astype(int)+1)
                        
                        
               
                        if(self._verbose):
                            print("Performing full truncation scan. Evaluating nsigma0 = {0} before pedestal".format(nsigma_0_scan_range))
                            
                            
                        if(self._fit_kwargs["truncate_nsigma0_chi2scanlim"] is not None):
                            
                            if(self._verbose):
                                print("Minimum Chi2/NDF for convergence: {:3.3f}".format(self._fit_kwargs["truncate_nsigma0_chi2scanlim"]))
                        
                            else:
                                print("Best loss taken.")
                                
                                
                        if(self._fit_kwargs["truncate_nsigma0_up"] is not None):
                           
                            
                            _nsigma0hi = self._fit_kwargs["truncate_nsigma0_up"]
                        else:
                            _nsigma0hi = 2
                            
                        if(self._verbose):
                                print("Evaluating up to nsigma0 = {:3.3f} after pedestal".format(_nsigma0hi))
                            
                            
       
                        m_DRMs = np.full(len(nsigma_0_scan_range), None)
                        chi2ndfs_range =  np.full(len(nsigma_0_scan_range), None)
                        chi2ndfs_full =  np.full(len(nsigma_0_scan_range), None)
                        time_fits = np.full(len(nsigma_0_scan_range), 0.0)
                    
                        Q_0_prefit =  self._prefit_values_bin["Q_0"]
                        sigma_0_prefit = self._prefit_values_bin["sigma_0"]
                        
                        fit_iters = 0
    
    
    
                        for fit_i, _nsigma0low in enumerate(nsigma_0_scan_range):
    
                            fit_iters+=1 
    
                            lim_nsigma0 = Q_0_prefit - _nsigma0low*sigma_0_prefit
                            
                            
                            print("Truncating fit, starting from {:3.3f} estimated sigma_0 from estimated pedestal ({:3.3f} bins) ...".format(_nsigma0low, lim_nsigma0))
    
    
                            cond_sel_nsigma0 = (self._hist_data["bin_numbers"]>lim_nsigma0)
    
    
    
                            f_DRM =  BinnedLH(DRM, 
                                              self._hist_data["bin_numbers"][cond_sel_nsigma0],  
                                              self._hist_data["counts"][cond_sel_nsigma0], 1)
    
    
                            m_DRM, time_fit = self.__MinuitFit(f_DRM, np.sum(self._hist_data["counts"][cond_sel_nsigma0]))
    
    
                            chi2_range, ndof_range = self.GetChi2(pars = m_DRM.values.to_dict(),
                                                                 n_sigma_lims = [_nsigma0low, _nsigma0hi]
                                                                 )
                            
                            red_chi2_range = chi2_range/ndof_range
                            
                            
                            chi2_full, ndof_full = self.GetChi2(pars = m_DRM.values.to_dict(),
                                                                 n_sigma_lims = [_nsigma0low, None]
                                                                 )
                            
                            red_chi2_full = chi2_full/ndof_full
    
    
                            m_DRMs[fit_i] = m_DRM
                            chi2ndfs_range[fit_i] = red_chi2_range
                            chi2ndfs_full[fit_i] = red_chi2_full
                            time_fits[fit_i] = time_fit
    
    
                            if(self._fit_kwargs["truncate_nsigma0_chi2scanlim"] is not None):
                                if(red_chi2_range<self._fit_kwargs["truncate_nsigma0_chi2scanlim"]):
                                    if(self._verbose):
                                        print("Fit chi2/NDF less than limit. (Fit: {:3.3f} < Limit {:3.3f}. Scan complete".format(red_chi2, 
                                                                                                                       self._fit_kwargs["truncate_nsigma0_chi2scanlim"]))
    
                                    self._m_DRM = deepcopy(m_DRM)
                                    self._time_fit = np.sum(time_fits)
                                    self._hist_data["truncate_nsigma0_do_min"] = _nsigma0low
                                    
    
                                    break
    
                                else:
                                    if(self._verbose):
                                        print("Fit chi2/NDF greater than limit. (Fit: {:3.3f} < Limit {:3.3f}. Performing next scan step...".format(red_chi2, 
                                                                                                                          self._fit_kwargs["truncate_nsigma0_chi2scanlim"])) 
                                
                        
                        if(fit_iters==len(nsigma_0_scan_range)):
                            fit_i_best = np.argmin(chi2ndfs_range)
                            if(self._verbose):
                                print("Scan Summary:")
                                for fit_i, _nsigma0 in enumerate(nsigma_0_scan_range):
                                    if(fit_i==fit_i_best):
                                        is_best = True
                                    else:
                                        is_best = False
    
                                    print("n_sigma0 = {:3.3f}\tchi2/NDF in pedestal range = {:3.3f}\tBest = {:s}".format(_nsigma0, chi2ndfs_range[fit_i], str(is_best)))
    
    
                                print("Scan complete. Using best fit at {:3.3f}".format(nsigma_0_scan_range[fit_i_best]))
    
                            self._m_DRM = deepcopy(m_DRMs[fit_i_best])
                            self._time_fit = np.sum(time_fits)
    
                            #print(self._hist_data["truncate_nsigma0_do_min"] )
                            self._hist_data["truncate_nsigma0_do_min"] = nsigma_0_scan_range[fit_i_best]
                            #print(self._hist_data["truncate_nsigma0_do_min"] )
                else:
                      
                    f_DRM = BinnedLH(DRM, 
                                     self._hist_data["bin_numbers"], 
                                     self._hist_data["counts"], 1)
                    
                    
                    m_DRM, time_fit = self.__MinuitFit(f_DRM, np.sum(self._hist_data["counts"]))
                    
    
                    
                    
                    self._time_fit = time_fit 
                    self._m_DRM = deepcopy(m_DRM)
                    
    
                ########################################################################################################
                ##Rescale fitted parameters back to their input units. 
                ########################################################################################################
    
    
                for key, value in self._m_DRM.values.to_dict().items():
                    if(key=="Q_0"):
                        self._fit_values[key] = _offset + value*_scale
                    elif(key=="G"):
                        self._fit_values[key] = value*_scale
                    elif(key=="sigma_0"):
                        self._fit_values[key] = value*_scale
                    elif(key=="sigma_1"):
                        self._fit_values[key] = value*_scale
                    else:
                        self._fit_values[key] = value
                        
                    self._fit_values_bin[key] = value
    
    
    
                for key, value in self._m_DRM.errors.to_dict().items():
                    if(key=="Q_0"):
                        self._fit_errors[key] = value*_scale
                    elif(key=="G"):
                        self._fit_errors[key] = value*_scale
                    elif(key=="sigma_0"):
                        self._fit_errors[key] = value*_scale
                    elif(key=="sigma_1"):
                        self._fit_errors[key] = value*_scale
                    else:
                        self._fit_errors[key] = value
                    
                    self._fit_errors_bin[key] = value
    
    
                _prefit_values_temp = self._prefit_values_bin.copy()
                _prefit_errors_temp = self._prefit_errors_bin.copy()
        
                for key, value in _prefit_values_temp.items():
                    if(key=="Q_0"):
                        _prefit_values_temp[key] = _offset + value*_scale
                    elif(key=="G"): 
                        _prefit_values_temp[key] = value*_scale
                    elif(key=="sigma_0"):
                        _prefit_values_temp[key] = value*_scale
                    elif(key=="sigma_1"):
                        _prefit_values_temp[key] = value*_scale
                    else:
                        _prefit_values_temp[key] = value
    
                for key, value in _prefit_errors_temp.items():
                    if(key=="Q_0"):
                        _prefit_errors_temp[key] = value*_scale
                    elif(key=="G"):
                        _prefit_errors_temp[key] = value*_scale
                    elif(key=="sigma_0"):
                        _prefit_errors_temp[key] = value*_scale
                    elif(key=="sigma_1"):
                        _prefit_errors_temp[key] = value*_scale
                    else:
                        _prefit_errors_temp[key] = value
    
                self._prefit_values.update(_prefit_values_temp)
                self._prefit_errors.update(_prefit_errors_temp)
                
      
            
        ########################################################################################################
        ## GetPreFit(prefit=False)
        ########################################################################################################
        ##
        ## VARIABLES:             
        ## tau:  slow time-constant of SiPM pulse in nanoseconds
        ## tau_err: optional error to allow tau to float
        ## t_0: pre-gate integration window in nanoseconds.
        ## t_0_err: optional error to allow t_0 to float
        ## tau_R: recovery time of SiPM in nanoseconds
        ## tau_R_err: optional error to allow tau_R to float
        ## t_gate: gate length in nanoseconds
        ## t_gate_err: optional error to allow t_gate to float
        ## bw: optional bin width if data is 1D 
        ## truncate_nG: optional parameter to truncate distribution a certain fraction/number of estimated gain units before the estimated pedestal.
        ## peak_dist_nG: required minimum distance between peaks in gain units to register a peaks. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html
        ## peak_rel_height: Chooses the relative height at which the peak width is measured as a percentage of its prominence. 1.0 calculates the width of the peak at its lowest contour line while 0.5 evaluates at half the prominence height. Must be at least 0. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.peak_widths.html#scipy.signal.peak_widths
        ## bin_method: binning method for unbinned data. "knuth", "freedman" or "scott" rules are available. 
        ## alpha_peaks: the maximum percentile of the distribution a found peak is allowed to have. This determines the threshold on the selected peak positions. 
        ## 
        ## Perform pre-fitting routine.
        ########################################################################################################  
            
            
                
    
        def GetPreFit(self, data, **kwargs):
            
            ########################################################################################################
            ##Get reconstructed data, bin width, number of bins and bin values
            ########################################################################################################
           
            
            data, bw, nbins, bins = self.GetBins(data, kwargs["bw"], kwargs["bin_method"], kwargs["bin_0"]) 
             
            counts, _ = np.histogram(data, bins=bins)
            counts_error = np.where(counts<1, 1, np.sqrt(counts))
            bin_centres =  (bins[:-1] + bins[1:])/2.
            nbins = len(bin_centres)
            bin_numbers = np.arange(0, nbins)
            
            
            self._len_pad = nbins
            
            
            
            ########################################################################################################
            ##  Perform FFT and calculate Power Spectrum. The inverse of the frequency of the maximum amplitude yields the gain. 
            ########################################################################################################
    
    
            nbins_pad = nbins+2*nbins    
            counts_pad = np.pad(counts, (nbins, nbins), "constant", constant_values=0)
    
    
            fhat = np.fft.fft(counts_pad) #computes the fft
            psd = fhat * np.conj(fhat)/nbins_pad
            fft_freq_orig = (1/nbins_pad) * np.arange(nbins_pad) #frequency array
            idxs_half = np.arange(1, np.floor(nbins_pad/2), dtype=np.int32)
            fft_amplitude = np.abs(psd[idxs_half])
            fft_freq = fft_freq_orig[idxs_half]
    
            try:
                f_interp_fft = InterpolatedUnivariateSpline(fft_freq, fft_amplitude, k=4)
            except:
                raise Exception("Interpolation of FFT failed. Fit cannot proceed.")
    
            try:
                fft_cr_pts = f_interp_fft.derivative().roots()
            except:
                raise Exception("Root finding of FFT failed. Fit cannot proceed.")
    
            if(len(fft_cr_pts)<1):
                raise Exception("No roots found. Fit cannot proceed.")
    
    
            fft_cr_vals = f_interp_fft(fft_cr_pts)
    
            inv_G_fft = fft_cr_pts[np.argmax(fft_cr_vals)]
            G_fft = 1/inv_G_fft
            
            
            self._len_pad = np.around(G_fft, 0).astype(int)
    
    
            ########################################################################################################
            #Estimate the position of the pedestal by minimising the GP gain w.r.t the FFT Gain
            ########################################################################################################
    
            mu_unsub = HistMean(counts, bin_numbers)
            sigma = HistStd(counts, bin_numbers)
            gamma = HistSkew(counts, bin_numbers)
    
    
         
            f_interp_counts = UnivariateSpline(bin_numbers, counts, w=1/counts_error, k=4)
    
    
            try:
                counts_cr_pts = f_interp_counts.derivative().roots()
            except:
                raise Exception("Root finding of spectrum failed. Fit cannot proceed.")
    
            if(len(counts_cr_pts)<1):
                raise Exception("No roots of spectrum found. Fit cannot proceed.")
    
            
            counts_cr_vals = f_interp_counts(counts_cr_pts)
            max_peak = counts_cr_pts[np.argmax(counts_cr_vals)]
            
            
            f_pedestal = lambda Q_0: (GP_gain(mu_unsub-Q_0, sigma, gamma) - G_fft)**2
            m_pedestal = Minuit(f_pedestal, Q_0 = max_peak)
            m_pedestal.errordef=m_pedestal.LEAST_SQUARES
            m_pedestal.limits["Q_0"] = (0, max_peak+epsilon())
            m_pedestal.migrad()
            Q_0_est = m_pedestal.values["Q_0"]
            
            
            idx_peaks = np.sort(np.concatenate( [np.arange(max_peak-G_fft, -G_fft/2, -G_fft), 
                                                np.array([max_peak]),
                                                np.arange(max_peak+G_fft, nbins+G_fft/2, G_fft),
    
                                             ] ))
            
            idx_Q_0 = np.argmin(abs(idx_peaks-Q_0_est))
            idx_peaks = idx_peaks[idx_Q_0:-1]
            
    
      
            
            ########################################################################################################
            #Select maximum peak position, and step in units of gain up until either end of the spectrum
            ########################################################################################################
    
            ########################################################################################################
            # - Select pedestal as closest peak position to estimated pedestal.
            # - Select only peaks up to pedestal
            # - Get midpoints and end values.
            # - Linearly interpolate midpoints
            # - Subtract 'background', and threshold to be a minimum of zero.
            ########################################################################################################
    
    
            counts_bg = np.full(len(idx_peaks)-1, None)
            idx_bg = np.full(len(idx_peaks)-1, None)
            
            
            for i_peak in range(len(idx_peaks)-1):
                last_peak= idx_peaks[i_peak]
                next_peak= idx_peaks[i_peak+1]
                mid_peak = idx_peaks[i_peak] + G_fft/2
    
                cond_sel_interpeak = (counts_cr_pts>=last_peak) & (counts_cr_pts<=next_peak)
                
                if(sum(cond_sel_interpeak)>0):
                    
                    counts_cr_pts_interpeak = counts_cr_pts[cond_sel_interpeak]
                    counts_cr_values_interpeak = counts_cr_vals[cond_sel_interpeak]
    
                    idx_min_interpeak = np.argmin(counts_cr_values_interpeak)
    
                    count_min_interpeak = counts_cr_values_interpeak[idx_min_interpeak]
                    idx_min_interpeak = counts_cr_pts_interpeak[idx_min_interpeak]
                
                    
                else:
                    if(self._verbose):
                        print("No local minimum found in peak range. Using value at peak + gain/2")
                    
                    idx_min_interpeak = mid_peak
                    count_min_interpeak = counts[np.argmin(abs(bin_numbers-mid_peak))]
                    
                idx_bg[i_peak] = idx_min_interpeak
                counts_bg[i_peak] = count_min_interpeak
                      
                    
            try:
                arg_sort_bg = np.argsort(idx_bg)
                idx_bg = idx_bg[arg_sort_bg]
                counts_bg = counts_bg[arg_sort_bg]
    
                first_nz_bin = bin_numbers[np.argmax(counts>0)]
                counts_first_nz_bin = counts[np.argmax(counts>0)] 
    
                idx_bg = np.pad(idx_bg, (1,0), "constant", constant_values=first_nz_bin)
                counts_bg = np.pad(counts_bg, (1,0), "constant", constant_values=counts_first_nz_bin)
    
                f_interp_bg = interp1d(idx_bg, counts_bg, bounds_error=False, fill_value=(0,0))
    
                counts_bg = f_interp_bg(bin_numbers).astype(float)
                counts_bgsub = counts - counts_bg
                counts_bgsub = np.where(counts_bgsub<0, 0, counts_bgsub)
                counts_bgsub_error = np.where(counts_bgsub>1, np.sqrt(counts_bgsub), 1)
                counts_bgsub_error = np.sqrt(counts_bgsub_error**2 + counts_error**2)
    
            except:
                if(self._verbose):
                    print("Background subtraction failed. Setting background subtracted spectrum to have no events.")
                counts_bgsub = counts
                counts_bg = np.zeros_like(counts)
                counts_bgsub_error = counts_error
                
               
            
            ########################################################################################################
            #Find maximum from spline fit of background subtracted 
            ########################################################################################################
    
                
            try:
                f_interp_counts_bgsub = UnivariateSpline(bin_numbers, 
                                                         counts_bgsub,
                                                         w = 1/counts_bgsub_error,
                                                         k=4)
            except:
                raise Exception("Interpolation of background-subtracted spectrum failed. Fit cannot proceed.")
    
            try:
                counts_cr_pts_bgsub = f_interp_counts_bgsub.derivative().roots()
            except:
                raise Exception("Root finding of background-subtracted spectrum failed. Fit cannot proceed.")
    
            if(len(counts_cr_pts_bgsub)<1):
                raise Exception("No roots found in background-subtracted spectrum. Fit cannot proceed.")
    
            
            counts_cr_vals_bgsub = f_interp_counts_bgsub(counts_cr_pts_bgsub)
            max_peak = counts_cr_pts_bgsub[np.argmax(counts_cr_vals_bgsub)]
                
            idx_peaks = np.sort(np.concatenate( [np.arange(max_peak-G_fft, -G_fft/2, -G_fft), 
                                                np.array([max_peak]),
                                                np.arange(max_peak+G_fft, nbins+G_fft/2, G_fft)] ))
            
            ########################################################################################################
            # - Select pedestal as closest peak position to estimated pedestal.
            # - Select only peaks up to pedestal
            # - Update pedestal position.
            ########################################################################################################
    
            
            idx_Q_0 = np.argmin(abs(idx_peaks-Q_0_est))
            idx_peaks = idx_peaks[idx_Q_0:-1]
            Q_0 = idx_peaks[0]
            
    
            ########################################################################################################
            # - Find means and variances of all peaks in background-subtracted spectrum with greater than 100 events.
            # - Fit line to means to get Gain/Pedestal - correction for binning
            # - Fit line to variances to Get sigma0/sigma1
            ########################################################################################################
    
            
            
            ####################
            #ITERATIVE GAUS FIT METHOD
            #####################
            
    
            
            idx_peak_mus = np.full(len(idx_peaks), np.nan)
            idx_peak_dmus = np.full(len(idx_peaks), np.nan)
            idx_peak_sigmas =  np.full(len(idx_peaks), np.nan)
            idx_peak_dsigmas =  np.full(len(idx_peaks), np.nan)
            idx_peak_vars =  np.full(len(idx_peaks), np.nan)
            idx_peak_dvars =  np.full(len(idx_peaks), np.nan)
            idx_peak_numbers= np.arange(0, len(idx_peaks)).astype(int)
            
    
            f_gaus = lambda x, mu, sigma, A: A*norm.pdf(x, loc=mu, scale=sigma)
            
            for p_i, peak in zip(idx_peak_numbers, idx_peaks):
                
                min_rng_peak = peak - G_fft/2
                max_rng_peak = peak + G_fft/2
    
                cond_sel_peak = (bin_numbers>min_rng_peak) & (bin_numbers<max_rng_peak)
    
                counts_bgsub_peak = counts_bgsub[cond_sel_peak]
                bin_numbers_peak = bin_numbers[cond_sel_peak]
    
                N_peak = np.around(np.sum(counts_bgsub_peak), 0).astype(int)
    
                    
    
                mu_peak = HistMean(counts_bgsub_peak, bin_numbers_peak)
                sigma_peak = HistStd(counts_bgsub_peak, bin_numbers_peak)
                
                dmu_peak = sigma_peak/np.sqrt(N_peak)
                dsigma_peak = sigma_peak/np.sqrt(2*N_peak-2)
                
                
                if(N_peak < kwargs["min_n_events_peak"]):
                    if(self._verbose):
                        print("Peak {:d} had {:d} events, less than {:d} event limit. Using mean and standard deviation...".format(p_i, N_peak, kwargs["min_n_events_peak"]))
    
                else:        
                
                    delta_mu = np.inf
                    delta_sigma = np.inf
    
                    try:    
    
                        iter_count = 0
                        iter_failed = False
    
                        while(iter_count<10 and (delta_mu>0.01*G_fft and delta_sigma>0.01*G_fft)):
    
                            mu_mns2sigma = max(mu_peak-2*sigma_peak, min_rng_peak)
                            mu_pls2sigma = min(mu_peak+2*sigma_peak, max_rng_peak)
    
                            cond_sel_2sigma = (bin_numbers_peak>mu_mns2sigma) & (bin_numbers_peak<mu_pls2sigma)
    
                            if(sum(cond_sel_2sigma)<3):
                                iter_failed=True
                                break
    
    
                            f_gaus_peak = BinnedLH(f_gaus,
                                               bin_numbers_peak[cond_sel_2sigma], 
                                               counts_bgsub_peak[cond_sel_2sigma ],
                                               1
                                              )
    
                            m_gaus_peak = Minuit(f_gaus_peak, mu=mu_peak, sigma=sigma_peak, A = sum(cond_sel_2sigma))
                            m_gaus_peak.limits['mu'] = (mu_mns2sigma, mu_pls2sigma)
                            m_gaus_peak.limits['sigma'] = (epsilon(), 2.0*sigma_peak)
                            m_gaus_peak.limits["A"] = (max(N_peak - 3*np.sqrt(N_peak), 1),  N_peak + 3*np.sqrt(N_peak))
                            m_gaus_peak.errordef=m_pedestal.LIKELIHOOD
                            m_gaus_peak.migrad()
                            m_gaus_peak.hesse()
                            
    
                            mu_peak_i = m_gaus_peak.values["mu"]            
                            sigma_peak_i = m_gaus_peak.values["sigma"]
    
                            dmu_peak_i = m_gaus_peak.errors["mu"]            
                            dsigma_peak_i = m_gaus_peak.errors["sigma"]
    
                            if(iter_count==0):
                                delta_mu = mu_peak_i
                                delta_sigma = sigma_peak_i
    
                            else:
    
                                delta_mu = abs(mu_peak - mu_peak_i)
                                delta_sigma = abs(sigma_peak - sigma_peak_i)
    
    
                            mu_peak = mu_peak_i
                            sigma_peak = sigma_peak_i
    
                            dmu_peak = dmu_peak_i
                            dsigma_peak = dsigma_peak_i
    
                            iter_count+=1
    
                        if(iter_failed):
                            if(self._verbose):
                                print("Iterative 2-sigma Gaus fit for Peak {:d} failed because the fit range went below 3 bins in size. Using mean and standard deviation.".format(p_i))
    
                            mu_peak = HistMean(counts_bgsub_peak, bin_numbers_peak)
                            sigma_peak = HistStd(counts_bgsub_peak, bin_numbers_peak)
    
                            dmu_peak = sigma_peak/np.sqrt(N_peak)
                            dsigma_peak = sigma_peak/np.sqrt(2*N_peak-2)
    
    
    
                    except:
                        if(self._verbose):
                            print("Iterative 2-sigma Gaus fit for Peak {:d} failed. Using mean and standard deviation...".format(p_i))
    
    
                        mu_peak = HistMean(counts_bgsub_peak, bin_numbers_peak)
                        sigma_peak = HistStd(counts_bgsub_peak, bin_numbers_peak)
    
                        dmu_peak = sigma_peak/np.sqrt(N_peak)
                        dsigma_peak = sigma_peak/np.sqrt(2*N_peak-2)
                    
    
                idx_peak_mus[p_i] = mu_peak
                idx_peak_dmus[p_i] = dmu_peak
    
                idx_peak_sigmas[p_i] = sigma_peak
                idx_peak_dsigmas[p_i] = dsigma_peak
    
    
            
            
            cond_sel_peaks = (~np.isnan(idx_peak_mus)) & (~np.isnan(idx_peak_sigmas)) & (~np.isnan(idx_peak_dmus)) & (~np.isnan(idx_peak_dsigmas)) & (idx_peak_sigmas>0.1)
            
            if(sum(cond_sel_peaks)<3):
                raise Exception("Less than three peaks found. Cannot proceed. Try reducing the min_n_events_peak variable.")
            
            
            idx_peak_numbers = idx_peak_numbers[cond_sel_peaks]
            idx_peak_mus = idx_peak_mus[cond_sel_peaks]
            idx_peak_sigmas = idx_peak_sigmas[cond_sel_peaks]
            idx_peak_dmus = idx_peak_dmus[cond_sel_peaks]
            idx_peak_dsigmas = idx_peak_dsigmas[cond_sel_peaks]
    
                
      
            idx_peak_vars = idx_peak_sigmas**2
            idx_peak_dvars = 2*idx_peak_sigmas*idx_peak_dsigmas
            
    
            f_mu_lin = HuberRegression(lambda x, G, Q_0: Linear(x, G, Q_0), 
                                       idx_peak_numbers, 
                                       idx_peak_mus, 
                                       idx_peak_dmus)
    
            f_var_lin = HuberRegression(lambda x, var_1, var_0: Linear(x, var_1, var_0), 
                                        idx_peak_numbers,
                                        idx_peak_vars, 
                                        idx_peak_dvars,
                                       )   
    
    
            m_mu_lin = Minuit(f_mu_lin, Q_0 = Q_0, G = G_fft)
            m_mu_lin.limits["Q_0"] = (epsilon(), nbins)
            m_mu_lin.fixed["G"] = True
            m_mu_lin.errordef = m_mu_lin.LEAST_SQUARES
            m_mu_lin.migrad()
            m_mu_lin.hesse()
    
    
    
            m_var_lin = Minuit(f_var_lin, var_0 = 1, var_1 = 1)
            m_var_lin.limits["var_0"] = (epsilon(), nbins)
            m_var_lin.limits["var_1"] = (epsilon(), nbins)
            m_var_lin.errordef = m_var_lin.LEAST_SQUARES
            m_var_lin.migrad()
            m_var_lin.hesse()
    
    
    
            Q_0 = m_mu_lin.values["Q_0"]
            dQ_0 = m_mu_lin.errors["Q_0"]
    
    
            sigma_0 =  np.sqrt(m_var_lin.values["var_0"])
            dsigma_0 = (0.5/sigma_0)*m_var_lin.errors["var_0"]
    
            sigma_1 =  np.sqrt(m_var_lin.values["var_1"])
            dsigma_1 = (0.5/sigma_0)*m_var_lin.errors["var_1"]
                
                
            ########################################################################################################
            # Use final estimate of pedestal to determine the mu, lambda
            # Calculate k_max by summing up the GP distribution probabilities (cumulative distribution function) for all peaks in 
            # the spectrum, and then finding the peak closest to the C_%
            ########################################################################################################
                
                
            mu_GP = max(epsilon(), GP_mu(mu_unsub-Q_0, sigma, gamma))
            lbda_GP = min(max(epsilon(), GP_lbda(mu_unsub-Q_0, sigma, gamma)), 1-epsilon())
            k_max = len(idx_peaks)
    
            if(self._max_n_peaks is not None):
                if(k_max>self._max_n_peaks):
                    if(self._verbose):
                        print("Max # light peaks ({:d}) exceeded set maximum number of light peaks. Setting # peaks to {:d}".format(k_max,  self._max_n_peaks))
                    k_max = self._max_n_peaks
                    
            elif(k_max<4):
                if(self._verbose):
                    print("Max # light peaks ({:d}) less than 4, so cannot fit. Setting # peaks to 4".format(k_max))
                k_max = 4
    
    
            ########################################################################################################
            # - Convert to charge units: (K= Q - Q_0)/G
            # - Estimate Probability density in a range of 0.1 about the minimum, K=0.5. N_DCR_min = mean(counts[0.45<K<0.55])/0.1
            # - Get N_DCR = sum(counts[0<K<0.5]) + 0.5*sum(counts[0.5<counts<1]) 
            # - calculate DCR
            ########################################################################################################
    
    
     
            K = (bin_numbers - Q_0)/G_fft
            dK = abs(K[1]-K[0])
            
    
            ################################
            # In order to do the interpolated integral, we need to change co-ordinate
            #bw = dx = 1
            #dK = dK/dx = 1/G dx
            #dx = G*dK
            #so ∫counts.dx = G*∫counts.dK
            #This is necessary for correct calculation of the error.
            ################################
            
            cond_NN = (K>=0.45)&(K<=0.55)
            cond_Nc = (K<0.5)
            
            
    
            NN_sum = max(np.sum(counts[cond_NN]),1)
            NN = np.mean(counts[cond_NN])
            
            if(NN!=NN):
                NN = float(counts[np.argmin(abs(K-0.5))])
                NN_sum = float(NN)
                
    
    
            
            Nc = max(float(np.sum(counts[cond_Nc])), 1)
    
             
            R0p5 = NN/Nc
            
        
            DCR = R0p5/(4*kwargs["tau"]*dK)
            DCR = DCR*np.exp(DCR*kwargs["tau"])
            dDCR = DCR*(1/np.sqrt(max(1, NN_sum)))
            
            mu_DCR = DCR*(kwargs["t_gate"] + kwargs["t_0"])
            
            if(DCR!=DCR or DCR<epsilon()):
                if(self._verbose):
                    print("DCR returned invalid, or less than machine epsilon. Choosing DCR to be machine epsilon...")
                    DCR = epsilon()
                    dDCR = 1e-9
                    
            
        
            if(self._max_n_peaks_dcr is not None):
                if(self._verbose):
                    print("Setting # dark peaks to {:d}".format( self._max_n_peaks_dcr))
                k_max_dcr = self._max_n_peaks_dcr
                
            elif(k_max_dcr<4):
                if(self._verbose):
                    print("Max # dark peaks ({:d}) less than 4, so cannot fit. Setting # peaks to 4".format(k_max_dcr))
                k_max_dcr = 4
          
    
    
        
            self._hist_data["bw"] = bw        
            self._hist_data["bc_min"] = bin_centres[0]
            self._hist_data["bins"]=bins
            self._hist_data["nbins"]=nbins
            self._hist_data["counts"] = counts
            self._hist_data["counts_error"] = counts_error
            self._hist_data["counts_bg"] = counts_bg
            self._hist_data["counts_bgsub"] = counts_bgsub
            self._hist_data["bin_centres"] = bin_centres
            self._hist_data["bin_numbers"] = bin_numbers
        
    
            self._hist_data["fft_amplitude"] = fft_amplitude
            self._hist_data["fft_freq"] = fft_freq
            self._hist_data["G_fft"] = G_fft
            self._hist_data["Q_0_est"] = Q_0_est
            
    
            self._hist_data["peaks"]["peak_position"] = idx_peaks
            self._hist_data["peaks"]["peak_index"] = idx_peak_numbers
            self._hist_data["peaks"]["peak_mean"] = idx_peak_mus
            self._hist_data["peaks"]["peak_mean_error"] = idx_peak_dmus
            self._hist_data["peaks"]["peak_variance"] = idx_peak_vars
            self._hist_data["peaks"]["peak_variance_error"] = idx_peak_dvars
            self._hist_data["peaks"]["peak_std_deviation"] = idx_peak_sigmas
            self._hist_data["peaks"]["peak_std_deviation_error"] = idx_peak_dsigmas
            
    
            self._prefit_values_bin["Q_0"] = Q_0
            self._prefit_values_bin["G"] = G_fft
            self._prefit_values_bin["mu"] = mu_GP
            self._prefit_values_bin["lbda"] = lbda_GP
            self._prefit_values_bin["sigma_0"] = sigma_0
            self._prefit_values_bin["sigma_1"] = sigma_1
            self._prefit_values_bin["DCR"] = DCR
            self._prefit_values_bin["p_Ap"] = 0.1
            self._prefit_values_bin["tau_Ap"] = 5.0
            self._prefit_values_bin["tau"] = kwargs["tau"]
            self._prefit_values_bin["t_0"] = kwargs["t_0"]
            self._prefit_values_bin["t_gate"] = kwargs["t_gate"]
            self._prefit_values_bin["tau_R"] = kwargs["tau_R"]
            self._prefit_values_bin["k_max"]=k_max
            self._prefit_values_bin["k_max_dcr"]=k_max_dcr
            self._prefit_values_bin["len_pad"]=self._len_pad
            self._prefit_values_bin["A"]=np.sum(counts)
            
            
            
            
            
            self._prefit_errors_bin["Q_0"] = dQ_0
            self._prefit_errors_bin["G"] = 1
            self._prefit_errors_bin["mu"] = 0.1
            self._prefit_errors_bin["lbda"] = 0.01
            self._prefit_errors_bin["sigma_0"] = dsigma_0
            self._prefit_errors_bin["sigma_1"] = dsigma_1
            self._prefit_errors_bin["p_Ap"] = 0.05
            self._prefit_errors_bin["tau_Ap"] = 0.05
            self._prefit_errors_bin["tau"] = kwargs["tau_err"]
            self._prefit_errors_bin["t_0"] = kwargs["t_0_err"]
            self._prefit_errors_bin["tau_R"] = kwargs["tau_R_err"]
            self._prefit_errors_bin["t_gate"] = kwargs["t_gate_err"]
            self._prefit_errors_bin["DCR"] = dDCR
            self._prefit_errors_bin["A"] = np.sqrt(np.sum(counts))