Source code for Histo1D

#!/usr/bin/env python2.7

from __future__ import print_function

import ROOT

from math import sqrt
from uuid import uuid4
from array import array
from collections import defaultdict

from Pad import Pad
from Plot import Plot
from MethodProxy import *
from Canvas import Canvas
from IOManager import IOManager
from Helpers import DissectProperties, MergeDicts, CheckPath, roundsig


def ExtendProperties(cls):
    # Add properties to configure the _errorband member histogram of Histo1Ds.
    cls._properties += ["errorband{}".format(p) for p in cls._properties]  # append!
    return cls


[docs]@ExtendProperties @PreloadProperties class Histo1D(MethodProxy, ROOT.TH1D): r"""Class for 1-dimensional histograms. +-------------------------------------------------------------------------------+ | Inherits from :class:`ROOT.TH1D`, see | | official `documentation <https://root.cern.ch/doc/master/classTH1.html>`_ | | as well! | +-------------------------------------------------------------------------------+ By default :func:`ROOT.TH1.SumW2` is called upon initialization. The properties of the **errorband** (which is itself of type ``Histo1D``) of the histogram object can be accessed by prepending the prefix 'errorband' in front of the property name. By default the errorband's fillcolor and markercolor matches the histogram's linecolor. In order to avoid memory leaks, **name** is an inaccessible property despite having corresponding getter and setter methods. Furthermore the properties **xtitle**, **ytitle** and **ztitle** are defined to be exclusive to the :class:`.Pad` class. """ # Properties not meant to be changed via keyword arguments: _ignore_properties = [ "at", "bincontent", "binerror", "bins", "canextend", "cellcontent", "cellerror", "content", "entries", "error", "maximum", "minimum", "name", "nametitle", "statoverflows", "stats", "xtitle", "ytitle", "ztitle", ] ROOT.TH1.SetDefaultSumw2(True)
[docs] def __init__(self, name, *args, **kwargs): r"""Initialize a 1-dimensional histograms. Create an instance of :class:`.Histo1D` with the specified **name** and binning (either with uniform or vairable bin widths). Can also be used to copy another histogram (or upgrade from a :class:`ROOT.TH1`). :param name: name of the histogram :type name: ``str`` :param \*args: see below :param \**kwargs: :class:`.Histo1D` properties :Arguments: Depending on the number of arguments (besides **name**) there are three ways to initialize a :class:`.Histo1D` object\: * *one* argument\: #. **histo** (``Histo1D``, ``TH1``) -- histogram to be copied * *two* arguments\: #. **title** (``str``) -- histogram title that will be used by the :class:`.Legend` class #. **xlowbinedges** (``list``, ``tuple``) -- list of lower bin-edges on the x-axis (for a histogram with variable bin widths) * *four* arguments\: #. **title** (``str``) -- histogram title that will be used by the :class:`.Legend` class #. **nbinsx** (``int``) -- number of bins on the x-axis (for a histogram with equal widths) #. **xmin** (``float``) -- minimum x-axis value (lower bin-edge of first bin) #. **xmax** (``float``) -- maximal x-axis value (upper bin-edge of last bin) """ MethodProxy.__init__(self) self._varexp = None self._cuts = None self._weight = None self._errorband = None self._drawoption = "" self._drawerrorband = False self._addtolegend = True self._legenddrawoption = "" self._stack = False # Stack property! self._attalpha = defaultdict(lambda: 1.0) self._includeoverflow = False self._includeunderflow = False if len(args) == 1: if args[0].InheritsFrom("TH1"): ROOT.TH1D.__init__(self) args[0].Copy(self) self.SetDirectory(0) self.SetName(name) if isinstance(args[0], Histo1D): self._varexp = args[0]._varexp self._cuts = args[0]._cuts self._weight = args[0]._cuts self._stack = args[0]._stack if args[0]._errorband is not None: self._errorband = Histo1D( "{}_errorband".format(name), args[0]._errorband ) if not name.endswith("_errorband"): self.DeclareProperties(**args[0].GetProperties()) self.DeclareProperties( **args[0]._errorband.GetProperties(prefix="errorband") ) elif len(args) == 2: assert isinstance(args[0], str) assert isinstance(args[1], (list, tuple)) lowbinedges = array("d", args[1]) ROOT.TH1D.__init__(self, name, args[0], len(lowbinedges) - 1, lowbinedges) elif len(args) == 4: assert isinstance(args[0], str) assert isinstance(args[1], int) ROOT.TH1D.__init__(self, name, *args) else: raise TypeError if not name.endswith("_errorband") and self._errorband is None: self._errorband = Histo1D("{}_errorband".format(self.GetName()), self) for key, value in self.GetTemplate( kwargs.get("template", "common") ).items(): kwargs.setdefault(key, value) self.DeclareProperties(**kwargs) self._lowbinedges = IOManager._getBinning(self)["xbinning"] self._nbins = len(self._lowbinedges) - 1
def DeclareProperty(self, property, args): # Properties starting with "errorband" will be applied to self._errorband. # All errorband's properties will be applied after the main histo properties. # By default the errorband fillcolor and markercolor matches the histogram's # linecolor. property = property.lower() if property.startswith("errorband"): super(Histo1D, self._errorband).DeclareProperty(property[9:], args) else: super(Histo1D, self).DeclareProperty(property, args) if property == "linecolor": errbndcol = args elif property == "linecoloralpha": errbndcol = args[0] else: return super(Histo1D, self._errorband).DeclareProperty("fillcolor", errbndcol) super(Histo1D, self._errorband).DeclareProperty("markercolor", errbndcol)
[docs] def Fill(self, *args, **kwargs): r"""Fill the histogram with entries. If a path (``str``) to an **infile** is given as the only argument the histogram if filled using the events in there as specified by the keyword arguments. Otherwise the standard :func:`ROOT.TH1.Fill` functionality is used. :param \*args: see below :param \**kwargs: see below :Arguments: Depending on the number of arguments (besides **name**) there are three ways to initialize a :class:`.Histo1D` object\: * *one* argument of type ``str``\: #. **infile** (``str``) -- path to the input :py:mod:`ROOT` file (use keyword arguments to define which events to select) * otherwise\: see :py:mod:`ROOT` documentation of :func:`TH1.Fill` (keyword arguments will be ignored) :Keyword Arguments: * **tree** (``str``) -- name of the input tree * **varexp** (``str``) -- name of the branch to be plotted on the x-axis * **cuts** (``str``, ``list``, ``tuple``) -- string or list of strings of boolean expressions, the latter will default to a logical *AND* of all items (default: '1') * **weight** (``str``) -- number or branch name to be applied as a weight (default: '1') * **append** (``bool``) -- append entries to the histogram instead of overwriting it (default: ``False``) """ self._varexp = kwargs.get("varexp") self._cuts = kwargs.get("cuts", []) self._weight = kwargs.get("weight", "1") if len(args) == 1 and isinstance(args[0], (str, unicode)): IOManager.FillHistogram(self, args[0], **kwargs) if not kwargs.get("append", False): self._errorband.Reset() self._errorband.Add(self) else: super(Histo1D, self).Fill(*args)
[docs] @CheckPath(mode="w") def Print(self, path, **kwargs): r"""Print the histogram to a file. Creates a PDF/PNG/... file with the absolute path defined by **path**. If a file with the same name already exists it will be overwritten (can be changed with the **overwrite** keyword argument). If **mkdir** is set to ``True`` (default: ``False``) directories in **path** with do not yet exist will be created automatically. The styling of the histogram, pad and canvas can be configured via their respective properties passed as keyword arguments. :param path: path of the output file (must end with '.pdf', '.png', ...) :type path: ``str`` :param \**kwargs: :class:`.Histo1D`, :class:`.Plot`, :class:`.Canvas` and :class:`.Pad` properties + additional properties (see below) Keyword Arguments: * **inject** (``list``, ``tuple``, ``ROOT.TObject``) -- inject a (list of) *drawable* :class:`ROOT` object(s) to the main pad, object properties can be specified by passing instead a ``tuple`` of the format :code:`(obj, props)` where :code:`props` is a ``dict`` holding the object properties (default: \[\]) * **overwrite** (``bool``) -- overwrite an existing file located at **path** (default: ``True``) * **mkdir** (``bool``) -- create non-existing directories in **path** (default: ``False``) """ injections = {"inject0": kwargs.pop("inject", [])} properties = DissectProperties(kwargs, [Histo1D, Plot, Canvas, Pad]) plot = Plot(npads=1) plot.Register(self, **MergeDicts(properties["Histo1D"], properties["Pad"])) plot.Print( path, **MergeDicts(properties["Plot"], properties["Canvas"], injections) )
[docs] def IncludeOverflow(self): if not self._includeoverflow: nbins = self.GetNbinsX() oflow = self.GetBinContent(nbins + 1) oflowerr = self.GetBinError(nbins + 1) self.SetBinContent(nbins, self.GetBinContent(nbins) + oflow) self.SetBinError(nbins, sqrt(self.GetBinError(nbins)**2 + oflowerr**2)) # Correct value for self.Integral(0, nbins): self.SetBinContent(nbins + 1, 0) self.SetBinError(nbins + 1, 0) self._includeoverflow = True else: logger.debug("Overflow is already included into the last bin! Skipping...")
[docs] def IncludeUnderflow(self): if not self._includeunderflow: uflow = self.GetBinContent(0) uflowerr = self.GetBinError(0) self.SetBinContent(1, self.GetBinContent(1) + uflow) self.SetBinError(1, sqrt(self.GetBinError(1)**2 + uflowerr**2)) # Correct value for self.Integral(0, nbins): self.SetBinContent(0, 0) self.SetBinError(0, 0) self._includeunderflow = True else: logger.debug( "Underflow is already included into the first bin! Skipping..." )
[docs] def SetDrawOption(self, option): r"""Define the draw option for the histogram. :param option: draw option (see :class:`ROOT.THistPainter` `class reference <https://root.cern/doc/master/classTHistPainter.html>`_) :type option: ``str`` """ if not isinstance(option, (str, unicode)): raise TypeError self._drawoption = option super(Histo1D, self).SetDrawOption(option)
[docs] def GetDrawOption(self): r"""Return the draw option defined for the histogram. :returntype: ``str`` """ return self._drawoption
[docs] def SetDrawErrorband(self, boolean): r"""Define whether the errorband should be drawn for the histogram. :param boolean: if set to ``True`` the errorband will be drawn :type boolean: ``bool`` """ self._drawerrorband = boolean
[docs] def GetDrawErrorband(self): r"""Return whether the errorband should be drawn for the histogram. :returntype: ``bool`` """ return self._drawerrorband
[docs] def GetXTitle(self): r"""Return the histogram's x-axis title. :returntype: ``str`` """ return self.GetXaxis().GetTitle()
[docs] def GetYTitle(self): r"""Return the histogram's y-axis title. :returntype: ``str`` """ return self.GetYaxis().GetTitle()
def Draw(self, option=None): # Draw the histogram to the current TPad together with it's errorband. if option is not None: self.SetDrawOption(option) self.DrawCopy(self.GetDrawOption(), "_{}".format(uuid4().hex[:8])) if self._drawerrorband: self._errorband.Reset() self._errorband.Add(self) # make sure the erroband is up-to-date self._errorband.DrawCopy( self._errorband.GetDrawOption() + "SAME", "_{}".format(uuid4().hex[:8]) ) with UsingProperties(self, fillalpha=0): # Histogram line must not be covered by the errorband: self.DrawCopy(self.GetDrawOption(), "_{}".format(uuid4().hex[:8]))
[docs] def GetBinWidths(self): r"""Return a list of all bin widths. :returntype: ``list`` """ binwidths = [ self._lowbinedges[i + 1] - self._lowbinedges[i] for i in range(len(self._lowbinedges) - 1) ] return binwidths
def BuildFrame(self, **kwargs): # Return the optimal axis ranges for the histogram. Gets called by Plot when the # histogram is registered to it. scale = 1.0 + kwargs.get("ypadding", 0.25) # Pad property logx = kwargs.get("logx", False) logy = kwargs.get("logy", False) xtitle = kwargs.get("xtitle", None) ytitle = kwargs.get("ytitle", "Entries") xunits = kwargs.get("xunits", None) if xtitle is None: xtitle = self._varexp if self._varexp is not None else "" binwidths = [roundsig(w, 4, decimals=True) for w in self.GetBinWidths()] if len(set(binwidths)) == 1: binwidth = ( int(binwidths[0]) if binwidths[0].is_integer() else round(binwidths[0], 1) ) if ( not ytitle.endswith((str(binwidth), str(xunits))) and self.GetNbinsX() > 1 ): ytitle += " / {}".format(binwidth) if xunits and not ytitle.endswith(xunits): ytitle += " {}".format(xunits) maxbinval = self.GetMaximum() frame = { "xmin": self._lowbinedges[0], "xmax": self._lowbinedges[self._nbins], "ymin": 0.0, "ymax": maxbinval if maxbinval > 0 else 1.1e-2, "xtitle": xtitle, "ytitle": ytitle, } if logx: frame["xmin"] = kwargs.get("xmin", 1e-2) if logy: frame["ymin"] = kwargs.get("ymin", 1e-2) frame["ymax"] = 10 ** ( scale * ROOT.TMath.Log10(frame["ymax"] / frame["ymin"]) + ROOT.TMath.Log10(frame["ymin"]) ) else: frame["ymax"] *= scale return frame
[docs] def Add(self, histo, scale=1): r"""Add another **histo** to the current histogram. A global weight can be set for the **histo** via the **scale**. The raw (unweighted) entries of the histograms will be added. :param histo: histogram to be added to the current object :type histo: ``Histo1D``, ``TH1D`` :param scale: global weight multiplied to **histo** (default: 1) :param scale: ``float`` """ raw_entries = self.GetEntries() + histo.GetEntries() super(Histo1D, self).Add(histo, scale) self.SetEntries(raw_entries)
[docs] def ApplyScaleFactor(self, scalefactor, uncertainty=0): r"""Apply a scale factor to the histogram. An uncertainty associated to the scale factor is included into the final per-bin uncertainty. :param scale: scale factor multiplied to current object :param scale: ``float` :param scale: absoulte uncertainty on the scale factor (default: 0) :param scale: ``float`` """ self.Scale(scalefactor) if uncertainty == 0: return for bn in range(0, self.GetNbinsX() + 2, 1): try: self.SetBinError( bn, sqrt( (self.GetBinError(bn) / self.GetBinContent(bn)) ** 2 + (uncertainty / scalefactor) ** 2 ) * self.GetBinContent(bn), ) except ZeroDivisionError: pass
[docs] def SetLegendDrawOption(self, option): r"""Define the draw option for the histogram's legend. :param option: draw option (see :class:`ROOT.TLegend` `class reference <https://root.cern/doc/master/classTLegend.html>`_) :type option: ``str`` """ self._legenddrawoption = option
[docs] def GetLegendDrawOption(self): r"""Return the draw option defined for the histogram's legend. :returntype: ``str`` """ return self._legenddrawoption
[docs] def SetLineAlpha(self, alpha): r"""Define the transparency of the histogram's line attribute. :param option: transparency of the histogram's line attribute (:math:`\alpha \in [0,1]` ) :type option: ``float`` """ self._attalpha["line"] = alpha self.SetLineColorAlpha(self.GetLineColor(), alpha)
[docs] def GetLineAlpha(self): r"""Return tthe transparency of the histogram's line attribute. :returntype: ``float`` """ return self._attalpha["line"]
[docs] def SetFillAlpha(self, alpha): r"""Define the transparency of the histogram's fill attribute. :param option: transparency of the histogram's fill attribute (:math:`\alpha \in [0,1]` ) :type option: ``float`` """ self._attalpha["fill"] = alpha self.SetFillColorAlpha(self.GetFillColor(), alpha)
[docs] def GetFillAlpha(self): r"""Return tthe transparency of the histogram's fill attribute. :returntype: ``float`` """ return self._attalpha["fill"]
[docs] def SetMarkerAlpha(self, alpha): r"""Define the transparency of the histogram's marker attribute. :param option: transparency of the histogram's marker attribute (:math:`\alpha \in [0,1]` ) :type option: ``float`` """ self._attalpha["marker"] = alpha self.SetMarkerColorAlpha(self.GetMarkerColor(), alpha)
[docs] def GetMarkerAlpha(self): r"""Return tthe transparency of the histogram's marker attribute. :returntype: ``float`` """ return self._attalpha["marker"]
def SetLineColor(self, color): self.SetLineColorAlpha(color, self._attalpha["line"]) def SetFillColor(self, color): self.SetFillColorAlpha(color, self._attalpha["fill"]) def SetMarkerColor(self, color): self.SetMarkerColorAlpha(color, self._attalpha["marker"])
[docs] def SetAddToLegend(self, boolean): r"""Define whether the histogram should be added to the :class:`.Legend`. :param boolean: if set to ``True`` the histogram will be added to the :class:`.Legend` :type boolean: ``bool`` """ self._addtolegend = boolean
[docs] def GetAddToLegend(self): r"""Return whether the histogram should be added to the :class:`.Legend`. :returntype: ``bool`` """ return self._addtolegend
[docs] def SetStack(self, boolean): """Set how the object is displayed if added to a :class:`.Stack`. If set to ``True`` and the histogram is registered to a :class:`.Stack`, it will be displayed in the stack of histograms. :param boolean: if set to ``True`` the histogram will be displayed in the stack of histograms :type boolean: ``bool`` """ self._stack = boolean
[docs] def GetStack(self): r"""Return how the object is displayed if added to a :class:`.Stack`. :returntype: ``bool`` """ return self._stack
def main(): filename = "../data/ds_data18.root" h = Histo1D("test", "test", 20, 0.0, 400.0) # Histo1D.PrintAvailableProperties() h.Fill(filename, tree="DirectStau", varexp="MET", cuts="tau1Pt>650") # print(h) # print(h.Integral()) h.Print("test_histo_data.pdf", template="data", logy=False, xunits="GeV") h.Print( "test_histo_background.pdf", template="background", logy=False, xunits="GeV" ) h2 = Histo1D("test2", h, template="signal", drawerrorband=True) h3 = Histo1D("test3", h2, linecolor=ROOT.kGreen) h3.Print("test_histo_signal.pdf", logy=True, xunits="GeV") if __name__ == "__main__": main()