Warning, /tutorial-analysis/episodes/03-analysis.md is written in an unsupported language. File is not indexed.
0001 ---
0002 title: "Analyzing the Reconstruction Output"
0003 teaching: 20
0004 exercises: 40
0005 ---
0006
0007 ::::::::::::::::::::::::::::::::::::::::::::: questions
0008
0009 - How does one utilize the reconstruction output trees to do an analysis?
0010
0011 :::::::::::::::::::::::::::::::::::::::::::::
0012
0013 ::::::::::::::::::::::::::::::::::::::::::::: objectives
0014
0015 - Become familiar with methods for reading the trees.
0016 - Understand how to access truth/particle information.
0017 - Find track efficiency and resolution.
0018
0019 :::::::::::::::::::::::::::::::::::::::::::::
0020
0021 So far, we have only looked at (and plotted) some information from our file interactively. This is very useful and can help us identify the variables we want to deal with. However, we can't really use these techniques to conduct a full analysis of the data. To do so, we typically use a script or macro. In this part of the tutorial, we will create a script that we can use to do a relatively straightforward analysis of our file.
0022
0023 ::::::::::::::::::::::::::::::::::::::::::::: callout
0024
0025 Note:
0026
0027 - The [branch dictionary](../learners/branch-dictionary.md) outlines all of the branches we will need to utilise in this section.
0028 - If you want, you can prune the branches you don't need from the input file using the [TreePrune.C script](../learners/tree-pruning-script.md).
0029
0030 :::::::::::::::::::::::::::::::::::::::::::::
0031
0032 ## Reading the Output Trees
0033
0034 The simulation output trees are "flat" in the sense that there is no event class structure embedded within the tree and no additional libraries are needed to handle the output. Therefore, the end user can simply read the values stored in each branch using whatever method/workflow they are most comfortable with. Examples of several common methods for reading the trees are provided below. We will see a ROOT TTreeReader based example using a ROOT macro and a python/uproot based version. There is also an example using the (relatively) new RDataFrame class of ROOT. During the tutorial, you should try the exercise using whichever language you feel most comfortable with. Five different approaches are currently provieded:
0035
0036 - [TTreeReaders](#sample-analysis-with-root-ttreereader-track-efficiency-and-resolution) - ROOT/C based
0037 - [Python/Uproot - Pythonic](#sample-analysis-with-python-uproot-pythonic-method-track-efficiency-and-resolution) - A pythonic based appoach using arrays directly
0038 - [Python/Uproot - ROOT/Pyroot](#sample-analysis-with-python-uproot-root-pyroot-style-track-efficiency-and-resolution) - An approach using Pyroot, halfway house between C and python
0039 - [ROOT RDataFrames](#root-rdataframes) - An approach using RDataFrames
0040 - [PODIO](#podio-direct-analysis) - An approach using the Plane Old Data IO (PODIO) approach. Use the flat datastructure directly
0041
0042 ## Sample Analysis with ROOT TTreeReader: Track Efficiency and Resolution
0043
0044 As a sample exercise to become familiar with the simulation output and how to use it in a realistic setting, we will find the tracking eficiency and resolution. We will need to access the reconstructed track information and the truth particle information and we will have to associate the individual tracks and particles to one another.
0045
0046 Before we begin, we should create a skeleton macro to handle file I/O. For the `TTreeReader` example, we will use a simple ROOT macro. Using your favorite text editor, create a file with a name like `trackAnalysis.C` or something similar and copy in the following code:
0047
0048 ```c++
0049 void trackAnalysis(TString infile="path_to_your_simu_file")
0050 {
0051 // Set output file for the histograms
0052 TFile *ofile = TFile::Open("out.hist.root","RECREATE");
0053
0054 // Analysis code will go here
0055
0056 ofile->Write(); // Write histograms to file
0057 ofile->Close(); // Close output file
0058 }
0059 ```
0060
0061 We will need momentum, generator status, and particle species information for the truth particles and momentum information for the reconstructed tracks. The reconstructed track information can be accessed from two different branches: CentralCKFTrackParameters and ReconstructedChargedParticles. We can access these branches using a TTreeReaderArray.
0062
0063 ::::::::::::::::::::::::::::::::::::::::::::: callout
0064
0065 ROOT TTreeReaderArrays:
0066
0067 TTreeReader and the associated TTreeReaderArray is a simple interface for reading data from a TTree. The class description and examples can be seen in [the ROOT TTreeReader class documentation](https://root.cern/doc/v630/classTTreeReader.html). To instantiate the reader and access values from a given branch (e.g. the MCParticles branch), one would use the following calls:
0068
0069 ```c++
0070 // Set up input file chain
0071 TChain *mychain = new TChain("events");
0072 mychain->Add(infile);
0073
0074 // Initialize reader
0075 TTreeReader tree_reader(mychain);
0076
0077 // Access whatever data-members you need
0078 TTreeReaderArray<int> partGenStat(tree_reader, "MCParticles.generatorStatus");
0079 TTreeReaderArray<float> partMomX(tree_reader, "MCParticles.momentum.x");
0080 ...
0081 ```
0082
0083 The branches and their members can be viewed by opening a file with TBrowser (`new TBrowser()`) from within ROOT. Once you have defined the `TTreeReaderArray` objects for the data-members you want to look at, you can loop over the events and the members within that event:
0084
0085 ```c++
0086 while(tree_reader.Next()) { // Loop over events
0087 for(unsigned int i=0; i<partGenStat.GetSize(); i++) // Loop through particles in the event
0088 {
0089 int particleStatus = partGenStat[i]; // Access data-members as you would an array
0090 float particleXMomentum = partMomX[i]; // partMomX should have same number of entries as partGenStat because they are in the same branch
0091 ...
0092 }
0093 }
0094 ```
0095
0096 All members of the same branch should have the same number of entries, so it is sufficient to use any member of the branch to set the limit of your loop.
0097
0098 :::::::::::::::::::::::::::::::::::::::::::::
0099
0100 We will proceed using the ReconstructedChargedParticles branch as this will give us a chance to practice using associations, copy the following lines into your analysis macro.
0101
0102 ```c++
0103 // Set up input file chain
0104 TChain *mychain = new TChain("events");
0105 mychain->Add(infile);
0106
0107 // Initialize reader
0108 TTreeReader tree_reader(mychain);
0109
0110 // Get Particle Information
0111 TTreeReaderArray<int> partGenStat(tree_reader, "MCParticles.generatorStatus");
0112 TTreeReaderArray<double> partMomX(tree_reader, "MCParticles.momentum.x");
0113 TTreeReaderArray<double> partMomY(tree_reader, "MCParticles.momentum.y");
0114 TTreeReaderArray<double> partMomZ(tree_reader, "MCParticles.momentum.z");
0115 TTreeReaderArray<int> partPdg(tree_reader, "MCParticles.PDG");
0116
0117 // Get Reconstructed Track Information
0118 TTreeReaderArray<float> trackMomX(tree_reader, "ReconstructedChargedParticles.momentum.x");
0119 TTreeReaderArray<float> trackMomY(tree_reader, "ReconstructedChargedParticles.momentum.y");
0120 TTreeReaderArray<float> trackMomZ(tree_reader, "ReconstructedChargedParticles.momentum.z");
0121
0122 // Get Associations Between MCParticles and ReconstructedChargedParticles
0123 TTreeReaderArray<int> recoAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_rec.index");
0124 TTreeReaderArray<int> simuAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_sim.index");
0125 ```
0126
0127 The last two lines encode the association between a ReconstructedChargedParticle and an MCParticle where the matching is determined by the EICrecon [reconstruction algorithms](https://github.com/eic/EICrecon/tree/main/src/algorithms/reco) which generate the ReconstructedChargedParticle objects.
0128
0129 ::::::::::::::::::::::::::::::::::::::::::::: callout
0130
0131 Compiling ROOT Macros:
0132
0133 - If you are analysing a large number of events, you may wish to compile your macro to increase throughput. An example of how you can create and compile a root macro is included in the [Exercise Scripts section](../learners/exercise-scripts.md#compiled-root-scripts).
0134
0135 :::::::::::::::::::::::::::::::::::::::::::::
0136
0137 ### Efficiency Analysis
0138
0139 ::::::::::::::::::::::::::::::::::::::::::::: callout
0140
0141 Hint:
0142 Refer to [the script template](../learners/exercise-scripts.md#efficiencyanalysisc) if you're having trouble putting things in the right place.
0143
0144 :::::::::::::::::::::::::::::::::::::::::::::
0145
0146 Now that we have access to the data we need we will begin constructing our efficiency plots, starting with efficiency as a function of the true particle pseudorapidity. The basic strategy is outlined below:
0147
0148 1. Loop over all events in the file
0149 2. Within each event, loop over all stable charged particles
0150 3. Identify the ReconstructedChargedParticle (if any) associated with the truth particle we are looking at
0151 4. Create and fill the necessary histograms
0152
0153 Here is the code to implement these steps:
0154
0155 ```c++
0156 // Define Histograms
0157 TH1D *partEta = new TH1D("partEta","Eta of Thrown Charged Particles;Eta",100,-5.,5.);
0158 TH1D *matchedPartEta = new TH1D("matchedPartEta","Eta of Thrown Charged Particles That Have Matching Track",100,-5.,5.);
0159
0160 TH1D *matchedPartTrackDeltaR = new TH1D("matchedPartTrackDeltaR","Delta R Between Matching Thrown and Reconstructed Charged Particle",5000,0.,5.);
0161
0162 while(tree_reader.Next()) { // Loop over events
0163
0164 for(unsigned int i=0; i<partGenStat.GetSize(); i++) // Loop over thrown particles
0165 {
0166 if(partGenStat[i] == 1) // Select stable thrown particles
0167 {
0168 int pdg = TMath::Abs(partPdg[i]);
0169
0170 if(pdg == 11 || pdg == 13 || pdg == 211 || pdg == 321 || pdg == 2212) // Look at charged particles (electrons, muons, pions, kaons, protons)
0171 {
0172 TVector3 trueMom(partMomX[i],partMomY[i],partMomZ[i]);
0173
0174 float trueEta = trueMom.PseudoRapidity();
0175 float truePhi = trueMom.Phi();
0176
0177 partEta->Fill(trueEta);
0178
0179 // Loop over associations to find matching ReconstructedChargedParticle
0180 for(unsigned int j=0; j<simuAssoc.GetSize(); j++)
0181 {
0182 if(simuAssoc[j] == i) // Find association index matching the index of the thrown particle we are looking at
0183 {
0184 TVector3 recMom(trackMomX[recoAssoc[j]],trackMomY[recoAssoc[j]],trackMomZ[recoAssoc[j]]); // recoAssoc[j] is the index of the matched ReconstructedChargedParticle
0185
0186 // Check the distance between the thrown and reconstructed particle
0187 float deltaEta = trueEta - recMom.PseudoRapidity();
0188 float deltaPhi = TVector2::Phi_mpi_pi(truePhi - recMom.Phi());
0189 float deltaR = TMath::Sqrt(deltaEta*deltaEta + deltaPhi*deltaPhi);
0190
0191 matchedPartTrackDeltaR->Fill(deltaR);
0192
0193 matchedPartEta->Fill(trueEta); // Plot the thrown eta if a matched ReconstructedChargedParticle was found
0194 }
0195 }
0196 }
0197 }
0198 }
0199 }
0200 ```
0201
0202 We should now have everything we need to find the track efficiency as a function of pseudorapidity. To run the macro and produce an output file containing the histograms we defined, simply type `root -l -q trackAnalysis.C`. After the macro runs, you can open the root file to inspect the histograms. The efficiency can be found by taking the ratio of matchedPartEta over partEta.
0203
0204 ::::::::::::::::::::::::::::::::::::::::::::: callout
0205
0206 Question:
0207
0208 - Do the histogram ranges make sense?
0209 - We plot the distance between thrown and reconstructed charged partices, does this distribution look reasonable?
0210 - When filling the matchedPartEta histogram (the numerator in our efficiency), why do we use again the true thrown eta instead of the associated reconstructed eta?
0211
0212 :::::::::::::::::::::::::::::::::::::::::::::
0213
0214 ::::::::::::::::::::::::::::::::::::::::::::: challenge
0215
0216 ## Exercise
0217
0218 For all **scattered electrons**, **charged pions** and **protons** in our events:
0219
0220 - Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
0221 - Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
0222 - Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
0223
0224 ::::::::::::::: solution
0225
0226 Build the efficiency exactly as for eta, but fill numerator/denominator histograms in the relevant
0227 kinematic variable and divide at the end. Select each species by its PDG code (11, 211, 2212) and
0228 require `generatorStatus == 1`. A momentum acceptance cut is usually needed to avoid dividing by
0229 near-empty bins at very low momentum. For the 2-D correlations, fill 2-D histograms of the thrown
0230 quantity (denominator) and the matched-thrown quantity (numerator) and divide. See the
0231 [efficiency script template](../learners/exercise-scripts.md#efficiencyanalysisc) for a worked
0232 version.
0233
0234 :::::::::::::::
0235
0236 :::::::::::::::::::::::::::::::::::::::::::::
0237
0238 ### Resolution Analysis
0239
0240 ::::::::::::::::::::::::::::::::::::::::::::: callout
0241
0242 Hint:
0243 Refer to [the script template](../learners/exercise-scripts.md#resolutionanalysisc) if you're having trouble putting things in the right place.
0244
0245 :::::::::::::::::::::::::::::::::::::::::::::
0246
0247 Next, we will look at track momentum resolution, that is, how well the momentum of the reconstructed track matches that of the thrown particle. We should have all of the "infrastructure" we need in place to do the analysis, we just need to define the appropriate quantities and make the histograms. It only makes sense to define the resolution for tracks and particles which are associated with one another, so we will work within the loop over associations. Define the resolution expression and fill a simple histogram:
0248
0249 ```c++
0250 TH1D *trackMomentumRes = new TH1D("trackMomentumRes","Track Momentum Resolution",2000,-10.,10.);
0251 ...
0252 // Loop over associations to find matching ReconstructedChargedParticle
0253 for(unsigned int j=0; j<simuAssoc.GetSize(); j++)
0254 {
0255 if(simuAssoc[j] == i) // Find association index matching the index of the thrown particle we are looking at
0256 {
0257 ...
0258 double momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag();
0259
0260 trackMomentumRes->Fill(momRes);
0261 }
0262 }
0263 ```
0264
0265 While this plot will give us a sense of what the tracking resolution is, we don't expect the resolution to be constant for all momenta or eta. We can get a more complete picture by plotting the resolution as a function of different kinematic quantities.
0266
0267 ::::::::::::::::::::::::::::::::::::::::::::: challenge
0268
0269 ## Exercise
0270
0271 For all **scattered electrons**, **charged pions** and **protons** in our events:
0272
0273 - Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
0274
0275 ::::::::::::::: solution
0276
0277 Inside the association loop, compute `momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()` and
0278 fill a 2-D histogram with the true momentum (or true pseudorapidity) on one axis and `momRes` on the
0279 other, once per matched particle of the chosen species. Profiling these 2-D histograms (or taking
0280 the width of `momRes` in slices) gives the resolution as a function of the kinematic variable. See
0281 the [resolution script template](../learners/exercise-scripts.md#resolutionanalysisc).
0282
0283 :::::::::::::::
0284
0285 :::::::::::::::::::::::::::::::::::::::::::::
0286
0287 ::::::::::::::::::::::::::::::::::::::::::::: callout
0288
0289 Question:
0290
0291 - Will the histogram ranges for each particle species be the same?
0292 - Could we present the resolution values in a more understandable way?
0293
0294 :::::::::::::::::::::::::::::::::::::::::::::
0295
0296
0297 ## Sample Analysis with Python uproot Pythonic Method: Track Efficiency and Resolution
0298
0299 ::::::::::::::::::::::::::::::::::::::::::::: callout
0300
0301 For some examples of using uproot to access information in .root files, please consult [this notebook](https://github.com/eic/HSF-India/blob/main/Working_With_Uproot/Working_With_Uproot_Standalone.ipynb) which can be run in Google Collab.
0302
0303 :::::::::::::::::::::::::::::::::::::::::::::
0304
0305 If you are more familiar with python than you are with C/C++, you might find that using a python based root macro is easier for you. Outlined below are sample blocks of code for creating and running a python based analysis script.
0306
0307 With python, some tasks become easier, e.g. string manipulation and writing to (non ROOT) files.
0308
0309 Before we begin, we should create a skeleton macro to handle file I/O. For this example, we will make a simple python script. Using your favourite editor, create a file with a name like `trackAnalysis.py` or something similar and copy in the following code:
0310
0311 ```python
0312 #! /usr/bin/python
0313 # Import some relevant packages
0314 import uproot as up
0315 import awkward as ak
0316 import numpy as np
0317 import pandas as pd
0318 import matplotlib as mpl
0319 import matplotlib.ticker as ticker
0320 import matplotlib.cm as cm
0321 import matplotlib.pylab as plt
0322 import scipy, vector, os
0323 from XRootD import client
0324 from scipy import stats
0325 from matplotlib import pyplot as plt
0326 from matplotlib.gridspec import GridSpec
0327 from matplotlib import colors as colours
0328
0329 # Set some matplot lib features
0330 plt.rcParams['ytick.direction'] = 'in'
0331 plt.rcParams['xtick.direction'] = 'in'
0332 plt.rcParams['xaxis.labellocation'] = 'right'
0333 plt.rcParams['yaxis.labellocation'] = 'top'
0334 plt.rcParams["figure.figsize"] = (16,9)
0335 kP6 = ['#5790fc','#f89c20','#e42536','#964a8b','#9c9ca1','#7a21dd'] # Set ROOT kP6 colours - see https://root.cern.ch/doc/v636/classTColor.html
0336
0337 # Open our file
0338 fname = "INPUT_FILE.root"
0339 if os.path.isfile(fname):
0340 file=up.open(fname)
0341 else:
0342 print("Error opening file - ", fname, " check your fname variable!")
0343
0344 # Open the tree
0345 tree = file['events']
0346
0347 # Convert relevant branches to arrays
0348 MCPartBr = tree["MCParticles"].arrays()
0349
0350 # Define some filters
0351
0352 # Use filters to manipulate data
0353
0354 # Create and write some plots
0355 ```
0356
0357 Make sure you change the input file name to match whatever you saved your file as earlier.
0358
0359 Note that we are using the module uproot to access the data here. See [further documentation here](https://masonproffitt.github.io/uproot-tutorial/03-trees/index.html). You may also need some of the other included packages too.
0360
0361 We will use uproot a little bit like we use the TTreeReader in the other example. We can define the branches we want and assign them to arrays with uproot.
0362 We can do this via:
0363 ```python
0364 # Open input file and define branches we want to look at with uproot
0365 fname = "INPUT_FILE.root" # Your file name
0366 file=up.open(fname)
0367 tree = file['events']
0368 # Convert relevant branches to arrays
0369 MCPartBr = tree["MCParticles"].arrays()
0370 # If we want, convert a specific branch element to an array
0371 partPdg = tree["MCParticles.PDG"].array()
0372 ```
0373 Uproot effectively takes the information in the tree, and turns it into an array. We can then access and manipulate this array in the same way that we can with any array in python.
0374
0375 ::::::::::::::::::::::::::::::::::::::::::::: callout
0376
0377 Warning: Note that if you are using an older version of uproot (v2.x.x), you will need to access the branches slightly differently via -
0378
0379 ```python
0380 partGenStat = tree.array("MCParticles.generatorStatus")
0381 ```
0382
0383 :::::::::::::::::::::::::::::::::::::::::::::
0384
0385 Once you create your script and add the template code in, you can try running it with``python3 trackAnalysis.py`` or ``python trackAnalysis.py``. At the moment, it shouldn't *do* anything, but we can change that!
0386
0387 Try assigning a quantity to an array, such as the MC particles PDG values above and printing some values of that array. Or, perhaps try printing the length of that array. We could also quickly make a plot of the values with -
0388
0389 ```python
0390 plt.hist(ak.flatten(partPdg),alpha=0.75, color=kP6[0])
0391 plt.savefig("TestOut.png", dpi = (160))
0392 plt.clf # Clear figure
0393 ```
0394 The script should now write out a figure, ``TestOut.png`` when you run it, showing the MC PDG values of entries in the file.
0395
0396 ::::::::::::::::::::::::::::::::::::::::::::: callout
0397
0398 We did not specify a number of bins or a range, so our plot looks a bit odd. What might be a useful range and number of bins to use here?
0399 We can specify our number of bins and our range with -
0400
0401 ```python
0402 plt.hist(ak.flatten(partPdg),bins=NBins, range=(X,Y),alpha=0.75, color=kP6[0])
0403 ```
0404
0405 With NBins being our number of bins and X and Y being our min/max range - think carefully about these numbers!
0406
0407 :::::::::::::::::::::::::::::::::::::::::::::
0408
0409 Note that we don't really need to define individual arrays either, we can just directly access the array we want once we've converted the branch to a series of arrays -
0410
0411 ```python
0412 MCPartBr = tree["MCParticles"].arrays()
0413 plt.hist(ak.flatten(MCPartBr["MCParticles.PDG"]),alpha=0.75, color=kP6[0])
0414 plt.savefig("TestOut.png", dpi = (160))
0415 ```
0416
0417 ::::::::::::::::::::::::::::::::::::::::::::: callout
0418
0419 Note:
0420 Remember to call:
0421
0422 ```python
0423 plt.clf() # Clear figure
0424 ```
0425
0426 After a figure to avoid drawing on the same plot.
0427
0428 :::::::::::::::::::::::::::::::::::::::::::::
0429
0430 We can also define and apply filters to our arrays as we plot or print from them -
0431
0432 ```python
0433 # We will filter on the particle status. Generator status == 1 corresponds to a stable particle (as opposed to a beam or intermediate particle) that we could detect in our detector. 4 is for beam particles
0434 BoolStable=(MCPartBr["MCParticles.generatorStatus"]==1) # This filter is actually an array of booleans. Any where the generator status for a particle == 1 will return true
0435 plt.hist(ak.flatten(MCPartBr["MCParticles.PDG"][BoolStable]),alpha=0.75, color=kP6[0]) # Apply filter to PDG array as we plot it. Only stable particles will now be plotted
0436 plt.savefig("TestOut2.png", dpi = (160))
0437 ```
0438
0439 And we can also combine conditions in our filters -
0440
0441 ```python
0442 BoolStablePos=((MCPartBr["MCParticles.generatorStatus"]==1) & (MCPartBr["MCParticles.charge"]>0)) # Create a filter to select out stable, positively chrarged particles
0443 plt.hist(ak.flatten(MCPartBr["MCParticles.PDG"][BoolStablePos]),alpha=0.75, color=kP6[0]) # Apply filter to PDG array as we plot it. Only stable particles will now be plotted
0444 plt.savefig("TestOut3.png", dpi = (160))
0445 ```
0446
0447 ::::::::::::::::::::::::::::::::::::::::::::: callout
0448
0449 We did not specify a number of bins or a range, so our plot looks a bit odd. What might be a useful range and number of bins to use here?
0450
0451 :::::::::::::::::::::::::::::::::::::::::::::
0452
0453 ### Efficiency Analysis
0454
0455 ::::::::::::::::::::::::::::::::::::::::::::: callout
0456
0457 Hint:
0458 Refer to [the script template](../learners/exercise-scripts.md#pythonic_efficiencyanalysispy) if you're having trouble putting things in the right place.
0459
0460 :::::::::::::::::::::::::::::::::::::::::::::
0461
0462 Our approach here is a bit different to the TTreeReader example, but we will still need to utilise our simulation and reconstruction association IDs. We can access them via -
0463
0464 ```python
0465 RecoAssocRec = tree['_ReconstructedChargedParticleAssociations_rec'].arrays()
0466 RecoAssocSim = tree['_ReconstructedChargedParticleAssociations_sim'].arrays()
0467 RecID=RecoAssocRec['_ReconstructedChargedParticleAssociations_rec.index'] # Array of reconstructed IDs
0468 SimID=RecoAssocSim['_ReconstructedChargedParticleAssociations_sim.index'] # Array of simulated IDs
0469 ```
0470
0471 These are arrays of indices which map our simulated data to events which have actually been reconstructed in our simulation. We can use these to index our arrays and pick out *only* events where a simulated particle has a matching reconstructed particle (OR vice versa, where a reconstructed particle has a matching simulated particle). Note that when we index by these particles, we also need to make sure any filters are also indexed appropriately. E.g.
0472
0473 ```python
0474 BoolChargeTrack = ((abs(MCPartBr["MCParticles.charge"])!=0) & (MCPartBr["MCParticles.generatorStatus"]==1)) # A filter to select out charged stable particles in the MC data
0475 ```
0476
0477 If we apply this filter to an array which has been indexed by the Simulation ID, we will run into problems -
0478
0479 ```python
0480 BoolChargeTrack = ((abs(MCPartBr["MCParticles.charge"])!=0) & (MCPartBr["MCParticles.generatorStatus"]==1)) # A filter to select out charged stable particles in the MC data
0481 MatchPDG = MCPartBr["MCParticles.PDG"][SimID] # An array of the MC PDG values for all MC particles with a matching reconstructed particle.
0482 # print(MatchPDG[BoolChargeTrack]) # Will return an error, arrays don't match sizing!
0483 ```
0484
0485 We could either -
0486
0487 1. Create a new filter which is explicitly indexed by the SimID's
0488 2. Index our previous filter by the SimID as we use it
0489
0490 Both should return the same answer -
0491
0492 ```python
0493 print(MatchPDG[BoolChargeTrack[SimID]]) # Explicitly index our filter by SimID first
0494 BoolChargeTrackMatch = ((abs(MCPartBr["MCParticles.charge"][SimID])!=0) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1)) # A filter to select out charged stable particles in the MC data
0495 print(MatchPDG[BoolChargeTrackMatch]) # Use a newly defined filter which has been indexed by the SimID
0496 ```
0497 To select out reconstructed particles that have a matching simulated particle, we can index by ``RecID`` in a similar way. **Note that we need to be careful what we conclude from our analysis if we match particles in this way. We are "cheating" in the sense that we are directly matching the truth to what we reconstruct. We cannot do this in a real experiment.**
0498
0499 We may want to group some of our quantities together as vectors for easy manipulation. For example, we could create an array of vectors corresponding to our charged particles -
0500
0501 ```python
0502 MC_Parts = vector.zip({'px': MCPartBr["MCParticles.momentum.x"], 'py': MCPartBr["MCParticles.momentum.y"], 'pz': MCPartBr["MCParticles.momentum.z"]})
0503 ```
0504 We can filter and index this like any other array. Creating 3 or 4-vectors in this way is useful as we can then use various functions to extract information from our vectors. For example, we can easily get -
0505
0506 - *Pseudorapidity* - Eta
0507 - *Polar angle* - Theta they make wrt the origin (in the lab frame, our bunch crossing point) - *Note, this is in radians by default*
0508 - *Transverse momentum - PT
0509 - ...
0510
0511 ```python
0512 print(MC_Parts.eta)
0513 print(MC_Parts.theta)
0514 print(MC_Parts.pt)
0515 ```
0516
0517 Now, we can use what we know to determine and plot some simple efficiency graphs for our reconstructed data. Efficiency is a measure of the probability that we will detect an incident particle. We could calculate our efficiency by straightforwardly counting how many particles we detect vs how many we "threw". For example, if we detect 9 particles and 10 were generated, our efficiency is -
0518
0519 9/10
0520
0521 i.e.
0522
0523 90%
0524
0525 This might be a useful figure. However, if we are evaluating the performance of a detector, it might be useful to consider the efficiency as a function of another quantity, for example *eta* or *p*. What this will tell us is how likely we are to detect particles incident on certain areas of the detector (or with a certain momentum for instance). We should not really expect these distributions to be completely flat.
0526
0527 In terms of our code, we can straightforwardly determine this by dividing some histograms. We can divide histograms via -
0528
0529 ```python
0530 RecPartBr = tree["ReconstructedChargedParticles"].arrays()
0531 BoolElec=((MCPartBr["MCParticles.PDG"][SimID]==11) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1)) # Define a filter to select out electrons which reconstruct in our detector
0532 MCHist = np.histogram(ak.flatten(MCPartBr["MCParticles.momentum.x"][BoolElec]), bins=100, range=(0,25)) # Create a hisogram of x-momenta for MC particles that are electrons
0533 RecHist = np.histogram(ak.flatten(RecPartBr["ReconstructedChargedParticles.momentum.x"][RecID][BoolElec]), bins=100, range=(0,25)) # Create a histogram of reconstructed x-momenta values for particles that are actual MC electrons that have reconstructed
0534 with np.errstate(divide='ignore'):
0535 Division = RecHist[0] / MCHist[0]
0536 Division = np.nan_to_num(Division,nan=0, posinf = 0) # Convert any nan or pos inf from /0 (empty bins) to 0
0537 Bin_Edges=MCHist[1]
0538 Bars = 0.5 * (Bin_Edges[1:] + Bin_Edges[:-1])
0539 BarWidth=Bars[1]-Bars[0]
0540 plt.bar(Bars, Division, width=BarWidth, alpha=0.75, color=kP6[2])
0541 plt.savefig("TestOut4.png", dpi = (160))
0542 ```
0543
0544 **Important** - What we have create here is __not__ our efficiency! We have simply divided the **reconstructed** electron P_{X} by its true value for MC electrons that have reconstructed in our detector. We used the PDG code for electrons, 11, to pick out electrons from our MC particles branch. There are a few caveats to actually calculating our efficiency. This just demonstrates how we can divide histograms in python.
0545
0546 For our efficiency. We need to compare our thrown particles of a given type to our detected particles of a given type.
0547
0548 - When we do our division, we should do this for the same quantity in each case (i.e. compare the true and values to each other).
0549 - How can you select the thrown MC Particles of a specific type?
0550 - How can you select the particles we detected of a specific type?
0551 - Note, this does not mean we need our reconstructed values.
0552
0553 ::::::::::::::::::::::::::::::::::::::::::::: challenge
0554
0555 ## Exercise
0556
0557 For all **scattered electrons**, **charged pions** and **protons** in our events:
0558
0559 - Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
0560 - Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
0561 - Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
0562
0563 ::::::::::::::: solution
0564
0565 The denominator is the truth distribution for the chosen species (select on
0566 `MCParticles.PDG` and `generatorStatus == 1`, **not** indexed by `SimID`). The numerator is the
0567 truth distribution for particles that were reconstructed (the same selection, indexed by `SimID`).
0568 Dividing these two `np.histogram` outputs bin-by-bin (guarding against divide-by-zero as shown
0569 above) gives the efficiency in that variable. For the 2-D correlations use `np.histogram2d` for
0570 numerator and denominator and divide. See the
0571 [pythonic efficiency template](../learners/exercise-scripts.md#pythonic_efficiencyanalysispy).
0572
0573 :::::::::::::::
0574
0575 :::::::::::::::::::::::::::::::::::::::::::::
0576
0577 ::::::::::::::::::::::::::::::::::::::::::::: callout
0578
0579 Hint:
0580 Getting the right arrays here is a bit tricky. We want three different things -
0581
0582 - Our MC particles (truth information), regardless of whether we have a matching track or not. This is just:
0583 - "MCPartBr['MCParticles.QUANTITY']"[SELECTION_CUTS] - We do not need to index this by the SimID
0584 - Our MC particles (truth information) that do have a matching reconstructed track, we just need to index these by the SimID:
0585 - "MCPartBr['MCParticles.QUANTITY'][SimID]" - We can then apply selection criteria
0586 - The Reconstructed particle information for events which correspond to a real MC track, we just need to index these by our RecID:
0587 - "ReconChPartBr['ReconstructedChargedParticles.QUANTITY'][RecID]"
0588
0589 :::::::::::::::::::::::::::::::::::::::::::::
0590
0591 ::::::::::::::::::::::::::::::::::::::::::::: callout
0592
0593 2D Histograms: We can make 2D histograms in python via -
0594
0595 ```python
0596 plt.hist2d(np.asarray(ak.flatten(Quantity1)), np.asarray(ak.flatten(Quantity2)), bins=[NBinsX,NBinsY], range=[[XLow,XHigh],[YLow,YHigh]], cmin=1)
0597 cb = plt.colorbar()
0598 cb.set_label('Counts/bin')
0599 ```
0600
0601 We can set titles etc as usual. Simply swap on the bin values and ranges, as well as the quantities as needed. Make sure your arrays contain equal numbers of entries.
0602
0603 :::::::::::::::::::::::::::::::::::::::::::::
0604
0605 ### Resolution Analysis
0606
0607 ::::::::::::::::::::::::::::::::::::::::::::: callout
0608
0609 Hint:
0610 Refer to [the script template](../learners/exercise-scripts.md#pythonic_resolutionanalysispy) if you're having trouble putting things in the right place.
0611
0612 :::::::::::::::::::::::::::::::::::::::::::::
0613
0614 Next, we will look at track momentum resolution. The resolution tells us how well we can reconstruct our "true" value. For example. we might want to know how well we can determine the energy of our particles. As such, we could calculate the energy resolution. Our resolution is simply -
0615
0616 - (Reconstructed - True)/True
0617
0618 This is often expressed as a percentage. So, for example, say we detect a particle and determine its energy to be 0.95 GeV. In reality, the energy was 1 GeV. As such, our energy resolution for this particle is -
0619
0620 - 0.95-1/1 = -5%
0621
0622 Let's see a quick example of this calculation for a a quantity -
0623
0624 ```python
0625 ElecMomXRes = ((RecPartBr["ReconstructedChargedParticles.momentum.x"][RecID][BoolElec]-MCPartBr["MCParticles.momentum.x"][SimID][BoolElec])/MCPartBr["MCParticles.momentum.x"][SimID][BoolElec])*100 # Multiply by 100 to get this as a %
0626 plt.hist(ak.flatten(ElecMomXRes), bins=50, range=(-25,25),alpha=0.5, color=kP6[2])
0627 plt.savefig("TestOut5.png", dpi = (160))
0628 ```
0629
0630 Here we've calculated and plotted the X momentum resolution for our charged tracks that correspond to true electrons in our sample. Whilst this plot will give us a sense of what the tracking resolution is, we don't expect the resolution to be constant for all momenta or eta. We can get a more complete picture by plotting the resolution as a function of different kinematic quantities.
0631
0632 ::::::::::::::::::::::::::::::::::::::::::::: challenge
0633
0634 ## Exercise
0635
0636 For all **scattered electrons**, **charged pions** and **protons** in our events:
0637
0638 - Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
0639
0640 ::::::::::::::: solution
0641
0642 Compute the resolution array `(reco - true)/true` for the matched particles of each species (both
0643 quantities indexed by `RecID`/`SimID` and filtered by species), then fill a `plt.hist2d` with the
0644 true momentum (or true pseudorapidity) on the x-axis and the resolution on the y-axis. Slicing the
0645 2-D histogram in x and taking the width of the resolution distribution gives the resolution as a
0646 function of that variable. See the
0647 [pythonic resolution template](../learners/exercise-scripts.md#pythonic_resolutionanalysispy).
0648
0649 :::::::::::::::
0650
0651 :::::::::::::::::::::::::::::::::::::::::::::
0652
0653 ::::::::::::::::::::::::::::::::::::::::::::: callout
0654
0655 Question:
0656
0657 - Will the histogram ranges for each particle species be the same?
0658 - Could we present the resolution values in a more understandable way?
0659
0660 :::::::::::::::::::::::::::::::::::::::::::::
0661
0662 ## Sample Analysis with Python uproot ROOT Pyroot Style: Track Efficiency and Resolution
0663
0664 ::::::::::::::::::::::::::::::::::::::::::::: callout
0665
0666 Comment:
0667 Despite using python/uproot, I have written these in a very "ROOT"/C way. Uproot converts our branches to arrays, so you can manipulate them in various fun ways using more pythonic methods if you want.
0668 See the previous method for an example of this approach.
0669
0670 :::::::::::::::::::::::::::::::::::::::::::::
0671
0672 If you are more familiar with python than you are with C/C++, you might find that using a python based root macro is easier for you. Outlined below are sample blocks of code for creating and running a python based analysis script.
0673
0674 With python, some tasks become easier, e.g. string manipulation and writing to (non ROOT) files.
0675
0676 Before we begin, we should create a skeleton macro to handle file I/O. For this example, we will make a simple python script. Using your favourite editor, create a file with a name like `trackAnalysis.py` or something similar and copy in the following code:
0677
0678 ```python
0679 #! /usr/bin/python
0680
0681 #Import relevant packages
0682 import ROOT, math, array
0683 from ROOT import TH1F, TH2F, TMath, TTree, TVector3, TVector2
0684 import uproot as up
0685
0686 # Define and open files
0687 infile="PATH_TO_FILE"
0688 ofile=ROOT.TFile.Open("TrackAnalysis_OutPy.root", "RECREATE")
0689
0690 # Open input file and define branches we want to look at with uproot
0691 events_tree = up.open(infile)["events"]
0692
0693 # Define histograms below
0694
0695 # Add main analysis loop(s) below
0696
0697 # Write output histograms to file below
0698
0699 # Close files
0700 ofile.Close()
0701 ```
0702 Note that we are using the module uproot to access the data here. See [further documentation here](https://masonproffitt.github.io/uproot-tutorial/03-trees/index.html). You may also need some of the other included packages too.
0703
0704 ::::::::::::::::::::::::::::::::::::::::::::: callout
0705
0706 We will use uproot a little bit like we use the TTreeReader in the other example. We can define the branches we want and assign them to arrays with uproot.
0707 We can do this via:
0708
0709 ```python
0710 # Open input file and define branches we want to look at with uproot
0711 events_tree = up.open(infile)["events"]
0712 # Get particle information# Get particle information
0713 partGenStat = events_tree["MCParticles.generatorStatus"].array()
0714 partMomX = events_tree["MCP articles.momentum.x"].array()
0715 partMomY = events_tree["MCParticles.momentum.y"].array()
0716 partMomZ = events_tree["MCParticles.momentum.z"].array()
0717 partPdg = events_tree["MCParticles.PDG"].array()
0718
0719 # Get reconstructed track information
0720 trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
0721 trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
0722 trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()
0723 ...
0724 ```
0725
0726 We can then access them as an array in a loop -
0727
0728 ```python
0729 # Add main analysis loop(s) below
0730 for i in range(0, len(partGenStat)): # Loop over all events
0731 for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
0732 if partGenStat[i][j] == 1: # Select stable particles
0733 pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
0734 ...
0735 ```
0736
0737 Uproot effectively takes the information in the tree, and turns it into an array. We can then access and manipulate this array in the same way that we can with any array in python.
0738
0739 Note that if you are using an older version of uproot (v2.x.x), you will need to access the branches slightly differently via -
0740
0741 ```python
0742 partGenStat = events_tree.array("MCParticles.generatorStatus")
0743 ```
0744
0745 :::::::::::::::::::::::::::::::::::::::::::::
0746
0747 You can run this file with ``python3 trackAnalysis.py``. It should open your file and create an empty output root file as specified. We will add histograms to this script and fill them in the next step.
0748
0749 Note that depending upon your setup, ``python trackAnalysis.py`` may work too.
0750
0751 ### Efficiency Analysis
0752
0753 ::::::::::::::::::::::::::::::::::::::::::::: callout
0754
0755 Hint:
0756 Refer to [the script template](../learners/exercise-scripts.md#efficiencyanalysispy) if you're having trouble putting things in the right place.
0757
0758 :::::::::::::::::::::::::::::::::::::::::::::
0759
0760 As with the ROOT TTreeReader example, we will find the tracking eficiency and resolution. We will need to access the reconstructed track information and the truth particle information and we will have to associate the individual tracks and particles to one another.
0761
0762 The basic strategy is the same:
0763
0764 1. Loop over all events in the file
0765 2. Within each event, loop over all stable charged particles
0766 3. Identify the ReconstructedChargedParticle (if any) associated with the truth particle we are looking at
0767 4. Create and fill the necessary histograms
0768
0769 Here is the sample code to implement these steps:
0770
0771 ```python
0772 # Get assocations between MCParticles and ReconstructedChargedParticles
0773 recoAssoc = events_tree["_ReconstructedChargedParticleAssociations_rec.index"].array()
0774 simuAssoc = events_tree["_ReconstructedChargedParticleAssociations_sim.index"].array()
0775
0776 # Define histograms below
0777 partEta = ROOT.TH1D("partEta","Eta of Thrown Charged Particles;Eta",100, -5 ,5 )
0778 matchedPartEta = ROOT.TH1D("matchedPartEta","Eta of Thrown Charged Particles That Have Matching Track", 100, -5 ,5);
0779 matchedPartTrackDeltaR = ROOT.TH1D("matchedPartTrackDeltaR","Delta R Between Matching Thrown and Reconstructed Charge Particle", 5000, 0, 5);
0780
0781 # Add main analysis loop(s) below
0782 for i in range(0, len(partGenStat)): # Loop over all events
0783 for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
0784 if partGenStat[i][j] == 1: # Select stable particles
0785 pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
0786 if(pdg == 11 or pdg == 13 or pdg == 211 or pdg == 321 or pdg == 2212):
0787 trueMom = ROOT.TVector3(partMomX[i][j], partMomY[i][j], partMomZ[i][j])
0788 trueEta = trueMom.PseudoRapidity()
0789 truePhi = trueMom.Phi()
0790 partEta.Fill(trueEta)
0791 for k in range(0,len(simuAssoc[i])): # Loop over associations to find matching ReconstructedChargedParticle
0792 if (simuAssoc[i][k] == j):
0793 recMom = ROOT.TVector3(trackMomX[i][recoAssoc[i][k]], trackMomY[i][recoAssoc[i][k]], trackMomZ[i][recoAssoc[i][k]])
0794 deltaEta = trueEta - recMom.PseudoRapidity()
0795 deltaPhi = TVector2. Phi_mpi_pi(truePhi - recMom.Phi())
0796 deltaR = math.sqrt((deltaEta*deltaEta) + (deltaPhi*deltaPhi))
0797 matchedPartEta.Fill(trueEta)
0798 matchedPartTrackDeltaR.Fill(deltaR)
0799
0800 # Write output histograms to file below
0801 partEta.Write()
0802 matchedPartEta.Write()
0803 matchedPartTrackDeltaR.Write()
0804
0805 # Close files
0806 ofile.Close()
0807 ```
0808 Insert this block of code appropriately. We should now have everything we need to find the track efficiency as a function of pseudorapidity. Run the script with `python3 trackAnalysis.py``. This should produce a root file with a few histograms in place. The efficiency can be found by taking the ratio of matchedPartEta over partEta.
0809
0810 ::::::::::::::::::::::::::::::::::::::::::::: callout
0811
0812 Question:
0813
0814 - Do the hisotgram ranges make sense?
0815 - We plot the distance between thrown and reconstructed charged partices, does this distribution look reasonable?
0816 - When filling the matchedPartEta histogram (the numerator in our efficiency), why do we use again the true thrown eta instead of the associated reconstructed eta?
0817
0818 :::::::::::::::::::::::::::::::::::::::::::::
0819
0820 ::::::::::::::::::::::::::::::::::::::::::::: challenge
0821
0822 ## Exercise
0823
0824 For all **scattered electrons**, **charged pions** and **protons** in our events:
0825
0826 - Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
0827 - Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
0828 - Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
0829
0830 ::::::::::::::: solution
0831
0832 As in the TTreeReader example, fill a denominator histogram with the thrown quantity for each
0833 species and a numerator histogram with the same quantity only for particles that have a matching
0834 track (found via the association loop), then divide with `TH1::Divide` (or `TH2::Divide` for the
0835 2-D correlations). Select species by PDG code and require `generatorStatus == 1`, and apply a
0836 momentum cut to avoid unstable low-statistics bins. See the
0837 [Pyroot efficiency template](../learners/exercise-scripts.md#efficiencyanalysispy).
0838
0839 :::::::::::::::
0840
0841 :::::::::::::::::::::::::::::::::::::::::::::
0842
0843 ### Resolution Analysis
0844
0845 ::::::::::::::::::::::::::::::::::::::::::::: callout
0846
0847 Hint:
0848 Refer to [the script template](../learners/exercise-scripts.md#resolutionanalysispy) if you're having trouble putting things in the right place.
0849
0850 :::::::::::::::::::::::::::::::::::::::::::::
0851
0852 Next, we will look at track momentum resolution, that is, how well the momentum of the reconstructed track matches that of the thrown particle. We should have all of the "infrastructure" we need in place to do the analysis, we just need to define the appropriate quantities and make the histograms. It only makes sense to define the resolution for tracks and particles which are associated with one another, so we will work within the loop over associations. Define the resolution expression and fill a simple histogram by inserting this block of code appropriately:
0853
0854 ```python
0855 trackMomentumRes = ROOT.TH1D("trackMomentumRes","Track Momentum Resolution",2000,-10.,10.);
0856 ...
0857 for k in range(0,len(simuAssoc[i])): # Loop over associations to find matching ReconstructedChargedParticle
0858 if (simuAssoc[i][k] == j):
0859 recMom = ROOT.TVector3(trackMomX[i][recoAssoc[i][k]], trackMomY[i][recoAssoc[i][k]], trackMomZ[i][recoAssoc[i][k]])
0860 momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()
0861
0862 trackMomentumRes.Fill(momRes)
0863 ```
0864
0865 Remember to write this histogram to the output file too! While this plot will give us a sense of what the tracking resolution is, we don't expect the resolution to be constant for all momenta or eta. We can get a more complete picture by plotting the resolution as a function of different kinematic quantities.
0866
0867 ::::::::::::::::::::::::::::::::::::::::::::: challenge
0868
0869 ## Exercise
0870
0871 For all **scattered electrons**, **charged pions** and **protons** in our events:
0872
0873 - Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
0874
0875 ::::::::::::::: solution
0876
0877 Within the association loop, compute `momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()` for the
0878 matched particle and fill a `TH2D` with the true momentum (or true pseudorapidity) on one axis and
0879 `momRes` on the other, once per species. A `TProfile` or slice-by-slice fit of the 2-D histogram
0880 then gives the resolution as a function of the kinematic variable. See the
0881 [Pyroot resolution template](../learners/exercise-scripts.md#resolutionanalysispy).
0882
0883 :::::::::::::::
0884
0885 :::::::::::::::::::::::::::::::::::::::::::::
0886
0887 ::::::::::::::::::::::::::::::::::::::::::::: callout
0888
0889 Question:
0890
0891 - Will the histogram ranges for each particle species be the same?
0892 - Could we present the resolution values in a more understandable way?
0893
0894 :::::::::::::::::::::::::::::::::::::::::::::
0895
0896 ## ROOT RDataFrames
0897
0898 ::::::::::::::::::::::::::::::::::::::::::::: callout
0899
0900 Note:
0901
0902 - This method does actually need you to be within eic-shell (or somewhere else with the correct EDM4hep/EDM4eic libraries installed).
0903
0904 :::::::::::::::::::::::::::::::::::::::::::::
0905
0906 Newer versions of root, such as the version in eic-shell, have access to a relatively new class, [RDataFrames](https://root.cern/doc/master/classROOT_1_1RDataFrame.html). These are similar to pythonic data frame style structures that you may be familiar with. Some people are moving towards utilising RDataFrames in their analysis. If you are more familiar with working with data frames, you may wish to investigate these further.
0907
0908 Included below is a quick script from [Simon Gardner](https://github.com/simonge/EIC_Analysis/blob/main/Analysis-Tutorial/EfficiencyAnalysisRDF.C) that utilises RDataFrames to analyse a data file. Copy the following into a new file called `EfficiencyAnalysisRDF.C` -
0909
0910 ```c++
0911 #include <edm4hep/utils/vector_utils.h>
0912 #include <edm4hep/MCParticle.h>
0913 #include <edm4eic/ReconstructedParticle.h>
0914 #include <ROOT/RDataFrame.hxx>
0915 #include <ROOT/RVec.hxx>
0916 #include <TFile.h>
0917
0918 // Define aliases for the data types
0919 using MCP = edm4hep::MCParticleData;
0920 using RecoP = edm4eic::ReconstructedParticleData;
0921
0922 // Define function to vectorize the edm4hep::utils methods
0923 template <typename T>
0924 auto getEta = [](ROOT::VecOps::RVec<T> momenta) {
0925 return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::eta(p.momentum); });
0926 };
0927
0928 template <typename T>
0929 auto getPhi = [](ROOT::VecOps::RVec<T> momenta) {
0930 return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::angleAzimuthal(p.momentum); });
0931 };
0932
0933 // Define the function to perform the efficiency analysis
0934 void EfficiencyAnalysisRDF(TString infile="PATH_TO_FILE"){
0935
0936 // Set up input file
0937 ROOT::RDataFrame df("events", infile);
0938
0939 // Define new dataframe node with additional columns
0940 auto df1 = df.Define("statusFilter", "MCParticles.generatorStatus == 1" )
0941 .Define("absPDG", "abs(MCParticles.PDG)" )
0942 .Define("pdgFilter", "absPDG == 11 || absPDG == 13 || absPDG == 211 || absPDG == 321 || absPDG == 2212")
0943 .Define("particleFilter","statusFilter && pdgFilter" )
0944 .Define("filtMCParts", "MCParticles[particleFilter]" )
0945 .Define("assoFilter", "Take(particleFilter,_ReconstructedChargedParticleAssociations_simID.index)") // Incase any of the associated particles happen to not be charged
0946 .Define("assoMCParts", "Take(MCParticles,_ReconstructedChargedParticleAssociations_simID.index)[assoFilter]")
0947 .Define("assoRecParts", "Take(ReconstructedChargedParticles,_ReconstructedChargedParticleAssociations_recID.index)[assoFilter]")
0948 .Define("filtMCEta", getEta<MCP> , {"filtMCParts"} )
0949 .Define("filtMCPhi", getPhi<MCP> , {"filtMCParts"} )
0950 .Define("accoMCEta", getEta<MCP> , {"assoMCParts"} )
0951 .Define("accoMCPhi", getPhi<MCP> , {"assoMCParts"} )
0952 .Define("assoRecEta", getEta<RecoP> , {"assoRecParts"})
0953 .Define("assoRecPhi", getPhi<RecoP> , {"assoRecParts"})
0954 .Define("deltaR", "ROOT::VecOps::DeltaR(assoRecEta, accoMCEta, assoRecPhi, accoMCPhi)");
0955
0956 // Define histograms
0957 auto partEta = df1.Histo1D({"partEta","Eta of Thrown Charged Particles;Eta",100,-5.,5.},"filtMCEta");
0958 auto matchedPartEta = df1.Histo1D({"matchedPartEta","Eta of Thrown Charged Particles That Have Matching Track",100,-5.,5.},"accoMCEta");
0959 auto matchedPartTrackDeltaR = df1.Histo1D({"matchedPartTrackDeltaR","Delta R Between Matching Thrown and Reconstructed Charged Particle",5000,0.,5.},"deltaR");
0960
0961 // Write histograms to file
0962 TFile *ofile = TFile::Open("EfficiencyAnalysis_Out_RDF.root","RECREATE");
0963
0964 // Booked Define and Histo1D lazy actions are only performed here
0965 partEta->Write();
0966 matchedPartEta->Write();
0967 matchedPartTrackDeltaR->Write();
0968
0969 ofile->Close(); // Close output file
0970 }
0971 ```
0972
0973 ::::::::::::::::::::::::::::::::::::::::::::: callout
0974
0975 Note:
0976
0977 - You will need to run this script with the command `root -l -q EfficiencyAnalysisRDF.C++`, within eic-shell (or somewhere else with the correct EDM4hep/EDM4eic libraries installed).
0978 - Remember to put in the correct file path.
0979
0980 :::::::::::::::::::::::::::::::::::::::::::::
0981
0982 If you like, you can try completing the exercises using this example to start from.
0983
0984 ## PODIO Direct Analysis
0985
0986 If you want to avoid ROOT entirely, you can analyse the PODIO files directly in a variety of ways.
0987
0988 See [Wouter's example use cases](https://indico.cern.ch/event/1343984/contributions/5908856/attachments/2842958/4970156/2024-04-23%20-%20Examples%20for%20Data%20Model%20Usage.pdf) from 23/04/24. Wouter shows a few ways in which the PODIO file can be accessed and analysed directly.
0989
0990 **As of March 2026, a full example and version of this method will be provided in the near future.**
0991
0992 ::::::::::::::::::::::::::::::::::::::::::::: keypoints
0993
0994 - Flat tree structure provides flexibility in analysis.
0995 - The ReconstructedChargedParticles branch holds information on reconstructed tracks.
0996
0997 :::::::::::::::::::::::::::::::::::::::::::::