Creating a TTree
Table of Contents
Creating branches
A TTree stores many similar events in a structured way. Each event corresponds to one entry, and the different pieces of information for that event are stored in branches. Before you can fill a TTree with data, you must first create the tree object and define its branches.
In a typical ROOT session or macro you start by creating a TTree:
TTree *tree = new TTree("tree", "Simple example tree");The first argument is the internal name of the tree, and the second is a human readable title. You then decide which quantities you want to store and how they should be represented in C++. Each quantity will correspond to at least one branch.
There are two common ways to create branches for simple variables: using Branch with a pointer to a C++ variable, or using the so called leaf list syntax for very simple trees.
The direct pointer style is the most explicit and is well suited for more complex analyses. You first declare variables that will temporarily hold the data for each event, then connect them to branches:
int eventID;
float energy;
double time;
tree->Branch("eventID", &eventID, "eventID/I");
tree->Branch("energy", &energy, "energy/F");
tree->Branch("time", &time, "time/D");
The first argument in Branch is the branch name. The second is the address of the variable that will be written to or read from that branch. The third is a leaf list string that describes the branch content. It has the form "leafName/type". Common type codes are I for 32 bit integer, F for float, and D for double. When branch and leaf names are identical, you write them only once as in the examples above.
For simple scalars you can also create branches without the leaf list, and let ROOT infer the type from the variable:
tree->Branch("eventID", &eventID);
tree->Branch("energy", &energy);
tree->Branch("time", &time);This is convenient for quick setups. However, the explicit leaf list makes the branch content completely clear and is more portable across ROOT versions. In more advanced use cases, especially with arrays or more complex structures, the leaf list becomes necessary because it specifies both shape and type.
If you want to store a small fixed size C style array, you use a leaf list with a length specifier in square brackets:
const int kMaxHits = 16;
int nHits;
float hitE[kMaxHits];
tree->Branch("nHits", &nHits, "nHits/I");
tree->Branch("hitE", hitE, "hitE[nHits]/F");
Here nHits tells ROOT how many elements of hitE are valid in each entry. When you fill the tree, you must make sure that nHits never exceeds kMaxHits to avoid memory corruption.
You can also create branches that store complete C++ objects such as std::vector<float> or your own classes. For example:
std::vector<float> hitEnergies;
tree->Branch("hitEnergies", &hitEnergies);
In this case you do not provide a leaf list string. ROOT uses its I/O system to inspect the type and handle it accordingly. This is a powerful way to store variable length information per event. The details of storing std::vector objects and custom classes are covered elsewhere, so at this stage you only need to understand that branches can be connected to more complex C++ types in exactly the same way as to simple variables.
Once branches are created, the structure of the TTree is fixed. You cannot change the branch layout of an existing tree in place. If you need to add or remove branches, you create a new tree with the desired structure and copy or refill the data.
A branch is defined by calling tree->Branch(...) before you start filling the TTree. After branches are created, you should not modify the connected variables' memory layout or type. Each branch must always be connected to a valid variable or object during filling.
Storing variables
After branches have been created, you use them by storing values in the connected variables and calling Fill() for each event. The TTree does not copy data automatically when you assign to a variable. Instead, the tree only inspects the current content of the connected variables when you call tree->Fill().
A typical event filling loop looks like this:
TTree *tree = new TTree("tree", "Example tree");
int eventID;
float energy;
double time;
tree->Branch("eventID", &eventID, "eventID/I");
tree->Branch("energy", &energy, "energy/F");
tree->Branch("time", &time, "time/D");
const int nEvents = 1000;
for (int i = 0; i < nEvents; ++i) {
eventID = i;
energy = 0.5f * i; // example value
time = 0.1 * i; // example value
tree->Fill();
}
On each iteration, you compute or read the values for this particular event and assign them to the variables. When you call Fill, ROOT takes the current values of eventID, energy, and time and writes them as a new entry in the corresponding branches. After Fill returns, you are free to overwrite the variables with values for the next event. The TTree keeps its own internal copy of the stored data.
The same pattern applies when you have array branches. You first decide how many elements are valid for this event, set the size variable, fill the array elements, and then call Fill:
const int kMaxHits = 16;
int eventID;
int nHits;
float hitE[kMaxHits];
tree->Branch("eventID", &eventID, "eventID/I");
tree->Branch("nHits", &nHits, "nHits/I");
tree->Branch("hitE", hitE, "hitE[nHits]/F");
for (int iEvent = 0; iEvent < 100; ++iEvent) {
eventID = iEvent;
nHits = iEvent % (kMaxHits + 1); // some number between 0 and kMaxHits
for (int iHit = 0; iHit < nHits; ++iHit) {
hitE[iHit] = 0.1f * iHit; // example energy per hit
}
tree->Fill();
}
For branches that hold std::vector objects, you manage the content of the vector inside the loop. Typically you clear the vector at the beginning of each event, push back new values, and then call Fill:
int eventID;
std::vector<float> hitEnergies;
tree->Branch("eventID", &eventID, "eventID/I");
tree->Branch("hitEnergies", &hitEnergies);
for (int iEvent = 0; iEvent < 100; ++iEvent) {
eventID = iEvent;
hitEnergies.clear();
int nHits = iEvent % 10;
for (int iHit = 0; iHit < nHits; ++iHit) {
hitEnergies.push_back(0.2f * iHit);
}
tree->Fill();
}
It is important to keep the lifetime of the variables and objects that are connected to branches at least as long as you are filling the tree. They must not be local to a smaller scope that disappears before the event loop finishes. The usual pattern is to declare them before you call Branch, and keep them alive until you have finished all Fill calls.
When you have defined all branches and filled all events, you normally write the TTree to a ROOT file so that you can use it in later analysis steps:
TFile *file = new TFile("mytree.root", "RECREATE");
tree->Write();
file->Close();
The act of writing the tree to a file is separate from filling it. Fill only adds entries to the tree in memory or in the current I/O buffer. Write makes sure the complete tree structure and all stored entries are saved in the file.
To store variables in a TTree you must follow this pattern:
- Create the TTree.
- Create branches with
Branch, connecting them to variables or objects. - For each event, assign values to those variables.
- Call
tree->Fill()once per event.
Never callFillbefore all branch variables have been assigned valid values for that event.
With this sequence you can build TTrees that represent complete event based datasets, ready for selection, histogramming, and more advanced analysis techniques.
Views: 9
KAHIBARO