20.4. Friends
Table of Contents
TTree friends
In many analyses you do not have a single self contained TTree. Instead, information about the same physical events is split across several trees, often in different files. ROOT “friends” provide a way to virtually merge such trees, so you can use all variables together without physically copying or restructuring the data.
A TTree friend is another TTree that ROOT associates entry by entry with a given “master” tree. Once the friendship is established, branches from the friend tree appear as if they were extra branches of the main tree. You can then use them transparently in TTree::Draw, in traditional event loops, and in RDataFrame.
The basic idea is simple. You start with a main TTree, for example:
TTree *t = (TTree*)file->Get("Events");and a second tree that you want to attach as a friend:
TTree *tf = (TTree*)file2->Get("Calib");You then declare the friendship:
t->AddFriend(tf);From that point on, when you run
t->Draw("energy:calib_factor");
ROOT will read the energy branch from the main tree t and the calib_factor branch from the friend tree tf, pairing entries by index (entry 0 with entry 0, entry 1 with entry 1, and so on).
A TTree friend is not a copy or merge of data. It is only a logical link. The underlying trees and files must remain accessible while you use the friendship, and entry pairing must be consistent with how your data were produced.
You can add a friend by passing a tree pointer, or by passing file and tree names as strings. The latter is useful when the friend lives in another file:
t->AddFriend("Calib", "calibration.root");
Here "Calib" is the tree name, and "calibration.root" is the file that contains it. ROOT will open the file when needed.
It is possible to have more than one friend. You can call AddFriend several times, and the main tree t will see all branches from all friends as if they were its own. If branch names clash, you can qualify them with the tree name, for example Calib.calib_factor to pick the correct one.
Using friends with TTree::Draw and similar high level functions is very natural. After the friendship is defined, you simply reference the friend branches by name in the draw expression or in the selection string:
t->Draw("energy*Calib.calib_factor", "qualityFlag && Trig.pass");
Here Calib and Trig could be two different friend trees, each providing a set of branches.
For more explicit processing in a C++ loop, the mechanism is the same. You set branch addresses on the main tree, including branches that actually belong to friends. Once the friendship exists, ROOT takes care of reading the right tree and entry for every event. You only call GetEntry on the main tree:
float energy, calib_factor;
t->SetBranchAddress("energy", &energy);
t->SetBranchAddress("calib_factor", &calib_factor); // from friend
for (Long64_t i = 0; i < t->GetEntries(); ++i) {
t->GetEntry(i); // also reads friend entry i
float energy_calibrated = energy * calib_factor;
// analysis code
}All the complexity of moving across multiple files and TTrees is hidden behind the friendship relation.
Important rule: when trees are friends, you must always drive the event loop with the main tree. Do not call GetEntry on the friend trees yourself, or you will break the synchronization that ROOT maintains.
Combining related datasets
Friends are especially useful to combine related datasets that were produced at different stages, by different people, or with different event content. The key is that all trees share a common way to identify corresponding entries.
The simplest and most common scenario is one to one alignment by entry index. This is typically the case when a downstream processing step reads a source TTree and writes a new TTree in the same order, one output entry per input entry. If you have
TTree *tReco = (TTree*)fReco->Get("Events");
TTree *tCalib = (TTree*)fCalib->Get("Events");
tReco->AddFriend(tCalib);
then entry i in tReco corresponds to entry i in tCalib. When you draw or loop over tReco, you can use reconstruction level variables and calibration constants together without any explicit joining logic.
In many workflows you maintain a “slimmed” or “derived” tree that contains only high level variables, and you keep the original tree with detailed detector level quantities. Instead of duplicating all variables into the derived tree, you can keep them in separate trees and connect them with friends. This saves space in files and makes it easy to regenerate or replace one part of the dataset without touching the other.
Sometimes trees are not aligned by simple entry index, for example when you have applied event selection and removed some entries in one of the trees. In such cases you need an explicit event identifier, such as a run number and event number pair, or any unique ID stored as branches in both trees. ROOT supports friend relationships based on variable lookup instead of raw entry index. You do this by defining an index on the trees:
tMain->BuildIndex("run", "event");
tFriend->BuildIndex("run", "event");
tMain->AddFriend(tFriend);
With this setup, when ROOT reads entry i from tMain, it uses run and event to find the matching entry in the friend tree. This allows you to combine datasets that were produced independently, as long as they share a consistent event identifier.
When combining datasets with friends, ensure that:
- There is a well defined mapping between entries, either by index or by explicit IDs.
- The mapping is consistent across the full range of entries.
- You do not overwrite or modify branches in a way that breaks the assumed mapping.
If these conditions fail, you will silently combine mismatched events and obtain incorrect physics results.
In practice, you often work with several friends at once. For example, a main physics tree might have friends providing trigger decisions, detector calibrations, pileup information, and machine learning scores. You can attach them all:
t->AddFriend("Trig", "triggers.root");
t->AddFriend("Calib", "calibration.root");
t->AddFriend("ML", "ml_scores.root");Then in a single analysis step you can cut on trigger decisions, apply calibrations, and read classifier outputs:
t->Draw("ML.score",
"Trig.pass && energy*Calib.factor > 1.0");Although the data are physically stored in multiple ROOT files, the analysis code sees a single logical dataset.
RDataFrame also supports friends. If t already has friends attached, constructing a dataframe from it gives you transparent access to all branches:
ROOT::RDataFrame df(*t);
auto h = df.Filter("Trig.pass")
.Define("Ecalib", "energy*Calib.factor")
.Histo1D("Ecalib");This approach keeps individual production steps modular and still allows you to combine everything into a coherent analysis view.
A useful pattern is to use one TTree per logical layer of processing, save each layer in its own file, and then connect them with friends when doing the final analysis. This keeps files smaller and more manageable, and if you need to redo a calibration or recompute a set of derived variables, you only regenerate the corresponding friend file, not the full dataset.
When working with large datasets you should also be aware of performance. Each friend may live in a separate file, so reading an event may require several I/O operations. It is usually better to keep related friend trees in as few files as practical and to avoid unnecessary friends that you do not use in your final analysis selections or histograms.
Finally, remember that friendships are not permanent modifications to the data. You create them at analysis time in your code or in an interactive ROOT session. This makes them a flexible tool to combine related datasets exactly as needed for a given analysis, without changing the underlying ROOT files.
Views: 11
KAHIBARO