KAHIBARO
Discord Login Register

Branch Addresses

`SetBranchAddress()`

To do low level work with TTrees you often need to read branch data entry by entry inside your own event loop. The central tool for this in C++ ROOT is the member function TTree::SetBranchAddress(). It tells the tree where in memory it should copy the data of a given branch when you call GetEntry().

Conceptually, a branch in a TTree holds a column of values, one value per entry. When you call:

cpp
tree->SetBranchAddress("branchName", &variablePointer);

you are giving ROOT the address of a variable or object, and ROOT promises to fill it with the content of branchName whenever you read an entry.

SetBranchAddress() does not copy data immediately.
The data are copied only when you call GetEntry(entryIndex).
Also, the pointer or variable you pass must stay valid for the entire time you read entries.

The typical steps are:

  1. Declare variables (or pointers) that will receive the data.
  2. Call SetBranchAddress() once for each branch.
  3. Loop over entries and call GetEntry(i) in the loop.
  4. Use the now filled variables for your analysis.

A minimal example for simple numeric branches looks like this:

cpp
TFile *f = TFile::Open("data.root");
TTree *t = (TTree*)f->Get("tree");
// Variables to hold branch data
Float_t energy;
Int_t   charge;
// Connect branches to variables
t->SetBranchAddress("energy", &energy);
t->SetBranchAddress("charge", &charge);
// Now you can loop over entries and use energy, charge

Here the branch "energy" is assumed to store a floating point value compatible with Float_t and "charge" an integer compatible with Int_t. ROOT will automatically convert between some compatible numeric types, but the safest approach is to match the exact type used when the tree was written.

You can check the structure and types with:

cpp
t->Print();

and adapt your receiving variables accordingly.

When branches contain C arrays, you provide a pointer to the first element of the array. For example:

cpp
Int_t    nHits;
Float_t  hitE[100];
// "nHits" is the number of hits in this event
t->SetBranchAddress("nHits", &nHits);
t->SetBranchAddress("hitE",  hitE);

In this case each entry may have a different number of active elements, up to the maximum that was used when the tree was created. Typically the branch definition encodes this, such as hitE[nHits]/F. When reading, you always provide the full array and use nHits to know how many elements are valid for a given entry.

For branches that store objects, particularly std::vector<T> branches, you provide a pointer to a pointer. A common pattern is:

cpp
std::vector<float> *pt = nullptr;
t->SetBranchAddress("pt", &pt);

After GetEntry(i), pt points to a vector owned by the tree entry, and you can access its contents like a normal std::vector<float>.

When using SetBranchAddress():

  1. Use the correct C++ type for each branch.
  2. Do not let the receiving variables go out of scope while reading.
  3. For std::vector branches, use a pointer to a pointer, for example std::vector<float> *v = nullptr; and pass &v.
  4. Always call GetEntry(entry) before using the variables for that entry.

For more advanced use cases, SetBranchAddress() has overloads that allow you to work directly with a TBranch * object, or to specify custom TClass and TBranch addresses, but for typical beginner analysis the simple name plus address form is sufficient.

Reading data manually

Once branch addresses are connected, you can manually drive the event loop and process your data entry by entry. This gives you full control over the flow of analysis, conditional logic, and interaction with histograms or other objects.

The skeleton of a manual reading loop is always the same:

  1. Obtain the number of entries using GetEntries().
  2. Loop over the entry index from 0 to nEntries - 1.
  3. Call GetEntry(i) inside the loop to load that entry’s data into your variables.
  4. Use the variables to perform calculations and fill histograms or graphs.

A typical example for a tree with simple scalar branches might look like this:

cpp
TFile *f = TFile::Open("data.root");
TTree *t = (TTree*)f->Get("tree");
// Variables to be filled
Float_t energy;
Int_t   charge;
// Connect branches
t->SetBranchAddress("energy", &energy);
t->SetBranchAddress("charge", &charge);
// Prepare some histograms
TH1F *hEnergy = new TH1F("hEnergy", "Energy;E;Entries", 100, 0.0, 10.0);
TH1F *hCharge = new TH1F("hCharge", "Charge;Q;Entries", 5, -2.5, 2.5);
// Event loop
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);            // fills energy and charge
    // Apply a simple selection
    if (energy > 0.5) {
        hEnergy->Fill(energy);
    }
    hCharge->Fill(charge);
}

Within the loop, after GetEntry(i), the variables energy and charge contain the values for entry i. You can apply arbitrary logic, combine values, and pass results to other functions.

For branches with variable length arrays, you use the companion size variable to know how many elements to loop over for each entry:

cpp
Int_t   nHits;
Float_t hitE[100];
t->SetBranchAddress("nHits", &nHits);
t->SetBranchAddress("hitE",  hitE);
TH1F *hHitE = new TH1F("hHitE", "Hit energies;E;Hits", 100, 0, 5);
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);
    for (Int_t j = 0; j < nHits; ++j) {
        hHitE->Fill(hitE[j]);
    }
}

For branches storing std::vector objects, the pattern is similar but you treat the variable as a pointer to a vector. The tree will manage the underlying vector allocation:

cpp
std::vector<float> *pt  = nullptr;
std::vector<float> *eta = nullptr;
t->SetBranchAddress("pt",  &pt);
t->SetBranchAddress("eta", &eta);
TH1F *hPt  = new TH1F("hPt",  "Transverse momentum;p_{T};Entries", 100, 0, 100);
TH1F *hEta = new TH1F("hEta", "Pseudorapidity;#eta;Entries", 60, -3, 3);
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);
    const std::size_t n = pt->size();
    for (std::size_t j = 0; j < n; ++j) {
        hPt->Fill(pt->at(j));
        hEta->Fill(eta->at(j));
    }
}

Here, after each GetEntry(i), the pointers pt and eta refer to vectors filled with the particle transverse momentum and pseudorapidity for that event. You access them like any standard vector.

Manual reading has several important implications and advantages.

First, you control which branches you actually read. If you are interested in only a few branches, you can disable others to speed up I/O and reduce memory usage:

cpp
t->SetBranchStatus("*", 0);          // disable all
t->SetBranchStatus("energy", 1);     // enable only what you need
t->SetBranchStatus("charge", 1);

You then call SetBranchAddress() only for the enabled branches that you need.

Second, you can embed complex selection and computation logic in your loop. You can compute derived quantities, build new observables from several branches, and apply multi step cuts that would be awkward to express as a single selection string.

For example, computing a derived quantity from two branches:

cpp
Float_t px, py;
t->SetBranchAddress("px", &px);
t->SetBranchAddress("py", &py);
TH1F *hPt = new TH1F("hPt", "p_{T};p_{T};Entries", 100, 0, 100);
Long64_t nEntries = t->GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);
    Float_t pt = std::sqrt(px*px + py*py);  // derived quantity
    hPt->Fill(pt);
}

Third, manual reading is the foundation for more advanced workflows, such as mixing data from several trees, performing complex event selection, or integrating with external libraries. Even if later you move to higher level interfaces, understanding manual reading with SetBranchAddress() will help you reason about performance, memory usage, and the true structure of your data.

Views: 10

Comments

Please login to add a comment.

Don't have an account? Register now!