Back to home page

EIC code displayed by LXR

 
 

    


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 :::::::::::::::::::::::::::::::::::::::::::::