Fork me on GitHub

Using DelphesAnalysis to write analysis code in Python


Delphes comes with all the ExRootAnalysis framework/facilities. It is powerful and very practical. Written in C++, it has all the nice (fast) and bad (heavy) aspects of that language. On the contrary, Python is an interpreted scripting language that allows for a fast development cycle. It is therefore (becoming) the preferred language for high-level applications.

  • Introspection makes the code easy to extend
  • Run-time type evaluation removes the need for templates

DelphesAnalysis is an analysis framework written in python where a clear separation is maintained between the standard code and what the user has to implement. This makes it easy to apprehend and generic, still retaining full flexibility and scalability. The framework is built out of:

  • AnalysisEvent class: interface to the ROOT tree + many more
  • Base ControlPlots class: interface to create plots
  • Base EventSelection class: interface to code cuts/categories

All that can be configured in a single configuration file.

The AnalysisEvent class

AnalysisEvent is an interface to the tree data. It provides new functionality w.r.t. the bare access:

  • A list of event products used in the analysis
    It allows to centralize the definition of the collections to use, and provides convenient aliases that you can use in your analysis to access the various objects/products.
  • A list of "producers" of analysis high-level quantities
    This is sometimes known as “Analysis on demand”. You don't have to explicitly call the analysis routines that were previously declared. Once registered, they are run when needed. Results are cached, so that the methods are called once per event. That functionality reduces a lot the number of import statements and contributes to the optimization of the analysis code.
  • A centralized weight infrastructure
    The user can define a set of weight classes where an event weight is computed based on event quantities. AnalysisEvent keeps track of every weight and caches the intermediate and final values. Beware that the caching mechanism for weights is based on the arguments passed to them... don't use object quantities that may change.
  • A volatile dictionary to cache temporarily reconstructed products
    Any high-level quantity can be “put in the event” and retrieved when needed. This means that if your analysis is split into modules, there is no need to pass many arguments to the various functions. Just pass the event instance and use the event to store what is needed.
  class AnalysisEvent(ROOT.TChain)
   |  __init__(self, inputFiles='', maxEvents=0)
   |      Initialize the AnalysisEvent like a standard Event, plus additional features.
   |  addCollection(self, name, inputTag)
   |      Register an event collection as used by the analysis.
   |      Example: addCollection("myjets","jets") 
   |      Note that the direct access to the branch is still possible but unsafe.
   |  removeCollection(self, name)
   |      Forget about the named event collection.
   |      This method will delete both the product from the cache (if any) and the definition.
   |      To simply clear the cache, use "del" instead.
   |  getCollection(self, name)
   |      Retrieve the event product or return the cached collection.
   |      Note that the prefered way to get the collection is instead to access the "" attribute.
   |  addProducer(self, name, producer, **kwargs)
   |      Register a producer to create new high-level analysis objects.
   |  removeProducer(self, name)
   |      Forget about the producer.
   |      This method will delete both the product from the cache (if any) and the producer.
   |      To simply clear the cache, use "del" instead.
   |  addWeight(self, name, weightClass)
   |      Declare a new class (engine) to compute the weights.
   |      weightClass must have a weight() method returning a float.
   |  delWeight(self, name)
   |      Remove one weight engine from the internal list.
   |  weight(self, weightList=None, **kwargs)
   |      Return the event weight. Arguments:
   |       * weightList is the list of engines to use, as a list of strings. 
   |            Default: all defined engines.
   |       * the other named arguments are forwarded to the engines.
   |      The output is the product of the selected individual weights.
   |  event(self)
   |      Event number
   |  to(self, event)
   |      Jump to some event
   |  (...)

The BaseControlPlots class and its subclasses

That class allows to define/fill/save a set of histograms or ntuple variables, following the traditional scheme in such cases. Still, it makes use of a simplified and unified approach to minimize the amount of code needed while maintaining all the functionality of the ROOT back-end.

As a user, you must create a derived class where the following two methods are implemented:

That method is called before the event loop. You should declare the histograms, using self.add(/*TH1 arguments*/), and do any needed initialization.
That method is called for each event. A proper implement should do the processing, taking all information from the input event. Remember to take benefit from the caching, producers, and other features of AnalysisEvent. The method must return a dictionary varName:value(s) that will be used internally by the framework to fill histograms or ntuple variables. It can be a list of values when more than one entry per event, but this is then only supported for histograms, not for ntuples (the corresponding ntuple branches will be empty).

  class BaseControlPlots
   |  __init__(self, dir=None, purpose='generic', dataset=None, mode='plots')
   |      Initialize the ControlPlots, creating output file if needed. If no file is given, it means it is delegated.
   |  add(self, *args)
   |      Add one item to the list of products. Arguments are as for TH1F.
   |  beginJob(self)
   |      Declare histograms, and for derived classes instantiate handles. Must be overloaded.
   |  defineCategories(self, categories)
   |      Define the categories, given a list of names. Only works for datasets
   |  endJob(self)
   |      Save and close.
   |  fill(self, data, weight=1.0)
   |      Fills whatever must be filled in
   |  process(self, event)
   |      Process event data to extract histogrammables. Must be overloaded.
   |  processEvent(self, event, weight=1.0)
   |      process event and fill histograms
   |  setCategories(self, categories)
   |      Set the categories, given a list of booleans. Only works for datasets
   |  (...)

The BaseWeight class

That class has to be subclassed to compute event weights. The base class itself does nothing... it is mostly there as a documentation of what you have to implement in a subclass. Namely,

the constructor
A simple constructor should be implemented, with any needed parameters to initialize the calculation.
Method returning the weight value for the current event. The implementation should be based on the event content and on a set of options passed as argument. Beware that the caching mechanism for weights is based on the arguments passed to them... don't use quantities that may change during the event processing.

  class BaseWeightClass
   |  weight(self, event)
   |      Lepton eff weight

The Event selection

The EventSelection module contains the set of functions needed to select and classify events. A proper implementation of the event selection will at least define the following:

an ordered list of the names of each category.
a method that computes a tuple containing the data needed to sort events in the categories named above in its simplest form. It can be a tuple of bools, or may contain more detailed information but should always be kept simple to make the isInCategory method fast.
isInCategory(cat, categoryData)
that method returns a boolean to tell if the event is in the category cat, definition, from category data contained in categoryData.

        Check analysis requirements for various steps
        and return a tuple of data used to decide 
        to what category an event belong
    isInCategory(category, categoryData)
        Check if the event enters category X, given the tuple computed by eventCategory.

    categoryNames = [ ]

Steering code and Configuration file

The framework is contained in python/DelphesAnalsys. No file in that directory should ever be modified.

The main script is ControlPlots. This is a single code to generate a full set of histograms for your analysis, organized in logical groups by ControlPlot class and by category. The same code will generate a ntuple if the proper parameter is changed in the configuration file. Internally, ControlPlots steers the user-defined codes: event selection and control plots.

DumpEventInfo is another script that will output all the information available on a given event.

All that is configured through a simple configuration script. The script used is defined via the DelphesAnalysisCfg environment variable, or optionally via the command line in the case of ControlPlots. The config file is where the control plot class and event selection module are declared and where the event collections, producers and weights are set.

#configuration of the ControlPlot machinery

from collections import namedtuple
controlPlot     = namedtuple("controlPlot",    ["label","module","classname","kwargs"])
eventCollection = namedtuple("eventCollection",["label","collection"])
eventProducer   = namedtuple("eventProducer",  ["label","module","function","kwargs"])
eventWeight     = namedtuple("eventWeight",    ["label","module","classname","kwargs"])

class configuration:
  # default I/O
  defaultFilename = "controlPlots"
  RDSname = "rds_delphes"
  WSname = "workspace_ras"

  # mode: plots or dataset
  runningMode = "plots"

  # event selection class
  eventSelection = ""

  # control plot classes
  controlPlots = [ ]

  # event content: lists of eventCollection, eventProducer, and eventWeight objects respectively.
  eventCollections = [ ]
  eventProducers   = [ ]
  eventWeights     = [ ]

class eventDumpConfig:
  # fine-tuning of the event content for display
  productsToPrint   = [ ] # list of product to display (use the producer label)
  collectionsToHide = [ ] # collections used in the analysis but not printed (use the collection label) 

# import the actual implementation of the configuration
import os
theConfig = os.getenv("DelphesAnalysisCfg")
if theConfig is not None:
  configImplementation = __import__(os.path.splitext(theConfig)[0])
  configuration = configImplementation.configuration
  eventDumpConfig = configImplementation.eventDumpConfig

Implementing an analysis

In order to implement an analysis, one should

  • implement the specific control plot class(es), with the plots that you want to see.
  • implement an event selection module, with the cuts that should be applied.
  • declare everything in the configuration file (name given as argument of ControlPlots or set as DelphesAnalysisCfg env var).

The release includes two examples.

  • Simple analysis.
  • Basic ttbar analysis

The following commands can be run from the Delphes ROOT directory:

# setup path
cd python
# run the analysis
DelphesAnalysis/ -i ../files/ -o controlPlots_demo.root --all -c
# dump a list of events in category 5 (ttbar candidates)
export DelphesAnalysisCfg=""
python DelphesAnalysis/ 5 ../delphes_output.root
# for each of them, print details
for i in `cat eventlist.txt| cut -d' ' -f 2`; { python DelphesAnalysis/ $i ../delphes_output.root; }
Last modified 5 years ago Last modified on 12/18/14 02:02:08