Source code for MPF.histograms

import copy
import math

import ROOT

from .pyrootHelpers import tempNames, rootStyleColor, calcPoissonCLLower, calcPoissonCLUpper
from . import pyrootHelpers as PH
from .errorBands import AE
import MPF.globalStyle as gst

from .commonHelpers.logger import logger
logger = logger.getChild(__name__)

[docs]class Graph(object): def __init__(self, **kwargs): self.color = 'kGray'
[docs] def setColor(self): logger.debug('set color of {} to {}'.format(self, self.color)) color = PH.rootStyleColor(self.color) self.SetLineColor(color) self.SetMarkerColor(color) self.SetMarkerStyle(6) self.SetLineWidth(2) self.SetFillColor(color)
[docs] def draw(self, **kwargs): logger.debug("Drawing {}".format(self)) self.setColor() self.Draw('p')
[docs]class Histogram(object): def __init__(self): self.process = '' self.style = None # default values self._color = 'kGray' self._fillColor = None self._lineColor = None self._markerColor = None self.lineStyle = None self.lineWidth = None self.legend = None self.hide = False self.drawLegend = True self.bins = self.GetSize()-2 # subtract over- and underflow bin self.binmin = self.GetXaxis().GetBinLowEdge(1) self.binmax = self.GetXaxis().GetBinUpEdge(self.bins) self.drawString = '' self.zIndex = 0 self.maskedBins = [] self.zoomBins = [] self.unit = None self.fillStyle = None self._markerStyle = None self.drawErrorBand = False self._errorband = None self.xTitle = self.GetXaxis().GetTitle() self.yTitle = self.GetYaxis().GetTitle() # set to None if empty if self.xTitle == "": self.xTitle = None if self.yTitle == "": self.yTitle = None def __lt__(self, other): return self.Integral() < other.Integral()
[docs] def add(self, other): self.Add(other)
[docs] def rebin(self, rebin=1): self.bins = self.GetSize()-2 if (self.bins)%rebin>0: logger.warning("trying to rebin {} with {} bins by a factor of {}".format(self.title, self.bins, rebin)) else: self.bins = self.bins/rebin self.Rebin(rebin)
@property def color(self): return rootStyleColor(self._color) @color.setter def color(self, value): self._color = value @property def fillColor(self): if self._fillColor is not None: logger.debug("Returning custom fill color: {}".format(self._lineColor)) return rootStyleColor(self._fillColor) else: return rootStyleColor(self._color) @fillColor.setter def fillColor(self, value): logger.debug("setting fill color to {}".format(value)) self._fillColor = value @property def lineColor(self): if self._lineColor is not None: logger.debug("Returning custom line color: {}".format(self._lineColor)) return rootStyleColor(self._lineColor) else: return rootStyleColor(self._color) @lineColor.setter def lineColor(self, value): self._lineColor = value @property def markerColor(self): if self._markerColor is not None: logger.debug("Returning custom marker color: {}".format(self._lineColor)) return rootStyleColor(self._markerColor) else: return rootStyleColor(self._color) @markerColor.setter def markerColor(self, value): self._markerColor = value @property def markerStyle(self): if self._markerStyle is None and self.style == "data": return 20 else: return self._markerStyle @markerStyle.setter def markerStyle(self, value): self._markerStyle = value
[docs] def setColorAndStyle(self): if self.fillStyle is not None: self.SetFillStyle(self.fillStyle) if self.markerStyle is not None: self.SetMarkerStyle(self.markerStyle) if self.style == 'signal': self.SetLineColor(self.lineColor) if self.lineStyle is None: self.SetLineStyle(2) else: self.SetLineStyle(self.lineStyle) if self.lineWidth is None: self.SetLineWidth(2) else: self.SetLineWidth(self.lineWidth) if self.fillStyle is not None: self.SetFillColor(self.fillColor) elif self.style == 'data': self.SetMarkerColor(self.markerColor) self.SetLineColor(self.lineColor) else: if gst.noLinesForBkg and self.lineWidth is None: self.SetLineWidth(0) elif self.lineWidth is not None: self.SetLineWidth(self.lineWidth) self.SetFillColor(self.fillColor)
[docs] def getXTitle(self): if self.xTitle is None: return "" elif self.unit is not None: return "{} [{}]".format(self.xTitle, self.unit) else: return self.xTitle
[docs] def getYTitle(self): if self.yTitle is not None: return self.yTitle elif not self.GetXaxis().IsVariableBinSize(): if self.unit == 'GeV': return 'Entries / {:.0f} {}'.format((self.binmax-self.binmin)/self.bins, self.unit) elif self.unit is not None: return 'Entries / {} {}'.format((self.binmax-self.binmin)/self.bins, self.unit) return 'Entries'
[docs] def underflow(self): return self.GetBinContent(0)
[docs] def overflow(self): return self.GetBinContent(self.GetNbinsX()+1)
[docs] def firstDraw(self, **kwargs): if self.style == 'data' and gst.poissonIntervalDataErrors: self.Draw("axis") self.draw(**kwargs) # apparantly nescessary for calling gPad.GetX1() etc later ROOT.gPad.Modified() ROOT.gPad.Update()
[docs] def draw(self, drawString = ""): logger.debug("Drawing {}".format(self)) drawString += self.drawString self.setColorAndStyle() if self.style == 'signal': # if not 'E' in drawstring: drawString += 'hist' elif self.style == 'data': drawString += 'PE0' elif self.style == 'axis': drawString = "axis" else: drawString += 'hist' logger.debug('drawing {} with options {} in color {} with Integral {}'.format(self.GetName(), drawString, self.color, self.Integral())) for i in self.maskedBins: self.SetBinContent(i,0) self.SetBinError(i,0) if self.style == 'data' and gst.poissonIntervalDataErrors: self.createPoissonErrorGraph() self.poissonErrorGraph.Draw(drawString) else: self.Draw(drawString) if self.drawErrorBand: self._errorband = AE(self) self._errorband.SetFillColor(gst.totalBGErrorColor) self._errorband.SetFillStyle(gst.totalBGFillStyle) self._errorband.draw()
[docs] def truncateErrors(self, value=0): for i in range(1, self.GetNbinsX()+1): self.SetBinError(i, value*self.GetBinContent(i))
[docs] def clone(self): """Clones a histogram. Until i find out how to do this properly (e.g. with deepcopy) do some stuff manually here """ cloneHist = getHM(self.Clone(next(tempNames))) self.cloneAttributes(cloneHist, self) return cloneHist
[docs] @staticmethod def cloneAttributes(tohist, fromhist): logger.debug("Cloning histogram {}".format(fromhist)) for attribute in ["color", "fillColor", "lineColor", "markerColor", "lineStyle", "lineWidth", # "hide", # there is a problem in SignalRatioPlots when we copy this - need to debug "drawLegend", "legend", "process", "drawString", "zIndex", "maskedBins", "zoomBins", "unit", "fillStyle", "markerStyle", "xTitle", "yTitle", "style", "drawErrorBand",]: try: setattr(tohist, attribute, getattr(fromhist, attribute)) except AttributeError as e: logger.warning("couldn't clone all expected attributes: {}".format(e))
yieldBinNumbers = PH.yieldBinNumbers
[docs] def addSystematicError(self, *hists, **kwargs): """ Add systematic variations to the errorband based on given variational histogram(s). :param hists: one ore more histograms to be added :param mode: how to add and symmetrise the errors? - symUpDown (default): independently add up and down variations quadratically and symmetrise afterwards - largest: also add quadratically up and down variations, but then use the max(up, down) as the error """ mode = kwargs.pop("mode", "symUpDown") if kwargs: raise KeyError("Got unexpected kwargs: {}".format(kwargs)) skippedBinHists = set() for i in self.yieldBinNumbers(overFlow=True, underFlow=True): totalUp = 0. totalDown = 0. for hist in hists: if hist.GetBinContent(i) == 0: try: histName = hist.process except AttributeError: histName = hist.GetName() logger.debug("Got a 0 bin content at bin {} for {}- ignoring it".format(i, histName)) skippedBinHists.add(histName) continue delta = hist.GetBinContent(i)-self.GetBinContent(i) if delta > 0: totalUp += delta**2 else: totalDown += delta**2 totalUp = math.sqrt(totalUp) totalDown = math.sqrt(totalDown) oldError = self.GetBinError(i) if mode == "symUpDown": newError = 0.5*(totalUp+totalDown) elif mode == "largest": newError = max(totalUp, totalDown) else: raise ValueError("Unknown mode {}".format(mode)) self.SetBinError(i, math.sqrt(oldError**2+newError**2)) if skippedBinHists: logger.warn("Got 0 bin contents for one or more bins for {} - ignored them".format(",".join(skippedBinHists)))
[docs] def addOverflowToLastBin(self): """ Add histograms overflow bin content (and error) to the last bin """ self = PH.addOverflowToLastBin(self)
[docs] def createPoissonErrorGraph(self): g = ROOT.TGraphAsymmErrors(self) for p in range(g.GetN()): x = ROOT.Double() y = ROOT.Double() g.GetPoint(p, x, y) low = calcPoissonCLLower(0.68, y) high = calcPoissonCLUpper(0.68, y) if y > 0: g.SetPointEYlow(p, y-low) g.SetPointEYhigh(p, high-y) else: g.RemovePoint(p) self.poissonErrorGraph = g
[docs]class HistogramD(ROOT.TH1D, Histogram): def __init__(self, histogram, **kwargs): super(HistogramD, self).__init__(histogram) # ROOT.TH1D.__init__(self, histogram) Histogram.__init__(self, **kwargs)
[docs]class HistogramF(ROOT.TH1F, Histogram): def __init__(self, histogram, **kwargs): super(HistogramF, self).__init__(histogram) # ROOT.TH1F.__init__(self, histogram) Histogram.__init__(self, **kwargs)
[docs]class HistogramStack(ROOT.THStack): def __init__(self, histogram): super(HistogramStack, self).__init__(histogram) self.xTitle = None self.yTitle = None self.drawString = '' self.zIndex = 0 self.bins = None self.binmin = None self.binmax = None self.unit = None self.axisHist = None self.minimum = None self.maximum = None
[docs] def firstDraw(self, **kwargs): self.axisHist.SetMinimum(self.minimum) self.axisHist.SetMaximum(self.maximum) self.axisHist.Draw("axis") # apparantly nescessary for calling gPad.GetX1() etc later ROOT.gPad.Modified() ROOT.gPad.Update() drawString = kwargs.pop("drawString", "") drawString += "same" self.draw(drawString=drawString, **kwargs)
[docs] def draw(self, drawString=''): logger.debug("Drawing {}".format(self)) drawString = self.drawString + drawString self.Draw('hist{}'.format(drawString))
[docs] def checkSet(self, attr, to): if getattr(self, attr) is None: setattr(self, attr, to) if not getattr(self, attr) == to: logger.warning("Trying to add a histogram with inconsistent attribute \"{}\" = {}" "to the stack (stack value: \"{}\")".format(attr, to, getattr(self, attr)))
[docs] def add(self, hist): self.checkSet("bins", hist.GetSize()-2) self.checkSet("binmin", hist.GetXaxis().GetBinLowEdge(1)) self.checkSet("binmax", hist.GetXaxis().GetBinUpEdge(self.bins)) if self.xTitle is None: self.checkSet("xTitle", hist.getXTitle()) if self.yTitle is None: self.checkSet("yTitle", hist.getYTitle()) if not self.axisHist: self.axisHist = hist super(HistogramStack, self).Add(hist)
[docs] def Add(self, hist): logger.warn("You should not use HistogramStack.Add (capital a) - better create an" "MPF histogram (getHM) and add it using HistogramStack.add (lowercase a)") super(HistogramStack, self).Add(hist)
[docs] def getYTitle(self): return self.yTitle
[docs] def getXTitle(self): return self.xTitle
[docs]class WrapTGraphAsymmErrors(ROOT.TGraphAsymmErrors, Graph): def __init__(self, graph, **kwargs): super(WrapTGraphAsymmErrors, self).__init__(graph) self.drawString = '' self.zIndex = 0 Graph.__init__(self, **kwargs)
[docs]def getHM(histogram): # create extended histogram if histogram.ClassName()[:4] == 'TH1D': return HistogramD(histogram) elif histogram.ClassName()[:4] == 'TH1F': return HistogramF(histogram) elif histogram.ClassName() == 'THStack': return HistogramStack(histogram) elif histogram.ClassName() == 'TGraphAsymmErrors': return WrapTGraphAsymmErrors(histogram) else: logger.warning('{} not implemented'.format(histogram.ClassName())) return