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