File indexing completed on 2024-11-26 02:34:39
0001 from builtins import range
0002 import os
0003 import sys
0004 import math
0005 import copy
0006 import array
0007 import difflib
0008 import collections
0009
0010 import ROOT
0011 ROOT.gROOT.SetBatch(True)
0012 ROOT.PyConfig.IgnoreCommandLineOptions = True
0013
0014 from . import html
0015
0016 verbose=False
0017 _ratioYTitle = "Ratio"
0018
0019 def _setStyle():
0020 _absoluteSize = True
0021 if _absoluteSize:
0022 font = 43
0023 titleSize = 22
0024 labelSize = 22
0025 statSize = 14
0026 else:
0027 font = 42
0028 titleSize = 0.05
0029 labelSize = 0.05
0030 statSize = 0.025
0031
0032 ROOT.gROOT.SetStyle("Plain")
0033 ROOT.gStyle.SetPadRightMargin(0.07)
0034 ROOT.gStyle.SetPadLeftMargin(0.13)
0035 ROOT.gStyle.SetTitleFont(font, "XYZ")
0036 ROOT.gStyle.SetTitleSize(titleSize, "XYZ")
0037 ROOT.gStyle.SetTitleOffset(1.2, "Y")
0038
0039 ROOT.gStyle.SetLabelFont(font, "XYZ")
0040 ROOT.gStyle.SetLabelSize(labelSize, "XYZ")
0041 ROOT.gStyle.SetTextSize(labelSize)
0042 ROOT.gStyle.SetStatFont(font)
0043 ROOT.gStyle.SetStatFontSize(statSize)
0044
0045 ROOT.TGaxis.SetMaxDigits(4)
0046
0047 def _getObject(tdirectory, name):
0048 obj = tdirectory.Get(name)
0049 if not obj:
0050 if verbose:
0051 print("Did not find {obj} from {dir}".format(obj=name, dir=tdirectory.GetPath()))
0052 return None
0053 return obj
0054
0055 def _getOrCreateObject(tdirectory, nameOrCreator):
0056 if hasattr(nameOrCreator, "create"):
0057 return nameOrCreator.create(tdirectory)
0058 return _getObject(tdirectory, nameOrCreator)
0059
0060 class GetDirectoryCode:
0061 class FileNotExist: pass
0062 class PossibleDirsNotExist: pass
0063 class SubDirNotExist: pass
0064
0065 @staticmethod
0066 def codesToNone(code):
0067 if code in [GetDirectoryCode.FileNotExist, GetDirectoryCode.PossibleDirsNotExist, GetDirectoryCode.SubDirNotExist]:
0068 return None
0069 return code
0070
0071 def _getDirectoryDetailed(tfile, possibleDirs, subDir=None):
0072 """Get TDirectory from TFile."""
0073 if tfile is None:
0074 return GetDirectoryCode.FileNotExist
0075 for pdf in possibleDirs:
0076 d = tfile.Get(pdf)
0077 if d:
0078 if subDir is not None:
0079
0080 d = d.Get(subDir)
0081 if d:
0082 return d
0083 else:
0084 if verbose:
0085 print("Did not find subdirectory '%s' from directory '%s' in file %s" % (subDir, pdf, tfile.GetName()))
0086
0087
0088 return GetDirectoryCode.SubDirNotExist
0089 else:
0090 return d
0091 if verbose:
0092 print("Did not find any of directories '%s' from file %s" % (",".join(possibleDirs), tfile.GetName()))
0093 return GetDirectoryCode.PossibleDirsNotExist
0094
0095 def _getDirectory(*args, **kwargs):
0096 return GetDirectoryCode.codesToNone(_getDirectoryDetailed(*args, **kwargs))
0097
0098 def _th1ToOrderedDict(th1, renameBin=None):
0099 values = collections.OrderedDict()
0100 for i in range(1, th1.GetNbinsX()+1):
0101 binLabel = th1.GetXaxis().GetBinLabel(i)
0102 if renameBin is not None:
0103 binLabel = renameBin(binLabel)
0104 values[binLabel] = (th1.GetBinContent(i), th1.GetBinError(i))
0105 return values
0106
0107 def _createCanvas(name, width, height):
0108
0109 if not verbose:
0110 backup = ROOT.gErrorIgnoreLevel
0111 ROOT.gErrorIgnoreLevel = ROOT.kError
0112 canvas = ROOT.TCanvas(name, name, width, height)
0113 if not verbose:
0114 ROOT.gErrorIgnoreLevel = backup
0115 return canvas
0116
0117 def _modifyPadForRatio(pad, ratioFactor):
0118 pad.Divide(1, 2)
0119
0120 divisionPoint = 1-1/ratioFactor
0121
0122 topMargin = pad.GetTopMargin()
0123 bottomMargin = pad.GetBottomMargin()
0124 divisionPoint += (1-divisionPoint)*bottomMargin
0125 divisionPointForPad1 = 1-( (1-divisionPoint) / (1-0.02) )
0126
0127
0128 pad1 = pad.cd(1)
0129 yup = 1.0
0130 ylow = divisionPointForPad1
0131 xup = 1.0
0132 xlow = 0.0
0133 pad1.SetPad(xlow, ylow, xup, yup)
0134 pad1.SetFillStyle(4000)
0135 pad1.SetBottomMargin(0.02)
0136
0137
0138 pad2 = pad.cd(2)
0139 yup = divisionPoint
0140 ylow = 0.0
0141 pad2.SetPad(xlow, ylow, xup, yup)
0142 pad2.SetFillStyle(4000)
0143 pad2.SetTopMargin(0.0)
0144 pad2.SetBottomMargin(bottomMargin/(ratioFactor*divisionPoint))
0145
0146 def _calculateRatios(histos, ratioUncertainty=False):
0147 """Calculate the ratios for a list of histograms"""
0148
0149 def _divideOrZero(numerator, denominator):
0150 if denominator == 0:
0151 return 0
0152 return numerator/denominator
0153
0154 def equal(a, b):
0155 if a == 0. and b == 0.:
0156 return True
0157 return abs(a-b)/max(abs(a),abs(b)) < 1e-3
0158
0159 def findBins(wrap, bins_xvalues):
0160 ret = []
0161 currBin = wrap.begin()
0162 i = 0
0163 while i < len(bins_xvalues) and currBin < wrap.end():
0164 (xcenter, xlow, xhigh) = bins_xvalues[i]
0165 xlowEdge = xcenter-xlow
0166 xupEdge = xcenter+xhigh
0167
0168 (curr_center, curr_low, curr_high) = wrap.xvalues(currBin)
0169 curr_lowEdge = curr_center-curr_low
0170 curr_upEdge = curr_center+curr_high
0171
0172 if equal(xlowEdge, curr_lowEdge) and equal(xupEdge, curr_upEdge):
0173 ret.append(currBin)
0174 currBin += 1
0175 i += 1
0176 elif curr_upEdge <= xlowEdge:
0177 currBin += 1
0178 elif curr_lowEdge >= xupEdge:
0179 ret.append(None)
0180 i += 1
0181 else:
0182 ret.append(None)
0183 currBin += 1
0184 i += 1
0185 if len(ret) != len(bins_xvalues):
0186 ret.extend([None]*( len(bins_xvalues) - len(ret) ))
0187 return ret
0188
0189
0190
0191 class WrapTH1:
0192 def __init__(self, th1, uncertainty):
0193 self._th1 = th1
0194 self._uncertainty = uncertainty
0195
0196 xaxis = th1.GetXaxis()
0197 xaxis_arr = xaxis.GetXbins()
0198 if xaxis_arr.GetSize() > 0:
0199 lst = [xaxis_arr[i] for i in range(0, xaxis_arr.GetSize())]
0200 arr = array.array("d", lst)
0201 self._ratio = ROOT.TH1F("foo", "foo", xaxis.GetNbins(), arr)
0202 else:
0203 self._ratio = ROOT.TH1F("foo", "foo", xaxis.GetNbins(), xaxis.GetXmin(), xaxis.GetXmax())
0204 _copyStyle(th1, self._ratio)
0205 self._ratio.SetStats(0)
0206 self._ratio.SetLineColor(ROOT.kBlack)
0207 self._ratio.SetLineWidth(1)
0208 def draw(self, style=None):
0209 st = style
0210 if st is None:
0211 if self._uncertainty:
0212 st = "EP"
0213 else:
0214 st = "HIST P"
0215 self._ratio.Draw("same "+st)
0216 def begin(self):
0217 return 1
0218 def end(self):
0219 return self._th1.GetNbinsX()+1
0220 def xvalues(self, bin):
0221 xval = self._th1.GetBinCenter(bin)
0222 xlow = xval-self._th1.GetXaxis().GetBinLowEdge(bin)
0223 xhigh = self._th1.GetXaxis().GetBinUpEdge(bin)-xval
0224 return (xval, xlow, xhigh)
0225 def yvalues(self, bin):
0226 yval = self._th1.GetBinContent(bin)
0227 yerr = self._th1.GetBinError(bin)
0228 return (yval, yerr, yerr)
0229 def y(self, bin):
0230 return self._th1.GetBinContent(bin)
0231 def divide(self, bin, scale):
0232 self._ratio.SetBinContent(bin, _divideOrZero(self._th1.GetBinContent(bin), scale))
0233 self._ratio.SetBinError(bin, _divideOrZero(self._th1.GetBinError(bin), scale))
0234 def makeRatio(self):
0235 pass
0236 def getRatio(self):
0237 return self._ratio
0238
0239 class WrapTGraph:
0240 def __init__(self, gr, uncertainty):
0241 self._gr = gr
0242 self._uncertainty = uncertainty
0243 self._xvalues = []
0244 self._xerrslow = []
0245 self._xerrshigh = []
0246 self._yvalues = []
0247 self._yerrshigh = []
0248 self._yerrslow = []
0249 def draw(self, style=None):
0250 if self._ratio is None:
0251 return
0252 st = style
0253 if st is None:
0254 if self._uncertainty:
0255 st = "PZ"
0256 else:
0257 st = "PX"
0258 self._ratio.Draw("same "+st)
0259 def begin(self):
0260 return 0
0261 def end(self):
0262 return self._gr.GetN()
0263 def xvalues(self, bin):
0264 return (self._gr.GetX()[bin], self._gr.GetErrorXlow(bin), self._gr.GetErrorXhigh(bin))
0265 def yvalues(self, bin):
0266 return (self._gr.GetY()[bin], self._gr.GetErrorYlow(bin), self._gr.GetErrorYhigh(bin))
0267 def y(self, bin):
0268 return self._gr.GetY()[bin]
0269 def divide(self, bin, scale):
0270
0271 if scale == 0:
0272 return
0273
0274 if bin >= self._gr.GetN():
0275 return
0276
0277 xvals = self.xvalues(bin)
0278 xval = xvals[0]
0279
0280 self._xvalues.append(xval)
0281 self._xerrslow.append(xvals[1])
0282 self._xerrshigh.append(xvals[2])
0283 yvals = self.yvalues(bin)
0284 self._yvalues.append(yvals[0] / scale)
0285 if self._uncertainty:
0286 self._yerrslow.append(yvals[1] / scale)
0287 self._yerrshigh.append(yvals[2] / scale)
0288 else:
0289 self._yerrslow.append(0)
0290 self._yerrshigh.append(0)
0291 def makeRatio(self):
0292 if len(self._xvalues) == 0:
0293 self._ratio = None
0294 return
0295 self._ratio = ROOT.TGraphAsymmErrors(len(self._xvalues), array.array("d", self._xvalues), array.array("d", self._yvalues),
0296 array.array("d", self._xerrslow), array.array("d", self._xerrshigh),
0297 array.array("d", self._yerrslow), array.array("d", self._yerrshigh))
0298 _copyStyle(self._gr, self._ratio)
0299 def getRatio(self):
0300 return self._ratio
0301 class WrapTGraph2D(WrapTGraph):
0302 def __init__(self, gr, uncertainty):
0303 WrapTGraph.__init__(self, gr, uncertainty)
0304 def xvalues(self, bin):
0305 return (self._gr.GetX()[bin], self._gr.GetErrorX(bin), self._gr.GetErrorX(bin))
0306 def yvalues(self, bin):
0307 return (self._gr.GetY()[bin], self._gr.GetErrorY(bin), self._gr.GetErrorY(bin))
0308
0309 def wrap(o):
0310 if isinstance(o, ROOT.TH1) and not isinstance(o, ROOT.TH2):
0311 return WrapTH1(o, ratioUncertainty)
0312 elif isinstance(o, ROOT.TGraph):
0313 return WrapTGraph(o, ratioUncertainty)
0314 elif isinstance(o, ROOT.TGraph2D):
0315 return WrapTGraph2D(o, ratioUncertainty)
0316
0317 wrappers = [wrap(h) for h in histos if wrap(h) is not None]
0318 if len(wrappers) < 1:
0319 return []
0320 ref = wrappers[0]
0321
0322 wrappers_bins = []
0323 ref_bins = [ref.xvalues(b) for b in range(ref.begin(), ref.end())]
0324 for w in wrappers:
0325 wrappers_bins.append(findBins(w, ref_bins))
0326
0327 for i, bin in enumerate(range(ref.begin(), ref.end())):
0328 (scale, ylow, yhigh) = ref.yvalues(bin)
0329 for w, bins in zip(wrappers, wrappers_bins):
0330 if bins[i] is None:
0331 continue
0332 w.divide(bins[i], scale)
0333
0334 for w in wrappers:
0335 w.makeRatio()
0336
0337 return wrappers
0338
0339
0340 def _getXmin(obj, limitToNonZeroContent=False):
0341 if isinstance(obj, ROOT.TH1):
0342 xaxis = obj.GetXaxis()
0343 if limitToNonZeroContent:
0344 for i in range(1, obj.GetNbinsX()+1):
0345 if obj.GetBinContent(i) != 0:
0346 return xaxis.GetBinLowEdge(i)
0347
0348 return None
0349 else:
0350 return xaxis.GetBinLowEdge(xaxis.GetFirst())
0351 elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0352 m = min([obj.GetX()[i] for i in range(0, obj.GetN())])
0353 return m*0.9 if m > 0 else m*1.1
0354 raise Exception("Unsupported type %s" % str(obj))
0355
0356 def _getXmax(obj, limitToNonZeroContent=False):
0357 if isinstance(obj, ROOT.TH1):
0358 xaxis = obj.GetXaxis()
0359 if limitToNonZeroContent:
0360 for i in range(obj.GetNbinsX(), 0, -1):
0361 if obj.GetBinContent(i) != 0:
0362 return xaxis.GetBinUpEdge(i)
0363
0364 return None
0365 else:
0366 return xaxis.GetBinUpEdge(xaxis.GetLast())
0367 elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0368 m = max([obj.GetX()[i] for i in range(0, obj.GetN())])
0369 return m*1.1 if m > 0 else m*0.9
0370 raise Exception("Unsupported type %s" % str(obj))
0371
0372 def _getYmin(obj, limitToNonZeroContent=False):
0373 if isinstance(obj, ROOT.TH2):
0374 yaxis = obj.GetYaxis()
0375 return yaxis.GetBinLowEdge(yaxis.GetFirst())
0376 elif isinstance(obj, ROOT.TH1):
0377 if limitToNonZeroContent:
0378 lst = [obj.GetBinContent(i) for i in range(1, obj.GetNbinsX()+1) if obj.GetBinContent(i) != 0 ]
0379 return min(lst) if len(lst) != 0 else 0
0380 else:
0381 return obj.GetMinimum()
0382 elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0383 m = min([obj.GetY()[i] for i in range(0, obj.GetN())])
0384 return m*0.9 if m > 0 else m*1.1
0385 raise Exception("Unsupported type %s" % str(obj))
0386
0387 def _getYmax(obj, limitToNonZeroContent=False):
0388 if isinstance(obj, ROOT.TH2):
0389 yaxis = obj.GetYaxis()
0390 return yaxis.GetBinUpEdge(yaxis.GetLast())
0391 elif isinstance(obj, ROOT.TH1):
0392 if limitToNonZeroContent:
0393 lst = [obj.GetBinContent(i) for i in range(1, obj.GetNbinsX()+1) if obj.GetBinContent(i) != 0 ]
0394 return max(lst) if len(lst) != 0 else 0
0395 else:
0396 return obj.GetMaximum()
0397 elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0398 m = max([obj.GetY()[i] for i in range(0, obj.GetN())])
0399 return m*1.1 if m > 0 else m*0.9
0400 raise Exception("Unsupported type %s" % str(obj))
0401
0402 def _getYmaxWithError(th1):
0403 return max([th1.GetBinContent(i)+th1.GetBinError(i) for i in range(1, th1.GetNbinsX()+1)])
0404
0405 def _getYminIgnoreOutlier(th1):
0406 yvals = sorted([n for n in [th1.GetBinContent(i) for i in range(1, th1.GetNbinsX()+1)] if n>0])
0407 if len(yvals) == 0:
0408 return th1.GetMinimum()
0409 if len(yvals) == 1:
0410 return yvals[0]
0411
0412
0413 ind_min = len(yvals)-1 - int(len(yvals)*0.95)
0414 min_val = yvals[ind_min]
0415 for i in range(0, ind_min):
0416 if yvals[i] > 0.1*min_val:
0417 return yvals[i]
0418
0419 return min_val
0420
0421 def _getYminMaxAroundMedian(obj, coverage, coverageRange=None):
0422 inRange = lambda x: True
0423 inRange2 = lambda xmin,xmax: True
0424 if coverageRange:
0425 inRange = lambda x: coverageRange[0] <= x <= coverageRange[1]
0426 inRange2 = lambda xmin,xmax: coverageRange[0] <= xmin and xmax <= coverageRange[1]
0427
0428 if isinstance(obj, ROOT.TH1):
0429 yvals = [obj.GetBinContent(i) for i in range(1, obj.GetNbinsX()+1) if inRange2(obj.GetXaxis().GetBinLowEdge(i), obj.GetXaxis().GetBinUpEdge(i))]
0430 yvals = [x for x in yvals if x != 0]
0431 elif isinstance(obj, ROOT.TGraph) or isinstance(obj, ROOT.TGraph2D):
0432 yvals = [obj.GetY()[i] for i in range(0, obj.GetN()) if inRange(obj.GetX()[i])]
0433 else:
0434 raise Exception("Unsupported type %s" % str(obj))
0435 if len(yvals) == 0:
0436 return (0, 0)
0437 if len(yvals) == 1:
0438 return (yvals[0], yvals[0])
0439 if len(yvals) == 2:
0440 return (yvals[0], yvals[1])
0441
0442 yvals.sort()
0443 nvals = int(len(yvals)*coverage)
0444 if nvals < 2:
0445
0446 if len(yvals) % 2 == 0:
0447 half = len(yvals)//2
0448 return ( yvals[half-1], yvals[half] )
0449 else:
0450 middle = len(yvals)//2
0451 return ( yvals[middle-1], yvals[middle+1] )
0452 ind_min = (len(yvals)-nvals)//2
0453 ind_max = len(yvals)-1 - ind_min
0454
0455 return (yvals[ind_min], yvals[ind_max])
0456
0457 def _findBounds(th1s, ylog, xmin=None, xmax=None, ymin=None, ymax=None):
0458 """Find x-y axis boundaries encompassing a list of TH1s if the bounds are not given in arguments.
0459
0460 Arguments:
0461 th1s -- List of TH1s
0462 ylog -- Boolean indicating if y axis is in log scale or not (affects the automatic ymax)
0463
0464 Keyword arguments:
0465 xmin -- Minimum x value; if None, take the minimum of TH1s
0466 xmax -- Maximum x value; if None, take the maximum of TH1s
0467 ymin -- Minimum y value; if None, take the minimum of TH1s
0468 ymax -- Maximum y value; if None, take the maximum of TH1s
0469 """
0470
0471 (ymin, ymax) = _findBoundsY(th1s, ylog, ymin, ymax)
0472
0473 if xmin is None or xmax is None or isinstance(xmin, list) or isinstance(max, list):
0474 xmins = []
0475 xmaxs = []
0476 for th1 in th1s:
0477 xmins.append(_getXmin(th1, limitToNonZeroContent=isinstance(xmin, list)))
0478 xmaxs.append(_getXmax(th1, limitToNonZeroContent=isinstance(xmax, list)))
0479
0480
0481 xmins = [h for h in xmins if h is not None]
0482 xmaxs = [h for h in xmaxs if h is not None]
0483
0484 if xmin is None:
0485 xmin = min(xmins)
0486 elif isinstance(xmin, list):
0487 if len(xmins) == 0:
0488 xmin = min(xmin)
0489 if verbose:
0490 print("Histogram is zero, using the smallest given value for xmin from", str(xmin))
0491 else:
0492 xm = min(xmins)
0493 xmins_below = [x for x in xmin if x<=xm]
0494 if len(xmins_below) == 0:
0495 xmin = min(xmin)
0496 if xm < xmin:
0497 if verbose:
0498 print("Histogram minimum x %f is below all given xmin values %s, using the smallest one" % (xm, str(xmin)))
0499 else:
0500 xmin = max(xmins_below)
0501
0502 if xmax is None:
0503 xmax = max(xmaxs)
0504 elif isinstance(xmax, list):
0505 if len(xmaxs) == 0:
0506 xmax = max(xmax)
0507 if verbose:
0508 print("Histogram is zero, using the smallest given value for xmax from", str(xmin))
0509 else:
0510 xm = max(xmaxs)
0511 xmaxs_above = [x for x in xmax if x>xm]
0512 if len(xmaxs_above) == 0:
0513 xmax = max(xmax)
0514 if xm > xmax:
0515 if verbose:
0516 print("Histogram maximum x %f is above all given xmax values %s, using the maximum one" % (xm, str(xmax)))
0517 else:
0518 xmax = min(xmaxs_above)
0519
0520 for th1 in th1s:
0521 th1.GetXaxis().SetRangeUser(xmin, xmax)
0522
0523 return (xmin, ymin, xmax, ymax)
0524
0525 def _findBoundsY(th1s, ylog, ymin=None, ymax=None, coverage=None, coverageRange=None):
0526 """Find y axis boundaries encompassing a list of TH1s if the bounds are not given in arguments.
0527
0528 Arguments:
0529 th1s -- List of TH1s
0530 ylog -- Boolean indicating if y axis is in log scale or not (affects the automatic ymax)
0531
0532 Keyword arguments:
0533 ymin -- Minimum y value; if None, take the minimum of TH1s
0534 ymax -- Maximum y value; if None, take the maximum of TH1s
0535 coverage -- If set, use only values within the 'coverage' part around the median are used for min/max (useful for ratio)
0536 coverageRange -- If coverage and this are set, use only the x axis specified by an (xmin,xmax) pair for the coverage
0537 """
0538 if coverage is not None or isinstance(th1s[0], ROOT.TH2):
0539
0540
0541
0542
0543
0544 y_scale_max = lambda y: y
0545 y_scale_min = lambda y: y
0546 else:
0547 if ylog:
0548 y_scale_max = lambda y: y*1.5
0549 else:
0550 y_scale_max = lambda y: y*1.05
0551 y_scale_min = lambda y: y*0.9
0552
0553 if ymin is None or ymax is None or isinstance(ymin, list) or isinstance(ymax, list):
0554 ymins = []
0555 ymaxs = []
0556 for th1 in th1s:
0557 if coverage is not None:
0558 (_ymin, _ymax) = _getYminMaxAroundMedian(th1, coverage, coverageRange)
0559 else:
0560 if ylog and isinstance(ymin, list):
0561 _ymin = _getYminIgnoreOutlier(th1)
0562 else:
0563 _ymin = _getYmin(th1, limitToNonZeroContent=isinstance(ymin, list))
0564 _ymax = _getYmax(th1, limitToNonZeroContent=isinstance(ymax, list))
0565
0566
0567 ymins.append(_ymin)
0568 ymaxs.append(_ymax)
0569
0570 if ymin is None:
0571 ymin = min(ymins)
0572 elif isinstance(ymin, list):
0573 ym_unscaled = min(ymins)
0574 ym = y_scale_min(ym_unscaled)
0575 ymins_below = [y for y in ymin if y<=ym]
0576 if len(ymins_below) == 0:
0577 ymin = min(ymin)
0578 if ym_unscaled < ymin:
0579 if verbose:
0580 print("Histogram minimum y %f is below all given ymin values %s, using the smallest one" % (ym, str(ymin)))
0581 else:
0582 ymin = max(ymins_below)
0583
0584 if ymax is None:
0585
0586
0587 ymax = y_scale_max(max(ymaxs+[ymin]))
0588 elif isinstance(ymax, list):
0589 ym_unscaled = max(ymaxs)
0590 ym = y_scale_max(ym_unscaled)
0591 ymaxs_above = [y for y in ymax if y>ym]
0592 if len(ymaxs_above) == 0:
0593 ymax = max(ymax)
0594 if ym_unscaled > ymax:
0595 if verbose:
0596 print("Histogram maximum y %f is above all given ymax values %s, using the maximum one" % (ym_unscaled, str(ymax)))
0597 else:
0598 ymax = min(ymaxs_above)
0599
0600 for th1 in th1s:
0601 th1.GetYaxis().SetRangeUser(ymin, ymax)
0602
0603 return (ymin, ymax)
0604
0605 def _th1RemoveEmptyBins(histos, xbinlabels):
0606 binsToRemove = set()
0607 for b in range(1, histos[0].GetNbinsX()+1):
0608 binEmpty = True
0609 for h in histos:
0610 if h.GetBinContent(b) > 0:
0611 binEmpty = False
0612 break
0613 if binEmpty:
0614 binsToRemove.add(b)
0615
0616 if len(binsToRemove) > 0:
0617
0618 xbinlab_new = []
0619 for i in range(len(xbinlabels)):
0620 if (i+1) not in binsToRemove:
0621 xbinlab_new.append(xbinlabels[i])
0622 xbinlabels = xbinlab_new
0623
0624
0625 histos_new = []
0626 for h in histos:
0627 values = []
0628 for b in range(1, h.GetNbinsX()+1):
0629 if b not in binsToRemove:
0630 values.append( (h.GetXaxis().GetBinLabel(b), h.GetBinContent(b), h.GetBinError(b)) )
0631
0632 if len(values) > 0:
0633 h_new = h.Clone(h.GetName()+"_empty")
0634 h_new.SetBins(len(values), h.GetBinLowEdge(1), h.GetBinLowEdge(1)+len(values))
0635 for b, (l, v, e) in enumerate(values):
0636 h_new.GetXaxis().SetBinLabel(b+1, l)
0637 h_new.SetBinContent(b+1, v)
0638 h_new.SetBinError(b+1, e)
0639
0640 histos_new.append(h_new)
0641 histos = histos_new
0642
0643 return (histos, xbinlabels)
0644
0645 def _th2RemoveEmptyBins(histos, xbinlabels, ybinlabels):
0646 xbinsToRemove = set()
0647 ybinsToRemove = set()
0648 for ih, h in enumerate(histos):
0649 for bx in range(1, h.GetNbinsX()+1):
0650 binEmpty = True
0651 for by in range(1, h.GetNbinsY()+1):
0652 if h.GetBinContent(bx, by) > 0:
0653 binEmpty = False
0654 break
0655 if binEmpty:
0656 xbinsToRemove.add(bx)
0657 elif ih > 0:
0658 xbinsToRemove.discard(bx)
0659
0660 for by in range(1, h.GetNbinsY()+1):
0661 binEmpty = True
0662 for bx in range(1, h.GetNbinsX()+1):
0663 if h.GetBinContent(bx, by) > 0:
0664 binEmpty = False
0665 break
0666 if binEmpty:
0667 ybinsToRemove.add(by)
0668 elif ih > 0:
0669 ybinsToRemove.discard(by)
0670
0671 if len(xbinsToRemove) > 0 or len(ybinsToRemove) > 0:
0672 xbinlabels_new = []
0673 xbins = []
0674 for b in range(1, len(xbinlabels)+1):
0675 if b not in xbinsToRemove:
0676 xbinlabels_new.append(histos[0].GetXaxis().GetBinLabel(b))
0677 xbins.append(b)
0678 xbinlabels = xbinlabels_new
0679 ybinlabels_new = []
0680 ybins = []
0681 for b in range(1, len(ybinlabels)+1):
0682 if b not in ybinsToRemove:
0683 ybinlabels.append(histos[0].GetYaxis().GetBinLabel(b))
0684 ybins.append(b)
0685 ybinlabels = xbinlabels_new
0686
0687 histos_new = []
0688 if len(xbinlabels) == 0 or len(ybinlabels) == 0:
0689 return (histos_new, xbinlabels, ybinlabels)
0690 for h in histos:
0691 h_new = ROOT.TH2F(h.GetName()+"_empty", h.GetTitle(), len(xbinlabels),0,len(xbinlabels), len(ybinlabels),0,len(ybinlabels))
0692 for b, l in enumerate(xbinlabels):
0693 h_new.GetXaxis().SetBinLabel(b+1, l)
0694 for b, l in enumerate(ybinlabels):
0695 h_new.GetYaxis().SetBinLabel(b+1, l)
0696
0697 for ix, bx in enumerate(xbins):
0698 for iy, by in enumerate(ybins):
0699 h_new.SetBinContent(ix+1, iy+1, h.GetBinContent(bx, by))
0700 h_new.SetBinError(ix+1, iy+1, h.GetBinError(bx, by))
0701 histos_new.append(h_new)
0702 histos = histos_new
0703 return (histos, xbinlabels, ybinlabels)
0704
0705 def _mergeBinLabelsX(histos):
0706 return _mergeBinLabels([[h.GetXaxis().GetBinLabel(i) for i in range(1, h.GetNbinsX()+1)] for h in histos])
0707
0708 def _mergeBinLabelsY(histos):
0709 return _mergeBinLabels([[h.GetYaxis().GetBinLabel(i) for i in range(1, h.GetNbinsY()+1)] for h in histos])
0710
0711 def _mergeBinLabels(labelsAll):
0712 labels_merged = labelsAll[0]
0713 for labels in labelsAll[1:]:
0714 diff = difflib.unified_diff(labels_merged, labels, n=max(len(labels_merged), len(labels)))
0715 labels_merged = []
0716 operation = []
0717 for item in diff:
0718 if item[:2] == "@@":
0719 break
0720 for item in diff:
0721 operation.append(item[0])
0722 lab = item[1:]
0723 if lab in labels_merged:
0724
0725 ind = labels_merged.index(lab)
0726 if operation[ind] == "-" and operation[-1] == "+":
0727 labels_merged.remove(lab)
0728 del operation[ind]
0729 elif operation[ind] == "+" and operation[-1] == "-":
0730 del operation[-1]
0731 continue
0732 else:
0733 raise Exception("This should never happen")
0734 labels_merged.append(lab)
0735
0736
0737 if len(labels_merged) == 0:
0738 labels_merged = labels
0739
0740 return labels_merged
0741
0742 def _th1IncludeOnlyBins(histos, xbinlabels):
0743 histos_new = []
0744 for h in histos:
0745 h_new = h.Clone(h.GetName()+"_xbinlabels")
0746 h_new.SetBins(len(xbinlabels), h.GetBinLowEdge(1), h.GetBinLowEdge(1)+len(xbinlabels))
0747 for i, label in enumerate(xbinlabels):
0748 bin = h.GetXaxis().FindFixBin(label)
0749 if bin >= 0:
0750 h_new.SetBinContent(i+1, h.GetBinContent(bin))
0751 h_new.SetBinError(i+1, h.GetBinError(bin))
0752 else:
0753 h_new.SetBinContent(i+1, 0)
0754 h_new.SetBinError(i+1, 0)
0755 histos_new.append(h_new)
0756 return histos_new
0757
0758
0759 class Subtract:
0760 """Class for subtracting two histograms"""
0761 def __init__(self, name, nameA, nameB, title=""):
0762 """Constructor
0763
0764 Arguments:
0765 name -- String for name of the resulting histogram (A-B)
0766 nameA -- String for A histogram
0767 nameB -- String for B histogram
0768
0769 Keyword arguments:
0770 title -- String for a title of the resulting histogram (default "")
0771
0772 Uncertainties are calculated with the assumption that B is a
0773 subset of A, and the histograms contain event counts.
0774 """
0775 self._name = name
0776 self._nameA = nameA
0777 self._nameB = nameB
0778 self._title = title
0779
0780 def __str__(self):
0781 """String representation, returns the name"""
0782 return self._name
0783
0784 def create(self, tdirectory):
0785 """Create and return the fake+duplicate histogram from a TDirectory"""
0786 histoA = _getObject(tdirectory, self._nameA)
0787 histoB = _getObject(tdirectory, self._nameB)
0788
0789 if not histoA or not histoB:
0790 return None
0791
0792 ret = histoA.Clone(self._name)
0793 ret.SetTitle(self._title)
0794
0795
0796
0797
0798 ret.SetCanExtend(False)
0799
0800 for i in range(0, histoA.GetNbinsX()+2):
0801 val = histoA.GetBinContent(i)-histoB.GetBinContent(i)
0802 ret.SetBinContent(i, val)
0803 ret.SetBinError(i, math.sqrt(val))
0804
0805 return ret
0806
0807 class Transform:
0808 """Class to transform bin contents in an arbitrary way."""
0809 def __init__(self, name, histo, func, title=""):
0810 """Constructor.
0811
0812 Argument:
0813 name -- String for name of the resulting histogram
0814 histo -- String for a source histogram (needs to be cumulative)
0815 func -- Function to operate on the bin content
0816 """
0817 self._name = name
0818 self._histo = histo
0819 self._func = func
0820 self._title = title
0821
0822 def __str__(self):
0823 """String representation, returns the name"""
0824 return self._name
0825
0826 def create(self, tdirectory):
0827 """Create and return the transformed histogram from a TDirectory"""
0828 histo = _getOrCreateObject(tdirectory, self._histo)
0829 if not histo:
0830 return None
0831
0832 ret = histo.Clone(self._name)
0833 ret.SetTitle(self._title)
0834
0835
0836
0837
0838 ret.SetCanExtend(False)
0839
0840 for i in range(0, histo.GetNbinsX()+2):
0841 ret.SetBinContent(i, self._func(histo.GetBinContent(i)))
0842 return ret
0843
0844 class FakeDuplicate:
0845 """Class to calculate the fake+duplicate rate"""
0846 def __init__(self, name, assoc, dup, reco, title=""):
0847 """Constructor.
0848
0849 Arguments:
0850 name -- String for the name of the resulting efficiency histogram
0851 assoc -- String for the name of the "associated" histogram
0852 dup -- String for the name of the "duplicates" histogram
0853 reco -- String for the name of the "reco" (denominator) histogram
0854
0855 Keyword arguments:
0856 title -- String for a title of the resulting histogram (default "")
0857
0858 The result is calculated as 1 - (assoc - dup) / reco
0859 """
0860 self._name = name
0861 self._assoc = assoc
0862 self._dup = dup
0863 self._reco = reco
0864 self._title = title
0865
0866 def __str__(self):
0867 """String representation, returns the name"""
0868 return self._name
0869
0870 def create(self, tdirectory):
0871 """Create and return the fake+duplicate histogram from a TDirectory"""
0872
0873 hassoc = _getObject(tdirectory, self._assoc)
0874 hdup = _getObject(tdirectory, self._dup)
0875 hreco = _getObject(tdirectory, self._reco)
0876
0877
0878 if not hassoc or not hdup or not hreco:
0879 return None
0880
0881 hfakedup = hreco.Clone(self._name)
0882 hfakedup.SetTitle(self._title)
0883
0884 for i in range(1, hassoc.GetNbinsX()+1):
0885 numerVal = hassoc.GetBinContent(i) - hdup.GetBinContent(i)
0886 denomVal = hreco.GetBinContent(i)
0887
0888 fakedupVal = (1 - numerVal / denomVal) if denomVal != 0.0 else 0.0
0889 errVal = math.sqrt(fakedupVal*(1-fakedupVal)/denomVal) if (denomVal != 0.0 and fakedupVal <= 1) else 0.0
0890
0891 hfakedup.SetBinContent(i, fakedupVal)
0892 hfakedup.SetBinError(i, errVal)
0893
0894 return hfakedup
0895
0896 class CutEfficiency:
0897 """Class for making a cut efficiency histograms.
0898
0899 N after cut
0900 eff = -----------
0901 N total
0902 """
0903 def __init__(self, name, histo, title=""):
0904 """Constructor
0905
0906 Arguments:
0907 name -- String for name of the resulting histogram
0908 histo -- String for a source histogram (needs to be cumulative)
0909 """
0910 self._name = name
0911 self._histo = histo
0912 self._title = title
0913
0914 def __str__(self):
0915 """String representation, returns the name"""
0916 return self._name
0917
0918 def create(self, tdirectory):
0919 """Create and return the cut efficiency histogram from a TDirectory"""
0920 histo = _getOrCreateObject(tdirectory, self._histo)
0921 if not histo:
0922 return None
0923
0924
0925 ascending = histo.GetBinContent(0) < histo.GetBinContent(histo.GetNbinsX())
0926 if ascending:
0927 n_tot = histo.GetBinContent(histo.GetNbinsX())
0928 else:
0929 n_tot = histo.GetBinContent(0)
0930
0931 if n_tot == 0:
0932 return histo
0933
0934 ret = histo.Clone(self._name)
0935 ret.SetTitle(self._title)
0936
0937
0938 for i in range(1, histo.GetNbinsX()+1):
0939 n = histo.GetBinContent(i)
0940 val = n/n_tot
0941 errVal = math.sqrt(val*(1-val)/n_tot)
0942 ret.SetBinContent(i, val)
0943 ret.SetBinError(i, errVal)
0944 return ret
0945
0946 class AggregateBins:
0947 """Class to create a histogram by aggregating bins of another histogram to a bin of the resulting histogram."""
0948 def __init__(self, name, histoName, mapping, normalizeTo=None, scale=None, renameBin=None, ignoreMissingBins=False, minExistingBins=None, originalOrder=False, reorder=None):
0949 """Constructor.
0950
0951 Arguments:
0952 name -- String for the name of the resulting histogram
0953 histoName -- String for the name of the source histogram
0954 mapping -- Dictionary for mapping the bins (see below)
0955
0956 Keyword arguments:
0957 normalizeTo -- Optional string of a bin label in the source histogram. If given, all bins of the resulting histogram are divided by the value of this bin.
0958 scale -- Optional number for scaling the histogram (passed to ROOT.TH1.Scale())
0959 renameBin -- Optional function (string -> string) to rename the bins of the input histogram
0960 originalOrder -- Boolean for using the order of bins in the histogram (default False)
0961 reorder -- Optional function to reorder the bins
0962
0963 Mapping structure (mapping):
0964
0965 Dictionary (you probably want to use collections.OrderedDict)
0966 should be a mapping from the destination bin label to a list
0967 of source bin labels ("dst -> [src]").
0968 """
0969 self._name = name
0970 self._histoName = histoName
0971 self._mapping = mapping
0972 self._normalizeTo = normalizeTo
0973 self._scale = scale
0974 self._renameBin = renameBin
0975 self._ignoreMissingBins = ignoreMissingBins
0976 self._minExistingBins = minExistingBins
0977 self._originalOrder = originalOrder
0978 self._reorder = reorder
0979 if self._originalOrder and self._reorder is not None:
0980 raise Exception("reorder is not None and originalOrder is True, please set only one of them")
0981
0982 def __str__(self):
0983 """String representation, returns the name"""
0984 return self._name
0985
0986 def create(self, tdirectory):
0987 """Create and return the histogram from a TDirectory"""
0988 th1 = _getOrCreateObject(tdirectory, self._histoName)
0989 if th1 is None:
0990 return None
0991
0992 binLabels = [""]*len(self._mapping)
0993 binValues = [None]*len(self._mapping)
0994
0995
0996 values = _th1ToOrderedDict(th1, self._renameBin)
0997
0998 binIndexOrder = []
0999 for i, (key, labels) in enumerate(self._mapping.items()):
1000 sumTime = 0.
1001 sumErrorSq = 0.
1002 nsum = 0
1003 for l in labels:
1004 try:
1005 sumTime += values[l][0]
1006 sumErrorSq += values[l][1]**2
1007 nsum += 1
1008 except KeyError:
1009 pass
1010
1011 if nsum > 0:
1012 binValues[i] = (sumTime, math.sqrt(sumErrorSq))
1013 binLabels[i] = key
1014
1015 ivalue = len(values)+1
1016 if len(labels) > 0:
1017
1018
1019 for lab in labels:
1020 if lab in values:
1021 ivalue = list(values.keys()).index(lab)
1022 break
1023 binIndexOrder.append( (ivalue, i) )
1024
1025 if self._originalOrder:
1026 binIndexOrder.sort(key=lambda t: t[0])
1027 tmpVal = []
1028 tmpLab = []
1029 for i in range(0, len(binValues)):
1030 fromIndex = binIndexOrder[i][1]
1031 tmpVal.append(binValues[fromIndex])
1032 tmpLab.append(binLabels[fromIndex])
1033 binValues = tmpVal
1034 binLabels = tmpLab
1035 if self._reorder is not None:
1036 order = self._reorder(tdirectory, binLabels)
1037 binValues = [binValues[i] for i in order]
1038 binLabels = [binLabels[i] for i in order]
1039
1040 if self._minExistingBins is not None and (len(binValues)-binValues.count(None)) < self._minExistingBins:
1041 return None
1042
1043 if self._ignoreMissingBins:
1044 for i, val in enumerate(binValues):
1045 if val is None:
1046 binLabels[i] = None
1047 binValues = [v for v in binValues if v is not None]
1048 binLabels = [v for v in binLabels if v is not None]
1049 if len(binValues) == 0:
1050 return None
1051
1052 result = ROOT.TH1F(self._name, self._name, len(binValues), 0, len(binValues))
1053 for i, (value, label) in enumerate(zip(binValues, binLabels)):
1054 if value is not None:
1055 result.SetBinContent(i+1, value[0])
1056 result.SetBinError(i+1, value[1])
1057 result.GetXaxis().SetBinLabel(i+1, label)
1058
1059 if self._normalizeTo is not None:
1060 bin = th1.GetXaxis().FindBin(self._normalizeTo)
1061 if bin <= 0:
1062 print("Trying to normalize {name} to {binlabel}, which does not exist".format(name=self._name, binlabel=self._normalizeTo))
1063 sys.exit(1)
1064 value = th1.GetBinContent(bin)
1065 if value != 0:
1066 result.Scale(1/value)
1067
1068 if self._scale is not None:
1069 result.Scale(self._scale)
1070
1071 return result
1072
1073 class AggregateHistos:
1074 """Class to create a histogram by aggregating integrals of another histogram."""
1075 def __init__(self, name, mapping, normalizeTo=None):
1076 """Constructor.
1077
1078 Arguments:
1079 name -- String for the name of the resulting histogram
1080 mapping -- Dictionary for mapping the bin label to a histogram name
1081
1082 Keyword arguments:
1083 normalizeTo -- Optional string for a histogram. If given, all bins of the resulting histograqm are divided by the integral of this histogram.
1084 """
1085 self._name = name
1086 self._mapping = mapping
1087 self._normalizeTo = normalizeTo
1088
1089 def __str__(self):
1090 """String representation, returns the name"""
1091 return self._name
1092
1093 def create(self, tdirectory):
1094 """Create and return the histogram from a TDirectory"""
1095 result = []
1096 for key, histoName in self._mapping.items():
1097 th1 = _getObject(tdirectory, histoName)
1098 if th1 is None:
1099 continue
1100 result.append( (key, th1.Integral(0, th1.GetNbinsX()+1)) )
1101 if len(result) == 0:
1102 return None
1103
1104 res = ROOT.TH1F(self._name, self._name, len(result), 0, len(result))
1105
1106 for i, (name, count) in enumerate(result):
1107 res.SetBinContent(i+1, count)
1108 res.GetXaxis().SetBinLabel(i+1, name)
1109
1110 if self._normalizeTo is not None:
1111 th1 = _getObject(tdirectory, self._normalizeTo)
1112 if th1 is None:
1113 return None
1114 scale = th1.Integral(0, th1.GetNbinsX()+1)
1115 res.Scale(1/scale)
1116
1117 return res
1118
1119 class ROC:
1120 """Class to construct a ROC curve (e.g. efficiency vs. fake rate) from two histograms"""
1121 def __init__(self, name, xhistoName, yhistoName, zaxis=False):
1122 """Constructor.
1123
1124 Arguments:
1125 name -- String for the name of the resulting histogram
1126 xhistoName -- String for the name of the x-axis histogram (or another "creator" object)
1127 yhistoName -- String for the name of the y-axis histogram (or another "creator" object)
1128
1129 Keyword arguments:
1130 zaxis -- If set to True (default False), create a TGraph2D with z axis showing the cut value (recommended drawStyle 'pcolz')
1131 """
1132 self._name = name
1133 self._xhistoName = xhistoName
1134 self._yhistoName = yhistoName
1135 self._zaxis = zaxis
1136
1137 def __str__(self):
1138 """String representation, returns the name"""
1139 return self._name
1140
1141 def create(self, tdirectory):
1142 """Create and return the histogram from a TDirectory"""
1143 xhisto = _getOrCreateObject(tdirectory, self._xhistoName)
1144 yhisto = _getOrCreateObject(tdirectory, self._yhistoName);
1145 if xhisto is None or yhisto is None:
1146 return None
1147
1148 x = []
1149 xerrup = []
1150 xerrdown = []
1151 y = []
1152 yerrup = []
1153 yerrdown = []
1154 z = []
1155
1156 for i in range(1, xhisto.GetNbinsX()+1):
1157 x.append(xhisto.GetBinContent(i))
1158 xerrup.append(xhisto.GetBinError(i))
1159 xerrdown.append(xhisto.GetBinError(i))
1160
1161 y.append(yhisto.GetBinContent(i))
1162 yerrup.append(yhisto.GetBinError(i))
1163 yerrdown.append(yhisto.GetBinError(i))
1164
1165 z.append(xhisto.GetXaxis().GetBinUpEdge(i))
1166
1167
1168 if x.count(0.0) == len(x) or y.count(0.0) == len(y):
1169 return None
1170
1171 arr = lambda v: array.array("d", v)
1172 gr = None
1173 if self._zaxis:
1174 gr = ROOT.TGraph2D(len(x), arr(x), arr(y), arr(z))
1175 else:
1176 gr = ROOT.TGraphAsymmErrors(len(x), arr(x), arr(y), arr(xerrdown), arr(xerrup), arr(yerrdown), arr(yerrup))
1177 gr.SetTitle("")
1178 return gr
1179
1180
1181
1182 _plotStylesColor = [4, 2, ROOT.kBlack, ROOT.kOrange+7, ROOT.kMagenta-3, ROOT.kGreen+2]
1183 _plotStylesMarker = [21, 20, 22, 34, 33, 23]
1184
1185 def _drawFrame(pad, bounds, zmax=None, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ybinlabels=None, suffix=""):
1186 """Function to draw a frame
1187
1188 Arguments:
1189 pad -- TPad to where the frame is drawn
1190 bounds -- List or 4-tuple for (xmin, ymin, xmax, ymax)
1191
1192 Keyword arguments:
1193 zmax -- Maximum Z, needed for TH2 histograms
1194 xbinlabels -- Optional list of strings for x axis bin labels
1195 xbinlabelsize -- Optional number for the x axis bin label size
1196 xbinlabeloption -- Optional string for the x axis bin options (passed to ROOT.TH1.LabelsOption())
1197 suffix -- Optional string for a postfix of the frame name
1198 """
1199 if xbinlabels is None and ybinlabels is None:
1200 frame = pad.DrawFrame(*bounds)
1201 else:
1202
1203 nbins = len(xbinlabels)
1204 if ybinlabels is None:
1205 frame = ROOT.TH1F("hframe"+suffix, "", nbins, bounds[0], bounds[2])
1206 frame.SetMinimum(bounds[1])
1207 frame.SetMaximum(bounds[3])
1208 frame.GetYaxis().SetLimits(bounds[1], bounds[3])
1209 else:
1210 ybins = len(ybinlabels)
1211 frame = ROOT.TH2F("hframe"+suffix, "", nbins,bounds[0],bounds[2], ybins,bounds[1],bounds[3])
1212 frame.SetMaximum(zmax)
1213
1214 frame.SetBit(ROOT.TH1.kNoStats)
1215 frame.SetBit(ROOT.kCanDelete)
1216 frame.Draw("")
1217
1218 xaxis = frame.GetXaxis()
1219 for i in range(nbins):
1220 xaxis.SetBinLabel(i+1, xbinlabels[i])
1221 if xbinlabelsize is not None:
1222 xaxis.SetLabelSize(xbinlabelsize)
1223 if xbinlabeloption is not None:
1224 frame.LabelsOption(xbinlabeloption)
1225
1226 if ybinlabels is not None:
1227 yaxis = frame.GetYaxis()
1228 for i, lab in enumerate(ybinlabels):
1229 yaxis.SetBinLabel(i+1, lab)
1230 if xbinlabelsize is not None:
1231 yaxis.SetLabelSize(xbinlabelsize)
1232 if xbinlabeloption is not None:
1233 frame.LabelsOption(xbinlabeloption, "Y")
1234
1235 return frame
1236
1237 class Frame:
1238 """Class for creating and managing a frame for a simple, one-pad plot"""
1239 def __init__(self, pad, bounds, zmax, nrows, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ybinlabels=None):
1240 self._pad = pad
1241 self._frame = _drawFrame(pad, bounds, zmax, xbinlabels, xbinlabelsize, xbinlabeloption, ybinlabels)
1242
1243 yoffsetFactor = 1
1244 xoffsetFactor = 0
1245
1246 self._frame.GetYaxis().SetTitleOffset(self._frame.GetYaxis().GetTitleOffset()*yoffsetFactor)
1247 self._frame.GetXaxis().SetTitleOffset(self._frame.GetXaxis().GetTitleOffset()*xoffsetFactor)
1248
1249
1250 def setLogx(self, log):
1251 self._pad.SetLogx(log)
1252
1253 def setLogy(self, log):
1254 self._pad.SetLogy(log)
1255
1256 def setGridx(self, grid):
1257 self._pad.SetGridx(grid)
1258
1259 def setGridy(self, grid):
1260 self._pad.SetGridy(grid)
1261
1262 def adjustMarginLeft(self, adjust):
1263 self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1264
1265 self._pad.cd()
1266 self._frame.Draw("")
1267
1268 def adjustMarginRight(self, adjust):
1269 self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1270
1271 self._pad.cd()
1272 self._frame.Draw("")
1273
1274 def setTitle(self, title):
1275 self._frame.SetTitle(title)
1276
1277 def setXTitle(self, title):
1278 self._frame.GetXaxis().SetTitle(title)
1279
1280 def setXTitleSize(self, size):
1281 self._frame.GetXaxis().SetTitleSize(size)
1282
1283 def setXTitleOffset(self, offset):
1284 self._frame.GetXaxis().SetTitleOffset(offset)
1285
1286 def setXLabelSize(self, size):
1287 self._frame.GetXaxis().SetLabelSize(size)
1288
1289 def setYTitle(self, title):
1290 self._frame.GetYaxis().SetTitle(title)
1291
1292 def setYTitleSize(self, size):
1293 self._frame.GetYaxis().SetTitleSize(size)
1294
1295 def setYTitleOffset(self, offset):
1296 self._frame.GetYaxis().SetTitleOffset(offset)
1297
1298 def redrawAxis(self):
1299 self._pad.RedrawAxis()
1300
1301 class FrameRatio:
1302 """Class for creating and managing a frame for a ratio plot with two subpads"""
1303 def __init__(self, pad, bounds, zmax, ratioBounds, ratioFactor, nrows, xbinlabels=None, xbinlabelsize=None, xbinlabeloption=None, ratioYTitle=_ratioYTitle):
1304 self._parentPad = pad
1305 self._pad = pad.cd(1)
1306 if xbinlabels is not None:
1307 self._frame = _drawFrame(self._pad, bounds, zmax, [""]*len(xbinlabels))
1308 else:
1309 self._frame = _drawFrame(self._pad, bounds, zmax)
1310 self._padRatio = pad.cd(2)
1311 self._frameRatio = _drawFrame(self._padRatio, ratioBounds, zmax, xbinlabels, xbinlabelsize, xbinlabeloption)
1312
1313 self._frame.GetXaxis().SetLabelSize(0)
1314 self._frame.GetXaxis().SetTitleSize(0)
1315
1316 yoffsetFactor = ratioFactor
1317 xoffsetFactor = 0
1318
1319 self._frame.GetYaxis().SetTitleOffset(self._frameRatio.GetYaxis().GetTitleOffset()*yoffsetFactor)
1320 self._frameRatio.GetYaxis().SetLabelSize(int(self._frameRatio.GetYaxis().GetLabelSize()*0.8))
1321 self._frameRatio.GetYaxis().SetTitleOffset(self._frameRatio.GetYaxis().GetTitleOffset()*yoffsetFactor)
1322 self._frameRatio.GetXaxis().SetTitleOffset(self._frameRatio.GetXaxis().GetTitleOffset()*xoffsetFactor)
1323
1324 self._frameRatio.GetYaxis().SetNdivisions(4, 5, 0)
1325
1326 self._frameRatio.GetYaxis().SetTitle(ratioYTitle)
1327
1328 def setLogx(self, log):
1329 self._pad.SetLogx(log)
1330 self._padRatio.SetLogx(log)
1331
1332 def setLogy(self, log):
1333 self._pad.SetLogy(log)
1334
1335 def setGridx(self, grid):
1336 self._pad.SetGridx(grid)
1337 self._padRatio.SetGridx(grid)
1338
1339 def setGridy(self, grid):
1340 self._pad.SetGridy(grid)
1341 self._padRatio.SetGridy(grid)
1342
1343 def adjustMarginLeft(self, adjust):
1344 self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1345 self._padRatio.SetLeftMargin(self._padRatio.GetLeftMargin()+adjust)
1346
1347 self._pad.cd()
1348 self._frame.Draw("")
1349 self._padRatio.cd()
1350 self._frameRatio.Draw("")
1351
1352 def adjustMarginRight(self, adjust):
1353 self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1354 self._padRatio.SetRightMargin(self._padRatio.GetRightMargin()+adjust)
1355
1356 self._pad.cd()
1357 self._frame.Draw("")
1358 self._padRatio.cd()
1359 self._frameRatio.Draw("")
1360
1361 def setTitle(self, title):
1362 self._frame.SetTitle(title)
1363
1364 def setXTitle(self, title):
1365 self._frameRatio.GetXaxis().SetTitle(title)
1366
1367 def setXTitleSize(self, size):
1368 self._frameRatio.GetXaxis().SetTitleSize(size)
1369
1370 def setXTitleOffset(self, offset):
1371 self._frameRatio.GetXaxis().SetTitleOffset(offset)
1372
1373 def setXLabelSize(self, size):
1374 self._frameRatio.GetXaxis().SetLabelSize(size)
1375
1376 def setYTitle(self, title):
1377 self._frame.GetYaxis().SetTitle(title)
1378
1379 def setYTitleRatio(self, title):
1380 self._frameRatio.GetYaxis().SetTitle(title)
1381
1382 def setYTitleSize(self, size):
1383 self._frame.GetYaxis().SetTitleSize(size)
1384 self._frameRatio.GetYaxis().SetTitleSize(size)
1385
1386 def setYTitleOffset(self, offset):
1387 self._frame.GetYaxis().SetTitleOffset(offset)
1388 self._frameRatio.GetYaxis().SetTitleOffset(offset)
1389
1390 def redrawAxis(self):
1391 self._padRatio.RedrawAxis()
1392 self._pad.RedrawAxis()
1393
1394 self._parentPad.cd()
1395
1396
1397 xmin=0.065
1398 ymin=0.285
1399 xmax=0.128
1400 ymax=0.33
1401 self._coverPad = ROOT.TPad("coverpad", "coverpad", xmin, ymin, xmax, ymax)
1402 self._coverPad.SetBorderMode(0)
1403 self._coverPad.Draw()
1404
1405 self._pad.cd()
1406 self._pad.Pop()
1407
1408 class FrameTGraph2D:
1409 """Class for creating and managing a frame for a plot from TGraph2D"""
1410 def __init__(self, pad, bounds, histos, ratioOrig, ratioFactor):
1411 self._pad = pad
1412 if ratioOrig:
1413 self._pad = pad.cd(1)
1414
1415
1416
1417 (xlow, ylow, width, height) = (self._pad.GetXlowNDC(), self._pad.GetYlowNDC(), self._pad.GetWNDC(), self._pad.GetHNDC())
1418 xup = xlow+width
1419 yup = ylow+height
1420
1421 bottomMargin = self._pad.GetBottomMargin()
1422 bottomMarginNew = ROOT.gStyle.GetPadBottomMargin()
1423
1424 ylowNew = yup - (1-bottomMargin)/(1-bottomMarginNew) * (yup-ylow)
1425 topMarginNew = self._pad.GetTopMargin() * (yup-ylow)/(yup-ylowNew)
1426
1427 self._pad.SetPad(xlow, ylowNew, xup, yup)
1428 self._pad.SetTopMargin(topMarginNew)
1429 self._pad.SetBottomMargin(bottomMarginNew)
1430
1431 self._xtitleoffset = 1.8
1432 self._ytitleoffset = 2.3
1433
1434 self._firstHisto = histos[0]
1435
1436 def setLogx(self, log):
1437 pass
1438
1439 def setLogy(self, log):
1440 pass
1441
1442 def setGridx(self, grid):
1443 pass
1444
1445 def setGridy(self, grid):
1446 pass
1447
1448 def adjustMarginLeft(self, adjust):
1449 self._pad.SetLeftMargin(self._pad.GetLeftMargin()+adjust)
1450 self._pad.cd()
1451
1452 def adjustMarginRight(self, adjust):
1453 self._pad.SetRightMargin(self._pad.GetRightMargin()+adjust)
1454 self._pad.cd()
1455
1456 def setTitle(self, title):
1457 pass
1458
1459 def setXTitle(self, title):
1460 self._xtitle = title
1461
1462 def setXTitleSize(self, size):
1463 self._xtitlesize = size
1464
1465 def setXTitleOffset(self, size):
1466 self._xtitleoffset = size
1467
1468 def setXLabelSize(self, size):
1469 self._xlabelsize = size
1470
1471 def setYTitle(self, title):
1472 self._ytitle = title
1473
1474 def setYTitleSize(self, size):
1475 self._ytitlesize = size
1476
1477 def setYTitleOffset(self, offset):
1478 self._ytitleoffset = offset
1479
1480 def setZTitle(self, title):
1481 self._firstHisto.GetZaxis().SetTitle(title)
1482
1483 def setZTitleOffset(self, offset):
1484 self._firstHisto.GetZaxis().SetTitleOffset(offset)
1485
1486 def redrawAxis(self):
1487
1488 epsilon = 1e-7
1489 self._pad.SetPhi(epsilon)
1490 self._pad.SetTheta(90+epsilon)
1491
1492 self._firstHisto.GetXaxis().SetTitleOffset(self._xtitleoffset)
1493 self._firstHisto.GetYaxis().SetTitleOffset(self._ytitleoffset)
1494
1495 if hasattr(self, "_xtitle"):
1496 self._firstHisto.GetXaxis().SetTitle(self._xtitle)
1497 if hasattr(self, "_xtitlesize"):
1498 self._firstHisto.GetXaxis().SetTitleSize(self._xtitlesize)
1499 if hasattr(self, "_xlabelsize"):
1500 self._firstHisto.GetXaxis().SetLabelSize(self._labelsize)
1501 if hasattr(self, "_ytitle"):
1502 self._firstHisto.GetYaxis().SetTitle(self._ytitle)
1503 if hasattr(self, "_ytitlesize"):
1504 self._firstHisto.GetYaxis().SetTitleSize(self._ytitlesize)
1505 if hasattr(self, "_ytitleoffset"):
1506 self._firstHisto.GetYaxis().SetTitleOffset(self._ytitleoffset)
1507
1508 class PlotText:
1509 """Abstraction on top of TLatex"""
1510 def __init__(self, x, y, text, size=None, bold=True, align="left", color=ROOT.kBlack, font=None):
1511 """Constructor.
1512
1513 Arguments:
1514 x -- X coordinate of the text (in NDC)
1515 y -- Y coordinate of the text (in NDC)
1516 text -- String to draw
1517 size -- Size of text (None for the default value, taken from gStyle)
1518 bold -- Should the text be bold?
1519 align -- Alignment of text (left, center, right)
1520 color -- Color of the text
1521 font -- Specify font explicitly
1522 """
1523 self._x = x
1524 self._y = y
1525 self._text = text
1526
1527 self._l = ROOT.TLatex()
1528 self._l.SetNDC()
1529 if not bold:
1530 self._l.SetTextFont(self._l.GetTextFont()-20)
1531 if font is not None:
1532 self._l.SetTextFont(font)
1533 if size is not None:
1534 self._l.SetTextSize(size)
1535 if isinstance(align, str):
1536 if align.lower() == "left":
1537 self._l.SetTextAlign(11)
1538 elif align.lower() == "center":
1539 self._l.SetTextAlign(21)
1540 elif align.lower() == "right":
1541 self._l.SetTextAlign(31)
1542 else:
1543 raise Exception("Error: Invalid option '%s' for text alignment! Options are: 'left', 'center', 'right'."%align)
1544 else:
1545 self._l.SetTextAlign(align)
1546 self._l.SetTextColor(color)
1547
1548 def Draw(self, options=None):
1549 """Draw the text to the current TPad.
1550
1551 Arguments:
1552 options -- For interface compatibility, ignored
1553
1554 Provides interface compatible with ROOT's drawable objects.
1555 """
1556 self._l.DrawLatex(self._x, self._y, self._text)
1557
1558
1559 class PlotTextBox:
1560 """Class for drawing text and a background box."""
1561 def __init__(self, xmin, ymin, xmax, ymax, lineheight=0.04, fillColor=ROOT.kWhite, transparent=True, **kwargs):
1562 """Constructor
1563
1564 Arguments:
1565 xmin -- X min coordinate of the box (NDC)
1566 ymin -- Y min coordinate of the box (NDC) (if None, deduced automatically)
1567 xmax -- X max coordinate of the box (NDC)
1568 ymax -- Y max coordinate of the box (NDC)
1569 lineheight -- Line height
1570 fillColor -- Fill color of the box
1571 transparent -- Should the box be transparent? (in practive the TPave is not created)
1572
1573 Keyword arguments are forwarded to constructor of PlotText
1574 """
1575
1576 self._xmin = xmin
1577 self._xmax = xmax
1578 self._ymin = ymin
1579 self._ymax = ymax
1580 self._lineheight = lineheight
1581 self._fillColor = fillColor
1582 self._transparent = transparent
1583 self._texts = []
1584 self._textArgs = {}
1585 self._textArgs.update(kwargs)
1586
1587 self._currenty = ymax
1588
1589 def addText(self, text):
1590 """Add text to current position"""
1591 self._currenty -= self._lineheight
1592 self._texts.append(PlotText(self._xmin+0.01, self._currenty, text, **self._textArgs))
1593
1594 def width(self):
1595 return self._xmax-self._xmin
1596
1597 def move(self, dx=0, dy=0, dw=0, dh=0):
1598 """Move the box and the contained text objects
1599
1600 Arguments:
1601 dx -- Movement in x (positive is to right)
1602 dy -- Movement in y (positive is to up)
1603 dw -- Increment of width (negative to decrease width)
1604 dh -- Increment of height (negative to decrease height)
1605
1606 dx and dy affect to both box and text objects, dw and dh
1607 affect the box only.
1608 """
1609 self._xmin += dx
1610 self._xmax += dx
1611 if self._ymin is not None:
1612 self._ymin += dy
1613 self._ymax += dy
1614
1615 self._xmax += dw
1616 if self._ymin is not None:
1617 self._ymin -= dh
1618
1619 for t in self._texts:
1620 t._x += dx
1621 t._y += dy
1622
1623 def Draw(self, options=""):
1624 """Draw the box and the text to the current TPad.
1625
1626 Arguments:
1627 options -- Forwarded to ROOT.TPave.Draw(), and the Draw() of the contained objects
1628 """
1629 if not self._transparent:
1630 ymin = self.ymin
1631 if ymin is None:
1632 ymin = self.currenty - 0.01
1633 self._pave = ROOT.TPave(self.xmin, self.ymin, self.xmax, self.ymax, 0, "NDC")
1634 self._pave.SetFillColor(self.fillColor)
1635 self._pave.Draw(options)
1636 for t in self._texts:
1637 t.Draw(options)
1638
1639 def _copyStyle(src, dst):
1640 properties = []
1641 if hasattr(src, "GetLineColor") and hasattr(dst, "SetLineColor"):
1642 properties.extend(["LineColor", "LineStyle", "LineWidth"])
1643 if hasattr(src, "GetFillColor") and hasattr(dst, "SetFillColor"):
1644 properties.extend(["FillColor", "FillStyle"])
1645 if hasattr(src, "GetMarkerColor") and hasattr(dst, "SetMarkerColor"):
1646 properties.extend(["MarkerColor", "MarkerSize", "MarkerStyle"])
1647
1648 for prop in properties:
1649 getattr(dst, "Set"+prop)(getattr(src, "Get"+prop)())
1650
1651 class PlotEmpty:
1652 """Denotes an empty place in a group."""
1653 def __init__(self):
1654 pass
1655
1656 def getName(self):
1657 return None
1658
1659 def drawRatioUncertainty(self):
1660 return False
1661
1662 def create(self, *args, **kwargs):
1663 pass
1664
1665 def isEmpty(self):
1666 return True
1667
1668 def getNumberOfHistograms(self):
1669 return 0
1670
1671 class Plot:
1672 """Represents one plot, comparing one or more histograms."""
1673 def __init__(self, name, **kwargs):
1674 """ Constructor.
1675
1676 Arguments:
1677 name -- String for name of the plot, or Efficiency object
1678
1679 Keyword arguments:
1680 fallback -- Dictionary for specifying fallback (default None)
1681 outname -- String for an output name of the plot (default None for the same as 'name')
1682 title -- String for a title of the plot (default None)
1683 xtitle -- String for x axis title (default None)
1684 xtitlesize -- Float for x axis title size (default None)
1685 xtitleoffset -- Float for x axis title offset (default None)
1686 xlabelsize -- Float for x axis label size (default None)
1687 ytitle -- String for y axis title (default None)
1688 ytitlesize -- Float for y axis title size (default None)
1689 ytitleoffset -- Float for y axis title offset (default None)
1690 ztitle -- String for z axis title (default None)
1691 ztitleoffset -- Float for z axis title offset (default None)
1692 xmin -- Float for x axis minimum (default None, i.e. automatic)
1693 xmax -- Float for x axis maximum (default None, i.e. automatic)
1694 ymin -- Float for y axis minimum (default 0)
1695 ymax -- Float for y axis maximum (default None, i.e. automatic)
1696 xlog -- Bool for x axis log status (default False)
1697 ylog -- Bool for y axis log status (default False)
1698 xgrid -- Bool for x axis grid status (default True)
1699 ygrid -- Bool for y axis grid status (default True)
1700 stat -- Draw stat box? (default False)
1701 fit -- Do gaussian fit? (default False)
1702 statx -- Stat box x coordinate (default 0.65)
1703 staty -- Stat box y coordinate (default 0.8)
1704 statyadjust -- List of floats for stat box y coordinate adjustments (default None)
1705 normalizeToUnitArea -- Normalize histograms to unit area? (default False)
1706 normalizeToNumberOfEvents -- Normalize histograms to number of events? If yes, the PlotFolder needs 'numberOfEventsHistogram' set to a histogram filled once per event (default False)
1707 profileX -- Take histograms via ProfileX()? (default False)
1708 fitSlicesY -- Take histograms via FitSlicesY() (default False)
1709 rebinX -- rebin x axis (default None)
1710 scale -- Scale histograms by a number (default None)
1711 xbinlabels -- List of x axis bin labels (if given, default None)
1712 xbinlabelsize -- Size of x axis bin labels (default None)
1713 xbinlabeloption -- Option string for x axis bin labels (default None)
1714 removeEmptyBins -- Bool for removing empty bins, but only if histogram has bin labels (default False)
1715 printBins -- Bool for printing bin values, but only if histogram has bin labels (default False)
1716 drawStyle -- If "hist", draw as line instead of points (default None)
1717 drawCommand -- Deliver this to Draw() (default: None for same as drawStyle)
1718 lineWidth -- If drawStyle=="hist", the width of line (default 2)
1719 legendDx -- Float for moving TLegend in x direction for separate=True (default None)
1720 legendDy -- Float for moving TLegend in y direction for separate=True (default None)
1721 legendDw -- Float for changing TLegend width for separate=True (default None)
1722 legendDh -- Float for changing TLegend height for separate=True (default None)
1723 legend -- Bool to enable/disable legend (default True)
1724 adjustMarginLeft -- Float for adjusting left margin (default None)
1725 adjustMarginRight -- Float for adjusting right margin (default None)
1726 ratio -- Possibility to disable ratio for this particular plot (default None)
1727 ratioYmin -- Float for y axis minimum in ratio pad (default: list of values)
1728 ratioYmax -- Float for y axis maximum in ratio pad (default: list of values)
1729 ratioFit -- Fit straight line in ratio? (default None)
1730 ratioUncertainty -- Plot uncertainties on ratio? (default True)
1731 ratioCoverageXrange -- Range of x axis values (xmin,xmax) to limit the automatic ratio y axis range calculation to (default None for disabled)
1732 histogramModifier -- Function to be called in create() to modify the histograms (default None)
1733 """
1734 self._name = name
1735
1736 def _set(attr, default):
1737 setattr(self, "_"+attr, kwargs.get(attr, default))
1738
1739 _set("fallback", None)
1740 _set("outname", None)
1741
1742 _set("title", None)
1743 _set("xtitle", None)
1744 _set("xtitlesize", None)
1745 _set("xtitleoffset", None)
1746 _set("xlabelsize", None)
1747 _set("ytitle", None)
1748 _set("ytitlesize", None)
1749 _set("ytitleoffset", None)
1750 _set("ztitle", None)
1751 _set("ztitleoffset", None)
1752
1753 _set("xmin", None)
1754 _set("xmax", None)
1755 _set("ymin", 0.)
1756 _set("ymax", None)
1757
1758 _set("xlog", False)
1759 _set("ylog", False)
1760 _set("xgrid", True)
1761 _set("ygrid", True)
1762
1763 _set("stat", False)
1764 _set("fit", False)
1765
1766 _set("statx", 0.65)
1767 _set("staty", 0.8)
1768 _set("statyadjust", None)
1769
1770 _set("normalizeToUnitArea", False)
1771 _set("normalizeToNumberOfEvents", False)
1772 _set("profileX", False)
1773 _set("fitSlicesY", False)
1774 _set("rebinX", None)
1775
1776 _set("scale", None)
1777 _set("xbinlabels", None)
1778 _set("xbinlabelsize", None)
1779 _set("xbinlabeloption", None)
1780 _set("removeEmptyBins", False)
1781 _set("printBins", False)
1782
1783 _set("drawStyle", "EP")
1784 _set("drawCommand", None)
1785 _set("lineWidth", 2)
1786
1787 _set("legendDx", None)
1788 _set("legendDy", None)
1789 _set("legendDw", None)
1790 _set("legendDh", None)
1791 _set("legend", True)
1792
1793 _set("adjustMarginLeft", None)
1794 _set("adjustMarginRight", None)
1795
1796 _set("ratio", None)
1797 _set("ratioYmin", [0, 0.2, 0.5, 0.7, 0.8, 0.9, 0.95])
1798 _set("ratioYmax", [1.05, 1.1, 1.2, 1.3, 1.5, 1.8, 2, 2.5, 3, 4, 5])
1799 _set("ratioFit", None)
1800 _set("ratioUncertainty", True)
1801 _set("ratioCoverageXrange", None)
1802
1803 _set("histogramModifier", None)
1804
1805 self._histograms = []
1806
1807 def setProperties(self, **kwargs):
1808 for name, value in kwargs.items():
1809 if not hasattr(self, "_"+name):
1810 raise Exception("No attribute '%s'" % name)
1811 setattr(self, "_"+name, value)
1812
1813 def clone(self, **kwargs):
1814 if not self.isEmpty():
1815 raise Exception("Plot can be cloned only before histograms have been created")
1816 cl = copy.copy(self)
1817 cl.setProperties(**kwargs)
1818 return cl
1819
1820 def getNumberOfHistograms(self):
1821 """Return number of existing histograms."""
1822 return len([h for h in self._histograms if h is not None])
1823
1824 def isEmpty(self):
1825 """Return true if there are no histograms created for the plot"""
1826 return self.getNumberOfHistograms() == 0
1827
1828 def isTGraph2D(self):
1829 for h in self._histograms:
1830 if isinstance(h, ROOT.TGraph2D):
1831 return True
1832 return False
1833
1834 def isRatio(self, ratio):
1835 if self._ratio is None:
1836 return ratio
1837 return ratio and self._ratio
1838
1839 def setName(self, name):
1840 self._name = name
1841
1842 def getName(self):
1843 if self._outname is not None:
1844 return self._outname
1845 if isinstance(self._name, list):
1846 return str(self._name[0])
1847 else:
1848 return str(self._name)
1849
1850 def drawRatioUncertainty(self):
1851 """Return true if the ratio uncertainty should be drawn"""
1852 return self._ratioUncertainty
1853
1854 def _createOne(self, name, index, tdir, nevents):
1855 """Create one histogram from a TDirectory."""
1856 if tdir == None:
1857 return None
1858
1859
1860 if isinstance(name, list):
1861 name = name[index]
1862
1863 h = _getOrCreateObject(tdir, name)
1864 if h is not None and self._normalizeToNumberOfEvents and nevents is not None and nevents != 0:
1865 h.Scale(1.0/nevents)
1866 return h
1867
1868 def create(self, tdirNEvents, requireAllHistograms=False):
1869 """Create histograms from list of TDirectories"""
1870 self._histograms = [self._createOne(self._name, i, tdirNEvent[0], tdirNEvent[1]) for i, tdirNEvent in enumerate(tdirNEvents)]
1871
1872 if self._fallback is not None:
1873 profileX = [self._profileX]*len(self._histograms)
1874 for i in range(0, len(self._histograms)):
1875 if self._histograms[i] is None:
1876 self._histograms[i] = self._createOne(self._fallback["name"], i, tdirNEvents[i][0], tdirNEvents[i][1])
1877 profileX[i] = self._fallback.get("profileX", self._profileX)
1878
1879 if self._histogramModifier is not None:
1880 self._histograms = self._histogramModifier(self._histograms)
1881
1882 if len(self._histograms) > len(_plotStylesColor):
1883 raise Exception("More histograms (%d) than there are plot styles (%d) defined. Please define more plot styles in this file" % (len(self._histograms), len(_plotStylesColor)))
1884
1885
1886
1887
1888 def _modifyHisto(th1, profileX):
1889 if th1 is None:
1890 return None
1891
1892 if profileX:
1893 th1 = th1.ProfileX()
1894
1895 if self._fitSlicesY:
1896 ROOT.TH1.AddDirectory(True)
1897 th1.FitSlicesY()
1898 th1 = ROOT.gDirectory.Get(th1.GetName()+"_2")
1899 th1.SetDirectory(None)
1900
1901 ROOT.TH1.AddDirectory(False)
1902
1903 if self._title is not None:
1904 th1.SetTitle(self._title)
1905
1906 if self._scale is not None:
1907 th1.Scale(self._scale)
1908
1909 return th1
1910
1911 if self._fallback is not None:
1912 self._histograms = list(map(_modifyHisto, self._histograms, profileX))
1913 else:
1914 self._histograms = list(map(lambda h: _modifyHisto(h, self._profileX), self._histograms))
1915 if requireAllHistograms and None in self._histograms:
1916 self._histograms = [None]*len(self._histograms)
1917
1918 def _setStats(self, histos, startingX, startingY):
1919 """Set stats box."""
1920 if not self._stat:
1921 for h in histos:
1922 if h is not None and hasattr(h, "SetStats"):
1923 h.SetStats(0)
1924 return
1925
1926 def _doStats(h, col, dy):
1927 if h is None:
1928 return
1929 h.SetStats(True)
1930
1931 if self._fit and h.GetEntries() > 0.5:
1932 h.Fit("gaus", "Q")
1933 f = h.GetListOfFunctions().FindObject("gaus")
1934 if f == None:
1935 h.SetStats(0)
1936 return
1937 f.SetLineColor(col)
1938 f.SetLineWidth(1)
1939 h.Draw()
1940 ROOT.gPad.Update()
1941 st = h.GetListOfFunctions().FindObject("stats")
1942 if self._fit:
1943 st.SetOptFit(0o010)
1944 st.SetOptStat(1001)
1945 st.SetOptStat(1110)
1946 st.SetX1NDC(startingX)
1947 st.SetX2NDC(startingX+0.3)
1948 st.SetY1NDC(startingY+dy)
1949 st.SetY2NDC(startingY+dy+0.12)
1950 st.SetTextColor(col)
1951
1952 dy = 0.0
1953 for i, h in enumerate(histos):
1954 if self._statyadjust is not None and i < len(self._statyadjust):
1955 dy += self._statyadjust[i]
1956
1957 _doStats(h, _plotStylesColor[i], dy)
1958 dy -= 0.16
1959
1960 def _normalize(self):
1961 """Normalise histograms to unit area"""
1962
1963 for h in self._histograms:
1964 if h is None:
1965 continue
1966 i = h.Integral()
1967 if i == 0:
1968 continue
1969 if h.GetSumw2().fN <= 0:
1970 h.Sumw2()
1971 h.Scale(1.0/i)
1972
1973 def draw(self, pad, ratio, ratioFactor, nrows):
1974 """Draw the histograms using values for a given algorithm."""
1975
1976
1977
1978
1979 isTGraph2D = self.isTGraph2D()
1980 if isTGraph2D:
1981
1982 ratioOrig = ratio
1983 ratio = False
1984
1985 if self._normalizeToUnitArea:
1986 self._normalize()
1987
1988 if self._rebinX is not None:
1989 for h in self._histograms:
1990 h.Rebin(self._rebinX)
1991
1992 def _styleMarker(h, msty, col):
1993 h.SetMarkerStyle(msty)
1994 h.SetMarkerColor(col)
1995 h.SetMarkerSize(0.7)
1996 h.SetLineColor(col)
1997 h.SetLineWidth(1)
1998
1999 def _styleHist(h, msty, col):
2000 _styleMarker(h, msty, col)
2001 h.SetLineColor(col)
2002 h.SetLineWidth(self._lineWidth)
2003
2004
2005 style = _styleMarker
2006 if "hist" in self._drawStyle.lower():
2007 style = _styleHist
2008 if len(self._histograms) > 0 and isinstance(self._histograms[0], ROOT.TGraph):
2009 if "l" in self._drawStyle.lower():
2010 style = _styleHist
2011
2012
2013 histos = []
2014 for i, h in enumerate(self._histograms):
2015 if h is None:
2016 continue
2017 style(h, _plotStylesMarker[i], _plotStylesColor[i])
2018 histos.append(h)
2019 if len(histos) == 0:
2020 if verbose:
2021 print("No histograms for plot {name}".format(name=self.getName()))
2022 return
2023
2024
2025
2026 histosHaveBinLabels = len(histos[0].GetXaxis().GetBinLabel(1)) > 0
2027 xbinlabels = self._xbinlabels
2028 ybinlabels = None
2029 if xbinlabels is None:
2030 if histosHaveBinLabels:
2031 xbinlabels = _mergeBinLabelsX(histos)
2032 if isinstance(histos[0], ROOT.TH2):
2033 ybinlabels = _mergeBinLabelsY(histos)
2034
2035 if len(histos) > 1:
2036
2037 histos = _th1IncludeOnlyBins(histos, xbinlabels)
2038 self._tmp_histos = histos
2039
2040
2041 if self._removeEmptyBins and histosHaveBinLabels:
2042
2043
2044 if isinstance(histos[0], ROOT.TH2):
2045 (histos, xbinlabels, ybinlabels) = _th2RemoveEmptyBins(histos, xbinlabels, ybinlabels)
2046 else:
2047 (histos, xbinlabels) = _th1RemoveEmptyBins(histos, xbinlabels)
2048 self._tmp_histos = histos
2049 if len(histos) == 0:
2050 if verbose:
2051 print("No histograms with non-empty bins for plot {name}".format(name=self.getName()))
2052 return
2053
2054 if self._printBins and histosHaveBinLabels:
2055 print("####################")
2056 print(self._name)
2057 width = max([len(l) for l in xbinlabels])
2058 tmp = "%%-%ds " % width
2059 for b in range(1, histos[0].GetNbinsX()+1):
2060 s = tmp % xbinlabels[b-1]
2061 for h in histos:
2062 s += "%.3f " % h.GetBinContent(b)
2063 print(s)
2064 print()
2065
2066 bounds = _findBounds(histos, self._ylog,
2067 xmin=self._xmin, xmax=self._xmax,
2068 ymin=self._ymin, ymax=self._ymax)
2069 zmax = None
2070 if isinstance(histos[0], ROOT.TH2):
2071 zmax = max([h.GetMaximum() for h in histos])
2072
2073
2074 self._mainAdditional = []
2075 self._ratioAdditional = []
2076
2077 if ratio:
2078 self._ratios = _calculateRatios(histos, self._ratioUncertainty)
2079 ratioHistos = [h for h in [r.getRatio() for r in self._ratios[1:]] if h is not None]
2080
2081 if len(ratioHistos) > 0:
2082 ratioBoundsY = _findBoundsY(ratioHistos, ylog=False, ymin=self._ratioYmin, ymax=self._ratioYmax, coverage=0.68, coverageRange=self._ratioCoverageXrange)
2083 else:
2084 ratioBoundsY = (0.9, 1,1)
2085
2086 if self._ratioFit is not None:
2087 for i, rh in enumerate(ratioHistos):
2088 tf_line = ROOT.TF1("line%d"%i, "[0]+x*[1]")
2089 tf_line.SetRange(self._ratioFit["rangemin"], self._ratioFit["rangemax"])
2090 fitres = rh.Fit(tf_line, "RINSQ")
2091 tf_line.SetLineColor(rh.GetMarkerColor())
2092 tf_line.SetLineWidth(2)
2093 self._ratioAdditional.append(tf_line)
2094 box = PlotTextBox(xmin=self._ratioFit.get("boxXmin", 0.14), ymin=None,
2095 xmax=self._ratioFit.get("boxXmax", 0.35), ymax=self._ratioFit.get("boxYmax", 0.09),
2096 color=rh.GetMarkerColor(), font=43, size=11, lineheight=0.02)
2097 box.move(dx=(box.width()+0.01)*i)
2098
2099
2100 box.addText("Const: %.4f#pm%.4f" % (fitres.Parameter(0), fitres.ParError(0)))
2101 box.addText("Slope: %.4f#pm%.4f" % (fitres.Parameter(1), fitres.ParError(1)))
2102 self._mainAdditional.append(box)
2103
2104
2105
2106
2107
2108
2109
2110 if ratio:
2111 pad.cd(1)
2112 self._setStats(histos, self._statx, self._staty)
2113
2114
2115 if isTGraph2D:
2116 frame = FrameTGraph2D(pad, bounds, histos, ratioOrig, ratioFactor)
2117 else:
2118 if ratio:
2119 ratioBounds = (bounds[0], ratioBoundsY[0], bounds[2], ratioBoundsY[1])
2120 frame = FrameRatio(pad, bounds, zmax, ratioBounds, ratioFactor, nrows, xbinlabels, self._xbinlabelsize, self._xbinlabeloption)
2121 else:
2122 frame = Frame(pad, bounds, zmax, nrows, xbinlabels, self._xbinlabelsize, self._xbinlabeloption, ybinlabels=ybinlabels)
2123
2124
2125 frame.setLogx(self._xlog)
2126 frame.setLogy(self._ylog)
2127 frame.setGridx(self._xgrid)
2128 frame.setGridy(self._ygrid)
2129
2130
2131 opt = "sames"
2132 ds = ""
2133 if self._drawStyle is not None:
2134 ds = self._drawStyle
2135 if self._drawCommand is not None:
2136 ds = self._drawCommand
2137 if len(ds) > 0:
2138 opt += " "+ds
2139
2140
2141 frame.setTitle(histos[0].GetTitle())
2142 if self._xtitle == 'Default':
2143 frame.setXTitle( histos[0].GetXaxis().GetTitle() )
2144 elif self._xtitle is not None:
2145 frame.setXTitle(self._xtitle)
2146 if self._xtitlesize is not None:
2147 frame.setXTitleSize(self._xtitlesize)
2148 if self._xtitleoffset is not None:
2149 frame.setXTitleOffset(self._xtitleoffset)
2150 if self._xlabelsize is not None:
2151 frame.setXLabelSize(self._xlabelsize)
2152 if self._ytitle == 'Default':
2153 frame.setYTitle( histos[0].GetYaxis().GetTitle() )
2154 elif self._ytitle is not None:
2155 frame.setYTitle(self._ytitle)
2156 if self._ytitlesize is not None:
2157 frame.setYTitleSize(self._ytitlesize)
2158 if self._ytitleoffset is not None:
2159 frame.setYTitleOffset(self._ytitleoffset)
2160 if self._ztitle is not None:
2161 frame.setZTitle(self._ztitle)
2162 if self._ztitleoffset is not None:
2163 frame.setZTitleOffset(self._ztitleoffset)
2164 if self._adjustMarginLeft is not None:
2165 frame.adjustMarginLeft(self._adjustMarginLeft)
2166 if self._adjustMarginRight is not None:
2167 frame.adjustMarginRight(self._adjustMarginRight)
2168 elif "z" in opt:
2169 frame.adjustMarginLeft(0.03)
2170 frame.adjustMarginRight(0.08)
2171
2172
2173 if ratio:
2174 frame._pad.cd()
2175
2176 for i, h in enumerate(histos):
2177 o = opt
2178 if isTGraph2D and i == 0:
2179 o = o.replace("sames", "")
2180 h.Draw(o)
2181
2182 for addl in self._mainAdditional:
2183 addl.Draw("same")
2184
2185
2186 if ratio and len(self._ratios) > 0:
2187 frame._padRatio.cd()
2188 firstRatio = self._ratios[0].getRatio()
2189 if self._ratioUncertainty and firstRatio is not None:
2190 firstRatio.SetFillStyle(1001)
2191 firstRatio.SetFillColor(ROOT.kGray)
2192 firstRatio.SetLineColor(ROOT.kGray)
2193 firstRatio.SetMarkerColor(ROOT.kGray)
2194 firstRatio.SetMarkerSize(0)
2195 self._ratios[0].draw("E2")
2196 frame._padRatio.RedrawAxis("G")
2197 for r in self._ratios[1:]:
2198 r.draw()
2199
2200 for addl in self._ratioAdditional:
2201 addl.Draw("same")
2202
2203 frame.redrawAxis()
2204 self._frame = frame
2205
2206 def addToLegend(self, legend, legendLabels, denomUncertainty):
2207 """Add histograms to a legend.
2208
2209 Arguments:
2210 legend -- TLegend
2211 legendLabels -- List of strings for the legend labels
2212 """
2213 first = denomUncertainty
2214 for h, label in zip(self._histograms, legendLabels):
2215 if h is None:
2216 first = False
2217 continue
2218 if first:
2219 self._forLegend = h.Clone()
2220 self._forLegend.SetFillStyle(1001)
2221 self._forLegend.SetFillColor(ROOT.kGray)
2222 entry = legend.AddEntry(self._forLegend, label, "lpf")
2223 first = False
2224 else:
2225 legend.AddEntry(h, label, "LP")
2226
2227 class PlotGroup(object):
2228 """Group of plots, results a TCanvas"""
2229 def __init__(self, name, plots, **kwargs):
2230 """Constructor.
2231
2232 Arguments:
2233 name -- String for name of the TCanvas, used also as the basename of the picture files
2234 plots -- List of Plot objects
2235
2236 Keyword arguments:
2237 ncols -- Number of columns (default 2)
2238 legendDx -- Float for moving TLegend in x direction (default None)
2239 legendDy -- Float for moving TLegend in y direction (default None)
2240 legendDw -- Float for changing TLegend width (default None)
2241 legendDh -- Float for changing TLegend height (default None)
2242 legend -- Bool for disabling legend (default True for legend being enabled)
2243 overrideLegendLabels -- List of strings for legend labels, if given, these are used instead of the ones coming from Plotter (default None)
2244 onlyForPileup -- Plots this group only for pileup samples
2245 """
2246 super(PlotGroup, self).__init__()
2247
2248 self._name = name
2249 self._plots = plots
2250
2251 def _set(attr, default):
2252 setattr(self, "_"+attr, kwargs.get(attr, default))
2253
2254 _set("ncols", 2)
2255
2256 _set("legendDx", None)
2257 _set("legendDy", None)
2258 _set("legendDw", None)
2259 _set("legendDh", None)
2260 _set("legend", True)
2261
2262 _set("overrideLegendLabels", None)
2263
2264 _set("onlyForPileup", False)
2265
2266 self._ratioFactor = 1.25
2267
2268 def setProperties(self, **kwargs):
2269 for name, value in kwargs.items():
2270 if not hasattr(self, "_"+name):
2271 raise Exception("No attribute '%s'" % name)
2272 setattr(self, "_"+name, value)
2273
2274 def getName(self):
2275 return self._name
2276
2277 def getPlots(self):
2278 return self._plots
2279
2280 def remove(self, name):
2281 for i, plot in enumerate(self._plots):
2282 if plot.getName() == name:
2283 del self._plots[i]
2284 return
2285 raise Exception("Did not find Plot '%s' from PlotGroup '%s'" % (name, self._name))
2286
2287 def clear(self):
2288 self._plots = []
2289
2290 def append(self, plot):
2291 self._plots.append(plot)
2292
2293 def getPlot(self, name):
2294 for plot in self._plots:
2295 if plot.getName() == name:
2296 return plot
2297 raise Exception("No Plot named '%s'" % name)
2298
2299 def onlyForPileup(self):
2300 """Return True if the PlotGroup is intended only for pileup samples"""
2301 return self._onlyForPileup
2302
2303 def create(self, tdirectoryNEvents, requireAllHistograms=False):
2304 """Create histograms from a list of TDirectories.
2305
2306 Arguments:
2307 tdirectoryNEvents -- List of (TDirectory, nevents) pairs
2308 requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2309 """
2310 for plot in self._plots:
2311 plot.create(tdirectoryNEvents, requireAllHistograms)
2312
2313 def draw(self, legendLabels, prefix=None, separate=False, saveFormat=".pdf", ratio=True, directory=""):
2314 """Draw the histograms using values for a given algorithm.
2315
2316 Arguments:
2317 legendLabels -- List of strings for legend labels (corresponding to the tdirectories in create())
2318 prefix -- Optional string for file name prefix (default None)
2319 separate -- Save the plots of a group to separate files instead of a file per group (default False)
2320 saveFormat -- String specifying the plot format (default '.pdf')
2321 ratio -- Add ratio to the plot (default True)
2322 directory -- Directory where to save the file (default "")
2323 """
2324
2325 if self._overrideLegendLabels is not None:
2326 legendLabels = self._overrideLegendLabels
2327
2328
2329 onlyEmptyPlots = True
2330 for plot in self._plots:
2331 if not plot.isEmpty():
2332 onlyEmptyPlots = False
2333 break
2334 if onlyEmptyPlots:
2335 return []
2336
2337 if separate:
2338 return self._drawSeparate(legendLabels, prefix, saveFormat, ratio, directory)
2339
2340 cwidth = 500*self._ncols
2341 nrows = int((len(self._plots)+self._ncols-1)/self._ncols)
2342 cheight = 500 * nrows
2343
2344 if ratio:
2345 cheight = int(cheight*self._ratioFactor)
2346
2347 canvas = _createCanvas(self._name, cwidth, cheight)
2348
2349 canvas.Divide(self._ncols, nrows)
2350 if ratio:
2351 for i, plot in enumerate(self._plots):
2352 pad = canvas.cd(i+1)
2353 self._modifyPadForRatio(pad)
2354
2355
2356 for i, plot in enumerate(self._plots):
2357 pad = canvas.cd(i+1)
2358 if not plot.isEmpty():
2359 plot.draw(pad, ratio, self._ratioFactor, nrows)
2360
2361 if plot._legend:
2362
2363 canvas.cd()
2364 if len(self._plots) <= 4:
2365 lx1 = 0.2
2366 lx2 = 0.9
2367 ly1 = 0.48
2368 ly2 = 0.53
2369 else:
2370 lx1 = 0.1
2371 lx2 = 0.9
2372 ly1 = 0.64
2373 ly2 = 0.67
2374 if self._legendDx is not None:
2375 lx1 += self._legendDx
2376 lx2 += self._legendDx
2377 if self._legendDy is not None:
2378 ly1 += self._legendDy
2379 ly2 += self._legendDy
2380 if self._legendDw is not None:
2381 lx2 += self._legendDw
2382 if self._legendDh is not None:
2383 ly1 -= self._legendDh
2384 plot = max(self._plots, key=lambda p: p.getNumberOfHistograms())
2385 denomUnc = sum([p.drawRatioUncertainty() for p in self._plots]) > 0
2386 legend = self._createLegend(plot, legendLabels, lx1, ly1, lx2, ly2,
2387 denomUncertainty=(ratio and denomUnc))
2388
2389 return self._save(canvas, saveFormat, prefix=prefix, directory=directory)
2390
2391 def _drawSeparate(self, legendLabels, prefix, saveFormat, ratio, directory):
2392 """Internal method to do the drawing to separate files per Plot instead of a file per PlotGroup"""
2393 width = 500
2394 height = 500
2395
2396 lx1def = 0.6
2397 lx2def = 0.95
2398 ly1def = 0.85
2399 ly2def = 0.95
2400
2401 ret = []
2402
2403 for plot in self._plots:
2404 if plot.isEmpty():
2405 continue
2406
2407 canvas = _createCanvas(self._name+"Single", width, height)
2408 canvasRatio = _createCanvas(self._name+"SingleRatio", width, int(height*self._ratioFactor))
2409
2410
2411 for c in [canvas, canvasRatio]:
2412 c.SetTopMargin(0.05)
2413 c.SetBottomMargin(0.13)
2414 c.SetLeftMargin(0.16)
2415 c.SetRightMargin(0.05)
2416
2417 ratioForThisPlot = plot.isRatio(ratio)
2418 c = canvas
2419 if ratioForThisPlot:
2420 c = canvasRatio
2421 c.cd()
2422 self._modifyPadForRatio(c)
2423
2424
2425 c.cd()
2426 plot.draw(c, ratioForThisPlot, self._ratioFactor, 1)
2427
2428 if plot._legend:
2429
2430 lx1 = lx1def
2431 lx2 = lx2def
2432 ly1 = ly1def
2433 ly2 = ly2def
2434
2435 if plot._legendDx is not None:
2436 lx1 += plot._legendDx
2437 lx2 += plot._legendDx
2438 if plot._legendDy is not None:
2439 ly1 += plot._legendDy
2440 ly2 += plot._legendDy
2441 if plot._legendDw is not None:
2442 lx2 += plot._legendDw
2443 if plot._legendDh is not None:
2444 ly1 -= plot._legendDh
2445
2446 c.cd()
2447 legend = self._createLegend(plot, legendLabels, lx1, ly1, lx2, ly2, textSize=0.03,
2448 denomUncertainty=(ratioForThisPlot and plot.drawRatioUncertainty))
2449
2450 ret.extend(self._save(c, saveFormat, prefix=prefix, postfix="/"+plot.getName(), single=True, directory=directory))
2451 return ret
2452
2453 def _modifyPadForRatio(self, pad):
2454 """Internal method to set divide a pad to two for ratio plots"""
2455 _modifyPadForRatio(pad, self._ratioFactor)
2456
2457 def _createLegend(self, plot, legendLabels, lx1, ly1, lx2, ly2, textSize=0.016, denomUncertainty=True):
2458 if not self._legend:
2459 return None
2460
2461 l = ROOT.TLegend(lx1, ly1, lx2, ly2)
2462 l.SetTextSize(textSize)
2463 l.SetLineColor(1)
2464 l.SetLineWidth(1)
2465 l.SetLineStyle(1)
2466 l.SetFillColor(0)
2467 l.SetMargin(0.07)
2468
2469 plot.addToLegend(l, legendLabels, denomUncertainty)
2470 l.Draw()
2471 return l
2472
2473 def _save(self, canvas, saveFormat, prefix=None, postfix=None, single=False, directory=""):
2474
2475 name = self._name
2476 if not os.path.exists(directory+'/'+name):
2477 os.makedirs(directory+'/'+name, exist_ok=True)
2478 if prefix is not None:
2479 name = prefix+name
2480 if postfix is not None:
2481 name = name+postfix
2482 name = os.path.join(directory, name)
2483
2484 if not verbose:
2485 backup = ROOT.gErrorIgnoreLevel
2486 ROOT.gErrorIgnoreLevel = ROOT.kWarning
2487 canvas.SaveAs(name+saveFormat)
2488 if not verbose:
2489 ROOT.gErrorIgnoreLevel = backup
2490
2491 if single:
2492 canvas.Clear()
2493 canvas.SetLogx(False)
2494 canvas.SetLogy(False)
2495 else:
2496 canvas.Clear("D")
2497
2498 return [name+saveFormat]
2499
2500 class PlotOnSideGroup(PlotGroup):
2501 """Resembles DQM GUI's "On side" layout.
2502
2503 Like PlotGroup, but has only a description of a single plot. The
2504 plot is drawn separately for each file. Useful for 2D histograms."""
2505
2506 def __init__(self, name, plot, ncols=2, onlyForPileup=False):
2507 super(PlotOnSideGroup, self).__init__(name, [], ncols=ncols, legend=False, onlyForPileup=onlyForPileup)
2508 self._plot = plot
2509 self._plot.setProperties(ratio=False)
2510
2511 def append(self, *args, **kwargs):
2512 raise Exception("PlotOnSideGroup.append() is not implemented")
2513
2514 def create(self, tdirectoryNEvents, requireAllHistograms=False):
2515 self._plots = []
2516 for i, element in enumerate(tdirectoryNEvents):
2517 pl = self._plot.clone()
2518 pl.create([element], requireAllHistograms)
2519 pl.setName(pl.getName()+"_"+str(i))
2520 self._plots.append(pl)
2521
2522 def draw(self, *args, **kwargs):
2523 kargs = copy.copy(kwargs)
2524 kargs["ratio"] = False
2525 return super(PlotOnSideGroup, self).draw(*args, **kargs)
2526
2527 class PlotFolder:
2528
2529 """Represents a collection of PlotGroups, produced from a single folder in a DQM file"""
2530 def __init__(self, *plotGroups, **kwargs):
2531 """Constructor.
2532
2533 Arguments:
2534 plotGroups -- List of PlotGroup objects
2535
2536 Keyword arguments
2537 loopSubFolders -- Should the subfolders be looped over? (default: True)
2538 onlyForPileup -- Plots this folder only for pileup samples
2539 onlyForElectron -- Plots this folder only for electron samples
2540 onlyForConversion -- Plots this folder only for conversion samples
2541 onlyForBHadron -- Plots this folder only for B-hadron samples
2542 purpose -- html.PlotPurpose member class for the purpose of the folder, used for grouping of the plots to the HTML pages
2543 page -- Optional string for the page in HTML generatin
2544 section -- Optional string for the section within a page in HTML generation
2545 numberOfEventsHistogram -- Optional path to histogram filled once per event. Needed if there are any plots normalized by number of events. Path is relative to "possibleDqmFolders".
2546 """
2547 self._plotGroups = list(plotGroups)
2548 self._loopSubFolders = kwargs.pop("loopSubFolders", True)
2549 self._onlyForPileup = kwargs.pop("onlyForPileup", False)
2550 self._onlyForElectron = kwargs.pop("onlyForElectron", False)
2551 self._onlyForConversion = kwargs.pop("onlyForConversion", False)
2552 self._onlyForBHadron = kwargs.pop("onlyForBHadron", False)
2553 self._purpose = kwargs.pop("purpose", None)
2554 self._page = kwargs.pop("page", None)
2555 self._section = kwargs.pop("section", None)
2556 self._numberOfEventsHistogram = kwargs.pop("numberOfEventsHistogram", None)
2557 if len(kwargs) > 0:
2558 raise Exception("Got unexpected keyword arguments: "+ ",".join(kwargs.keys()))
2559
2560 def loopSubFolders(self):
2561 """Return True if the PlotGroups of this folder should be applied to the all subfolders"""
2562 return self._loopSubFolders
2563
2564 def onlyForPileup(self):
2565 """Return True if the folder is intended only for pileup samples"""
2566 return self._onlyForPileup
2567
2568 def onlyForElectron(self):
2569 return self._onlyForElectron
2570
2571 def onlyForConversion(self):
2572 return self._onlyForConversion
2573
2574 def onlyForBHadron(self):
2575 return self._onlyForBHadron
2576
2577 def getPurpose(self):
2578 return self._purpose
2579
2580 def getPage(self):
2581 return self._page
2582
2583 def getSection(self):
2584 return self._section
2585
2586 def getNumberOfEventsHistogram(self):
2587 return self._numberOfEventsHistogram
2588
2589 def append(self, plotGroup):
2590 self._plotGroups.append(plotGroup)
2591
2592 def set(self, plotGroups):
2593 self._plotGroups = plotGroups
2594
2595 def getPlotGroups(self):
2596 return self._plotGroups
2597
2598 def getPlotGroup(self, name):
2599 for pg in self._plotGroups:
2600 if pg.getName() == name:
2601 return pg
2602 raise Exception("No PlotGroup named '%s'" % name)
2603
2604 def create(self, dirsNEvents, labels, isPileupSample=True, requireAllHistograms=False):
2605 """Create histograms from a list of TFiles.
2606
2607 Arguments:
2608 dirsNEvents -- List of (TDirectory, nevents) pairs
2609 labels -- List of strings for legend labels corresponding the files
2610 isPileupSample -- Is sample pileup (some PlotGroups may limit themselves to pileup)
2611 requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2612 """
2613
2614 if len(dirsNEvents) != len(labels):
2615 raise Exception("len(dirsNEvents) should be len(labels), now they are %d and %d" % (len(dirsNEvents), len(labels)))
2616
2617 self._labels = labels
2618
2619 for pg in self._plotGroups:
2620 if pg.onlyForPileup() and not isPileupSample:
2621 continue
2622 pg.create(dirsNEvents, requireAllHistograms)
2623
2624 def draw(self, prefix=None, separate=False, saveFormat=".pdf", ratio=True, directory=""):
2625 """Draw and save all plots using settings of a given algorithm.
2626
2627 Arguments:
2628 prefix -- Optional string for file name prefix (default None)
2629 separate -- Save the plots of a group to separate files instead of a file per group (default False)
2630 saveFormat -- String specifying the plot format (default '.pdf')
2631 ratio -- Add ratio to the plot (default True)
2632 directory -- Directory where to save the file (default "")
2633 """
2634 ret = []
2635
2636 for pg in self._plotGroups:
2637 ret.extend(pg.draw(self._labels, prefix=prefix, separate=separate, saveFormat=saveFormat, ratio=ratio, directory=directory))
2638 return ret
2639
2640
2641
2642 def translateSubFolder(self, dqmSubFolderName):
2643 """Method called to (possibly) translate a subfolder name to more 'readable' form
2644
2645 The implementation in this (base) class just returns the
2646 argument. The idea is that a deriving class might want to do
2647 something more complex (like trackingPlots.TrackingPlotFolder
2648 does)
2649 """
2650 return dqmSubFolderName
2651
2652 def iterSelectionName(self, plotFolderName, translatedDqmSubFolder):
2653 """Iterate over possible selections name (used in output directory name and legend) from the name of PlotterFolder, and a return value of translateSubFolder"""
2654 ret = ""
2655 if plotFolderName != "":
2656 ret += "_"+plotFolderName
2657 if translatedDqmSubFolder is not None:
2658 ret += "_"+translatedDqmSubFolder
2659 yield ret
2660
2661 def limitSubFolder(self, limitOnlyTo, translatedDqmSubFolder):
2662 """Return True if this subfolder should be processed
2663
2664 Arguments:
2665 limitOnlyTo -- List/set/similar containing the translatedDqmSubFolder
2666 translatedDqmSubFolder -- Return value of translateSubFolder
2667 """
2668 return translatedDqmSubFolder in limitOnlyTo
2669
2670 class DQMSubFolder:
2671 """Class to hold the original name and a 'translated' name of a subfolder in the DQM ROOT file"""
2672 def __init__(self, subfolder, translated):
2673 self.subfolder = subfolder
2674 self.translated = translated
2675
2676 def equalTo(self, other):
2677 """Equality is defined by the 'translated' name"""
2678 return self.translated == other.translated
2679
2680 class PlotterFolder:
2681 """Plotter for one DQM folder.
2682
2683 This class is supposed to be instantiated by the Plotter class (or
2684 PlotterItem, to be more specific), and not used directly by the
2685 user.
2686 """
2687 def __init__(self, name, possibleDqmFolders, dqmSubFolders, plotFolder, fallbackNames, fallbackDqmSubFolders, tableCreators):
2688 """
2689 Constructor
2690
2691 Arguments:
2692 name -- Name of the folder (is used in the output directory naming)
2693 possibleDqmFolders -- List of strings for possible directories of histograms in TFiles
2694 dqmSubFolders -- List of lists of strings for list of subfolders per input file, or None if no subfolders
2695 plotFolder -- PlotFolder object
2696 fallbackNames -- List of names for backward compatibility (can be empty). These are used only by validation.Validation (class responsible of the release validation workflow) in case the reference file pointed by 'name' does not exist.
2697 fallbackDqmSubFolders -- List of dicts of (string->string) for mapping the subfolder names found in the first file to another names. Use case is comparing files that have different iteration naming convention.
2698 tableCreators -- List of PlotterTableItem objects for tables to be created from this folder
2699 """
2700 self._name = name
2701 self._possibleDqmFolders = possibleDqmFolders
2702 self._plotFolder = plotFolder
2703
2704 if dqmSubFolders is None:
2705 self._dqmSubFolders = None
2706 else:
2707
2708
2709 subfolders = {}
2710 for sf_list in dqmSubFolders:
2711 for sf in sf_list:
2712 sf_translated = self._plotFolder.translateSubFolder(sf)
2713 if sf_translated is not None and not sf_translated in subfolders:
2714 subfolders[sf_translated] = DQMSubFolder(sf, sf_translated)
2715
2716 self._dqmSubFolders = sorted(subfolders.values(), key=lambda sf: sf.subfolder)
2717
2718 self._fallbackNames = fallbackNames
2719 self._fallbackDqmSubFolders = fallbackDqmSubFolders
2720 self._tableCreators = tableCreators
2721
2722 def getName(self):
2723 return self._name
2724
2725 def getPurpose(self):
2726 return self._plotFolder.getPurpose()
2727
2728 def getPage(self):
2729 return self._plotFolder.getPage()
2730
2731 def getSection(self):
2732 return self._plotFolder.getSection()
2733
2734 def onlyForPileup(self):
2735 return self._plotFolder.onlyForPileup()
2736
2737 def onlyForElectron(self):
2738 return self._plotFolder.onlyForElectron()
2739
2740 def onlyForConversion(self):
2741 return self._plotFolder.onlyForConversion()
2742
2743 def onlyForBHadron(self):
2744 return self._plotFolder.onlyForBHadron()
2745
2746 def getPossibleDQMFolders(self):
2747 return self._possibleDqmFolders
2748
2749 def getDQMSubFolders(self, limitOnlyTo=None):
2750 """Get list of subfolders, possibly limiting to some of them.
2751
2752 Keyword arguments:
2753 limitOnlyTo -- Object depending on the PlotFolder type for limiting the set of subfolders to be processed
2754 """
2755
2756 if self._dqmSubFolders is None:
2757 return [None]
2758
2759 if limitOnlyTo is None:
2760 return self._dqmSubFolders
2761
2762 return [s for s in self._dqmSubFolders if self._plotFolder.limitSubFolder(limitOnlyTo, s.translated)]
2763
2764 def getTableCreators(self):
2765 return self._tableCreators
2766
2767 def getSelectionNameIterator(self, dqmSubFolder):
2768 """Get a generator for the 'selection name', looping over the name and fallbackNames"""
2769 for name in [self._name]+self._fallbackNames:
2770 for selname in self._plotFolder.iterSelectionName(name, dqmSubFolder.translated if dqmSubFolder is not None else None):
2771 yield selname
2772
2773 def getSelectionName(self, dqmSubFolder):
2774 return next(self.getSelectionNameIterator(dqmSubFolder))
2775
2776 def create(self, files, labels, dqmSubFolder, isPileupSample=True, requireAllHistograms=False):
2777 """Create histograms from a list of TFiles.
2778 Arguments:
2779 files -- List of TFiles
2780 labels -- List of strings for legend labels corresponding the files
2781 dqmSubFolder -- DQMSubFolder object for a subfolder (or None for no subfolder)
2782 isPileupSample -- Is sample pileup (some PlotGroups may limit themselves to pileup)
2783 requireAllHistograms -- If True, a plot is produced if histograms from all files are present (default: False)
2784 """
2785
2786 subfolder = dqmSubFolder.subfolder if dqmSubFolder is not None else None
2787 neventsHisto = self._plotFolder.getNumberOfEventsHistogram()
2788 dirsNEvents = []
2789
2790 for tfile in files:
2791 ret = _getDirectoryDetailed(tfile, self._possibleDqmFolders, subfolder)
2792
2793 if ret is GetDirectoryCode.SubDirNotExist:
2794 for fallbackFunc in self._fallbackDqmSubFolders:
2795 fallback = fallbackFunc(subfolder)
2796 if fallback is not None:
2797 ret = _getDirectoryDetailed(tfile, self._possibleDqmFolders, fallback)
2798 if ret is not GetDirectoryCode.SubDirNotExist:
2799 break
2800 d = GetDirectoryCode.codesToNone(ret)
2801 nev = None
2802 if neventsHisto is not None and tfile is not None:
2803 hnev = _getObject(tfile, neventsHisto)
2804 if hnev is not None:
2805 nev = hnev.GetEntries()
2806 dirsNEvents.append( (d, nev) )
2807
2808 self._plotFolder.create(dirsNEvents, labels, isPileupSample, requireAllHistograms)
2809
2810 def draw(self, *args, **kwargs):
2811 """Draw and save all plots using settings of a given algorithm."""
2812 return self._plotFolder.draw(*args, **kwargs)
2813
2814
2815 class PlotterInstance:
2816 """Instance of plotter that knows the directory content, holds many folders."""
2817 def __init__(self, folders):
2818 self._plotterFolders = [f for f in folders if f is not None]
2819
2820 def iterFolders(self, limitSubFoldersOnlyTo=None):
2821 for plotterFolder in self._plotterFolders:
2822 limitOnlyTo = None
2823 if limitSubFoldersOnlyTo is not None:
2824 limitOnlyTo = limitSubFoldersOnlyTo.get(plotterFolder.getName(), None)
2825
2826 for dqmSubFolder in plotterFolder.getDQMSubFolders(limitOnlyTo=limitOnlyTo):
2827 yield plotterFolder, dqmSubFolder
2828
2829
2830 class PlotterItem:
2831 def __init__(self, name, possibleDirs, plotFolder, fallbackNames=[], fallbackDqmSubFolders=[]):
2832 """ Constructor
2833
2834 Arguments:
2835 name -- Name of the folder (is used in the output directory naming)
2836 possibleDirs -- List of strings for possible directories of histograms in TFiles
2837 plotFolder -- PlotFolder object
2838
2839 Keyword arguments
2840 fallbackNames -- Optional list of names for backward compatibility. These are used only by validation.Validation (class responsible of the release validation workflow) in case the reference file pointed by 'name' does not exist.
2841 fallbackDqmSubFolders -- Optional list of functions for (string->string) mapping the subfolder names found in the first file to another names (function should return None for no mapping). Use case is comparing files that have different iteration naming convention.
2842 """
2843 self._name = name
2844 self._possibleDirs = possibleDirs
2845 self._plotFolder = plotFolder
2846 self._fallbackNames = fallbackNames
2847 self._fallbackDqmSubFolders = fallbackDqmSubFolders
2848 self._tableCreators = []
2849
2850 def getName(self):
2851 return self._name
2852
2853 def getPlotFolder(self):
2854 return self._plotFolder
2855
2856 def appendTableCreator(self, tc):
2857 self._tableCreators.append(tc)
2858
2859 def readDirs(self, files):
2860 """Read available subfolders from the files
2861
2862 Arguments:
2863 files -- List of strings for paths to files, or list of TFiles
2864
2865 For each file, loop over 'possibleDirs', and read the
2866 subfolders of first one that exists.
2867
2868 Returns a PlotterFolder if at least one file for which one of
2869 'possibleDirs' exists. Otherwise, return None to signal that
2870 there is nothing available for this PlotFolder.
2871 """
2872 subFolders = None
2873 if self._plotFolder.loopSubFolders():
2874 subFolders = []
2875 possibleDirFound = False
2876 for fname in files:
2877 if fname is None:
2878 continue
2879
2880 isOpenFile = isinstance(fname, ROOT.TFile)
2881 if isOpenFile:
2882 tfile = fname
2883 else:
2884 tfile = ROOT.TFile.Open(fname)
2885 for pd in self._possibleDirs:
2886 d = tfile.Get(pd)
2887 if d:
2888 possibleDirFound = True
2889 if subFolders is not None:
2890 subf = []
2891 for key in d.GetListOfKeys():
2892 if isinstance(key.ReadObj(), ROOT.TDirectory):
2893 subf.append(key.GetName())
2894 subFolders.append(subf)
2895 break
2896 else:
2897 print("Did not find directory '%s' from file %s" % (pd, tfile.GetName()))
2898
2899 if not isOpenFile:
2900 tfile.Close()
2901
2902 if not possibleDirFound:
2903 return None
2904
2905 return PlotterFolder(self._name, self._possibleDirs, subFolders, self._plotFolder, self._fallbackNames, self._fallbackDqmSubFolders, self._tableCreators)
2906
2907 class PlotterTableItem:
2908 def __init__(self, possibleDirs, tableCreator):
2909 self._possibleDirs = possibleDirs
2910 self._tableCreator = tableCreator
2911
2912 def create(self, openFiles, legendLabels, dqmSubFolder):
2913 if isinstance(dqmSubFolder, list):
2914 if len(dqmSubFolder) != len(openFiles):
2915 raise Exception("When dqmSubFolder is a list, len(dqmSubFolder) should be len(openFiles), now they are %d and %d" % (len(dqmSubFolder), len(openFiles)))
2916 else:
2917 dqmSubFolder = [dqmSubFolder]*len(openFiles)
2918 dqmSubFolder = [sf.subfolder if sf is not None else None for sf in dqmSubFolder]
2919
2920 tbl = []
2921 for f, sf in zip(openFiles, dqmSubFolder):
2922 data = None
2923 tdir = _getDirectory(f, self._possibleDirs, sf)
2924 if tdir is not None:
2925 data = self._tableCreator.create(tdir)
2926 tbl.append(data)
2927
2928
2929 allNones = True
2930 colLen = 0
2931 for col in tbl:
2932 if col is not None:
2933 allNones = False
2934 colLen = len(col)
2935 break
2936 if allNones:
2937 return None
2938
2939
2940 for i in range(len(tbl)):
2941 if tbl[i] is None:
2942 tbl[i] = [None]*colLen
2943
2944 return html.Table(columnHeaders=legendLabels, rowHeaders=self._tableCreator.headers(), table=tbl,
2945 purpose=self._tableCreator.getPurpose(), page=self._tableCreator.getPage(), section=self._tableCreator.getSection(dqmSubFolder[0]))
2946
2947 class Plotter:
2948 """Contains PlotFolders, i.e. the information what plots to do, and creates a helper object to actually produce the plots."""
2949 def __init__(self):
2950 self._plots = []
2951 _setStyle()
2952 ROOT.TH1.AddDirectory(False)
2953
2954 def append(self, *args, **kwargs):
2955 """Append a plot folder to the plotter.
2956
2957 All arguments are forwarded to the constructor of PlotterItem.
2958 """
2959 self._plots.append(PlotterItem(*args, **kwargs))
2960
2961 def appendTable(self, attachToFolder, *args, **kwargs):
2962 for plotterItem in self._plots:
2963 if plotterItem.getName() == attachToFolder:
2964 plotterItem.appendTableCreator(PlotterTableItem(*args, **kwargs))
2965 return
2966 raise Exception("Did not find plot folder '%s' when trying to attach a table creator to it" % attachToFolder)
2967
2968 def clear(self):
2969 """Remove all plot folders and tables"""
2970 self._plots = []
2971
2972 def getPlotFolderNames(self):
2973 return [item.getName() for item in self._plots]
2974
2975 def getPlotFolders(self):
2976 return [item.getPlotFolder() for item in self._plots]
2977
2978 def getPlotFolder(self, name):
2979 for item in self._plots:
2980 if item.getName() == name:
2981 return item.getPlotFolder()
2982 raise Exception("No PlotFolder named '%s'" % name)
2983
2984 def readDirs(self, *files):
2985 """Returns PlotterInstance object, which knows how exactly to produce the plots for these files"""
2986 return PlotterInstance([plotterItem.readDirs(files) for plotterItem in self._plots])