Your analysis task
In the previous section we discussed some of the basics of writing an analysis task from a 'theoretical viewpoint'. In this section, we will go through a working analysis class line-by-line.
As you now know, your own analysis task will be derived from the base class AliAnalysisTaskSE
. We will use a fixed format to write our tasks, which means that we need to create three files:
The header file (.h) which contains function prototypes and in which your class members are defined
The implementation file (.cxx), in which the methods of your analysis are implemented
An AddTask.C macro which creates an instance of your class and configures it.
The class header (.h)
Let’s start by looking at our header (the .h file), where we define the prototypes of all the methods that we want to implement in our class
#ifndef AliAnalysisTaskMyTask_H
#define AliAnalysisTaskMyTask_H
class AliAnalysisTaskMyTask : public AliAnalysisTaskSE
{
public:
// two class constructors
AliAnalysisTaskMyTask();
AliAnalysisTaskMyTask(const char *name);
// class destructor
virtual ~AliAnalysisTaskMyTask();
// called once at beginning of runtime
virtual void UserCreateOutputObjects();
// called for each event
virtual void UserExec(Option_t\* option);
// called at end of analysis
virtual void Terminate(Option_t\* option);
/// \cond CLASSDEF
ClassDef(AliAnalysisTaskMyTask, 1);
/// \endcond
};
#endif
Let's go through the snippet of code line-by-line. First of all, we see
#ifndef AliAnalysisTaskMyTask_H
#define AliAnalysisTaskMyTask_H
.
.
.
#endif
These three lines form an include guard (sometimes called macro guard, or header guard). This construct is used to avoid the problem of double inclusion: should the header of your class be included more than once, the include guard will protect your code from double definitions which result in invalid code.
Include guards
What is the risk of not using include guards?
drum roll Click to expand
Class definition and constructors
If we continue looking at the code, we see the class definition and two class constructors
class AliAnalysisTaskMyTask : public AliAnalysisTaskSE
{
public:
// two class constructors
AliAnalysisTaskMyTask();
AliAnalysisTaskMyTask(const char *name);
// class destructor
virtual ~AliAnalysisTaskMyTask();
The first line means that we define a class AliAnalysisTaskMyTask
, which is derived from AliAnalysisTaskSE
.
Class constructors and destructor
The two functions AliAnalysisTaskMyTask()
and AliAnalysisTaskMyTask(const char *name)
are class constructors. A class constructor is a special function in a class that is called when a new object of the class is created.
We always need to define two class constructors for our analysis task, why this is, will be explained later. The third function is the class destructor. A destructor is also a special function which is called when the created object is deleted. We will later see, that a class that has pointer data members should include, in addition to a destructor, a copy constructor and an assignment operator - but for now we can just forget about those.
AliAnalysisTaskSE inherited functions
After the constructors and destructors, we define the prototypes of the functions that we have already seen in the AliAnalysisTaskSE
section
// called once at beginning of runtime
virtual void UserCreateOutputObjects();
// called for each event
virtual void UserExec(Option_t* option);
// called at end of analysis
virtual void Terminate(Option_t* option);
These functions will be the heart of our analysis. In the UserCreateOutputObjects
, we will define whatever output we want to write to our root
files. The UserExec
function will be called for all events in the data sample that we are looking at. Finally, Terminate
is called at the very end of the analysis.
Virtual functions
In the code snippet above, we see the virtual
keyword. Do you know what this means ?
Click to show answer Click to expand
Histograms, lists, and more
Now that we have defined the methods of our analysis class, it is time to add some members. What we are going to add, are three pointer data members
fAOD
which is a pointer to an eventfOutputList
which is a (pointer to a) list that holds all our output objectsfHistPt
which is a (pointer to a) histogram that will hold our pt spectrum
Since these members are part of our class definition, we have to add them to our header file
class AliAnalysisTaskMyTask : public AliAnalysisTaskSE
.
.
.
private:
AliAODEvent* fAOD; //!<! input event
TList* fOutputList; //!<! output list
TH1F* fHistPt; //!<! dummy histogram
In the code snippet above, you might see something interesting: when we write comments that describe what our class members are doing, we append the expression !<!
to the double slashes //
that start our comment giving us //!<!
. Contrary to what you might think, this expression mark is seen by ROOT (even though it's written as a comment) and it is essential for the correct documentation generation. We will get later to the logic of this locution, but for now a rule of thumb suffices: pointers to objects that are initialized at run-time (in the User*
methods) should be marked with a //!<!
.
The ClassDef definition
At the very end of our header file, we add the following line
/// \cond CLASSDEF
ClassDef(AliAnalysisTaskMyTask, 1);
/// \endcond
};
There is a lot going on behind this one single line: ClassDef
is a C preprocessor macro that must be used if your class derives from TObject
. ClassDef
contains member declarations, i.e. it inserts a few new members into your class; the ClassDef
macro family is defined in the file Rtypes.h
, should you be interested.
The two comments surrounding the ClassDef
statement are required to properly produce the documentation.
How all this works exactly is not very relevant at this point. We will later see that you will need to increase the version number whenever you change the definition of your class, or ROOT will not be able to handle objects written before and after this change in one process. The version number 0 (zero) disables I/O for the class completely, so we start counting at 1.
The implementation of your analysis task (.cxx)
We are for now done with our class header, it's time to move to the implementation of the class: the AliAnalysisTaskMyTask.cxx
file, in which we will actually implement our methods. Let's start by defining our class constructors. As stated before, ROOT requires two class constructors (we’ll get later to why this is necessary), one of which is the I/O constructor, which is not allowed to allocate any memory. So we start our implementation with
AliAnalysisTaskMyTask::AliAnalysisTaskMyTask() : AliAnalysisTaskSE(),
fAOD{0}, fOutputList{0}, fHistPt{0}
{
// ROOT IO constructor, don't allocate memory here!
}
AliAnalysisTaskMyTask::AliAnalysisTaskMyTask(const char* name) : AliAnalysisTaskSE(name),
fAOD{0}, fOutputList{0}, fHistPt{0}
{
DefineInput(0, TChain::Class());
DefineOutput(1, TList::Class());
}
As you see, in the constructor, we initialize members to their default values. Always initialize members to default values (and pointers to nullptr
). If you fail to do so, values contained by the members will be random, which can lead to unexpected behavior of your code.
Undefined behavior
You have written a small function
int f(int x)
{
int a;
if(x>0) a = 42;
return a;
}
What happens when you call
f(10)
and what happens when you call
f(-10)
?
drum rollClick to expand
In the second constructor of this task, we define what the input and output does the analysis class handle. In our case, the input is of type TChain
, and as we will see later, the output is a TList
.
UserCreateOutputObjects()
In our UserCreateOutputObjects
function, we will define the output objects of our task. These are commonly histograms, profiles, etc. In our specific example, we will add one histogram, and define a list to which we will attach the histogram.
Output lists
Adding all your output histograms to a list makes your life easier: the list will allow you to manipulate many output objects simultaneously. By calling TList::SetOwner(true)
, we transfer ownership of all memory allocated by the list items to the TList
itself, this means that in our destructor, we can simply call delete list
to delete all our list items, rather than calling delete
for all items individually.
The implementation of the UserCreateOutputObjects
method looks like this
...
#include "TList.h"
#include "TH1F.h"
...
AliAnalysisTaskMyTask::UserCreateOutputObjects()
{
// create a new TList that OWNS its objects
fOutputList = new TList();
fOutputList->SetOwner(true);
// create our histo and add it to the list
fHistPt = new TH1F("fHistPt", "fHistPt", 100, 0, 100);
fOutputList->Add(fHistPt);
// add the list to our output file
PostData(1,fOutputList);
}
Note that we create a TList
and make it the owner of the memory of all its elements. After we create the histogram, we add it to the output list. Finally, in the last line, we call PostData
, which will notify the client tasks of the data container that that the data pointer has changed compared to the previous post.
UserExec
The UserExec
is the heart of our analysis: it is called for each event in our input data set. First, we need to access our event, which we can do via a call to the method InputEvent
...
#include "AliAODEvent.h"
...
AliAnalysisTaskMyTask::UserExec(Option_t*)
{
// get an event from the analysis manager
fAOD = dynamic_cast<AliAODEvent*>(InputEvent());
// check if there actually is an event
if(!fAOD)
::Fatal("AliAnalysisTaskMyTask::UserExec", "No AOD event found, check the event handler.");
Once we have access to our input event, we can e.g. loop over all the tracks that it contains, and store the pt of the tracks in our histogram:
...
// let's loop over the tracks and fill our histogram
// first we get the number of tracks
int iTracks{fAOD->GetNumberOfTracks()};
// and then loop over them
for(int i{0}; i < iTracks; i++) {
AliAODTrack* track = static_cast<AliAODTrack*>(fAOD->GetTrack(i));
if(!track) continue;
// here we do some track selection
if(!track->TestFilterbit(128) continue;
// fill our histogram
fHistPt->Fill(track->Pt());
}
// and save the data gathered in this iteration
PostData(1, fOutputList);
}
Take a look at the code and make sure that you understand the logic of all the lines. In principle, this is all you need in an analysis task to run a small analysis.
Almost there: the AddTask macro
The last thing that we need to fully define our analysis task, is a small file, that we will call the AddTask
macro, that instantiates our task (class), defines its in- and output, and connects it to the analysis manager. We will put this piece of code in an independent, third file, called AddMyTask.C
. It is important, that this macro defines a function, in our case called AddMyTask
, that returns a pointer to our analysis task. We need to follow this exact convention if we later on want to run our analysis in the automated LEGO train system.
Our AddTask
macro looks as follows:
AliAnalysisTaskMyTask* AddMyTask(TString name = "name") {
AliAnalysisManager *mgr = AliAnalysisManager::GetAnalysisManager();
// resolve the name of the output file
TString fileName = AliAnalysisManager::GetCommonFileName();
fileName += ":MyTask"; // create a subfolder in the file
// now we create an instance of your task
AliAnalysisTaskMyTask* task = new AliAnalysisTaskMyTask(name.Data());
// add your task to the manager
mgr->AddTask(task);
// connect the manager to your task
mgr->ConnectInput(task,0,mgr->GetCommonInputContainer());
// same for the output
mgr->ConnectOutput(task,1,mgr->CreateContainer("MyOutputContainer", TList::Class(), AliAnalysisManager::kOutputContainer, fileName.Data()));
// important: return a pointer to your task
return task;
}
And that's it - we now have our three files that contain a minimal analysis!