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