示例#1
0
class FakeBNormalizationManager:
    '''
    Base class for QCD measurement normalization from which specialized algorithm classes inherit
    '''
    def __init__(self,
                 binLabels,
                 resultDirName,
                 moduleInfoString,
                 verbose=False):
        self._verbose = verbose
        self._templates = {}
        self._binLabels = binLabels
        self._sources = {}
        self._commentLines = []
        self._BinLabelMap = {}
        self._TF = {}  # Transfer Factor (TF)
        self._TF_Error = {}
        self._TF_Up = {}
        self._TF_Down = {}
        self._dqmKeys = OrderedDict()
        self._myPath = os.path.join(resultDirName, "normalisationPlots")
        if not isinstance(binLabels, list):
            raise Exception("Error: binLabels needs to be a list of strings")
        self.Verbose("__init__")

        # No optimisation mode
        if moduleInfoString == "":
            moduleInfoString = "Default"

        if not os.path.exists(self._myPath):
            self.Print("Creating new directory %s" % (self._myPath), True)
            os.mkdir(self._myPath)
        self._plotDirName = os.path.join(resultDirName, "normalisationPlots",
                                         moduleInfoString)

        # If already exists, Delete an entire directory tree
        if os.path.exists(self._plotDirName):
            msg = "Removing directory tree %s" % (self._plotDirName)
            self.Verbose(
                ShellStyles.NoteStyle() + msg + ShellStyles.NormalStyle(),
                True)
            shutil.rmtree(self._plotDirName)
        msg = "Creating directory %s" % (self._plotDirName)
        self.Verbose(
            ShellStyles.SuccessStyle() + msg + ShellStyles.NormalStyle(),
            False)
        os.mkdir(self._plotDirName)
        return

    def Print(self, msg, printHeader=False):
        fName = __file__.split("/")[-1]
        if printHeader == True:
            print "=== ", fName + ": class " + self.__class__.__name__
            print "\t", msg
        else:
            print "\t", msg
            return

    def Verbose(self, msg, printHeader=True, verbose=False):
        if not self._verbose:
            return
        self.Print(msg, printHeader)
        return

    def GetQCDNormalization(self, binLabel):
        if binLabel in self._TF.keys():
            return self._TF[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! " %
                            (binLabel))

    def GetTransferFactor(self, binLabel):
        return self.GetQCDNormalization(binLabel)

    def GetQCDNormalizationError(self, binLabel):
        if binLabel in self._TF_Error.keys():
            return self._TF_Error[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! " %
                            (binLabel))

    def CalculateTransferFactor(self,
                                binLabel,
                                hFakeB_Baseline,
                                hFakeB_Inverted,
                                verbose=False):
        '''
        Calculates the combined normalization and, if specified, 
        varies it up or down by factor (1+variation)
 
        TF = Transfer Factor
        SR = Signal Region
        CR = Control Region
        VR = Verification Region
        '''
        self.verbose = verbose

        # Obtain counts for QCD and EWK fakes
        lines = []

        # NOTES: Add EWKGenuineB TF, Add Data TF, add QCD TF, Add EWK TF, add MCONLY TFs
        nSR_Error = ROOT.Double(0.0)
        nCR_Error = ROOT.Double(0.0)
        # nTotalError = ROOT.TMath.Sqrt(nSRerror**2 + nCRError**2)

        nSR = hFakeB_Baseline.IntegralAndError(1,
                                               hFakeB_Baseline.GetNbinsX() + 1,
                                               nSR_Error)
        nCR = hFakeB_Inverted.IntegralAndError(1,
                                               hFakeB_Inverted.GetNbinsX() + 1,
                                               nCR_Error)
        # nTotal = nSR + nCR

        # Calculate Transfer Factor (TF) from Control Region (R) to Signal Region (SR): R = N_CR1/ N_CR2
        TF = None
        TF_Up = None
        TF_Down = None
        TF_Error = None

        if 1:  ## nTotal > 0.0:
            TF = nSR / nCR
            TF_Error = errorPropagation.errorPropagationForDivision(
                nSR, nSR_Error, nCR, nCR_Error)
            TF_Up = TF + TF_Error
            if TF_Up > 1.0:
                TF_Up = 1.0
            TF_Down = TF - TF_Error
            if TF_Down < 0.0:
                TF_Down = 0.0
        lines.append("TF (bin=%s) = N_CR1 / N_CR2 = %f / %f =  %f +- %f" %
                     (binLabel, nSR, nCR, TF, TF_Error))

        # Calculate the combined normalization factor (f_fakes = w*f_QCD + (1-w)*f_EWKfakes)
        fakeRate = None
        fakeRateError = None
        fakeRateUp = None
        fakeRateDown = None
        if TF != None:
            #     fakeRate = w*self._TF[binLabel] + (1.0-w)*self._ewkNormalization[binLabel]
            #     fakeRateUp = wUp*self._TF[binLabel] + (1.0-wUp)*self._ewkNormalization[binLabel]
            #     fakeRateDown = wDown*self._TF[binLabel] + (1.0-wDown)*self._ewkNormalization[binLabel]
            #     fakeRateErrorPart1 = errorPropagation.errorPropagationForProduct(w, wError, self._TF[binLabel], self._TFError[binLabel])
            #     fakeRateErrorPart2 = errorPropagation.errorPropagationForProduct(w, wError, self._ewkNormalization[binLabel], self._ewkNormalizationError[binLabel])
            #     fakeRateError = ROOT.TMath.Sqrt(fakeRateErrorPart1**2 + fakeRateErrorPart2**2)

            # Replace bin label with histo title (has exact binning info)
            self._BinLabelMap[binLabel] = hFakeB_Inverted.GetTitle()
            self._TF[binLabel] = TF
            self._TF_Error[binLabel] = TF_Error
            self._TF_Up[binLabel] = TF_Up
            self._TF_Down[binLabel] = TF_Down
        # self._combinedFakesNormalizationError[binLabel] = fakeRateError
        # self._combinedFakesNormalizationUp[binLabel] = fakeRateUp
        # self._combinedFakesNormalizationDown[binLabel] = fakeRateDown

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)

        # Print output and store comments
        if 0:
            for i, line in enumerate(lines, 1):
                Print(line, i == 1)
        return

    def writeNormFactorFile(self, filename, opts):
        '''
        Save the fit results for QCD and EWK.

        The results will are stored in a python file starting with name:
        "QCDInvertedNormalizationFactors_" + moduleInfoString

        The script also summarizes warnings and errors encountered:
        - Green means deviation from normal is 0-3 %,
        - Yellow means deviation of 3-10 %, and
        - Red means deviation of >10 % (i.e. something is clearly wrong).
        
        If necessary, do adjustments to stabilize the fits to get rid of the errors/warnings. 
        The first things to work with are:
        a) Make sure enough events are in the histograms used
        b) Adjust fit parameters and/or fit functions and re-fit results
        
        Move on only once you are pleased with the normalisation coefficients
        '''
        s = ""
        s += "# Generated on %s\n" % datetime.datetime.now().ctime()
        s += "# by %s\n" % os.path.basename(sys.argv[0])
        s += "\n"
        s += "import sys\n"
        s += "\n"
        s += "def QCDInvertedNormalizationSafetyCheck(era, searchMode, optimizationMode):\n"
        s += "    validForEra        = \"%s\"\n" % opts.dataEra
        s += "    validForSearchMode = \"%s\"\n" % opts.searchMode
        s += "    validForOptMode    = \"%s\"\n" % opts.optMode
        s += "    if not era == validForEra:\n"
        s += "        raise Exception(\"Error: inconsistent era, normalisation factors valid for\",validForEra,\"but trying to use with\",era)\n"
        s += "    if not searchMode == validForSearchMode:\n"
        s += "        raise Exception(\"Error: inconsistent search mode, normalisation factors valid for\",validForSearchMode,\"but trying to use with\",searchMode)\n"
        s += "    if not optimizationMode == validForOptMode:\n"
        s += "        raise Exception(\"Error: inconsistent optimization mode, normalisation factors valid for\",validForOptMode,\"but trying to use with\",optimizationMode)\n"
        s += "    return"
        s += "\n"

        s += "QCDNormalization = {\n"
        for k in self._TF:
            if 0:
                print "key = %s, value = %s" % (k, self._TF[k])
            s += '    "%s": %f,\n' % (k, self._TF[k])
        s += "}\n"

        s += "QCDNormalizationError = {\n"
        for k in self._TF_Error:
            s += '    "%s": %f,\n' % (k, self._TF_Error[k])
        s += "}\n"

        s += "QCDNormalizationErrorUp = {\n"
        for k in self._TF_Up:
            s += '    "%s": %f,\n' % (k, self._TF_Up[k])
        s += "}\n"

        s += "QCDNormalizationErrorDown = {\n"
        for k in self._TF_Down:
            s += '    "%s": %f,\n' % (k, self._TF_Down[k])
        s += "}\n"

        self.Verbose("Writing results in file %s" % filename, True)
        fOUT = open(filename, "w")
        fOUT.write(s)
        fOUT.write("'''\n")
        for l in self._commentLines:
            fOUT.write(l + "\n")
        fOUT.write("'''\n")
        fOUT.close()

        msg = "Results written in file %s" % (
            ShellStyles.SuccessStyle() + filename + ShellStyles.NormalStyle())
        self.Print(msg, True)

        # Create the transfer factors plot (for each bin of FakeB measurement)
        self._generateCoefficientPlot()
        # self._generateDQMplot()
        return

    def _generateCoefficientPlot(self):
        '''
        This probably is needed in the case the measurement is done in
        bins of a correlated quantity (e.g. pT in the case of inverted tau isolation
        '''
        def makeGraph(markerStyle, color, binList, valueDict, upDict,
                      downDict):
            g = ROOT.TGraphAsymmErrors(len(binList))
            for i in range(len(binList)):
                g.SetPoint(i, i + 0.5, valueDict[binList[i]])
                g.SetPointEYhigh(i, upDict[binList[i]])
                g.SetPointEYlow(i, downDict[binList[i]])
            g.SetMarkerSize(1.2)
            g.SetMarkerStyle(markerStyle)
            g.SetLineColor(color)
            g.SetLineWidth(3)
            g.SetMarkerColor(color)
            return g

        # Obtain bin list in right order
        keyList = []
        keys = self._TF.keys()
        keys.sort()
        #        for k in keys:
        #            if "lt" in k:
        #                keyList.append(k)
        #        for k in keys:
        #            if "eq" in k:
        #                keyList.append(k)
        #        for k in keys:
        #            if "gt" in k:
        #                keyList.append(k)

        # For-loop: All Fake-b measurement bins
        for k in keys:
            keyList.append(k)
            #if "Inclusive" in keys:
            #    keyList.append("Inclusive")

        # Apply TDR style
        style = tdrstyle.TDRStyle()
        style.setOptStat(False)
        style.setGridX(True)
        style.setGridY(True)

        # Create graphs
        gFakeB = makeGraph(ROOT.kFullCircle, ROOT.kRed, keyList, self._TF,
                           self._TF_Error, self._TF_Error)

        # Make plot
        hFrame = ROOT.TH1F("frame", "frame", len(keyList), 0, len(keyList))
        # Change bin labels to text
        for i, binLabel in enumerate(keyList, 1):
            # for i in range(len(keyList)):
            binLabelText = self.getFormattedBinLabelString(binLabel)
            #hFrame.GetXaxis().SetBinLabel(i+1, binLabelText)
            hFrame.GetXaxis().SetBinLabel(i, binLabelText)

        # Set axes names
        hFrame.GetYaxis().SetTitle("transfer factor ")
        # hFrame.GetYaxis().SetTitle("transfer factors (R_{i})")
        # hFrame.GetXaxis().SetTitle("Fake-b bin")

        # Customise axes
        hFrame.SetMinimum(0.6e-1)
        hFrame.SetMaximum(2e0)
        if len(self._BinLabelMap) > 12:
            lSize = 8
        elif len(self._BinLabelMap) > 8:
            lSize = 12
        else:
            lSize = 16
        hFrame.GetXaxis().SetLabelSize(lSize)  # 20
        hFrame.GetXaxis().LabelsOption("d")
        # Label Style options
        # "a" sort by alphabetic order
        # ">" sort by decreasing values
        # "<" sort by increasing values
        # "h" draw labels horizonthal
        # "v" draw labels vertical
        # "u" draw labels up (end of label right adjusted)
        # "d" draw labels down (start of label left adjusted)

        # Create canvas
        c = ROOT.TCanvas()
        c.SetLogy(True)
        c.SetGridx()
        c.SetGridy()

        hFrame.Draw()
        gFakeB.Draw("p same")
        histograms.addStandardTexts(cmsTextPosition="outframe")

        # Create the legend & draw it
        l = ROOT.TLegend(0.65, 0.80, 0.90, 0.90)
        l.SetFillStyle(-1)
        l.SetBorderSize(0)
        # l.AddEntry(gFakeB, "Fake-#it{b} #pm Stat.", "LP")
        l.AddEntry(gFakeB, "Value #pm Stat.", "LP")
        l.SetTextSize(0.035)
        l.Draw()

        # Store ROOT ignore level to normal before changing it
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning

        # Save the plot
        for item in ["png", "C", "pdf"]:
            c.Print(self._plotDirName +
                    "/QCDNormalisationCoefficients.%s" % item)

        # Reset the ROOT ignore level to normal
        ROOT.gErrorIgnoreLevel = backup

        # Inform user
        msg = "Transfer-factors written in %s " % (ShellStyles.SuccessStyle() +
                                                   self._plotDirName +
                                                   ShellStyles.NormalStyle())
        self.Print(msg, False)
        return

    def getFormattedBinLabelString(self, binLabel):
        '''
        Dirty trick to get what I want
        '''
        if binLabel not in self._BinLabelMap:
            # for k in self._BinLabelMap:
            #     print k
            raise Exception("Got unexpected bin label \"%s\"!" % binLabel)
        newLabel = self._BinLabelMap[binLabel]
        newLabel = newLabel.replace("abs(", "|")
        newLabel = newLabel.replace(")", "|")
        newLabel = newLabel.replace("..", "-")
        newLabel = newLabel.replace(":", ",")
        newLabel = newLabel.replace("TetrajetBjet", "")  #"b^{ldg} ")
        newLabel = newLabel.replace("Pt", "p_{T} ")
        newLabel = newLabel.replace("Eta", "#eta ")
        if "inclusive" in binLabel.lower():
            newLabel = "Inclusive"
        return newLabel

    def _generateDQMplot(self):
        '''
        Create a DQM style plot
        '''
        # Check the uncertainties on the normalization factors
        for k in self._dqmKeys.keys():
            self._addDqmEntry(k, "norm.coeff.uncert::QCD", self._TFError[k],
                              0.03, 0.10)
            self._addDqmEntry(k, "norm.coeff.uncert::fake",
                              self._ewkNormalizationError[k], 0.03, 0.10)
            value = abs(self._combinedFakesNormalizationUp[k] -
                        self._combinedFakesNormalization[k])
            value = max(
                value,
                abs(self._combinedFakesNormalizationDown[k] -
                    self._combinedFakesNormalizationUp[k]))
            self._addDqmEntry(k, "norm.coeff.uncert::combined", value, 0.03,
                              0.10)
        # Construct the DQM histogram
        h = ROOT.TH2F("QCD DQM", "QCD DQM",
                      len(self._dqmKeys[self._dqmKeys.keys()[0]].keys()), 0,
                      len(self._dqmKeys[self._dqmKeys.keys()[0]].keys()),
                      len(self._dqmKeys.keys()), 0, len(self._dqmKeys.keys()))
        h.GetXaxis().SetLabelSize(15)
        h.GetYaxis().SetLabelSize(15)
        h.SetMinimum(0)
        h.SetMaximum(3)
        #h.GetXaxis().LabelsOption("v")
        nWarnings = 0
        nErrors = 0
        for i in range(h.GetNbinsX()):
            for j in range(h.GetNbinsY()):
                ykey = self._dqmKeys.keys()[j]
                xkey = self._dqmKeys[ykey].keys()[i]
                h.SetBinContent(i + 1, j + 1, self._dqmKeys[ykey][xkey])
                h.GetYaxis().SetBinLabel(j + 1, ykey)
                h.GetXaxis().SetBinLabel(i + 1, xkey)
                if self._dqmKeys[ykey][xkey] > 2:
                    nErrors += 1
                elif self._dqmKeys[ykey][xkey] > 1:
                    nWarnings += 1
        palette = array.array("i", [ROOT.kGreen + 1, ROOT.kYellow, ROOT.kRed])
        ROOT.gStyle.SetPalette(3, palette)
        c = ROOT.TCanvas()
        c.SetBottomMargin(0.2)
        c.SetLeftMargin(0.2)
        c.SetRightMargin(0.2)
        h.Draw("colz")

        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning
        for item in ["png", "C", "pdf"]:
            c.Print(self._plotDirName + "/QCDNormalisationDQM.%s" % item)
        ROOT.gErrorIgnoreLevel = backup
        ROOT.gStyle.SetPalette(1)
        print "Obtained %d warnings and %d errors for the normalization" % (
            nWarnings, nErrors)
        if nWarnings > 0 or nErrors > 0:
            print "Please have a look at %s/QCDNormalisationDQM.png to see the origin of the warning(s) and error(s)" % self._plotDirName
        return

    def _addDqmEntry(self, binLabel, name, value, okTolerance, warnTolerance):
        if not binLabel in self._dqmKeys.keys():
            self._dqmKeys[binLabel] = OrderedDict()
        result = 2.5
        if abs(value) < okTolerance:
            result = 0.5
        elif abs(value) < warnTolerance:
            result = 1.5
        self._dqmKeys[binLabel][name] = result
        return

    def _getSanityCheckTextForFractions(self,
                                        dataTemplate,
                                        binLabel,
                                        saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        
        NOTE: Should one divide the fractions with dataTemplate.getFittedParameters()[0] ? 
              Right now not because the correction is so small.
        '''
        self.Verbose("_getSanityCheckTextForFractions()", True)

        # Get variables
        label = "QCD"
        fraction = dataTemplate.getFittedParameters()[1]
        fractionError = dataTemplate.getFittedParameterErrors()[1]
        nBaseline = self._templates["%s_Baseline" %
                                    label].getNeventsFromHisto(False)
        nCalculated = fraction * dataTemplate.getNeventsFromHisto(False)

        if nCalculated > 0:
            ratio = nBaseline / nCalculated
        else:
            ratio = 0
        lines = []
        lines.append("Fitted %s fraction: %f +- %f" %
                     (label, fraction, fractionError))
        lines.append(
            "Sanity check: ratio = %.3f: baseline = %.1f vs. fitted = %.1f" %
            (ratio, nBaseline, nCalculated))

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines

    def _checkOverallNormalization(self,
                                   template,
                                   binLabel,
                                   saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        '''
        self.Verbose("_checkOverallNormalization()")

        # Calculatotions
        value = template.getFittedParameters()[0]
        error = template.getFittedParameterErrors()[0]

        # Definitions
        lines = []
        lines.append(
            "The fitted overall normalization factor for purity is: (should be 1.0)"
        )
        lines.append("NormFactor = %f +/- %f" % (value, error))

        self._addDqmEntry(binLabel, "OverallNormalization(par0)", value - 1.0,
                          0.03, 0.10)

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines

    ## Helper method to be called from parent class when calculating norm.coefficients
    def _getResultOutput(self, binLabel):
        lines = []
        lines.append("   Normalization factor (QCD): %f +- %f" %
                     (self._TF[binLabel], self._TFError[binLabel]))
        lines.append("   Normalization factor (EWK fake taus): %f +- %f" %
                     (self._ewkNormalization[binLabel],
                      self._ewkNormalizationError[binLabel]))
        lines.append("   Combined norm. factor: %f +- %f" %
                     (self._combinedFakesNormalization[binLabel],
                      self._combinedFakesNormalizationError[binLabel]))

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)
        return lines
class SystTopBDTNormalizationManager:
    '''
    Base class for QCD measurement normalization from which specialized algorithm classes inherit
    '''
    def __init__(self, resultDirName, moduleInfoString, verbose=False):
        self._verbose      = verbose
        self._templates    = {}
        self._sources      = {}
        self._commentLines = []
        self._BinLabelMap  = {}
        self._TF           = {} # Transfer Factor (TF)
        self._TF_Error     = {}
        self._TF_Up        = {}
        self._TF_Down      = {}
        self._dqmKeys      = OrderedDict()
        self._myPath       = os.path.join(resultDirName, "normalisationPlots")
        self.Verbose("__init__")

        # No optimisation mode
        if moduleInfoString == "":
            moduleInfoString = "Default" 
        
        if not os.path.exists(self._myPath):
            self.Print("Creating new directory %s" % (self._myPath), True )
            os.mkdir(self._myPath)
        self._plotDirName = os.path.join(resultDirName, "normalisationPlots", moduleInfoString)

        # If already exists, Delete an entire directory tree
        if os.path.exists(self._plotDirName):
            msg = "Removing directory tree %s" % (self._plotDirName)
            self.Verbose(ShellStyles.NoteStyle() + msg + ShellStyles.NormalStyle(), True)
            shutil.rmtree(self._plotDirName)
        msg = "Creating directory %s" % (self._plotDirName)
        self.Verbose(ShellStyles.SuccessStyle() + msg + ShellStyles.NormalStyle(), False)
        os.mkdir(self._plotDirName)
        return

    def Print(self, msg, printHeader=False):
        fName = __file__.split("/")[-1]
        if printHeader==True:
            print "=== ", fName + ": class " + self.__class__.__name__
            print "\t", msg
        else:
            print "\t", msg
            return
        
    def Verbose(self, msg, printHeader=True, verbose=False):
        if not self._verbose:
            return
        self.Print(msg, printHeader)
        return

    def GetQCDNormalization(self, binLabel):
        if binLabel in self._TF.keys():
            return self._TF[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! "% (binLabel) )

    def GetTransferFactor(self, binLabel):
        return self.GetQCDNormalization(binLabel)


    def GetQCDNormalizationError(self, binLabel):
        if binLabel in self._TF_Error.keys():
            return self._TF_Error[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! "% (binLabel) )
    
    def CalculateTransferFactor(self, binLabel, hFakeB_Baseline, hFakeB_Inverted, verbose=False):
        '''
        Calculates the combined normalization and, if specified, 
        varies it up or down by factor (1+variation)
 
        TF = Transfer Factor
        SR = Signal Region
        CR = Control Region
        VR = Verification Region
        '''
        self.verbose = verbose
        

        print "======= Calculate TransferFactor "


        # Obtain counts for QCD and EWK fakes
        lines = []

        # NOTES: Add EWKGenuineB TF, Add Data TF, add QCD TF, Add EWK TF, add MCONLY TFs
        nSR_Error = ROOT.Double(0.0)
        nCR_Error = ROOT.Double(0.0)
        # nTotalError = ROOT.TMath.Sqrt(nSRerror**2 + nCRError**2)
        
        nSR = hFakeB_Baseline.IntegralAndError(1, hFakeB_Baseline.GetNbinsX()+1, nSR_Error)
        nCR = hFakeB_Inverted.IntegralAndError(1, hFakeB_Inverted.GetNbinsX()+1, nCR_Error)
        # nTotal = nSR + nCR

        # Calculate Transfer Factor (TF) from Control Region (R) to Signal Region (SR): R = N_CR1/ N_CR2
        TF       = None
        TF_Up    = None
        TF_Down  = None
        TF_Error = None

        if 1: ## nTotal > 0.0:
            TF = nSR / nCR
            TF_Error = errorPropagation.errorPropagationForDivision(nSR, nSR_Error, nCR, nCR_Error)
            TF_Up = TF + TF_Error
            if TF_Up > 1.0:
                TF_Up = 1.0
            TF_Down = TF - TF_Error
            if TF_Down < 0.0:
                TF_Down = 0.0
        lines.append("TF (bin=%s) = N_CR1 / N_CR2 = %f / %f =  %f +- %f" % (binLabel, nSR, nCR, TF, TF_Error) )

        # Calculate the combined normalization factor (f_fakes = w*f_QCD + (1-w)*f_EWKfakes)
        fakeRate      = None
        fakeRateError = None
        fakeRateUp    = None
        fakeRateDown  = None
        if TF != None:
            #     fakeRate = w*self._TF[binLabel] + (1.0-w)*self._ewkNormalization[binLabel]
            #     fakeRateUp = wUp*self._TF[binLabel] + (1.0-wUp)*self._ewkNormalization[binLabel]
            #     fakeRateDown = wDown*self._TF[binLabel] + (1.0-wDown)*self._ewkNormalization[binLabel]
            #     fakeRateErrorPart1 = errorPropagation.errorPropagationForProduct(w, wError, self._TF[binLabel], self._TFError[binLabel])
            #     fakeRateErrorPart2 = errorPropagation.errorPropagationForProduct(w, wError, self._ewkNormalization[binLabel], self._ewkNormalizationError[binLabel])
            #     fakeRateError = ROOT.TMath.Sqrt(fakeRateErrorPart1**2 + fakeRateErrorPart2**2)
            
            # Replace bin label with histo title (has exact binning info)
            self._BinLabelMap[binLabel] = hFakeB_Inverted.GetTitle()
            self._TF[binLabel   ]       = TF
            self._TF_Error[binLabel]    = TF_Error
            self._TF_Up[binLabel]       = TF_Up
            self._TF_Down[binLabel]     = TF_Down
        # self._combinedFakesNormalizationError[binLabel] = fakeRateError
        # self._combinedFakesNormalizationUp[binLabel] = fakeRateUp
        # self._combinedFakesNormalizationDown[binLabel] = fakeRateDown

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)

        # Print output and store comments
        if 1:
            for i, line in enumerate(lines, 1):
                Print(line, i==1)
        return
        
    def writeNormFactorFile(self, filename, opts):
        '''
        Save the fit results for QCD and EWK.

        The results will are stored in a python file starting with name:
        "QCDInvertedNormalizationFactors_" + moduleInfoString

        The script also summarizes warnings and errors encountered:
        - Green means deviation from normal is 0-3 %,
        - Yellow means deviation of 3-10 %, and
        - Red means deviation of >10 % (i.e. something is clearly wrong).
        
        If necessary, do adjustments to stabilize the fits to get rid of the errors/warnings. 
        The first things to work with are:
        a) Make sure enough events are in the histograms used
        b) Adjust fit parameters and/or fit functions and re-fit results
        
        Move on only once you are pleased with the normalisation coefficients
        '''
        s = ""
        s += "# Generated on %s\n"% datetime.datetime.now().ctime()
        s += "# by %s\n" % os.path.basename(sys.argv[0])
        s += "\n"
        s += "import sys\n"
        s += "\n"
        s += "def QCDInvertedNormalizationSafetyCheck(era, searchMode, optimizationMode):\n"
        s += "    validForEra        = \"%s\"\n" % opts.dataEra
        s += "    validForSearchMode = \"%s\"\n" % opts.searchMode
        s += "    validForOptMode    = \"%s\"\n" % opts.optMode
        s += "    if not era == validForEra:\n"
        s += "        raise Exception(\"Error: inconsistent era, normalisation factors valid for\",validForEra,\"but trying to use with\",era)\n"
        s += "    if not searchMode == validForSearchMode:\n"
        s += "        raise Exception(\"Error: inconsistent search mode, normalisation factors valid for\",validForSearchMode,\"but trying to use with\",searchMode)\n"
        s += "    if not optimizationMode == validForOptMode:\n"
        s += "        raise Exception(\"Error: inconsistent optimization mode, normalisation factors valid for\",validForOptMode,\"but trying to use with\",optimizationMode)\n"
        s += "    return"
        s += "\n"

        s += "QCDNormalization = {\n"
        for k in self._TF:
            if 0:
                print "key = %s, value = %s" % (k, self._TF[k])
            s += '    "%s": %f,\n'%(k, self._TF[k])
        s += "}\n"

        s += "QCDNormalizationError = {\n"
        for k in self._TF_Error:
            s += '    "%s": %f,\n'%(k, self._TF_Error[k])
        s += "}\n"

        s += "QCDNormalizationErrorUp = {\n"
        for k in self._TF_Up:
            s += '    "%s": %f,\n'%(k, self._TF_Up[k])
        s += "}\n"

        s += "QCDNormalizationErrorDown = {\n"
        for k in self._TF_Down:
            s += '    "%s": %f,\n'%(k, self._TF_Down[k])
        s += "}\n"

        self.Verbose("Writing results in file %s" % filename, True)
        fOUT = open(filename,"w")
        fOUT.write(s)
        fOUT.write("'''\n")
        for l in self._commentLines:
            fOUT.write(l + "\n")
        fOUT.write("'''\n")
        fOUT.close()

        msg = "Results written in file %s" % (ShellStyles.SuccessStyle()  + filename + ShellStyles.NormalStyle())
        self.Print(msg, True)

        # Create the transfer factors plot (for each bin of FakeB measurement)
        self._generateCoefficientPlot() 
        # self._generateDQMplot()
        return

    def _generateCoefficientPlot(self):
        '''
        This probably is needed in the case the measurement is done in
        bins of a correlated quantity (e.g. pT in the case of inverted tau isolation
        '''
        def makeGraph(markerStyle, color, binList, valueDict, upDict, downDict):
            g = ROOT.TGraphAsymmErrors(len(binList))
            for i in range(len(binList)):
                g.SetPoint(i, i+0.5, valueDict[binList[i]])
                g.SetPointEYhigh(i, upDict[binList[i]])
                g.SetPointEYlow(i, downDict[binList[i]])
            g.SetMarkerSize(1.2)
            g.SetMarkerStyle(markerStyle)
            g.SetLineColor(color)
            g.SetLineWidth(3)
            g.SetMarkerColor(color)
            return g
        # Obtain bin list in right order
        keyList = []
        keys = self._TF.keys()
        keys.sort()
#        for k in keys:
#            if "lt" in k:
#                keyList.append(k)
#        for k in keys:
#            if "eq" in k:
#                keyList.append(k)
#        for k in keys:
#            if "gt" in k:
#                keyList.append(k)

        # For-loop: All Fake-b measurement bins
        for k in keys:        
            keyList.append(k)
            #if "Inclusive" in keys:
            #    keyList.append("Inclusive")
            
            
        # Apply TDR style
        style = tdrstyle.TDRStyle()
        style.setOptStat(False)
        style.setGridX(True)
        style.setGridY(True)

        # Create graphs
        gFakeB  = makeGraph(ROOT.kFullCircle, ROOT.kRed, keyList, self._TF, self._TF_Error, self._TF_Error)

        # Make plot
        hFrame = ROOT.TH1F("frame","frame", len(keyList), 0, len(keyList))
        # Change bin labels to text
        for i, binLabel in enumerate(keyList, 1):
            # for i in range(len(keyList)):
            binLabelText = self.getFormattedBinLabelString(binLabel)
            #hFrame.GetXaxis().SetBinLabel(i+1, binLabelText)
            hFrame.GetXaxis().SetBinLabel(i, binLabelText)

        # Set axes names
        hFrame.GetYaxis().SetTitle("transfer factor ")
        # hFrame.GetYaxis().SetTitle("transfer factors (R_{i})")        
        # hFrame.GetXaxis().SetTitle("Fake-b bin")
        
        # Customise axes
        hFrame.SetMinimum(0.6e-1)
        hFrame.SetMaximum(2e0)
        if len(self._BinLabelMap) > 12:
            lSize = 8
        elif len(self._BinLabelMap) > 8:
            lSize = 12
        else:
            lSize = 16
        hFrame.GetXaxis().SetLabelSize(lSize) # 20
        hFrame.GetXaxis().LabelsOption("d")
        # Label Style options
        # "a" sort by alphabetic order 
        # ">" sort by decreasing values 
        # "<" sort by increasing values 
        # "h" draw labels horizonthal 
        # "v" draw labels vertical
        # "u" draw labels up (end of label right adjusted)
        # "d" draw labels down (start of label left adjusted)

        # Create canvas
        c = ROOT.TCanvas()
        c.SetLogy(True)
        c.SetGridx()
        c.SetGridy()

        hFrame.Draw()
        gFakeB.Draw("p same")
        histograms.addStandardTexts(cmsTextPosition="outframe")
        
        # Create the legend & draw it
        l = ROOT.TLegend(0.65, 0.80, 0.90, 0.90)
        l.SetFillStyle(-1)
        l.SetBorderSize(0)
        # l.AddEntry(gFakeB, "Fake-#it{b} #pm Stat.", "LP")
        l.AddEntry(gFakeB, "Value #pm Stat.", "LP")
        l.SetTextSize(0.035)
        l.Draw()

        # Store ROOT ignore level to normal before changing it
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning

        # Save the plot
        for item in ["png", "C", "pdf"]:
            c.Print(self._plotDirName + "/QCDNormalisationCoefficients.%s" % item)

        # Reset the ROOT ignore level to normal
        ROOT.gErrorIgnoreLevel = backup

        # Inform user
        msg = "Transfer-factors written in %s " % (ShellStyles.SuccessStyle() + self._plotDirName + ShellStyles.NormalStyle())
        self.Print(msg, False)
        return

    def getFormattedBinLabelString(self, binLabel):
        '''
        Dirty trick to get what I want
        '''
        if binLabel not in self._BinLabelMap:
            # for k in self._BinLabelMap:
            #     print k
            raise Exception("Got unexpected bin label \"%s\"!" % binLabel)
        newLabel = self._BinLabelMap[binLabel]
        newLabel = newLabel.replace("abs(", "|")
        newLabel = newLabel.replace(")", "|")
        newLabel = newLabel.replace("..", "-")
        newLabel = newLabel.replace(":", ",")
        newLabel = newLabel.replace("TetrajetBjet", "") #"b^{ldg} ")
        newLabel = newLabel.replace("Pt", "p_{T} ")
        newLabel = newLabel.replace("Eta", "#eta ")
        if "inclusive" in binLabel.lower():
            newLabel = "Inclusive"
        return newLabel

    def _generateDQMplot(self):
        '''
        Create a DQM style plot
        '''
        # Check the uncertainties on the normalization factors
        for k in self._dqmKeys.keys():
            self._addDqmEntry(k, "norm.coeff.uncert::QCD" , self._TFError[k], 0.03, 0.10)
            self._addDqmEntry(k, "norm.coeff.uncert::fake", self._ewkNormalizationError[k], 0.03, 0.10)
            value = abs(self._combinedFakesNormalizationUp[k]-self._combinedFakesNormalization[k])
            value = max(value, abs(self._combinedFakesNormalizationDown[k]-self._combinedFakesNormalizationUp[k]))
            self._addDqmEntry(k, "norm.coeff.uncert::combined", value, 0.03, 0.10)
        # Construct the DQM histogram
        h = ROOT.TH2F("QCD DQM", "QCD DQM",
                      len(self._dqmKeys[self._dqmKeys.keys()[0]].keys()), 0, len(self._dqmKeys[self._dqmKeys.keys()[0]].keys()),
                      len(self._dqmKeys.keys()), 0, len(self._dqmKeys.keys()))
        h.GetXaxis().SetLabelSize(15)
        h.GetYaxis().SetLabelSize(15)
        h.SetMinimum(0)
        h.SetMaximum(3)
        #h.GetXaxis().LabelsOption("v")
        nWarnings = 0
        nErrors = 0
        for i in range(h.GetNbinsX()):
            for j in range(h.GetNbinsY()):
                ykey = self._dqmKeys.keys()[j]
                xkey = self._dqmKeys[ykey].keys()[i]
                h.SetBinContent(i+1, j+1, self._dqmKeys[ykey][xkey])
                h.GetYaxis().SetBinLabel(j+1, ykey)
                h.GetXaxis().SetBinLabel(i+1, xkey)
                if self._dqmKeys[ykey][xkey] > 2:
                    nErrors += 1
                elif self._dqmKeys[ykey][xkey] > 1:
                    nWarnings += 1
        palette = array.array("i", [ROOT.kGreen+1, ROOT.kYellow, ROOT.kRed])
        ROOT.gStyle.SetPalette(3, palette)
        c = ROOT.TCanvas()
        c.SetBottomMargin(0.2)
        c.SetLeftMargin(0.2)
        c.SetRightMargin(0.2)
        h.Draw("colz")
        
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning
        for item in ["png", "C", "pdf"]:
            c.Print(self._plotDirName+"/QCDNormalisationDQM.%s"%item)
        ROOT.gErrorIgnoreLevel = backup
        ROOT.gStyle.SetPalette(1)
        print "Obtained %d warnings and %d errors for the normalization"%(nWarnings, nErrors)
        if nWarnings > 0 or nErrors > 0:
            print "Please have a look at %s/QCDNormalisationDQM.png to see the origin of the warning(s) and error(s)"%self._plotDirName
        return

    def _addDqmEntry(self, binLabel, name, value, okTolerance, warnTolerance):
        if not binLabel in self._dqmKeys.keys():
            self._dqmKeys[binLabel] = OrderedDict()
        result = 2.5
        if abs(value) < okTolerance:
            result = 0.5
        elif abs(value) < warnTolerance:
            result = 1.5
        self._dqmKeys[binLabel][name] = result
        return

    def _getSanityCheckTextForFractions(self, dataTemplate, binLabel, saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        
        NOTE: Should one divide the fractions with dataTemplate.getFittedParameters()[0] ? 
              Right now not because the correction is so small.
        '''
        self.Verbose("_getSanityCheckTextForFractions()", True)
        
        # Get variables
        label         = "QCD"
        fraction      = dataTemplate.getFittedParameters()[1]
        fractionError = dataTemplate.getFittedParameterErrors()[1]
        nBaseline     = self._templates["%s_Baseline" % label].getNeventsFromHisto(False)
        nCalculated   = fraction * dataTemplate.getNeventsFromHisto(False)

        if nCalculated > 0:
            ratio = nBaseline / nCalculated
        else:
            ratio = 0
        lines = []
        lines.append("Fitted %s fraction: %f +- %f" % (label, fraction, fractionError))
        lines.append("Sanity check: ratio = %.3f: baseline = %.1f vs. fitted = %.1f" % (ratio, nBaseline, nCalculated))

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines

    def _checkOverallNormalization(self, template, binLabel, saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        '''
        self.Verbose("_checkOverallNormalization()")
        
        # Calculatotions
        value = template.getFittedParameters()[0]
        error = template.getFittedParameterErrors()[0]

        # Definitions
        lines = []
        lines.append("The fitted overall normalization factor for purity is: (should be 1.0)")
        lines.append("NormFactor = %f +/- %f" % (value, error))

        self._addDqmEntry(binLabel, "OverallNormalization(par0)", value-1.0, 0.03, 0.10)

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines
    
    ## Helper method to be called from parent class when calculating norm.coefficients
    def _getResultOutput(self, binLabel):
        lines = []
        lines.append("   Normalization factor (QCD): %f +- %f"%(self._TF[binLabel], self._TFError[binLabel]))
        lines.append("   Normalization factor (EWK fake taus): %f +- %f"%(self._ewkNormalization[binLabel], self._ewkNormalizationError[binLabel]))
        lines.append("   Combined norm. factor: %f +- %f"%(self._combinedFakesNormalization[binLabel], self._combinedFakesNormalizationError[binLabel]))

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)
        return lines
class FakeBNormalizationManager:
    '''
    Base class for QCD measurement normalization from which specialized algorithm classes inherit
    '''
    def __init__(self, binLabels, resultDirName, moduleInfoString, verbose=False):
        self._verbose      = verbose
        self._templates    = {}
        self._binLabels    = binLabels
        self._sources      = {}
        self._commentLines = []
        self._NEvtsCR1       = {}
        self._NEvtsCR1_Error = {}
        self._NEvtsCR2       = {}
        self._NEvtsCR2_Error = {}
        self._NEvtsCR3       = {}
        self._NEvtsCR3_Error = {}
        self._NEvtsCR4       = {}
        self._NEvtsCR4_Error = {}
        self._TF           = {} # Transfer Factor (TF)
        self._TF_Error     = {}
        self._TF_Up        = {}
        self._TF_Down      = {}
        self._TF_Up2x      = {}
        self._TF_Down2x    = {}
        self._TF_Up3x      = {}
        self._TF_Down3x    = {}
        self._dqmKeys      = OrderedDict()
        self._myPath       = os.path.join(resultDirName, "normalisationPlots")
        self._BinLabelMap  = {}
        self._FakeBNormalization       = {} # for the time being same as TF
        self._FakeBNormalizationError  = {} # for the time being same as TF_Error
        self._FakeBNormalizationUp     = {} # for the time being same as TF_Up
        self._FakeBNormalizationUp2x   = {} 
        self._FakeBNormalizationUp3x   = {} 
        self._FakeBNormalizationDown   = {} # for the time being same as TF_Down
        self._FakeBNormalizationDown2x = {} 
        self._FakeBNormalizationDown3x = {} 

        if not isinstance(binLabels, list):
            raise Exception("Error: binLabels needs to be a list of strings")
        self.Verbose("__init__")

        # No optimisation mode
        if moduleInfoString == "":
            moduleInfoString = "Default" 
        
        if not os.path.exists(self._myPath):
            self.Print("Creating new directory %s" % (self._myPath), True )
            os.mkdir(self._myPath)
        self._plotDirName = os.path.join(resultDirName, "normalisationPlots", moduleInfoString)

        # If already exists, Delete an entire directory tree
        if os.path.exists(self._plotDirName):
            msg = "Removing directory tree %s" % (self._plotDirName)
            self.Verbose(ShellStyles.NoteStyle() + msg + ShellStyles.NormalStyle(), True)
            shutil.rmtree(self._plotDirName)
        msg = "Creating directory %s" % (self._plotDirName)
        self.Verbose(ShellStyles.SuccessStyle() + msg + ShellStyles.NormalStyle(), False)
        os.mkdir(self._plotDirName)
        return

    def Print(self, msg, printHeader=False):
        fName = __file__.split("/")[-1]
        if printHeader==True:
           # print "=== ", fName + ": class " + self.__class__.__name__
            print "=== ", fName
            print "\t", msg
        else:
            print "\t", msg
            return
        
    def Verbose(self, msg, printHeader=True, verbose=False):
        if not self._verbose:
            return
        self.Print(msg, printHeader)
        return

    def GetQCDNormalization(self, binLabel):
        if binLabel in self._TF.keys():
            return self._TF[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! "% (binLabel) )

    def GetTransferFactor(self, binLabel):
        return self.GetQCDNormalization(binLabel)


    def GetQCDNormalizationError(self, binLabel):
        if binLabel in self._TF_Error.keys():
            return self._TF_Error[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! "% (binLabel) )
    
    def CalculateTransferFactor(self, binLabel, hFakeB_CR1, hFakeB_CR2, hFakeB_CR3, hFakeB_CR4, verbose=False):
        '''
        Calculates the combined normalization and, if specified, 
        varies it up or down by factor (1+variation)
 
        TF = Transfer Factor
        SR = Signal Region
        CR = Control Region
        VR = Verification Region
        '''
        self.verbose = verbose

        # Obtain counts for QCD and EWK fakes
        lines = []

        # NOTES: Add EWKGenuineB TF, Add Data TF, add QCD TF, Add EWK TF, add MCONLY TFs
        nCR1_Error = ROOT.Double(0.0)
        nCR2_Error = ROOT.Double(0.0)
        nCR3_Error = ROOT.Double(0.0)
        nCR4_Error = ROOT.Double(0.0)
        
        # Get Events in all CRs and their associated errors
        nCR1 = hFakeB_CR1.IntegralAndError(1, hFakeB_CR1.GetNbinsX()+1, nCR1_Error)
        nCR2 = hFakeB_CR2.IntegralAndError(1, hFakeB_CR2.GetNbinsX()+1, nCR2_Error)
        nCR3 = hFakeB_CR3.IntegralAndError(1, hFakeB_CR3.GetNbinsX()+1, nCR3_Error)
        nCR4 = hFakeB_CR4.IntegralAndError(1, hFakeB_CR4.GetNbinsX()+1, nCR4_Error)

        # Calculate Transfer Factor (TF) from Control Region (R) to Signal Region (SR): R = N_CR1/ N_CR2
        TF        = None
        TF_Up     = None
        TF_Up2x   = None
        TF_Up3x   = None
        TF_Down   = None
        TF_Down2x = None
        TF_Down3x = None
        TF_Error  = None
        TF        = (nCR1 / nCR2)
        TF_Error  = errorPropagation.errorPropagationForDivision(nCR1, nCR1_Error, nCR2, nCR2_Error)

        # Up variations
        TF_Up    = TF + TF_Error
        TF_Up2x  = TF + 2*TF_Error
        TF_Up3x  = TF + 3*TF_Error
        if TF_Up > 1.0:
            Print("Forcing TF_Up (=%.3f) to be equal to 1!" % ( TF_Up), True) # added  23 Oct 2018
            TF_Up = 1.0
        if TF_Up2x > 1.0:
            Print("Forcing TF_Up2x (=%.3f) to be equal to 1!" % ( TF_Up2x), True) # added  23 Oct 2018
            TF_Up2x = 1.0
        if TF_Up3x > 1.0:
            Print("Forcing TF_Up3x (=%.3f) to be equal to 1!" % ( TF_Up3x), True) # added  23 Oct 2018
            TF_Up3x = 1.0

        # Down variations
        TF_Down   = TF - TF_Error
        TF_Down2x = TF - 2*TF_Error
        TF_Down3x = TF - 3*TF_Error
        if TF_Down < 0.0:
            Print("Forcing TF_Down   (=%.3f) to be equal to 0" % (TF_Down), True) # added  23 Oct 2018
            TF_Down = 0.0
        if TF_Down2x < 0.0:
            Print("Forcing TF_Down2x (=%.3f) to be equal to 0" % (TF_Down2x), True) # added  23 Oct 2018
            TF_Down2x = 0.0
        if TF_Down3x < 0.0:
            Print("Forcing TF_Down3x (=%.3f) to be equal to 0" % (TF_Down3x), True) # added  23 Oct 2018
            TF_Down3x = 0.0

        lines.append("TF (bin=%s) = N_CR1 / N_CR2 = %f / %f =  %f +- %f" % (binLabel, nCR1, nCR2, TF, TF_Error) )

        # Calculate the transfer factors (R_{i}) where i is index of bin the Fake-b measurement is made in (pT and/or eta of ldg b-jet)
        if TF != None:
            # Replace bin label with histo title (has exact binning info)
            self._BinLabelMap[binLabel] = self.getNiceBinLabel(hFakeB_CR2.GetTitle())
            self._NEvtsCR1[binLabel]       = nCR1
            self._NEvtsCR1_Error[binLabel] = nCR1_Error
            self._NEvtsCR2[binLabel]       = nCR2
            self._NEvtsCR2_Error[binLabel] = nCR2_Error
            self._NEvtsCR3[binLabel]       = nCR3
            self._NEvtsCR3_Error[binLabel] = nCR3_Error
            self._NEvtsCR4[binLabel]       = nCR4
            self._NEvtsCR4_Error[binLabel] = nCR4_Error
            self._TF[binLabel   ]       = TF
            self._TF_Error[binLabel]    = TF_Error
            self._TF_Up[binLabel]       = TF_Up
            self._TF_Up2x[binLabel]     = TF_Up2x
            self._TF_Up3x[binLabel]     = TF_Up3x
            self._TF_Down[binLabel]     = TF_Down 
            self._TF_Down2x[binLabel]   = TF_Down2x 
            self._TF_Down3x[binLabel]   = TF_Down3x 
            self._FakeBNormalization[binLabel]       = TF         # TF
            self._FakeBNormalizationError[binLabel]  = TF_Error   # Error(TF)
            self._FakeBNormalizationUp[binLabel]     = TF_Up      # TF + Error
            self._FakeBNormalizationUp2x[binLabel]   = TF_Up2x    # TF + 2*Error
            self._FakeBNormalizationUp3x[binLabel]   = TF_Up3x    # TF + 3*Error
            self._FakeBNormalizationDown[binLabel]   = TF_Down    # TF - Error
            self._FakeBNormalizationDown2x[binLabel] = TF_Down2x  # TF - 2*Error
            self._FakeBNormalizationDown3x[binLabel] = TF_Down3x  # TF - 3*Error

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)

        # Print output and store comments
        if 0:
            for i, line in enumerate(lines, 1):
                Print(line, i==1)
        return

    def getNiceBinLabel(self, binLabel):
        newLabel = binLabel.replace("abs", "")
        if 1:
            newLabel = newLabel.replace("TetrajetBjet", " ")
            newLabel = newLabel.replace("TetrajetBJet", " ")
        else: #more room
            newLabel = newLabel.replace("TetrajetBjet", "b-jet ")
            newLabel = newLabel.replace("TetrajetBJet", "b-jet ")
        newLabel = newLabel.replace("_", " ")
        newLabel = newLabel.replace("(", "|")
        newLabel = newLabel.replace(")", "|")
        newLabel = newLabel.replace("Eta", "#eta")
        newLabel = newLabel.replace("..", "-")
        newLabel = newLabel.replace("CRtwo", "")
        return newLabel

    def writeTransferFactorsToFile(self, filename, opts):
        '''
        Save the fit results for QCD and EWK.

        The results will are stored in a python file starting with name:
        "QCDInvertedNormalizationFactors_" + moduleInfoString

        The script also summarizes warnings and errors encountered:
        - Green means deviation from normal is 0-3 %,
        - Yellow means deviation of 3-10 %, and
        - Red means deviation of >10 % (i.e. something is clearly wrong).
        
        If necessary, do adjustments to stabilize the fits to get rid of the errors/warnings. 
        The first things to work with are:
        a) Make sure enough events are in the histograms used
        b) Adjust fit parameters and/or fit functions and re-fit results
        
        Move on only once you are pleased with the normalisation coefficients
        '''
        s = ""
        s += "# Generated on %s\n"% datetime.datetime.now().ctime()
        s += "# by %s\n" % os.path.basename(sys.argv[0])
        s += "\n"
        s += "import sys\n"
        s += "\n"
        s += "def FakeBNormalisationSafetyCheck(era, searchMode, optimizationMode):\n"
        s += "    validForEra        = \"%s\"\n" % opts.dataEra
        s += "    validForSearchMode = \"%s\"\n" % opts.searchMode
        s += "    validForOptMode    = \"%s\"\n" % opts.optMode
        s += "    if not era == validForEra:\n"
        s += "        raise Exception(\"Error: inconsistent era, normalisation factors valid for\",validForEra,\"but trying to use with\",era)\n"
        s += "    if not searchMode == validForSearchMode:\n"
        s += "        raise Exception(\"Error: inconsistent search mode, normalisation factors valid for\",validForSearchMode,\"but trying to use with\",searchMode)\n"
        s += "    if not optimizationMode == validForOptMode:\n"
        s += "        raise Exception(\"Error: inconsistent optimization mode, normalisation factors valid for\",validForOptMode,\"but trying to use with\",optimizationMode)\n"
        s += "    return"
        s += "\n"
        s += "\n"

        # First write the transfer factor (for each Fake-b measurement bin)
        s += "FakeBNormalisation_Value = {\n"
        for binLabel in self._TF:            
            s += '    "%s": %f,\n' % (binLabel, self._TF[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factor error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_Error = {\n"
        for binLabel in self._TF_Error:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Error[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors + error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorUp = {\n"
        for binLabel in self._TF_Up:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Up[binLabel]) 
        s += "}\n"
        s += "\n"

        # Then write the transfer factors + 2*error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorUp2x = {\n"
        for binLabel in self._TF_Up2x:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Up2x[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors + 3*error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorUp3x = {\n"
        for binLabel in self._TF_Up3x:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Up3x[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors - error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorDown = {\n"
        for binLabel in self._TF_Down:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Down[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors - 2*error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorDown2x = {\n"
        for binLabel in self._TF_Down2x:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Down2x[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors - 3*error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorDown3x = {\n"
        for binLabel in self._TF_Down3x:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Down3x[binLabel])
        s += "}\n"
        s += "\n"

        # Then write bin label map
        s += "BinLabelMap = {\n"
        for binLabel in self._BinLabelMap:
            s += '    "%s": \"%s\",\n' % (binLabel, self._BinLabelMap[binLabel])
        s += "}\n"
        s += "\n"
            
        self.Verbose("Writing results in file %s" % filename, True)
        fOUT = open(filename,"w")
        fOUT.write(s)
        fOUT.write("'''\n")
        for l in self._commentLines:
            fOUT.write(l + "\n")
        fOUT.write("'''\n")
        fOUT.close()

        msg = "Results written in file %s" % (ShellStyles.SuccessStyle()  + filename + ShellStyles.NormalStyle())
        self.Print(msg, True)

        # Create the transfer factors plot (for each bin of FakeB measurement)
        self._generateTransferFactorsPlot() 
        self._generateDQMPlot()
        return

    def _generateTransferFactorsPlot(self):
        '''
        The resulting plot will contain the transfer factors (y-axis) for a given measurement bin (x-axis)
        This is needed in the case the Fake-b measurement is done in
        bins of a correlated quantity (e.g. eta of leading b-jet in Fake-b for HToTB, and tau-pT in the 
        case of inverted tau isolatio for HToTau)
        '''
        def makeGraph(markerStyle, color, binList, valueDict, upDict, downDict):
            g = ROOT.TGraphAsymmErrors(len(binList))
            for i in range(len(binList)):
                g.SetPoint(i, i+0.5, valueDict[binList[i]])
                g.SetPointEYhigh(i, upDict[binList[i]])
                g.SetPointEYlow(i, downDict[binList[i]])
            g.SetMarkerSize(1.2)
            g.SetMarkerStyle(markerStyle)
            g.SetLineColor(color)
            g.SetLineWidth(3)
            g.SetMarkerColor(color)
            return g
        # Obtain bin list in right order
        keyList = []
        keys = self._TF.keys()
        keys.sort()
#        for k in keys:
#            if "lt" in k:
#                keyList.append(k)
#        for k in keys:
#            if "eq" in k:
#                keyList.append(k)
#        for k in keys:
#            if "gt" in k:
#                keyList.append(k)

        # For-loop: All Fake-b measurement bins
        for k in keys:        
            keyList.append(k)
            #if "Inclusive" in keys:
            #    keyList.append("Inclusive")
            
            
        # Apply TDR style
        style = tdrstyle.TDRStyle()
        style.setGridX(False)
        style.setGridY(False)
        style.setOptStat(False)

        # Create graphs
        gFakeB = makeGraph(ROOT.kFullCircle, ROOT.kAzure, keyList, self._TF, self._TF_Error, self._TF_Error)

        # Make plot
        hFrame = ROOT.TH1F("frame","frame", len(keyList), 0, len(keyList))

        # Change bin labels to text
        for i, binLabel in enumerate(keyList, 1):
            binLabelText = self.getFormattedBinLabelString(binLabel)
            hFrame.GetXaxis().SetBinLabel(i, binLabelText)

        # Set axes names
        hFrame.GetYaxis().SetTitle("transfer factor") #R_{i}
        # hFrame.GetXaxis().SetTitle("Fake-b bin")
        
        # Customise axes
        logy = False
        if logy:
            hFrame.SetMinimum(0.6e-1)
            hFrame.SetMaximum(2e0)
        else:
            hFrame.SetMinimum(0.0)
            hFrame.SetMaximum(1.0)
            
        if len(self._BinLabelMap) > 12:
            lSize = 8
        elif len(self._BinLabelMap) > 8:
            lSize = 12
        else:
            lSize = 16
        hFrame.GetXaxis().SetLabelSize(lSize) # 20
        hFrame.GetXaxis().LabelsOption("d")
        # Label Style options
        # "a" sort by alphabetic order 
        # ">" sort by decreasing values 
        # "<" sort by increasing values 
        # "h" draw labels horizonthal 
        # "v" draw labels vertical
        # "u" draw labels up (end of label right adjusted)
        # "d" draw labels down (start of label left adjusted)

        # Create canvas
        c = ROOT.TCanvas()
        c.SetLogy(logy)
        c.SetGridx(False)
        c.SetGridy(False)

        hFrame.Draw()
        gFakeB.Draw("p same")
        histograms.addStandardTexts(cmsTextPosition="outframe")
        
        # Create the legend & draw it
        l = ROOT.TLegend(0.65, 0.80, 0.90, 0.90)
        l.SetFillStyle(-1)
        l.SetBorderSize(0)
        l.AddEntry(gFakeB, "Value #pm stat.", "LP") # "Fake-#it{b} #pm Stat.", "LP"
        l.SetTextSize(0.035)
        if 0:
            l.Draw()

        # Store ROOT ignore level to normal before changing it
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning

        # Save the plot
        for item in ["png", "C", "pdf"]:
            c.Print(self._plotDirName + "/FakeBNormalisationCoefficients.%s" % item)

        # Reset the ROOT ignore level to normal
        ROOT.gErrorIgnoreLevel = backup

        # Inform user
        msg = "Plot saved under %s" % (ShellStyles.SuccessStyle() + self._plotDirName + "/" + ShellStyles.NormalStyle())
        self.Print(msg, True)
        return

    def getFormattedBinLabelString(self, binLabel):
        '''
        Dirty trick to get what I want
        '''
        if binLabel not in self._BinLabelMap:
            raise Exception("Got unexpected bin label \"%s\"!" % binLabel)
        newLabel = self._BinLabelMap[binLabel]
        newLabel = newLabel.replace("abs(", "|")
        newLabel = newLabel.replace(")", "|")
        newLabel = newLabel.replace("..", "-")
        newLabel = newLabel.replace(":", ",")
        newLabel = newLabel.replace("TetrajetBJet", "") #"b^{ldg} ")
        newLabel = newLabel.replace("Pt", "p_{T} ")
        newLabel = newLabel.replace("Eta", "#eta ")
        if "inclusive" in binLabel.lower():
            newLabel = "Inclusive"
        return newLabel

    def _generateDQMPlot(self):
        '''
        Create a Data Quality Monitor (DQM) style plot
        to easily check the error for each transfer factor
        and whether it is within an acceptable relative error
        '''
        # Define error warning/tolerance on relative errors
        okay  = 1.0/(len(self._BinLabelMap.keys()))
        warn  = 0.5*okay
        NEvts = []
        # Check the uncertainties on the normalization factors
        for k in self._BinLabelMap:
            relErrorUp   = abs(self._TF_Up[k])/(self._TF[k])
            relErrorDown = abs(self._TF_Down[k])/(self._TF[k])
            relError     = self._TF_Error[k]/self._TF[k]
            if 0: 
                print "bin = %s , relErrorUp = %s, relErrorDown = %s " % (k, relErrorUp, relErrorDown)

            # Add DQM entries
            NCR1 = 0
            NCR2 = 0
            NCR3 = 0
            NCR4 = 0
            for j in self._NEvtsCR1:
                NCR1 += self._NEvtsCR1[j]
                NEvts.append(self._NEvtsCR1[j])
            for j in self._NEvtsCR2:
                NCR2 += self._NEvtsCR2[j]
                NEvts.append(self._NEvtsCR2[j])                
            for j in self._NEvtsCR3:
                NCR3 += self._NEvtsCR3[j]
                NEvts.append(self._NEvtsCR3[j])
            for j in self._NEvtsCR4:
                NCR4 += self._NEvtsCR4[j]
                NEvts.append(self._NEvtsCR4[j])                

            if 0:
                print "NCR1[%s] = %0.1f, NCR2[%s] = %0.1f, k = %s" % (k, self._NEvtsCR1[k], k, self._NEvtsCR2[k], k)
                print "NCR1 = %s, NCR2 = %s, k = %s" % (NCR1, NCR2, k)
                print "error/NCR1[%s] = %0.2f, error/NCR2[%s] = %0.2f" % (k, self._NEvtsCR1_Error[k]/self._NEvtsCR1[k], k, self._NEvtsCR2_Error[k]/self._NEvtsCR2[k])
            
            # Add DQM plot entries
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR1}", self._NEvtsCR1[k], NCR1*okay, NCR1*warn)
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR2}", self._NEvtsCR2[k], NCR2*okay, NCR2*warn)
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR3}", self._NEvtsCR3[k], NCR3*okay, NCR3*warn)
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR4}", self._NEvtsCR4[k], NCR4*okay, NCR4*warn)
            # self._addDqmEntry(self._BinLabelMap[k], "#frac{#sigma_{CR1}}{N_{CR1}}", self._NEvtsCR1_Error[k]/NCR1, 0.05, 0.15)
            # self._addDqmEntry(self._BinLabelMap[k], "#frac{#sigma_{CR2}}{N_{CR2}}", self._NEvtsCR2_Error[k]/NCR2, 0.05, 0.15)
            
        # Construct the DQM histogram
        nBinsX = len(self._dqmKeys[self._dqmKeys.keys()[0]].keys())
        nBinsY = len(self._dqmKeys.keys())
        h = ROOT.TH2F("FakeB DQM", "FakeB DQM", nBinsX, 0, nBinsX, nBinsY, 0, nBinsY)

        # Customise axes
        h.GetXaxis().SetLabelSize(15)
        h.GetYaxis().SetLabelSize(10)

        # Set Min and Max of z-axis
        if 0: # red, yellow, green for DQM 
            h.SetMinimum(0)
            h.SetMaximum(3)
        else: # pure entries instead of red, yellow, green
            h.SetMinimum(min(NEvts)*0.25)
            h.SetMaximum(round(max(NCR1, NCR2, NCR3, NCR4)))
            h.SetContour(10)
            #h.SetContour(3)
        if 0:
            h.GetXaxis().LabelsOption("v")
            h.GetYaxis().LabelsOption("v")
            
        nWarnings = 0
        nErrors   = 0
        # For-loop: All x-axis bins
        for i in range(h.GetNbinsX()):
            # For-loop: All y-axis bins
            for j in range(h.GetNbinsY()):
                ykey = self._dqmKeys.keys()[j]
                xkey = self._dqmKeys[ykey].keys()[i]

                # Set the bin content
                h.SetBinContent(i+1, j+1, self._dqmKeys[ykey][xkey])
                h.GetXaxis().SetBinLabel(i+1, xkey)
                h.GetYaxis().SetBinLabel(j+1, ykey)
                if self._dqmKeys[ykey][xkey] > 2:
                    nErrors += 1
                elif self._dqmKeys[ykey][xkey] > 1:
                    nWarnings += 1

        # Apply TDR style
        style = tdrstyle.TDRStyle()
        style.setOptStat(False)
        style.setGridX(False)
        style.setGridY(False)
        style.setWide(True, 0.15)
        
        # Set the colour styling (red, yellow, green)
        if 0:
            palette = array.array("i", [ROOT.kGreen+1, ROOT.kYellow, ROOT.kRed])
            ROOT.gStyle.SetPalette(3, palette)
        else:
            # https://root.cern.ch/doc/master/classTColor.html
            ROOT.gStyle.SetPalette(ROOT.kLightTemperature)
            # ROOT.gStyle.SetPalette(ROOT.kColorPrintableOnGrey)
            #tdrstyle.setRainBowPalette()
            #tdrstyle.setDeepSeaPalette()

        # Create canvas        
        c = ROOT.TCanvas()
        c.SetLogx(False)
        c.SetLogy(False)
        c.SetLogz(True)
        c.SetGridx()
        c.SetGridy()
        h.Draw("COLZ") #"COLZ TEXT"


        # Add CMS text and text with colour keys
        histograms.addStandardTexts(cmsTextPosition="outframe") 
        if 0:
            histograms.addText(0.55, 0.80, "green < %.0f %%" % (okay*100), size=20)
            histograms.addText(0.55, 0.84, "yellow < %.0f %%" % (warn*100), size=20)
            histograms.addText(0.55, 0.88, "red > %.0f %%" % (warn*100), size=20)

        # Save the canvas to a file
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning
        plotName = os.path.join(self._plotDirName, "FakeBNormalisationDQM")
        # For-loop: Save formats
        for ext in ["png", "C", "pdf"]:
            saveName ="%s.%s" % (plotName, ext)
            c.Print(saveName)
        ROOT.gErrorIgnoreLevel = backup
        ROOT.gStyle.SetPalette(1)

        msg = "Obtained %d warnings and %d errors for the normalisation" % (nWarnings, nErrors)
        self.Verbose(msg)
        if nWarnings > 0:
            msg = "DQM has %d warnings and %d errors! Please have a look at %s.png." % (nWarnings, nErrors, os.path.basename(plotName))
            self.Verbose(ShellStyles.ErrorStyle() + msg + ShellStyles.NormalStyle(), True)

        #if nWarnings > 0 or nErrors > 0:
        if nErrors > 0:
            msg = "DQM has %d warnings and %d errors! Please have a look at %s.png." % (nWarnings, nErrors, os.path.basename(plotName))
            self.Verbose(ShellStyles.ErrorStyle() + msg + ShellStyles.NormalStyle(), True)
        return

    def _addDqmEntry(self, binLabel, name, value, okTolerance, warnTolerance):
        # Define colour codes
        red    = 2.5
        yellow = 1.5 
        green  = 0.5

        if not binLabel in self._dqmKeys.keys():
            self._dqmKeys[binLabel] = OrderedDict()
        result = red

        if abs(value) > okTolerance:
            result = green
        elif abs(value) > warnTolerance:
            result = yellow
        else:
            pass

        #self._dqmKeys[binLabel][name] = result
        self._dqmKeys[binLabel][name] = value
        return

    def _getSanityCheckTextForFractions(self, dataTemplate, binLabel, saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        
        NOTE: Should one divide the fractions with dataTemplate.getFittedParameters()[0] ? 
              Right now not because the correction is so small.
        '''
        self.Verbose("_getSanityCheckTextForFractions()", True)
        
        # Get variables
        label         = "QCD"
        fraction      = dataTemplate.getFittedParameters()[1]
        fractionError = dataTemplate.getFittedParameterErrors()[1]
        nBaseline     = self._templates["%s_Baseline" % label].getNeventsFromHisto(False)
        nCalculated   = fraction * dataTemplate.getNeventsFromHisto(False)

        if nCalculated > 0:
            ratio = nBaseline / nCalculated
        else:
            ratio = 0
        lines = []
        lines.append("Fitted %s fraction: %f +- %f" % (label, fraction, fractionError))
        lines.append("Sanity check: ratio = %.3f: baseline = %.1f vs. fitted = %.1f" % (ratio, nBaseline, nCalculated))

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines

    def _checkOverallNormalization(self, template, binLabel, saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        '''
        self.Verbose("_checkOverallNormalization()")
        
        # Calculatotions
        value = template.getFittedParameters()[0]
        error = template.getFittedParameterErrors()[0]

        # Definitions
        lines = []
        lines.append("The fitted overall normalization factor for purity is: (should be 1.0)")
        lines.append("NormFactor = %f +/- %f" % (value, error))

        self._addDqmEntry(binLabel, "OverallNormalization(par0)", value-1.0, 0.03, 0.10)

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines
    
    ## Helper method to be called from parent class when calculating norm.coefficients
    def _getResultOutput(self, binLabel):
        lines = []
        lines.append("Transfer Factor (bin=%s): %f +- %f" % (binLabel, self._TF[binLabel], self._TFError[binLabel]) )
        lines.append("Normalisation (bin=%s)  : %f +- %f" % (binLabel, self._FakeBNormalization[binLabel], self._FakeBNormalizationError[binLabel]))

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)
        return lines
class FakeBNormalizationManager:
    '''
    Base class for QCD measurement normalization from which specialized algorithm classes inherit
    '''
    def __init__(self,
                 binLabels,
                 resultDirName,
                 moduleInfoString,
                 verbose=False):
        self._verbose = verbose
        self._templates = {}
        self._binLabels = binLabels
        self._sources = {}
        self._commentLines = []
        self._NEvtsCR1 = {}
        self._NEvtsCR1_Error = {}
        self._NEvtsCR2 = {}
        self._NEvtsCR2_Error = {}
        self._NEvtsCR3 = {}
        self._NEvtsCR3_Error = {}
        self._NEvtsCR4 = {}
        self._NEvtsCR4_Error = {}
        self._TF = {}  # Transfer Factor (TF)
        self._TF_Error = {}
        self._TF_Up = {}
        self._TF_Down = {}
        self._dqmKeys = OrderedDict()
        self._myPath = os.path.join(resultDirName, "normalisationPlots")
        self._BinLabelMap = {}
        self._FakeBNormalization = {}  # for the time being same as TF
        self._FakeBNormalizationError = {
        }  # for the time being same as TF_Error
        self._FakeBNormalizationUp = {}  # for the time being same as TF_Up
        self._FakeBNormalizationDown = {}  # for the time being same as TF_Down
        if not isinstance(binLabels, list):
            raise Exception("Error: binLabels needs to be a list of strings")
        self.Verbose("__init__")

        # No optimisation mode
        if moduleInfoString == "":
            moduleInfoString = "Default"

        if not os.path.exists(self._myPath):
            self.Print("Creating new directory %s" % (self._myPath), True)
            os.mkdir(self._myPath)
        self._plotDirName = os.path.join(resultDirName, "normalisationPlots",
                                         moduleInfoString)

        # If already exists, Delete an entire directory tree
        if os.path.exists(self._plotDirName):
            msg = "Removing directory tree %s" % (self._plotDirName)
            self.Verbose(
                ShellStyles.NoteStyle() + msg + ShellStyles.NormalStyle(),
                True)
            shutil.rmtree(self._plotDirName)
        msg = "Creating directory %s" % (self._plotDirName)
        self.Verbose(
            ShellStyles.SuccessStyle() + msg + ShellStyles.NormalStyle(),
            False)
        os.mkdir(self._plotDirName)
        return

    def Print(self, msg, printHeader=False):
        fName = __file__.split("/")[-1]
        if printHeader == True:
            # print "=== ", fName + ": class " + self.__class__.__name__
            print "=== ", fName
            print "\t", msg
        else:
            print "\t", msg
            return

    def Verbose(self, msg, printHeader=True, verbose=False):
        if not self._verbose:
            return
        self.Print(msg, printHeader)
        return

    def GetQCDNormalization(self, binLabel):
        if binLabel in self._TF.keys():
            return self._TF[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! " %
                            (binLabel))

    def GetTransferFactor(self, binLabel):
        return self.GetQCDNormalization(binLabel)

    def GetQCDNormalizationError(self, binLabel):
        if binLabel in self._TF_Error.keys():
            return self._TF_Error[binLabel]
        else:
            raise Exception("Error: _TF dictionary has no key \"%s\"! " %
                            (binLabel))

    def CalculateTransferFactor(self,
                                binLabel,
                                hFakeB_CR1,
                                hFakeB_CR2,
                                hFakeB_CR3,
                                hFakeB_CR4,
                                verbose=False):
        '''
        Calculates the combined normalization and, if specified, 
        varies it up or down by factor (1+variation)
 
        TF = Transfer Factor
        SR = Signal Region
        CR = Control Region
        VR = Verification Region
        '''
        self.verbose = verbose

        # Obtain counts for QCD and EWK fakes
        lines = []

        # NOTES: Add EWKGenuineB TF, Add Data TF, add QCD TF, Add EWK TF, add MCONLY TFs
        nCR1_Error = ROOT.Double(0.0)
        nCR2_Error = ROOT.Double(0.0)
        nCR3_Error = ROOT.Double(0.0)
        nCR4_Error = ROOT.Double(0.0)

        # Get Events in all CRs and their associated errors
        nCR1 = hFakeB_CR1.IntegralAndError(1,
                                           hFakeB_CR1.GetNbinsX() + 1,
                                           nCR1_Error)
        nCR2 = hFakeB_CR2.IntegralAndError(1,
                                           hFakeB_CR2.GetNbinsX() + 1,
                                           nCR2_Error)
        nCR3 = hFakeB_CR3.IntegralAndError(1,
                                           hFakeB_CR3.GetNbinsX() + 1,
                                           nCR3_Error)
        nCR4 = hFakeB_CR4.IntegralAndError(1,
                                           hFakeB_CR4.GetNbinsX() + 1,
                                           nCR4_Error)

        # Calculate Transfer Factor (TF) from Control Region (R) to Signal Region (SR): R = N_CR1/ N_CR2
        TF = None
        TF_Up = None
        TF_Down = None
        TF_Error = None
        TF = (nCR1 / nCR2)
        TF_Error = errorPropagation.errorPropagationForDivision(
            nCR1, nCR1_Error, nCR2, nCR2_Error)
        TF_Up = TF + TF_Error
        if TF_Up > 1.0:
            TF_Up = 1.0
        TF_Down = TF - TF_Error
        if TF_Down < 0.0:
            TF_Down = 0.0
        lines.append("TF (bin=%s) = N_CR1 / N_CR2 = %f / %f =  %f +- %f" %
                     (binLabel, nCR1, nCR2, TF, TF_Error))

        # Calculate the transfer factors (R_{i}) where i is index of bin the Fake-b measurement is made in (pT and/or eta of ldg b-jet)
        if TF != None:
            # Replace bin label with histo title (has exact binning info)
            self._BinLabelMap[binLabel] = self.getNiceBinLabel(
                hFakeB_CR2.GetTitle())
            self._NEvtsCR1[binLabel] = nCR1
            self._NEvtsCR1_Error[binLabel] = nCR1_Error
            self._NEvtsCR2[binLabel] = nCR2
            self._NEvtsCR2_Error[binLabel] = nCR2_Error
            self._NEvtsCR3[binLabel] = nCR3
            self._NEvtsCR3_Error[binLabel] = nCR3_Error
            self._NEvtsCR4[binLabel] = nCR4
            self._NEvtsCR4_Error[binLabel] = nCR4_Error
            self._TF[binLabel] = TF
            self._TF_Error[binLabel] = TF_Error
            self._TF_Up[binLabel] = TF_Up
            self._TF_Down[binLabel] = TF_Down
            self._FakeBNormalization[binLabel] = TF  # TF
            self._FakeBNormalizationError[binLabel] = TF_Error  # Error(TF)
            self._FakeBNormalizationUp[binLabel] = TF_Up  # TF + Error
            self._FakeBNormalizationDown[binLabel] = TF_Down  # TF - Error

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)

        # Print output and store comments
        if 0:
            for i, line in enumerate(lines, 1):
                Print(line, i == 1)
        return

    def getNiceBinLabel(self, binLabel):
        newLabel = binLabel.replace("abs", "")
        if 1:
            newLabel = newLabel.replace("TetrajetBjet", " ")
            newLabel = newLabel.replace("TetrajetBJet", " ")
        else:  #more room
            newLabel = newLabel.replace("TetrajetBjet", "b-jet ")
            newLabel = newLabel.replace("TetrajetBJet", "b-jet ")
        newLabel = newLabel.replace("_", " ")
        newLabel = newLabel.replace("(", "|")
        newLabel = newLabel.replace(")", "|")
        newLabel = newLabel.replace("Eta", "#eta")
        newLabel = newLabel.replace("..", "-")
        newLabel = newLabel.replace("CRtwo", "")
        return newLabel

    def writeTransferFactorsToFile(self, filename, opts):
        '''
        Save the fit results for QCD and EWK.

        The results will are stored in a python file starting with name:
        "QCDInvertedNormalizationFactors_" + moduleInfoString

        The script also summarizes warnings and errors encountered:
        - Green means deviation from normal is 0-3 %,
        - Yellow means deviation of 3-10 %, and
        - Red means deviation of >10 % (i.e. something is clearly wrong).
        
        If necessary, do adjustments to stabilize the fits to get rid of the errors/warnings. 
        The first things to work with are:
        a) Make sure enough events are in the histograms used
        b) Adjust fit parameters and/or fit functions and re-fit results
        
        Move on only once you are pleased with the normalisation coefficients
        '''
        s = ""
        s += "# Generated on %s\n" % datetime.datetime.now().ctime()
        s += "# by %s\n" % os.path.basename(sys.argv[0])
        s += "\n"
        s += "import sys\n"
        s += "\n"
        s += "def FakeBNormalisationSafetyCheck(era, searchMode, optimizationMode):\n"
        s += "    validForEra        = \"%s\"\n" % opts.dataEra
        s += "    validForSearchMode = \"%s\"\n" % opts.searchMode
        s += "    validForOptMode    = \"%s\"\n" % opts.optMode
        s += "    if not era == validForEra:\n"
        s += "        raise Exception(\"Error: inconsistent era, normalisation factors valid for\",validForEra,\"but trying to use with\",era)\n"
        s += "    if not searchMode == validForSearchMode:\n"
        s += "        raise Exception(\"Error: inconsistent search mode, normalisation factors valid for\",validForSearchMode,\"but trying to use with\",searchMode)\n"
        s += "    if not optimizationMode == validForOptMode:\n"
        s += "        raise Exception(\"Error: inconsistent optimization mode, normalisation factors valid for\",validForOptMode,\"but trying to use with\",optimizationMode)\n"
        s += "    return"
        s += "\n"
        s += "\n"

        # First write the transfer factor (for each Fake-b measurement bin)
        s += "FakeBNormalisation_Value = {\n"
        for binLabel in self._TF:
            s += '    "%s": %f,\n' % (binLabel, self._TF[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factor error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_Error = {\n"
        for binLabel in self._TF_Error:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Error[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors + error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorUp = {\n"
        for binLabel in self._TF_Up:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Up[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors - error (for each Fake-b measurement bin)
        s += "FakeBNormalisation_ErrorDown = {\n"
        for binLabel in self._TF_Down:
            s += '    "%s": %f,\n' % (binLabel, self._TF_Down[binLabel])
        s += "}\n"
        s += "\n"

        # Then write the transfer factors - error (for each Fake-b measurement bin)
        s += "BinLabelMap = {\n"
        for binLabel in self._BinLabelMap:
            s += '    "%s": \"%s\",\n' % (binLabel,
                                          self._BinLabelMap[binLabel])
        s += "}\n"
        s += "\n"

        self.Verbose("Writing results in file %s" % filename, True)
        fOUT = open(filename, "w")
        fOUT.write(s)
        fOUT.write("'''\n")
        for l in self._commentLines:
            fOUT.write(l + "\n")
        fOUT.write("'''\n")
        fOUT.close()

        msg = "Results written in file %s" % (
            ShellStyles.SuccessStyle() + filename + ShellStyles.NormalStyle())
        self.Print(msg, True)

        # Create the transfer factors plot (for each bin of FakeB measurement)
        self._generateTransferFactorsPlot()
        self._generateDQMPlot()
        return

    def _generateTransferFactorsPlot(self):
        '''
        The resulting plot will contain the transfer factors (y-axis) for a given measurement bin (x-axis)
        This is needed in the case the Fake-b measurement is done in
        bins of a correlated quantity (e.g. eta of leading b-jet in Fake-b for HToTB, and tau-pT in the 
        case of inverted tau isolatio for HToTau)
        '''
        def makeGraph(markerStyle, color, binList, valueDict, upDict,
                      downDict):
            g = ROOT.TGraphAsymmErrors(len(binList))
            for i in range(len(binList)):
                g.SetPoint(i, i + 0.5, valueDict[binList[i]])
                g.SetPointEYhigh(i, upDict[binList[i]])
                g.SetPointEYlow(i, downDict[binList[i]])
            g.SetMarkerSize(1.2)
            g.SetMarkerStyle(markerStyle)
            g.SetLineColor(color)
            g.SetLineWidth(3)
            g.SetMarkerColor(color)
            return g

        # Obtain bin list in right order
        keyList = []
        keys = self._TF.keys()
        keys.sort()
        #        for k in keys:
        #            if "lt" in k:
        #                keyList.append(k)
        #        for k in keys:
        #            if "eq" in k:
        #                keyList.append(k)
        #        for k in keys:
        #            if "gt" in k:
        #                keyList.append(k)

        # For-loop: All Fake-b measurement bins
        for k in keys:
            keyList.append(k)
            #if "Inclusive" in keys:
            #    keyList.append("Inclusive")

        # Apply TDR style
        style = tdrstyle.TDRStyle()
        style.setGridX(False)
        style.setGridY(False)
        style.setOptStat(False)

        # Create graphs
        gFakeB = makeGraph(ROOT.kFullCircle, ROOT.kAzure, keyList, self._TF,
                           self._TF_Error, self._TF_Error)

        # Make plot
        hFrame = ROOT.TH1F("frame", "frame", len(keyList), 0, len(keyList))

        # Change bin labels to text
        for i, binLabel in enumerate(keyList, 1):
            binLabelText = self.getFormattedBinLabelString(binLabel)
            hFrame.GetXaxis().SetBinLabel(i, binLabelText)

        # Set axes names
        hFrame.GetYaxis().SetTitle("transfer factor")  #R_{i}
        # hFrame.GetXaxis().SetTitle("Fake-b bin")

        # Customise axes
        logy = False
        if logy:
            hFrame.SetMinimum(0.6e-1)
            hFrame.SetMaximum(2e0)
        else:
            hFrame.SetMinimum(0.0)
            hFrame.SetMaximum(1.0)

        if len(self._BinLabelMap) > 12:
            lSize = 8
        elif len(self._BinLabelMap) > 8:
            lSize = 12
        else:
            lSize = 16
        hFrame.GetXaxis().SetLabelSize(lSize)  # 20
        hFrame.GetXaxis().LabelsOption("d")
        # Label Style options
        # "a" sort by alphabetic order
        # ">" sort by decreasing values
        # "<" sort by increasing values
        # "h" draw labels horizonthal
        # "v" draw labels vertical
        # "u" draw labels up (end of label right adjusted)
        # "d" draw labels down (start of label left adjusted)

        # Create canvas
        c = ROOT.TCanvas()
        c.SetLogy(logy)
        c.SetGridx(False)
        c.SetGridy(False)

        hFrame.Draw()
        gFakeB.Draw("p same")
        histograms.addStandardTexts(cmsTextPosition="outframe")

        # Create the legend & draw it
        l = ROOT.TLegend(0.65, 0.80, 0.90, 0.90)
        l.SetFillStyle(-1)
        l.SetBorderSize(0)
        l.AddEntry(gFakeB, "Value #pm stat.",
                   "LP")  # "Fake-#it{b} #pm Stat.", "LP"
        l.SetTextSize(0.035)
        if 0:
            l.Draw()

        # Store ROOT ignore level to normal before changing it
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning

        # Save the plot
        for item in ["png", "C", "pdf"]:
            c.Print(self._plotDirName +
                    "/FakeBNormalisationCoefficients.%s" % item)

        # Reset the ROOT ignore level to normal
        ROOT.gErrorIgnoreLevel = backup

        # Inform user
        msg = "Plot saved under %s" % (ShellStyles.SuccessStyle() +
                                       self._plotDirName + "/" +
                                       ShellStyles.NormalStyle())
        self.Print(msg, True)
        return

    def getFormattedBinLabelString(self, binLabel):
        '''
        Dirty trick to get what I want
        '''
        if binLabel not in self._BinLabelMap:
            raise Exception("Got unexpected bin label \"%s\"!" % binLabel)
        newLabel = self._BinLabelMap[binLabel]
        newLabel = newLabel.replace("abs(", "|")
        newLabel = newLabel.replace(")", "|")
        newLabel = newLabel.replace("..", "-")
        newLabel = newLabel.replace(":", ",")
        newLabel = newLabel.replace("TetrajetBJet", "")  #"b^{ldg} ")
        newLabel = newLabel.replace("Pt", "p_{T} ")
        newLabel = newLabel.replace("Eta", "#eta ")
        if "inclusive" in binLabel.lower():
            newLabel = "Inclusive"
        return newLabel

    def _generateDQMPlot(self):
        '''
        Create a Data Quality Monitor (DQM) style plot
        to easily check the error for each transfer factor
        and whether it is within an acceptable relative error
        '''
        # Define error warning/tolerance on relative errors
        okay = 1.0 / (len(self._BinLabelMap.keys()))
        warn = 0.5 * okay
        NEvts = []
        # Check the uncertainties on the normalization factors
        for k in self._BinLabelMap:
            relErrorUp = abs(self._TF_Up[k]) / (self._TF[k])
            relErrorDown = abs(self._TF_Down[k]) / (self._TF[k])
            relError = self._TF_Error[k] / self._TF[k]
            if 0:
                print "bin = %s , relErrorUp = %s, relErrorDown = %s " % (
                    k, relErrorUp, relErrorDown)

            # Add DQM entries
            NCR1 = 0
            NCR2 = 0
            NCR3 = 0
            NCR4 = 0
            for j in self._NEvtsCR1:
                NCR1 += self._NEvtsCR1[j]
                NEvts.append(self._NEvtsCR1[j])
            for j in self._NEvtsCR2:
                NCR2 += self._NEvtsCR2[j]
                NEvts.append(self._NEvtsCR2[j])
            for j in self._NEvtsCR3:
                NCR3 += self._NEvtsCR3[j]
                NEvts.append(self._NEvtsCR3[j])
            for j in self._NEvtsCR4:
                NCR4 += self._NEvtsCR4[j]
                NEvts.append(self._NEvtsCR4[j])

            if 0:
                print "NCR1[%s] = %0.1f, NCR2[%s] = %0.1f, k = %s" % (
                    k, self._NEvtsCR1[k], k, self._NEvtsCR2[k], k)
                print "NCR1 = %s, NCR2 = %s, k = %s" % (NCR1, NCR2, k)
                print "error/NCR1[%s] = %0.2f, error/NCR2[%s] = %0.2f" % (
                    k, self._NEvtsCR1_Error[k] / self._NEvtsCR1[k], k,
                    self._NEvtsCR2_Error[k] / self._NEvtsCR2[k])

            # Add DQM plot entries
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR1}",
                              self._NEvtsCR1[k], NCR1 * okay, NCR1 * warn)
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR2}",
                              self._NEvtsCR2[k], NCR2 * okay, NCR2 * warn)
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR3}",
                              self._NEvtsCR3[k], NCR3 * okay, NCR3 * warn)
            self._addDqmEntry(self._BinLabelMap[k], "N_{CR4}",
                              self._NEvtsCR4[k], NCR4 * okay, NCR4 * warn)
            # self._addDqmEntry(self._BinLabelMap[k], "#frac{#sigma_{CR1}}{N_{CR1}}", self._NEvtsCR1_Error[k]/NCR1, 0.05, 0.15)
            # self._addDqmEntry(self._BinLabelMap[k], "#frac{#sigma_{CR2}}{N_{CR2}}", self._NEvtsCR2_Error[k]/NCR2, 0.05, 0.15)

        # Construct the DQM histogram
        nBinsX = len(self._dqmKeys[self._dqmKeys.keys()[0]].keys())
        nBinsY = len(self._dqmKeys.keys())
        h = ROOT.TH2F("FakeB DQM", "FakeB DQM", nBinsX, 0, nBinsX, nBinsY, 0,
                      nBinsY)

        # Customise axes
        h.GetXaxis().SetLabelSize(15)
        h.GetYaxis().SetLabelSize(10)

        # Set Min and Max of z-axis
        if 0:  # red, yellow, green for DQM
            h.SetMinimum(0)
            h.SetMaximum(3)
        else:  # pure entries instead of red, yellow, green
            h.SetMinimum(min(NEvts) * 0.25)
            h.SetMaximum(round(max(NCR1, NCR2, NCR3, NCR4)))
            h.SetContour(10)
            #h.SetContour(3)
        if 0:
            h.GetXaxis().LabelsOption("v")
            h.GetYaxis().LabelsOption("v")

        nWarnings = 0
        nErrors = 0
        # For-loop: All x-axis bins
        for i in range(h.GetNbinsX()):
            # For-loop: All y-axis bins
            for j in range(h.GetNbinsY()):
                ykey = self._dqmKeys.keys()[j]
                xkey = self._dqmKeys[ykey].keys()[i]

                # Set the bin content
                h.SetBinContent(i + 1, j + 1, self._dqmKeys[ykey][xkey])
                h.GetXaxis().SetBinLabel(i + 1, xkey)
                h.GetYaxis().SetBinLabel(j + 1, ykey)
                if self._dqmKeys[ykey][xkey] > 2:
                    nErrors += 1
                elif self._dqmKeys[ykey][xkey] > 1:
                    nWarnings += 1

        # Apply TDR style
        style = tdrstyle.TDRStyle()
        style.setOptStat(False)
        style.setGridX(False)
        style.setGridY(False)
        style.setWide(True, 0.15)

        # Set the colour styling (red, yellow, green)
        if 0:
            palette = array.array("i",
                                  [ROOT.kGreen + 1, ROOT.kYellow, ROOT.kRed])
            ROOT.gStyle.SetPalette(3, palette)
        else:
            # https://root.cern.ch/doc/master/classTColor.html
            ROOT.gStyle.SetPalette(ROOT.kLightTemperature)
            # ROOT.gStyle.SetPalette(ROOT.kColorPrintableOnGrey)
            #tdrstyle.setRainBowPalette()
            #tdrstyle.setDeepSeaPalette()

        # Create canvas
        c = ROOT.TCanvas()
        c.SetLogx(False)
        c.SetLogy(False)
        c.SetLogz(True)
        c.SetGridx()
        c.SetGridy()
        h.Draw("COLZ")  #"COLZ TEXT"

        # Add CMS text and text with colour keys
        histograms.addStandardTexts(cmsTextPosition="outframe")
        if 0:
            histograms.addText(0.55,
                               0.80,
                               "green < %.0f %%" % (okay * 100),
                               size=20)
            histograms.addText(0.55,
                               0.84,
                               "yellow < %.0f %%" % (warn * 100),
                               size=20)
            histograms.addText(0.55,
                               0.88,
                               "red > %.0f %%" % (warn * 100),
                               size=20)

        # Save the canvas to a file
        backup = ROOT.gErrorIgnoreLevel
        ROOT.gErrorIgnoreLevel = ROOT.kWarning
        plotName = os.path.join(self._plotDirName, "FakeBNormalisationDQM")
        # For-loop: Save formats
        for ext in ["png", "C", "pdf"]:
            saveName = "%s.%s" % (plotName, ext)
            c.Print(saveName)
        ROOT.gErrorIgnoreLevel = backup
        ROOT.gStyle.SetPalette(1)

        msg = "Obtained %d warnings and %d errors for the normalisation" % (
            nWarnings, nErrors)
        self.Verbose(msg)
        if nWarnings > 0:
            msg = "DQM has %d warnings and %d errors! Please have a look at %s.png." % (
                nWarnings, nErrors, os.path.basename(plotName))
            self.Verbose(
                ShellStyles.ErrorStyle() + msg + ShellStyles.NormalStyle(),
                True)

        #if nWarnings > 0 or nErrors > 0:
        if nErrors > 0:
            msg = "DQM has %d warnings and %d errors! Please have a look at %s.png." % (
                nWarnings, nErrors, os.path.basename(plotName))
            self.Verbose(
                ShellStyles.ErrorStyle() + msg + ShellStyles.NormalStyle(),
                True)
        return

    def _addDqmEntry(self, binLabel, name, value, okTolerance, warnTolerance):
        # Define colour codes
        red = 2.5
        yellow = 1.5
        green = 0.5

        if not binLabel in self._dqmKeys.keys():
            self._dqmKeys[binLabel] = OrderedDict()
        result = red

        if abs(value) > okTolerance:
            result = green
        elif abs(value) > warnTolerance:
            result = yellow
        else:
            pass

        #self._dqmKeys[binLabel][name] = result
        self._dqmKeys[binLabel][name] = value  #iro
        return

    def _getSanityCheckTextForFractions(self,
                                        dataTemplate,
                                        binLabel,
                                        saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        
        NOTE: Should one divide the fractions with dataTemplate.getFittedParameters()[0] ? 
              Right now not because the correction is so small.
        '''
        self.Verbose("_getSanityCheckTextForFractions()", True)

        # Get variables
        label = "QCD"
        fraction = dataTemplate.getFittedParameters()[1]
        fractionError = dataTemplate.getFittedParameterErrors()[1]
        nBaseline = self._templates["%s_Baseline" %
                                    label].getNeventsFromHisto(False)
        nCalculated = fraction * dataTemplate.getNeventsFromHisto(False)

        if nCalculated > 0:
            ratio = nBaseline / nCalculated
        else:
            ratio = 0
        lines = []
        lines.append("Fitted %s fraction: %f +- %f" %
                     (label, fraction, fractionError))
        lines.append(
            "Sanity check: ratio = %.3f: baseline = %.1f vs. fitted = %.1f" %
            (ratio, nBaseline, nCalculated))

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines

    def _checkOverallNormalization(self,
                                   template,
                                   binLabel,
                                   saveToComments=False):
        '''
        Helper method to be called from parent class when calculating norm.coefficients
        '''
        self.Verbose("_checkOverallNormalization()")

        # Calculatotions
        value = template.getFittedParameters()[0]
        error = template.getFittedParameterErrors()[0]

        # Definitions
        lines = []
        lines.append(
            "The fitted overall normalization factor for purity is: (should be 1.0)"
        )
        lines.append("NormFactor = %f +/- %f" % (value, error))

        self._addDqmEntry(binLabel, "OverallNormalization(par0)", value - 1.0,
                          0.03, 0.10)

        # Store all information for later used (write to file)
        if saveToComments:
            self._commentLines.extend(lines)
        return lines

    ## Helper method to be called from parent class when calculating norm.coefficients
    def _getResultOutput(self, binLabel):
        lines = []
        lines.append("Transfer Factor (bin=%s): %f +- %f" %
                     (binLabel, self._TF[binLabel], self._TFError[binLabel]))
        lines.append("Normalisation (bin=%s)  : %f +- %f" %
                     (binLabel, self._FakeBNormalization[binLabel],
                      self._FakeBNormalizationError[binLabel]))

        # Store all information for later used (write to file)
        self._commentLines.extend(lines)
        return lines