KAHIBARO
Discord Login Register

14.2 Event Loops

Reading events

In event based analysis you almost always start from a TTree that stores one entry per event. Each entry typically represents a detector trigger, a collision, or one simulated event. An event loop means iterating over all entries in a TTree, reading their contents, and performing calculations or selections.

The essential pattern for reading events manually is always the same. You open the ROOT file, get the TTree, connect C++ variables to the branches, then loop over all entries and call GetEntry in each iteration.

A minimal structure looks like this:

cpp
void loop_example() {
  TFile *f = TFile::Open("data.root");
  TTree *t = (TTree*)f->Get("Events");
  // Variables to hold branch data
  float energy;
  int   nHits;
  // Connect branches to variables
  t->SetBranchAddress("energy", &energy);
  t->SetBranchAddress("nHits",  &nHits);
  Long64_t nEntries = t->GetEntries();
  for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);  // Read event i into energy and nHits
    // Work with this event here
  }
  f->Close();
}

The call SetBranchAddress tells the TTree where to store the data when you call GetEntry. Each call to GetEntry(i) fills all connected variables with the content of entry i. Inside the loop the variables behave like regular C++ variables. You can read them, combine them, and use them to decide which events to keep or reject.

You should treat the event index as opaque. In most analyses you loop from 0 to nEntries-1 in order. For more advanced workflows you can also restrict the loop to a subset of entries or process entries in blocks, but the basic idea is always entry by entry access via GetEntry.

Although ROOT offers high level interfaces like TTree::Draw and RDataFrame, learning to write a manual event loop is important. It shows clearly when data are read, when calculations happen, and how often histograms are filled. This control is essential once you build more complex analyses.

In every manual event loop you must:

  1. Connect all needed branches using SetBranchAddress.
  2. Call GetEntry(i) once per event inside the loop.
  3. Use the filled variables only after GetEntry has been called.

Applying calculations

Once you can read each event, the next step is to compute quantities derived from the raw branches. These derived quantities can be simple arithmetic combinations or more involved physics observables.

For each event you typically proceed as follows. First, call GetEntry(i) to load the event. Next, apply basic checks such as discarding obviously invalid or incomplete events. Then compute the quantities you care about and finally decide whether the event should contribute to a given histogram or result.

Here is a simple pattern that computes a derived variable from branches:

cpp
void loop_with_calculations() {
  TFile *f = TFile::Open("data.root");
  TTree *t = (TTree*)f->Get("Events");
  float energy1, energy2;
  float time1, time2;
  t->SetBranchAddress("energy1", &energy1);
  t->SetBranchAddress("energy2", &energy2);
  t->SetBranchAddress("time1",   &time1);
  t->SetBranchAddress("time2",   &time2);
  Long64_t nEntries = t->GetEntries();
  for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);
    // Derived quantities
    float totalEnergy = energy1 + energy2;
    float deltaT      = time2 - time1;
    // Simple quality checks
    if (totalEnergy <= 0) continue;
    if (fabs(deltaT) > 100.0) continue;
    // Use totalEnergy and deltaT for further analysis or histogramming
  }
  f->Close();
}

The calculations themselves can use ordinary C++ arithmetic and functions from <cmath> and ROOT helper classes. For vector like quantities you may use ROOT classes such as TLorentzVector or TVector3. For example, constructing a four vector per event and then asking for invariant mass fits naturally into this per event pattern, but the detailed physics belongs to later chapters.

It is important to keep calculations inside the event loop independent between events. Each iteration must rely only on the data loaded for that specific event. Do not accidentally carry values over from a previous iteration unless you explicitly want cumulative sums or counters.

Another practical point is to separate calculations that are constant from those that depend on the event. Calibration constants, scale factors, or geometry parameters should be defined outside the loop. Inside the loop you reuse these constants for each event. This keeps the loop code simpler and avoids unnecessary repeated computations.

Within an event loop:

  1. Call GetEntry(i) before any calculations for event i.
  2. Use only the current event data to compute derived quantities.
  3. Place constant parameters and calibration factors outside the loop.

Filling histograms

The ultimate goal of most event loops is to summarize event by event information into histograms or similar objects. The pattern is straightforward. You create and configure histograms once before the loop, then fill them for each selected event inside the loop, and finally inspect or save them after the loop has finished.

A minimal example ties everything together:

cpp
void loop_fill_histograms() {
  TFile *f = TFile::Open("data.root");
  TTree *t = (TTree*)f->Get("Events");
  float energy;
  t->SetBranchAddress("energy", &energy);
  // Create histograms before the loop
  TH1F *hEnergy      = new TH1F("hEnergy",      "Energy;E [MeV];Events", 100, 0, 1000);
  TH1F *hEnergyHigh  = new TH1F("hEnergyHigh",  "High energy;E [MeV];Events", 100, 500, 1500);
  Long64_t nEntries = t->GetEntries();
  for (Long64_t i = 0; i < nEntries; ++i) {
    t->GetEntry(i);
    // Basic selection
    if (energy <= 0) continue;
    // Fill general spectrum
    hEnergy->Fill(energy);
    // Fill second histogram only for high energy events
    if (energy > 600) {
      hEnergyHigh->Fill(energy);
    }
  }
  // After the loop, you can draw or save the histograms
  TCanvas *c1 = new TCanvas("c1", "Energy spectra", 800, 600);
  hEnergy->Draw();
  c1->SaveAs("energy_spectrum.png");
  f->Close();
}

You can also fill histograms with weights. In that case Fill(x, w) increments the bin corresponding to x by the weight w instead of 1. This can be useful for efficiency corrections, luminosity scaling, or when input events already carry weights.

When you use weighted fills and you care about proper statistical uncertainties, you should tell the histogram to store the sum of squared weights. This is done once, before filling, with Sumw2().

cpp
TH1F *hWeighted = new TH1F("hWeighted", "Weighted distribution;X;Weighted events", 50, 0, 1);
hWeighted->Sumw2();  // enable proper error handling
for (Long64_t i = 0; i < nEntries; ++i) {
  t->GetEntry(i);
  double x      = /* some derived quantity */;
  double weight = /* event weight */;
  hWeighted->Fill(x, weight);
}

This event loop pattern extends naturally to multiple histograms and multiple TTrees. You can create several histograms before the loop, fill each one under its specific conditions inside the same loop, and at the end you can write all of them to a ROOT file. The key is always to keep histogram creation outside the loop and to call Fill only after you have computed and validated the quantities for a given event.

For histogramming inside event loops:

  1. Create and configure histograms before the event loop.
  2. Fill histograms only after selection and calculations for each event.
  3. Call Sumw2() before the loop if you will use weighted fills and want correct bin errors.

Views: 10

Comments

Please login to add a comment.

Don't have an account? Register now!