Writing Custom ROOT Objects from Athena using rootcint

Below are the steps to write out a branch of a ROOT TTree containing a custom object and then read and manipulate it from ROOT's command line. The rootcint program is used to generate the dictionary.

Writing out a simple Event object with Jets

The following tutorial shows how to write out a complex Event object that contains a list of all jets found in the event. The Jet object will inherrit from a TLorentzVector and contain an additional b-tag weight. This example also has the algorithm create its own ROOT file and Tree in that file. The data objects and the algorithm objects are in seperate packages in the ATLAS CMT build system.

Attached to this page is ROOTIOTests.tar.gz, which is the complex tar/gzip of the code described below.

  1. Create the package where you'll put the data object that ROOT will write out. I've called mine ROOTTest for this example.
  2. In the Create the Jet header that will contain the information we want to write out about a jet.
    #ifndef JetInfo_H
    #define JetInfo_H
    
    #include "TLorentzVector.h"
    
    class JetInfo : public TLorentzVector
    {
     public:
      double defaultTagWeight;
    
      ~JetInfo ();
    
     private:
      ClassDef(JetInfo,1);
    
    };
    #endif
    • I called the jet object JetInfo because there is already something called Jet in the ATLAS EDM.
    • Note the ROOT ClassDef macro - that is required to declare a number of secret ROOT methods that help it parse the object and fill the dictionary.
    • I've only defined the defaultTagWeight as a new value we will write out. However, since this is a TLorentzVector we also get that data for free.
  3. Create the JetInfo.cxx source file:
    ///
    /// Simple impl file for Jet
    ///
    #include "RootIOTest/JetInfo.h"
    
    ClassImp(JetInfo)
    
    JetInfo::~JetInfo()
    {
    }
    
    #if defined(__MAKECINT__)
    #pragma link C++ class JetInfo;
    #endif
    • The ClassImp generates the bodies of the methods that were declared by the ClassDef macro.
    • The C++ preprocessor macro MAKECINT is only required for loading these files from the ROOT command line when we want to read back the data. The pragma is basically a custom command sent to rootcint. There is no need to include that #pragma if you'll never load this object from ROOT.
    • You can also get rid of the dtor method there. Since we inherrit from TLorentzVector and it has already defined a virtual destructor there is no need for this one.
  4. And the top level MyEventInfo object (header and C++ code) - very similar to the ROOT object:
    • The Event.h file:
      #include "RootIOTest/JetInfo.h"
      
      #include "TObject.h"
      
      #include <vector>
      
      class MyEventInfo : public TObject
      {
       public:
        int nJets;
        std::vector <JetInfo> Jets;
      
        ~Event();
      
        ClassDef(Event,1);
      };
      #endif
    • The Event.h file:
      #include "ROOTTest/MyEventInfo.h"
      
      ClassImp(MyEventInfo)
      
      ///
      /// Event
      ///
      
      MyEventInfo::~MyEventInfo()
      {
      }
      
      
      #if defined(__MAKECINT__)
      #pragma link C++ class MyEventInfo;
      #endif
  5. The next job is to create the requirements file.
    package ROOTTest
    
    use AtlasPolicy          AtlasPolicy-*
    use AtlasROOT * External
    
    
    library ROOTTest *.cxx
    apply_pattern installed_library
    
    apply_tag ROOTBasicLibs 
    apply_tag ROOTRFIOLibs
    apply_tag ROOTMathLibs
    
    private
    
    macro_append ROOTTest_dependencies " ROOTTestDict "
    
    apply_pattern have_root_headers          root_headers="MyEventInfo.h JetInfo.h"          headers_lib=ROOTTest
    • The library line declears the name of this library and that all cxx files in the src subdirectory should be included.
    • The installed_library pattern will make sure that a library that others can link against is build
    • The have_root_headers patternis the key thing for the ROOT objects. The root_headers should contain a complete list of the objects that you wish to include. Note that is not required to have a LinkDef.h file, but you can (make it the last one). It should be filled in as normal. The headers_lib should be the same name as the library - that way the generated root dictionary is compiled and built into the library.
    • The macro_append ROOTTest_dependencies is a bug fix - there is a bug in have_root_headers which has been fixed in svn, but not yet included in a release.
  6. Create a second package to contain the Athena algorithm/utilities to write out your ntuple. This is a very run-of-the-mill package as far as athena algorithms go. Here is the requirements files that I used for this package:
    package ROOTTestWriter
    author Gordon Watts
    
    use AtlasPolicy          AtlasPolicy-*
    use GaudiInterface GaudiInterface-01-* External
    
    use ROOTTest * mytests
    
    use AtlasROOT * External
    
    library ROOTTestWriter *.cxx -s=components *.cxx
    
    apply_pattern component_library
    
    apply_tag ROOTBasicLibs 
    apply_tag ROOTRFIOLibs
    apply_tag ROOTMathLibs
    
    private
    
    use JetEvent             JetEvent-*              Reconstruction/Jet
    use AthenaBaseComps      AthenaBaseComps-*       Control
    • The library statement is as before, but we now have the extra components directory where the usual algorithm Entries and Load files are placed.
    • The component_library pattern tells the build system this library is not for linking against, but rather contains just a sharable library that will be loaded by Athena.
    • Finally, there is the use statement to haul ROOTTest onto the link line so we can link against the various objects.
  7. Create an Athena algoirthm where you will extract the variables you want to write out. For the most part this is a vanilla algorithm - I'll mention the ROOT specific parts below.
  8. In the algorithm object add supporting member variables:
      /// The top level root object which we hold onto
      MyEventInfo *m_Event;
    
      /// Root i/o stuff
      TFile *m_rootFile;
      TTree *m_rootTree;
      std::string m_rootFileName;
    • The MyEventInfo object is our top level object that will be written out to the ROOT Tree. ROOT expects us to reuse the same object in every single event, just resetting its values. So we need to hold the pointer across events.
    • The TFile and TTree deal with the I/O itself.
    • Make sure to correctly init the m_rootFileName before it gets used in the code below (perhaps as a declareProperty or something similar)!!
  9. In the algorithm's initalize method create the event object, open the file, create the tree, and add the Event object we just created to it:
      m_Event = new MyEventInfo();
    
      m_rootFile = new TFile (m_rootFileName.c_str(), "RECREATE");
      m_rootTree = new TTree("CustomTree", "Our Custome Object Containing Tree");
      m_rootTree->Branch("event", &m_Event);
  10. And in the algorithms finalize method you need to similarly close things down:
      m_rootFile->Write();
      m_rootFile->Close();
      delete m_rootFile;
    • You have to either call Write on the File or on the Tree object. If you do not you'll loose any buffers that are in memory. Which means if you run a short test it will never write everything out! smile
    • You don't, strictly speaking, have to delete m_rootFile.
  11. Finally, the execute method. This is pretty simple now that we've setup everything around it. What you do is pretty simple:
    1. Clear the event object out from last event.
    2. Populate the Event object with data from this event
    3. Tell the tree to fill itself. At this point the data has been recorded by ROOT, so you could also clear the event here in order to save memory between events.
  /// Get out the jet collection and loop over the jets

  const JetCollection *jets;
  StatusCode sc=m_storeGate->retrieve( jets, m_particleJetContainerName);
  if (sc.isFailure()) {
    msg() << MSG::WARNING << "Jet container " 
               << m_particleJetContainerName
               << " was not found in event"
     << endreq;
    return StatusCode::SUCCESS;
  }

  int nJets = jets->size();
  msg() << MSG::INFO << "  See " << nJets << " jets" << endreq;

  m_Event->nJets = nJets;
  m_Event->Jets.clear();

  for_each(jets->begin(), jets->end(), ProcessJets(m_Event->Jets));

  ///
  /// Record the data in ROOT tree
  ///

  m_rootTree->Fill();
    • And the ProcessJets functor class, which is called in the for_each STL function above. This is a pretty simple class which just translates each jet object into one of our custom JetInfo objects.
namespace
{
  class ProcessJets {
  public:
    inline ProcessJets (vector<JetInfo> &jetArray)
      : _jets(jetArray)
    {
    }

    void operator() (const Jet &jet)
    {
      /// Extract basic Jet info
      JetInfo info;
      info.SetPtEtaPhiM(jet.pt(), jet.eta(), jet.phi(), jet.m());

      info.defaultTagWeight = jet.getFlavourTagWeight();

      _jets.push_back(info);
    }

  private:
    vector<JetInfo> &_jets;

  };
}

When you build and run on a random input file with jets in it you'll get a output ROOT file. The jobOptions file I used to test this is as follows:

#!/bin/env athena.py

include( "EventAthenaPool/EventAthenaPool_joboptions.py" )

from AthenaCommon.AppMgr import ServiceMgr
import AthenaPoolCnvSvc.ReadAthenaPool
ServiceMgr.EventSelector.InputCollections = ['/home/gwatts/data/ttbar.pool.root.1']

# Full job is a list of algorithms
from AthenaCommon.AlgSequence import AlgSequence
job = AlgSequence()

# Add top algorithms to be run
from ROOTTestWriter.ROOTTestWriterConf import CustomObjOutputAlg
job += CustomObjOutputAlg("OutputWriter")


# Number of events to be processed
theApp.EvtMax = 10

print theApp

Reading In The Custom Tree

In order to read the tree back ROOT needs to know how the objects are put together and laid out in the ROOT file. How you do this depends on several factors. If your objects are "dumb" and contain no extra methods or calculations you can use the MakeProject method. The nice thing about this method is that it will make sure all built-in-object methods (like those in TLorentzVector) are availible from the ROOT command line and MakeClass outputs. If you've added lots of helper methods to your projects then you may want to actually load those into ROOT. This is a bit more complex, but if you wrap the code up into a ROOT macro file then you can just call that. BTW, you can also do nothing and just open the file. In that case you'll have access to all members of your objects but you won't have things like TLorentzVector::pT() availible.

MakeProject

After you have opened your root TFile, just call the TFile::MakeProject as follows:

_file0->MakeProject("myfilelib", "*", "update++");

Look at TFile::MakeProject documentation on the ROOT site for more information. Once run pop up a TBrowser window and you shoudl be able to see everything.

I've been told that this will be, basically, automatic in versions of ROOT post 5.24.

Loading The Object Files

This means loading the MyEventInfo and JetInfo objects and dictionaries into ROOT directly by compiling them using ACLIC. There are several ways to do this, but I'll demonstrate the fastest that works when you have a small number of objects - using ROOT's built-in interface to the platform compiler. This will work on any platform (Windows, Mac, or Linux).

  1. Get the source code down and local on the machine you'll be processing the ROOT files on. I'll assume that the directory structure you are using is the same as a standard Athena package, so adjust accordingly. These commands are being run from the run subdirectory of the ROOTTestWriter package I made for this tutorial.
  2. Start ROOT and load in your file
    root -l custom.root
  3. Tell ROOT where it can search for include files:
    gSystem->SetIncludePath("-I/home/gwatts/testarea/15.0.0.2/mytests/ROOTTest")
  4. Load and build the files in ROOT:
    .L ../../ROOTTest/src/JetInfo.cxx+
    .L ../../ROOTTest/src/MyEventInfo.cxx+
    • The order is important - keep your inter-object dependencies well ordered and no dependency loops or you'll have a headache to solve at this stage!!
  5. At this point ROOT knows exactly what to do with these objects. You can issue a command like the following:
    CustomTree->Draw("Jets.Pt()")
    And you'll get a plot that looks like this: ROOT Plot generated with the Draw Jet.Pt Command.
  6. And if you use TBrowser to explore the Jet object you'll get the following. Double clicking on any of those items will generate the comenserate plot. If you add any functions to your objects they will also show up here and be accessible anywhere.

ROOT TBrowser view of the Jet object.

Notes and Caveats

These are just some notes based on previous experience - things that have tripped me up in the past.

  • Seperate your ROOT objects (Event and JetInfo in this case) from the code that writes them out (the Athena algorithm). For one thing this allows you to cleanly download your EDM objects to other platforms without bringing down lots of extra code. If you do plan on using this on other platforms without the ATLAS codebase make sure the requirements file for your ROOT objects Athena package contains no uses lines!
  • Reading back in a program as opposed to the ROOT command line is simple. For example, you can just link against the objects in your main program, open the TFile, get the TTree, set the Branch address to a local copy of the Event object, and start processing. Everything should just work.
  • It is tempting to calcualte derived quantities by adding them to the JetInfo (or equivalent) obect. For a small light weight root-tuple for personal or small group use this is fine (whatever gets results the quickest), but if this starts to grow then this can lead to a maintence headache down the line - as usual it is best to keep algorithms and data seperate. It is really too bad C++ doesn't support advanced features like extension methods which allow you to have this particular slice of cake and eat it too.
  • There are other things you can do to control the rootcint command line in the cmt environment. See the defintion of the macro for more information.

Future Improvements

  • If a .rootmap file is generated as part of the build then there may be a way to automatically load these objects when running under Athena and not have to explicitly use the .L command.

-- GordonWatts - 05 Jun 2009

Topic attachments
I Attachment History Action Size Date Who Comment
Unknown file formatcxx CustomObjOutputAlg.cxx r2 r1 manage 2.9 K 2009-07-17 - 15:44 GordonWatts athena algorithm to write out files
Header fileh CustomObjOutputAlg.h r2 r1 manage 0.9 K 2009-07-17 - 15:44 GordonWatts  
PNGpng PlotOfJetPt.png r1 manage 20.3 K 2009-07-17 - 16:21 GordonWatts  
Unknown file formatgz ROOTIOTests.tar.gz r1 manage 103.2 K 2009-07-17 - 16:27 GordonWatts  
PNGpng TBrowserViewOfJets.png r1 manage 23.5 K 2009-07-17 - 16:22 GordonWatts  
Edit | Attach | Watch | Print version | History: r4 < r3 < r2 < r1 | Backlinks | Raw View | WYSIWYG | More topic actions
Topic revision: r4 - 2009-07-20 - GordonWatts
 
    • Cern Search Icon Cern Search
    • TWiki Search Icon TWiki Search
    • Google Search Icon Google Search

    Main All webs login

This site is powered by the TWiki collaboration platform Powered by PerlCopyright & 2008-2019 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback