Exercise Scripts

Last updated on 2026-07-10 | Edit this page

Included below is a selection of scripts for the exercises in part 3 of this tutorial.

You should be able to copy the code text directly into a new file. The name of the file is included as the title of each script section and in the accompanying descriptive text.

ROOT TTreeReader Scripts


EfficiencyAnalysis.C

Create a file called EfficiencyAnalysis.C and copy in the code below to get started on the efficiency analysis exercise. Note that you will need to correctly specify your input file path in the first line.

CPP

void EfficiencyAnalysis(TString infile="PATH_TO_INPUT_FILE"){
  // Set output file for the histograms
  TFile *ofile = TFile::Open("EfficiencyAnalysis_Out.root","RECREATE");
  
  // Set up input file chain
  TChain *mychain = new TChain("events");
  mychain->Add(infile);
  
  // Initialize reader
  TTreeReader tree_reader(mychain);
  
  // Get Particle Information
  TTreeReaderArray<int> partGenStat(tree_reader, "MCParticles.generatorStatus");
  TTreeReaderArray<double> partMomX(tree_reader, "MCParticles.momentum.x");
  TTreeReaderArray<double> partMomY(tree_reader, "MCParticles.momentum.y");
  TTreeReaderArray<double> partMomZ(tree_reader, "MCParticles.momentum.z");
  TTreeReaderArray<int> partPdg(tree_reader, "MCParticles.PDG");
  
  // Get Reconstructed Track Information
  TTreeReaderArray<float> trackMomX(tree_reader, "ReconstructedChargedParticles.momentum.x");
  TTreeReaderArray<float> trackMomY(tree_reader, "ReconstructedChargedParticles.momentum.y");
  TTreeReaderArray<float> trackMomZ(tree_reader, "ReconstructedChargedParticles.momentum.z");
  
  // Get Associations Between MCParticles and ReconstructedChargedParticles
  TTreeReaderArray<int> recoAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_rec.index");
  TTreeReaderArray<int> simuAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_sim.index");
      
  // Define Histograms
  TH1D *partEta = new TH1D("partEta","Eta of Thrown Charged Particles;Eta",100,-5.,5.);
  TH1D *matchedPartEta = new TH1D("matchedPartEta","Eta of Thrown Charged Particles That Have Matching Track",100,-5.,5.);
  TH1D *matchedPartTrackDeltaR = new TH1D("matchedPartTrackDeltaR","Delta R Between Matching Thrown and Reconstructed Charged Particle",5000,0.,5.);

  while(tree_reader.Next()) { // Loop over events
    for(unsigned int i=0; i<partGenStat.GetSize(); i++){ // Loop over thrown particles
	if(partGenStat[i] == 1){ // Select stable thrown particles
	    int pdg = TMath::Abs(partPdg[i]);
	    if(pdg == 11 || pdg == 13 || pdg == 211 || pdg == 321 || pdg == 2212){ // Look at charged particles (electrons, muons, pions, kaons, protons)
		TVector3 trueMom(partMomX[i],partMomY[i],partMomZ[i]);

		float trueEta = trueMom.PseudoRapidity();
		float truePhi = trueMom.Phi();
	    
		partEta->Fill(trueEta);

		for(unsigned int j=0; j<simuAssoc.GetSize(); j++){ // Loop over associations to find matching ReconstructedChargedParticle
		    if(simuAssoc[j] == i){ // Find association index matching the index of the thrown particle we are looking at
			TVector3 recMom(trackMomX[recoAssoc[j]],trackMomY[recoAssoc[j]],trackMomZ[recoAssoc[j]]); // recoAssoc[j] is the index of the matched ReconstructedChargedParticle

			// Check the distance between the thrown and reconstructed particle
			float deltaEta = trueEta - recMom.PseudoRapidity();
			float deltaPhi = TVector2::Phi_mpi_pi(truePhi - recMom.Phi());
			float deltaR = TMath::Sqrt(deltaEta*deltaEta + deltaPhi*deltaPhi);

			matchedPartTrackDeltaR->Fill(deltaR);

			matchedPartEta->Fill(trueEta); // Plot the thrown eta if a matched ReconstructedChargedParticle was found
                    }
                } // End loop over associations 
            } // End PDG check          
        } // End stable particles condition  
    } // End loop over thrown particles
  } // End loop over events 
  ofile->Write(); // Write histograms to file
  ofile->Close(); // Close output file
}

A “solution” version of the script for the exercise is included below -

CPP

void EfficiencyAnalysis_Exercise(TString infile="PATH_TO_FILE"){
  
  // Set output file for the histograms
  TFile *ofile = TFile::Open("EfficiencyAnalysis_Exercise_Out.root","RECREATE");

  // Analysis code will go here
  // Set up input file chain
  TChain *mychain = new TChain("events");
  mychain->Add(infile);

  // Initialize reader
  TTreeReader tree_reader(mychain);

  // Get Particle Information
  TTreeReaderArray<int> partGenStat(tree_reader, "MCParticles.generatorStatus");
  TTreeReaderArray<double> partMomX(tree_reader, "MCParticles.momentum.x");
  TTreeReaderArray<double> partMomY(tree_reader, "MCParticles.momentum.y");
  TTreeReaderArray<double> partMomZ(tree_reader, "MCParticles.momentum.z");
  TTreeReaderArray<int> partPdg(tree_reader, "MCParticles.PDG");

  // Get Reconstructed Track Information
  TTreeReaderArray<float> trackMomX(tree_reader, "ReconstructedChargedParticles.momentum.x");
  TTreeReaderArray<float> trackMomY(tree_reader, "ReconstructedChargedParticles.momentum.y");
  TTreeReaderArray<float> trackMomZ(tree_reader, "ReconstructedChargedParticles.momentum.z");

  // Get Associations Between MCParticles and ReconstructedChargedParticles
  TTreeReaderArray<int> recoAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_rec.index");
  TTreeReaderArray<int> simuAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_sim.index");
    
  // Define Histograms
  TH1D *partEta = new TH1D("partEta","#eta of Thrown Charged Particles; #eta", 120, -6, 6);
  TH1D *matchedPartEta = new TH1D("matchedPartEta","#eta of Thrown Charged Particles That Have Matching Track; #eta", 120, -6, 6);
  TH1D* partMom = new TH1D("partMom", "Momentum of Thrown Charged Particles (truth); P(GeV/c)", 150, 0, 150);
  TH1D* matchedPartMom = new TH1D("matchedPartMom", "Momentum of Thrown Charged Particles (truth), with matching track; P(GeV/c)", 150, 0, 150);
  TH1D* partPhi = new TH1D("partPhi", "#phi of Thrown Charged Particles (truth); #phi(rad)", 320, -3.2, 3.2);
  TH1D* matchedPartPhi = new TH1D("matchedPartPhi", "#phi of Thrown Charged Particles (truth), with matching track; #phi(rad)", 320, -3.2, 3.2);

  TH2D* partPEta = new TH2D("partPEta", "P vs #eta of Thrown Charged Particles; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6);
  TH2D* matchedPartPEta = new TH2D("matchedPartPEta", "P vs #eta of Thrown Charged Particles, with matching track; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6);
  TH2D* partPhiEta = new TH2D("partPhiEta", "#phi vs #eta of Thrown Charged Particles; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6);
  TH2D* matchedPartPhiEta = new TH2D("matchedPartPhiEta", "#phi vs #eta of Thrown Charged Particles; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6);
    
  TH1D *matchedPartTrackDeltaEta = new TH1D("matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructed Charged Particle; #Delta#eta", 100, -0.25, 0.25);
  TH1D *matchedPartTrackDeltaPhi = new TH1D("matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2);
  TH1D *matchedPartTrackDeltaR = new TH1D("matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle; #Delta R", 300, 0, 0.3);
  TH1D *matchedPartTrackDeltaMom = new TH1D("matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10);
    
  // Define some histograms for our efficiencies
  TH1D *TrackEff_Eta = new TH1D("TrackEff_Eta", "Tracking efficiency as fn of #eta; #eta; Eff(%)", 120, -6, 6); 
  TH1D *TrackEff_Mom = new TH1D("TrackEff_Mom", "Tracking efficiency as fn of P; P(GeV/c); Eff(%)", 150, 0, 150); 
  TH1D *TrackEff_Phi = new TH1D("TrackEff_Phi", "Tracking efficiency as fn of #phi; #phi(rad); Eff(%)", 320, -3.2, 3.2);

  // 2D Efficiencies
  TH2D* TrackEff_PEta = new TH2D("TrackEff_PEta", "Tracking efficiency as fn of P and #eta; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6);
  TH2D* TrackEff_PhiEta = new TH2D("TrackEff_PhiEta", "Tracking efficiency as fn of #phi and #eta; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6);

  // All charged particle histos
  TH1D *ChargedEta = new TH1D("ChargedEta", "#eta of all charged particles; #eta", 120, -6, 6);
  TH1D *ChargedPhi = new TH1D("ChargedPhi", "#phi of all charged particles; #phi (rad)", 120, -3.2, 3.2);
  TH1D *ChargedP = new TH1D("ChargedP", "P of all charged particles; P(GeV/c)", 150, 0, 150);
  
  while(tree_reader.Next()) { // Loop over events

    for(unsigned int i=0; i<partGenStat.GetSize(); i++) // Loop over thrown particles
      {
	if(partGenStat[i] == 1) // Select stable thrown particles
	  {
	    int pdg = TMath::Abs(partPdg[i]);

	    if(pdg == 11 || pdg == 13 || pdg == 211 || pdg == 321 || pdg == 2212) // Look at charged particles (electrons, muons, pions, kaons, protons)
	      {
		TVector3 trueMom(partMomX[i],partMomY[i],partMomZ[i]);

		float trueEta = trueMom.PseudoRapidity();
		float truePhi = trueMom.Phi();
	    
		partEta->Fill(trueEta);
		partPhi->Fill(truePhi);
		partMom->Fill(trueMom.Mag());
		partPEta->Fill(trueMom.Mag(), trueEta);
		partPhiEta->Fill(truePhi, trueEta);

		// Loop over associations to find matching ReconstructedChargedParticle
		for(unsigned int j=0; j<simuAssoc.GetSize(); j++)
		  {
		    if(simuAssoc[j] == i) // Find association index matching the index of the thrown particle we are looking at
		      {
			TVector3 recMom(trackMomX[recoAssoc[j]],trackMomY[recoAssoc[j]],trackMomZ[recoAssoc[j]]); // recoAssoc[j] is the index of the matched ReconstructedChargedParticle

			// Check the distance between the thrown and reconstructed particle
			float deltaEta = trueEta - recMom.PseudoRapidity();
			float deltaPhi = TVector2::Phi_mpi_pi(truePhi - recMom.Phi());
			float deltaR = TMath::Sqrt(deltaEta*deltaEta + deltaPhi*deltaPhi);
			float deltaMom = ((trueMom.Mag()) - (recMom.Mag()));

			matchedPartTrackDeltaEta->Fill(deltaEta);
			matchedPartTrackDeltaPhi->Fill(deltaPhi);
			matchedPartTrackDeltaR->Fill(deltaR);
			matchedPartTrackDeltaMom->Fill(deltaMom);

			matchedPartEta->Fill(trueEta); // Plot the thrown eta if a matched ReconstructedChargedParticle was found
			matchedPartPhi->Fill(truePhi);
			matchedPartMom->Fill(trueMom.Mag());

			matchedPartPEta->Fill(trueMom.Mag(), trueEta);
			matchedPartPhiEta->Fill(truePhi, trueEta);
	
		      }
		  }// End loop over associations
	      } // End PDG check
	  } // End stable particles condition
      } // End loop over thrown particles
    // Loop over all charged particles and fill some histograms of kinematics quantities
    for(unsigned int k=0; k<trackMomX.GetSize(); k++){ // Loop over all charged particles, thrown or not
      
      TVector3 CPartMom(trackMomX[k], trackMomY[k], trackMomZ[k]);

      float CPartEta = CPartMom.PseudoRapidity();
      float CPartPhi = CPartMom.Phi();

      ChargedEta->Fill(CPartEta);
      ChargedPhi->Fill(CPartPhi);
      ChargedP->Fill(CPartMom.Mag());
      
    } // End loop over all charged particles
  } // End loop over events

  // Take the ratio of the histograms above to get our efficiency plots
  TrackEff_Eta->Divide(matchedPartEta, partEta, 1, 1, "b");
  TrackEff_Mom->Divide(matchedPartMom, partMom, 1, 1, "b");
  TrackEff_Phi->Divide(matchedPartPhi, partPhi, 1, 1, "b");
  TrackEff_PEta->Divide(matchedPartPEta, partPEta, 1, 1, "b");
  TrackEff_PhiEta->Divide(matchedPartPhiEta, partPhiEta, 1, 1, "b");
  
  ofile->Write(); // Write histograms to file
  ofile->Close(); // Close output file
}

Insert your input file path and execute as the example code above.

ResolutionAnalysis.C

Create a file called ResolutionAnalysis.C and copy in the code below to get started on the resolution analysis exercise. Note that you will need to correctly specify your input file path in the first line.

CPP

void ResolutionAnalysis(TString infile="PATH_TO_INPUT_FILE"){
  // Set output file for the histograms
  TFile *ofile = TFile::Open("ResolutionAnalysis_Out.root","RECREATE");

  // Analysis code will go here
  // Set up input file chain
  TChain *mychain = new TChain("events");
  mychain->Add(infile);

  // Initialize reader
  TTreeReader tree_reader(mychain);

  // Get Particle Information
  TTreeReaderArray<int> partGenStat(tree_reader, "MCParticles.generatorStatus");
  TTreeReaderArray<double> partMomX(tree_reader, "MCParticles.momentum.x");
  TTreeReaderArray<double> partMomY(tree_reader, "MCParticles.momentum.y");
  TTreeReaderArray<double> partMomZ(tree_reader, "MCParticles.momentum.z");
  TTreeReaderArray<int> partPdg(tree_reader, "MCParticles.PDG");

  // Get Reconstructed Track Information
  TTreeReaderArray<float> trackMomX(tree_reader, "ReconstructedChargedParticles.momentum.x");
  TTreeReaderArray<float> trackMomY(tree_reader, "ReconstructedChargedParticles.momentum.y");
  TTreeReaderArray<float> trackMomZ(tree_reader, "ReconstructedChargedParticles.momentum.z");

  // Get Associations Between MCParticles and ReconstructedChargedParticles
  TTreeReaderArray<int> recoAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_rec.index");
  TTreeReaderArray<int> simuAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_sim.index");
    
  // Define Histograms
  TH1D *trackMomentumRes = new TH1D("trackMomentumRes","Track Momentum Resolution", 400, -2, 2);
 
  TH1D *matchedPartTrackDeltaEta = new TH1D("matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructed Charged Particle; #Delta#eta", 100, -0.25, 0.25);
  TH1D *matchedPartTrackDeltaPhi = new TH1D("matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2);
  TH1D *matchedPartTrackDeltaR = new TH1D("matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle; #Delta R", 300, 0, 0.3);
  TH1D *matchedPartTrackDeltaMom = new TH1D("matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10);
  while(tree_reader.Next()) { // Loop over events
    for(unsigned int i=0; i<partGenStat.GetSize(); i++){ // Loop over thrown particles
	if(partGenStat[i] == 1){ // Select stable thrown particles
	    int pdg = TMath::Abs(partPdg[i]);
	    if(pdg == 11 || pdg == 13 || pdg == 211 || pdg == 321 || pdg == 2212){ // Look at charged particles (electrons, muons, pions, kaons, protons)
		TVector3 trueMom(partMomX[i],partMomY[i],partMomZ[i]);

		float trueEta = trueMom.PseudoRapidity();
		float truePhi = trueMom.Phi();
	    
		for(unsigned int j=0; j<simuAssoc.GetSize(); j++){ // Loop over associations to find matching ReconstructedChargedParticle
		    if(simuAssoc[j] == i){ // Find association index matching the index of the thrown particle we are looking at
			TVector3 recMom(trackMomX[recoAssoc[j]],trackMomY[recoAssoc[j]],trackMomZ[recoAssoc[j]]); // recoAssoc[j] is the index of the matched ReconstructedChargedParticle

			// Check the distance between the thrown and reconstructed particle
			float deltaEta = trueEta - recMom.PseudoRapidity();
			float deltaPhi = TVector2::Phi_mpi_pi(truePhi - recMom.Phi());
			float deltaR = TMath::Sqrt(deltaEta*deltaEta + deltaPhi*deltaPhi);
			float deltaMom = ((trueMom.Mag()) - (recMom.Mag()));
			double momRes = (recMom.Mag()- trueMom.Mag())/trueMom.Mag();
      
			trackMomentumRes->Fill(momRes);

			matchedPartTrackDeltaEta->Fill(deltaEta);
			matchedPartTrackDeltaPhi->Fill(deltaPhi);
			matchedPartTrackDeltaR->Fill(deltaR);
			matchedPartTrackDeltaMom->Fill(deltaMom);
                    }
                } // End loop over associations 
            } // End PDG check          
        } // End stable particles condition  
    } // End loop over thrown particles
  } // End loop over events 
  ofile->Write(); // Write histograms to file
  ofile->Close(); // Close output file
}

A “solution” version of the script for the exercise is included below -

CPP

void ResolutionAnalysis_Exercise(TString infile="PATH_TO_FILE"){
  // Set output file for the histograms
  TFile *ofile = TFile::Open("ResolutionAnalysis_Exercise_Out.root","RECREATE");

  // Analysis code will go here
  // Set up input file chain
  TChain *mychain = new TChain("events");
  mychain->Add(infile);

  // Initialize reader
  TTreeReader tree_reader(mychain);

  // Get Particle Information
  TTreeReaderArray<int> partGenStat(tree_reader, "MCParticles.generatorStatus");
  TTreeReaderArray<double> partMomX(tree_reader, "MCParticles.momentum.x");
  TTreeReaderArray<double> partMomY(tree_reader, "MCParticles.momentum.y");
  TTreeReaderArray<double> partMomZ(tree_reader, "MCParticles.momentum.z");
  TTreeReaderArray<int> partPdg(tree_reader, "MCParticles.PDG");

  // Get Reconstructed Track Information
  TTreeReaderArray<float> trackMomX(tree_reader, "ReconstructedChargedParticles.momentum.x");
  TTreeReaderArray<float> trackMomY(tree_reader, "ReconstructedChargedParticles.momentum.y");
  TTreeReaderArray<float> trackMomZ(tree_reader, "ReconstructedChargedParticles.momentum.z");

  // Get Associations Between MCParticles and ReconstructedChargedParticles
  TTreeReaderArray<int> recoAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_rec.index");
  TTreeReaderArray<int> simuAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_sim.index");
    
  // Define Histograms
  TH1D *trackMomentumRes = new TH1D("trackMomentumRes","Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
  TH2D* trackMomResP = new TH2D("trackMomResP", "Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
  TH2D* trackMomResEta = new TH2D("trackMomResEta", "Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

  TH1D *trackMomentumRes_e = new TH1D("trackMomentumRes_e","e^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
  TH2D* trackMomResP_e = new TH2D("trackMomResP_e", "e^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 25);
  TH2D* trackMomResEta_e = new TH2D("trackMomResEta_e", "e^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

  TH1D *trackMomentumRes_mu = new TH1D("trackMomentumRes_mu","#mu^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
  TH2D* trackMomResP_mu = new TH2D("trackMomResP_mu", "#mu^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 25);
  TH2D* trackMomResEta_mu = new TH2D("trackMomResEta_mu", "#mu^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

  TH1D *trackMomentumRes_pi = new TH1D("trackMomentumRes_pi","#pi^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
  TH2D* trackMomResP_pi = new TH2D("trackMomResP_pi", "#pi^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
  TH2D* trackMomResEta_pi = new TH2D("trackMomResEta_pi", "#pi^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

  TH1D *trackMomentumRes_K = new TH1D("trackMomentumRes_K","K^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
  TH2D* trackMomResP_K = new TH2D("trackMomResP_K", "K^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
  TH2D* trackMomResEta_K = new TH2D("trackMomResEta_K", "K^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

  TH1D *trackMomentumRes_p = new TH1D("trackMomentumRes_p","p Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
  TH2D* trackMomResP_p = new TH2D("trackMomResP_p", "p Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
  TH2D* trackMomResEta_p = new TH2D("trackMomResEta_p", "p Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);
  
  TH1D *matchedPartTrackDeltaEta = new TH1D("matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructed Charged Particle; #Delta#eta", 100, -0.25, 0.25);
  TH1D *matchedPartTrackDeltaPhi = new TH1D("matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2);
  TH1D *matchedPartTrackDeltaR = new TH1D("matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle; #Delta R", 300, 0, 0.3);
  TH1D *matchedPartTrackDeltaMom = new TH1D("matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10);

  while(tree_reader.Next()) { // Loop over events

    for(unsigned int i=0; i<partGenStat.GetSize(); i++) // Loop over thrown particles
      {
	if(partGenStat[i] == 1) // Select stable thrown particles
	  {
	    int pdg = TMath::Abs(partPdg[i]);

	    if(pdg == 11 || pdg == 13 || pdg == 211 || pdg == 321 || pdg == 2212) // Look at charged particles (electrons, muons, pions, kaons, protons)
	      {
		TVector3 trueMom(partMomX[i],partMomY[i],partMomZ[i]);

		float trueEta = trueMom.PseudoRapidity();
		float truePhi = trueMom.Phi();

		// Loop over associations to find matching ReconstructedChargedParticle
		for(unsigned int j=0; j<simuAssoc.GetSize(); j++)
		  {
		    if(simuAssoc[j] == i) // Find association index matching the index of the thrown particle we are looking at
		      {
			TVector3 recMom(trackMomX[recoAssoc[j]],trackMomY[recoAssoc[j]],trackMomZ[recoAssoc[j]]); // recoAssoc[j] is the index of the matched ReconstructedChargedParticle

			// Check the distance between the thrown and reconstructed particle
			float deltaEta = trueEta - recMom.PseudoRapidity();
			float deltaPhi = TVector2::Phi_mpi_pi(truePhi - recMom.Phi());
			float deltaR = TMath::Sqrt(deltaEta*deltaEta + deltaPhi*deltaPhi);
			float deltaMom = ((trueMom.Mag()) - (recMom.Mag()));

			double momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag();
	
			trackMomentumRes->Fill(momRes); // Could also multiply by 100 and express as a percentage instead
			trackMomResP->Fill(momRes, trueMom.Mag());
			trackMomResEta->Fill(momRes, trueEta);

			if( pdg == 11){
			  trackMomentumRes_e->Fill(momRes);
			  trackMomResP_e->Fill(momRes, trueMom.Mag());
			  trackMomResEta_e->Fill(momRes, trueEta);
			}
			else if( pdg == 13){
			  trackMomentumRes_mu->Fill(momRes);
			  trackMomResP_mu->Fill(momRes, trueMom.Mag());
			  trackMomResEta_mu->Fill(momRes, trueEta);
			}
			else if( pdg == 211){
			  trackMomentumRes_pi->Fill(momRes);
			  trackMomResP_pi->Fill(momRes, trueMom.Mag());
			  trackMomResEta_pi->Fill(momRes, trueEta);
			}
			else if( pdg == 321){
			  trackMomentumRes_K->Fill(momRes);
			  trackMomResP_K->Fill(momRes, trueMom.Mag());
			  trackMomResEta_K->Fill(momRes, trueEta);
			}
			else if( pdg == 2212){
			  trackMomentumRes_p->Fill(momRes);
			  trackMomResP_p->Fill(momRes, trueMom.Mag());
			  trackMomResEta_p->Fill(momRes, trueEta);
			}
			  
			matchedPartTrackDeltaEta->Fill(deltaEta);
			matchedPartTrackDeltaPhi->Fill(deltaPhi);
			matchedPartTrackDeltaR->Fill(deltaR);
			matchedPartTrackDeltaMom->Fill(deltaMom);
			
		      }
		  }// End loop over associations
	      } // End PDG check
	  } // End stable particles condition
      } // End loop over thrown particles
  } // End loop over events

  ofile->Write(); // Write histograms to file
  ofile->Close(); // Close output file
}

Insert your input file path and execute as the example code above.

Compiled ROOT Scripts

As brought up in the tutorial, you may wish to compile your ROOT based scripts for faster processing. Included below are some scripts and a short example of a compiled ROOT macro provided by Kolja Kauder.

Each file is uploaded individually, but your directory should be structured as follows -

  • helloroot
    • README.md
    • CMakeLists.txt
    • include
      • helloroot
        • helloroot.hh
    • src
      • helloexec.cxx
      • helloroot.cxx

Note that any entry in the above without a file extension is a directory.

The contents of README.md are -

To build using cmake, create a build directory, navigate to it and run cmake. e.g.:

\```
mkdir build
cd build
cmake ..
make
\```
You can specify a number of parallel build threads with the -j flag, e.g.
\```
make -j4
\```

You can specify an install directory to cmake with
-DCMAKE_INSTALL_PREFIX=<path>
then, after building,
\```
make install
\```
to install the headers and libraries under that location.
There is no "make uninstall" but (on Unix-like systems)
you can do
xargs rm < install_manifest.txt
from the cmake build directory.

Note that you should delete the  characters in this block.

The contents of CMakeLists.txt are -

CMAKE

# CMakeLists.txt for helloroot.
# More complicated than needed but demonstrates making and linking your own libraries
# cf. https://cliutils.gitlab.io/modern-cmake/
# https://root.cern/manual/integrate_root_into_my_cmake_project/

cmake_minimum_required(VERSION 3.10)
project(helloroot VERSION 1.0 LANGUAGES CXX ) # not needed

 # Find ROOT. Use at least 6.20 for smoother cmake support
 find_package(ROOT 6.20 REQUIRED )

 message ( " ROOT Libraries = " ${ROOT_LIBRARIES} )

 ##############################################################################################################

# Main target is the libhelloroot library
add_library(
  # You can use wildcards but it's cleaner to list the files explicitly
   helloroot
   SHARED
   src/helloroot.cxx
   )
## The particular syntax here is a bit annoying because you have to list all the sub-modules you need
## but it picks up automatically all the compile options needed for root, e.g. the c++ std version
## Find all available ROOT modules with `root-config --libs`
target_link_libraries(helloroot PUBLIC ROOT::Core ROOT::RIO ROOT::Rint ROOT::Tree ROOT::EG ROOT::Physics )

## The above _should_ be true, and it is on most systems. If it's not, uncoment one of the following lines
# target_compile_features(helloroot PUBLIC cxx_std_17)
# target_compile_features(helloroot PUBLIC cxx_std_20)

# include directories - this is also overkill but useful if you want to create dictionaries
# Contact kkauder@gmail.com for that - it's too much for this example
target_include_directories(helloroot 
PUBLIC 
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 )

# Can add addtional options here
target_compile_options(helloroot PRIVATE -Wall -Wextra -pedantic -g)  

##############################################################################################################

## Build executables
add_executable(helloexec src/helloexec.cxx)
# target_compile_options(helloexec PRIVATE -Wall -Wextra -pedantic -g)
target_link_libraries(helloexec helloroot )
target_include_directories(helloexec
  PRIVATE
  ${ROOT_INCLUDE_DIRS}
  )

install(TARGETS helloexec DESTINATION bin)

##############################################################################################################

## Install library
# Could also use include(GNUInstallDirs)
# and then destinations of the form ${CMAKE_INSTALL_INCLUDEDIR}
install(TARGETS helloroot
  EXPORT helloroot-export
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  )

## Install headers
install (DIRECTORY ${CMAKE_SOURCE_DIR}/include/helloroot
  DESTINATION  include/helloroot
  )

## Generate configuration file - this allows you to use cmake in another project 
## to find and link the installed helloroot library
install(EXPORT helloroot-export
  FILE
  hellorootConfig.cmake
  NAMESPACE
    helloroot::
  DESTINATION
  cmake
  )

## Final message
message( " Done!")

The contents of helloroot.hh are -

CPP

#ifndef HELLO_ROOT_H
#define HELLO_ROOT_H

void HelloRoot();

#endif // HELLO_ROOT_H

The contents of helloexec.cxx are -

CPP

#include<helloroot/helloroot.hh>

#include<iostream>
#include<string>

int main()
{
    std::cout << "Hello from main " << std::endl;
    HelloRoot(); 
    
    return 0;
}

And finally, the contents of helloroot.cxx are -

CPP

#include<helloroot/helloroot.hh>

#include<iostream>
#include<string>

#include<TH1D.h>
#include<TPad.h>

void HelloRoot()
{
  std::cout << "Hello from HelloRoot" << std::endl;

  // do something with root
  TH1D h("h", "h", 100, -5, 5);
  h.FillRandom("gaus", 1000);
  h.Draw();
  gPad->SaveAs("hello.png");

  return;
}

Please consult the README and script comments for further instructions.

Python Uproot Scripts - Pythonic Versions


Some template scripts that utilise an python array based approach are included below. For some examples of using uproot to access information in .root files, please consult this notebook which can be run in Google Colab.

Pythonic_EfficiencyAnalysis.py

Create a file called EfficiencyAnalysis.py and copy in the code below to get started on the efficiency analysis exercise. Note that you will need to correctly specify your input file path in the variable fname. Note that some example code to process the division of two histograms is included as a commented section at the end of this example.

PYTHON

#! /usr/bin/python
# Import some relevant packages
import uproot as up
import awkward as ak
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.ticker as ticker
import matplotlib.cm as cm
import matplotlib.pylab as plt
import scipy, vector, os
from XRootD import client
from scipy import stats
from matplotlib import pyplot as plt
from matplotlib.gridspec import GridSpec
from matplotlib import colors as colours

# Set some matplot lib features
plt.rcParams['ytick.direction'] = 'in'
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['xaxis.labellocation'] = 'right'
plt.rcParams['yaxis.labellocation'] = 'top'
plt.rcParams["figure.figsize"] = (16,9)
kP6 = ['#5790fc','#f89c20','#e42536','#964a8b','#9c9ca1','#7a21dd'] # Set ROOT kP6 colours - see https://root.cern.ch/doc/v636/classTColor.html

# Open our file
fname = "INPUT_FILE.root"
if os.path.isfile(fname):
    file=up.open(fname)
else:
    print("Error opening file - ", fname, " check your fname variable!")

# Open the tree
tree = file['events']

# Convert relevant branches to arrays
MCPartBr = tree["MCParticles"].arrays()
RecoAssocRec = tree['_ReconstructedChargedParticleAssociations_rec'].arrays()
RecoAssocSim = tree['_ReconstructedChargedParticleAssociations_sim'].arrays()
ReconChPartBr = tree["ReconstructedChargedParticles"].arrays()

RecID=RecoAssocRec['_ReconstructedChargedParticleAssociations_rec.index'] # Array of reconstructed IDs
SimID=RecoAssocSim['_ReconstructedChargedParticleAssociations_sim.index'] # Array of simulated IDs

# Create some filters, anything with [SimID] or [RecID] will index the event by the associations. This means we will only retain events with a matching truth particle/matching reconstructed particle
BoolMatch=(MCPartBr["MCParticles.PDG"][SimID])==(ReconChPartBr["ReconstructedChargedParticles.PDG"][RecID]) # Use simulated or reconstructed IDs as indices, this checks if the pdg between each array matches
BoolChargeTrack = ((abs(MCPartBr["MCParticles.charge"])!=0) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolChargeTrackMatch = ((abs(MCPartBr["MCParticles.charge"][SimID])!=0) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1))
BoolElec=((abs(MCPartBr["MCParticles.PDG"])==11) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolMuon=((abs(MCPartBr["MCParticles.PDG"])==13) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolPion=((abs(MCPartBr["MCParticles.PDG"])==211) & (MCPartBr["MCParticles.generatorStatus"]==1)) # Use abs to include both positive and negative pions
BoolKaon=((abs(MCPartBr["MCParticles.PDG"])==321) & (MCPartBr["MCParticles.generatorStatus"]==1)) # Use abs to include both positive and negative kaons
BoolProton=((abs(MCPartBr["MCParticles.PDG"])==2212) & (MCPartBr["MCParticles.generatorStatus"]==1)) # Use abs to include both positive and negative protons

# Define some doubles
ElecMass = 511*(10**-6) # Electron mass in GeV

# Convert some branches into arrays of vectors
MC_Parts = vector.zip({'px': MCPartBr["MCParticles.momentum.x"], 'py': MCPartBr["MCParticles.momentum.y"], 'pz': MCPartBr["MCParticles.momentum.z"]})
Rec_Parts = vector.zip({'px': ReconChPartBr["ReconstructedChargedParticles.momentum.x"], 'py': ReconChPartBr["ReconstructedChargedParticles.momentum.y"], 'pz': ReconChPartBr["ReconstructedChargedParticles.momentum.z"], 'E':ReconChPartBr["ReconstructedChargedParticles.energy"]})

# Determine the energy for a few MC particles of specific types
MCEnerElec = np.sqrt(MC_Parts[BoolElec].p**2 + ElecMass**2)

# Calculate some additional quantities which are differences between true and reconstructed values for MC particles with a matched reconstructed track
DeltaEta = MC_Parts[SimID][BoolChargeTrackMatch].eta - Rec_Parts[RecID][BoolChargeTrackMatch].eta
DeltaPhi = MC_Parts[SimID][BoolChargeTrackMatch].phi - Rec_Parts[RecID][BoolChargeTrackMatch].phi
DeltaR = np.sqrt(DeltaEta**2 + DeltaPhi**2)

# Plot some quantities as one image
fig, axs = plt.subplots(2,2, tight_layout=True) # Ironically, this makes things *less* tight
axs[-1, -1].axis('off') # Don't draw any blank subfigs
axs[0,0].hist(ak.flatten(MC_Parts[BoolChargeTrack].eta), bins=100, range=(-5,5),alpha=0.5, color=kP6[1]) # Plot the MC eta values for all charged particles at an MC level
axs[0,0].set_title(r"$\eta_{MC}$ of Charged Particles")
axs[0,0].set(xlabel=r'$\eta_{MC}$', ylabel=r'# Entries / 0.1')
axs[0,1].hist(ak.flatten(MC_Parts[SimID][BoolChargeTrackMatch].eta), bins=100, range=(-5,5),alpha=0.5, color=kP6[1]) # Plot the MC eta values for all charged particles at an MC level that have a matching reconstructed track
axs[0,1].set_title(r"$\eta_{MC}$ of matched Charged Particles")
axs[0,1].set(xlabel=r'$\eta_{MC}$', ylabel=r'# Entries / 0.1')
axs[1,0].hist(ak.flatten(DeltaR), bins=5000, range=(0,5),alpha=0.5, color=kP6[1]) # Plot one of our calculated quantities
axs[1,0].set_title(r"$\Delta R$ of Matched Charged Particles")
axs[1,0].set(xlabel=r'$\Delta R$', ylabel=r'# Entries / 0.001')
plt.savefig("EfficiencyAnalysis_Out.png", dpi = (160))

# Commented out, but to divide histograms we can do the following, just put the array we want to plot as the histo in place of Quantity
#MCHist = np.histogram(ak.flatten(Quantity), bins=100, range=(0,25))
#RecHist = np.histogram(ak.flatten(Quantity), bins=100, range=(0,25))
#with np.errstate(divide='ignore'):
#    Division = RecHist[0] / MCHist[0]
#Division = np.nan_to_num(Division,nan=0, posinf = 0)
#Bin_Edges=MCHist[1]
#Bars = 0.5 * (Bin_Edges[1:] + Bin_Edges[:-1])
#BarWidth=Bars[1]-Bars[0]
#plt.bar(Bars, Division, width=BarWidth, alpha=0.5, color='kP6[0]')

“Complete” exercise example included below

PYTHON

#! /usr/bin/python
# Import some relevant packages
import uproot as up
import awkward as ak
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.ticker as ticker
import matplotlib.cm as cm
import matplotlib.pylab as plt
import scipy, vector, os
from XRootD import client
from scipy import stats
from matplotlib import pyplot as plt
from matplotlib.gridspec import GridSpec
from matplotlib import colors as colours

# Set some matplot lib features
plt.rcParams['ytick.direction'] = 'in'
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['xaxis.labellocation'] = 'right'
plt.rcParams['yaxis.labellocation'] = 'top'
plt.rcParams["figure.figsize"] = (16,9)
kP6 = ['#5790fc','#f89c20','#e42536','#964a8b','#9c9ca1','#7a21dd'] # Set ROOT kP6 colours - see https://root.cern.ch/doc/v636/classTColor.html

# Open our file
fname = "YOUR_INPUT_FILE"
if os.path.isfile(fname):
    file=up.open(fname)
else:
    print("Error opening file - ", fname, " check your fname variable!")

# Open the tree
tree = file['events']

# Convert relevant branches to arrays
MCPartBr = tree["MCParticles"].arrays()
RecoAssocRec = tree['_ReconstructedChargedParticleAssociations_rec'].arrays()
RecoAssocSim = tree['_ReconstructedChargedParticleAssociations_sim'].arrays()
ReconChPartBr = tree["ReconstructedChargedParticles"].arrays()

RecID=RecoAssocRec['_ReconstructedChargedParticleAssociations_rec.index'] # Array of reconstructed IDs
SimID=RecoAssocSim['_ReconstructedChargedParticleAssociations_sim.index'] # Array of simulated IDs

# Create some filters, anything with [SimID] or [RecID] will index the event by the associations. This means we will only retain events with a matching truth particle/matching reconstructed particle
BoolMatch=(MCPartBr["MCParticles.PDG"][SimID])==(ReconChPartBr["ReconstructedChargedParticles.PDG"][RecID]) # Use simulated or reconstructed IDs as indices, this checks if the pdg between each array matches
BoolChargeTrack = ((abs(MCPartBr["MCParticles.charge"])!=0) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolChargeTrackMatch = ((abs(MCPartBr["MCParticles.charge"][SimID])!=0) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1))
BoolElec=((abs(MCPartBr["MCParticles.PDG"])==11) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolMuon=((abs(MCPartBr["MCParticles.PDG"])==13) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolPion=((abs(MCPartBr["MCParticles.PDG"])==211) & (MCPartBr["MCParticles.generatorStatus"]==1)) # Use abs to include both positive and negative pions
BoolKaon=((abs(MCPartBr["MCParticles.PDG"])==321) & (MCPartBr["MCParticles.generatorStatus"]==1)) # Use abs to include both positive and negative kaons
BoolProton=((abs(MCPartBr["MCParticles.PDG"])==2212) & (MCPartBr["MCParticles.generatorStatus"]==1)) # Use abs to include both positive and negative protons
BoolElecMatch=((MCPartBr["MCParticles.PDG"][SimID]==11) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1))
BoolPionMatch=((abs(MCPartBr["MCParticles.PDG"][SimID])==211) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1)) # Use abs to include both positive and negative pions

MCStatus = MCPartBr['MCParticles.generatorStatus'] == 1
MCNegCharge = MCPartBr['MCParticles.charge'] == -1
MCScElecPDG = MCPartBr['MCParticles.PDG'] == 11
MCPionPDG = abs(MCPartBr['MCParticles.PDG']) == 211

# We index by the simulation ID to ONLY select events with a matching track

# Define some doubles
ElecMass = 511*(10**-6) # Electron mass in GeV

# Convert some branches into arrays of vectors
MC_Parts = vector.zip({'px': MCPartBr["MCParticles.momentum.x"], 'py': MCPartBr["MCParticles.momentum.y"], 'pz': MCPartBr["MCParticles.momentum.z"]})
Rec_Parts = vector.zip({'px': ReconChPartBr["ReconstructedChargedParticles.momentum.x"], 'py': ReconChPartBr["ReconstructedChargedParticles.momentum.y"], 'pz': ReconChPartBr["ReconstructedChargedParticles.momentum.z"], 'E':ReconChPartBr["ReconstructedChargedParticles.energy"]})
Rec_Vects = vector.zip({'px': MCPartBr["MCParticles.momentum.x"][SimID], 'py': MCPartBr["MCParticles.momentum.y"][SimID], 'pz': MCPartBr["MCParticles.momentum.z"][SimID]}) 
MC_ScElec = MC_Parts[MCStatus & MCNegCharge & MCScElecPDG]
MC_Pions = MC_Parts[MCStatus & MCPionPDG]
Rec_ScElec = Rec_Vects[BoolElecMatch]
Rec_Pions = Rec_Vects[BoolPionMatch]

# Determine the energy for a few MC particles of specific types
MCEnerElec = np.sqrt(MC_Parts[BoolElec].p**2 + ElecMass**2)

# Calculate some additional quantities which are differences between true and reconstructed values for MC particles with a matched reconstructed track
DeltaEta = MC_Parts[SimID][BoolChargeTrackMatch].eta - Rec_Parts[RecID][BoolChargeTrackMatch].eta
DeltaPhi = MC_Parts[SimID][BoolChargeTrackMatch].phi - Rec_Parts[RecID][BoolChargeTrackMatch].phi
DeltaR = np.sqrt(DeltaEta**2 + DeltaPhi**2)

# Plot some quantities as one image
fig, axs = plt.subplots(2,2, tight_layout=True) # Ironically, this makes things *less* tight
axs[-1, -1].axis('off') # Don't draw any blank subfigs
axs[0,0].hist(ak.flatten(MC_Parts[BoolChargeTrack].eta), bins=100, range=(-5,5),alpha=0.5, color=kP6[1]) # Plot the MC eta values for all charged particles at an MC level
axs[0,0].set_title(r"$\eta_{MC}$ of Charged Particles")
axs[0,0].set(xlabel=r'$\eta_{MC}$', ylabel=r'# Entries / 0.1')
axs[0,1].hist(ak.flatten(MC_Parts[SimID][BoolChargeTrackMatch].eta), bins=100, range=(-5,5),alpha=0.5, color=kP6[1]) # Plot the MC eta values for all charged particles at an MC level that have a matching reconstructed track
axs[0,1].set_title(r"$\eta_{MC}$ of matched Charged Particles")
axs[0,1].set(xlabel=r'$\eta_{MC}$', ylabel=r'# Entries / 0.1')
axs[1,0].hist(ak.flatten(DeltaR), bins=5000, range=(0,5),alpha=0.5, color=kP6[1]) # Plot one of our calculated quantities
axs[1,0].set_title(r"$\Delta R$ of Matched Charged Particles")
axs[1,0].set(xlabel=r'$\Delta R$', ylabel=r'# Entries / 0.001')
plt.savefig("EfficiencyAnalysis_Out.png", dpi = (160))

bins_eta=100
range_eta=(-5,5)
bins_p=100
range_p_elec=(0,25)
range_p_pi=(0,50)

# Make histograms of our scattered electrons and pions - Full truth distributions in P and eta
MC_ScElec_eta = np.histogram(ak.flatten(MC_ScElec.eta), bins = bins_eta, range= range_eta)
MC_Pions_eta = np.histogram(ak.flatten(MC_Pions.eta), bins = bins_eta, range= range_eta)
MC_ScElec_P = np.histogram(ak.flatten(MC_ScElec.p), bins = bins_p, range= range_p_elec)
MC_Pions_P = np.histogram(ak.flatten(MC_Pions.p), bins = bins_p, range= range_p_pi)
# Make histograms of our scattered electrons and pions - Truth distributions for particles that reconstructed in P and eta
Rec_ScElec_eta = np.histogram(ak.flatten(Rec_ScElec.eta), bins = bins_eta, range= range_eta)
Rec_Pions_eta = np.histogram(ak.flatten(Rec_Pions.eta), bins = bins_eta, range= range_eta)
Rec_ScElec_P = np.histogram(ak.flatten(Rec_ScElec.p), bins = bins_p, range= range_p_elec)
Rec_Pions_P = np.histogram(ak.flatten(Rec_Pions.p), bins = bins_p, range= range_p_pi)
# Divide to get efficiency
with np.errstate(divide='ignore'):
    Eff_ScElec_eta = Rec_ScElec_eta[0]/MC_ScElec_eta[0]
    Eff_Pion_eta = Rec_Pions_eta[0]/MC_Pions_eta[0]
    Eff_ScElec_P = Rec_ScElec_P[0]/MC_ScElec_P[0]
    Eff_Pion_P = Rec_Pions_P[0]/MC_Pions_P[0]

Eff_ScElec_eta=np.nan_to_num(Eff_ScElec_eta,nan=0,posinf=0)
Eff_Pion_eta=np.nan_to_num(Eff_Pion_eta,nan=0,posinf=0)
Eff_ScElec_P=np.nan_to_num(Eff_ScElec_P,nan=0,posinf=0)
Eff_Pion_P=np.nan_to_num(Eff_Pion_P,nan=0,posinf=0)

fig, axs = plt.subplots(2,2, tight_layout=True) # Ironically, this makes things *less* tight
#axs[-1, -1].axis('off') # Don't draw any blank subfigs
Bin_Edges=Rec_ScElec_eta[1]
Bars=0.5 * (Bin_Edges[1:] + Bin_Edges[:-1])
BarWidth=Bars[1]-Bars[0]
axs[0,0].bar(Bars, Eff_ScElec_eta, width=BarWidth, alpha=0.75, color=kP6[1]) # Plot the MC eta values for all charged particles at an MC level
axs[0,0].set_title(r"Reconstructed $e'$ Efficiency as fn of $\eta$")
axs[0,0].set(xlabel=r'"$\eta$', ylabel=r"$e'$ Effiency")
Bin_Edges=Rec_ScElec_P[1]
Bars=0.5 * (Bin_Edges[1:] + Bin_Edges[:-1])
BarWidth=Bars[1]-Bars[0]
axs[0,1].bar(Bars, Eff_ScElec_P, width=BarWidth, alpha=0.75, color=kP6[1]) # Plot the MC eta values for all charged particles at an MC level that have a matching reconstructed track
axs[0,1].set_title(r"Reconstructed $e'$ Efficiency as fn of $P$")
axs[0,1].set(xlabel=r"$P_{e'}$", ylabel=r"$P_{e'}$")
Bin_Edges=Rec_Pions_eta[1]
Bars=0.5 * (Bin_Edges[1:] + Bin_Edges[:-1])
BarWidth=Bars[1]-Bars[0]
axs[1,0].bar(Bars, Eff_Pion_eta, width=BarWidth, alpha=0.75, color=kP6[1]) # Plot one of our calculated quantities
axs[1,0].set_title(r"Reconstructed $\pi$ Efficiency as fn of $\eta$")
axs[1,0].set(xlabel=r"$\eta$", ylabel=r"$\pi$ Effiency")
Bin_Edges=Rec_Pions_P[1]
Bars=0.5 * (Bin_Edges[1:] + Bin_Edges[:-1])
BarWidth=Bars[1]-Bars[0]
axs[1,1].bar(Bars, Eff_Pion_P, width=BarWidth, alpha=0.75, color=kP6[1]) # Plot one of our calculated quantities
axs[1,1].set_title(r"Reconstructed $\pi$ Efficiency as fn of $P$")
axs[1,1].set(xlabel=r"$P_{\pi}$", ylabel=r"$\pi$ Effiency")
plt.savefig("EfficiencyAnalysis_Exercise_Out.png", dpi = (160))

Pythonic_ResolutionAnalysis.py

Create a file called ResolutionAnalysis.py and copy in the code below to get started on the efficiency analysis exercise. Note that you will need to correctly specify your input file path in the variable fname.

PYTHON

#! /usr/bin/python
# Import some relevant packages
import uproot as up
import awkward as ak
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.ticker as ticker
import matplotlib.cm as cm
import matplotlib.pylab as plt
import scipy, vector, os
from XRootD import client
from scipy import stats
from matplotlib import pyplot as plt
from matplotlib.gridspec import GridSpec
from matplotlib import colors as colours

# Set some matplot lib features
plt.rcParams['ytick.direction'] = 'in'
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['xaxis.labellocation'] = 'right'
plt.rcParams['yaxis.labellocation'] = 'top'
plt.rcParams["figure.figsize"] = (16,9)
kP6 = ['#5790fc','#f89c20','#e42536','#964a8b','#9c9ca1','#7a21dd'] # Set ROOT kP6 colours - see https://root.cern.ch/doc/v636/classTColor.html

# Open our file
fname = "INPUT_FILE.root"
if os.path.isfile(fname):
    file=up.open(fname)
else:
    print("Error opening file - ", fname, " check your fname variable!")

# Open the tree
tree = file['events']

# Convert relevant branches to arrays
MCPartBr = tree["MCParticles"].arrays()
RecoAssocRec = tree['_ReconstructedChargedParticleAssociations_rec'].arrays()
RecoAssocSim = tree['_ReconstructedChargedParticleAssociations_sim'].arrays()
ReconChPartBr = tree["ReconstructedChargedParticles"].arrays()

RecID=RecoAssocRec['_ReconstructedChargedParticleAssociations_rec.index'] # Array of reconstructed IDs
SimID=RecoAssocSim['_ReconstructedChargedParticleAssociations_sim.index'] # Array of simulated IDs

# Create some filters, anything with [SimID] or [RecID] will index the event by the associations. This means we will only retain events with a matching truth particle/matching reconstructed particle
BoolChargeTrack = ((abs(MCPartBr["MCParticles.charge"])!=0) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolChargeTrackMatch = ((abs(MCPartBr["MCParticles.charge"][SimID])!=0) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1))
BoolElec=((abs(MCPartBr["MCParticles.PDG"])==11) & (MCPartBr["MCParticles.generatorStatus"]==1))

# Define some doubles
ElecMass = 511*(10**-6) # Electron mass in GeV

# Convert some branches into arrays of vectors
MC_Parts = vector.zip({'px': MCPartBr["MCParticles.momentum.x"], 'py': MCPartBr["MCParticles.momentum.y"], 'pz': MCPartBr["MCParticles.momentum.z"]})
Rec_Parts = vector.zip({'px': ReconChPartBr["ReconstructedChargedParticles.momentum.x"], 'py': ReconChPartBr["ReconstructedChargedParticles.momentum.y"], 'pz': ReconChPartBr["ReconstructedChargedParticles.momentum.z"], 'E':ReconChPartBr["ReconstructedChargedParticles.energy"]})

# Determine the energy for a few MC particles of specific types
MCEnerElec = np.sqrt(MC_Parts[BoolElec].p**2 + ElecMass**2)

# Calculate some additional quantities which are differences between true and reconstructed values for MC particles with a matched reconstructed track
DeltaEta = MC_Parts[SimID][BoolChargeTrackMatch].eta - Rec_Parts[RecID][BoolChargeTrackMatch].eta
DeltaPhi = MC_Parts[SimID][BoolChargeTrackMatch].phi - Rec_Parts[RecID][BoolChargeTrackMatch].phi
DeltaP = MC_Parts[SimID][BoolChargeTrackMatch].p - Rec_Parts[RecID][BoolChargeTrackMatch].p
DeltaR = np.sqrt(DeltaEta**2 + DeltaPhi**2)
ResP = ((Rec_Parts[RecID][BoolChargeTrackMatch].p - MC_Parts[SimID][BoolChargeTrackMatch].p)/MC_Parts[SimID][BoolChargeTrackMatch].p ) # Momentum resolution as a percentage
# Plot some quantities as one image
fig, axs = plt.subplots(2,3, tight_layout=True) # Ironically, this makes things *less* tight
axs[-1, -1].axis('off') # Don't draw any blank subfigs
axs[0,0].hist(ak.flatten(DeltaEta), bins=100, range=(-0.25,0.25),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed eta
axs[0,0].set_title(r"$\Delta \eta$ of Matched Charged Particles")
axs[0,0].set(xlabel=r'$\Delta \eta$', ylabel=r'# Entries / 0.005')
axs[0,1].hist(ak.flatten(DeltaPhi), bins=200, range=(-0.2,0.2),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed phi
axs[0,1].set_title(r"$\Delta \phi$ of Matched Charged Particles")
axs[0,1].set(xlabel=r'$\Delta \phi$', ylabel='# Entries / 0.002')
axs[0,2].hist(ak.flatten(DeltaR), bins=300, range=(0,0.3),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed R
axs[0,2].set_title(r"$\Delta R$ of Matched Charged Particles")
axs[0,2].set(xlabel=r'$\Delta R$', ylabel=r'# Entries / 0.003')
axs[1,0].hist(ak.flatten(DeltaP), bins=200, range=(-10,10),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed momentum
axs[1,0].set_title(r"$\Delta P$ of Matched Charged Particles")
axs[1,0].set(xlabel=r'$\Delta \eta$', ylabel=r'# Entries / 0.1 GeV/c')
axs[1,1].hist(ak.flatten(ResP), bins=400, range=(-2,2),alpha=0.5, color=kP6[1]) # Plot the momentum resolution
axs[1,1].set_title(r"Momentum Resolution of Matched Charged Particles")
axs[1,1].set(xlabel=r'$(P_{Rec} - P_{MC})/P_{MC}$', ylabel=r'# Entries / 0.01')
plt.savefig("ResolutionAnalysis_Out.png", dpi = (160))

“Complete” exercise example included below

PYTHON


#! /usr/bin/python
# Import some relevant packages
import uproot as up
import awkward as ak
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.ticker as ticker
import matplotlib.cm as cm
import matplotlib.pylab as plt
import scipy, vector, os
from XRootD import client
from scipy import stats
from matplotlib import pyplot as plt
from matplotlib.gridspec import GridSpec
from matplotlib import colors as colours

# Set some matplot lib features
plt.rcParams['ytick.direction'] = 'in'
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['xaxis.labellocation'] = 'right'
plt.rcParams['yaxis.labellocation'] = 'top'
plt.rcParams["figure.figsize"] = (16,9)
kP6 = ['#5790fc','#f89c20','#e42536','#964a8b','#9c9ca1','#7a21dd'] # Set ROOT kP6 colours - see https://root.cern.ch/doc/v636/classTColor.html

# Open our file
fname = "YOUR_INPUT_FILE"
if os.path.isfile(fname):
    file=up.open(fname)
else:
    print("Error opening file - ", fname, " check your fname variable!")

# Open the tree
tree = file['events']

# Convert relevant branches to arrays
MCPartBr = tree["MCParticles"].arrays()
RecoAssocRec = tree['_ReconstructedChargedParticleAssociations_rec'].arrays()
RecoAssocSim = tree['_ReconstructedChargedParticleAssociations_sim'].arrays()
ReconChPartBr = tree["ReconstructedChargedParticles"].arrays()

RecID=RecoAssocRec['_ReconstructedChargedParticleAssociations_rec.index'] # Array of reconstructed IDs
SimID=RecoAssocSim['_ReconstructedChargedParticleAssociations_sim.index'] # Array of simulated IDs

# Create some filters, anything with [SimID] or [RecID] will index the event by the associations. This means we will only retain events with a matching truth particle/matching reconstructed particle
MCStatus = MCPartBr['MCParticles.generatorStatus'] == 1
MCNegCharge = MCPartBr['MCParticles.charge'] == -1
MCScElecPDG = MCPartBr['MCParticles.PDG'] == 11
MCPionPDG = abs(MCPartBr['MCParticles.PDG']) == 211
BoolChargeTrack = ((abs(MCPartBr["MCParticles.charge"])!=0) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolChargeTrackMatch = ((abs(MCPartBr["MCParticles.charge"][SimID])!=0) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1))
BoolElec=((abs(MCPartBr["MCParticles.PDG"])==11) & (MCPartBr["MCParticles.generatorStatus"]==1))
BoolElecMatch=((MCPartBr["MCParticles.PDG"][SimID]==11) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1))
BoolPionMatch=((abs(MCPartBr["MCParticles.PDG"][SimID])==211) & (MCPartBr["MCParticles.generatorStatus"][SimID]==1)) # Use abs to include both positive and negative pions

# Define some doubles
ElecMass = 511*(10**-6) # Electron mass in GeV

# Convert some branches into arrays of vectors
MC_Parts = vector.zip({'px': MCPartBr["MCParticles.momentum.x"], 'py': MCPartBr["MCParticles.momentum.y"], 'pz': MCPartBr["MCParticles.momentum.z"]})
Rec_Parts = vector.zip({'px': ReconChPartBr["ReconstructedChargedParticles.momentum.x"], 'py': ReconChPartBr["ReconstructedChargedParticles.momentum.y"], 'pz': ReconChPartBr["ReconstructedChargedParticles.momentum.z"], 'E':ReconChPartBr["ReconstructedChargedParticles.energy"]})

# We redfine MC vects here to ONLY be the MC particles that have a reconstructed track
MC_Vects = vector.zip({'px': MCPartBr["MCParticles.momentum.x"][SimID], 'py': MCPartBr["MCParticles.momentum.y"][SimID], 'pz': MCPartBr["MCParticles.momentum.z"][SimID]}) 
MC_ScElec = MC_Vects[BoolElecMatch]
MC_Pions = MC_Vects[BoolPionMatch]
# In this case, we need to access our reconstructed charged particles branch and index it by the reconstructed ID.
RecVects = vector.zip({'px': ReconChPartBr["ReconstructedChargedParticles.momentum.x"][RecID], 'py': ReconChPartBr["ReconstructedChargedParticles.momentum.y"][RecID], 'pz': ReconChPartBr["ReconstructedChargedParticles.momentum.z"][RecID]})

Rec_ScElec = RecVects[BoolElecMatch]
Rec_Pions = RecVects[BoolPionMatch]
ElecMomRes = ((Rec_ScElec.p - MC_ScElec.p)/MC_ScElec.p)*100
ElecEtaRes = ((Rec_ScElec.eta - MC_ScElec.eta)/MC_ScElec.eta)*100
PiMomRes = ((Rec_Pions.p - MC_Pions.p)/MC_Pions.p)*100
PiEtaRes = ((Rec_Pions.eta - MC_Pions.eta)/MC_Pions.eta)*100

# Determine the energy for a few MC particles of specific types
MCEnerElec = np.sqrt(MC_Parts[BoolElec].p**2 + ElecMass**2)

# Calculate some additional quantities which are differences between true and reconstructed values for MC particles with a matched reconstructed track
DeltaEta = MC_Parts[SimID][BoolChargeTrackMatch].eta - Rec_Parts[RecID][BoolChargeTrackMatch].eta
DeltaPhi = MC_Parts[SimID][BoolChargeTrackMatch].phi - Rec_Parts[RecID][BoolChargeTrackMatch].phi
DeltaP = MC_Parts[SimID][BoolChargeTrackMatch].p - Rec_Parts[RecID][BoolChargeTrackMatch].p
DeltaR = np.sqrt(DeltaEta**2 + DeltaPhi**2)
ResP = ((Rec_Parts[RecID][BoolChargeTrackMatch].p - MC_Parts[SimID][BoolChargeTrackMatch].p)/MC_Parts[SimID][BoolChargeTrackMatch].p ) # Momentum resolution as a percentage
# Plot some quantities as one image
fig, axs = plt.subplots(2,3, tight_layout=True) # Ironically, this makes things *less* tight
axs[-1, -1].axis('off') # Don't draw any blank subfigs
axs[0,0].hist(ak.flatten(DeltaEta), bins=100, range=(-0.25,0.25),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed eta
axs[0,0].set_title(r"$\Delta \eta$ of Matched Charged Particles")
axs[0,0].set(xlabel=r'$\Delta \eta$', ylabel=r'# Entries / 0.005')
axs[0,1].hist(ak.flatten(DeltaPhi), bins=200, range=(-0.2,0.2),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed phi
axs[0,1].set_title(r"$\Delta \phi$ of Matched Charged Particles")
axs[0,1].set(xlabel=r'$\Delta \phi$', ylabel='# Entries / 0.002')
axs[0,2].hist(ak.flatten(DeltaR), bins=300, range=(0,0.3),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed R
axs[0,2].set_title(r"$\Delta R$ of Matched Charged Particles")
axs[0,2].set(xlabel=r'$\Delta R$', ylabel=r'# Entries / 0.003')
axs[1,0].hist(ak.flatten(DeltaP), bins=200, range=(-10,10),alpha=0.5, color=kP6[1]) # Plot the difference between true and reconstructed momentum
axs[1,0].set_title(r"$\Delta P$ of Matched Charged Particles")
axs[1,0].set(xlabel=r'$\Delta \eta$', ylabel=r'# Entries / 0.1 GeV/c')
axs[1,1].hist(ak.flatten(ResP), bins=400, range=(-2,2),alpha=0.5, color=kP6[1]) # Plot the momentum resolution
axs[1,1].set_title(r"Momentum Resolution of Matched Charged Particles")
axs[1,1].set(xlabel=r'$(P_{Rec} - P_{MC})/P_{MC}$', ylabel=r'# Entries / 0.01')
plt.savefig("ResolutionAnalysis_Out.png", dpi = (160))

fig, axs = plt.subplots(2,3, tight_layout=True) # Ironically, this makes things *less* tight
axs[0,0].hist(ak.flatten(ElecMomRes), bins=50, range=(-25,25),alpha=0.5, color=kP6[1], label=r"$e'$")
axs[0,0].hist(ak.flatten(PiMomRes), bins=50, range=(-25,25),alpha=0.25, color=kP6[0], label=r"$\pi$")
axs[0,0].set_title(r"Reconstructed $P$ Resolution")
axs[0,0].set(xlabel=r"$P$ Resolution [%]", ylabel=r'# Entries')
axs[0,0].legend(loc='upper right')
axs[0,1].hist(ak.flatten(ElecEtaRes), bins=100, range=(-2,2),alpha=0.5, color=kP6[1], label=r"$e'$")
axs[0,1].hist(ak.flatten(PiEtaRes), bins=100, range=(-2,2),alpha=0.25, color=kP6[0], label=r"$\pi$")
axs[0,1].set_title(r"Reconstructed $\eta$ Resolution")
axs[0,1].set(xlabel=r"$\eta$ Resolution [%]", ylabel=r'# Entries')
axs[0,1].legend(loc='upper right')
# 2D plots
Hist2D1 = axs[0,2].hist2d(np.asarray(ak.flatten(ElecMomRes)), np.asarray(ak.flatten(MC_ScElec.p)), bins=[50,50], range=[[-25,25],[0,20]], cmin=1)
axs[0,2].set_title(r"Reconstructed $P_{e'}$ Resolution as a function of $P_{e'MC}$")
axs[0,2].set(xlabel=r"$P_{e'}$ Resolution [%]", ylabel=r"$P_{e'MC}$")
cb1=plt.colorbar(Hist2D1[3],ax=axs[0,2]) # [3] is the z axis info
cb1.set_label('Counts/bin')
Hist2D2=axs[1,0].hist2d(np.asarray(ak.flatten(PiMomRes)), np.asarray(ak.flatten(MC_Pions.p)), bins=[50,50], range=[[-25,25],[0,20]], cmin=1)
axs[1,0].set_title(r"Reconstructed $P_{\pi}$ Resolution as a function of $P_{\pi MC}$")
axs[1,0].set(xlabel=r"$P_{\pi}$ Resolution [%]", ylabel=r"$P_{\pi'MC}$")
cb2=plt.colorbar(Hist2D2[3],ax=axs[1,0]) # [3] is the z axis info
cb2.set_label('Counts/bin')
Hist2D3=axs[1,1].hist2d(np.asarray(ak.flatten(ElecEtaRes)), np.asarray(ak.flatten(MC_ScElec.eta)), bins=[100,100], range=[[-2,2],[-5,5]], cmin=1)
axs[1,1].set_title(r"Reconstructed $\eta_{e'}$ Resolution as a function of $\eta_{e'MC}$")
axs[1,1].set(xlabel=r"$\eta_{e'}$ Resolution [%]", ylabel=r"$\eta_{e'MC}$")
cb3=plt.colorbar(Hist2D3[3],ax=axs[1,1]) # [3] is the z axis info
cb3.set_label('Counts/bin')
Hist2D4=axs[1,2].hist2d(np.asarray(ak.flatten(PiEtaRes)), np.asarray(ak.flatten(MC_Pions.eta)), bins=[100,100], range=[[-2,2],[-5,5]], cmin=1)
axs[1,2].set_title(r"Reconstructed $\eta_{e'}$ Resolution as a function of $\eta_{e'MC}$")
axs[1,2].set(xlabel=r"$\eta_{\pi}$ Resolution [%]", ylabel=r"$\eta_{\pi MC}$")
cb4=plt.colorbar(Hist2D4[3],ax=axs[1,2]) # [3] is the z axis info
cb4.set_label('Counts/bin')
plt.savefig("ResolutionAnalysis_Exercise_Out.png", dpi = (160))

EfficiencyAnalysis.py

Create a file called EfficiencyAnalysis.py and copy in the code below to get started on the efficiency analysis exercise. Note that you will need to correctly specify your input file path in the variable infile.

PYTHON

#! /usr/bin/python

#Import relevant packages
import ROOT, math, array
from ROOT import TH1F, TH2F, TMath, TTree, TVector3, TVector2
import uproot as up

#Define and open files
infile="PATH_TO_INPUT_FILE"
ofile=ROOT.TFile.Open("EfficiencyAnalysis_OutPy.root", "RECREATE")

# Open input file and define branches we want to look at with uproot
events_tree = up.open(infile)["events"]

# Get particle information
partGenStat = events_tree["MCParticles.generatorStatus"].array()
partMomX = events_tree["MCParticles.momentum.x"].array()
partMomY = events_tree["MCParticles.momentum.y"].array()
partMomZ = events_tree["MCParticles.momentum.z"].array()
partPdg = events_tree["MCParticles.PDG"].array()

# Get reconstructed track information
trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()

# Get assocations between MCParticles and ReconstructedChargedParticles
recoAssoc = events_tree["_ReconstructedChargedParticleAssociations_rec.index"].array()
simuAssoc = events_tree["_ReconstructedChargedParticleAssociations_sim.index"].array()

# Define histograms below
partEta = ROOT.TH1D("partEta","Eta of Thrown Charged Particles;Eta",100, -5 ,5 )
matchedPartEta = ROOT.TH1D("matchedPartEta","Eta of Thrown Charged Particles That Have Matching Track", 100, -5 ,5);
matchedPartTrackDeltaR = ROOT.TH1D("matchedPartTrackDeltaR","Delta R Between Matching Thrown and Reconstructed Charge Particle", 5000, 0, 5);

# Add main analysis loop(s) below
for i in range(0, len(partGenStat)): # Loop over all events
    for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
        if partGenStat[i][j] == 1: # Select stable particles
            pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
            if(pdg == 11 or pdg == 13 or pdg == 211 or pdg == 321 or pdg == 2212):
                trueMom = ROOT.TVector3(partMomX[i][j], partMomY[i][j], partMomZ[i][j])
                trueEta = trueMom.PseudoRapidity()
                truePhi = trueMom.Phi()
                partEta.Fill(trueEta)
                for k in range(0,len(simuAssoc[i])): # Loop over associations to find matching ReconstructedChargedParticle
                    if (simuAssoc[i][k] == j):
                        recMom = ROOT.TVector3(trackMomX[i][recoAssoc[i][k]], trackMomY[i][recoAssoc[i][k]], trackMomZ[i][recoAssoc[i][k]])
                        deltaEta = trueEta - recMom.PseudoRapidity()
                        deltaPhi = TVector2. Phi_mpi_pi(truePhi - recMom.Phi())
                        deltaR = math.sqrt((deltaEta*deltaEta) + (deltaPhi*deltaPhi))
                        matchedPartEta.Fill(trueEta)
                        matchedPartTrackDeltaR.Fill(deltaR)
                        
# Write output histograms to file below
partEta.Write()
matchedPartEta.Write()
matchedPartTrackDeltaR.Write()

# Close files
ofile.Close()

A “solution” version of the script for the exercise is included below -

PYTHON

#! /usr/bin/python

#Import relevant packages
import ROOT, math, array
from ROOT import TCanvas, TColor, TGaxis, TH1F, TH2F, TPad, TStyle, gStyle, gPad, TGaxis, TLine, TMath, TPaveText, TTree, TVector3, TVector2
import uproot as up

#Define and open files
infile="PATH_TO_FILE"
ofile=ROOT.TFile.Open("EfficiencyAnalysis_Exercise_OutPy.root", "RECREATE")

# Open input file and define branches we want to look at with uproot
events_tree = up.open(infile)["events"]

# Get particle information
partGenStat = events_tree["MCParticles.generatorStatus"].array()
partMomX = events_tree["MCParticles.momentum.x"].array()
partMomY = events_tree["MCParticles.momentum.y"].array()
partMomZ = events_tree["MCParticles.momentum.z"].array()
partPdg = events_tree["MCParticles.PDG"].array()

# Get reconstructed track information
trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()

# Get assocations between MCParticles and ReconstructedChargedParticles
recoAssoc = events_tree["_ReconstructedChargedParticleAssociations_rec.index"].array()
simuAssoc = events_tree["_ReconstructedChargedParticleAssociations_sim.index"].array()

# Define histograms below
partEta = ROOT.TH1D("partEta","#eta of Thrown Charged Particles; #eta", 120, -6, 6)
matchedPartEta = ROOT.TH1D("matchedPartEta","#eta of Thrown Charged Particles That Have Matching Track; #eta", 120, -6, 6)
partMom = ROOT.TH1D("partMom", "Momentum of Thrown Charged Particles (truth); P(GeV/c)", 150, 0, 150)
matchedPartMom = ROOT.TH1D("matchedPartMom", "Momentum of Thrown Charged Particles (truth), with matching track; P(GeV/c)", 150, 0, 150)
partPhi = ROOT.TH1D("partPhi", "#phi of Thrown Charged Particles (truth); #phi(rad)", 320, -3.2, 3.2)
matchedPartPhi = ROOT.TH1D("matchedPartPhi", "#phi of Thrown Charged Particles (truth), with matching track; #phi(rad)", 320, -3.2, 3.2)

partPEta = ROOT.TH2D("partPEta", "P vs #eta of Thrown Charged Particles; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6)
matchedPartPEta = ROOT.TH2D("matchedPartPEta", "P vs #eta of Thrown Charged Particles, with matching track; P(GeV/C); #eta", 150, 0, 150, 120, -6, 6)
partPhiEta = ROOT.TH2D("partPhiEta", "#phi vs #eta of Thrown Charged Particles; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6)
matchedPartPhiEta = ROOT.TH2D("matchedPartPhiEta", "#phi vs #eta of Thrown Charged Particles; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6)

matchedPartTrackDeltaEta = ROOT.TH1D("matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructe Charged Particle; #Delta#eta", 100, -0.25, 0.25)
matchedPartTrackDeltaPhi = ROOT.TH1D("matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2)
matchedPartTrackDeltaR = ROOT.TH1D("matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle; #Delta R", 300, 0, 0.3)
matchedPartTrackDeltaMom = ROOT.TH1D("matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10)

# Define some histograms for our efficiencies
TrackEff_Eta = ROOT.TH1D("TrackEff_Eta", "Tracking efficiency as fn of #eta; #eta; Eff(%)", 120, -6, 6)
TrackEffMom = ROOT.TH1D("TrackEff_Mom", "Tracking efficiency as fn of P; P(GeV/c); Eff(%)", 150, 0, 150)
TrackEffPhi = ROOT.TH1D("TrackEff_Phi", "Tracking efficiency as fn of #phi; #phi(rad); Eff(%)", 320, -3.2, 3.2)

# 2D Efficiencies
TrackEff_PEta = ROOT.TH2D("TrackEff_PEta", "Tracking efficiency as fn of P and #eta; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6)
TrackEff_PhiEta = ROOT.TH2D("TrackEff_PhiEta", "Tracking efficiency as fn of #phi and #eta; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6)

# All charged particle histos
ChargedEta = ROOT.TH1D("ChargedEta", "#eta of all charged particles; #eta", 120, -6, 6)
ChargedPhi = ROOT.TH1D("ChargedPhi", "#phi of all charged particles; #phi (rad)", 120, -3.2, 3.2)
ChargedP = ROOT.TH1D("ChargedP", "P of all charged particles; P(GeV/c)", 150, 0, 150)

# Add main analysis loop(s) below
for i in range(0, len(partGenStat)): # Loop over all events
    for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
        if partGenStat[i][j] == 1: # Select stable particles
            pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
            if(pdg == 11 or pdg == 13 or pdg == 211 or pdg == 321 or pdg == 2212):
                trueMom = ROOT.TVector3(partMomX[i][j], partMomY[i][j], partMomZ[i][j])
                trueEta = trueMom.PseudoRapidity()
                truePhi = trueMom.Phi()
                partEta.Fill(trueEta)
                partPhi.Fill(truePhi)
                partMom.Fill(trueMom.Mag())
                partPEta.Fill(trueMom.Mag(), trueEta)
                partPhiEta.Fill(truePhi, trueEta)
                for k in range(0,len(simuAssoc[i])): # Loop over associations to find matching ReconstructedChargedParticle
                    if (simuAssoc[i][k] == j):
                        recMom = ROOT.TVector3(trackMomX[i][recoAssoc[i][k]], trackMomY[i][recoAssoc[i][k]], trackMomZ[i][recoAssoc[i][k]])
                        deltaEta = trueEta - recMom.PseudoRapidity()
                        deltaPhi = TVector2. Phi_mpi_pi(truePhi - recMom.Phi())
                        deltaR = math.sqrt((deltaEta*deltaEta) + (deltaPhi*deltaPhi))
                        deltaMom = ((trueMom.Mag()) - (recMom.Mag()))
                        matchedPartTrackDeltaEta.Fill(deltaEta)
                        matchedPartTrackDeltaPhi.Fill(deltaPhi)
                        matchedPartTrackDeltaR.Fill(deltaR)
                        matchedPartTrackDeltaMom.Fill(deltaMom)
                        matchedPartEta.Fill(trueEta)
                        matchedPartPhi.Fill(truePhi)
                        matchedPartMom.Fill(trueMom.Mag())
                        matchedPartPEta.Fill(trueMom.Mag(), trueEta)
                        matchedPartPhiEta.Fill(truePhi, trueEta)
    for x in range (0, len(trackMomX[i])): # Loop over all charged particles, thrown or not
        CPartMom = ROOT.TVector3(trackMomX[i][x], trackMomY[i][x], trackMomZ[i][x])
        CPartEta = CPartMom.PseudoRapidity()
        CPartPhi = CPartMom.Phi()
        ChargedEta.Fill(CPartEta)
        ChargedPhi.Fill(CPartPhi)
        ChargedP.Fill(CPartMom.Mag())
	
# Write output histograms to file below
partEta.Write()
matchedPartEta.Write()
partMom.Write()
matchedPartMom.Write()
partPhi.Write()
matchedPartPhi.Write()
partPEta.Write()
matchedPartPEta.Write()
partPhiEta.Write()
matchedPartPhiEta.Write()
matchedPartTrackDeltaEta.Write()
matchedPartTrackDeltaPhi.Write()
matchedPartTrackDeltaR.Write()
matchedPartTrackDeltaMom.Write()
ChargedEta.Write()
ChargedPhi.Write()
ChargedP.Write()
TrackEff_Eta.Divide(matchedPartEta, partEta, 1, 1, "b")
TrackEffMom.Divide(matchedPartMom, partMom, 1, 1, "b")
TrackEffPhi.Divide(matchedPartPhi, partPhi, 1, 1, "b")
TrackEff_PEta.Divide(matchedPartPEta, partPEta, 1, 1, "b")
TrackEff_PhiEta.Divide(matchedPartPhiEta, partPhiEta, 1, 1, "b")
TrackEff_Eta.Write()
TrackEffMom.Write()
TrackEffPhi.Write()
TrackEff_PEta.Write()
TrackEff_PhiEta.Write()

# Close files
ofile.Close()

Insert your input file path and execute as the example code above. ### ResolutionAnalysis.py

Create a file called ResolutionAnalysis.py and copy in the code below to get started on the resolution analysis exercise. Note that you will need to correctly specify your input file path in the variable infile.

PYTHON

#! /usr/bin/python

#Import relevant packages
import ROOT, math, array
from ROOT import TH1F, TH2F, TMath, TTree, TVector3, TVector2
import uproot as up

#Define and open files
infile="PATH_TO_INPUT_FILE"
ofile=ROOT.TFile.Open("ResolutionAnalysis_OutPy.root", "RECREATE")

# Open input file and define branches we want to look at with uproot
events_tree = up.open(infile)["events"]

# Get particle information
partGenStat = events_tree["MCParticles.generatorStatus"].array()
partMomX = events_tree["MCParticles.momentum.x"].array()
partMomY = events_tree["MCParticles.momentum.y"].array()
partMomZ = events_tree["MCParticles.momentum.z"].array()
partPdg = events_tree["MCParticles.PDG"].array()

# Get reconstructed track information
trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()

# Get assocations between MCParticles and ReconstructedChargedParticles
recoAssoc = events_tree["_ReconstructedChargedParticleAssociations_rec.index"].array()
simuAssoc = events_tree["_ReconstructedChargedParticleAssociations_sim.index"].array()

# Define histograms below
trackMomentumRes = ROOT.TH1D("trackMomentumRes","Track Momentum Resolution", 400, -2, 2)

matchedPartTrackDeltaEta = ROOT.TH1D("matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructe Charged Particle; #Delta#eta", 100, -0.25, 0.25)
matchedPartTrackDeltaPhi = ROOT.TH1D("matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2)
matchedPartTrackDeltaR = ROOT.TH1D("matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle; #Delta R", 300, 0, 0.3)
matchedPartTrackDeltaMom = ROOT.TH1D("matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10)

# Add main analysis loop(s) below
for i in range(0, len(partGenStat)): # Loop over all events
    for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
        if partGenStat[i][j] == 1: # Select stable particles
            pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
            if(pdg == 11 or pdg == 13 or pdg == 211 or pdg == 321 or pdg == 2212):
                trueMom = ROOT.TVector3(partMomX[i][j], partMomY[i][j], partMomZ[i][j])
                trueEta = trueMom.PseudoRapidity()
                truePhi = trueMom.Phi()
                for k in range(0,len(simuAssoc[i])): # Loop over associations to find matching ReconstructedChargedParticle
                    if (simuAssoc[i][k] == j):
                        recMom = ROOT.TVector3(trackMomX[i][recoAssoc[i][k]], trackMomY[i][recoAssoc[i][k]], trackMomZ[i][recoAssoc[i][k]])
                        deltaEta = trueEta - recMom.PseudoRapidity()
                        deltaPhi = TVector2. Phi_mpi_pi(truePhi - recMom.Phi())
                        deltaR = math.sqrt((deltaEta*deltaEta) + (deltaPhi*deltaPhi))
                        deltaMom = ((trueMom.Mag()) - (recMom.Mag()))
                        momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()
                        matchedPartTrackDeltaEta.Fill(deltaEta)
                        matchedPartTrackDeltaPhi.Fill(deltaPhi)
                        matchedPartTrackDeltaR.Fill(deltaR)
                        matchedPartTrackDeltaMom.Fill(deltaMom)
                        trackMomentumRes.Fill(momRes)
                        
# Write output histograms to file below
trackMomentumRes.Write()
matchedPartTrackDeltaEta.Write()
matchedPartTrackDeltaPhi.Write()
matchedPartTrackDeltaR.Write()
matchedPartTrackDeltaMom.Write()

# Close files
ofile.Close()

A “solution” version of the script for the exercise is included below -

PYTHON

#! /usr/bin/python

#Import relevant packages
import ROOT, math, array
from ROOT import TCanvas, TColor, TGaxis, TH1F, TH2F, TPad, TStyle, gStyle, gPad, TGaxis, TLine, TMath, TPaveText, TTree, TVector3, TVector2
import uproot as up

#Define and open files
infile="PATH_TO_FILE"
ofile=ROOT.TFile.Open("ResolutionAnalysis_Exercise_OutPy.root", "RECREATE")

# Open input file and define branches we want to look at with uproot
events_tree = up.open(infile)["events"]

# Get particle information
partGenStat = events_tree["MCParticles.generatorStatus"].array()
partMomX = events_tree["MCParticles.momentum.x"].array()
partMomY = events_tree["MCParticles.momentum.y"].array()
partMomZ = events_tree["MCParticles.momentum.z"].array()
partPdg = events_tree["MCParticles.PDG"].array()

# Get reconstructed track information
trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()

# Get assocations between MCParticles and ReconstructedChargedParticles
recoAssoc = events_tree["_ReconstructedChargedParticleAssociations_rec.index"].array()
simuAssoc = events_tree.["_ReconstructedChargedParticleAssociations_sim.index"].array()

# Define histograms below
trackMomentumRes = ROOT.TH1D("trackMomentumRes","Track Momentum Resolution", 400, -2, 2)
trackMomResP = ROOT.TH2D("trackMomResP", "Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
trackMomResEta = ROOT.TH2D("trackMomResEta", "Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

trackMomentumRes_e = ROOT.TH1D("trackMomentumRes_e","e^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
trackMomResP_e = ROOT.TH2D("trackMomResP_e", "e^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 25);
trackMomResEta_e = ROOT.TH2D("trackMomResEta_e", "e^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

trackMomentumRes_mu = ROOT.TH1D("trackMomentumRes_mu","#mu^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
trackMomResP_mu = ROOT.TH2D("trackMomResP_mu", "#mu^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 25);
trackMomResEta_mu = ROOT.TH2D("trackMomResEta_mu", "#mu^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

trackMomentumRes_pi = ROOT.TH1D("trackMomentumRes_pi","#pi^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
trackMomResP_pi = ROOT.TH2D("trackMomResP_pi", "#pi^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
trackMomResEta_pi = ROOT.TH2D("trackMomResEta_pi", "#pi^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

trackMomentumRes_K = ROOT.TH1D("trackMomentumRes_K","K^{#pm} Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
trackMomResP_K = ROOT.TH2D("trackMomResP_K", "K^{#pm} Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
trackMomResEta_K = ROOT.TH2D("trackMomResEta_K", "K^{#pm} Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

trackMomentumRes_p = ROOT.TH1D("trackMomentumRes_p","p Track Momentum Resolution; (P_{rec} - P_{MC})/P_{MC}", 400, -2, 2);
trackMomResP_p = ROOT.TH2D("trackMomResP_p", "p Track Momentum Resolution vs P; (P_{rec} - P_{MC})/P_{MC}; P_{MC}(GeV/c)", 400, -2, 2, 150, 0, 150);
trackMomResEta_p = ROOT.TH2D("trackMomResEta_p", "p Track Momentum Resolution vs #eta; (P_{rec} - P_{MC})/P_{MC}; #eta_{MC}", 400, -2, 2, 120, -6, 6);

matchedPartTrackDeltaEta = ROOT.TH1D("matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructe Charged Particle; #Delta#eta", 100, -0.25, 0.25)
matchedPartTrackDeltaPhi = ROOT.TH1D("matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2)
matchedPartTrackDeltaR = ROOT.TH1D("matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle; #Delta R", 300, 0, 0.3)
matchedPartTrackDeltaMom = ROOT.TH1D("matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10)

# Add main analysis loop(s) below
for i in range(0, len(partGenStat)): # Loop over all events
    for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
        if partGenStat[i][j] == 1: # Select stable particles
            pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
            if(pdg == 11 or pdg == 13 or pdg == 211 or pdg == 321 or pdg == 2212):
                trueMom = ROOT.TVector3(partMomX[i][j], partMomY[i][j], partMomZ[i][j])
                trueEta = trueMom.PseudoRapidity()
                truePhi = trueMom.Phi()
                for k in range(0,len(simuAssoc[i])): # Loop over associations to find matching ReconstructedChargedParticle
                    if (simuAssoc[i][k] == j):
                        recMom = ROOT.TVector3(trackMomX[i][recoAssoc[i][k]], trackMomY[i][recoAssoc[i][k]], trackMomZ[i][recoAssoc[i][k]])
                        deltaEta = trueEta - recMom.PseudoRapidity()
                        deltaPhi = TVector2. Phi_mpi_pi(truePhi - recMom.Phi())
                        deltaR = math.sqrt((deltaEta*deltaEta) + (deltaPhi*deltaPhi))
                        deltaMom = ((trueMom.Mag()) - (recMom.Mag()))
                        momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()
                        trackMomentumRes.Fill(momRes)
                        trackMomResP.Fill(momRes, trueMom.Mag())
                        trackMomResEta.Fill(momRes, trueEta)
                        if( pdg == 11):
                            trackMomentumRes_e.Fill(momRes)
                            trackMomResP_e.Fill(momRes, trueMom.Mag())
                            trackMomResEta_e.Fill(momRes, trueEta)
                        elif( pdg == 13):
                            trackMomentumRes_mu.Fill(momRes)
                            trackMomResP_mu.Fill(momRes, trueMom.Mag())
                            trackMomResEta_mu.Fill(momRes, trueEta)
                        elif( pdg == 211):
                            trackMomentumRes_pi.Fill(momRes)
                            trackMomResP_pi.Fill(momRes, trueMom.Mag())
                            trackMomResEta_pi.Fill(momRes, trueEta)
                        elif( pdg == 321):
                            trackMomentumRes_K.Fill(momRes)
                            trackMomResP_K.Fill(momRes, trueMom.Mag())
                            trackMomResEta_K.Fill(momRes, trueEta)
                        elif( pdg == 2212):
                            trackMomentumRes_p.Fill(momRes)
                            trackMomResP_p.Fill(momRes, trueMom.Mag())
                            trackMomResEta_p.Fill(momRes, trueEta)                            
                        matchedPartTrackDeltaEta.Fill(deltaEta)
                        matchedPartTrackDeltaPhi.Fill(deltaPhi)
                        matchedPartTrackDeltaR.Fill(deltaR)
                        matchedPartTrackDeltaMom.Fill(deltaMom)
                        
# Write output histograms to file below
trackMomentumRes.Write()
trackMomResP.Write()
trackMomResEta.Write()
trackMomentumRes_e.Write()
trackMomResP_e.Write()
trackMomResEta_e.Write()
trackMomentumRes_mu.Write()
trackMomResP_mu.Write()
trackMomResEta_mu.Write()
trackMomentumRes_pi.Write()
trackMomResP_pi.Write()
trackMomResEta_pi.Write()
trackMomentumRes_K.Write()
trackMomResP_K.Write()
trackMomResEta_K.Write()
trackMomentumRes_p.Write()
trackMomResP_p.Write()
trackMomResEta_p.Write()
matchedPartTrackDeltaEta.Write()
matchedPartTrackDeltaPhi.Write()
matchedPartTrackDeltaR.Write()
matchedPartTrackDeltaMom.Write()

# Close files
ofile.Close()

Insert your input file path and execute as the example code above. ## RDataFrames Example

Note that only the initial stage of the efficiency example is presented here in RDF format. This example was kindly created by Simon.

EfficiencyAnalysisRDF.C

Create a file called EfficiencyAnalysisRDF.C and paste the code below in. Remember to change the file path.

Execute this script via - root -l -q EfficiencyAnalysisRDF.C++. Do this within eic-shell or somewhere else with the correct EDM4hep/EDM4eic libraries installed.

CPP

#include <edm4hep/utils/vector_utils.h>
#include <edm4hep/MCParticle.h>
#include <edm4eic/ReconstructedParticle.h>
#include <ROOT/RDataFrame.hxx>
#include <ROOT/RVec.hxx>
#include <TFile.h>

// Define aliases for the data types 
using MCP = edm4hep::MCParticleData;
using RecoP = edm4eic::ReconstructedParticleData;

// Define function to vectorize the edm4hep::utils methods
template <typename T>
auto getEta = [](ROOT::VecOps::RVec<T> momenta) {
  return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::eta(p.momentum); });
};

template <typename T>
auto getPhi = [](ROOT::VecOps::RVec<T> momenta) {
  return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::angleAzimuthal(p.momentum); });
};

// Define the function to perform the efficiency analysis
void EfficiencyAnalysisRDF(TString infile="PATH_TO_FILE"){
   
  // Set up input file 
  ROOT::RDataFrame df("events", infile);

  // Define new dataframe node with additional columns
  auto df1 =  df.Define("statusFilter",  "MCParticles.generatorStatus == 1"    )
                .Define("absPDG",        "abs(MCParticles.PDG)"                )
                .Define("pdgFilter",     "absPDG == 11 || absPDG == 13 || absPDG == 211 || absPDG == 321 || absPDG == 2212")
                .Define("particleFilter","statusFilter && pdgFilter"           )
                .Define("filtMCParts",   "MCParticles[particleFilter]"         )
                .Define("assoFilter",    "Take(particleFilter,ReconstructedChargedParticleAssociations_sim.index)") // Incase any of the associated particles happen to not be charged
                .Define("assoMCParts",   "Take(MCParticles,ReconstructedChargedParticleAssociations)sim.index)[assoFilter]")
                .Define("assoRecParts",  "Take(ReconstructedChargedParticles,ReconstructedChargedParticleAssociations._rec.index)[assoFilter]")
                .Define("filtMCEta",     getEta<MCP>   , {"filtMCParts"} )
                .Define("filtMCPhi",     getPhi<MCP>   , {"filtMCParts"} )
                .Define("accoMCEta",     getEta<MCP>   , {"assoMCParts"} )
                .Define("accoMCPhi",     getPhi<MCP>   , {"assoMCParts"} )
                .Define("assoRecEta",    getEta<RecoP> , {"assoRecParts"})
                .Define("assoRecPhi",    getPhi<RecoP> , {"assoRecParts"})
                .Define("deltaR",        "ROOT::VecOps::DeltaR(assoRecEta, accoMCEta, assoRecPhi, accoMCPhi)");

  // Define histograms
  auto partEta                = df1.Histo1D({"partEta","Eta of Thrown Charged Particles;Eta",100,-5.,5.},"filtMCEta");
  auto matchedPartEta         = df1.Histo1D({"matchedPartEta","Eta of Thrown Charged Particles That Have Matching Track",100,-5.,5.},"accoMCEta");
  auto matchedPartTrackDeltaR = df1.Histo1D({"matchedPartTrackDeltaR","Delta R Between Matching Thrown and Reconstructed Charged Particle",5000,0.,5.},"deltaR");

  // Write histograms to file
  TFile *ofile = TFile::Open("EfficiencyAnalysis_Out_RDF.root","RECREATE");

  // Booked Define and Histo1D lazy actions are only performed here
  partEta->Write();
  matchedPartEta->Write();
  matchedPartTrackDeltaR->Write();
      
  ofile->Close(); // Close output file
}

A “solution” using RDataFrames is included below,

CPP

#include <edm4hep/utils/vector_utils.h>
#include <edm4hep/MCParticle.h>
#include <edm4eic/ReconstructedParticle.h>
#include <ROOT/RDataFrame.hxx>
#include <ROOT/RVec.hxx>
#include <TFile.h>

// Define aliases for the data types 
using MCP = edm4hep::MCParticleData;
using RecoP = edm4eic::ReconstructedParticleData;

// Define function to vectorize the edm4hep::utils methods
template <typename T>
auto getEta = [](ROOT::VecOps::RVec<T> momenta) {
  return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::eta(p.momentum); });
};

template <typename T>
auto getPhi = [](ROOT::VecOps::RVec<T> momenta) {
  return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::angleAzimuthal(p.momentum); });
};

template <typename T>
auto getP = [](ROOT::VecOps::RVec<T> momenta) {
  //return ROOT::VecOps::Map(momenta, [](const T& p) { return (p.momentum); }); // This is a vector3f
  return ROOT::VecOps::Map(momenta, [](const T& p) { return edm4hep::utils::magnitude(p.momentum); }); // This is a the magnitude of that vector3f
};

// Define the function to perform the efficiency analysis
void EfficiencyAnalysisRDF_Exercise(TString infile="PATH_TO_INPUT_FILE"){
   
  // Set up input file 
  ROOT::RDataFrame df("events", infile);

  // Define new dataframe node with additional columns
  auto df1 =  df.Define("statusFilter",  "MCParticles.generatorStatus == 1"    )
    .Define("absPDG",        "abs(MCParticles.PDG)"                )
    .Define("pdgFilter",     "absPDG == 11 || absPDG == 13 || absPDG == 211 || absPDG == 321 || absPDG == 2212")
    .Define("particleFilter","statusFilter && pdgFilter"           )
    .Define("filtMCParts",   "MCParticles[particleFilter]"         )
    .Define("assoFilter",    "Take(particleFilter,_ReconstructedChargedParticleAssociations_sim.index)") // In case any of the associated particles happen to not be charged
    .Define("assoMCParts",   "Take(MCParticles,_ReconstructedChargedParticleAssociations_sim.index)[assoFilter]")
    .Define("assoRecParts",  "Take(ReconstructedChargedParticles,_ReconstructedChargedParticleAssociations_rec.index)[assoFilter]")
    .Define("filtMCEta",     getEta<MCP>   , {"filtMCParts"} )
    .Define("filtMCPhi",     getPhi<MCP>   , {"filtMCParts"} )
    .Define("filtMCp",       getP<MCP>     , {"filtMCParts"} )
    .Define("assoMCEta",     getEta<MCP>   , {"assoMCParts"} )
    .Define("assoMCPhi",     getPhi<MCP>   , {"assoMCParts"} )
    .Define("assoMCp",       getP<MCP>     , {"assoMCParts"} )
    .Define("assoRecEta",    getEta<RecoP> , {"assoRecParts"})
    .Define("assoRecPhi",    getPhi<RecoP> , {"assoRecParts"})
    .Define("assoRecp",      getP<RecoP>   , {"assoRecParts"})
    .Define("deltaEta",      "assoMCEta - assoRecEta"        )
    .Define("deltaPhi",      "ROOT::VecOps::DeltaPhi(assoRecPhi, assoMCPhi)")
    .Define("deltaR",        "ROOT::VecOps::DeltaR(assoRecEta, assoMCEta, assoRecPhi, assoMCPhi)")
    .Define("deltaMom",      "assoMCp - assoRecp")
    .Define("recoEta",       getEta<RecoP>,  {"ReconstructedChargedParticles"})
    .Define("recoPhi",       getPhi<RecoP>,  {"ReconstructedChargedParticles"})
    .Define("recoP",         getP<RecoP>,    {"ReconstructedChargedParticles"});

  // Define histograms. We create a histogram with the usual naming/titles/bins/range etc, then specify how to fill the histogram based upon things we have defined for our dataframe
  auto partEta                = df1.Histo1D({"partEta","#eta of Thrown Charged Particles;#eta",120,-6.,6.},"filtMCEta");
  auto matchedPartEta         = df1.Histo1D({"matchedPartEta","#eta of Thrown Charged Particles That Have Matching Track;#eta",120,-6.,6.},"assoMCEta");
  auto partMom = df1.Histo1D({"partMom", "Momentum of Thrown Charged Particles (truth); P(GeV/c)", 150, 0, 150}, "filtMCp");
  auto matchedPartMom = df1.Histo1D({"matchedPartMom", "Momentum of Thrown Charged Particles (truth), with matching track; P(GeV/c)", 150, 0, 150}, "assoMCp");
  auto partPhi = df1.Histo1D({"partPhi", "#phi of Thrown Charged Particles (truth); #phi(rad)", 320, -3.2, 3.2},"filtMCPhi");
  auto matchedPartPhi = df1.Histo1D({"matchedPartPhi", "#phi of Thrown Charged Particles (truth), with matching track; #phi(rad)", 320, -3.2, 3.2}, "assoMCPhi");
  
  auto partPEta = df1.Histo2D({"partPEta", "P vs #eta of Thrown Charged Particles; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6}, "filtMCp", "filtMCEta");
  auto matchedPartPEta = df1.Histo2D({"matchedPartPEta", "P vs #eta of Thrown Charged Particles, with matching track; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6}, "assoMCp", "assoMCEta");
  auto partPhiEta = df1.Histo2D({"partPhiEta", "#phi vs #eta of Thrown Charged Particles; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6}, "filtMCPhi", "filtMCEta");
  auto matchedPartPhiEta = df1.Histo2D({"matchedPartPhiEta", "#phi vs #eta of Thrown Charged Particles; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6}, "assoMCPhi", "assoMCEta");
  
  auto matchedPartTrackDeltaEta = df1.Histo1D({"matchedPartTrackDeltaEta","#Delta#eta Between Matching Thrown and Reconstructed Charged Particle; #Delta#eta", 100, -0.25, 0.25}, "deltaEta");
  auto matchedPartTrackDeltaPhi = df1.Histo1D({"matchedPartTrackDeltaPhi","#Detla #phi Between Matching Thrown and Reconstructed Charged Particle; #Delta#phi", 200, -0.2, 0.2}, "deltaPhi");
  auto matchedPartTrackDeltaR = df1.Histo1D({"matchedPartTrackDeltaR","#Delta R Between Matching Thrown and Reconstructed Charged Particle;#Delta R",300,0.,0.3}, "deltaR");
  auto matchedPartTrackDeltaMom = df1.Histo1D({"matchedPartTrackDeltaMom","#Delta P Between Matching Thrown and Reconstructed Charged Particle; #Delta P", 200, -10, 10}, "deltaMom");
    
  // Define some histograms for our efficiencies - Done "old school" root style - Maybe the division can be done direct from a DF?
  TH1D *TrackEff_Eta = new TH1D("TrackEff_Eta", "Tracking efficiency as fn of #eta; #eta; Eff(%)", 120, -6, 6); 
  TH1D *TrackEff_Mom = new TH1D("TrackEff_Mom", "Tracking efficiency as fn of P; P(GeV/c); Eff(%)", 150, 0, 150); 
  TH1D *TrackEff_Phi = new TH1D("TrackEff_Phi", "Tracking efficiency as fn of #phi; #phi(rad); Eff(%)", 320, -3.2, 3.2);
  // 2D Efficiencies
  TH2D* TrackEff_PEta = new TH2D("TrackEff_PEta", "Tracking efficiency as fn of P and #eta; P(GeV/c); #eta", 150, 0, 150, 120, -6, 6);
  TH2D* TrackEff_PhiEta = new TH2D("TrackEff_PhiEta", "Tracking efficiency as fn of #phi and #eta; #phi(rad); #eta", 160, -3.2, 3.2, 120, -6, 6);

  auto ChargedEta = df1.Histo1D({"ChargedEta", "#eta of all charged particles; #eta", 120, -6, 6}, "recoEta");
  auto ChargedPhi = df1.Histo1D({"ChargedPhi", "#phi of all charged particles; #phi (rad)", 120, -3.2, 3.2}, "recoPhi");
  auto ChargedP = df1.Histo1D({"ChargedP", "P of all charged particles; P(GeV/c)", 150, 0, 150}, "recoP");

  // Write histograms to file
  TFile *ofile = TFile::Open("EfficiencyAnalysis_Exercise_Out_RDF.root","RECREATE");

  // Booked Define and Histo1D lazy actions are only performed here
  partEta->Write();
  matchedPartEta->Write();
  partPhi->Write();
  matchedPartPhi->Write();
  partMom->Write();
  matchedPartMom->Write();
  partPEta->Write();
  matchedPartPEta->Write();
  partPhiEta->Write();
  matchedPartPhiEta->Write();
  matchedPartTrackDeltaEta->Write();
  matchedPartTrackDeltaPhi->Write();
  matchedPartTrackDeltaR->Write();
  matchedPartTrackDeltaMom->Write();

  // Create efficiency histograms by dividing appropriately. Note we must actually get the pointer explicitly.
  TrackEff_Eta->Divide(matchedPartEta.GetPtr(), partEta.GetPtr(), 1, 1, "b");
  TrackEff_Mom->Divide(matchedPartMom.GetPtr(), partMom.GetPtr(), 1, 1, "b");  
  TrackEff_Phi->Divide(matchedPartPhi.GetPtr(), partPhi.GetPtr(), 1, 1, "b");
  TrackEff_PEta->Divide(matchedPartPEta.GetPtr(), partPEta.GetPtr(), 1, 1, "b");
  TrackEff_PhiEta->Divide(matchedPartPhiEta.GetPtr(), partPhiEta.GetPtr(), 1, 1, "b");
  
  TrackEff_Eta->Write();
  TrackEff_Mom->Write();
  TrackEff_Phi->Write();
  TrackEff_PEta->Write();
  TrackEff_PhiEta->Write();

  ChargedEta->Write();
  ChargedPhi->Write();
  ChargedP->Write();
				
  ofile->Close(); // Close output file
}