KAHIBARO
Discord Login Register

20.3. TChain

Combining multiple ROOT files

In many real analyses, your data is naturally split across many ROOT files instead of a single large file. For example, each run of an experiment or each job on a computing cluster may produce its own ROOT file that contains a TTree with the same structure. TChain is ROOT’s solution for treating such a collection of TTrees as if it were one long TTree.

A TChain is a class that inherits from TTree and represents an ordered list of TTrees that all share the same name and compatible branch structure. You work with a TChain almost exactly as you would with a single TTree, but behind the scenes it automatically switches files and trees as you loop over entries.

The basic idea is simple. You create a TChain by specifying the tree name, then you add files that contain a TTree with that name. For example, if each file contains a TTree called "Events", you can write:

cpp
TChain chain("Events");
chain.Add("run1.root");
chain.Add("run2.root");
chain.Add("run3.root");

Now chain behaves like a single TTree whose entries are the concatenation of all entries in the TTrees from "run1.root", then "run2.root", and so on. If all files share a pattern, you can add them in one step:

cpp
TChain chain("Events");
chain.Add("run*.root");

Here ROOT expands the wildcard pattern and adds all matching files in the current directory that contain a TTree named "Events". You can also add TTrees from files in different directories or with different names, as long as they contain a TTree with the same tree name you used when creating the TChain.

TChain also allows you to add a subset of the entries from each file using an optional selection expression, but for most basic use cases you simply add full files. The important point is that every TTree inside the chain must have a compatible branch layout. Branches that do not exist in all files can be handled, but this is more advanced and can complicate your analysis, so for beginners you should aim for exactly matching structures.

Once the chain is built, you set branch addresses and loop over entries in exactly the same way as for a TTree. For instance:

cpp
TChain chain("Events");
chain.Add("data_*.root");
float energy;
chain.SetBranchAddress("energy", &energy);
const Long64_t nEntries = chain.GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
    chain.GetEntry(i);
    // Use 'energy' as usual
}

Here GetEntries() returns the total number of entries over all files and all TTrees in the chain. GetEntry(i) automatically finds which file and which local tree entry correspond to the global index i, opens the correct file if necessary, and loads the data. You do not have to manage file changes yourself.

Because TChain is derived from TTree, you can also use all the high level methods that operate on TTrees. This includes Draw(), Scan(), and many other analysis functions. For example:

cpp
TChain chain("Events");
chain.Add("data_*.root");
// Quick histogram of energy from all files combined
chain.Draw("energy >> hEnergy(100, 0, 1000)");

In this way, TChain integrates multiple ROOT files into a single logical dataset for both manual loops and interactive commands.

A common question is how to check which file or tree you are reading from during a loop. TChain provides methods to access this information. For example:

cpp
TFile *currentFile = chain.GetFile();
TTree *currentTree = chain.GetTree();

You can call these inside your event loop to inspect where the current entry is coming from or to perform file specific actions. You can also get the index of the current tree within the chain using chain.GetTreeNumber().

To summarize the typical workflow: decide the common TTree name across your files, construct a TChain with that name, add your ROOT files, set branch addresses exactly as you would for a TTree, and then analyze the combined dataset using loops or TTree::Draw() style commands. TChain takes care of switching files and keeps track of the global entry numbering for you.

Important rule: A TChain only works reliably when the TTrees it chains have compatible branch structures. You should always ensure that the same branches exist with the same types across all files you add to the chain.

Processing large datasets

TChain becomes particularly powerful when dealing with large datasets that are too big to store in a single file or that are naturally produced as many separate ROOT files. It allows you to scale your analysis from small tests to very large collections of data with minimal changes to your code.

When you analyze a single TTree stored in one file, it is straightforward to loop over its entries or use Draw() to create histograms. With TChain, you can do the same thing across hundreds or thousands of files. The logic of your event loop does not depend on the number of files. You only adjust how you construct the chain and which files you add.

For example, consider an analysis where you want to fill a histogram of an energy variable from a large dataset split across many files:

cpp
TChain chain("Events");
// Add a large number of files
chain.Add("/data/runs/run_*.root");
// Set up branches
float energy;
chain.SetBranchAddress("energy", &energy);
// Create output histogram
TH1F hEnergy("hEnergy", "Energy;E [MeV];Entries", 100, 0, 1000);
// Global event loop across all files
const Long64_t nEntries = chain.GetEntries();
for (Long64_t i = 0; i < nEntries; ++i) {
    chain.GetEntry(i);
    hEnergy.Fill(energy);
}

This code works the same whether you have 1 file or 10,000 files, as long as you have enough disk bandwidth and processing time. ROOT opens and closes files as needed and only keeps the current file in memory. This behavior is the key to handling large datasets that would not fit in memory if loaded all at once.

TChain also interacts well with selective reading of branches, which is important for performance. For very large datasets, you seldom need all branches for a given analysis. You can turn off all branches by default and then enable only the ones you need:

cpp
TChain chain("Events");
chain.Add("data_*.root");
chain.SetBranchStatus("*", 0);          // Disable all branches
chain.SetBranchStatus("energy", 1);     // Enable only 'energy'
float energy;
chain.SetBranchAddress("energy", &energy);
Long64_t nEntries = chain.GetEntries();
// Loop is now lighter because only one branch is read
for (Long64_t i = 0; i < nEntries; ++i) {
    chain.GetEntry(i);
    // Process energy
}

This approach reduces both disk I/O and memory usage, which becomes critical when your data volume is large and you run on shared computing resources or batch systems.

When you work with many files, it is also common to maintain file lists explicitly. Instead of using a wildcard pattern, you may have a text file that lists all ROOT files that belong to your dataset. TChain can read such lists directly:

cpp
TChain chain("Events");
chain.Add("file_list.txt");

In this format, "file_list.txt" contains one file path per line. ROOT reads the list, adds all files to the chain, and you can then process them as usual. This is particularly useful when your files are distributed across different directories or storage systems.

Large datasets often come from multiple production campaigns or from different periods of data taking. As long as the TTree name and branch structure remain compatible, you can mix files from different locations in one TChain. For example:

cpp
TChain chain("Events");
chain.Add("/data/2018/run_*.root");
chain.Add("/data/2019/run_*.root");
chain.Add("/mc/signal/*.root");

If you need to distinguish between the different sources while looping, you can look at the current file name:

cpp
for (Long64_t i = 0; i < chain.GetEntries(); ++i) {
    chain.GetEntry(i);
    TFile *f = chain.GetFile();
    const char *fname = f ? f->GetName() : "";
    // Use 'fname' to check if entry comes from data or simulation, for example
}

For very large analyses, it is often useful to break processing into stages. You might first run a selection step that reads a TChain of raw data, applies cuts, computes derived variables, and writes a new, smaller set of ROOT files with slimmed TTrees. In a second step, you then analyze these reduced files using another TChain. This hierarchical approach limits the amount of data you need to read repeatedly and helps you iterate more quickly on the final analysis.

TChain also interacts well with more modern tools such as RDataFrame, which can take a TChain as input and parallelize the processing automatically if you enable implicit multithreading. In that setup, the TChain provides the unified view of many files, while RDataFrame schedules the event processing across CPU cores. The conceptual role of TChain remains the same: it hides file boundaries and provides a single logical dataset.

When working with very large chains, you should be aware of potential pitfalls. If files are missing or corrupted, Add() may fail silently or print warnings. It is good practice to check how many files were added and how many entries are available:

cpp
TChain chain("Events");
int nFilesAdded = chain.Add("data_*.root");
std::cout << "Files added: " << nFilesAdded << std::endl;
std::cout << "Total entries: " << chain.GetEntries() << std::endl;

You can also inspect the structure of the chained trees using methods like chain.Print() to verify that branches are consistent across all files.

Finally, remember that TChain only concatenates the TTrees; it does not merge histograms or other objects stored in the files. When your dataset is large, you typically use TChain to read event level information from TTrees and then create new histograms, graphs, or output trees that summarize the information you need. Those derived objects can then be saved to new ROOT files with much smaller size.

Important rule: For large datasets, always limit reading to the branches you need and use TChain to stream data file by file. This combination of selective branch activation and file streaming is essential for efficient processing of big ROOT datasets.

Views: 11

Comments

Please login to add a comment.

Don't have an account? Register now!