Source code for MPF.n1plotter

#!/usr/bin/env python

"""
Create "n-1" plots (plots where a set of cuts is applied except the
cut on the plotted distribution). This is in particular useful for
signal region optimization when used together with
:py:class:`~MPF.significanceScanPlot.SignificanceScanPlot`. However,
you can create every plot that
:py:class:`~MPF.treePlotter.TreePlotter` can create.

To create multiple n-1 plots first set up a TreePlotter in the usual way - for Example::

  from MPF.treePlotter import TreePlotter

  tp = TreePlotter(plotType="SignificanceScanPlot", inputLumi=0.001, targetLumi=36.1,
                   cut="lep1Pt>35&&nLep_base==1&&nLep_signal==1",
                   weight="eventWeight*genWeight")

Afterwards add your backgrounds and signals in the usual way (have a look at :py:mod:`~MPF.treePlotter`).

Then create the N1Plotter and pass it your :py:class:`~MPF.treePlotter.TreePlotter`::

  from MPF.n1plotter import N1Plotter

  np = N1Plotter(tp)

The output format can also be specified - for example::

  np = N1Plotter(tp, outputFormat="<mydir>/{name}.pdf")

Now you can add several cuts (for each of which a plot should be
created). You can give arbitrary custom plotting options (see
:py:mod:`~MPF.treePlotter.TreePlotter`) for each cut. For Example::

  np.addCut(varexp="met", value=350,
            plotOptions=dict(xmin=0, xmax=1000, nbins=20))
  np.addCut(varexp="met/meffInc30", value=0.1, name="met_over_meff",
            plotOptions=dict(xmin=0, xmax=0.4, nbins=50))

If you choose to change a few options (like the cut value) afterwards
you can do that by using :py:meth:`~MPF.n1plotter.N1Plotter.getCut`. For Example::

  np.getCut("met").value = 200
  np.getCut("met_over_meff").value = 0.2

Finally create all the plots::

  np.makePlots()

Or, alternatively, register them and plot them at once (using :py:mod:`~MPF.multiHistDrawer`)::

  np.makePlots(registerOnly=True)
  tp.plotAll()

"""

from .commonHelpers.logger import logger

logger = logger.getChild(__name__)

[docs]class Cut: def __init__(self, varexp, value, comparator=">", name=None, plotOptions=None, removeCuts=None): """ :param varexp: the expression to be plotted/cut on :param value: the cut value :param comparator: how to compare the varexp to the value? :param name: name for the cut (will be used to name the output file and for referencing - see :py:meth:`~MPF.n1plotter.N1Plotter.getCut`). If no name is given, the varexp will be used. :param removeCuts: when creating the n-1 expression, in addition also remove these cuts :param plotOptions: dictionary of arbitrary options for the plot that corresponds to this cut """ self.varexp = varexp self.comparator = comparator self.value = value self.plotOptions = plotOptions self.removeCuts = removeCuts if removeCuts is not None else [] self._name = name @property def name(self): if self._name is None: self._name = self.varexp return self._name @name.setter def name(self, value): self._name = value
[docs]class N1Plotter: def __init__(self, plotter, outputFormat="{name}.pdf"): self.cuts = [] self.tp = plotter self.outputFormat = outputFormat
[docs] def addCut(self, *args, **kwargs): """ Add a cut for which an n-1 plot should be created. The arguments are passed to :py:class:`~MPF.n1plotter.Cut`. """ cut = Cut(*args, **kwargs) if cut.name in [c.name for c in self.cuts]: raise KeyError("Cut with name {} already exists".format(cut.name)) self.cuts.append(cut)
[docs] def getCut(self, name): """ Get the cut referenced by the given name. You can use this to change the options for a particular cut. For example:: np.getCut("met").value = 200 """ for cut in self.cuts: if cut.name == name: return cut
[docs] def getN1Expr(self, *names): """ Return cut expression for all cuts except the ones referenced by the given names and the cuts that are supposed to be removed with them """ removeCuts = set() for name in names: removeCuts.add(self.getCut(name)) if self.getCut(name): for removeCut in self.getCut(name).removeCuts: removeCuts.add(self.getCut(removeCut)) else: logger.warning("Cut {} non existent, returning full expression".format(name)) logger.debug("Removing cuts {} ".format(removeCuts)) cutexpr = self.tp.getOpt().cut for cut in self.cuts: if cut in removeCuts: continue cutexpr+= "&&{}{}{}".format(cut.varexp, cut.comparator, cut.value) logger.debug(cutexpr) return cutexpr
[docs] def makePlots(self, registerOnly=False): """ Create all plots. If registerOnly is set, then the plots are only registered to the :py:mod:`~MPF.treePlotter.TreePlotter` and can be plotted later by using :py:meth:`~MPF.treePlotter.TreePlotter.plotAll` Returns the list of filenames plotted (or to be plotted) """ plotNames = [] for cut in self.cuts: cutexpr = self.getN1Expr(cut.name) # maybe also respect tree plotter cut and weight? plotOptions = dict(cut=cutexpr, varexp=cut.varexp, scanDirection="forward") if cut.comparator == ">" or cut.comparator == ">=": plotOptions["scanDirection"] = "forward" plotOptions["lowerCut"] = cut.value elif cut.comparator == "<" or cut.comparator == "<=": plotOptions["scanDirection"] = "backwards" plotOptions["upperCut"] = cut.value elif not "scanDirection" in cut.plotOptions: logger.warning("Can't determine scan direction for comparator {} - using forward".format(cut.comparator)) logger.warning("Note: You can give the scan direction explicitely via the plot options") if cut.plotOptions: plotOptions.update(cut.plotOptions) if not self.tp.getOpt().plotType == "SignificanceScanPlot": plotOptions.pop("scanDirection") logger.debug("Final cut expression: "+plotOptions["cut"]) plotName = self.outputFormat.format(name=cut.name) plotNames.append(plotName) if registerOnly: self.tp.registerPlot(plotName, **plotOptions) else: self.tp.plot(plotName, **plotOptions) return plotNames