from .histProjector import HistProjector
from .process import Process
from .commonHelpers.options import checkUpdateOpt, checkUpdateDict
from . import pyrootHelpers as PH
from .histograms import getHM
from .commonHelpers.logger import logger
logger = logger.getChild(__name__)
[docs]class ProcessProjector(object):
"""
Serves as a base class for use cases where multiple histograms
should be projected from trees - defined by process instances
Used by :py:meth:`~MPF.treePlotter.TreePlotter` and :py:meth:`~MPF.signalGridProjector.SignalGridProjector`
{parameters}
"""
_doc_parameters = """
:param cut: Cut expression to be applied for all registered processes (default: "1")
:param weight: Weight expression to be applied for all registered processes (default: "1")
:param varexp: Expression to be used for filling histograms (default: "1")
:param inputLumi: luminosity the trees are normalised to
:param targetLumi: luminosity the histograms should be scaled to
:param xmin: minimum on the x axis
:param xmax: maximum on the x axis
:param nbins: number of bins
:param binLowEdges: list of low edges of bins (in this case xmin, xmax and nbins are ignored)
:param useMultiHistDraw: use :py:meth:`~MPF.multiHistDrawer` when calling
:py:meth:`~MPF.processProjector.ProcessProjector.fillHists`
(loop tree only once and fill all histograms) (default: True)
:param cutsDict: if this is given, fetch only yields for all cuts
and create a histogram and yieldsDict for each process
"""
__doc__ = __doc__.format(parameters=_doc_parameters)
defaults = dict(
cut = "1",
weight = "1",
varexp = "1",
inputLumi = 1.,
targetLumi = 1.,
xmin = 0.,
xmax = 2.,
nbins = 1,
binLowEdges = None,
useMultiHistDraw = True,
cutsDict=None,
)
def __init__(self, **kwargs):
self.processes = []
self.processDict = {}
self.sysProcesses = {}
self.projector = HistProjector()
self.defaults = checkUpdateDict(self.defaults, **kwargs)
[docs] def addProcess(self, process):
"""
Add a :py:meth:`~MPF.process.Process`
"""
self.processes.append(process)
if process.name in self.processDict:
raise KeyError("Process {} already exists!".format(process.name))
self.processDict[process.name] = process
[docs] def addProcessTree(self, name, filename, treename, **kwargs):
"""
Create and add a process from one tree in one file. The kwargs are
passed to :py:meth:`~MPF.process.Process`:
{Process_parameters}
"""
process = Process(name, **kwargs)
process.addTree(filename, treename)
self.addProcess(process)
addProcessTree.__doc__ = addProcessTree.__doc__.format(Process_parameters=Process._doc_parameters)
[docs] def addSysTreeToProcess(self, nomProcessName, sysName, filename, treename, **kwargs):
"""
Create and add a process from one tree in one file and register it
as a systematic variation for the nominal process. The kwargs are
passed to :py:meth:`~MPF.process.Process`
:param nomProcessName: name of the nominal process
:param sysName: name of the systematic variation
:param treename: name of the tree
:param filename: path to the rootfile containing the tree
:param normToProcess: normalise the histogram to the same
integral as the given process (by name) before plotting (only
used in :py:class:`~MPF.treePlotter.TreePlotter`).
"""
process = Process(sysName+"_"+nomProcessName, sysTag=sysName, style="systematic", **kwargs)
process.addTree(filename, treename)
self.addProcess(process)
if not nomProcessName in self.sysProcesses:
self.sysProcesses[nomProcessName] = []
self.sysProcesses[nomProcessName].append(process)
[docs] def setProcessOptions(self, processName, **kwargs):
"""
Change the options of an existing process - referenced by its
name. Only change the given options, leave the existing ones
"""
process = self.getProcess(processName)
process.updateOptions(**kwargs)
[docs] def getProcesses(self, *selection):
for p in self.processes:
if len(selection) < 1:
yield p
elif p.style in selection:
yield p
[docs] def getProcess(self, processName):
for p in self.processes:
if p.name == processName:
return p
[docs] def registerToProjector(self, *selection, **kwargs):
"""Register hists for each process to the histProjector (to be filled
later with multiHistDraw).
Mainly for internal use in
:py:meth:`~MPF.treePlotter.TreePlotter.registerPlot` and
:py:meth:`~MPF.signalGridProjector.SignalGridProjector.registerHarvestList`
:param opt: namedtuple containing :py:meth:`~MPF.processProjector.ProcessProjector` options
:param selection: only register processes of this style(s) (like "background")
"""
opt = kwargs.pop("opt", self.getOpt())
logger.debug("Filtering the following processes: {}".format(selection))
for p in self.getProcesses(*selection):
logger.debug("Registering hists for process {}".format(p.name))
if not p.cut is None:
cut = "({})*({})".format(opt.cut, p.cut)
else:
cut = opt.cut
if not p.varexp is None:
varexp = p.varexp
else:
varexp = opt.varexp
for filename, treename, treeKwargs in p.trees:
if treeKwargs["cut"] is not None:
treecut = "({})*({})".format(cut, treeKwargs["cut"])
else:
treecut = cut
if opt.cutsDict is not None:
self.projector.registerYieldsDict(treename, opt.cutsDict, filename, weight=opt.weight)
else:
# this can be made more generic
self.projector.registerTH1Path(treename, treecut, filename,
weight=opt.weight,
xmin=opt.xmin,
xmax=opt.xmax,
varexp=varexp,
nbins=opt.nbins,
binLowEdges=opt.binLowEdges)
[docs] def fillHists(self, opt=None):
"""
Project histograms for all processes
:param opt: if given use these options instead of the current ones (see :py:meth:`~MPF.processProjector.ProcessProjector.getOpt`)
"""
if opt is None:
opt = self.getOpt()
if opt.cutsDict is not None:
self.fillYieldsDicts(opt)
return
for p in self.processes:
logger.debug("Creating hists for process {}".format(p.name))
hists = []
if not p.cut is None:
cut = "({})*({})".format(opt.cut, p.cut)
else:
cut = opt.cut
if not p.varexp is None:
varexp = p.varexp
else:
varexp = opt.varexp
for filename, treename, treeKwargs in p.trees:
if treeKwargs["cut"] is not None:
treecut = "({})*({})".format(cut, treeKwargs["cut"])
else:
treecut = cut
hists.append(self.projector.getTH1Path(treename, treecut, filename,
weight=opt.weight,
xmin=opt.xmin,
xmax=opt.xmax,
varexp=varexp,
nbins=opt.nbins,
binLowEdges=opt.binLowEdges))
p.hist = PH.getMergedHist(hists)
if (not p.style == "data"
and opt.targetLumi is not None
and opt.inputLumi is not None
and not p.noLumiNorm):
p.hist.Scale(opt.targetLumi/opt.inputLumi)
if p.norm:
try:
p.hist.Scale(1./p.hist.Integral())
except ZeroDivisionError:
logger.error("Can't normalise histogram for process {} - Integral is empty".format(p.name))
if p.scale is not None:
p.hist.Scale(p.scale)
[docs] def fillHistsSysErrors(self):
"""Adds errors based on variational histograms for all
processes to their histograms. Should only be used if
variations are not correlated across different processes
(e.g. **don't** use it for
:py:class:`~MPF.treePlotter.TreePlotter` - there is a
treatment included for this via :py:meth:`~MPF.plotStore.PlotStore.registerSysHist`)
"""
for p in self.processes:
if p.style == "systematic":
continue
if not p.name in self.sysProcesses:
continue
p.hist = getHM(p.hist)
p.hist.addSystematicError(*[_p.hist for _p in self.sysProcesses[p.name]])
[docs] def fillYieldsDicts(self, opt=None):
"""
Fill yields dicts from cutsDict
:param opt: if given use these options instead of the current ones (see :py:meth:`~MPF.processProjector.ProcessProjector.getOpt`)
"""
if opt is None:
opt = self.getOpt()
for p in self.processes:
logger.debug("Creating yieldsDict for process {}".format(p.name))
if not p.cut is None:
weight = "({})*({})".format(opt.weight, p.cut)
else:
weight = opt.weight
yieldsDicts = []
for filename, treename, treeKwargs in p.trees:
if treeKwargs["cut"] is not None:
treeweight = "({})*({})".format(weight, treeKwargs["cut"])
else:
treeweight = weight
yieldsDicts.append(self.projector.getYieldsDict(treename, opt.cutsDict, filename, weight=treeweight))
p.yieldsDict = PH.getMergedYieldsDict(*yieldsDicts)
if (not p.style == "data"
and opt.targetLumi is not None
and opt.inputLumi is not None):
PH.scaleYieldsDict(p.yieldsDict, opt.targetLumi/opt.inputLumi)
if p.scale is not None:
PH.scaleYieldsDict(p.yieldsDict, p.scale)
p.hist = PH.getHistFromYieldsDict(p.yieldsDict)
if p.norm:
# only do this for hist
p.hist.Scale(1./p.hist.Integral())
[docs] def setDefaults(self, **kwargs):
self.defaults = checkUpdateDict(self.defaults, **kwargs)
[docs] def getOpt(self, **kwargs):
"""
Get the namedtuple containing the current :py:meth:`~MPF.processProjector.ProcessProjector` options, updated by the given arguments.
"""
return checkUpdateOpt(self.defaults, **kwargs)