#!/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