#!/usr/bin/env python2.7
from __future__ import print_function
import ROOT
from uuid import uuid4
from Line import Line
from Arrow import Arrow
from Histo1D import Histo1D
from MethodProxy import *
from Helpers import DissectProperties, MergeDicts, IsInherited
def ExtendProperties(cls):
# Add properties to configure the _baseline histogram of RatioPlots and add new
# properties and methods manually since RatioPlot does not inherit from MethodProxy
# directly.
cls._properties += ["baseline{}".format(p) for p in Histo1D._properties] # append!
return cls
[docs]@ExtendProperties
@PreloadProperties
class RatioPlot(MethodProxy):
r"""Class for comparing 1-dimensional histograms.
+-------------------------------------------------------------------------------+
| Inherits from :class:`.Histo1D` which inherits from :class:`ROOT.TH1D`, see |
| official `documentation <https://root.cern.ch/doc/master/classTH1.html>`_ |
| as well! |
+-------------------------------------------------------------------------------+
Compares histograms to a **baseline** by computing the ratio for each bin.
The properties of the **baseline** (which is itself of type ``Histo1D``) of the
:class:`.RatioPlot` object can be accessed by prepending the prefix 'baseline' in
front of the property name.
"""
# Properties not meant to be changed via keyword arguments:
_ignore_properties = ["name"]
[docs] def __init__(self, numerator, denominator=0, **kwargs):
r"""Initialize a ratio plot for 1-dimensional histograms.
Create an instance of :class:`.RatioPlot` with the specified (list of)
**numerator** histogram(s) and **denominator** histogram.
:param numerator: numerator histogram or list thereof
:type name: ``Histo1D``, ``TH1D``, ``list``
:param denominator: denominator histogram which will act as the **baseline**
:type name: ``Histo1D``, ``TH1D``
:param \**kwargs: :class:`.RatioPlot` properties
"""
MethodProxy.__init__(self)
self._name = uuid4().hex[:8]
self._drawarrows = False
self._drawbenchmarklines = False
if not isinstance(numerator, list):
numerator = [numerator]
kwargs.setdefault("template", "common")
if denominator.InheritsFrom("TH1"):
self._baseline = Histo1D(
"{}_RatioPlotDenominator".format(denominator.GetName()), denominator
)
self._numeratorhistos = []
for i, histo in enumerate(numerator, start=1):
assert histo.InheritsFrom("TH1")
numname = histo.GetName()
numerator = Histo1D(
"{}_RatioPlotNumerator{}".format(denominator.GetName(), i), histo
)
numerator.Divide(
histo, self._baseline, 1.0, 1.0, kwargs.get("divideoption", "pois")
)
self._numeratorhistos.append(numerator)
for bn in range(self._baseline.GetNbinsX() + 2):
try:
self._baseline.SetBinError(
bn,
self._baseline.GetBinError(bn)
/ self._baseline.GetBinContent(bn),
)
except ZeroDivisionError:
self._baseline.SetBinError(bn, 0.0)
self._baseline.SetBinContent(bn, 1.0)
elif (
denominator.InheritsFrom("TFitResult")
or denominator == 0
or denominator.InheritsFrom("THStack")
):
raise NotImplementedError
else:
raise TypeError
self._ymin = None
self._ymax = None
self._numeratordrawoption = ""
self.DeclareProperties(**kwargs)
def Draw(self, option=None):
# Draw the baseline, its errorband, the numerator histograms and 'out-of-range'
# arrows. The specified draw option will be used for all numerator histograms.
self._baseline.Draw(self._baseline.GetDrawOption() + "SAME") # Histo1D.Draw
if self._drawbenchmarklines:
self.DrawBenchmarkLines()
self.SetDrawOption(option.upper().replace("SAME", ""))
for numerator in self._numeratorhistos:
numerator.Draw(numerator.GetDrawOption() + "SAME")
if self._drawarrows:
self.DrawArrows()
def DeclareProperty(self, property, args):
# Properties starting with "baseline" will be applied to the denominator
# histogram
property = property.lower()
if property.startswith("baseline"):
self._baseline.DeclareProperty(property[8:], args)
else:
# for numerator in self._numeratorhistos:
# numerator.DeclareProperty(property, args)
super(RatioPlot, self).DeclareProperty(property, args)
pass
[docs] def SetYMin(self, value):
r"""Set the minimal value of the y-axis.
:param value: minimal value of the y-axis
:type value: ``float``
"""
self._ymin = value
[docs] def GetYMin(self):
r"""Return the minimal value of the y-axis.
:returntype: ``float``
"""
return self._ymin
[docs] def SetYMax(self, value):
r"""Set the maximal value of the y-axis.
:param value: maximal value of the y-axis
:type value: ``float``
"""
self._ymax = value
[docs] def GetYMax(self):
r"""Return the maximal value of the y-axis.
:returntype: ``float``
"""
return self._ymax
[docs] def BuildFrame(self, **kwargs):
# Compute optimal x- and y-axis ranges.
frame = {}
for histo in self._numeratorhistos:
if not frame:
frame = histo.BuildFrame(**kwargs)
else:
for key, func in [
("xmin", min),
("ymin", min),
("xmax", max),
("ymax", max),
]:
frame[key] = func(frame[key], histo.BuildFrame(**kwargs)[key])
if self._ymin is not None:
frame["ymin"] = self._ymin
if self._ymax is not None:
frame["ymax"] = self._ymax
return frame
def DrawArrows(self, **kwargs):
# Draw the 'out-of-range' arrows for ratio value outside the given y-axis range.
currentpad = ROOT.gPad
if not currentpad:
return
ymax = currentpad.GetUymax()
ymin = currentpad.GetUymin()
self._arrows = []
for bn in range(1, self._baseline.GetNbinsX() + 1, 1):
bnclist = [h.GetBinContent(bn) for h in self._numeratorhistos]
if all([bnc == 0 for bnc in bnclist]):
continue
if max(bnclist) > ymax:
self._arrows.append(
Arrow(
self._baseline.GetXaxis().GetBinCenter(bn),
ymax - (ymax - ymin) * 0.03,
self._baseline.GetXaxis().GetBinCenter(bn),
ymax - (ymax - ymin) * 0.15,
arrowsize=0.01,
)
)
elif min(bnclist) < ymin:
self._arrows.append(
Arrow(
self._baseline.GetXaxis().GetBinCenter(bn),
ymin + (ymax - ymin) * 0.03,
self._baseline.GetXaxis().GetBinCenter(bn),
ymin + (ymax - ymin) * 0.17,
arrowsize=0.01,
)
)
for arrow in self._arrows:
arrow.Draw()
def DrawBenchmarkLines(self):
# Draw benchmark lines.
currentpad = ROOT.gPad
if not currentpad:
return
xmin = currentpad.GetUxmin()
xmax = currentpad.GetUxmax()
ymin = currentpad.GetUymin()
ymax = currentpad.GetUymax()
self._benchmarklines = []
for bm in [i * 0.1 for i in range(-5, 41, 5)]:
if bm < ymin or bm == 1:
continue
if bm > ymax:
break
self._benchmarklines.append(
Line(xmin, bm, xmax, bm, linestyle=7, linecoloralpha=(ROOT.kBlack, 0.6))
)
for line in self._benchmarklines:
line.Draw()
[docs] def SetName(self, name):
r"""Set the name of object.
:param name: name of the object
:type name: ``str``
"""
self._name = name
[docs] def GetName(self):
r"""Return the name of the object.
:returntype: ``str``
"""
return self._name
[docs] def InheritsFrom(self, classname):
# Dummy function (SensitivityScan does not inherit from any ROOT function)
return False
[docs] def SetDrawOption(self, option):
r"""Define the draw option for all numerator histograms.
:param option: draw option (see :class:`ROOT.THistPainter`
`class reference <https://root.cern/doc/master/classTHistPainter.html>`_)
:type option: ``str``
"""
if option:
self._numeratordrawoption = option
for numerator in self._numeratorhistos:
numerator.SetDrawOption(self._numeratordrawoption)
[docs] def GetDrawOption(self):
r"""Return the draw option defined for all numerator histograms.
:returntype: ``str``
"""
return self._numeratordrawoption
[docs] def SetDrawArrows(self, boolean):
r"""Define whether the arrows should be drawn.
:param boolean: if set to ``True`` arrows will be drawn whenever the ratio is
outside the given y-axis range
:type boolean: ``bool``
"""
self._drawarrows = boolean
[docs] def GetDrawArrows(self):
r"""Return whether the arrows should be drawn.
:returntype: ``bool``
"""
return self._drawarrows
[docs] def SetDrawBenchmarkLines(self, boolean):
r"""Define whether the benchmark lines should be drawn.
:param boolean: if set to ``True`` the benchmark lines will be drawn
:type boolean: ``bool``
"""
self._drawbenchmarklines = boolean
[docs] def GetDrawBenchmarklines(self):
r"""Return whether the benchmark lines should be drawn.
:returntype: ``bool``
"""
return self._drawbenchmarklines
if __name__ == "__main__":
from Plot import Plot
from IOManager import IOManager
filename = "../data/ds_data18.root"
RatioPlot.PrintAvailableProperties()
h1 = Histo1D("test1", "test1", 20, 0.0, 400.0)
h2 = Histo1D("test2", "test2", 20, 0.0, 400.0)
h3 = Histo1D("test3", "test3", 20, 0.0, 400.0)
h1.Fill(filename, tree="DirectStau", varexp="MET", cuts="tau1Pt>650")
h2.Fill(filename, tree="DirectStau", varexp="MET", cuts="tau1Pt>620")
h3.Fill(filename, tree="DirectStau", varexp="MET", cuts="tau1Pt>700")
h1.DeclareProperty("template", "background")
h2.DeclareProperties(template="data", markercolor="#a7126b", linecolor="#a7126b")
h3.DeclareProperties(template="signal", linecolor="#17a242")
rp = RatioPlot([h2, h3], h1)
p1 = Plot(npads=2)
p1.Register(h1, 0)
p1.Register(h2, 0, xunits="GeV")
p1.Register(h3, 0)
p1.Register(rp, 1, logy=False, ymin=0.5, ymax=2.0, ytitle="Data / Bkg.")
p1.Print("ratioplot_test.pdf")