From 55b053ddec732b82ccbad35aa1eb5fb7f422d206 Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Mon, 16 Mar 2026 09:47:41 +0100 Subject: [PATCH 01/15] Add tracking validation consumer --- .../components/TrackingValidationConsumer.cpp | 1294 +++++++++++++++++ 1 file changed, 1294 insertions(+) create mode 100644 Tracking/components/TrackingValidationConsumer.cpp diff --git a/Tracking/components/TrackingValidationConsumer.cpp b/Tracking/components/TrackingValidationConsumer.cpp new file mode 100644 index 0000000..96195ad --- /dev/null +++ b/Tracking/components/TrackingValidationConsumer.cpp @@ -0,0 +1,1294 @@ +// TrackingValidationConsumer +// +// Validation consumer that writes the following TTrees: +// 1) finder_particle_to_tracks +// 2) finder_track_to_particles +// 3) perfect_particle_to_tracks +// 4) perfect_track_to_particles +// 5) fitter_vs_mc +// 6) fitter_vs_perfect +// +// In finalize(), the consumer also produces summary plots written to the same ROOT file: +// - tracking efficiency vs momentum +// - d0 resolution vs momentum +// - momentum resolution vs momentum +// - transverse-momentum resolution vs momentum +// +// The fitter-vs-perfect tree is filled only when perfect-fitted tracks are provided +// and DoPerfectFit is enabled + +// k4FWCore +#include "k4FWCore/Consumer.h" + +// Gaudi +#include "Gaudi/Property.h" +#include "GaudiKernel/MsgStream.h" + +// EDM4hep +#include "edm4hep/MCParticleCollection.h" +#include "edm4hep/TrackCollection.h" +#include "edm4hep/TrackState.h" +#include "edm4hep/TrackerHitSimTrackerHitLinkCollection.h" + +// podio +#include "podio/ObjectID.h" + +// ROOT +#include "TFile.h" +#include "TTree.h" +#include "TH1F.h" +#include "TGraphErrors.h" +#include "TCanvas.h" +#include "TF1.h" +#include "TStyle.h" + +// STL +#include +#include +#include +#include +#include +#include +#include +#include + +// ---------- helpers ---------- +static inline uint64_t oidKey(const podio::ObjectID& id) { + return (uint64_t(id.collectionID) << 32) | uint64_t(uint32_t(id.index)); +} + +static inline float safeAtan2(float y, float x) { return std::atan2(y, x); } + +static inline float wrapDeltaPhi(float a, float b) { + float d = a - b; + while (d > M_PI) d -= 2.f * M_PI; + while (d < -M_PI) d += 2.f * M_PI; + return d; +} + +struct HelixParams { + float D0 = 0.f; // mm + float Z0 = 0.f; // mm + float phi = 0.f; // rad + float omega = 0.f; // 1/mm + float tanLambda = 0.f; // unitless + float p = 0.f; // GeV + float pT = 0.f; // GeV +}; + +// Constants matching the fitter code +static constexpr float c_mm_s = 2.998e11f; +static constexpr float a_genfit = 1e-15f * c_mm_s; // ~2.998e-4 + +// --- GenFit-like PCAInfo in mm, ported from GenfitTrack::PCAInfo --- +// Returns PCA point (x,y,z) and Phi0 (tangent angle at PCA). +struct PCAInfoHelper { + float pcaX = 0.f; + float pcaY = 0.f; + float pcaZ = 0.f; + float phi0 = 0.f; + bool ok = false; +}; + +// position (x,y,z) in mm, momentum (px,py,pz) in GeV, refPoint in mm +static PCAInfoHelper PCAInfo_mm(float x, float y, float z, + float px, float py, float pz, + int chargeSign, + float refX, float refY, + float Bz) { + PCAInfoHelper out; + + const float pt = std::sqrt(px*px + py*py); + if (pt == 0.f) return out; + if (chargeSign == 0) chargeSign = 1; + if (Bz == 0.f) return out; + + // Radius in mm: + // GenfitTrack::PCAInfo uses R = pt/(0.3*|q|*Bz)*100 [cm] + // -> multiply by 10 to get mm: *1000 + const float R = pt / (0.3f * std::abs(chargeSign) * Bz) * 1000.f; + + const float tx = px / pt; + const float ty = py / pt; + + const float nx = float(chargeSign) * (ty); + const float ny = float(chargeSign) * (-tx); + + const float xc = x + R * nx; + const float yc = y + R * ny; + + const float vx = refX - xc; + const float vy = refY - yc; + const float vxy = std::sqrt(vx*vx + vy*vy); + if (vxy == 0.f) return out; + + const float ux = vx / vxy; + const float uy = vy / vxy; + + const float pcaX = xc + R * ux; + const float pcaY = yc + R * uy; + + // tangent direction at PCA (same as GenfitTrack::PCAInfo) + const float rx = pcaX - xc; + const float ry = pcaY - yc; + + const int sign = (chargeSign > 0) ? 1 : -1; + float tanX = -sign * ry; + float tanY = sign * rx; + + const float tnorm = std::sqrt(tanX*tanX + tanY*tanY); + if (tnorm == 0.f) return out; + + tanX /= tnorm; + tanY /= tnorm; + + const float phi0 = std::atan2(tanY, tanX); + + // ZPCA approximation from GenfitTrack::PCAInfo (ported) + // Uses a straight-line minimization in (R,z) with pR=pt, pZ=pz. + const float pR = pt; + const float pZ = pz; + const float R0 = std::sqrt(x*x + y*y); + const float Z0 = z; + + const float denom = (pR*pR + pZ*pZ); + if (denom == 0.f) return out; + + const float tPCA = -(R0*pR + Z0*pZ) / denom; + const float ZPCA = Z0 + pZ * tPCA; + + out.pcaX = pcaX; + out.pcaY = pcaY; + out.pcaZ = ZPCA; + out.phi0 = phi0; + out.ok = true; + return out; +} + +// Build MC truth helix parameters using the fitter convention +static HelixParams truthFromMC_GenfitConvention(const edm4hep::MCParticle& mc, + float Bz, + float refX, float refY, float refZ) { + HelixParams hp; + + const auto& mom = mc.getMomentum(); + const float px = float(mom.x); + const float py = float(mom.y); + const float pz = float(mom.z); + + const float pT = std::sqrt(px*px + py*py); + const float p = std::sqrt(px*px + py*py + pz*pz); + + hp.pT = pT; + hp.p = p; + + // Charge sign consistent with fitter usage (sign matters for omega) + int qSign = 1; + if (mc.getCharge() < 0.f) qSign = -1; + + const auto& v = mc.getVertex(); + const float x = float(v.x); // mm + const float y = float(v.y); // mm + const float z = float(v.z); // mm + + const auto info = PCAInfo_mm(x, y, z, px, py, pz, qSign, refX, refY, Bz); + if (!info.ok) { + const float NaN = std::numeric_limits::quiet_NaN(); + hp.D0 = NaN; + hp.Z0 = NaN; + hp.phi = NaN; + hp.omega = NaN; + hp.tanLambda = NaN; + return hp; + } + + // D0/Z0 in the same convention as the fitter (mm) + hp.D0 = ( (-(refX - info.pcaX)) * std::sin(info.phi0) + (refY - info.pcaY) * std::cos(info.phi0) ); // mm + hp.Z0 = (info.pcaZ - refZ); // mm + + // phi in fitter is taken from momentum.Phi() at the evaluated state. + // For MC we use the momentum direction. + hp.phi = safeAtan2(py, px); + + hp.tanLambda = (pT > 0.f) ? (pz / pT) : 0.f; + + // omega convention matches fitter: omega = +/- |a * Bz / pT| + hp.omega = (pT > 0.f) ? (std::abs(a_genfit * Bz / pT) * float(qSign)) : 0.f; + + return hp; +} + +static bool getAtIPState(const edm4hep::Track& trk, edm4hep::TrackState& out) { + for (const auto& st : trk.getTrackStates()) { + if (st.location == edm4hep::TrackState::AtIP) { + out = st; + return true; + } + } + return false; +} + + + + +static float ptFromState(const edm4hep::TrackState& st, float Bz) { + const float omega = std::abs(float(st.omega)); + if (omega == 0.f) return 0.f; + return a_genfit * std::abs(Bz) / omega; +} + +static float momentumFromState(const edm4hep::TrackState& st, float Bz) { + const float pT = ptFromState(st, Bz); + const float tl = float(st.tanLambda); + return pT * std::sqrt(1.f + tl * tl); +} +// Helper functions for plotting + +static std::vector makeLogBins(double min, double max, double step) { + std::vector bins; + for (double x = std::log10(min); x <= std::log10(max); x += step) { + bins.push_back(std::pow(10., x)); + } + if (bins.empty() || bins.back() < max) bins.push_back(max); + return bins; +} + +static TF1* fitGaussianCore(TH1F* h, const std::string& name) { + if (!h || h->GetEntries() < 10) return nullptr; + + const double mean = h->GetMean(); + const double rms = h->GetRMS(); + if (rms <= 0.) return nullptr; + + TF1* g1 = new TF1((name + "_g1").c_str(), "gaus", mean - 2.0 * rms, mean + 2.0 * rms); + h->Fit(g1, "RQ0"); + + double m1 = g1->GetParameter(1); + double s1 = std::abs(g1->GetParameter(2)); + if (s1 <= 0.) s1 = rms; + + TF1* g2 = new TF1(name.c_str(), "gaus", m1 - 1.5 * s1, m1 + 1.5 * s1); + h->Fit(g2, "RQ0"); + + return g2; +} + +static TGraphErrors* makeD0ResolutionVsMomentum(TTree* tree, + const char* graphName = "g_d0_resolution_vs_p", + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15) { + if (!tree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector> hists; + hists.reserve(nBins); + + for (int i = 0; i < nBins; ++i) { + hists.emplace_back(std::make_unique( + Form("h_d0_bin_%d", i), + Form("d0 residual bin %d;resD0 [#mum];Entries", i), + 120, -20.0, 20.0)); + } + + + std::vector* resD0 = nullptr; + std::vector* p_ref_vec = nullptr; + + // Tree stores vectors per event + tree->SetBranchAddress("p_ref", &p_ref_vec); + tree->SetBranchAddress("resD0", &resD0); + + const Long64_t nEntries = tree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + tree->GetEntry(ievt); + + if (!p_ref_vec || !resD0) continue; + if (p_ref_vec->size() != resD0->size()) continue; + + for (size_t i = 0; i < p_ref_vec->size(); ++i) { + const double p = (*p_ref_vec)[i]; + const double d0_um = (*resD0)[i] * 1000.0; // mm -> um + + if (!std::isfinite(p) || !std::isfinite(d0_um)) continue; + if (p < pMin || p >= pMax) continue; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (p >= bins[b] && p < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + hists[bin]->Fill(d0_um); + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p_{ref} [GeV];#sigma(d_{0}) [#mum]"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (hists[b]->GetEntries() < 20) continue; + + TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_d0_bin_%d", b)); + if (!fit) continue; + + const double sigma = std::abs(fit->GetParameter(2)); + const double sigmaErr = fit->GetParError(2); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, sigma); + g->SetPointError(ip, 0.0, sigmaErr); + ++ip; + } + + return g; +} + +static TCanvas* drawD0ResolutionCanvas(TGraphErrors* g, + const char* canvasName = "c_d0_resolution_vs_p", + double xMin = 0.1, + double xMax = 100.0) { + if (!g) return nullptr; + + gStyle->SetOptStat(0); + + TCanvas* c = new TCanvas(canvasName, "d0 resolution vs momentum", 800, 600); + c->SetLogx(); + + g->SetMarkerStyle(20); + g->SetLineWidth(2); + g->GetXaxis()->SetLimits(xMin, xMax); + g->Draw("AP"); + + return c; +} + +static TGraphErrors* makeMomentumResolutionVsMomentum(TTree* tree, + const char* graphName = "g_p_resolution_vs_p", + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15) { + if (!tree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector> hists; + hists.reserve(nBins); + + for (int i = 0; i < nBins; ++i) { + hists.emplace_back(std::make_unique( + Form("h_pres_bin_%d", i), + Form("p resolution bin %d;(p_{reco}-p_{ref})/p_{ref};Entries", i), + 120, -0.2, 0.2)); + } + + std::vector* p_ref_vec = nullptr; + std::vector* p_reco_vec = nullptr; + + tree->SetBranchAddress("p_ref", &p_ref_vec); + tree->SetBranchAddress("p_reco", &p_reco_vec); + + const Long64_t nEntries = tree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + tree->GetEntry(ievt); + + if (!p_ref_vec || !p_reco_vec) continue; + if (p_ref_vec->size() != p_reco_vec->size()) continue; + + for (size_t i = 0; i < p_ref_vec->size(); ++i) { + const double pRef = (*p_ref_vec)[i]; + const double pReco = (*p_reco_vec)[i]; + + if (!std::isfinite(pRef) || !std::isfinite(pReco)) continue; + if (pRef <= 0.) continue; + if (pRef < pMin || pRef >= pMax) continue; + + const double res = (pReco - pRef) / pRef; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (pRef >= bins[b] && pRef < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + hists[bin]->Fill(res); + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p_{ref} [GeV];#sigma((p_{reco}-p_{ref})/p_{ref})"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (hists[b]->GetEntries() < 20) continue; + + TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_pres_bin_%d", b)); + if (!fit) continue; + + const double sigma = std::abs(fit->GetParameter(2)); + const double sigmaErr = fit->GetParError(2); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, sigma); + g->SetPointError(ip, 0.0, sigmaErr); + ++ip; + } + + return g; +} + +static TGraphErrors* makePtResolutionVsMomentum(TTree* tree, + const char* graphName = "g_pt_resolution_vs_p", + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15) { + if (!tree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector> hists; + hists.reserve(nBins); + + for (int i = 0; i < nBins; ++i) { + hists.emplace_back(std::make_unique( + Form("h_ptres_bin_%d", i), + Form("pT resolution bin %d;(pT_{reco}-pT_{ref})/pT_{ref};Entries", i), + 120, -0.2, 0.2)); + } + + std::vector* p_ref_vec = nullptr; + std::vector* pt_ref_vec = nullptr; + std::vector* pt_reco_vec = nullptr; + + tree->SetBranchAddress("p_ref", &p_ref_vec); + tree->SetBranchAddress("pT_ref", &pt_ref_vec); + tree->SetBranchAddress("pT_reco", &pt_reco_vec); + + const Long64_t nEntries = tree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + tree->GetEntry(ievt); + + if (!p_ref_vec || !pt_ref_vec || !pt_reco_vec) continue; + if (p_ref_vec->size() != pt_ref_vec->size()) continue; + if (pt_ref_vec->size() != pt_reco_vec->size()) continue; + + for (size_t i = 0; i < p_ref_vec->size(); ++i) { + const double pRef = (*p_ref_vec)[i]; + const double ptRef = (*pt_ref_vec)[i]; + const double ptReco = (*pt_reco_vec)[i]; + + if (!std::isfinite(pRef) || !std::isfinite(ptRef) || !std::isfinite(ptReco)) continue; + if (ptRef <= 0.) continue; + if (pRef < pMin || pRef >= pMax) continue; + + const double res = (ptReco - ptRef) / ptRef; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (pRef >= bins[b] && pRef < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + hists[bin]->Fill(res); + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p_{ref} [GeV];#sigma((pT_{reco}-pT_{ref})/pT_{ref})"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (hists[b]->GetEntries() < 20) continue; + + TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_ptres_bin_%d", b)); + if (!fit) continue; + + const double sigma = std::abs(fit->GetParameter(2)); + const double sigmaErr = fit->GetParError(2); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, sigma); + g->SetPointError(ip, 0.0, sigmaErr); + ++ip; + } + + return g; +} + +static TCanvas* drawResolutionCanvas(TGraphErrors* g, + const char* canvasName, + const char* title, + double xMin = 0.1, + double xMax = 100.0) { + if (!g) return nullptr; + + gStyle->SetOptStat(0); + + TCanvas* c = new TCanvas(canvasName, title, 800, 600); + c->SetLogx(); + + g->SetMarkerStyle(20); + g->SetLineWidth(2); + g->SetTitle(title); + g->GetXaxis()->SetLimits(xMin, xMax); + g->Draw("AP"); + + return c; +} +// A truth particle is counted as reconstructed if at least one associated finder track +// has purity above the configured threshold +static TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, + const char* graphName, + double purityThreshold, + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15) { + if (!finderTree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector nDen(nBins, 0); + std::vector nNum(nBins, 0); + + std::vector* pVec = nullptr; + std::vector>* purVec = nullptr; + + finderTree->SetBranchAddress("p", &pVec); + finderTree->SetBranchAddress("matchPurity", &purVec); + + const Long64_t nEntries = finderTree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + finderTree->GetEntry(ievt); + + if (!pVec || !purVec) continue; + if (pVec->size() != purVec->size()) continue; + + for (size_t i = 0; i < pVec->size(); ++i) { + const double p = (*pVec)[i]; + if (!std::isfinite(p) || p < pMin || p >= pMax) continue; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (p >= bins[b] && p < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + // denominator: all truth particles present in finder_particle_to_tracks tree + // (genStatus1 and with at least one true hit, as enforced in fillFinderAssoc) + nDen[bin]++; + + bool isMatched = false; + for (size_t j = 0; j < (*purVec)[i].size(); ++j) { + if ((*purVec)[i][j] >= purityThreshold) { + isMatched = true; + break; + } + } + if (isMatched) nNum[bin]++; + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p [GeV];Tracking efficiency"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (nDen[b] == 0) continue; + + const double eff = double(nNum[b]) / double(nDen[b]); + const double err = std::sqrt(eff * (1.0 - eff) / double(nDen[b])); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, eff); + g->SetPointError(ip, 0.0, err); + ++ip; + } + + return g; +} + +static TCanvas* drawEfficiencyCanvas(TGraphErrors* g, + const char* canvasName, + const char* title, + double xMin = 0.1, + double xMax = 100.0) { + if (!g) return nullptr; + + gStyle->SetOptStat(0); + + TCanvas* c = new TCanvas(canvasName, title, 800, 600); + c->SetLogx(); + + g->SetMarkerStyle(20); + g->SetLineWidth(2); + g->SetTitle(title); + g->GetYaxis()->SetRangeUser(0.0, 1.05); + g->GetXaxis()->SetLimits(xMin, xMax); + g->Draw("AP"); + + return c; +} + +// ---------- CONSUMER ---------- +struct TrackingValidationConsumer final + : k4FWCore::Consumer&, // planar links + const std::vector&, // optional DCH links + const edm4hep::TrackCollection&, // finder tracks + const edm4hep::TrackCollection&, // fitted tracks (reco) + const std::vector& // optional perfect fitted tracks + )> { + + TrackingValidationConsumer(const std::string& name, ISvcLocator* svcLoc) + : Consumer( + name, svcLoc, + { + KeyValues("MCParticles", {"MCParticles"}), + + KeyValues("PlanarLinks", + {"SiWrBSimDigiLinks", "SiWrDSimDigiLinks", "VTXBSimDigiLinks", "VTXDSimDigiLinks"}), + + + KeyValues("DCHLinks", {"DCH_DigiSimAssociationCollection"}), + + KeyValues("FinderTracks", {"GGTFTracks"}), + KeyValues("FittedTracks", {"FittedTracks"}), + + + KeyValues("PerfectFittedTracks", {"PerfectFitted_tracks"}), + }) {} + + StatusCode initialize() override { + info() << "Initializing TrackingValidationConsumer" << endmsg; + + m_outFile = std::make_unique(m_outputFile.value().c_str(), "RECREATE"); + if (!m_outFile || m_outFile->IsZombie()) { + error() << "Cannot open output file: " << m_outputFile.value() << endmsg; + return StatusCode::FAILURE; + } + + bookAssocTree(m_finder_p2t, "finder_particle_to_tracks"); + bookAssocTree(m_finder_t2p, "finder_track_to_particles"); + bookAssocTree(m_perf_p2t, "perfect_particle_to_tracks"); + bookAssocTree(m_perf_t2p, "perfect_track_to_particles"); + + bookFitterTree(m_fit_vs_mc, "fitter_vs_mc"); + bookFitterTree(m_fit_vs_perfect, "fitter_vs_perfect"); + + return StatusCode::SUCCESS; + } + + void operator()(const edm4hep::MCParticleCollection& mcParts, + const std::vector& planarLinksVec, + const std::vector& dchLinksVec, + const edm4hep::TrackCollection& finderTracks, + const edm4hep::TrackCollection& fittedTracks, + const std::vector& perfectFittedTracksVec) const override { + + const int event = m_evt++; + const int mode = m_mode.value(); // 0 full, 1 finder-only, 2 fitter-only + + // ---------- Build truth maps: hit -> particle, particle -> hits ---------- + std::unordered_map> hitsPerParticle; + hitsPerParticle.reserve(mcParts.size()); + + std::unordered_map hitToParticle; + hitToParticle.reserve(200000); + + // planar links + for (const auto* links : planarLinksVec) { + if (!links) continue; + for (const auto& link : *links) { + const auto digi = link.getFrom(); + const auto sim = link.getTo(); + const auto mc = sim.getParticle(); + if (!digi.isAvailable() || !mc.isAvailable()) continue; + + const int pid = mc.getObjectID().index; + const uint64_t key = oidKey(digi.getObjectID()); + hitsPerParticle[pid].push_back(key); + hitToParticle[key] = pid; + } + } + + // DCH links + for (const auto* links : dchLinksVec) { + if (!links) continue; + for (const auto& link : *links) { + const auto digi = link.getFrom(); + const auto sim = link.getTo(); + const auto mc = sim.getParticle(); + if (!digi.isAvailable() || !mc.isAvailable()) continue; + + const int pid = mc.getObjectID().index; + const uint64_t key = oidKey(digi.getObjectID()); + hitsPerParticle[pid].push_back(key); + hitToParticle[key] = pid; + } + } + + // ---------- Finder & Perfect association trees ---------- + if (mode == 0 || mode == 1) { + fillPerfectAssoc(event, mcParts, hitsPerParticle); + fillFinderAssoc(event, mcParts, finderTracks, hitToParticle, hitsPerParticle); + } + + // ---------- Build pid -> best perfect-fitted AtIP state ---------- + struct StateWithNHits { + edm4hep::TrackState st; + int nHits = 0; + }; + + std::unordered_map perfectAtIPByPid; + + const bool wantPerfect = m_doPerfectFit.value(); + const bool havePerfectCollections = !perfectFittedTracksVec.empty(); + const bool doPerfect = wantPerfect && havePerfectCollections; + + if (wantPerfect && !havePerfectCollections && !m_warnedMissingPerfectInput) { + warning() << "DoPerfectFit=true but no PerfectFittedTracks collection was provided. " + << "fitter_vs_perfect will be filled with NaNs/empty content." << endmsg; + m_warnedMissingPerfectInput = true; + } + + if (doPerfect) { + size_t nPerfectTracks = 0; + for (const auto* coll : perfectFittedTracksVec) { + if (!coll) continue; + nPerfectTracks += coll->size(); + } + perfectAtIPByPid.reserve(nPerfectTracks); + + for (const auto* coll : perfectFittedTracksVec) { + if (!coll) continue; + + for (const auto& trk : *coll) { + edm4hep::TrackState st; + if (!getAtIPState(trk, st)) continue; + + const int pid = majorityParticleForTrack(trk, hitToParticle); + if (pid < 0 || pid >= (int)mcParts.size()) continue; + + const int nHits = (int)trk.getTrackerHits().size(); + auto it = perfectAtIPByPid.find(pid); + if (it == perfectAtIPByPid.end() || nHits > it->second.nHits) { + perfectAtIPByPid[pid] = StateWithNHits{st, nHits}; + } + } + } + } + + // ---------- Fitter trees ---------- + if (mode == 0 || mode == 2) { + fillFitterTrees(event, mcParts, fittedTracks, hitToParticle, perfectAtIPByPid, doPerfect); + } + } + + StatusCode finalize() override { + info() << "Finalizing TrackingValidationConsumer, wrote " << m_evt << " events" << endmsg; + + if (m_outFile) { + m_outFile->cd(); + + //write trees + if (m_finder_p2t.tree) m_finder_p2t.tree->Write(); + if (m_finder_t2p.tree) m_finder_t2p.tree->Write(); + if (m_perf_p2t.tree) m_perf_p2t.tree->Write(); + if (m_perf_t2p.tree) m_perf_t2p.tree->Write(); + + if (m_fit_vs_mc.tree) m_fit_vs_mc.tree->Write(); + if (m_fit_vs_perfect.tree) m_fit_vs_perfect.tree->Write(); + + //fitter summary plots + // d0 resolution vs momentum from fitter_vs_mc + TGraphErrors* g_d0_vs_p = makeD0ResolutionVsMomentum(m_fit_vs_mc.tree, + "g_d0_resolution_vs_p", + 0.1, 100.0, 0.15); + if (g_d0_vs_p) { + TCanvas* c_d0_vs_p = drawD0ResolutionCanvas(g_d0_vs_p, + "c_d0_resolution_vs_p", + 0.1, 100.0); + g_d0_vs_p->Write(); + if (c_d0_vs_p) c_d0_vs_p->Write(); + } + // p resolution vs momentum + TGraphErrors* g_p_vs_p = makeMomentumResolutionVsMomentum(m_fit_vs_mc.tree, + "g_p_resolution_vs_p", + 0.1, 100.0, 0.15); + if (g_p_vs_p) { + TCanvas* c_p_vs_p = drawResolutionCanvas(g_p_vs_p, + "c_p_resolution_vs_p", + "momentum resolution vs momentum;p_{ref} [GeV];#sigma((p_{reco}-p_{ref})/p_{ref})", + 0.1, 100.0); + g_p_vs_p->Write(); + if (c_p_vs_p) c_p_vs_p->Write(); + } + + // pT resolution vs momentum + TGraphErrors* g_pt_vs_p = makePtResolutionVsMomentum(m_fit_vs_mc.tree, + "g_pt_resolution_vs_p", + 0.1, 100.0, 0.15); + if (g_pt_vs_p) { + TCanvas* c_pt_vs_p = drawResolutionCanvas(g_pt_vs_p, + "c_pt_resolution_vs_p", + "pT resolution vs momentum;p_{ref} [GeV];#sigma((pT_{reco}-pT_{ref})/pT_{ref})", + 0.1, 100.0); + g_pt_vs_p->Write(); + if (c_pt_vs_p) c_pt_vs_p->Write(); + } + + // finder summary plot + TGraphErrors* g_eff_vs_p = makeEfficiencyVsMomentum( + m_finder_p2t.tree, + "g_efficiency_vs_p", + m_finderPurityThreshold.value(), + 0.1, 100.0, 0.15); + if (g_eff_vs_p) { + TCanvas* c_eff_vs_p = drawEfficiencyCanvas( + g_eff_vs_p, + "c_efficiency_vs_p", + "tracking efficiency vs momentum;p [GeV];Efficiency", + 0.1, 100.0); + g_eff_vs_p->Write(); + if (c_eff_vs_p) c_eff_vs_p->Write(); + + } + + m_outFile->Close(); + } + return StatusCode::SUCCESS; + } + +private: + // ---------- properties ---------- + Gaudi::Property m_outputFile{this, "OutputFile", "validation.root", "Output ROOT file (TTrees)"}; + + // 0 full, 1 finder-only, 2 fitter-only + Gaudi::Property m_mode{this, "Mode", 0, "Validation mode: 0=Full, 1=FinderOnly, 2=FitterOnly"}; + + Gaudi::Property m_Bz{this, "Bz", 2.f, "Magnetic field Bz [T] used in omega convention (GenFit-style)"}; + + Gaudi::Property m_refX{this, "RefPointX", 0.f, "Reference point X [mm] (must match fitter m_VP_referencePoint)"}; + Gaudi::Property m_refY{this, "RefPointY", 0.f, "Reference point Y [mm] (must match fitter m_VP_referencePoint)"}; + Gaudi::Property m_refZ{this, "RefPointZ", 0.f, "Reference point Z [mm] (must match fitter m_VP_referencePoint)"}; + + Gaudi::Property m_finderPurityThreshold{ + this, "FinderPurityThreshold", 0.75f, + "Minimum purity for a particle-track match to count in tracking efficiency"}; + + Gaudi::Property m_doPerfectFit{ + this, "DoPerfectFit", false, + "If true: fill fitter_vs_perfect using PerfectFitted_tracks if available. " + "If false: tree exists but is empty per event."}; + + // ---------- output structs ---------- + struct AssocTree { + TTree* tree = nullptr; + int event = 0; + std::vector index; + std::vector p; + std::vector pT; + std::vector nTrueHits; + std::vector> assoc; + + // only really used for finder_particle_to_tracks + std::vector> sharedHits; + std::vector> matchEfficiency; + std::vector> matchPurity; + + void clear() { + index.clear(); + p.clear(); + pT.clear(); + nTrueHits.clear(); + assoc.clear(); + sharedHits.clear(); + matchEfficiency.clear(); + matchPurity.clear(); + } + }; + + struct FitterTree { + TTree* tree = nullptr; + int event = 0; + + std::vector track_index; + std::vector track_location; + + std::vector resD0, resZ0, resPhi, resOmega, resTanL; + std::vector p_reco, p_ref; + std::vector pT_reco, pT_ref; + + void clear() { + track_index.clear(); + track_location.clear(); + resD0.clear(); + resZ0.clear(); + resPhi.clear(); + resOmega.clear(); + resTanL.clear(); + p_reco.clear(); + p_ref.clear(); + pT_reco.clear(); + pT_ref.clear(); + } + }; + + static void bookAssocTree(AssocTree& t, const char* name) { + t.tree = new TTree(name, name); + t.tree->Branch("event", &t.event); + t.tree->Branch("index", &t.index); + t.tree->Branch("p", &t.p); + t.tree->Branch("pT", &t.pT); + t.tree->Branch("nTrueHits", &t.nTrueHits); + t.tree->Branch("assoc", &t.assoc); + t.tree->Branch("sharedHits", &t.sharedHits); + t.tree->Branch("matchEfficiency", &t.matchEfficiency); + t.tree->Branch("matchPurity", &t.matchPurity); + } + + static void bookFitterTree(FitterTree& t, const char* name) { + t.tree = new TTree(name, name); + t.tree->Branch("event", &t.event); + t.tree->Branch("track_index", &t.track_index); + t.tree->Branch("track_location", &t.track_location); + t.tree->Branch("resD0", &t.resD0); + t.tree->Branch("resZ0", &t.resZ0); + t.tree->Branch("resPhi", &t.resPhi); + t.tree->Branch("resOmega", &t.resOmega); + t.tree->Branch("resTanLambda", &t.resTanL); + t.tree->Branch("p_reco", &t.p_reco); + t.tree->Branch("p_ref", &t.p_ref); + t.tree->Branch("pT_reco", &t.pT_reco); + t.tree->Branch("pT_ref", &t.pT_ref); + } + + // ---------- association trees ---------- + void fillPerfectAssoc(int event, const edm4hep::MCParticleCollection& mcParts, + const std::unordered_map>& hitsPerParticle) const { + + m_perf_p2t.clear(); + m_perf_t2p.clear(); + m_perf_p2t.event = event; + m_perf_t2p.event = event; + + for (int i = 0; i < (int)mcParts.size(); ++i) { + const auto& mc = mcParts[i]; + if (mc.getGeneratorStatus() != 1) continue; + + auto it = hitsPerParticle.find(i); + if (it == hitsPerParticle.end() || it->second.empty()) continue; + const auto& mom = mc.getMomentum(); + const float px = float(mom.x); + const float py = float(mom.y); + const float pz = float(mom.z); + const float p = std::sqrt(px*px + py*py + pz*pz); + const float pT = std::sqrt(px*px + py*py); + const int nHits = (int)it->second.size(); + + m_perf_p2t.index.push_back(i); + m_perf_p2t.p.push_back(p); + m_perf_p2t.pT.push_back(pT); + m_perf_p2t.nTrueHits.push_back(nHits); + m_perf_p2t.assoc.push_back({i}); + m_perf_p2t.sharedHits.push_back({}); + m_perf_p2t.matchEfficiency.push_back({}); + m_perf_p2t.matchPurity.push_back({}); + + m_perf_t2p.index.push_back(i); + m_perf_t2p.p.push_back(p); + m_perf_t2p.pT.push_back(pT); + m_perf_t2p.nTrueHits.push_back(nHits); + m_perf_t2p.assoc.push_back({i}); + m_perf_t2p.sharedHits.push_back({}); + m_perf_t2p.matchEfficiency.push_back({}); + m_perf_t2p.matchPurity.push_back({}); + } + + if (m_perf_p2t.tree) m_perf_p2t.tree->Fill(); + if (m_perf_t2p.tree) m_perf_t2p.tree->Fill(); + } + + void fillFinderAssoc(int event, const edm4hep::MCParticleCollection& mcParts, + const edm4hep::TrackCollection& finderTracks, + const std::unordered_map& hitToParticle, + const std::unordered_map>& hitsPerParticle) const { + + m_finder_p2t.clear(); + m_finder_t2p.clear(); + m_finder_p2t.event = event; + m_finder_t2p.event = event; + + std::vector> trackParticleCounts; + std::vector trackNHits; + trackParticleCounts.resize(finderTracks.size()); + trackNHits.resize(finderTracks.size(), 0); + + int tIdx = 0; + for (const auto& trk : finderTracks) { + trackNHits[tIdx] = (int)trk.getTrackerHits().size(); + for (const auto& h : trk.getTrackerHits()) { + const uint64_t hk = oidKey(h.getObjectID()); + auto it = hitToParticle.find(hk); + if (it == hitToParticle.end()) continue; + trackParticleCounts[tIdx][it->second] += 1; + } + ++tIdx; + } + + // track -> particles + for (int t = 0; t < (int)finderTracks.size(); ++t) { + m_finder_t2p.index.push_back(t); + m_finder_t2p.p.push_back(-1.f); + m_finder_t2p.pT.push_back(-1.f); + m_finder_t2p.nTrueHits.push_back(trackNHits[t]); + + std::vector parts; + std::vector sh; + std::vector effs; + std::vector purs; + + for (const auto& kv : trackParticleCounts[t]) { + const int pid = kv.first; + if (pid < 0) continue; + + const int shared = kv.second; + const int nTrackHits = trackNHits[t]; + const int nParticleHits = + hitsPerParticle.count(pid) ? (int)hitsPerParticle.at(pid).size() : 0; + + const float eff = (nParticleHits > 0) ? float(shared) / float(nParticleHits) : 0.f; + const float pur = (nTrackHits > 0) ? float(shared) / float(nTrackHits) : 0.f; + + parts.push_back(pid); + sh.push_back(shared); + effs.push_back(eff); + purs.push_back(pur); + } + + m_finder_t2p.assoc.push_back(parts); + m_finder_t2p.sharedHits.push_back(sh); + m_finder_t2p.matchEfficiency.push_back(effs); + m_finder_t2p.matchPurity.push_back(purs); + } + + // particle -> tracks + for (int p = 0; p < (int)mcParts.size(); ++p) { + const auto& mc = mcParts[p]; + if (mc.getGeneratorStatus() != 1) continue; + + auto itHits = hitsPerParticle.find(p); + if (itHits == hitsPerParticle.end() || itHits->second.empty()) continue; + + const auto& mom = mc.getMomentum(); + const float px = float(mom.x); + const float py = float(mom.y); + const float pz = float(mom.z); + const float pAbs = std::sqrt(px*px + py*py + pz*pz); + const float pT = std::sqrt(px*px + py*py); + const int nParticleHits = (int)itHits->second.size(); + + std::vector tracks; + std::vector sh; + std::vector effs; + std::vector purs; + + for (int t = 0; t < (int)finderTracks.size(); ++t) { + auto it = trackParticleCounts[t].find(p); + if (it == trackParticleCounts[t].end()) continue; + + const int shared = it->second; + const int nTrackHits = trackNHits[t]; + + const float eff = (nParticleHits > 0) ? float(shared) / float(nParticleHits) : 0.f; + const float pur = (nTrackHits > 0) ? float(shared) / float(nTrackHits) : 0.f; + + tracks.push_back(t); + sh.push_back(shared); + effs.push_back(eff); + purs.push_back(pur); + } + + m_finder_p2t.index.push_back(p); + m_finder_p2t.p.push_back(pAbs); + m_finder_p2t.pT.push_back(pT); + m_finder_p2t.nTrueHits.push_back(nParticleHits); + m_finder_p2t.assoc.push_back(tracks); + m_finder_p2t.sharedHits.push_back(sh); + m_finder_p2t.matchEfficiency.push_back(effs); + m_finder_p2t.matchPurity.push_back(purs); + } + + if (m_finder_p2t.tree) m_finder_p2t.tree->Fill(); + if (m_finder_t2p.tree) m_finder_t2p.tree->Fill(); +} + + // ---------- matching helper ---------- + int majorityParticleForTrack(const edm4hep::Track& trk, + const std::unordered_map& hitToParticle) const { + std::unordered_map counts; + for (const auto& h : trk.getTrackerHits()) { + const uint64_t hk = oidKey(h.getObjectID()); + auto it = hitToParticle.find(hk); + if (it == hitToParticle.end()) continue; + counts[it->second] += 1; + } + if (counts.empty()) return -1; + + int bestP = -1; + int bestN = -1; + for (const auto& kv : counts) { + if (kv.second > bestN) { + bestN = kv.second; + bestP = kv.first; + } + } + return bestP; + } + + // ---------- fitter trees ---------- + template + void fillFitterTrees(int event, + const edm4hep::MCParticleCollection& mcParts, + const edm4hep::TrackCollection& fittedTracks, + const std::unordered_map& hitToParticle, + const PerfectMapT& perfectAtIPByPid, + bool doPerfect) const { + + m_fit_vs_mc.clear(); + m_fit_vs_perfect.clear(); + m_fit_vs_mc.event = event; + m_fit_vs_perfect.event = event; + + const float NaN = std::numeric_limits::quiet_NaN(); + + int tIdx = 0; + for (const auto& trk : fittedTracks) { + edm4hep::TrackState stReco; + if (!getAtIPState(trk, stReco)) { + ++tIdx; + continue; + } + + const int pid = majorityParticleForTrack(trk, hitToParticle); + if (pid < 0 || pid >= (int)mcParts.size()) { + ++tIdx; + continue; + } + + const auto& mc = mcParts[pid]; + + // reco params (already in fitter convention) + HelixParams reco; + reco.D0 = float(stReco.D0); + reco.Z0 = float(stReco.Z0); + reco.phi = float(stReco.phi); + reco.omega = float(stReco.omega); + reco.tanLambda = float(stReco.tanLambda); + reco.pT = ptFromState(stReco, m_Bz.value()); + reco.p = momentumFromState(stReco, m_Bz.value()); + + // ref from MC using the SAME convention as fitter (PCA + phi0 + ZPCA + omega=a*B/pT) + const HelixParams refMC = truthFromMC_GenfitConvention(mc, m_Bz.value(), m_refX.value(), m_refY.value(), m_refZ.value()); + + + // --- vs MC --- + m_fit_vs_mc.track_index.push_back(tIdx); + m_fit_vs_mc.track_location.push_back(int(stReco.location)); + m_fit_vs_mc.resD0.push_back(reco.D0 - refMC.D0); + m_fit_vs_mc.resZ0.push_back(reco.Z0 - refMC.Z0); + m_fit_vs_mc.resPhi.push_back(wrapDeltaPhi(reco.phi, refMC.phi)); + m_fit_vs_mc.resOmega.push_back(reco.omega - refMC.omega); + m_fit_vs_mc.resTanL.push_back(reco.tanLambda - refMC.tanLambda); + m_fit_vs_mc.p_reco.push_back(reco.p); + m_fit_vs_mc.p_ref.push_back(refMC.p); + m_fit_vs_mc.pT_reco.push_back(reco.pT); + m_fit_vs_mc.pT_ref.push_back(refMC.pT); + + // --- vs perfect-fitted --- + if (doPerfect) { + auto it = perfectAtIPByPid.find(pid); + if (it != perfectAtIPByPid.end()) { + const auto& stPerf = it->second.st; + + HelixParams refP; + refP.D0 = float(stPerf.D0); + refP.Z0 = float(stPerf.Z0); + refP.phi = float(stPerf.phi); + refP.omega = float(stPerf.omega); + refP.tanLambda = float(stPerf.tanLambda); + refP.pT = ptFromState(stPerf, m_Bz.value()); + refP.p = momentumFromState(stPerf, m_Bz.value()); + + m_fit_vs_perfect.track_index.push_back(tIdx); + m_fit_vs_perfect.track_location.push_back(int(stReco.location)); + m_fit_vs_perfect.resD0.push_back(reco.D0 - refP.D0); + m_fit_vs_perfect.resZ0.push_back(reco.Z0 - refP.Z0); + m_fit_vs_perfect.resPhi.push_back(wrapDeltaPhi(reco.phi, refP.phi)); + m_fit_vs_perfect.resOmega.push_back(reco.omega - refP.omega); + m_fit_vs_perfect.resTanL.push_back(reco.tanLambda - refP.tanLambda); + m_fit_vs_perfect.p_reco.push_back(reco.p); + m_fit_vs_perfect.p_ref.push_back(refP.p); + m_fit_vs_perfect.pT_reco.push_back(reco.pT); + m_fit_vs_perfect.pT_ref.push_back(refP.pT); + } else { + m_fit_vs_perfect.track_index.push_back(tIdx); + m_fit_vs_perfect.track_location.push_back(int(stReco.location)); + m_fit_vs_perfect.resD0.push_back(NaN); + m_fit_vs_perfect.resZ0.push_back(NaN); + m_fit_vs_perfect.resPhi.push_back(NaN); + m_fit_vs_perfect.resOmega.push_back(NaN); + m_fit_vs_perfect.resTanL.push_back(NaN); + m_fit_vs_perfect.p_reco.push_back(reco.p); + m_fit_vs_perfect.p_ref.push_back(NaN); + m_fit_vs_perfect.pT_reco.push_back(reco.pT); + m_fit_vs_perfect.pT_ref.push_back(NaN); + } + } + + ++tIdx; + } + + if (m_fit_vs_mc.tree) m_fit_vs_mc.tree->Fill(); + if (m_fit_vs_perfect.tree) m_fit_vs_perfect.tree->Fill(); + } + +private: + mutable int m_evt = 0; + mutable bool m_warnedMissingPerfectInput = false; + + std::unique_ptr m_outFile; + + mutable AssocTree m_finder_p2t; + mutable AssocTree m_finder_t2p; + mutable AssocTree m_perf_p2t; + mutable AssocTree m_perf_t2p; + + mutable FitterTree m_fit_vs_mc; + mutable FitterTree m_fit_vs_perfect; +}; + +DECLARE_COMPONENT(TrackingValidationConsumer) \ No newline at end of file From 1aed2bd2ffccb467ecb1c1f4b9683df69a2fe623 Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Mon, 16 Mar 2026 09:51:52 +0100 Subject: [PATCH 02/15] Rename TrackingValidation consumer source file --- .../{TrackingValidationConsumer.cpp => TrackingValidation.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Tracking/components/{TrackingValidationConsumer.cpp => TrackingValidation.cpp} (100%) diff --git a/Tracking/components/TrackingValidationConsumer.cpp b/Tracking/components/TrackingValidation.cpp similarity index 100% rename from Tracking/components/TrackingValidationConsumer.cpp rename to Tracking/components/TrackingValidation.cpp From 2d0e91e2504760e208667f26ba1d796e1bf4c43b Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Mon, 16 Mar 2026 12:15:19 +0100 Subject: [PATCH 03/15] Add TrackingPerformance package and build configuration --- CMakeLists.txt | 1 + TrackingPerformance/CMakeLists.txt | 72 +++++++++++++++++++ .../src}/components/TrackingValidation.cpp | 10 +-- 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 TrackingPerformance/CMakeLists.txt rename {Tracking => TrackingPerformance/src}/components/TrackingValidation.cpp (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d2eccf..164938b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,5 +47,6 @@ function(set_test_env _testname) endfunction() add_subdirectory(RecoMCTruthLinkers) +add_subdirectory(TrackingPerformance) include(cmake/CreateProjectConfig.cmake) diff --git a/TrackingPerformance/CMakeLists.txt b/TrackingPerformance/CMakeLists.txt new file mode 100644 index 0000000..1c92035 --- /dev/null +++ b/TrackingPerformance/CMakeLists.txt @@ -0,0 +1,72 @@ +#[[ +Copyright (c) 2020-2024 Key4hep-Project. + +This file is part of Key4hep. +See https://key4hep.github.io/key4hep-doc/ for further info. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]] +set(PackageName TrackingPerformance) + +project(${PackageName}) + +file(GLOB sources + ${PROJECT_SOURCE_DIR}/src/*.cc + ${PROJECT_SOURCE_DIR}/src/components/*.cpp +) + +file(GLOB headers + ${PROJECT_SOURCE_DIR}/include/*.h +) + +find_package(ROOT REQUIRED COMPONENTS Core RIO Tree MathCore MathMore Graf Graf3d Hist) + + +gaudi_add_module(${PackageName} + SOURCES ${sources} + LINK + Gaudi::GaudiKernel + EDM4HEP::edm4hep + k4FWCore::k4FWCore + ROOT::MathCore + ROOT::MathMore + ROOT::Physics + ROOT::Graf + ROOT::Graf3d + ROOT::Hist +) + + +target_include_directories(${PackageName} PUBLIC + $ + $ + ${MarlinUtil_INCLUDE_DIRS} + ${DELPHES_INCLUDE_DIRS} +) + +set_target_properties(${PackageName} PROPERTIES PUBLIC_HEADER "${headers}") + +file(GLOB scripts + ${PROJECT_SOURCE_DIR}/test/*.py +) + +file(COPY ${scripts} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/test) + +install(TARGETS ${PackageName} + EXPORT ${CMAKE_PROJECT_NAME}Targets + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT bin + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT shlib + PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME}" COMPONENT dev +) + +install(FILES ${scripts} DESTINATION test) \ No newline at end of file diff --git a/Tracking/components/TrackingValidation.cpp b/TrackingPerformance/src/components/TrackingValidation.cpp similarity index 99% rename from Tracking/components/TrackingValidation.cpp rename to TrackingPerformance/src/components/TrackingValidation.cpp index 96195ad..b64d620 100644 --- a/Tracking/components/TrackingValidation.cpp +++ b/TrackingPerformance/src/components/TrackingValidation.cpp @@ -1,4 +1,4 @@ -// TrackingValidationConsumer +// TrackingValidation // // Validation consumer that writes the following TTrees: // 1) finder_particle_to_tracks @@ -652,7 +652,7 @@ static TCanvas* drawEfficiencyCanvas(TGraphErrors* g, } // ---------- CONSUMER ---------- -struct TrackingValidationConsumer final +struct TrackingValidation final : k4FWCore::Consumer&, // planar links @@ -662,7 +662,7 @@ struct TrackingValidationConsumer final const std::vector& // optional perfect fitted tracks )> { - TrackingValidationConsumer(const std::string& name, ISvcLocator* svcLoc) + TrackingValidation(const std::string& name, ISvcLocator* svcLoc) : Consumer( name, svcLoc, { @@ -808,7 +808,7 @@ struct TrackingValidationConsumer final } StatusCode finalize() override { - info() << "Finalizing TrackingValidationConsumer, wrote " << m_evt << " events" << endmsg; + info() << "Finalizing TrackingValidation, wrote " << m_evt << " events" << endmsg; if (m_outFile) { m_outFile->cd(); @@ -1291,4 +1291,4 @@ struct TrackingValidationConsumer final mutable FitterTree m_fit_vs_perfect; }; -DECLARE_COMPONENT(TrackingValidationConsumer) \ No newline at end of file +DECLARE_COMPONENT(TrackingValidation) \ No newline at end of file From 847ce7bd23e9579cf6c2c55101c4cacad85ef3de Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Wed, 18 Mar 2026 10:26:47 +0100 Subject: [PATCH 04/15] Finalize TrackingValidation README --- TrackingPerformance/CMakeLists.txt | 46 +- TrackingPerformance/README.md | 132 ++++ .../src/components/TrackingValidation.cpp | 63 +- .../test/SteeringFile_IDEA_o1_v03.py | 715 ++++++++++++++++++ .../inputFiles/SimpleGatrIDEAv3o1.onnx.md5 | 1 + .../test/runTrackingValidation.py | 259 +++++++ .../test/testTrackingValidation.sh | 126 +++ .../test/validation_output.root | Bin 0 -> 58309 bytes .../test/validation_output_test.root | Bin 0 -> 56655 bytes 9 files changed, 1322 insertions(+), 20 deletions(-) create mode 100644 TrackingPerformance/README.md create mode 100644 TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py create mode 100644 TrackingPerformance/test/inputFiles/SimpleGatrIDEAv3o1.onnx.md5 create mode 100644 TrackingPerformance/test/runTrackingValidation.py create mode 100644 TrackingPerformance/test/testTrackingValidation.sh create mode 100644 TrackingPerformance/test/validation_output.root create mode 100644 TrackingPerformance/test/validation_output_test.root diff --git a/TrackingPerformance/CMakeLists.txt b/TrackingPerformance/CMakeLists.txt index 1c92035..0307df5 100644 --- a/TrackingPerformance/CMakeLists.txt +++ b/TrackingPerformance/CMakeLists.txt @@ -16,13 +16,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ]] + set(PackageName TrackingPerformance) project(${PackageName}) +include(ExternalData) +list(APPEND ExternalData_URL_TEMPLATES + "https://key4hep.web.cern.ch:443/testFiles/k4RecTracker/%(hash)" +) + file(GLOB sources - ${PROJECT_SOURCE_DIR}/src/*.cc - ${PROJECT_SOURCE_DIR}/src/components/*.cpp + ${PROJECT_SOURCE_DIR}/src/*.cc + ${PROJECT_SOURCE_DIR}/src/components/*.cpp ) file(GLOB headers @@ -31,7 +37,6 @@ file(GLOB headers find_package(ROOT REQUIRED COMPONENTS Core RIO Tree MathCore MathMore Graf Graf3d Hist) - gaudi_add_module(${PackageName} SOURCES ${sources} LINK @@ -46,7 +51,6 @@ gaudi_add_module(${PackageName} ROOT::Hist ) - target_include_directories(${PackageName} PUBLIC $ $ @@ -54,13 +58,24 @@ target_include_directories(${PackageName} PUBLIC ${DELPHES_INCLUDE_DIRS} ) -set_target_properties(${PackageName} PROPERTIES PUBLIC_HEADER "${headers}") +set_target_properties(${PackageName} PROPERTIES + PUBLIC_HEADER "${headers}" +) -file(GLOB scripts +file(GLOB test_python_scripts ${PROJECT_SOURCE_DIR}/test/*.py ) -file(COPY ${scripts} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/test) +set(test_shell_scripts + ${PROJECT_SOURCE_DIR}/test/testTrackingValidation.sh +) + +set(test_scripts + ${test_python_scripts} + ${test_shell_scripts} +) + +file(COPY ${test_scripts} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/test) install(TARGETS ${PackageName} EXPORT ${CMAKE_PROJECT_NAME}Targets @@ -68,5 +83,20 @@ install(TARGETS ${PackageName} LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT shlib PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME}" COMPONENT dev ) +message(STATUS "CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}") +message(STATUS "CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}") +message(STATUS "PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}") +install(FILES ${test_scripts} DESTINATION test) + +SET(test_name "testTrackingValidation") + +ExternalData_Add_Test(testTrackingValidation + NAME ${test_name} + COMMAND sh +x TrackingPerformance/test/testTrackingValidation.sh + DATA{${CMAKE_SOURCE_DIR}/TrackingPerformance/test/inputFiles/SimpleGatrIDEAv3o1.onnx} +) + +set_test_env(${test_name}) +set_tests_properties(${test_name} PROPERTIES WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") +ExternalData_Add_Target(${test_name}) -install(FILES ${scripts} DESTINATION test) \ No newline at end of file diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md new file mode 100644 index 0000000..9317542 --- /dev/null +++ b/TrackingPerformance/README.md @@ -0,0 +1,132 @@ +# TrackingValidation + +## Overview + +`TrackingValidation` is a validation algorithm for studying the performance of track finding and track fitting in the tracking reconstruction. +It is desighned to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking. The algorithm writes validation information to a ROOT output file containing TTrees that can be used later for performance studies and plotting. + +Typical use cases include: +- validation of track-finder performance, +- validation of fitted-track parameters against MC truth, +- comparison between standard reconstructed tracks and perfectly associated reference tracks. + +--- + +## Inputs + +`TrackingValidation` expects an EDM4hep event content in which the relevant collections have already been produced by the preceding steps of the reconstruction chain. + +### Input collection types + +`TrackingValidation` consumes the following types of event collections: + +- **MC particle collection** + Used as the truth reference for particle-level validation. + +- **Planar digi-to-sim association collections** + Used to connect reconstructed planar hits to the originating simulated particles. + +- **Wire-hit digi-to-sim association collection** + Used to connect reconstructed drift-chamber hits to the originating simulated particles. + +- **Finder track collection** + Collection of tracks produced by the track-finding stage. + +- **Fitted track collection** + Collection of tracks produced by the standard fitting stage. + +- **Perfect fitted-track collection (optional)** + Collection of fitted tracks produced from perfect truth-based associations, used as an additional reference when perfect-fit validation is enabled. + + --- + + ## Outputs + + The algorithm writes a ROOT file specified by the `OutputFile`. + +The exact content of the output depends mainly on the validation mode selected through `Mode`: + +- **`Mode = 0` (full-pipeline mode)** + Produces both finder-level and fitter-level validation trees. + +- **`Mode = 1` (finder-only mode)** + Produces the trees related to track-finder validation. + +- **`Mode = 2` (fitter-only mode)** + Produces the trees related to fitted-track validation. + +In addition, `DoPerfectFit` controls whether the comparison to perfectly associated fitted tracks is filled. When enabled, the output also includes the fitter-versus-perfect validation information. + +--- + +## Finder validation: efficiency and purity + +To evaluate finder performance, each reconstructed track is matched to the truth particle with which it shares the largest number of hits. + +For each particle–track pair, the algorithm stores two standard hit-based quantities: + +- **track hit purity**: the fraction of hits on the reconstructed track that originate from the matched truth particle; +- **track hit efficiency**: the fraction of the truth-particle hits that are recovered in the reconstructed track. + +The summary **tracking efficiency** can then be defined in more than one way. + +- **`FinderEfficiencyDefinition = 1`** + A truth particle is counted as reconstructed if it is associated to at least one finder track with + `purity >= FinderPurityThreshold`. + In the default configuration, `FinderPurityThreshold = 0.75`, following the CMS association convention in which a reconstructed track is associated to a simulated particle if more than 75% of its hits originate from that particle. The tracking efficiency is then defined as the fraction of simulated tracks associated to at least one reconstructed track. :contentReference[oaicite:0]{index=0} + +- **`FinderEfficiencyDefinition = 2`** + A truth particle is counted as reconstructed if it is associated to at least one finder track with + `purity >= 0.5` **and** `efficiency >= 0.5`. + This corresponds to the stricter two-ratio variant, where both the purity of the reconstructed track and the fraction of recovered truth hits must exceed 50%. + +In the current implementation, the denominator of the efficiency plot includes generator-level particles with status 1 and at least one truth-linked hit. + +For more details on the CMS association convention and the related definitions of tracking efficiency, fake rate, and duplicate rate, see the CMS performance note *Performance of the track selection DNN in Run 3*. :contentReference[oaicite:1]{index=1} + +--- + +## How to run + +To run the `TrackingValidation` test locally, first set up the Key4hep environment: + +```bash +cd k4RecTracker +source /cvmfs/sw-nightlies.hsf.org/key4hep/setup.sh +``` + +Then build and install `k4RecTracker`: + +```bash +k4_local_repo +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=../install +make install -j 8 +``` + +Next, build and install `k4DetectorPerformance`, and expose its local install: + +```bash +cd k4DetectorPerformance +k4_local_repo +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=../install +make install -j 8 +``` + +Finally, run the test from the `k4DetectorPerformance` build directory: + +```bash +cd k4DetectorPerformance/build +ctest -V -R testTrackingValidation +``` + +The validation output is written to: + +```text +k4DetectorPerformance/TrackingPerformance/test/validation_output_test.root +``` + + diff --git a/TrackingPerformance/src/components/TrackingValidation.cpp b/TrackingPerformance/src/components/TrackingValidation.cpp index b64d620..32c7efb 100644 --- a/TrackingPerformance/src/components/TrackingValidation.cpp +++ b/TrackingPerformance/src/components/TrackingValidation.cpp @@ -552,10 +552,13 @@ static TCanvas* drawResolutionCanvas(TGraphErrors* g, return c; } -// A truth particle is counted as reconstructed if at least one associated finder track -// has purity above the configured threshold +// A truth particle is counted as reconstructed according to the selected summary definition: +// definition 1: at least one associated finder track has purity above FinderPurityThreshold +// definition 2: at least one associated finder track has both purity >= 0.5 +// and efficiency >= 0.5 static TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, const char* graphName, + int efficiencyDefinition, double purityThreshold, double pMin = 0.1, double pMax = 100.0, @@ -570,16 +573,19 @@ static TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, std::vector* pVec = nullptr; std::vector>* purVec = nullptr; + std::vector>* effVec = nullptr; finderTree->SetBranchAddress("p", &pVec); finderTree->SetBranchAddress("matchPurity", &purVec); + finderTree->SetBranchAddress("matchEfficiency", &effVec); const Long64_t nEntries = finderTree->GetEntries(); for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { finderTree->GetEntry(ievt); - if (!pVec || !purVec) continue; + if (!pVec || !purVec || !effVec) continue; if (pVec->size() != purVec->size()) continue; + if (pVec->size() != effVec->size()) continue; for (size_t i = 0; i < pVec->size(); ++i) { const double p = (*pVec)[i]; @@ -595,16 +601,36 @@ static TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, if (bin < 0) continue; // denominator: all truth particles present in finder_particle_to_tracks tree - // (genStatus1 and with at least one true hit, as enforced in fillFinderAssoc) + // (genStatus == 1 and with at least one true hit, as enforced in fillFinderAssoc) nDen[bin]++; bool isMatched = false; - for (size_t j = 0; j < (*purVec)[i].size(); ++j) { - if ((*purVec)[i][j] >= purityThreshold) { - isMatched = true; - break; + + const auto& purities = (*purVec)[i]; + const auto& efficiencies = (*effVec)[i]; + const size_t nMatches = std::min(purities.size(), efficiencies.size()); + + for (size_t j = 0; j < nMatches; ++j) { + const float purity = purities[j]; + const float efficiency = efficiencies[j]; + + if (efficiencyDefinition == 2) { + // CMS-style combined definition: + // require both purity and efficiency above 50%. + if (purity >= 0.5f && efficiency >= 0.5f) { + isMatched = true; + break; + } + } else { + // Default definition: + // require only purity above the configurable threshold. + if (purity >= purityThreshold) { + isMatched = true; + break; + } + } } - } + if (isMatched) nNum[bin]++; } } @@ -629,6 +655,7 @@ static TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, return g; } + static TCanvas* drawEfficiencyCanvas(TGraphErrors* g, const char* canvasName, const char* title, @@ -678,7 +705,7 @@ struct TrackingValidation final KeyValues("FittedTracks", {"FittedTracks"}), - KeyValues("PerfectFittedTracks", {"PerfectFitted_tracks"}), + KeyValues("PerfectFittedTracks", {"PerfectFittedTracks"}), }) {} StatusCode initialize() override { @@ -864,6 +891,7 @@ struct TrackingValidation final TGraphErrors* g_eff_vs_p = makeEfficiencyVsMomentum( m_finder_p2t.tree, "g_efficiency_vs_p", + m_finderEfficiencyDefinition.value(), m_finderPurityThreshold.value(), 0.1, 100.0, 0.15); if (g_eff_vs_p) { @@ -895,9 +923,20 @@ struct TrackingValidation final Gaudi::Property m_refY{this, "RefPointY", 0.f, "Reference point Y [mm] (must match fitter m_VP_referencePoint)"}; Gaudi::Property m_refZ{this, "RefPointZ", 0.f, "Reference point Z [mm] (must match fitter m_VP_referencePoint)"}; +// Definition used for the summary tracking-efficiency plot. +// Default = 1 keeps the current behaviour unchanged. + Gaudi::Property m_finderEfficiencyDefinition{ + this, "FinderEfficiencyDefinition", 1, + "Definition used for the tracking-efficiency summary plot: " + "1 = require purity >= FinderPurityThreshold; " + "2 = require purity >= 0.5 and efficiency >= 0.5" + }; + Gaudi::Property m_finderPurityThreshold{ - this, "FinderPurityThreshold", 0.75f, - "Minimum purity for a particle-track match to count in tracking efficiency"}; + this, "FinderPurityThreshold", 0.75f, + "Minimum purity for a particle-track match to count in tracking efficiency " + "when FinderEfficiencyDefinition = 1" + }; Gaudi::Property m_doPerfectFit{ this, "DoPerfectFit", false, diff --git a/TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py b/TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py new file mode 100644 index 0000000..d5852d4 --- /dev/null +++ b/TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py @@ -0,0 +1,715 @@ +from DDSim.DD4hepSimulation import DD4hepSimulation +from g4units import mm, GeV, MeV + +################################### +# user options +simulateCalo = True # set to False to skip the calo SD action +################################### + +SIM = DD4hepSimulation() + +## The compact XML file, or multiple compact files, if the last one is the closer. +SIM.compactFile = ["../FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml"] +## Lorentz boost for the crossing angle, in radian! +SIM.crossingAngleBoost = 0.015 +SIM.enableDetailedShowerMode = False +SIM.enableG4GPS = False +SIM.enableG4Gun = False +SIM.enableGun = True +## InputFiles for simulation .stdhep, .slcio, .HEPEvt, .hepevt, .pairs, .hepmc, .hepmc.gz, .hepmc.xz, .hepmc.bz2, .hepmc3, .hepmc3.gz, .hepmc3.xz, .hepmc3.bz2, .hepmc3.tree.root files are supported +SIM.inputFiles = [] +## Macro file to execute for runType 'run' or 'vis' +SIM.macroFile = "" +## number of events to simulate, used in batch mode +SIM.numberOfEvents = 10 +## Outputfile from the simulation: .slcio, edm4hep.root and .root output files are supported +SIM.outputFile = "testIDEA_o1_v03.root" +## Physics list to use in simulation +SIM.physicsList = "FTFP_BERT" +## Verbosity use integers from 1(most) to 7(least) verbose +## or strings: VERBOSE, DEBUG, INFO, WARNING, ERROR, FATAL, ALWAYS +SIM.printLevel = 3 +## The type of action to do in this invocation +## batch: just simulate some events, needs numberOfEvents, and input file or gun +## vis: enable visualisation, run the macroFile if it is set +## qt: enable visualisation in Qt shell, run the macroFile if it is set +## run: run the macroFile and exit +## shell: enable interactive session +SIM.runType = "batch" +## Skip first N events when reading a file +SIM.skipNEvents = 0 +## Steering file to change default behaviour +SIM.steeringFile = None +## FourVector of translation for the Smearing of the Vertex position: x y z t +SIM.vertexOffset = [0.0, 0.0, 0.0, 0.0] +## FourVector of the Sigma for the Smearing of the Vertex position: x y z t +SIM.vertexSigma = [0.0, 0.0, 0.0, 0.0] + + +################################################################################ +## Helper holding sensitive detector and other actions. +## +## The default tracker and calorimeter sensitive actions can be set with +## +## >>> SIM = DD4hepSimulation() +## >>> SIM.action.tracker=('Geant4TrackerWeightedAction', {'HitPositionCombination': 2, 'CollectSingleDeposits': False}) +## >>> SIM.action.calo = "Geant4CalorimeterAction" +## +## The default sensitive actions for calorimeters and trackers are applied based on the sensitive type. +## The list of sensitive types can be changed with +## +## >>> SIM = DD4hepSimulation() +## >>> SIM.action.trackerSDTypes = ['tracker', 'myTrackerSensType'] +## >>> SIM.calor.calorimeterSDTypes = ['calorimeter', 'myCaloSensType'] +## +## For specific subdetectors specific sensitive detectors can be set based on patterns in the name of the subdetector. +## +## >>> SIM = DD4hepSimulation() +## >>> SIM.action.mapActions['tpc'] = "TPCSDAction" +## +## and additional parameters for the sensitive detectors can be set when the map is given a tuple +## +## >>> SIM = DD4hepSimulation() +## >>> SIM.action.mapActions['ecal'] =( "CaloPreShowerSDAction", {"FirstLayerNumber": 1} ) +## +## Additional actions can be set as well with the following syntax variations: +## +## >>> SIM = DD4hepSimulation() +## # single action by name only: +## >>> SIM.action.run = "Geant4TestRunAction" +## # multiple actions with comma-separated names: +## >>> SIM.action.event = "Geant4TestEventAction/Action0,Geant4TestEventAction/Action1" +## # single action by tuple of name and parameter dict: +## >>> SIM.action.track = ( "Geant4TestTrackAction", {"Property_int": 10} ) +## # single action by dict of name and parameter dict: +## >>> SIM.action.step = { "name": "Geant4TestStepAction", "parameter": {"Property_int": 10} } +## # multiple actions by list of dict of name and parameter dict: +## >>> SIM.action.stack = [ { "name": "Geant4TestStackAction", "parameter": {"Property_int": 10} } ] +## +## On the command line or in python, these actions can be specified as JSON strings: +## $ ddsim --action.stack '{ "name": "Geant4TestStackAction", "parameter": { "Property_int": 10 } }' +## or +## >>> SIM.action.stack = ''' +## { +## "name": "Geant4TestStackAction", +## "parameter": { +## "Property_int": 10, +## "Property_double": "1.0*mm" +## } +## } +## ''' +## +## +################################################################################ + +## set the default calorimeter action +if simulateCalo: + SIM.action.calo = "Geant4ScintillatorCalorimeterAction" + ## List of patterns matching sensitive detectors of type Calorimeter. + SIM.action.calorimeterSDTypes = ["calorimeter", "DRcaloSiPMSD"] + SIM.action.mapActions["DRcalo"] = "DRCaloSDAction" + ## configure regex SD + SIM.geometry.regexSensitiveDetector["DRcalo"] = {"Match": ["(core|clad)"], "OutputLevel": 3} +else: + SIM.action.calo = "Geant4VoidSensitiveAction" + SIM.action.mapActions["DRcalo"] = "Geant4VoidSensitiveAction" + +## set the default event action +SIM.action.event = [] + +## Create a map of patterns and actions to be applied to sensitive detectors. +## +## Example: if the name of the detector matches 'tpc' the TPCSDAction is used. +## +## SIM.action.mapActions['tpc'] = "TPCSDAction" +## + +## Set the drift chamber action +SIM.action.mapActions['DCH_v2'] = "Geant4TrackerAction" + +## set the default run action +SIM.action.run = [] + +## set the default stack action +SIM.action.stack = [] + +## set the default step action +SIM.action.step = [] + +## set the default track action +SIM.action.track = [] + +## set the default tracker action +SIM.action.tracker = ( + "Geant4TrackerWeightedAction", + {"HitPositionCombination": 2, "CollectSingleDeposits": False}, +) + +## List of patterns matching sensitive detectors of type Tracker. +SIM.action.trackerSDTypes = ["tracker"] + + +################################################################################ +## Configuration for the magnetic field (stepper) +################################################################################ +SIM.field.delta_chord = 0.25 +SIM.field.delta_intersection = 0.001 +SIM.field.delta_one_step = 0.01 +SIM.field.eps_max = 0.001 +SIM.field.eps_min = 5e-05 +SIM.field.equation = "Mag_UsualEqRhs" +SIM.field.largest_step = 10000.0 +SIM.field.min_chord_step = 0.01 +SIM.field.stepper = "ClassicalRK4" + + +################################################################################ +## Configuration for sensitive detector filters +## +## Set the default filter for 'tracker' +## >>> SIM.filter.tracker = "edep1kev" +## Use no filter for 'calorimeter' by default +## >>> SIM.filter.calo = "" +## +## Assign a filter to a sensitive detector via pattern matching +## >>> SIM.filter.mapDetFilter['FTD'] = "edep1kev" +## +## Or more than one filter: +## >>> SIM.filter.mapDetFilter['FTD'] = ["edep1kev", "geantino"] +## +## Don't use the default filter or anything else: +## >>> SIM.filter.mapDetFilter['TPC'] = None ## or "" or [] +## +## Create a custom filter. The dictionary is used to instantiate the filter later on +## >>> SIM.filter.filters['edep3kev'] = dict(name="EnergyDepositMinimumCut/3keV", parameter={"Cut": 3.0*keV} ) +## +## +################################################################################ + +## +## default filter for calorimeter sensitive detectors; +## this is applied if no other filter is used for a calorimeter +## +# note: do not turn on the calo filter, otherwise all optical photons will be killed! +SIM.filter.calo = "" + +## list of filter objects: map between name and parameter dictionary +SIM.filter.filters = { + "geantino": {"name": "GeantinoRejectFilter/GeantinoRejector", "parameter": {}}, + "edep1kev": {"name": "EnergyDepositMinimumCut", "parameter": {"Cut": 0.001}}, + "edep0": {"name": "EnergyDepositMinimumCut/Cut0", "parameter": {"Cut": 0.0}}, +} + +## a map between patterns and filter objects, using patterns to attach filters to sensitive detector +SIM.filter.mapDetFilter["DCH_v2"] = "edep0" +SIM.filter.mapDetFilter["Muon-System"] = "edep0" + +## default filter for tracking sensitive detectors; this is applied if no other filter is used for a tracker +SIM.filter.tracker = "edep1kev" + + +################################################################################ +## Configuration for the Detector Construction. +################################################################################ +SIM.geometry.dumpGDML = "" +SIM.geometry.dumpHierarchy = 0 + +## Print Debug information about Elements +SIM.geometry.enableDebugElements = False + +## Print Debug information about Materials +SIM.geometry.enableDebugMaterials = False + +## Print Debug information about Placements +SIM.geometry.enableDebugPlacements = False + +## Print Debug information about Reflections +SIM.geometry.enableDebugReflections = False + +## Print Debug information about Regions +SIM.geometry.enableDebugRegions = False + +## Print Debug information about Shapes +SIM.geometry.enableDebugShapes = False + +## Print Debug information about Surfaces +SIM.geometry.enableDebugSurfaces = False + +## Print Debug information about Volumes +SIM.geometry.enableDebugVolumes = False + +## Print information about placements +SIM.geometry.enablePrintPlacements = False + +## Print information about Sensitives +SIM.geometry.enablePrintSensitives = False + +################################################################################ +## Configuration for the GuineaPig InputFiles +################################################################################ + +## Set the number of pair particles to simulate per event. +## Only used if inputFile ends with ".pairs" +## If "-1" all particles will be simulated in a single event +## +SIM.guineapig.particlesPerEvent = "-1" + + +################################################################################ +## Configuration for the DDG4 ParticleGun +################################################################################ + +## direction of the particle gun, 3 vector +SIM.gun.direction = (1.0, 0.1, 0.1) + +## choose the distribution of the random direction for theta +## +## Options for random distributions: +## +## 'uniform' is the default distribution, flat in theta +## 'cos(theta)' is flat in cos(theta) +## 'eta', or 'pseudorapidity' is flat in pseudorapity +## 'ffbar' is distributed according to 1+cos^2(theta) +## +## Setting a distribution will set isotrop = True +## +SIM.gun.distribution = None + +## Total energy (including mass) for the particle gun. +## +## If not None, it will overwrite the setting of momentumMin and momentumMax +SIM.gun.energy = 10.0 * GeV + +## Maximal pseudorapidity for random distibution (overrides thetaMin) +SIM.gun.etaMax = None + +## Minimal pseudorapidity for random distibution (overrides thetaMax) +SIM.gun.etaMin = None + +## isotropic distribution for the particle gun +## +## use the options phiMin, phiMax, thetaMin, and thetaMax to limit the range of randomly distributed directions +## if one of these options is not None the random distribution will be set to True and cannot be turned off! +## +SIM.gun.isotrop = False + +## Maximal momentum when using distribution (default = 0.0) +# SIM.gun.momentumMax = 10000.0 + +## Minimal momentum when using distribution (default = 0.0) +# SIM.gun.momentumMin = 0.0 +SIM.gun.multiplicity = 1 +SIM.gun.particle = "e-" + +## Maximal azimuthal angle for random distribution +SIM.gun.phiMax = None + +## Minimal azimuthal angle for random distribution +SIM.gun.phiMin = None + +## position of the particle gun, 3 vector +SIM.gun.position = (0.0, 0.0, 0.0) + +## Maximal polar angle for random distribution +SIM.gun.thetaMax = None + +## Minimal polar angle for random distribution +SIM.gun.thetaMin = None + + +################################################################################ +## Configuration for the hepmc3 InputFiles +################################################################################ + +## Set the name of the attribute contraining color flow information index 0. +SIM.hepmc3.Flow1 = "flow1" + +## Set the name of the attribute contraining color flow information index 1. +SIM.hepmc3.Flow2 = "flow2" + +## Set to false if the input should be opened with the hepmc2 ascii reader. +## +## If ``True`` a '.hepmc' file will be opened with the HEPMC3 Reader Factory. +## +## Defaults to true if DD4hep was build with HEPMC3 support. +## +SIM.hepmc3.useHepMC3 = True + + +################################################################################ +## Configuration for Input Files. +################################################################################ + +## Set one or more functions to configure input steps. +## +## The functions must take a ``DD4hepSimulation`` object as their only argument and return the created generatorAction +## ``gen`` (for example). +## +## For example one can add this to the ddsim steering file: +## +## def exampleUserPlugin(dd4hepSimulation): +## '''Example code for user created plugin. +## +## :param DD4hepSimulation dd4hepSimulation: The DD4hepSimulation instance, so all parameters can be accessed +## :return: GeneratorAction +## ''' +## from DDG4 import GeneratorAction, Kernel +## # Geant4InputAction is the type of plugin, Cry1 just an identifier +## gen = GeneratorAction(Kernel(), 'Geant4InputAction/Cry1' , True) +## # CRYEventReader is the actual plugin, steeringFile its constructor parameter +## gen.Input = 'CRYEventReader|' + 'steeringFile' +## # we can give a dictionary of Parameters that has to be interpreted by the setParameters function of the plugin +## gen.Parameters = {'DataFilePath': '/path/to/files/data'} +## gen.enableUI() +## return gen +## +## SIM.inputConfig.userInputPlugin = exampleUserPlugin +## +## Repeat function definition and assignment to add multiple input steps +## +## +SIM.inputConfig.userInputPlugin = [] + + +################################################################################ +## Configuration for the generator-level InputFiles +################################################################################ + +## Set the name of the collection containing the MCParticle input. +## Default is "MCParticle". +## +SIM.lcio.mcParticleCollectionName = "MCParticle" + + +################################################################################ +## Configuration for the LCIO output file settings +################################################################################ + +## The event number offset to write in slcio output file. E.g setting it to 42 will start counting events from 42 instead of 0 +SIM.meta.eventNumberOffset = 0 + +## Event parameters to write in every event. Use C/F/I ids to specify parameter type. E.g parameterName/F=0.42 to set a float parameter +SIM.meta.eventParameters = [] + +## The run number offset to write in slcio output file. E.g setting it to 42 will start counting runs from 42 instead of 0 +SIM.meta.runNumberOffset = 0 + + +################################################################################ +## Configuration for the output levels of DDG4 components +################################################################################ + +## Output level for geometry. +SIM.output.geometry = 2 + +## Output level for input sources +SIM.output.inputStage = 3 + +## Output level for Geant4 kernel +SIM.output.kernel = 3 + +## Output level for ParticleHandler +SIM.output.part = 3 + +## Output level for Random Number Generator setup +SIM.output.random = 6 + + +################################################################################ +## Configuration for Output Files. +################################################################################ + +## Use the DD4HEP output plugin regardless of outputfilename. +SIM.outputConfig.forceDD4HEP = False + +## Use the EDM4HEP output plugin regardless of outputfilename. +SIM.outputConfig.forceEDM4HEP = False + +## Use the LCIO output plugin regardless of outputfilename. +SIM.outputConfig.forceLCIO = False + +## Set a function to configure the outputFile. +## +## The function must take a ``DD4hepSimulation`` object as its only argument and return ``None``. +## +## For example one can add this to the ddsim steering file: +## +## def exampleUserPlugin(dd4hepSimulation): +## '''Example code for user created plugin. +## +## :param DD4hepSimulation dd4hepSimulation: The DD4hepSimulation instance, so all parameters can be accessed +## :return: None +## ''' +## from DDG4 import EventAction, Kernel +## dd = dd4hepSimulation # just shorter variable name +## evt_root = EventAction(Kernel(), 'Geant4Output2ROOT/' + dd.outputFile, True) +## evt_root.HandleMCTruth = True or False +## evt_root.Control = True +## output = dd.outputFile +## if not dd.outputFile.endswith(dd.outputConfig.myExtension): +## output = dd.outputFile + dd.outputConfig.myExtension +## evt_root.Output = output +## evt_root.enableUI() +## Kernel().eventAction().add(evt_root) +## return None +## +## SIM.outputConfig.userOutputPlugin = exampleUserPlugin +## # arbitrary options can be created and set via the steering file or command line +## SIM.outputConfig.myExtension = '.csv' +## + + +def Geant4Output2EDM4hep_DRC_plugin(dd4hepSimulation): + from DDG4 import EventAction, Kernel + + evt_root = EventAction( + Kernel(), "Geant4Output2EDM4hep_DRC/" + dd4hepSimulation.outputFile, True + ) + evt_root.Control = True + output = dd4hepSimulation.outputFile + evt_root.Output = output + evt_root.enableUI() + Kernel().eventAction().add(evt_root) + return None + + +SIM.outputConfig.userOutputPlugin = Geant4Output2EDM4hep_DRC_plugin + +################################################################################ +## Configuration for the Particle Handler/ MCTruth treatment +################################################################################ + +## Enable lots of printout on simulated hits and MC-truth information +SIM.part.enableDetailedHitsAndParticleInfo = False + +## Keep all created particles +SIM.part.keepAllParticles = False + +## Minimal distance between particle vertex and endpoint of parent after +## which the vertexIsNotEndpointOfParent flag is set +## +SIM.part.minDistToParentVertex = 2.2e-14 + +## MinimalKineticEnergy to store particles created in the tracking region +SIM.part.minimalKineticEnergy = 1.0 + +## Printout at End of Tracking +SIM.part.printEndTracking = False + +## Printout at Start of Tracking +SIM.part.printStartTracking = False + +## List of processes to save, on command line give as whitespace separated string in quotation marks +SIM.part.saveProcesses = ["Decay"] + +## Optionally enable an extended Particle Handler +SIM.part.userParticleHandler = "Geant4TCUserParticleHandler" + + +################################################################################ +## Configuration for the PhysicsList and Monte Carlo particle selection. +## +## To load arbitrary plugins, add a function to be executed. +## +## The function must take the DDG4.Kernel() object as the only argument. +## +## For example, add a function definition and the call to a steering file:: +## +## def setupCerenkov(kernel): +## from DDG4 import PhysicsList +## seq = kernel.physicsList() +## cerenkov = PhysicsList(kernel, 'Geant4CerenkovPhysics/CerenkovPhys') +## cerenkov.MaxNumPhotonsPerStep = 10 +## cerenkov.MaxBetaChangePerStep = 10.0 +## cerenkov.TrackSecondariesFirst = True +## cerenkov.VerboseLevel = 2 +## cerenkov.enableUI() +## seq.adopt(cerenkov) +## ph = PhysicsList(kernel, 'Geant4OpticalPhotonPhysics/OpticalGammaPhys') +## ph.addParticleConstructor('G4OpticalPhoton') +## ph.VerboseLevel = 2 +## ph.enableUI() +## seq.adopt(ph) +## return None +## +## SIM.physics.setupUserPhysics(setupCerenkov) +## +## # End of example +## +################################################################################ + +## Set of Generator Statuses that are used to mark unstable particles that should decay inside of Geant4. +## +SIM.physics.alternativeDecayStatuses = set() + +## If true, add decay processes for all particles. +## +## Only enable when creating a physics list not based on an existing Geant4 list! +## +SIM.physics.decays = False + +## The name of the Geant4 Physics list. +SIM.physics.list = "FTFP_BERT" + +## location of particle.tbl file containing extra particles and their lifetime information +## +## For example in $DD4HEP/examples/DDG4/examples/particle.tbl +## +SIM.physics.pdgfile = None + +## The global geant4 rangecut for secondary production +## +## Default is 0.7 mm as is the case in geant4 10 +## +## To disable this plugin and be absolutely sure to use the Geant4 default range cut use "None" +## +## Set printlevel to DEBUG to see a printout of all range cuts, +## but this only works if range cut is not "None" +## +SIM.physics.rangecut = None + +## Set of PDG IDs that will not be passed from the input record to Geant4. +## +## Quarks, gluons and W's Z's etc should not be treated by Geant4 +## +SIM.physics.rejectPDGs = { + 1, + 2, + 3, + 4, + 5, + 6, + 3201, + 3203, + 4101, + 4103, + 21, + 23, + 24, + 5401, + 25, + 2203, + 5403, + 3101, + 3103, + 4403, + 2101, + 5301, + 2103, + 5303, + 4301, + 1103, + 4303, + 5201, + 5203, + 3303, + 4201, + 4203, + 5101, + 5103, + 5503, +} + +## Set of PDG IDs for particles that should not be passed to Geant4 if their properTime is 0. +## +## The properTime of 0 indicates a documentation to add FSR to a lepton for example. +## +SIM.physics.zeroTimePDGs = {17, 11, 13, 15} + + +def setupOpticalPhysics(kernel): + from DDG4 import PhysicsList + + seq = kernel.physicsList() + cerenkov = PhysicsList(kernel, "Geant4CerenkovPhysics/CerenkovPhys") + cerenkov.TrackSecondariesFirst = True + cerenkov.VerboseLevel = 1 + cerenkov.enableUI() + seq.adopt(cerenkov) + + opt = PhysicsList(kernel, "Geant4OpticalPhotonPhysics/OpticalGammaPhys") + opt.addParticleConstructor("G4OpticalPhoton") + opt.VerboseLevel = 1 + # set BoundaryInvokeSD to true when using DRC wafer as the SD + # opt.BoundaryInvokeSD = True + opt.enableUI() + seq.adopt(opt) + + return None + +if simulateCalo: + SIM.physics.setupUserPhysics(setupOpticalPhysics) + +def setupDRCFastSim(kernel): + from DDG4 import DetectorConstruction, Geant4, PhysicsList + + geant4 = Geant4(kernel) + seq = geant4.detectorConstruction() + # Create a model for fast simulation + model = DetectorConstruction(kernel, str("Geant4DRCFiberModel/ShowerModel")) + # Mandatory model parameters + model.RegionName = "FastSimOpFiberRegion" + model.Enable = True + model.ApplicableParticles = ["opticalphoton"] + model.enableUI() + seq.adopt(model) + # Now build the physics list: + phys = kernel.physicsList() + ph = PhysicsList(kernel, str("Geant4FastPhysics/FastPhysicsList")) + ph.EnabledParticles = ["opticalphoton"] + ph.BeVerbose = True + ph.enableUI() + phys.adopt(ph) + phys.dump() + + +# turn-on fastsim if the skipScint option of the SD is set to false +# SIM.physics.setupUserPhysics(setupDRCFastSim) + +################################################################################ +## Properties for the random number generator +################################################################################ + +## If True, calculate random seed for each event basedon eventID and runID +## Allows reproducibility even whenSkippingEvents +SIM.random.enableEventSeed = False +SIM.random.file = None +SIM.random.luxury = 1 +SIM.random.replace_gRandom = True +SIM.random.seed = None +SIM.random.type = None + + +################################################################################ +## Configuration for setting commands to run during different phases. +## +## In this section, one can configure commands that should be run during the different phases of the Geant4 execution. +## +## 1. Configuration +## 2. Initialization +## 3. Pre Run +## 4. Post Run +## 5. Terminate / Finalization +## +## For example, one can add +## +## >>> SIM.ui.commandsConfigure = ['/physics_lists/em/SyncRadiation true'] +## +## Further details should be taken from the Geant4 documentation. +## +################################################################################ + +## List of UI commands to run during the 'Configure' phase. +SIM.ui.commandsConfigure = [] + +## List of UI commands to run during the 'Initialize' phase. +SIM.ui.commandsInitialize = [] + +## List of UI commands to run during the 'PostRun' phase. +SIM.ui.commandsPostRun = [] + +## List of UI commands to run during the 'PreRun' phase. +SIM.ui.commandsPreRun = [] + +## List of UI commands to run during the 'Terminate' phase. +SIM.ui.commandsTerminate = [] diff --git a/TrackingPerformance/test/inputFiles/SimpleGatrIDEAv3o1.onnx.md5 b/TrackingPerformance/test/inputFiles/SimpleGatrIDEAv3o1.onnx.md5 new file mode 100644 index 0000000..3df28d7 --- /dev/null +++ b/TrackingPerformance/test/inputFiles/SimpleGatrIDEAv3o1.onnx.md5 @@ -0,0 +1 @@ +303e5169a22383d74627a3c412707835 \ No newline at end of file diff --git a/TrackingPerformance/test/runTrackingValidation.py b/TrackingPerformance/test/runTrackingValidation.py new file mode 100644 index 0000000..bfcc23d --- /dev/null +++ b/TrackingPerformance/test/runTrackingValidation.py @@ -0,0 +1,259 @@ +import os +import math + +from Gaudi.Configuration import INFO +from Configurables import EventDataSvc, GeoSvc, UniqueIDGenSvc, RndmGenSvc +from k4FWCore import IOSvc, ApplicationMgr +from k4FWCore.parseArgs import parser + +# -------------------- +# Arguments +# -------------------- +parser.add_argument("--inputFile", required=True, help="Input simulated EDM4hep ROOT file") +parser.add_argument("--modelPath", required=True, help="Path to the GGTF ONNX model") +parser.add_argument("--outputFile", default="out_reco.root", + help="Output EDM4hep ROOT file with reconstructed collections") +parser.add_argument("--validationFile", default="validation.root", + help="Output ROOT file written by TrackingValidationConsumer") +args = parser.parse_args() + +# -------------------- +# IO +# -------------------- +io = IOSvc("IOSvc") +io.Input = args.inputFile +io.Output = args.outputFile + +# -------------------- +# Geometry +# -------------------- +geoservice = GeoSvc("GeoSvc") +geoservice.detectors = [ + os.path.join(os.environ["K4GEO"], "FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml") +] +geoservice.EnableGeant4Geo = False +geoservice.OutputLevel = INFO + +# -------------------- +# Digitizers +# -------------------- +from Configurables import DDPlanarDigi, DCHdigi_v02 + +innerVertexResolution_x = 0.003 +innerVertexResolution_y = 0.003 +innerVertexResolution_t = 1000 + +outerVertexResolution_x = 0.050 / math.sqrt(12) +outerVertexResolution_y = 0.150 / math.sqrt(12) +outerVertexResolution_t = 1000 + +vtxb_digitizer = DDPlanarDigi("VTXBdigitizer") +vtxb_digitizer.SubDetectorName = "Vertex" +vtxb_digitizer.IsStrip = False +vtxb_digitizer.ResolutionU = [ + innerVertexResolution_x, + innerVertexResolution_x, + innerVertexResolution_x, + outerVertexResolution_x, + outerVertexResolution_x, +] +vtxb_digitizer.ResolutionV = [ + innerVertexResolution_y, + innerVertexResolution_y, + innerVertexResolution_y, + outerVertexResolution_y, + outerVertexResolution_y, +] +vtxb_digitizer.ResolutionT = [ + innerVertexResolution_t, + innerVertexResolution_t, + innerVertexResolution_t, + outerVertexResolution_t, + outerVertexResolution_t, +] +vtxb_digitizer.SimTrackHitCollectionName = ["VertexBarrelCollection"] +vtxb_digitizer.SimTrkHitRelCollection = ["VTXBSimDigiLinks"] +vtxb_digitizer.TrackerHitCollectionName = ["VTXBDigis"] +vtxb_digitizer.ForceHitsOntoSurface = True + +vtxd_digitizer = DDPlanarDigi("VTXDdigitizer") +vtxd_digitizer.SubDetectorName = "Vertex" +vtxd_digitizer.IsStrip = False +vtxd_digitizer.ResolutionU = [ + outerVertexResolution_x, + outerVertexResolution_x, + outerVertexResolution_x, +] +vtxd_digitizer.ResolutionV = [ + outerVertexResolution_y, + outerVertexResolution_y, + outerVertexResolution_y, +] +vtxd_digitizer.ResolutionT = [ + outerVertexResolution_t, + outerVertexResolution_t, + outerVertexResolution_t, +] +vtxd_digitizer.SimTrackHitCollectionName = ["VertexEndcapCollection"] +vtxd_digitizer.SimTrkHitRelCollection = ["VTXDSimDigiLinks"] +vtxd_digitizer.TrackerHitCollectionName = ["VTXDDigis"] +vtxd_digitizer.ForceHitsOntoSurface = True + +siWrapperResolution_x = 0.050 / math.sqrt(12) +siWrapperResolution_y = 1.0 / math.sqrt(12) +siWrapperResolution_t = 0.040 + +siwrb_digitizer = DDPlanarDigi("SiWrBdigitizer") +siwrb_digitizer.SubDetectorName = "SiWrB" +siwrb_digitizer.IsStrip = False +siwrb_digitizer.ResolutionU = [siWrapperResolution_x, siWrapperResolution_x] +siwrb_digitizer.ResolutionV = [siWrapperResolution_y, siWrapperResolution_y] +siwrb_digitizer.ResolutionT = [siWrapperResolution_t, siWrapperResolution_t] +siwrb_digitizer.SimTrackHitCollectionName = ["SiWrBCollection"] +siwrb_digitizer.SimTrkHitRelCollection = ["SiWrBSimDigiLinks"] +siwrb_digitizer.TrackerHitCollectionName = ["SiWrBDigis"] +siwrb_digitizer.ForceHitsOntoSurface = True + +siwrd_digitizer = DDPlanarDigi("SiWrDdigitizer") +siwrd_digitizer.SubDetectorName = "SiWrD" +siwrd_digitizer.IsStrip = False +siwrd_digitizer.ResolutionU = [siWrapperResolution_x, siWrapperResolution_x] +siwrd_digitizer.ResolutionV = [siWrapperResolution_y, siWrapperResolution_y] +siwrd_digitizer.ResolutionT = [siWrapperResolution_t, siWrapperResolution_t] +siwrd_digitizer.SimTrackHitCollectionName = ["SiWrDCollection"] +siwrd_digitizer.SimTrkHitRelCollection = ["SiWrDSimDigiLinks"] +siwrd_digitizer.TrackerHitCollectionName = ["SiWrDDigis"] +siwrd_digitizer.ForceHitsOntoSurface = True + +dch_digitizer = DCHdigi_v02( + "DCHdigi2", + InputSimHitCollection=["DCHCollection"], + OutputDigihitCollection=["DCH_DigiCollection"], + OutputLinkCollection=["DCH_DigiSimAssociationCollection"], + DCH_name="DCH_v2", + zResolution_mm=30.0, + xyResolution_mm=0.1, + Deadtime_ns=400.0, + GasType=0, + ReadoutWindowStartTime_ns=1.0, + ReadoutWindowDuration_ns=450.0, + DriftVelocity_um_per_ns=-1.0, + SignalVelocity_mm_per_ns=200.0, + OutputLevel=INFO, +) + +# -------------------- +# Track finder +# -------------------- +from Configurables import GGTFTrackFinder + + +ggtf = GGTFTrackFinder( + "GGTFTrackFinder", + InputPlanarHitCollections=["VTXBDigis", "VTXDDigis", "SiWrBDigis", "SiWrDDigis"], + InputWireHitCollections=["DCH_DigiCollection"], + OutputTracksGGTF=["GGTFTracks"], + ModelPath=args.modelPath, + Tbeta=0.6, + Td=0.3, + OutputLevel=INFO, +) + +# -------------------- +# Reco fitter +# -------------------- +from Configurables import GenfitTrackFitter + +reco_fitter = GenfitTrackFitter("RecoTrackFitter") +reco_fitter.InputTracks = ["GGTFTracks"] +reco_fitter.OutputFittedTracks = ["FittedTracks"] +reco_fitter.RunSingleEvaluation = True +reco_fitter.UseBrems = False +reco_fitter.BetaInit = 100.0 +reco_fitter.BetaFinal = 0.1 +reco_fitter.BetaSteps = 10 +reco_fitter.InitializationType = 1 +reco_fitter.SkipTrackOrdering = False +reco_fitter.SkipUnmatchedTracks = False +reco_fitter.OutputLevel = INFO + +# -------------------- +# Perfect track finder +# -------------------- +from Configurables import PerfectTrackFinder + +perfect = PerfectTrackFinder("PerfectTrackFinder") +perfect.InputMCParticles = ["MCParticles"] +perfect.InputPlanarHitCollections = [ + "SiWrBSimDigiLinks", + "SiWrDSimDigiLinks", + "VTXBSimDigiLinks", + "VTXDSimDigiLinks", +] +perfect.InputWireHitCollections = ["DCH_DigiSimAssociationCollection"] +perfect.OutputPerfectTracks = ["PerfectTracks"] +perfect.OutputLevel = INFO + +# -------------------- +# Perfect fitter +# -------------------- +perfect_fitter = GenfitTrackFitter("PerfectTrackFitter") +perfect_fitter.InputTracks = ["PerfectTracks"] +perfect_fitter.OutputFittedTracks = ["PerfectFittedTracks"] +perfect_fitter.RunSingleEvaluation = True +perfect_fitter.UseBrems = False +perfect_fitter.BetaInit = 100.0 +perfect_fitter.BetaFinal = 0.1 +perfect_fitter.BetaSteps = 10 +perfect_fitter.InitializationType = 1 +perfect_fitter.SkipTrackOrdering = False +perfect_fitter.SkipUnmatchedTracks = False +perfect_fitter.OutputLevel = INFO + +# -------------------- +# Validation consumer +# -------------------- +from Configurables import TrackingValidation + +val = TrackingValidation("TrackingValidation") +val.OutputFile = args.validationFile +val.Mode = 0 +val.Bz = 2.0 +val.RefPointX = 0.0 +val.RefPointY = 0.0 +val.RefPointZ = 0.0 +val.DoPerfectFit = True +val.MCParticles = ["MCParticles"] +val.PlanarLinks = [ + "SiWrBSimDigiLinks", + "SiWrDSimDigiLinks", + "VTXBSimDigiLinks", + "VTXDSimDigiLinks", +] +val.DCHLinks = ["DCH_DigiSimAssociationCollection"] +val.FinderTracks = ["GGTFTracks"] +val.FittedTracks = ["FittedTracks"] +val.PerfectFittedTracks = ["PerfectFittedTracks"] +val.OutputLevel = INFO + +# -------------------- +# AppMgr +# -------------------- +ApplicationMgr( + TopAlg=[ + dch_digitizer, + vtxb_digitizer, + vtxd_digitizer, + siwrb_digitizer, + siwrd_digitizer, + ggtf, + reco_fitter, + perfect, + perfect_fitter, + val, + ], + EvtSel="NONE", + EvtMax=-1, + ExtSvc=[geoservice, EventDataSvc("EventDataSvc"), UniqueIDGenSvc("uidSvc"), RndmGenSvc()], + OutputLevel=INFO, +) diff --git a/TrackingPerformance/test/testTrackingValidation.sh b/TrackingPerformance/test/testTrackingValidation.sh new file mode 100644 index 0000000..4ac0cb4 --- /dev/null +++ b/TrackingPerformance/test/testTrackingValidation.sh @@ -0,0 +1,126 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MODEL_FILE="${1:-}" + +if [ -z "${MODEL_FILE}" ]; then + echo "ERROR: missing ONNX model path argument" + echo "Usage: $0 /full/path/to/SimpleGatrIDEAv3o1.onnx" + exit 1 +fi + +if [ ! -f "${MODEL_FILE}" ]; then + echo "ERROR: ONNX model file not found: ${MODEL_FILE}" + exit 1 +fi + +if [ -z "${K4GEO:-}" ]; then + echo "ERROR: K4GEO is not set" + exit 1 +fi + +if [ ! -d "${K4GEO}" ]; then + echo "ERROR: K4GEO does not point to a valid directory: ${K4GEO}" + exit 1 +fi + +if ! command -v ddsim >/dev/null 2>&1; then + echo "ERROR: ddsim not found in PATH" + exit 1 +fi + +if ! command -v k4run >/dev/null 2>&1; then + echo "ERROR: k4run not found in PATH" + exit 1 +fi + +XML_FILE="${K4GEO}/FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml" +STEERING_FILE="${SCRIPT_DIR}/SteeringFile_IDEA_o1_v03.py" +RUN_FILE="${SCRIPT_DIR}/runTrackingValidation.py" +VAL_FILE="${SCRIPT_DIR}/validation_output_test.root" + +if [ ! -f "${XML_FILE}" ]; then + echo "ERROR: geometry XML file not found: ${XML_FILE}" + exit 1 +fi + +if [ ! -f "${STEERING_FILE}" ]; then + echo "ERROR: DDSim steering file not found: ${STEERING_FILE}" + exit 1 +fi + +if [ ! -f "${RUN_FILE}" ]; then + echo "ERROR: tracking validation run file not found: ${RUN_FILE}" + exit 1 +fi + +TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/tracking_validation.XXXXXX")" +trap 'rm -rf "${TMPDIR}"' EXIT + +SIM_FILE="${TMPDIR}/out_sim_edm4hep.root" +RECO_FILE="${TMPDIR}/out_reco.root" + +rm -f "${VAL_FILE}" + +N_EVENTS=5 +SEED=42 + +echo "=== Test configuration ===" +echo "Script dir: ${SCRIPT_DIR}" +echo "Temporary dir: ${TMPDIR}" +echo "Geometry XML: ${XML_FILE}" +echo "DDSim steering: ${STEERING_FILE}" +echo "Run script: ${RUN_FILE}" +echo "ONNX model: ${MODEL_FILE}" +echo "Simulation file: ${SIM_FILE}" +echo "Reco file: ${RECO_FILE}" +echo "Validation file: ${VAL_FILE}" +echo "Events: ${N_EVENTS}" +echo "Seed: ${SEED}" + +echo "=== Step 1: DDSim ===" +ddsim \ + --steeringFile "${STEERING_FILE}" \ + --compactFile "${XML_FILE}" \ + -G \ + --gun.particle mu- \ + --gun.energy "5*GeV" \ + --gun.distribution uniform \ + --gun.thetaMin "89*deg" \ + --gun.thetaMax "89*deg" \ + --gun.phiMin "0*deg" \ + --gun.phiMax "0*deg" \ + --random.seed "${SEED}" \ + --numberOfEvents "${N_EVENTS}" \ + --outputFile "${SIM_FILE}" + +if [ ! -f "${SIM_FILE}" ]; then + echo "ERROR: simulation output was not created: ${SIM_FILE}" + exit 1 +fi + +echo "=== Step 2: reconstruction + perfect tracking + validation ===" +k4run "${RUN_FILE}" \ + --inputFile "${SIM_FILE}" \ + --modelPath "${MODEL_FILE}" \ + --outputFile "${RECO_FILE}" \ + --validationFile "${VAL_FILE}" + +if [ ! -f "${RECO_FILE}" ]; then + echo "ERROR: reconstruction output was not created: ${RECO_FILE}" + exit 1 +fi + +if [ ! -f "${VAL_FILE}" ]; then + echo "ERROR: validation output was not created: ${VAL_FILE}" + exit 1 +fi + +echo "=== Step 3: check outputs ===" +test -f "${SIM_FILE}" +test -f "${RECO_FILE}" +test -f "${VAL_FILE}" + +echo "Test completed successfully." +echo "Validation file: ${VAL_FILE}" \ No newline at end of file diff --git a/TrackingPerformance/test/validation_output.root b/TrackingPerformance/test/validation_output.root new file mode 100644 index 0000000000000000000000000000000000000000..b558651aa5e6a31cec6119fc328b5b58c3b5ee49 GIT binary patch literal 58309 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%0AfH-PK2Lgw8>txN7Yn|I?$J#9C%YnpS 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 Bgv0;< literal 0 HcmV?d00001 diff --git a/TrackingPerformance/test/validation_output_test.root b/TrackingPerformance/test/validation_output_test.root new file mode 100644 index 0000000000000000000000000000000000000000..a00512808ee54cb74b04e1837f83207f16da46e5 GIT binary patch literal 56655 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Μ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 NG Date: Wed, 18 Mar 2026 10:34:11 +0100 Subject: [PATCH 05/15] Clean README for TrackingValidation PR --- TrackingPerformance/README.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index 9317542..ba9e4d3 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -88,35 +88,24 @@ For more details on the CMS association convention and the related definitions o ## How to run -To run the `TrackingValidation` test locally, first set up the Key4hep environment: +Set up the Key4hep environment: ```bash -cd k4RecTracker source /cvmfs/sw-nightlies.hsf.org/key4hep/setup.sh ``` -Then build and install `k4RecTracker`: +Build and install the package: ```bash -k4_local_repo -mkdir build -cd build -cmake .. -DCMAKE_INSTALL_PREFIX=../install -make install -j 8 -``` -Next, build and install `k4DetectorPerformance`, and expose its local install: - -```bash -cd k4DetectorPerformance -k4_local_repo -mkdir build +mkdir build install cd build cmake .. -DCMAKE_INSTALL_PREFIX=../install make install -j 8 ``` -Finally, run the test from the `k4DetectorPerformance` build directory: + +Run the test from the build directory: ```bash cd k4DetectorPerformance/build From 7f7f4e8295a91d571c01ffe0b2069700db616a78 Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Wed, 18 Mar 2026 17:43:03 +0100 Subject: [PATCH 06/15] Refactor TrackingValidation helpers and update documentation --- TrackingPerformance/README.md | 63 +- .../include/TrackingValidationHelpers.h | 71 ++ .../include/TrackingValidationPlots.h | 83 ++ .../src/TrackingValidationHelpers.cpp | 181 +++++ .../src/TrackingValidationPlots.cpp | 440 +++++++++++ .../src/components/TrackingValidation.cpp | 738 ++---------------- 6 files changed, 894 insertions(+), 682 deletions(-) create mode 100644 TrackingPerformance/include/TrackingValidationHelpers.h create mode 100644 TrackingPerformance/include/TrackingValidationPlots.h create mode 100644 TrackingPerformance/src/TrackingValidationHelpers.cpp create mode 100644 TrackingPerformance/src/TrackingValidationPlots.cpp diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index ba9e4d3..1d2d06c 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -3,7 +3,7 @@ ## Overview `TrackingValidation` is a validation algorithm for studying the performance of track finding and track fitting in the tracking reconstruction. -It is desighned to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking. The algorithm writes validation information to a ROOT output file containing TTrees that can be used later for performance studies and plotting. +It is desighned to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking. The algorithm writes validation information to a ROOT output file containing TTrees and summary plots that can be used later for performance studies and plotting. Typical use cases include: - validation of track-finder performance, @@ -14,48 +14,76 @@ Typical use cases include: ## Inputs -`TrackingValidation` expects an EDM4hep event content in which the relevant collections have already been produced by the preceding steps of the reconstruction chain. +`TrackingValidation` expects EDM4hep event content in which the relevant collections have already been produced by the preceding steps of the reconstruction chain. ### Input collection types -`TrackingValidation` consumes the following types of event collections: +`TrackingValidation` consumes the following input collections: - **MC particle collection** + Type: `edm4hep::MCParticleCollection` Used as the truth reference for particle-level validation. -- **Planar digi-to-sim association collections** +- **Planar digi-to-sim link collections** + Type: `std::vector` Used to connect reconstructed planar hits to the originating simulated particles. -- **Wire-hit digi-to-sim association collection** +- **Drift-chamber digi-to-sim link collections** + Type: `std::vector` Used to connect reconstructed drift-chamber hits to the originating simulated particles. - **Finder track collection** + Type: `edm4hep::TrackCollection` Collection of tracks produced by the track-finding stage. - **Fitted track collection** + Type: `edm4hep::TrackCollection` Collection of tracks produced by the standard fitting stage. -- **Perfect fitted-track collection (optional)** - Collection of fitted tracks produced from perfect truth-based associations, used as an additional reference when perfect-fit validation is enabled. +- **Perfect fitted-track collections (optional)** + Type: `std::vector` + Optional reference collections produced from perfect truth-based associations, used when perfect-fit validation is enabled. - --- +--- + +## Outputs + +The algorithm writes a ROOT file specified by `OutputFile`. - ## Outputs +The file contains validation TTrees for finder-level and fitter-level studies, together with summary performance plots produced in `finalize()`. The fitter validation trees store residuals of the reconstructed track parameters with respect to the chosen reference. - The algorithm writes a ROOT file specified by the `OutputFile`. +### Output content by mode -The exact content of the output depends mainly on the validation mode selected through `Mode`: +The exact content filled in the output depends on the validation mode selected through `Mode`: - **`Mode = 0` (full-pipeline mode)** - Produces both finder-level and fitter-level validation trees. + Both finder-level and fitter-level validation are performed. + The output includes the association trees and the fitter residual trees. - **`Mode = 1` (finder-only mode)** - Produces the trees related to track-finder validation. + Only the finder-level validation is performed. + The finder and perfect-association trees are filled, while the fitter trees are booked in the file but are not filled. - **`Mode = 2` (fitter-only mode)** - Produces the trees related to fitted-track validation. + Only the fitter-level validation is performed. + The fitter trees are filled, while the finder and perfect-association trees are booked in the file but are not filled. + +### Effect of `DoPerfectFit` + +The flag `DoPerfectFit` controls the handling of the `fitter_vs_perfect` output: + +- if **`DoPerfectFit = true`** and perfect fitted-track collections are provided, the fitter-to-perfect comparison is filled; +- if **`DoPerfectFit = false`**, the `fitter_vs_perfect` tree is still created but its per-event content remains empty; +- if **`DoPerfectFit = true`** but no perfect fitted-track collection is provided, the tree is still written and a warning is issued. -In addition, `DoPerfectFit` controls whether the comparison to perfectly associated fitted tracks is filled. When enabled, the output also includes the fitter-versus-perfect validation information. +### Summary plots + +In `finalize()`, the algorithm also writes summary plots to the same ROOT file, including: + +- tracking efficiency vs momentum, +- `d0` resolution vs momentum, +- momentum resolution vs momentum, +- transverse-momentum resolution vs momentum. --- @@ -63,7 +91,7 @@ In addition, `DoPerfectFit` controls whether the comparison to perfectly associa To evaluate finder performance, each reconstructed track is matched to the truth particle with which it shares the largest number of hits. -For each particle–track pair, the algorithm stores two standard hit-based quantities: +For each particle-track pair, the algorithm stores two standard hit-based quantities: - **track hit purity**: the fraction of hits on the reconstructed track that originate from the matched truth particle; - **track hit efficiency**: the fraction of the truth-particle hits that are recovered in the reconstructed track. @@ -83,7 +111,6 @@ The summary **tracking efficiency** can then be defined in more than one way. In the current implementation, the denominator of the efficiency plot includes generator-level particles with status 1 and at least one truth-linked hit. For more details on the CMS association convention and the related definitions of tracking efficiency, fake rate, and duplicate rate, see the CMS performance note *Performance of the track selection DNN in Run 3*. :contentReference[oaicite:1]{index=1} - --- ## How to run @@ -117,5 +144,3 @@ The validation output is written to: ```text k4DetectorPerformance/TrackingPerformance/test/validation_output_test.root ``` - - diff --git a/TrackingPerformance/include/TrackingValidationHelpers.h b/TrackingPerformance/include/TrackingValidationHelpers.h new file mode 100644 index 0000000..ee26c57 --- /dev/null +++ b/TrackingPerformance/include/TrackingValidationHelpers.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020-2024 Key4hep-Project. + * + * This file is part of Key4hep. + * See https://key4hep.github.io/key4hep-doc/ for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TRACKINGVALIDATIONHELPERS_H +#define TRACKINGVALIDATIONHELPERS_H + +#include "edm4hep/MCParticle.h" +#include "edm4hep/Track.h" +#include "edm4hep/TrackState.h" +#include "podio/ObjectID.h" + +#include + +namespace TrackingValidationHelpers { + +struct HelixParams { + float D0 = 0.f; + float Z0 = 0.f; + float phi = 0.f; + float omega = 0.f; + float tanLambda = 0.f; + float p = 0.f; + float pT = 0.f; +}; + +struct PCAInfoHelper { + float pcaX = 0.f; + float pcaY = 0.f; + float pcaZ = 0.f; + float phi0 = 0.f; + bool ok = false; +}; + +uint64_t oidKey(const podio::ObjectID& id); +float safeAtan2(float y, float x); +float wrapDeltaPhi(float a, float b); + +PCAInfoHelper PCAInfo_mm(float x, float y, float z, + float px, float py, float pz, + int chargeSign, + float refX, float refY, + float Bz); + +HelixParams truthFromMC_GenfitConvention(const edm4hep::MCParticle& mc, + float Bz, + float refX, float refY, float refZ); + +bool getAtIPState(const edm4hep::Track& trk, edm4hep::TrackState& out); + +float ptFromState(const edm4hep::TrackState& st, float Bz); +float momentumFromState(const edm4hep::TrackState& st, float Bz); + +} // namespace TrackingValidationHelpers + +#endif diff --git a/TrackingPerformance/include/TrackingValidationPlots.h b/TrackingPerformance/include/TrackingValidationPlots.h new file mode 100644 index 0000000..37b4d0e --- /dev/null +++ b/TrackingPerformance/include/TrackingValidationPlots.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020-2024 Key4hep-Project. + * + * This file is part of Key4hep. + * See https://key4hep.github.io/key4hep-doc/ for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TRACKINGVALIDATIONPLOTS_H +#define TRACKINGVALIDATIONPLOTS_H + +#include "TCanvas.h" +#include "TF1.h" +#include "TGraphErrors.h" +#include "TH1F.h" +#include "TTree.h" + +#include +#include +#include + +namespace TrackingValidationPlots { + +std::vector makeLogBins(double min, double max, double step); +TF1* fitGaussianCore(TH1F* h, const std::string& name); + +TGraphErrors* makeD0ResolutionVsMomentum(TTree* tree, + const char* graphName = "g_d0_resolution_vs_p", + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15); + +TCanvas* drawD0ResolutionCanvas(TGraphErrors* g, + const char* canvasName = "c_d0_resolution_vs_p", + double xMin = 0.1, + double xMax = 100.0); + +TGraphErrors* makeMomentumResolutionVsMomentum(TTree* tree, + const char* graphName = "g_p_resolution_vs_p", + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15); + +TGraphErrors* makePtResolutionVsMomentum(TTree* tree, + const char* graphName = "g_pt_resolution_vs_p", + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15); + +TCanvas* drawResolutionCanvas(TGraphErrors* g, + const char* canvasName, + const char* title, + double xMin = 0.1, + double xMax = 100.0); + +TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, + const char* graphName, + int efficiencyDefinition, + double purityThreshold, + double pMin = 0.1, + double pMax = 100.0, + double logStep = 0.15); + +TCanvas* drawEfficiencyCanvas(TGraphErrors* g, + const char* canvasName, + const char* title, + double xMin = 0.1, + double xMax = 100.0); + +} // namespace TrackingValidationPlots + +#endif diff --git a/TrackingPerformance/src/TrackingValidationHelpers.cpp b/TrackingPerformance/src/TrackingValidationHelpers.cpp new file mode 100644 index 0000000..5d89025 --- /dev/null +++ b/TrackingPerformance/src/TrackingValidationHelpers.cpp @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2020-2024 Key4hep-Project. + * + * This file is part of Key4hep. + * See https://key4hep.github.io/key4hep-doc/ for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TrackingValidationHelpers.h" + +#include +#include + +namespace TrackingValidationHelpers { + +// Constants matching the fitter code +static constexpr float c_mm_s = 2.998e11f; +static constexpr float a_genfit = 1e-15f * c_mm_s; + +uint64_t oidKey(const podio::ObjectID& id) { + return (uint64_t(id.collectionID) << 32) | uint64_t(uint32_t(id.index)); +} + +float safeAtan2(float y, float x) { + return std::atan2(y, x); +} + +float wrapDeltaPhi(float a, float b) { + float d = a - b; + while (d > M_PI) d -= 2.f * M_PI; + while (d < -M_PI) d += 2.f * M_PI; + return d; +} + +PCAInfoHelper PCAInfo_mm(float x, float y, float z, + float px, float py, float pz, + int chargeSign, + float refX, float refY, + float Bz) { + PCAInfoHelper out; + + const float pt = std::sqrt(px * px + py * py); + if (pt == 0.f) return out; + if (chargeSign == 0) chargeSign = 1; + if (Bz == 0.f) return out; + + const float R = pt / (0.3f * std::abs(chargeSign) * Bz) * 1000.f; + + const float tx = px / pt; + const float ty = py / pt; + + const float nx = float(chargeSign) * ty; + const float ny = float(chargeSign) * (-tx); + + const float xc = x + R * nx; + const float yc = y + R * ny; + + const float vx = refX - xc; + const float vy = refY - yc; + const float vxy = std::sqrt(vx * vx + vy * vy); + if (vxy == 0.f) return out; + + const float ux = vx / vxy; + const float uy = vy / vxy; + + const float pcaX = xc + R * ux; + const float pcaY = yc + R * uy; + + const float rx = pcaX - xc; + const float ry = pcaY - yc; + + const int sign = (chargeSign > 0) ? 1 : -1; + float tanX = -sign * ry; + float tanY = sign * rx; + + const float tnorm = std::sqrt(tanX * tanX + tanY * tanY); + if (tnorm == 0.f) return out; + + tanX /= tnorm; + tanY /= tnorm; + + const float phi0 = std::atan2(tanY, tanX); + + const float pR = pt; + const float pZ = pz; + const float R0 = std::sqrt(x * x + y * y); + const float Z0 = z; + + const float denom = (pR * pR + pZ * pZ); + if (denom == 0.f) return out; + + const float tPCA = -(R0 * pR + Z0 * pZ) / denom; + const float ZPCA = Z0 + pZ * tPCA; + + out.pcaX = pcaX; + out.pcaY = pcaY; + out.pcaZ = ZPCA; + out.phi0 = phi0; + out.ok = true; + return out; +} + +HelixParams truthFromMC_GenfitConvention(const edm4hep::MCParticle& mc, + float Bz, + float refX, float refY, float refZ) { + HelixParams hp; + + const auto& mom = mc.getMomentum(); + const float px = float(mom.x); + const float py = float(mom.y); + const float pz = float(mom.z); + + const float pT = std::sqrt(px * px + py * py); + const float p = std::sqrt(px * px + py * py + pz * pz); + + hp.pT = pT; + hp.p = p; + + int qSign = 1; + if (mc.getCharge() < 0.f) qSign = -1; + + const auto& v = mc.getVertex(); + const float x = float(v.x); + const float y = float(v.y); + const float z = float(v.z); + + const auto info = PCAInfo_mm(x, y, z, px, py, pz, qSign, refX, refY, Bz); + if (!info.ok) { + const float NaN = std::numeric_limits::quiet_NaN(); + hp.D0 = NaN; + hp.Z0 = NaN; + hp.phi = NaN; + hp.omega = NaN; + hp.tanLambda = NaN; + return hp; + } + + hp.D0 = ((-(refX - info.pcaX)) * std::sin(info.phi0) + + (refY - info.pcaY) * std::cos(info.phi0)); + hp.Z0 = (info.pcaZ - refZ); + hp.phi = safeAtan2(py, px); + hp.tanLambda = (pT > 0.f) ? (pz / pT) : 0.f; + hp.omega = (pT > 0.f) ? (std::abs(a_genfit * Bz / pT) * float(qSign)) : 0.f; + + return hp; +} + +bool getAtIPState(const edm4hep::Track& trk, edm4hep::TrackState& out) { + for (const auto& st : trk.getTrackStates()) { + if (st.location == edm4hep::TrackState::AtIP) { + out = st; + return true; + } + } + return false; +} + +float ptFromState(const edm4hep::TrackState& st, float Bz) { + const float omega = std::abs(float(st.omega)); + if (omega == 0.f) return 0.f; + return a_genfit * std::abs(Bz) / omega; +} + +float momentumFromState(const edm4hep::TrackState& st, float Bz) { + const float pT = ptFromState(st, Bz); + const float tl = float(st.tanLambda); + return pT * std::sqrt(1.f + tl * tl); +} + +} // namespace TrackingValidationHelpers diff --git a/TrackingPerformance/src/TrackingValidationPlots.cpp b/TrackingPerformance/src/TrackingValidationPlots.cpp new file mode 100644 index 0000000..38f2ded --- /dev/null +++ b/TrackingPerformance/src/TrackingValidationPlots.cpp @@ -0,0 +1,440 @@ +#include "TrackingValidationPlots.h" +#include "TStyle.h" +#include +#include +#include +#include + +// makeLogBins +//fitGaussianCore +//makeD0ResolutionVsMomentum +//drawD0ResolutionCanvas +//makeMomentumResolutionVsMomentum +//makePtResolutionVsMomentum +//drawResolutionCanvas +//makeEfficiencyVsMomentum +//drawEfficiencyCanvas + +namespace TrackingValidationPlots { +std::vector makeLogBins(double min, double max, double step) { + std::vector bins; + for (double x = std::log10(min); x <= std::log10(max); x += step) { + bins.push_back(std::pow(10., x)); + } + if (bins.empty() || bins.back() < max) bins.push_back(max); + return bins; +} + +TF1* fitGaussianCore(TH1F* h, const std::string& name) { + if (!h || h->GetEntries() < 10) return nullptr; + + const double mean = h->GetMean(); + const double rms = h->GetRMS(); + if (rms <= 0.) return nullptr; + + TF1* g1 = new TF1((name + "_g1").c_str(), "gaus", mean - 2.0 * rms, mean + 2.0 * rms); + h->Fit(g1, "RQ0"); + + double m1 = g1->GetParameter(1); + double s1 = std::abs(g1->GetParameter(2)); + if (s1 <= 0.) s1 = rms; + + TF1* g2 = new TF1(name.c_str(), "gaus", m1 - 1.5 * s1, m1 + 1.5 * s1); + h->Fit(g2, "RQ0"); + + return g2; +} + +TGraphErrors* makeD0ResolutionVsMomentum(TTree* tree, + const char* graphName, + double pMin, + double pMax, + double logStep) { + if (!tree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector> hists; + hists.reserve(nBins); + + for (int i = 0; i < nBins; ++i) { + hists.emplace_back(std::make_unique( + Form("h_d0_bin_%d", i), + Form("d0 residual bin %d;resD0 [#mum];Entries", i), + 120, -20.0, 20.0)); + } + + std::vector* resD0 = nullptr; + std::vector* p_ref_vec = nullptr; + + tree->SetBranchAddress("p_ref", &p_ref_vec); + tree->SetBranchAddress("resD0", &resD0); + + const Long64_t nEntries = tree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + tree->GetEntry(ievt); + + if (!p_ref_vec || !resD0) continue; + if (p_ref_vec->size() != resD0->size()) continue; + + for (size_t i = 0; i < p_ref_vec->size(); ++i) { + const double p = (*p_ref_vec)[i]; + const double d0_um = (*resD0)[i] * 1000.0; + + if (!std::isfinite(p) || !std::isfinite(d0_um)) continue; + if (p < pMin || p >= pMax) continue; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (p >= bins[b] && p < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + hists[bin]->Fill(d0_um); + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p_{ref} [GeV];#sigma(d_{0}) [#mum]"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (hists[b]->GetEntries() < 20) continue; + + TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_d0_bin_%d", b)); + if (!fit) continue; + + const double sigma = std::abs(fit->GetParameter(2)); + const double sigmaErr = fit->GetParError(2); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, sigma); + g->SetPointError(ip, 0.0, sigmaErr); + ++ip; + } + + return g; +} + +TCanvas* drawD0ResolutionCanvas(TGraphErrors* g, + const char* canvasName, + double xMin, + double xMax) { + if (!g) return nullptr; + + gStyle->SetOptStat(0); + + TCanvas* c = new TCanvas(canvasName, "d0 resolution vs momentum", 800, 600); + c->SetLogx(); + + g->SetMarkerStyle(20); + g->SetLineWidth(2); + g->GetXaxis()->SetLimits(xMin, xMax); + g->Draw("AP"); + + return c; +} + +TGraphErrors* makeMomentumResolutionVsMomentum(TTree* tree, + const char* graphName, + double pMin, + double pMax, + double logStep) { + if (!tree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector> hists; + hists.reserve(nBins); + + for (int i = 0; i < nBins; ++i) { + hists.emplace_back(std::make_unique( + Form("h_pres_bin_%d", i), + Form("p resolution bin %d;(p_{reco}-p_{ref})/p_{ref};Entries", i), + 120, -0.2, 0.2)); + } + + std::vector* p_ref_vec = nullptr; + std::vector* p_reco_vec = nullptr; + + tree->SetBranchAddress("p_ref", &p_ref_vec); + tree->SetBranchAddress("p_reco", &p_reco_vec); + + const Long64_t nEntries = tree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + tree->GetEntry(ievt); + + if (!p_ref_vec || !p_reco_vec) continue; + if (p_ref_vec->size() != p_reco_vec->size()) continue; + + for (size_t i = 0; i < p_ref_vec->size(); ++i) { + const double pRef = (*p_ref_vec)[i]; + const double pReco = (*p_reco_vec)[i]; + + if (!std::isfinite(pRef) || !std::isfinite(pReco)) continue; + if (pRef <= 0.) continue; + if (pRef < pMin || pRef >= pMax) continue; + + const double res = (pReco - pRef) / pRef; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (pRef >= bins[b] && pRef < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + hists[bin]->Fill(res); + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p_{ref} [GeV];#sigma((p_{reco}-p_{ref})/p_{ref})"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (hists[b]->GetEntries() < 20) continue; + + TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_pres_bin_%d", b)); + if (!fit) continue; + + const double sigma = std::abs(fit->GetParameter(2)); + const double sigmaErr = fit->GetParError(2); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, sigma); + g->SetPointError(ip, 0.0, sigmaErr); + ++ip; + } + + return g; +} + +TGraphErrors* makePtResolutionVsMomentum(TTree* tree, + const char* graphName, + double pMin, + double pMax, + double logStep) { + if (!tree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector> hists; + hists.reserve(nBins); + + for (int i = 0; i < nBins; ++i) { + hists.emplace_back(std::make_unique( + Form("h_ptres_bin_%d", i), + Form("pT resolution bin %d;(pT_{reco}-pT_{ref})/pT_{ref};Entries", i), + 120, -0.2, 0.2)); + } + + std::vector* p_ref_vec = nullptr; + std::vector* pt_ref_vec = nullptr; + std::vector* pt_reco_vec = nullptr; + + tree->SetBranchAddress("p_ref", &p_ref_vec); + tree->SetBranchAddress("pT_ref", &pt_ref_vec); + tree->SetBranchAddress("pT_reco", &pt_reco_vec); + + const Long64_t nEntries = tree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + tree->GetEntry(ievt); + + if (!p_ref_vec || !pt_ref_vec || !pt_reco_vec) continue; + if (p_ref_vec->size() != pt_ref_vec->size()) continue; + if (pt_ref_vec->size() != pt_reco_vec->size()) continue; + + for (size_t i = 0; i < p_ref_vec->size(); ++i) { + const double pRef = (*p_ref_vec)[i]; + const double ptRef = (*pt_ref_vec)[i]; + const double ptReco = (*pt_reco_vec)[i]; + + if (!std::isfinite(pRef) || !std::isfinite(ptRef) || !std::isfinite(ptReco)) continue; + if (ptRef <= 0.) continue; + if (pRef < pMin || pRef >= pMax) continue; + + const double res = (ptReco - ptRef) / ptRef; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (pRef >= bins[b] && pRef < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + hists[bin]->Fill(res); + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p_{ref} [GeV];#sigma((pT_{reco}-pT_{ref})/pT_{ref})"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (hists[b]->GetEntries() < 20) continue; + + TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_ptres_bin_%d", b)); + if (!fit) continue; + + const double sigma = std::abs(fit->GetParameter(2)); + const double sigmaErr = fit->GetParError(2); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, sigma); + g->SetPointError(ip, 0.0, sigmaErr); + ++ip; + } + + return g; +} + +TCanvas* drawResolutionCanvas(TGraphErrors* g, + const char* canvasName, + const char* title, + double xMin, + double xMax) { + if (!g) return nullptr; + + gStyle->SetOptStat(0); + + TCanvas* c = new TCanvas(canvasName, title, 800, 600); + c->SetLogx(); + + g->SetMarkerStyle(20); + g->SetLineWidth(2); + g->SetTitle(title); + g->GetXaxis()->SetLimits(xMin, xMax); + g->Draw("AP"); + + return c; +} + +TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, + const char* graphName, + int efficiencyDefinition, + double purityThreshold, + double pMin, + double pMax, + double logStep) { + if (!finderTree) return nullptr; + + std::vector bins = makeLogBins(pMin, pMax, logStep); + const int nBins = bins.size() - 1; + + std::vector nDen(nBins, 0); + std::vector nNum(nBins, 0); + + std::vector* pVec = nullptr; + std::vector>* purVec = nullptr; + std::vector>* effVec = nullptr; + + finderTree->SetBranchAddress("p", &pVec); + finderTree->SetBranchAddress("matchPurity", &purVec); + finderTree->SetBranchAddress("matchEfficiency", &effVec); + + const Long64_t nEntries = finderTree->GetEntries(); + for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { + finderTree->GetEntry(ievt); + + if (!pVec || !purVec || !effVec) continue; + if (pVec->size() != purVec->size()) continue; + if (pVec->size() != effVec->size()) continue; + + for (size_t i = 0; i < pVec->size(); ++i) { + const double p = (*pVec)[i]; + if (!std::isfinite(p) || p < pMin || p >= pMax) continue; + + int bin = -1; + for (int b = 0; b < nBins; ++b) { + if (p >= bins[b] && p < bins[b + 1]) { + bin = b; + break; + } + } + if (bin < 0) continue; + + nDen[bin]++; + + bool isMatched = false; + + const auto& purities = (*purVec)[i]; + const auto& efficiencies = (*effVec)[i]; + const size_t nMatches = std::min(purities.size(), efficiencies.size()); + + for (size_t j = 0; j < nMatches; ++j) { + const float purity = purities[j]; + const float efficiency = efficiencies[j]; + + if (efficiencyDefinition == 2) { + if (purity >= 0.5f && efficiency >= 0.5f) { + isMatched = true; + break; + } + } else { + if (purity >= purityThreshold) { + isMatched = true; + break; + } + } + } + + if (isMatched) nNum[bin]++; + } + } + + TGraphErrors* g = new TGraphErrors(); + g->SetName(graphName); + g->SetTitle(";p [GeV];Tracking efficiency"); + + int ip = 0; + for (int b = 0; b < nBins; ++b) { + if (nDen[b] == 0) continue; + + const double eff = double(nNum[b]) / double(nDen[b]); + const double err = std::sqrt(eff * (1.0 - eff) / double(nDen[b])); + const double pCenter = std::sqrt(bins[b] * bins[b + 1]); + + g->SetPoint(ip, pCenter, eff); + g->SetPointError(ip, 0.0, err); + ++ip; + } + + return g; +} + +TCanvas* drawEfficiencyCanvas(TGraphErrors* g, + const char* canvasName, + const char* title, + double xMin, + double xMax) { + if (!g) return nullptr; + + gStyle->SetOptStat(0); + + TCanvas* c = new TCanvas(canvasName, title, 800, 600); + c->SetLogx(); + + g->SetMarkerStyle(20); + g->SetLineWidth(2); + g->SetTitle(title); + g->GetYaxis()->SetRangeUser(0.0, 1.05); + g->GetXaxis()->SetLimits(xMin, xMax); + g->Draw("AP"); + + return c; +} +} diff --git a/TrackingPerformance/src/components/TrackingValidation.cpp b/TrackingPerformance/src/components/TrackingValidation.cpp index 32c7efb..8e02bc2 100644 --- a/TrackingPerformance/src/components/TrackingValidation.cpp +++ b/TrackingPerformance/src/components/TrackingValidation.cpp @@ -1,21 +1,24 @@ -// TrackingValidation -// -// Validation consumer that writes the following TTrees: -// 1) finder_particle_to_tracks -// 2) finder_track_to_particles -// 3) perfect_particle_to_tracks -// 4) perfect_track_to_particles -// 5) fitter_vs_mc -// 6) fitter_vs_perfect -// -// In finalize(), the consumer also produces summary plots written to the same ROOT file: -// - tracking efficiency vs momentum -// - d0 resolution vs momentum -// - momentum resolution vs momentum -// - transverse-momentum resolution vs momentum -// -// The fitter-vs-perfect tree is filled only when perfect-fitted tracks are provided -// and DoPerfectFit is enabled +/* + * Copyright (c) 2020-2024 Key4hep-Project. + * + * This file is part of Key4hep. + * See https://key4hep.github.io/key4hep-doc/ for further info. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include "TrackingValidationHelpers.h" +#include "TrackingValidationPlots.h" // k4FWCore #include "k4FWCore/Consumer.h" @@ -52,631 +55,40 @@ #include #include -// ---------- helpers ---------- -static inline uint64_t oidKey(const podio::ObjectID& id) { - return (uint64_t(id.collectionID) << 32) | uint64_t(uint32_t(id.index)); -} - -static inline float safeAtan2(float y, float x) { return std::atan2(y, x); } - -static inline float wrapDeltaPhi(float a, float b) { - float d = a - b; - while (d > M_PI) d -= 2.f * M_PI; - while (d < -M_PI) d += 2.f * M_PI; - return d; -} - -struct HelixParams { - float D0 = 0.f; // mm - float Z0 = 0.f; // mm - float phi = 0.f; // rad - float omega = 0.f; // 1/mm - float tanLambda = 0.f; // unitless - float p = 0.f; // GeV - float pT = 0.f; // GeV -}; - -// Constants matching the fitter code -static constexpr float c_mm_s = 2.998e11f; -static constexpr float a_genfit = 1e-15f * c_mm_s; // ~2.998e-4 - -// --- GenFit-like PCAInfo in mm, ported from GenfitTrack::PCAInfo --- -// Returns PCA point (x,y,z) and Phi0 (tangent angle at PCA). -struct PCAInfoHelper { - float pcaX = 0.f; - float pcaY = 0.f; - float pcaZ = 0.f; - float phi0 = 0.f; - bool ok = false; -}; - -// position (x,y,z) in mm, momentum (px,py,pz) in GeV, refPoint in mm -static PCAInfoHelper PCAInfo_mm(float x, float y, float z, - float px, float py, float pz, - int chargeSign, - float refX, float refY, - float Bz) { - PCAInfoHelper out; - - const float pt = std::sqrt(px*px + py*py); - if (pt == 0.f) return out; - if (chargeSign == 0) chargeSign = 1; - if (Bz == 0.f) return out; - - // Radius in mm: - // GenfitTrack::PCAInfo uses R = pt/(0.3*|q|*Bz)*100 [cm] - // -> multiply by 10 to get mm: *1000 - const float R = pt / (0.3f * std::abs(chargeSign) * Bz) * 1000.f; - - const float tx = px / pt; - const float ty = py / pt; - - const float nx = float(chargeSign) * (ty); - const float ny = float(chargeSign) * (-tx); - - const float xc = x + R * nx; - const float yc = y + R * ny; - - const float vx = refX - xc; - const float vy = refY - yc; - const float vxy = std::sqrt(vx*vx + vy*vy); - if (vxy == 0.f) return out; - - const float ux = vx / vxy; - const float uy = vy / vxy; - - const float pcaX = xc + R * ux; - const float pcaY = yc + R * uy; - - // tangent direction at PCA (same as GenfitTrack::PCAInfo) - const float rx = pcaX - xc; - const float ry = pcaY - yc; - - const int sign = (chargeSign > 0) ? 1 : -1; - float tanX = -sign * ry; - float tanY = sign * rx; - - const float tnorm = std::sqrt(tanX*tanX + tanY*tanY); - if (tnorm == 0.f) return out; - - tanX /= tnorm; - tanY /= tnorm; - - const float phi0 = std::atan2(tanY, tanX); - - // ZPCA approximation from GenfitTrack::PCAInfo (ported) - // Uses a straight-line minimization in (R,z) with pR=pt, pZ=pz. - const float pR = pt; - const float pZ = pz; - const float R0 = std::sqrt(x*x + y*y); - const float Z0 = z; - - const float denom = (pR*pR + pZ*pZ); - if (denom == 0.f) return out; - - const float tPCA = -(R0*pR + Z0*pZ) / denom; - const float ZPCA = Z0 + pZ * tPCA; - - out.pcaX = pcaX; - out.pcaY = pcaY; - out.pcaZ = ZPCA; - out.phi0 = phi0; - out.ok = true; - return out; -} - -// Build MC truth helix parameters using the fitter convention -static HelixParams truthFromMC_GenfitConvention(const edm4hep::MCParticle& mc, - float Bz, - float refX, float refY, float refZ) { - HelixParams hp; - - const auto& mom = mc.getMomentum(); - const float px = float(mom.x); - const float py = float(mom.y); - const float pz = float(mom.z); - - const float pT = std::sqrt(px*px + py*py); - const float p = std::sqrt(px*px + py*py + pz*pz); - - hp.pT = pT; - hp.p = p; - - // Charge sign consistent with fitter usage (sign matters for omega) - int qSign = 1; - if (mc.getCharge() < 0.f) qSign = -1; - - const auto& v = mc.getVertex(); - const float x = float(v.x); // mm - const float y = float(v.y); // mm - const float z = float(v.z); // mm - - const auto info = PCAInfo_mm(x, y, z, px, py, pz, qSign, refX, refY, Bz); - if (!info.ok) { - const float NaN = std::numeric_limits::quiet_NaN(); - hp.D0 = NaN; - hp.Z0 = NaN; - hp.phi = NaN; - hp.omega = NaN; - hp.tanLambda = NaN; - return hp; - } - - // D0/Z0 in the same convention as the fitter (mm) - hp.D0 = ( (-(refX - info.pcaX)) * std::sin(info.phi0) + (refY - info.pcaY) * std::cos(info.phi0) ); // mm - hp.Z0 = (info.pcaZ - refZ); // mm - - // phi in fitter is taken from momentum.Phi() at the evaluated state. - // For MC we use the momentum direction. - hp.phi = safeAtan2(py, px); - - hp.tanLambda = (pT > 0.f) ? (pz / pT) : 0.f; - - // omega convention matches fitter: omega = +/- |a * Bz / pT| - hp.omega = (pT > 0.f) ? (std::abs(a_genfit * Bz / pT) * float(qSign)) : 0.f; - - return hp; -} - -static bool getAtIPState(const edm4hep::Track& trk, edm4hep::TrackState& out) { - for (const auto& st : trk.getTrackStates()) { - if (st.location == edm4hep::TrackState::AtIP) { - out = st; - return true; - } - } - return false; -} - - - - -static float ptFromState(const edm4hep::TrackState& st, float Bz) { - const float omega = std::abs(float(st.omega)); - if (omega == 0.f) return 0.f; - return a_genfit * std::abs(Bz) / omega; -} - -static float momentumFromState(const edm4hep::TrackState& st, float Bz) { - const float pT = ptFromState(st, Bz); - const float tl = float(st.tanLambda); - return pT * std::sqrt(1.f + tl * tl); -} -// Helper functions for plotting - -static std::vector makeLogBins(double min, double max, double step) { - std::vector bins; - for (double x = std::log10(min); x <= std::log10(max); x += step) { - bins.push_back(std::pow(10., x)); - } - if (bins.empty() || bins.back() < max) bins.push_back(max); - return bins; -} - -static TF1* fitGaussianCore(TH1F* h, const std::string& name) { - if (!h || h->GetEntries() < 10) return nullptr; - - const double mean = h->GetMean(); - const double rms = h->GetRMS(); - if (rms <= 0.) return nullptr; - - TF1* g1 = new TF1((name + "_g1").c_str(), "gaus", mean - 2.0 * rms, mean + 2.0 * rms); - h->Fit(g1, "RQ0"); - - double m1 = g1->GetParameter(1); - double s1 = std::abs(g1->GetParameter(2)); - if (s1 <= 0.) s1 = rms; - - TF1* g2 = new TF1(name.c_str(), "gaus", m1 - 1.5 * s1, m1 + 1.5 * s1); - h->Fit(g2, "RQ0"); - - return g2; -} - -static TGraphErrors* makeD0ResolutionVsMomentum(TTree* tree, - const char* graphName = "g_d0_resolution_vs_p", - double pMin = 0.1, - double pMax = 100.0, - double logStep = 0.15) { - if (!tree) return nullptr; - - std::vector bins = makeLogBins(pMin, pMax, logStep); - const int nBins = bins.size() - 1; - - std::vector> hists; - hists.reserve(nBins); - - for (int i = 0; i < nBins; ++i) { - hists.emplace_back(std::make_unique( - Form("h_d0_bin_%d", i), - Form("d0 residual bin %d;resD0 [#mum];Entries", i), - 120, -20.0, 20.0)); - } - - - std::vector* resD0 = nullptr; - std::vector* p_ref_vec = nullptr; - - // Tree stores vectors per event - tree->SetBranchAddress("p_ref", &p_ref_vec); - tree->SetBranchAddress("resD0", &resD0); - - const Long64_t nEntries = tree->GetEntries(); - for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { - tree->GetEntry(ievt); - - if (!p_ref_vec || !resD0) continue; - if (p_ref_vec->size() != resD0->size()) continue; - - for (size_t i = 0; i < p_ref_vec->size(); ++i) { - const double p = (*p_ref_vec)[i]; - const double d0_um = (*resD0)[i] * 1000.0; // mm -> um - - if (!std::isfinite(p) || !std::isfinite(d0_um)) continue; - if (p < pMin || p >= pMax) continue; - - int bin = -1; - for (int b = 0; b < nBins; ++b) { - if (p >= bins[b] && p < bins[b + 1]) { - bin = b; - break; - } - } - if (bin < 0) continue; - - hists[bin]->Fill(d0_um); - } - } - - TGraphErrors* g = new TGraphErrors(); - g->SetName(graphName); - g->SetTitle(";p_{ref} [GeV];#sigma(d_{0}) [#mum]"); - - int ip = 0; - for (int b = 0; b < nBins; ++b) { - if (hists[b]->GetEntries() < 20) continue; - - TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_d0_bin_%d", b)); - if (!fit) continue; - - const double sigma = std::abs(fit->GetParameter(2)); - const double sigmaErr = fit->GetParError(2); - const double pCenter = std::sqrt(bins[b] * bins[b + 1]); - - g->SetPoint(ip, pCenter, sigma); - g->SetPointError(ip, 0.0, sigmaErr); - ++ip; - } - - return g; -} - -static TCanvas* drawD0ResolutionCanvas(TGraphErrors* g, - const char* canvasName = "c_d0_resolution_vs_p", - double xMin = 0.1, - double xMax = 100.0) { - if (!g) return nullptr; - - gStyle->SetOptStat(0); - - TCanvas* c = new TCanvas(canvasName, "d0 resolution vs momentum", 800, 600); - c->SetLogx(); - - g->SetMarkerStyle(20); - g->SetLineWidth(2); - g->GetXaxis()->SetLimits(xMin, xMax); - g->Draw("AP"); - - return c; -} - -static TGraphErrors* makeMomentumResolutionVsMomentum(TTree* tree, - const char* graphName = "g_p_resolution_vs_p", - double pMin = 0.1, - double pMax = 100.0, - double logStep = 0.15) { - if (!tree) return nullptr; - - std::vector bins = makeLogBins(pMin, pMax, logStep); - const int nBins = bins.size() - 1; - - std::vector> hists; - hists.reserve(nBins); - - for (int i = 0; i < nBins; ++i) { - hists.emplace_back(std::make_unique( - Form("h_pres_bin_%d", i), - Form("p resolution bin %d;(p_{reco}-p_{ref})/p_{ref};Entries", i), - 120, -0.2, 0.2)); - } - - std::vector* p_ref_vec = nullptr; - std::vector* p_reco_vec = nullptr; - - tree->SetBranchAddress("p_ref", &p_ref_vec); - tree->SetBranchAddress("p_reco", &p_reco_vec); - - const Long64_t nEntries = tree->GetEntries(); - for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { - tree->GetEntry(ievt); - - if (!p_ref_vec || !p_reco_vec) continue; - if (p_ref_vec->size() != p_reco_vec->size()) continue; - - for (size_t i = 0; i < p_ref_vec->size(); ++i) { - const double pRef = (*p_ref_vec)[i]; - const double pReco = (*p_reco_vec)[i]; - - if (!std::isfinite(pRef) || !std::isfinite(pReco)) continue; - if (pRef <= 0.) continue; - if (pRef < pMin || pRef >= pMax) continue; - - const double res = (pReco - pRef) / pRef; - - int bin = -1; - for (int b = 0; b < nBins; ++b) { - if (pRef >= bins[b] && pRef < bins[b + 1]) { - bin = b; - break; - } - } - if (bin < 0) continue; - - hists[bin]->Fill(res); - } - } - - TGraphErrors* g = new TGraphErrors(); - g->SetName(graphName); - g->SetTitle(";p_{ref} [GeV];#sigma((p_{reco}-p_{ref})/p_{ref})"); - - int ip = 0; - for (int b = 0; b < nBins; ++b) { - if (hists[b]->GetEntries() < 20) continue; - - TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_pres_bin_%d", b)); - if (!fit) continue; - - const double sigma = std::abs(fit->GetParameter(2)); - const double sigmaErr = fit->GetParError(2); - const double pCenter = std::sqrt(bins[b] * bins[b + 1]); - - g->SetPoint(ip, pCenter, sigma); - g->SetPointError(ip, 0.0, sigmaErr); - ++ip; - } - - return g; -} - -static TGraphErrors* makePtResolutionVsMomentum(TTree* tree, - const char* graphName = "g_pt_resolution_vs_p", - double pMin = 0.1, - double pMax = 100.0, - double logStep = 0.15) { - if (!tree) return nullptr; - - std::vector bins = makeLogBins(pMin, pMax, logStep); - const int nBins = bins.size() - 1; - - std::vector> hists; - hists.reserve(nBins); - - for (int i = 0; i < nBins; ++i) { - hists.emplace_back(std::make_unique( - Form("h_ptres_bin_%d", i), - Form("pT resolution bin %d;(pT_{reco}-pT_{ref})/pT_{ref};Entries", i), - 120, -0.2, 0.2)); - } - - std::vector* p_ref_vec = nullptr; - std::vector* pt_ref_vec = nullptr; - std::vector* pt_reco_vec = nullptr; - - tree->SetBranchAddress("p_ref", &p_ref_vec); - tree->SetBranchAddress("pT_ref", &pt_ref_vec); - tree->SetBranchAddress("pT_reco", &pt_reco_vec); - - const Long64_t nEntries = tree->GetEntries(); - for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { - tree->GetEntry(ievt); - - if (!p_ref_vec || !pt_ref_vec || !pt_reco_vec) continue; - if (p_ref_vec->size() != pt_ref_vec->size()) continue; - if (pt_ref_vec->size() != pt_reco_vec->size()) continue; - - for (size_t i = 0; i < p_ref_vec->size(); ++i) { - const double pRef = (*p_ref_vec)[i]; - const double ptRef = (*pt_ref_vec)[i]; - const double ptReco = (*pt_reco_vec)[i]; - - if (!std::isfinite(pRef) || !std::isfinite(ptRef) || !std::isfinite(ptReco)) continue; - if (ptRef <= 0.) continue; - if (pRef < pMin || pRef >= pMax) continue; - const double res = (ptReco - ptRef) / ptRef; +/** @struct TrackingValidation + * + * Gaudi Consumer that validates the performance of track finding and track fitting + * by comparing reconstructed tracks with Monte Carlo truth information and, + * optionally, with perfectly associated fitted tracks. + * + * The consumer writes several ROOT TTrees containing finder-level associations, + * fitter residuals with respect to MC truth, and fitter residuals with respect + * to perfectly fitted reference tracks. In addition, summary performance plots + * are produced in finalize() and written to the same ROOT file. + * + * The supported validation modes are: + * - full pipeline validation, + * - finder-only validation, + * - fitter-only validation. + * + * input: + * - MC particle collection : edm4hep::MCParticleCollection + * - planar digi-to-sim link collections : std::vector + * - drift-chamber digi-to-sim link collections : std::vector + * - finder track collection : edm4hep::TrackCollection + * - fitted track collection : edm4hep::TrackCollection + * - optional perfect fitted-track collections : std::vector + * + * output: + * - ROOT file containing validation TTrees + * - summary performance plots written to the same ROOT file + * + * @author Arina Ponomareva + * @date 2026-03 + * + */ - int bin = -1; - for (int b = 0; b < nBins; ++b) { - if (pRef >= bins[b] && pRef < bins[b + 1]) { - bin = b; - break; - } - } - if (bin < 0) continue; - - hists[bin]->Fill(res); - } - } - - TGraphErrors* g = new TGraphErrors(); - g->SetName(graphName); - g->SetTitle(";p_{ref} [GeV];#sigma((pT_{reco}-pT_{ref})/pT_{ref})"); - - int ip = 0; - for (int b = 0; b < nBins; ++b) { - if (hists[b]->GetEntries() < 20) continue; - - TF1* fit = fitGaussianCore(hists[b].get(), Form("fit_ptres_bin_%d", b)); - if (!fit) continue; - - const double sigma = std::abs(fit->GetParameter(2)); - const double sigmaErr = fit->GetParError(2); - const double pCenter = std::sqrt(bins[b] * bins[b + 1]); - - g->SetPoint(ip, pCenter, sigma); - g->SetPointError(ip, 0.0, sigmaErr); - ++ip; - } - - return g; -} - -static TCanvas* drawResolutionCanvas(TGraphErrors* g, - const char* canvasName, - const char* title, - double xMin = 0.1, - double xMax = 100.0) { - if (!g) return nullptr; - - gStyle->SetOptStat(0); - - TCanvas* c = new TCanvas(canvasName, title, 800, 600); - c->SetLogx(); - - g->SetMarkerStyle(20); - g->SetLineWidth(2); - g->SetTitle(title); - g->GetXaxis()->SetLimits(xMin, xMax); - g->Draw("AP"); - - return c; -} -// A truth particle is counted as reconstructed according to the selected summary definition: -// definition 1: at least one associated finder track has purity above FinderPurityThreshold -// definition 2: at least one associated finder track has both purity >= 0.5 -// and efficiency >= 0.5 -static TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, - const char* graphName, - int efficiencyDefinition, - double purityThreshold, - double pMin = 0.1, - double pMax = 100.0, - double logStep = 0.15) { - if (!finderTree) return nullptr; - - std::vector bins = makeLogBins(pMin, pMax, logStep); - const int nBins = bins.size() - 1; - - std::vector nDen(nBins, 0); - std::vector nNum(nBins, 0); - - std::vector* pVec = nullptr; - std::vector>* purVec = nullptr; - std::vector>* effVec = nullptr; - - finderTree->SetBranchAddress("p", &pVec); - finderTree->SetBranchAddress("matchPurity", &purVec); - finderTree->SetBranchAddress("matchEfficiency", &effVec); - - const Long64_t nEntries = finderTree->GetEntries(); - for (Long64_t ievt = 0; ievt < nEntries; ++ievt) { - finderTree->GetEntry(ievt); - - if (!pVec || !purVec || !effVec) continue; - if (pVec->size() != purVec->size()) continue; - if (pVec->size() != effVec->size()) continue; - - for (size_t i = 0; i < pVec->size(); ++i) { - const double p = (*pVec)[i]; - if (!std::isfinite(p) || p < pMin || p >= pMax) continue; - - int bin = -1; - for (int b = 0; b < nBins; ++b) { - if (p >= bins[b] && p < bins[b + 1]) { - bin = b; - break; - } - } - if (bin < 0) continue; - - // denominator: all truth particles present in finder_particle_to_tracks tree - // (genStatus == 1 and with at least one true hit, as enforced in fillFinderAssoc) - nDen[bin]++; - - bool isMatched = false; - - const auto& purities = (*purVec)[i]; - const auto& efficiencies = (*effVec)[i]; - const size_t nMatches = std::min(purities.size(), efficiencies.size()); - - for (size_t j = 0; j < nMatches; ++j) { - const float purity = purities[j]; - const float efficiency = efficiencies[j]; - - if (efficiencyDefinition == 2) { - // CMS-style combined definition: - // require both purity and efficiency above 50%. - if (purity >= 0.5f && efficiency >= 0.5f) { - isMatched = true; - break; - } - } else { - // Default definition: - // require only purity above the configurable threshold. - if (purity >= purityThreshold) { - isMatched = true; - break; - } - } - } - - if (isMatched) nNum[bin]++; - } - } - - TGraphErrors* g = new TGraphErrors(); - g->SetName(graphName); - g->SetTitle(";p [GeV];Tracking efficiency"); - - int ip = 0; - for (int b = 0; b < nBins; ++b) { - if (nDen[b] == 0) continue; - - const double eff = double(nNum[b]) / double(nDen[b]); - const double err = std::sqrt(eff * (1.0 - eff) / double(nDen[b])); - const double pCenter = std::sqrt(bins[b] * bins[b + 1]); - - g->SetPoint(ip, pCenter, eff); - g->SetPointError(ip, 0.0, err); - ++ip; - } - - return g; -} - - -static TCanvas* drawEfficiencyCanvas(TGraphErrors* g, - const char* canvasName, - const char* title, - double xMin = 0.1, - double xMax = 100.0) { - if (!g) return nullptr; - - gStyle->SetOptStat(0); - - TCanvas* c = new TCanvas(canvasName, title, 800, 600); - c->SetLogx(); - - g->SetMarkerStyle(20); - g->SetLineWidth(2); - g->SetTitle(title); - g->GetYaxis()->SetRangeUser(0.0, 1.05); - g->GetXaxis()->SetLimits(xMin, xMax); - g->Draw("AP"); - - return c; -} // ---------- CONSUMER ---------- struct TrackingValidation final @@ -755,7 +167,7 @@ struct TrackingValidation final if (!digi.isAvailable() || !mc.isAvailable()) continue; const int pid = mc.getObjectID().index; - const uint64_t key = oidKey(digi.getObjectID()); + const uint64_t key = TrackingValidationHelpers::oidKey(digi.getObjectID()); hitsPerParticle[pid].push_back(key); hitToParticle[key] = pid; } @@ -771,7 +183,7 @@ struct TrackingValidation final if (!digi.isAvailable() || !mc.isAvailable()) continue; const int pid = mc.getObjectID().index; - const uint64_t key = oidKey(digi.getObjectID()); + const uint64_t key = TrackingValidationHelpers::oidKey(digi.getObjectID()); hitsPerParticle[pid].push_back(key); hitToParticle[key] = pid; } @@ -814,7 +226,7 @@ struct TrackingValidation final for (const auto& trk : *coll) { edm4hep::TrackState st; - if (!getAtIPState(trk, st)) continue; + if (!TrackingValidationHelpers::getAtIPState(trk, st)) continue; const int pid = majorityParticleForTrack(trk, hitToParticle); if (pid < 0 || pid >= (int)mcParts.size()) continue; @@ -851,18 +263,18 @@ struct TrackingValidation final //fitter summary plots // d0 resolution vs momentum from fitter_vs_mc - TGraphErrors* g_d0_vs_p = makeD0ResolutionVsMomentum(m_fit_vs_mc.tree, + TGraphErrors* g_d0_vs_p = TrackingValidationPlots::makeD0ResolutionVsMomentum(m_fit_vs_mc.tree, "g_d0_resolution_vs_p", 0.1, 100.0, 0.15); if (g_d0_vs_p) { - TCanvas* c_d0_vs_p = drawD0ResolutionCanvas(g_d0_vs_p, + TCanvas* c_d0_vs_p = TrackingValidationPlots::drawD0ResolutionCanvas(g_d0_vs_p, "c_d0_resolution_vs_p", 0.1, 100.0); g_d0_vs_p->Write(); if (c_d0_vs_p) c_d0_vs_p->Write(); } // p resolution vs momentum - TGraphErrors* g_p_vs_p = makeMomentumResolutionVsMomentum(m_fit_vs_mc.tree, + TGraphErrors* g_p_vs_p = TrackingValidationPlots::makeMomentumResolutionVsMomentum(m_fit_vs_mc.tree, "g_p_resolution_vs_p", 0.1, 100.0, 0.15); if (g_p_vs_p) { @@ -875,11 +287,11 @@ struct TrackingValidation final } // pT resolution vs momentum - TGraphErrors* g_pt_vs_p = makePtResolutionVsMomentum(m_fit_vs_mc.tree, + TGraphErrors* g_pt_vs_p = TrackingValidationPlots::makePtResolutionVsMomentum(m_fit_vs_mc.tree, "g_pt_resolution_vs_p", 0.1, 100.0, 0.15); if (g_pt_vs_p) { - TCanvas* c_pt_vs_p = drawResolutionCanvas(g_pt_vs_p, + TCanvas* c_pt_vs_p = TrackingValidationPlots::drawResolutionCanvas(g_pt_vs_p, "c_pt_resolution_vs_p", "pT resolution vs momentum;p_{ref} [GeV];#sigma((pT_{reco}-pT_{ref})/pT_{ref})", 0.1, 100.0); @@ -888,14 +300,14 @@ struct TrackingValidation final } // finder summary plot - TGraphErrors* g_eff_vs_p = makeEfficiencyVsMomentum( + TGraphErrors* g_eff_vs_p = TrackingValidationPlots::makeEfficiencyVsMomentum( m_finder_p2t.tree, "g_efficiency_vs_p", m_finderEfficiencyDefinition.value(), m_finderPurityThreshold.value(), 0.1, 100.0, 0.15); if (g_eff_vs_p) { - TCanvas* c_eff_vs_p = drawEfficiencyCanvas( + TCanvas* c_eff_vs_p = TrackingValidationPlots::drawEfficiencyCanvas( g_eff_vs_p, "c_efficiency_vs_p", "tracking efficiency vs momentum;p [GeV];Efficiency", @@ -1090,7 +502,7 @@ struct TrackingValidation final for (const auto& trk : finderTracks) { trackNHits[tIdx] = (int)trk.getTrackerHits().size(); for (const auto& h : trk.getTrackerHits()) { - const uint64_t hk = oidKey(h.getObjectID()); + const uint64_t hk = TrackingValidationHelpers::oidKey(h.getObjectID()); auto it = hitToParticle.find(hk); if (it == hitToParticle.end()) continue; trackParticleCounts[tIdx][it->second] += 1; @@ -1190,7 +602,7 @@ struct TrackingValidation final const std::unordered_map& hitToParticle) const { std::unordered_map counts; for (const auto& h : trk.getTrackerHits()) { - const uint64_t hk = oidKey(h.getObjectID()); + const uint64_t hk = TrackingValidationHelpers::oidKey(h.getObjectID()); auto it = hitToParticle.find(hk); if (it == hitToParticle.end()) continue; counts[it->second] += 1; @@ -1227,7 +639,7 @@ struct TrackingValidation final int tIdx = 0; for (const auto& trk : fittedTracks) { edm4hep::TrackState stReco; - if (!getAtIPState(trk, stReco)) { + if (!TrackingValidationHelpers::getAtIPState(trk, stReco)) { ++tIdx; continue; } @@ -1241,17 +653,17 @@ struct TrackingValidation final const auto& mc = mcParts[pid]; // reco params (already in fitter convention) - HelixParams reco; + TrackingValidationHelpers::HelixParams reco; reco.D0 = float(stReco.D0); reco.Z0 = float(stReco.Z0); reco.phi = float(stReco.phi); reco.omega = float(stReco.omega); reco.tanLambda = float(stReco.tanLambda); - reco.pT = ptFromState(stReco, m_Bz.value()); - reco.p = momentumFromState(stReco, m_Bz.value()); + reco.pT = TrackingValidationHelpers::ptFromState(stReco, m_Bz.value()); + reco.p = TrackingValidationHelpers::momentumFromState(stReco, m_Bz.value()); // ref from MC using the SAME convention as fitter (PCA + phi0 + ZPCA + omega=a*B/pT) - const HelixParams refMC = truthFromMC_GenfitConvention(mc, m_Bz.value(), m_refX.value(), m_refY.value(), m_refZ.value()); + const TrackingValidationHelpers::HelixParams refMC = TrackingValidationHelpers::truthFromMC_GenfitConvention(mc, m_Bz.value(), m_refX.value(), m_refY.value(), m_refZ.value()); // --- vs MC --- @@ -1259,7 +671,7 @@ struct TrackingValidation final m_fit_vs_mc.track_location.push_back(int(stReco.location)); m_fit_vs_mc.resD0.push_back(reco.D0 - refMC.D0); m_fit_vs_mc.resZ0.push_back(reco.Z0 - refMC.Z0); - m_fit_vs_mc.resPhi.push_back(wrapDeltaPhi(reco.phi, refMC.phi)); + m_fit_vs_mc.resPhi.push_back(TrackingValidationHelpers::wrapDeltaPhi(reco.phi, refMC.phi)); m_fit_vs_mc.resOmega.push_back(reco.omega - refMC.omega); m_fit_vs_mc.resTanL.push_back(reco.tanLambda - refMC.tanLambda); m_fit_vs_mc.p_reco.push_back(reco.p); @@ -1273,20 +685,20 @@ struct TrackingValidation final if (it != perfectAtIPByPid.end()) { const auto& stPerf = it->second.st; - HelixParams refP; + TrackingValidationHelpers::HelixParams refP; refP.D0 = float(stPerf.D0); refP.Z0 = float(stPerf.Z0); refP.phi = float(stPerf.phi); refP.omega = float(stPerf.omega); refP.tanLambda = float(stPerf.tanLambda); - refP.pT = ptFromState(stPerf, m_Bz.value()); - refP.p = momentumFromState(stPerf, m_Bz.value()); + refP.pT = TrackingValidationHelpers::ptFromState(stPerf, m_Bz.value()); + refP.p = TrackingValidationHelpers::momentumFromState(stPerf, m_Bz.value()); m_fit_vs_perfect.track_index.push_back(tIdx); m_fit_vs_perfect.track_location.push_back(int(stReco.location)); m_fit_vs_perfect.resD0.push_back(reco.D0 - refP.D0); m_fit_vs_perfect.resZ0.push_back(reco.Z0 - refP.Z0); - m_fit_vs_perfect.resPhi.push_back(wrapDeltaPhi(reco.phi, refP.phi)); + m_fit_vs_perfect.resPhi.push_back(TrackingValidationHelpers::wrapDeltaPhi(reco.phi, refP.phi)); m_fit_vs_perfect.resOmega.push_back(reco.omega - refP.omega); m_fit_vs_perfect.resTanL.push_back(reco.tanLambda - refP.tanLambda); m_fit_vs_perfect.p_reco.push_back(reco.p); From 6cdea70d6f2738c1ad76dc432d184433102c1a1e Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Wed, 18 Mar 2026 18:13:37 +0100 Subject: [PATCH 07/15] Improve TrackingValidation documentation --- TrackingPerformance/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index 1d2d06c..ba9d49e 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -110,7 +110,7 @@ The summary **tracking efficiency** can then be defined in more than one way. In the current implementation, the denominator of the efficiency plot includes generator-level particles with status 1 and at least one truth-linked hit. -For more details on the CMS association convention and the related definitions of tracking efficiency, fake rate, and duplicate rate, see the CMS performance note *Performance of the track selection DNN in Run 3*. :contentReference[oaicite:1]{index=1} +For more details on the CMS association convention and the related definitions of tracking efficiency, fake rate, and duplicate rate, see the CMS performance note [*Performance of the track selection DNN in Run 3*](https://cds.cern.ch/record/2854696/files/DP2023_009.pdf). --- ## How to run From b54ec739032ee0c30aeb0a361b0e33d2f41986cb Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Wed, 18 Mar 2026 18:15:57 +0100 Subject: [PATCH 08/15] Improve TrackingValidation documentation --- TrackingPerformance/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index ba9d49e..df80ffa 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -109,8 +109,11 @@ The summary **tracking efficiency** can then be defined in more than one way. This corresponds to the stricter two-ratio variant, where both the purity of the reconstructed track and the fraction of recovered truth hits must exceed 50%. In the current implementation, the denominator of the efficiency plot includes generator-level particles with status 1 and at least one truth-linked hit. - For more details on the CMS association convention and the related definitions of tracking efficiency, fake rate, and duplicate rate, see the CMS performance note [*Performance of the track selection DNN in Run 3*](https://cds.cern.ch/record/2854696/files/DP2023_009.pdf). + + + + --- ## How to run From d957a97411947d14c9245ac244629ba02d5e8564 Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Wed, 18 Mar 2026 18:25:35 +0100 Subject: [PATCH 09/15] Improve TrackingValidation documentation --- .../src/components/TrackingValidation.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/TrackingPerformance/src/components/TrackingValidation.cpp b/TrackingPerformance/src/components/TrackingValidation.cpp index 8e02bc2..1f1783a 100644 --- a/TrackingPerformance/src/components/TrackingValidation.cpp +++ b/TrackingPerformance/src/components/TrackingValidation.cpp @@ -33,17 +33,12 @@ #include "edm4hep/TrackState.h" #include "edm4hep/TrackerHitSimTrackerHitLinkCollection.h" -// podio -#include "podio/ObjectID.h" - // ROOT #include "TFile.h" #include "TTree.h" -#include "TH1F.h" #include "TGraphErrors.h" #include "TCanvas.h" -#include "TF1.h" -#include "TStyle.h" + // STL #include @@ -278,7 +273,7 @@ struct TrackingValidation final "g_p_resolution_vs_p", 0.1, 100.0, 0.15); if (g_p_vs_p) { - TCanvas* c_p_vs_p = drawResolutionCanvas(g_p_vs_p, + TCanvas* c_p_vs_p = TrackingValidationPlots::drawResolutionCanvas(g_p_vs_p, "c_p_resolution_vs_p", "momentum resolution vs momentum;p_{ref} [GeV];#sigma((p_{reco}-p_{ref})/p_{ref})", 0.1, 100.0); From a5e107de855e4872f6e8115a759ef4f295b0c91f Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Thu, 19 Mar 2026 15:39:02 +0100 Subject: [PATCH 10/15] Finalize TrackingValidation steering, test, and documentation --- TrackingPerformance/test/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 TrackingPerformance/test/.gitignore diff --git a/TrackingPerformance/test/.gitignore b/TrackingPerformance/test/.gitignore new file mode 100644 index 0000000..a8efbec --- /dev/null +++ b/TrackingPerformance/test/.gitignore @@ -0,0 +1 @@ +TrackingPerformance/test/*.root From 4db05df77c0750e5ae03a8e0be846dec29b21b9a Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Thu, 19 Mar 2026 15:49:34 +0100 Subject: [PATCH 11/15] Finalize TrackingValidation steering, test, and documentation --- TrackingPerformance/CMakeLists.txt | 1 + TrackingPerformance/README.md | 50 +- TrackingPerformance/test/.gitignore | 2 +- .../test/runTrackingValidation.py | 475 ++++++++++-------- .../test/testTrackingValidation.sh | 47 +- .../test/validation_output.root | Bin 58309 -> 0 bytes .../test/validation_output_test.root | Bin 56655 -> 0 bytes 7 files changed, 360 insertions(+), 215 deletions(-) delete mode 100644 TrackingPerformance/test/validation_output.root delete mode 100644 TrackingPerformance/test/validation_output_test.root diff --git a/TrackingPerformance/CMakeLists.txt b/TrackingPerformance/CMakeLists.txt index 0307df5..ae4d405 100644 --- a/TrackingPerformance/CMakeLists.txt +++ b/TrackingPerformance/CMakeLists.txt @@ -28,6 +28,7 @@ list(APPEND ExternalData_URL_TEMPLATES file(GLOB sources ${PROJECT_SOURCE_DIR}/src/*.cc + ${PROJECT_SOURCE_DIR}/src/*.cpp ${PROJECT_SOURCE_DIR}/src/components/*.cpp ) diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index df80ffa..38d22c0 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -2,8 +2,8 @@ ## Overview -`TrackingValidation` is a validation algorithm for studying the performance of track finding and track fitting in the tracking reconstruction. -It is desighned to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking. The algorithm writes validation information to a ROOT output file containing TTrees and summary plots that can be used later for performance studies and plotting. +`TrackingValidation` is a validation algorithm for studying the performance (efficiency, purity, residuals, resolutions) of track finding and track fitting in the tracking reconstruction. +It is desighned to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking i.e. tracks fitted using the correct simhits from the particle truth information. The algorithm writes validation information to a ROOT output file containing TTrees and summary plots that can be used later for performance studies and plotting. Typical use cases include: - validation of track-finder performance, @@ -118,6 +118,17 @@ For more details on the CMS association convention and the related definitions o ## How to run +`TrackingValidation` is tested through a small end-to-end workflow driven by `ctest`. The test starts from a simulated EDM4hep file, runs the reconstruction and validation steering, and writes the final validation ROOT output. + +In the current setup: + +- the simulation step is performed with `ddsim` in the shell test, +- the reconstruction and validation steps are controlled by `runTrackingValidation.py`, +- the full test is launched through `ctest`. + + +### Build and run the test + Set up the Key4hep environment: ```bash @@ -147,3 +158,38 @@ The validation output is written to: ```text k4DetectorPerformance/TrackingPerformance/test/validation_output_test.root ``` +### Test configuration and steering options + +The current test runs the full reconstruction and validation chain after simulation with the following settings: + +- `runDigi = 1` + digitization step (`0` = skip digitization, `1` = run digitization); + +- `runFinder = 1` + track-finder step (`0` = skip track finder, `1` = run track finder); + +- `runFitter = 1` + reconstructed-track fitter (`0` = skip reco fitter, `1` = run reco fitter); + +- `runPerfectTracking = 1` + perfect-tracking and perfect-fitter chain (`0` = skip perfect tracking/perfect fitter, `1` = run them); + +- `runValidation = 1` + validation step (`0` = skip validation, `1` = run validation); + +- `useDCH = 1` + drift-chamber collections (`0` = disable DCH, `1` = use DCH); + +- `mode = 0` + validation mode (`0` = full validation, `1` = finder-only validation, `2` = fitter-only validation); + +- `doPerfectFit = 1` + fitter-versus-perfect comparison (`0` = disable fitter-vs-perfect filling, `1` = enable it); + +- `finderEfficiencyDefinition = 1` + tracking-efficiency definition (`1` = purity-based definition, `2` = purity >= 0.5 and efficiency >= 0.5); + +- `finderPurityThreshold = 0.75` + purity threshold used when `FinderEfficiencyDefinition = 1`. + +These command-line flags are defined in `runTrackingValidation.py`, which allows the same steering file to be used either for the full chain or for reduced workflows in which some reconstruction steps are skipped and only the validation is run. diff --git a/TrackingPerformance/test/.gitignore b/TrackingPerformance/test/.gitignore index a8efbec..3c5bea1 100644 --- a/TrackingPerformance/test/.gitignore +++ b/TrackingPerformance/test/.gitignore @@ -1 +1 @@ -TrackingPerformance/test/*.root +test/*.root diff --git a/TrackingPerformance/test/runTrackingValidation.py b/TrackingPerformance/test/runTrackingValidation.py index bfcc23d..3ac555a 100644 --- a/TrackingPerformance/test/runTrackingValidation.py +++ b/TrackingPerformance/test/runTrackingValidation.py @@ -9,14 +9,91 @@ # -------------------- # Arguments # -------------------- -parser.add_argument("--inputFile", required=True, help="Input simulated EDM4hep ROOT file") -parser.add_argument("--modelPath", required=True, help="Path to the GGTF ONNX model") +parser.add_argument("--inputFile", required=True, + help="Input EDM4hep ROOT file") +parser.add_argument("--modelPath", default="", + help="Path to the GGTF ONNX model (required only if --runFinder 1)") parser.add_argument("--outputFile", default="out_reco.root", help="Output EDM4hep ROOT file with reconstructed collections") parser.add_argument("--validationFile", default="validation.root", - help="Output ROOT file written by TrackingValidationConsumer") + help="Output ROOT file written by TrackingValidation") + +parser.add_argument("--geom", + default=os.path.join(os.environ["K4GEO"], "FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml"), + help="Detector geometry XML file") + +# Pipeline control +parser.add_argument("--runDigi", type=int, default=1, + help="0=skip digitization, 1=run digitization") +parser.add_argument("--runFinder", type=int, default=1, + help="0=skip track finder, 1=run track finder") +parser.add_argument("--runFitter", type=int, default=1, + help="0=skip reco fitter, 1=run reco fitter") +parser.add_argument("--runPerfectTracking", type=int, default=1, + help="0=skip perfect tracking/perfect fitter, 1=run them") +parser.add_argument("--runValidation", type=int, default=1, + help="0=skip validation, 1=run TrackingValidation") +parser.add_argument("--useDCH", type=int, default=1, + help="0=disable DCH collections, 1=use DCH collections") + +# Validation control +parser.add_argument("--mode", type=int, default=0, + help="Validation mode: 0=Full, 1=FinderOnly, 2=FitterOnly") +parser.add_argument("--doPerfectFit", type=int, default=1, + help="0=do not fill fitter_vs_perfect, 1=fill fitter_vs_perfect") +parser.add_argument("--finderEfficiencyDefinition", type=int, default=1, + help="1=purity-based definition, 2=purity+efficiency >= 0.5 definition") +parser.add_argument("--finderPurityThreshold", type=float, default=0.75, + help="Purity threshold used when FinderEfficiencyDefinition = 1") + args = parser.parse_args() +if args.runFinder == 1 and not args.modelPath: + parser.error("--modelPath is required when --runFinder 1") + +if args.runValidation == 0 and args.doPerfectFit == 1: + print("WARNING: --doPerfectFit is ignored when --runValidation 0") + +if args.runDigi == 0 and args.runFinder == 1: + print("WARNING: --runFinder 1 with --runDigi 0 assumes digi collections are already present in the input file") + +if args.runFinder == 0 and args.runFitter == 1: + print("WARNING: --runFitter 1 with --runFinder 0 assumes finder-track collections are already present in the input file") + +if args.runPerfectTracking == 0 and args.doPerfectFit == 1: + print("WARNING: --doPerfectFit 1 with --runPerfectTracking 0 assumes PerfectFittedTracks is already present in the input file") + +if all(flag == 0 for flag in [args.runDigi, args.runFinder, args.runFitter, args.runPerfectTracking, args.runValidation]): + parser.error("Nothing to do: all run flags are set to 0") + +# -------------------- +# Fixed collection names +# -------------------- +MC_COLLECTION = "MCParticles" + +PLANAR_LINK_COLLECTIONS = [ + "SiWrBSimDigiLinks", + "SiWrDSimDigiLinks", + "VTXBSimDigiLinks", + "VTXDSimDigiLinks", +] + +DCH_LINK_COLLECTIONS = ["DCH_DigiSimAssociationCollection"] if args.useDCH == 1 else [] + +PLANAR_DIGI_COLLECTIONS = [ + "VTXBDigis", + "VTXDDigis", + "SiWrBDigis", + "SiWrDDigis", +] + +DCH_DIGI_COLLECTIONS = ["DCH_DigiCollection"] if args.useDCH == 1 else [] + +FINDER_TRACK_COLLECTION = "GGTFTracks" +FITTED_TRACK_COLLECTION = "FittedTracks" +PERFECT_TRACK_COLLECTION = "PerfectTracks" +PERFECT_FITTED_TRACK_COLLECTION = "PerfectFittedTracks" + # -------------------- # IO # -------------------- @@ -28,232 +105,232 @@ # Geometry # -------------------- geoservice = GeoSvc("GeoSvc") -geoservice.detectors = [ - os.path.join(os.environ["K4GEO"], "FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml") -] +geoservice.detectors = [args.geom] geoservice.EnableGeant4Geo = False geoservice.OutputLevel = INFO # -------------------- -# Digitizers +# Algorithm sequence # -------------------- -from Configurables import DDPlanarDigi, DCHdigi_v02 - -innerVertexResolution_x = 0.003 -innerVertexResolution_y = 0.003 -innerVertexResolution_t = 1000 - -outerVertexResolution_x = 0.050 / math.sqrt(12) -outerVertexResolution_y = 0.150 / math.sqrt(12) -outerVertexResolution_t = 1000 - -vtxb_digitizer = DDPlanarDigi("VTXBdigitizer") -vtxb_digitizer.SubDetectorName = "Vertex" -vtxb_digitizer.IsStrip = False -vtxb_digitizer.ResolutionU = [ - innerVertexResolution_x, - innerVertexResolution_x, - innerVertexResolution_x, - outerVertexResolution_x, - outerVertexResolution_x, -] -vtxb_digitizer.ResolutionV = [ - innerVertexResolution_y, - innerVertexResolution_y, - innerVertexResolution_y, - outerVertexResolution_y, - outerVertexResolution_y, -] -vtxb_digitizer.ResolutionT = [ - innerVertexResolution_t, - innerVertexResolution_t, - innerVertexResolution_t, - outerVertexResolution_t, - outerVertexResolution_t, -] -vtxb_digitizer.SimTrackHitCollectionName = ["VertexBarrelCollection"] -vtxb_digitizer.SimTrkHitRelCollection = ["VTXBSimDigiLinks"] -vtxb_digitizer.TrackerHitCollectionName = ["VTXBDigis"] -vtxb_digitizer.ForceHitsOntoSurface = True - -vtxd_digitizer = DDPlanarDigi("VTXDdigitizer") -vtxd_digitizer.SubDetectorName = "Vertex" -vtxd_digitizer.IsStrip = False -vtxd_digitizer.ResolutionU = [ - outerVertexResolution_x, - outerVertexResolution_x, - outerVertexResolution_x, -] -vtxd_digitizer.ResolutionV = [ - outerVertexResolution_y, - outerVertexResolution_y, - outerVertexResolution_y, -] -vtxd_digitizer.ResolutionT = [ - outerVertexResolution_t, - outerVertexResolution_t, - outerVertexResolution_t, -] -vtxd_digitizer.SimTrackHitCollectionName = ["VertexEndcapCollection"] -vtxd_digitizer.SimTrkHitRelCollection = ["VTXDSimDigiLinks"] -vtxd_digitizer.TrackerHitCollectionName = ["VTXDDigis"] -vtxd_digitizer.ForceHitsOntoSurface = True - -siWrapperResolution_x = 0.050 / math.sqrt(12) -siWrapperResolution_y = 1.0 / math.sqrt(12) -siWrapperResolution_t = 0.040 - -siwrb_digitizer = DDPlanarDigi("SiWrBdigitizer") -siwrb_digitizer.SubDetectorName = "SiWrB" -siwrb_digitizer.IsStrip = False -siwrb_digitizer.ResolutionU = [siWrapperResolution_x, siWrapperResolution_x] -siwrb_digitizer.ResolutionV = [siWrapperResolution_y, siWrapperResolution_y] -siwrb_digitizer.ResolutionT = [siWrapperResolution_t, siWrapperResolution_t] -siwrb_digitizer.SimTrackHitCollectionName = ["SiWrBCollection"] -siwrb_digitizer.SimTrkHitRelCollection = ["SiWrBSimDigiLinks"] -siwrb_digitizer.TrackerHitCollectionName = ["SiWrBDigis"] -siwrb_digitizer.ForceHitsOntoSurface = True - -siwrd_digitizer = DDPlanarDigi("SiWrDdigitizer") -siwrd_digitizer.SubDetectorName = "SiWrD" -siwrd_digitizer.IsStrip = False -siwrd_digitizer.ResolutionU = [siWrapperResolution_x, siWrapperResolution_x] -siwrd_digitizer.ResolutionV = [siWrapperResolution_y, siWrapperResolution_y] -siwrd_digitizer.ResolutionT = [siWrapperResolution_t, siWrapperResolution_t] -siwrd_digitizer.SimTrackHitCollectionName = ["SiWrDCollection"] -siwrd_digitizer.SimTrkHitRelCollection = ["SiWrDSimDigiLinks"] -siwrd_digitizer.TrackerHitCollectionName = ["SiWrDDigis"] -siwrd_digitizer.ForceHitsOntoSurface = True - -dch_digitizer = DCHdigi_v02( - "DCHdigi2", - InputSimHitCollection=["DCHCollection"], - OutputDigihitCollection=["DCH_DigiCollection"], - OutputLinkCollection=["DCH_DigiSimAssociationCollection"], - DCH_name="DCH_v2", - zResolution_mm=30.0, - xyResolution_mm=0.1, - Deadtime_ns=400.0, - GasType=0, - ReadoutWindowStartTime_ns=1.0, - ReadoutWindowDuration_ns=450.0, - DriftVelocity_um_per_ns=-1.0, - SignalVelocity_mm_per_ns=200.0, - OutputLevel=INFO, -) +TopAlg = [] # -------------------- -# Track finder +# Digitizers # -------------------- -from Configurables import GGTFTrackFinder +if args.runDigi == 1: + from Configurables import DDPlanarDigi, DCHdigi_v02 + innerVertexResolution_x = 0.003 + innerVertexResolution_y = 0.003 + innerVertexResolution_t = 1000 -ggtf = GGTFTrackFinder( - "GGTFTrackFinder", - InputPlanarHitCollections=["VTXBDigis", "VTXDDigis", "SiWrBDigis", "SiWrDDigis"], - InputWireHitCollections=["DCH_DigiCollection"], - OutputTracksGGTF=["GGTFTracks"], - ModelPath=args.modelPath, - Tbeta=0.6, - Td=0.3, - OutputLevel=INFO, -) + outerVertexResolution_x = 0.050 / math.sqrt(12) + outerVertexResolution_y = 0.150 / math.sqrt(12) + outerVertexResolution_t = 1000 + + siWrapperResolution_x = 0.050 / math.sqrt(12) + siWrapperResolution_y = 1.0 / math.sqrt(12) + siWrapperResolution_t = 0.040 + + vtxb_digitizer = DDPlanarDigi("VTXBdigitizer") + vtxb_digitizer.SubDetectorName = "Vertex" + vtxb_digitizer.IsStrip = False + vtxb_digitizer.ResolutionU = [ + innerVertexResolution_x, + innerVertexResolution_x, + innerVertexResolution_x, + outerVertexResolution_x, + outerVertexResolution_x, + ] + vtxb_digitizer.ResolutionV = [ + innerVertexResolution_y, + innerVertexResolution_y, + innerVertexResolution_y, + outerVertexResolution_y, + outerVertexResolution_y, + ] + vtxb_digitizer.ResolutionT = [ + innerVertexResolution_t, + innerVertexResolution_t, + innerVertexResolution_t, + outerVertexResolution_t, + outerVertexResolution_t, + ] + vtxb_digitizer.SimTrackHitCollectionName = ["VertexBarrelCollection"] + vtxb_digitizer.SimTrkHitRelCollection = ["VTXBSimDigiLinks"] + vtxb_digitizer.TrackerHitCollectionName = ["VTXBDigis"] + vtxb_digitizer.ForceHitsOntoSurface = True + + vtxd_digitizer = DDPlanarDigi("VTXDdigitizer") + vtxd_digitizer.SubDetectorName = "Vertex" + vtxd_digitizer.IsStrip = False + vtxd_digitizer.ResolutionU = [ + outerVertexResolution_x, + outerVertexResolution_x, + outerVertexResolution_x, + ] + vtxd_digitizer.ResolutionV = [ + outerVertexResolution_y, + outerVertexResolution_y, + outerVertexResolution_y, + ] + vtxd_digitizer.ResolutionT = [ + outerVertexResolution_t, + outerVertexResolution_t, + outerVertexResolution_t, + ] + vtxd_digitizer.SimTrackHitCollectionName = ["VertexEndcapCollection"] + vtxd_digitizer.SimTrkHitRelCollection = ["VTXDSimDigiLinks"] + vtxd_digitizer.TrackerHitCollectionName = ["VTXDDigis"] + vtxd_digitizer.ForceHitsOntoSurface = True + + siwrb_digitizer = DDPlanarDigi("SiWrBdigitizer") + siwrb_digitizer.SubDetectorName = "SiWrB" + siwrb_digitizer.IsStrip = False + siwrb_digitizer.ResolutionU = [siWrapperResolution_x, siWrapperResolution_x] + siwrb_digitizer.ResolutionV = [siWrapperResolution_y, siWrapperResolution_y] + siwrb_digitizer.ResolutionT = [siWrapperResolution_t, siWrapperResolution_t] + siwrb_digitizer.SimTrackHitCollectionName = ["SiWrBCollection"] + siwrb_digitizer.SimTrkHitRelCollection = ["SiWrBSimDigiLinks"] + siwrb_digitizer.TrackerHitCollectionName = ["SiWrBDigis"] + siwrb_digitizer.ForceHitsOntoSurface = True + + siwrd_digitizer = DDPlanarDigi("SiWrDdigitizer") + siwrd_digitizer.SubDetectorName = "SiWrD" + siwrd_digitizer.IsStrip = False + siwrd_digitizer.ResolutionU = [siWrapperResolution_x, siWrapperResolution_x] + siwrd_digitizer.ResolutionV = [siWrapperResolution_y, siWrapperResolution_y] + siwrd_digitizer.ResolutionT = [siWrapperResolution_t, siWrapperResolution_t] + siwrd_digitizer.SimTrackHitCollectionName = ["SiWrDCollection"] + siwrd_digitizer.SimTrkHitRelCollection = ["SiWrDSimDigiLinks"] + siwrd_digitizer.TrackerHitCollectionName = ["SiWrDDigis"] + siwrd_digitizer.ForceHitsOntoSurface = True + + TopAlg += [vtxb_digitizer, vtxd_digitizer, siwrb_digitizer, siwrd_digitizer] + + if args.useDCH == 1: + dch_digitizer = DCHdigi_v02( + "DCHdigi2", + InputSimHitCollection=["DCHCollection"], + OutputDigihitCollection=["DCH_DigiCollection"], + OutputLinkCollection=["DCH_DigiSimAssociationCollection"], + DCH_name="DCH_v2", + zResolution_mm=30.0, + xyResolution_mm=0.1, + Deadtime_ns=400.0, + GasType=0, + ReadoutWindowStartTime_ns=1.0, + ReadoutWindowDuration_ns=450.0, + DriftVelocity_um_per_ns=-1.0, + SignalVelocity_mm_per_ns=200.0, + OutputLevel=INFO, + ) + TopAlg += [dch_digitizer] # -------------------- -# Reco fitter +# Track finder # -------------------- -from Configurables import GenfitTrackFitter - -reco_fitter = GenfitTrackFitter("RecoTrackFitter") -reco_fitter.InputTracks = ["GGTFTracks"] -reco_fitter.OutputFittedTracks = ["FittedTracks"] -reco_fitter.RunSingleEvaluation = True -reco_fitter.UseBrems = False -reco_fitter.BetaInit = 100.0 -reco_fitter.BetaFinal = 0.1 -reco_fitter.BetaSteps = 10 -reco_fitter.InitializationType = 1 -reco_fitter.SkipTrackOrdering = False -reco_fitter.SkipUnmatchedTracks = False -reco_fitter.OutputLevel = INFO +if args.runFinder == 1: + from Configurables import GGTFTrackFinder + + ggtf = GGTFTrackFinder( + "GGTFTrackFinder", + InputPlanarHitCollections=PLANAR_DIGI_COLLECTIONS, + InputWireHitCollections=DCH_DIGI_COLLECTIONS, + OutputTracksGGTF=[FINDER_TRACK_COLLECTION], + ModelPath=args.modelPath, + Tbeta=0.6, + Td=0.3, + OutputLevel=INFO, + ) + TopAlg += [ggtf] # -------------------- -# Perfect track finder +# Reco fitter # -------------------- -from Configurables import PerfectTrackFinder +if args.runFitter == 1: + from Configurables import GenfitTrackFitter -perfect = PerfectTrackFinder("PerfectTrackFinder") -perfect.InputMCParticles = ["MCParticles"] -perfect.InputPlanarHitCollections = [ - "SiWrBSimDigiLinks", - "SiWrDSimDigiLinks", - "VTXBSimDigiLinks", - "VTXDSimDigiLinks", -] -perfect.InputWireHitCollections = ["DCH_DigiSimAssociationCollection"] -perfect.OutputPerfectTracks = ["PerfectTracks"] -perfect.OutputLevel = INFO + reco_fitter = GenfitTrackFitter("RecoTrackFitter") + reco_fitter.InputTracks = [FINDER_TRACK_COLLECTION] + reco_fitter.OutputFittedTracks = [FITTED_TRACK_COLLECTION] + reco_fitter.RunSingleEvaluation = True + reco_fitter.UseBrems = False + reco_fitter.BetaInit = 100.0 + reco_fitter.BetaFinal = 0.1 + reco_fitter.BetaSteps = 10 + reco_fitter.InitializationType = 1 + reco_fitter.SkipTrackOrdering = False + reco_fitter.SkipUnmatchedTracks = False + reco_fitter.OutputLevel = INFO + + TopAlg += [reco_fitter] # -------------------- -# Perfect fitter +# Perfect tracking + perfect fitter # -------------------- -perfect_fitter = GenfitTrackFitter("PerfectTrackFitter") -perfect_fitter.InputTracks = ["PerfectTracks"] -perfect_fitter.OutputFittedTracks = ["PerfectFittedTracks"] -perfect_fitter.RunSingleEvaluation = True -perfect_fitter.UseBrems = False -perfect_fitter.BetaInit = 100.0 -perfect_fitter.BetaFinal = 0.1 -perfect_fitter.BetaSteps = 10 -perfect_fitter.InitializationType = 1 -perfect_fitter.SkipTrackOrdering = False -perfect_fitter.SkipUnmatchedTracks = False -perfect_fitter.OutputLevel = INFO +if args.runPerfectTracking == 1: + from Configurables import PerfectTrackFinder, GenfitTrackFitter + + perfect = PerfectTrackFinder("PerfectTrackFinder") + perfect.InputMCParticles = [MC_COLLECTION] + perfect.InputPlanarHitCollections = PLANAR_LINK_COLLECTIONS + perfect.InputWireHitCollections = DCH_LINK_COLLECTIONS + perfect.OutputPerfectTracks = [PERFECT_TRACK_COLLECTION] + perfect.OutputLevel = INFO + + perfect_fitter = GenfitTrackFitter("PerfectTrackFitter") + perfect_fitter.InputTracks = [PERFECT_TRACK_COLLECTION] + perfect_fitter.OutputFittedTracks = [PERFECT_FITTED_TRACK_COLLECTION] + perfect_fitter.RunSingleEvaluation = True + perfect_fitter.UseBrems = False + perfect_fitter.BetaInit = 100.0 + perfect_fitter.BetaFinal = 0.1 + perfect_fitter.BetaSteps = 10 + perfect_fitter.InitializationType = 1 + perfect_fitter.SkipTrackOrdering = False + perfect_fitter.SkipUnmatchedTracks = False + perfect_fitter.OutputLevel = INFO + + TopAlg += [perfect, perfect_fitter] # -------------------- # Validation consumer # -------------------- -from Configurables import TrackingValidation - -val = TrackingValidation("TrackingValidation") -val.OutputFile = args.validationFile -val.Mode = 0 -val.Bz = 2.0 -val.RefPointX = 0.0 -val.RefPointY = 0.0 -val.RefPointZ = 0.0 -val.DoPerfectFit = True -val.MCParticles = ["MCParticles"] -val.PlanarLinks = [ - "SiWrBSimDigiLinks", - "SiWrDSimDigiLinks", - "VTXBSimDigiLinks", - "VTXDSimDigiLinks", -] -val.DCHLinks = ["DCH_DigiSimAssociationCollection"] -val.FinderTracks = ["GGTFTracks"] -val.FittedTracks = ["FittedTracks"] -val.PerfectFittedTracks = ["PerfectFittedTracks"] -val.OutputLevel = INFO +if args.runValidation == 1: + from Configurables import TrackingValidation + + val = TrackingValidation("TrackingValidation") + val.OutputFile = args.validationFile + val.Mode = args.mode + + # Fixed fitter/truth convention settings + val.Bz = 2.0 + val.RefPointX = 0.0 + val.RefPointY = 0.0 + val.RefPointZ = 0.0 + + val.DoPerfectFit = bool(args.doPerfectFit) + val.FinderEfficiencyDefinition = args.finderEfficiencyDefinition + val.FinderPurityThreshold = args.finderPurityThreshold + + val.MCParticles = [MC_COLLECTION] + val.PlanarLinks = PLANAR_LINK_COLLECTIONS + val.DCHLinks = DCH_LINK_COLLECTIONS + val.FinderTracks = [FINDER_TRACK_COLLECTION] + val.FittedTracks = [FITTED_TRACK_COLLECTION] + val.PerfectFittedTracks = [PERFECT_FITTED_TRACK_COLLECTION] if args.doPerfectFit == 1 else [] + val.OutputLevel = INFO + + TopAlg += [val] # -------------------- # AppMgr # -------------------- ApplicationMgr( - TopAlg=[ - dch_digitizer, - vtxb_digitizer, - vtxd_digitizer, - siwrb_digitizer, - siwrd_digitizer, - ggtf, - reco_fitter, - perfect, - perfect_fitter, - val, - ], + TopAlg=TopAlg, EvtSel="NONE", EvtMax=-1, ExtSvc=[geoservice, EventDataSvc("EventDataSvc"), UniqueIDGenSvc("uidSvc"), RndmGenSvc()], OutputLevel=INFO, -) +) \ No newline at end of file diff --git a/TrackingPerformance/test/testTrackingValidation.sh b/TrackingPerformance/test/testTrackingValidation.sh index 4ac0cb4..9f22310 100644 --- a/TrackingPerformance/test/testTrackingValidation.sh +++ b/TrackingPerformance/test/testTrackingValidation.sh @@ -67,17 +67,27 @@ N_EVENTS=5 SEED=42 echo "=== Test configuration ===" -echo "Script dir: ${SCRIPT_DIR}" -echo "Temporary dir: ${TMPDIR}" -echo "Geometry XML: ${XML_FILE}" -echo "DDSim steering: ${STEERING_FILE}" -echo "Run script: ${RUN_FILE}" -echo "ONNX model: ${MODEL_FILE}" -echo "Simulation file: ${SIM_FILE}" -echo "Reco file: ${RECO_FILE}" -echo "Validation file: ${VAL_FILE}" -echo "Events: ${N_EVENTS}" -echo "Seed: ${SEED}" +echo "Script dir: ${SCRIPT_DIR}" +echo "Temporary dir: ${TMPDIR}" +echo "Geometry XML: ${XML_FILE}" +echo "DDSim steering: ${STEERING_FILE}" +echo "Run script: ${RUN_FILE}" +echo "ONNX model: ${MODEL_FILE}" +echo "Simulation file: ${SIM_FILE}" +echo "Reco file: ${RECO_FILE}" +echo "Validation file: ${VAL_FILE}" +echo "Events: ${N_EVENTS}" +echo "Seed: ${SEED}" +echo "runDigi: 1" +echo "runFinder: 1" +echo "runFitter: 1" +echo "runPerfectTracking: 1" +echo "runValidation: 1" +echo "useDCH: 1" +echo "mode: 0" +echo "doPerfectFit: 1" +echo "finderEfficiencyDefinition: 1" +echo "finderPurityThreshold: 0.75" echo "=== Step 1: DDSim ===" ddsim \ @@ -100,12 +110,23 @@ if [ ! -f "${SIM_FILE}" ]; then exit 1 fi -echo "=== Step 2: reconstruction + perfect tracking + validation ===" +echo "=== Step 2: digitization + tracking + validation ===" k4run "${RUN_FILE}" \ --inputFile "${SIM_FILE}" \ --modelPath "${MODEL_FILE}" \ --outputFile "${RECO_FILE}" \ - --validationFile "${VAL_FILE}" + --validationFile "${VAL_FILE}" \ + --geom "${XML_FILE}" \ + --runDigi 1 \ + --runFinder 1 \ + --runFitter 1 \ + --runPerfectTracking 1 \ + --runValidation 1 \ + --useDCH 1 \ + --mode 0 \ + --doPerfectFit 1 \ + --finderEfficiencyDefinition 1 \ + --finderPurityThreshold 0.75 if [ ! -f "${RECO_FILE}" ]; then echo "ERROR: reconstruction output was not created: ${RECO_FILE}" diff --git a/TrackingPerformance/test/validation_output.root b/TrackingPerformance/test/validation_output.root deleted file mode 100644 index b558651aa5e6a31cec6119fc328b5b58c3b5ee49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58309 zcmeFZcU)6h*EXDxMj-S^S3?g?LKOrxp-DFaDj;G)2N5GpuuT$*pr{d0sTvdztbiye z>I4J@1w=pv!5I|928wl1XU1tGtT>d&vXCredos_?d0TId#}Crwbowy#I0VP zfIz&lMj#NO2*gM$$ZUAN9RvyT01f}b5Qq~r1i~N|f&9rK=ZvPf+_N3O;)5H`o_`9T ze$Ro_MhGv&-*9)L7Lg{pGW;3>f$?>YiU|ugVTQ+>goMS#&I*Y%NsJGRGhv!AS*v4L zuVTiTth8_pO9%@|SRF?Xiwj>Jw~84X5@zBX#|&8+6&vySdkJCj2_{L*n5a-@Le%Qm z6{`~yScwU<;)E*xZ~O)VBh(M%Bxsfi>OBAv>4`wxKK_r5myQ~T^U0#`FNbHM11Y?Y z@Ce2E+B4%B1MnL)f8WAaH`{Lkv7(L2w}AAb60YA`oZ)3_?r_J`o=O0U`YLo#Qnu|=US_+qgctpox=O+{Jx zwK;`S9L8WYNW~c=loRnuaM8n`f%S*iqC@P=$0wh_B3b_e=sh7&*ROyk_~jYfRNG;( z*jNDN0+ImY4H6EL1t4Dl$Qx!9%6K?~Vcjkeuu3=_4ie$sspJNLj(i3+;3Gf;3PzUb z_ykpq<@+zNj{POp=V6Q#z%&H12t)-W48#~DS6NwEkwT#y31Kk$F96Oh9L^mq^Z*Jm zt~#?A@U(q~mH81N0xkFP*(a>{Sl_tBFqf!=_V_a%W~k_~OHfI?|Wifl)MEG_gp&7Wxz@rM>z&o8x*S;b5UiF67Nj|zzjiw#Nn z7p<=*RK@!%RnW|L(DjEh>y%sRt?Eaf0Loz)i7wE_m0(>D;CViDO7&hqO@ht{gU&bz zYW)nIVFRL!s85t@2b}evX%O;<1~{27HINi(Ku?T|O87VEm4xWszm5Js=ELg{2dMO2%K7>Gsbkg@O0_8kZ05%Vp~q)nvj%2U2@!}ac0uv`CL*e6dYUuu z^-S3q=s8#M9d9sc;mK>lst}p@1GJco_i+56rzBbNzF>eFGqP4}{WeYshZbFW9+8Sl zoz$+57X+vawDmIK;=kg}G+L_2rY1HhqPmGn@^oe8GHNWYiot>*=4tHSX~@LozTGRn z0@|$1jMB&5Wl9lT*m;G6j5YV&F6ov^4z_TPS!Fg9 z$`rTcX>MWu7(VXMbkqS9Diw*!h5zUw83JWKb%R)$a>oVFnRyI~Kwu=JI)Nam#A32XXh;}4n zq1Cu%E||2ILTL*=Q?hpEf;leOc8yw8-X(#!=M%!pXXUBtFZ+?WtuRkg`t|q;>`5Hz zNgsPuMqA0@wwRejT9%vC%F@C~Z^ly1D*lpa#$6|~JTu+3{?T!+n|6AJ2jMFjh0NPc z0;~j@S4B~6!7_1J-Mv`sSTZ~+vb;|tybQ!92}!+$R<}B(n(jp#Ye3QQ{FCfjuG$TJT+p6iNQ!O4RI3~Hzf7#j79lDUex8;d-gA0$CPUk z!qmP&zTDlXDdlA>BdEfhe74gsuy|jGgl<)dn|kStWt&ZQwT!sw#CH2~8_yw=ELt8C zE4C-n_W-2bb7JqF+)Z?qi(9ODx8^L-)&g>8=p8JK|5%?-+Es z7`b^OcNw^NMC&Qfu3WL>jyt8GLEZyl(Ui>II?a$(!<|G|uF1ZxK-4V>q0b%xg%kV+ zMGBvGy`)r_5J+>~oP{b*NZ{p4{}3DO8key$ARs(0JUS>&XU@J`S#4^M9y?6QFETym zNW^$L(uN&J#k?HFpMu_e+nFT`nQ?Q_iF1RtENRc`R62gL%w-F0D~XJWdd|JP|6*T} zMDi8QWszJnj1KwY%wcpnx0v71gg4;Vkx!J#7kEyjjY_|InzRA0ueQ`)f~%`-!m_VL z>S?FVhS9?-+1^n>L9W_-M%BdWFuJ8{$ISq5?RGCJVfTYYVl#FD>Bs>Vw+Q3~_=L=0 z#_%B~W7xk%AXr<#q0zYj4pFITs^-WGp5Rw$(@rjE3nG=h6u#gb{L5}|? zII)z*2MnKq_!B4oYY_hhPAt{+CD{LP;_m?Z7o2$LXPBM-PdIUv!L$|t^i57Io$@8r z&p7dSVEqeD{Aa9Wm`VvCW*~|nLY^ZD)_da*e$xt^GF8Oi56MmwHeRcU6?}+`K5=3h zoiDL|&56GQvWOD{Ir};``iv9X`EK+~9yM4r+8qN^nuyu}t!Wd|8yHI>dILd6wa`f$ z0$E^!R$D=V?#Yh&wXa zp(R z>4X?Q6;U7o@rP^@gK;;C!Pvf*!3aj`?UEcW(?nclAqj+ROg5&^OLITz3OLs#Bkr1a z8geq1qflEbARL%XBdTGEFxszf*u$%9QE2clZNA7D))(zno?jGS9lEpm@)5~sXf?T-20#d_*A45~2}I23y^ zANk=D^@FyASB*>yTIsrz$hkqGOZ1J?t3w$5?bGX&Ry&1Ad+NR&4tN&PCIrt zyJdeLkzU1ZDNEw6NqzfD`8+x|%diPeRmjFQ_|=Fh8Hz7tt6uV(OJnxfU=z(jVb=u= ztnl*MvmZ{`=WaJ7KcuW-=Wdsd>$!8))5x`p<>`LKqsyby{g~r@A6vN;hm9M>S7;hs z+?BbXpG6ig&I}Y}iQkzHk!QmF126PFyf*WXU2?x>V1vJSM@ZyNBjwQLB+nqae_#GO zALMHPP)SdE1s~lyg(~Lz@)U@(t!bD%#jxGJnkTACctfSX-`;Mb5jNuHLhDW&#dx_` z4X33|3iE2Uy*mepy8H?HQX!LPrx7QGq00b1vq^rgo8Ukkf#6X~AQ&AY5MpK!2#PA1JXZ3v%X= zJpV+6|0YkI@$nMhl_&l{B|h+>Pbicvzd|A6#NUD96DfWqOpyFjqF5aQUulqOAUI$< z7BF~)d8#7daMBRv6hoNNfPzG^$}&6~FnyD0Dvx}L7RmZINdJOq{y9_pFPNss2_;`Z z^mzszNa2S=Wyi-Sp9ll6;(r77zOY%pnb!U+ReTMaumI#9$Wf47@NbHcF*yP(vmcUY ztU^eh>4Hh45EF=*6#(hdXSDcJ(o)s#OR)c;#s3EL6Cq|ujs7h!{${Q?2kbzCdJJ-a z4?yyP%~x}Q7^{J)$bqT2gKjc#iZa5{K{^M(eppEOkSYFT&1%AR65;Ww%|8(1f1v_W zUZ{fOSLwb#rHUb!N{2j4H-x~HbU>*BD5U|V51?ENAb+CSwF(f3w8eN~LiNOFE&eGz zsdh|=6OK<%KV!xJ0x#m}_l!O>73IEjph@w70{*nU!8{99y2RArl2WVUeNLXLc2Fw`1{1_Vewa{b0YE(lL zWVH#S0SL{dc*yM|>OR9A`UmdmDj%PH!u>h%{Wq8=zX!7f;FSY`G3PK7aH|9E#emxu zaBqibH4yAO84Ybc<3~J<6ZEi%^$v^Dz)OUle0nF~{m6R%2HoF$1QyIeBZmJ-1YZy( zf?GZ&g1s?SPEl3uI-mINe=@VNl zF|^ksF1U7*jbjq~xw3LO|48Cg4^4XAI6JkK!_iP4SD??KPNgE(MpK{Ta<-PvLa1Px z9m_Kk>7L2@)rLEL>4ugr`I;(jD!GkDmZ!CUY|}MA-m)FMPDUaQrAC&eRy0S-s-;a= z&QDfV&d-HMKY{B#G3zREfq5VTg$e;qdonjuxh)P1(Q+!s#rl|b@J)eWKeRCeJ4?$# z5Qc7U=ZDe-eu(}2Q&fH4eOi8HUp4j+-o)ud-=wmTq&~OzaR~6*q)DDz?VQzlv1kUG z|B!R4P)&T$?-uq3#TdB1wQ$@ zH|%d`LiVsr6C-{R&o*UWv3oKf^aSwF+a*5`>i z+YQsFXg-;0*6Dt1AMs{MxSIH8Ek9z#o{<(uiK9JR^Q25qDU@fls5+?>29z~b-9uWR z?-@5fhu78vENuMl*z6FP$mFFY}R<%eKy0PkK3$|ximo)d)s6O&OOFZhm*~0w) zg!=B>{5H}2Z>aA8K}dbWT{l-|m<1hd(r)*hbH2N5M8a*3hx%~Yfz;xJsdxqV9P_~5 z9kw1uko+G^ss>%j z%GI_-n)>ZufKT=X({tU);W)t}S0@0{g4c&(97Vv8{5cVerKw=CcYHU@LLmaV&!2-Q z`zHIwiV7iw1q%Q7>{rYDD-?fbzkiM5zrcRA?tF>%Gxqx( zNdGt4ulBYtv3|~ezXR<5Ci~Ty^Cj4Cu;1^%{9k6jy1*So$0wfi1^fMH75=x_ukMjA zp?-_~{xjbHMfR(=;!DVX$$q~B_x~RIB`bZ2`tvfoUDny6j6zUxXg9BrK9 z3cI-#cpX;Z6v9hEqAiMRZc~nR3(qBni&R+U+Cm8V?)1GiO?)H*kD1G1lp% zvygURv#hyJ%yl+i9=_KM&Q%7;h1f2Nb~&;rue8cZwK!k~+eb2bM60I3dVaw)4{zID z*0havsw)h#F70v~;#Z;9u1AeWX7=iPOg~&Ys}=I6iIt^l**B*p>p1Z(33+0cw#0E| z<)k>=MC-C^`3|{R^Q;RtAoUbw!U1^z|TB@5~K304lbv-jBVU`AUmzMdJadM8oUBWI{=MACE4F@D*F7Npml zIc=_+=g+Ruwz*(wY;100ZfSB_zGUHqW0}~iYtkFCDwUUNZsF?gC$Ti+kb0VF!rV%C z$&Rjr6q^b5nHf;7Fj_W4RIbp36&;K~z6WLR8pniqX& z7(I<1kmv86)lrdT@6CR@R`PsW-Zn!G3{4?N$y#H8tsvj527yJJ*Me}LeD*2o@@){P zi1n}&r%&GU%KLNncMD{H3nBYE53;{&Ap0u?+21RW{XGWR-^-BwJ$N@>8L_hJg#-Aq zWKmAyL-v>c(HA3(|APM#!lM!rph!3=e#NSguNZIgA72;F^#4>B{2@cu@G-CO@td>0 zTK|^(;6O#Z_nW`|iRu0%%zk)4a_EmD%KsDu^Cfpk#mp>R4(%r_LS%yIgE)ae(-1a- z#6#bSiV-0VV0{rXlgLqT1iAyS2gr_#n&<=6PgTLQx<6w6L}#QwK#f@)BB~bud$f0i zXg$9|i)Nmg>=(Q?<==u7qp0o^*5G+iv?nUwgCaf(#5T-%khckdaQQX>*Ml4xWc7uG zdy+rky#Unp&)|OQQIj9Ogo}v_i+7~_J<5I|$_-zktSf%-N~-C^u^?a0@Wka8#txH5 zOw2T@h9Vxmff>!j%sK4*40byUk){8IHvKwx{l-e<3MIC!~Y{6K~9?fLw8Owgs zd}fzoD|;5T_pE1|-7I%VLRnRBB#5uUElh$;v8W`S3I>)85(p9v;sx>$`pF#(&I+_u zhiVaPs7w|h5Zz0sMFHk(pK0UsQ3r(S)R)?bgEk9Sg+(y`UY|jsK8auHGZd&LU=J5q z+$25AR+b?>^H4nRcz4y(`Rfs-SNl7~T)U<=h|8~zrz0{iVNc1laBs!*%>a-e1``Y* z9u!au3#sX_{Ll^XMb)r2&@oFuzYCyW1Z@j4ae(+M!0!J{p{0K)WG1ZX6&|0uhX4xs zGGjfMtAa!SL8+gGN+o}-)Ro4qQdyT21lwCi40ciPJ{$jO^YFx*x$n%l^?QDqgOlHJ z-$g}!$6)Iv7!%;iP&w8+lBUDR5^i#^7G)h#4yN#^1u`F$5q8>jP{vl6L5I!?gwA3h z5L(w%2!Q7O7J6E;R@DLqzm?_aXK0U zvRaDHyi=54y(m%A?D$ebfs#${M^$Q*X|RkU(gez=9qd46fk1(>gC1xi+!av{niPR1 z(y$g#3~K=l*ivvtbvj_J{7jQizRqH$Pz^XfX@U`I68`s!hzTeU0w}eLY%_f&+cMMK!?r`6i+F?601Dae*BCEc#PrTsfYn1|}vxA?Sd<|c=UzG1bFm-Wkf%yYOjV35uzIW zD3NnT)T_UT4O2K_?50K)F^oCFl?$uQLVzC%*#Be;e|iCJ+k!?=)kJ0Lyi-isH4Fwl zgb~UJWh12b)^+6M^zEOjy*o7&O56^Oh(js(5M55fGc^=!UU(t&rL(Z%y!?%*7I7?3 zJiq-nX%AD@5Vu>r=XVqrJPrwe$Mi>bH^JnX(KLi9(;~BTQdpV(bTqi><$QCs%vHa7 zNfQY0H%=gUOTNM=t1Fk~3%Zai*C2B3NIe|RGrhJn*@OFA&W;kWIPAqegtKt6H>p0= zmz~Z0tJ-vjt&;8GmFh})*hOtIvOkJB3MVNj6wlNQ#81O4f-6KlkRPrDWhXJVKkHeV zb}Eee+G3qS9TZj<{^J{Ff<2Hnv3d0RT6TwXD}1H)ft9I-A<|zmo>FU#RV$PAm%V+n zEzYr~6#M9yj+>?F7Io$PUj?POtPMAd`-Whl1OV&CV`sttB`h~OXLB&jiECXK1;-_A z2oG|L#+a$CEbL}yn70{Q_c)rlNbj`d+hLvOkyl!tw^+oP;WJEvrIAeBJBd79*-gQd zVh0IotjpA?EM)yYUZ6~(RGS~vitiHDigT7{mvIBC92ZV&r(ez=3WG(ZH%?XsDLP4D z+vZ1XmHVk2gxNIb)P53_*@wcK!;uMLCB2uu(Xe(|dU4tm$(5`(-=;>}e7?*!_lY!Z z@i;{o9_u#Xt7MnjZ;50ukh}Dd!*wHQFI(}e(4PF$Gq$-iB?4p;Z6T|!|3FMsPhyH` zr~2^3$r#glh=U}@MZ)PQATMA%7@<*B5@gMv(YVFr9F&2Z#CE^rHY(c3%}K8sG3YB7 z7R^Ziw(di1kMWA0xaSHq9m2}I{+P2mghdavB#P(lCq(X=ckJ4%jRGi>dE4OZq1|n; zQ8`Uk6mogZl<0o$AQrgE4!LJ$=2#50^s+_KYpBk;c@V(eNqhT99TqP#52K&4cax-t zRWFGLj*!%^vY#YN^tRblE)yp3wK4*P3H+Qd(g zjQCNlh~g!`vwJ0iStRh62gxU`wN0<;naVedTC|#wcg?LVd%1l;a z4eFpu+5+Xtl(go$c+{gRTOtiBT#8ti&DAn{S!2D^g$)RjSDo4t#wjZQyJEEzbn-Ht^yQ; zwsHjC4KEDsKCIAx2=zYIZP+z_iT8_8?>iuiRyV#U-h<)|9&DndL;Tkb3tklHW+ooI zjxJ2fC~#c&!3oELuc06iGQZ*3fUM@THvZJXb60qxVd}4HWOfJiu)Z zxNiz^*YA@U26R`yWgXO~U*i54O??Mqk#&5N-~aoNN*Or)KbHJ}rVIq4(^?g%_@6?R zPws?v{!4W}gjC<5jmVv>`pTVr6(DvTPUdzTj-`#g+M@A7GXgdPR&yposEQ`SPWb*| z3!OtiG=!-NtE##`>zPmSyM5M|s{9dkeTOol`2Cw*^Y25ie-XE5E>M*P)E%Gc^T`i6 z{E?FUG?jl0y}m;ok)?gJZ~m-KhvA&<9fp?nV<$H{k71rdAv*&#c!_PacQIsAL}|+J za!@dJDI@moRD}g?jJPN|a}z~pjzTWsD9rtSjLyD>{`EUBNj0JoZXe^cq!3}829;AE z;m z$L9ZXuy$eF=fT>{iXU%qI5grNHy)8_3;T^r-Pv0098Qzc_=bIbEfx!TJ`>8yO*L(! z5L_aE0Y8Omk&%^uv_!G;nM)p&oEe?*^l*93nBT0PZM3}!+bH8oynWNfyT!B=8}N(V z!D8{n(gcr9!`-Ql=T~|~$3_R?Q&Ff~8`K^YO2N=tr&8cx3JkrmjBILSJI~xi=cHJ{ zHcL;~IiWI2RFBeB(cavqN}qu`K*>9aTv1p_-J#Etg#x{n)QuQ71@ZZ&$sC^yDHJ)3 z^lJ-yaG}ew&1bNoMzDjz&5A1AK{3l2c2M{RU)c6NJ1A;bG8m^|2Zb3*PeYL<-(&A( zzk#Qnm%FdnFQ;JN=8NS?_VOqjpe9UMLGjHr)Nrz(RA+)WL9=_eD_BpE?0{Xd$JSZQ zO4o-nqKRbv!gBs?QIOL@)JA#IH2K7qDMT?>)+q%drj?7LYqlcO%97UINnaPEb^fLN z4#RT|v|h<40pjh#-MZUEyLETXiFqN~Pis=Q!Szb}Li8?*XkYSfievc>GV5)onKJhw zYV2ic@w{=Oi>p}j^_=2)ME^%*paQto?3LI z-qpMa0?d6yg?q&#=(Z*7#<;Wq-KvJ3EE$WE26k_jiggLe4n{6w?d)P2GeD*LM&?Fy z{dVN>fSlef7>lFI?B2^WSpkb2ybSFi(9_6DnU{6iRX>ZY9(_<2*BRJsJ-kVXX#X+1*DXavL zqp%Oz1}yEDbl8KO3VV=YA9CFm4sF8>RO+bQhFBD0L?gi-f~fmKKZq_8`9X%z54su) z{UE`2K^&76=@b{YIxe0Vu_BZLiNW~QF^QrZ5yAQ!OUstEVm)LvH`oMRGtSq68Jon6Cx(2Z zhgCu;r2lj}DJh<`YV|6(Ra4?B=vN7U#0mmZc`|oc{RV`#ATcvlEr&UN09j=-I;DP+0UDDT+mgICLAgP=KYkQG5z-MznQvQowzu+ zI5X5DAMdkyE7s6nv-`S+E2rSWbLk`o6RRwBhsopR+be|!BXE{lsVJ@clGr5_+_8)S z4kxTrXdaszlynqB8x*JAQ<`ROO{w;XK)Aj#uLif>z#Wl1vN_#Bd+xke(s@V{%|BQN z2|1JVN373N2hNbrpCMZwU7~SouBFk9fYa9Ib7wEo(m8*Nzr;r)z^2|}wxa$UN>lDt zNtp9zu{0HsI@7AB^V76sl5!V@ufb>PWj=|4oa9GyBy}z3#TSsWeDQYzb+JdfFa_;R zf)*mi8*GrQ?=aLXp6Ff(jCz`IYD^V#(P>E}5ROdCnpV(ymK+4uLT54{eE1c&q$=#( z)SNxWKV(;Q-gNkl)Ju)QI%AC?dLX_lF{d$hJ7{+}j@QAZwEB}0s|)B(M|P10omgm7 zuLbM`EgP}DOa93U#XE=kjZ!m(spK?Q`|UUzS@UP4qASqNybL4CzUdVEgwJ-sMlw@%(Nny{S4;E}n*)(!Ft7!v9^C$td5rvf(uup~_Akz9I~MGES>F?*62{u8_v3R5$w3GT4+hs-DM%v-}Jjj#Mtq zDtqp1)47(%wdF!p`=VkfxzxHB7KDR$UWCPkX<7&b=RSQrUvG>_)*|J*X8L9Ov$$it)?48KA6SwTb959jKZni=7z-HHLaB z99W@CWuL35F(sr*nOa$4dmUB7yN(qTu%HU}^*rJA`(^i;M+m>+Uu}4>kSqKKZ(~4! zDtyBg6=BXwaVFu|xfwc*afNcipBEogQd7G?>8}nZuDD$4@T1S8_IecK=}Hh7*Tx980T^YOYYxEYNkUiEo1z|sDA1Bu6v;;qdDg` zWmlB&4q~rW6tLx&?XN0_Qt?!@b!k<3`M}orfjOq<`kBWK>MwZ3K3u-v(>!dcm)Sy# zQy!2a;MLWOZ$QQEIhTEL_N59XwegcNk`orAx(cfbRT|@hx0IJ-Dz$TJZz@z_?`f~! zYaTIuZAS^iWLb5K*L}GLg}VZFePC*tYXY{3qKI-F7Cd6yEu1HRg>@^zLrO=z> zlS`{;zolk$KKAL%15r=Ud$suAd;bGHy2fy>1;?*9TdIGTu}6+nlfGlu-o>#v>rVX@ z_u~{p7>L%)+lLk(&a&#{!H=0;)0FFbz^lJsYm6=rT<`QtBBS25t2j*0Cf@2=g-VU# zGG+e??uajv`1W<2Etk6HQOEk4z97c4MB9s7!W@gj32qv$?ZYe|^g-D{uFf#)#qaNB z@@9|6C_R#0rCRCcQs;8A4$PHai%LY%^c~~Ij3Q*P&b+G=etI!Fw=8^;!s2}--Q69a zRl*u?9(_(y7{{!rpVQc~Wx#yJ{o0Mpnf7&2;p3OX6&0O_HyjFH`z+DWdc5UyOv!Ic zjv^PYNK)h+&$Q|+UOr&dYIjYnFK^Mw#skl!#+NNQJ)P7$K2J?;a7M5v>6V3LQrL@7 z_bWu+!t3Lk=DtMS?-krU+I8*y`@zG<6>8qatya8N;cssc&QjSg4L6Lt61f3Y@A1a* z(5m;B6-w+q2>rVh6cjvjsS|-x^oKK`VGN4YTsG~*XV3+Qzyor;Xfzjt(%x%^&j z$Dq~ClkScB_WGLBv&wK=kd5as?&H$0E?KXZHp0}G*n}*VUsY&BPWLMd;6C!%xPD?u z3hNxn`&DU3@NtsCUQO*YZ<>g&P_7|wSgP7fpFf?=; zYs+g2(rwxfX7FcW$@d56Cak!BHuj2Q&D2JZ15U z2M}h{tKjZb>G`U)hRalGjZqh0+b{Srb#N7waCMbjyf|zpsp^_hXM`^bZ#fpTSKP9* z<;mkxuZE~qQBm4O*hl(;q44OnFZIZ=O;SGh#|lokSaqf?A3%>;nKwqgowPa;f1%)n zc2W4{V=*y=S5>E{drJb zX*C~Z6ZQqLFDer}1-Hcvc^6`oRw$v%m)wsDQOMaCjW_EgtHcyqwprL-n4?r3RaJJW zssPz$p^!NkBdADBNZ@+9GZ@;}SA}PgdOdOlu_rJ^r?e*L_ScbnLYg|nLkmxv_FGKa zac@gTt*DODD7(ZhFm1Dt%p9c72&umRJ5j=_bKlDi(cxld#&UP9CSUT-Jx}zij@ldA z!qso9)6N`Jr`-u(eQv!>zePmP3vPi*TOCo0)nLQ%lnmm@SaojC_Cwsmr1pX3buoHt zm)#Ro_!rxtBwS$IR@TFf`J5N_;Jzy(M#Ze8eCOQoFi*15oon*pZ3FTTXPQ0T2l|JP zxUVzHPZgeOR(Wh~YSCEy*pU<1i)59ioFVmMrJkM~>MHlO4Ngf zZBkZVPzZBJ@&(UjC!8=zQ8<+I;t(5MPjEZt5)xT-> zROp@mxP$6!M18?UwyoMk>5Vzyrd)h}=T3WSiiX@A@{`qvH%J9O;d)?Xu5ht0s5aZ$ zk}}hgjkP=F`sxt2(k>+ks|n=88V0isj0A6K+&#-dHE;#TvoUT=y$aq2!;2QTG;WJE zEgQlVCFQ()8IFwBY8q0ES*zmh&Gj5yu+GRYzuIt_U`R3eYQsI1%20F>R+X%0sVEq! zqVLP#uA9N?UBZ)-!P6`tB!id2U~lzOLr zy)(x*7dbmO-W0Q_kceMawz1qxeaONrAaysEpOxR|J6Y#NY}0;scJ0%qHHFl5=NM7q zX2z|A7}W+`=uGLtgAokY^WxL&zO#%dw3#vf?kVN!!STiGn_|taPLT#ILU>VGvh}T8 zPoK377%R`Q4m^wpWI;G`jhZzUW~#cTikk8;UQD5Wn?-y)tD!Y@DEt>`dCBcEZJ8Kz zUQ`87-*HlW$L&pvSu3urIZ-3l9&3&(2p3zkOtXL0Z>D@Jd}_$D0AzcGKik8c+hB~3 zDLiM8yesuUO%1BOg7e(IP@`p4JE=G37YC2{{%oe|dHmj(?m0!iNns}}4iG10H*L9n zZv8J49;aeX-t?>KIqh(F&+Y4nZ;q!QXR94UhtI1m+4B6#3u*}3Im?-E%Jp4;z5L3! z#gZdvW_Moo?Kdx!#&trQ?y-SmoyT{@T)Io>p4{jiEngYzbz=6dx`Cf!f8Mmd_vm2p z0K|pHN#k|P4O^t2+i{$ulh_3c$=>~aLp>y^KE7-HvijI_=LBh4uPuN0ea?c$pt_qq zJ+&qgRjGwzsFF*G$g3llHLi}th2!#_n#a8AgblFnFps%~+whmRE1M?ZzM zF=_ql@srHR$b`U~=LCCq%AHk})b=9JM*FcxEC+fhowCH^Z(i@!TR8Ch)U)2C3mMy_ zx7$l4{ql5v^)E*kb5)%Mhmi#-127LN>h8|soBk@ceuskIgsx{{%<54 z7r!oE=S1(FJSZ=@wr$JSmb?~XJl&3X>+!MWxaOoEH~oe^9wS>ar*qbCx<)~G>n@eE z>P^i?17hljonFVRnjqo0A=j(c6`tFz|%{rpw+6DOA*N7m5t8DsS<6SpIwORdZ;&8<+c2HnQJO6IIuJKn#hQ9;jZj!75; zam}iG@4Tj8!y*x`@fV|ZWA*v=$NG+qU0uQQmrs0rDqi62xjrZ;X#MiptXslDo%Haf zF;~{tq6kvM>8=6$%V`D>P^z>o<@=rVOrGp^HJH=1w`6)?2d4%9_C`0N_)(5Gp_f2a zIW_cC($jhLLnmUPq4O+4s|V{>|G4q!%$Po4&}6g55OQ6_IS6T?ogS>z^9Ug~?xe0DwCQ2oTJ!IH*fwYTPO`n4tK z5L2_Se(J=LQsj}Y8Vh$b%)PA!^tOTJjY+p-dn-D(%K`bx-sYLK^-LXqOfHUegy3R1 z+Z;RF{m?}3z8?`fN_4GD&&E&TlCHul6>Kxw-Hwjn-n7#3*VRqkd}igi@!BV~H~mlC zc+!-*+3T2>;kc8N)5MLX71YiP`+5mdf>--bm!7V|kk_r8m@#)<%rB3wujI`xQGjtg zZ{yqG!M*3Dqg6{Eq=yE&AINiLT|9b1snB9z-lH3D8%4^5k=UgK8(R7bs*jErxO%p}D}cRF*_1(JJ0c5e9Yv6NHuUJ7u@G z3uaRYW;0r_*BrI+m1~Cak}n+N>2H!Q124hDCs_ETMGaSY&U~TSLa|O>3HEp~;^^LL zh$*bF&JNC|lRAEtk3IT#`E-b`n7z?@&vZiSHp%;9GKLCI=lFE1B2q0--A0f6yX{V` zf@p_Ch^^@=wvjyRoDq4GGiP)xLpOteXsr00)9}i{)|(ira?DVHIp4|cVAk3ao5r~4 z{aL5+Oi{v=9mL0rE-_qmb9?c1lW#_qzUtC0aB z?ZOknwE{xjuQGLFip5W!u$u{Kw1@E1Rm=!<&2Xr&%hkN!V0le7mB;e2GJ+W~TP2k^i_N%6Ow0 zrW|e|o}u^g`)ZqsN&Izdi90d`)C>ft%hjr^=%{3LF#Gj2_E@UQAT^Al{0{76mIv+1 z`N!|yW3%KQ-J(gMceGTLb8aj)JC*v9Q#0D(&v-#y*&}D3ko>Ne{=!Ib_(X2gSR#Ab zLXtUSOa;;&M*}c3-jRAX4lf$7voJTuVx1>dWqI$Yk5ihSpu&QChHwGE--d$!VGT3Q+qk__C+g5eH>GAB(2nTh7C{{Y<9_^@dRwPsA#U$J7k6_jD9ji~ zK14CQ-L$KJpgmbhYTtN~H-#v=Dfx?u(K+_m@1$^U5=&0uM`zw`BICu|;os8>anI}L zM%{@JADAs>b$n*o%xvZu$Ey5Dz)1-rHqCiE>7k5;JF?(?2>N(C_8~fKCF=oagS&V1 zwTy8~7pznn#Vaj+Mu#2Goi>(ASyOXZN@{L(N-m=mH(RD}saEGkeoC;p`Q(uDNGNYT zRXKJB`ToxEz4*xA)FLr(Vf<+f54>uUGik)gxandiN(I#;y`OfGW4F!MTkg)B?l`#+bMxvoFK8Fp z*@$b55xS(o*%x8Mp@aY_obf?L_H+5RWhB*5>2?_5-lH}$J+7-R(!6n?g$kAD{*hZ<7 zHctH(ksRYh3^TBqSD}kC zh|we^t_PNBzs;6+h0!W+-Dkz|bda{}3(l$@uLr=(wO()Ro%W+oH zfe-xyxDwL3Kd>w@my0RAd|idy5ehvW74Of4c-*CHLF47(PM6onilQRM9QTqkA_r7 zY5T5Y^a^(EHBH8e`-oS9@(l}Vv0Ak~s3PpKm)YBz=1E}+(9NuUIrN8cnWgJ4US#ac z5MDNaVbghCogw&cjcMgnxCE{MeKDe|7?ZB1ziUR@D!!Xc?ZU14V!QS4lwzOj;UA*B zA94yu;09@vT2+iv>}x&!3eGQn>;U0gi&*uXLJ16kK>C3@gl|qeq|cjwvT4^|4$``B zKeK`}gqF6HkJm5LiDe)Tz04;qkfik(T1$5u&V{vw`47A`XHQ<9pXVTGII&UV{uTBW zo$g4abv<{_YEHBCsKaC)F8XxZT1!HQ^;Ov!A5s)#bZkVcMh8&CsW13XU7hkc z!yKG4g9kj);`V65LG%VJg1ml(*!DIIYXQ(J;1G7k1wAB&SQF)P$*>%PE=mngM=1m#b+1%+u>;wmsbn0EvkXI2) z3x7@8+L73FT_N;Iw!eCw6wRNQcS3M5sWe>dZ2T4a3%(_>@RW-eF77MYsT! zyYXX2JcGjT;*Rl@i>cL4mJNOY1X?2X)%wakF~gGpPe#% z`y{V@b@=2HjcT>4K)9@A5+7|g)^Wn4F<$X}zm7i0?ePN!)t=tg3Bjrhdfq|**+wJ!zPQe_$0F|OTIuCwi-cC6& z%d6^jY(&7&W9|^f3>7;AuY!)=tI}o{xuzUvCNmmQouUW%>!UHe_q2TaD@jWky99!< z*DG2q>Wxz9vEDTgkaKyC4wO`Cr#W)*YTil14lJpjGbCoF6eGIUj0QF`5ep>&QnWF4 z2kPD0`F$J9S~ts)H)&k)%-_Zs@&LtbE z9jMk>ef7GGUcPNhaV&3%I@JTUcqYGHg}%0AfH-PK2Lgw8>txN7Yn|I?$J#9C%YnpS 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 Bgv0;< diff --git a/TrackingPerformance/test/validation_output_test.root b/TrackingPerformance/test/validation_output_test.root deleted file mode 100644 index a00512808ee54cb74b04e1837f83207f16da46e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56655 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Μ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 NG Date: Fri, 20 Mar 2026 15:06:04 +0100 Subject: [PATCH 12/15] Finalize test cleanup and helper documentation --- .gitignore | 3 + .../include/TrackingValidationHelpers.h | 28 + .../include/TrackingValidationPlots.h | 35 + TrackingPerformance/test/.gitignore | 1 - .../test/SteeringFile_IDEA_o1_v03.py | 715 ------------------ .../test/testTrackingValidation.sh | 22 +- 6 files changed, 82 insertions(+), 722 deletions(-) delete mode 100644 TrackingPerformance/test/.gitignore delete mode 100644 TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py diff --git a/.gitignore b/.gitignore index 656d005..aefaf52 100644 --- a/.gitignore +++ b/.gitignore @@ -247,3 +247,6 @@ test/k4FWCoreTest/**/*.root test/inputFiles/*.slcio test/gaudi_opts/testConverterConstants.py +# TrackingValidation test outputs +TrackingPerformance/test/*.root +TrackingPerformance/test/*.log diff --git a/TrackingPerformance/include/TrackingValidationHelpers.h b/TrackingPerformance/include/TrackingValidationHelpers.h index ee26c57..eb0ec1a 100644 --- a/TrackingPerformance/include/TrackingValidationHelpers.h +++ b/TrackingPerformance/include/TrackingValidationHelpers.h @@ -29,6 +29,7 @@ namespace TrackingValidationHelpers { +/// Container for helix parameters in the fitter convention struct HelixParams { float D0 = 0.f; float Z0 = 0.f; @@ -39,6 +40,7 @@ struct HelixParams { float pT = 0.f; }; +/// Helper container for PCA position and tangent angle struct PCAInfoHelper { float pcaX = 0.f; float pcaY = 0.f; @@ -47,23 +49,49 @@ struct PCAInfoHelper { bool ok = false; }; +/// Build a unique integer key from a podio object identifier uint64_t oidKey(const podio::ObjectID& id); + +/// Safe wrapper around std::atan2 float safeAtan2(float y, float x); + +/// Wrap a phi difference into the interval [-pi, pi] float wrapDeltaPhi(float a, float b); +/** + * @brief Compute the PCA position and tangent angle in mm using a GenFit-like convention. + * + * The function derives the point of closest approach to the reference point + * and the corresponding tangent direction from the input position, momentum, + * charge sign, and magnetic field. + */ + PCAInfoHelper PCAInfo_mm(float x, float y, float z, float px, float py, float pz, int chargeSign, float refX, float refY, float Bz); + +/** + * @brief Build truth helix parameters in the same convention used by the fitter. + * + * The returned parameters are derived from the MC particle kinematics and vertex + * using the same reference-point and helix convention adopted for fitted tracks, + * so that residuals can be computed consistently. + */ + HelixParams truthFromMC_GenfitConvention(const edm4hep::MCParticle& mc, float Bz, float refX, float refY, float refZ); +/// Retrieve the track state stored at the interaction point bool getAtIPState(const edm4hep::Track& trk, edm4hep::TrackState& out); +/// Compute the transverse momentum from a track state float ptFromState(const edm4hep::TrackState& st, float Bz); + +/// Compute the total momentum from a track state float momentumFromState(const edm4hep::TrackState& st, float Bz); } // namespace TrackingValidationHelpers diff --git a/TrackingPerformance/include/TrackingValidationPlots.h b/TrackingPerformance/include/TrackingValidationPlots.h index 37b4d0e..acec572 100644 --- a/TrackingPerformance/include/TrackingValidationPlots.h +++ b/TrackingPerformance/include/TrackingValidationPlots.h @@ -32,38 +32,72 @@ namespace TrackingValidationPlots { +/// Build logarithmic momentum bins std::vector makeLogBins(double min, double max, double step); + +/// Fit the Gaussian core of a histogram TF1* fitGaussianCore(TH1F* h, const std::string& name); +/** + * @brief Build the d0 resolution as a function of momentum. + * + * The function reads the fitter validation tree, fills residual histograms in + * momentum bins, fits the Gaussian core in each bin, and returns the extracted + * sigma values as a TGraphErrors. + */ + TGraphErrors* makeD0ResolutionVsMomentum(TTree* tree, const char* graphName = "g_d0_resolution_vs_p", double pMin = 0.1, double pMax = 100.0, double logStep = 0.15); +/// Draw the d0 resolution graph on a logarithmic momentum axis TCanvas* drawD0ResolutionCanvas(TGraphErrors* g, const char* canvasName = "c_d0_resolution_vs_p", double xMin = 0.1, double xMax = 100.0); +/** + * @brief Build the total-momentum resolution as a function of momentum. + * + * The function reads the fitter validation tree, fills relative momentum + * residual histograms in momentum bins, fits the Gaussian core in each bin, + * and returns the extracted sigma values as a TGraphErrors. + */ TGraphErrors* makeMomentumResolutionVsMomentum(TTree* tree, const char* graphName = "g_p_resolution_vs_p", double pMin = 0.1, double pMax = 100.0, double logStep = 0.15); +/** + * @brief Build the transverse-momentum resolution as a function of momentum. + * + * The function reads the fitter validation tree, fills relative transverse- + * momentum residual histograms in momentum bins, fits the Gaussian core in + * each bin, and returns the extracted sigma values as a TGraphErrors. + */ TGraphErrors* makePtResolutionVsMomentum(TTree* tree, const char* graphName = "g_pt_resolution_vs_p", double pMin = 0.1, double pMax = 100.0, double logStep = 0.15); +/// Draw a generic resolution graph on a logarithmic momentum axis TCanvas* drawResolutionCanvas(TGraphErrors* g, const char* canvasName, const char* title, double xMin = 0.1, double xMax = 100.0); +/** + * @brief Build the tracking-efficiency graph as a function of momentum. + * + * The function reads the finder validation tree and computes the efficiency + * according to the selected matching definition, returning the result as a + * TGraphErrors. + */ TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, const char* graphName, int efficiencyDefinition, @@ -72,6 +106,7 @@ TGraphErrors* makeEfficiencyVsMomentum(TTree* finderTree, double pMax = 100.0, double logStep = 0.15); +/// Draw the tracking-efficiency graph on a logarithmic momentum axis TCanvas* drawEfficiencyCanvas(TGraphErrors* g, const char* canvasName, const char* title, diff --git a/TrackingPerformance/test/.gitignore b/TrackingPerformance/test/.gitignore deleted file mode 100644 index 3c5bea1..0000000 --- a/TrackingPerformance/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test/*.root diff --git a/TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py b/TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py deleted file mode 100644 index d5852d4..0000000 --- a/TrackingPerformance/test/SteeringFile_IDEA_o1_v03.py +++ /dev/null @@ -1,715 +0,0 @@ -from DDSim.DD4hepSimulation import DD4hepSimulation -from g4units import mm, GeV, MeV - -################################### -# user options -simulateCalo = True # set to False to skip the calo SD action -################################### - -SIM = DD4hepSimulation() - -## The compact XML file, or multiple compact files, if the last one is the closer. -SIM.compactFile = ["../FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml"] -## Lorentz boost for the crossing angle, in radian! -SIM.crossingAngleBoost = 0.015 -SIM.enableDetailedShowerMode = False -SIM.enableG4GPS = False -SIM.enableG4Gun = False -SIM.enableGun = True -## InputFiles for simulation .stdhep, .slcio, .HEPEvt, .hepevt, .pairs, .hepmc, .hepmc.gz, .hepmc.xz, .hepmc.bz2, .hepmc3, .hepmc3.gz, .hepmc3.xz, .hepmc3.bz2, .hepmc3.tree.root files are supported -SIM.inputFiles = [] -## Macro file to execute for runType 'run' or 'vis' -SIM.macroFile = "" -## number of events to simulate, used in batch mode -SIM.numberOfEvents = 10 -## Outputfile from the simulation: .slcio, edm4hep.root and .root output files are supported -SIM.outputFile = "testIDEA_o1_v03.root" -## Physics list to use in simulation -SIM.physicsList = "FTFP_BERT" -## Verbosity use integers from 1(most) to 7(least) verbose -## or strings: VERBOSE, DEBUG, INFO, WARNING, ERROR, FATAL, ALWAYS -SIM.printLevel = 3 -## The type of action to do in this invocation -## batch: just simulate some events, needs numberOfEvents, and input file or gun -## vis: enable visualisation, run the macroFile if it is set -## qt: enable visualisation in Qt shell, run the macroFile if it is set -## run: run the macroFile and exit -## shell: enable interactive session -SIM.runType = "batch" -## Skip first N events when reading a file -SIM.skipNEvents = 0 -## Steering file to change default behaviour -SIM.steeringFile = None -## FourVector of translation for the Smearing of the Vertex position: x y z t -SIM.vertexOffset = [0.0, 0.0, 0.0, 0.0] -## FourVector of the Sigma for the Smearing of the Vertex position: x y z t -SIM.vertexSigma = [0.0, 0.0, 0.0, 0.0] - - -################################################################################ -## Helper holding sensitive detector and other actions. -## -## The default tracker and calorimeter sensitive actions can be set with -## -## >>> SIM = DD4hepSimulation() -## >>> SIM.action.tracker=('Geant4TrackerWeightedAction', {'HitPositionCombination': 2, 'CollectSingleDeposits': False}) -## >>> SIM.action.calo = "Geant4CalorimeterAction" -## -## The default sensitive actions for calorimeters and trackers are applied based on the sensitive type. -## The list of sensitive types can be changed with -## -## >>> SIM = DD4hepSimulation() -## >>> SIM.action.trackerSDTypes = ['tracker', 'myTrackerSensType'] -## >>> SIM.calor.calorimeterSDTypes = ['calorimeter', 'myCaloSensType'] -## -## For specific subdetectors specific sensitive detectors can be set based on patterns in the name of the subdetector. -## -## >>> SIM = DD4hepSimulation() -## >>> SIM.action.mapActions['tpc'] = "TPCSDAction" -## -## and additional parameters for the sensitive detectors can be set when the map is given a tuple -## -## >>> SIM = DD4hepSimulation() -## >>> SIM.action.mapActions['ecal'] =( "CaloPreShowerSDAction", {"FirstLayerNumber": 1} ) -## -## Additional actions can be set as well with the following syntax variations: -## -## >>> SIM = DD4hepSimulation() -## # single action by name only: -## >>> SIM.action.run = "Geant4TestRunAction" -## # multiple actions with comma-separated names: -## >>> SIM.action.event = "Geant4TestEventAction/Action0,Geant4TestEventAction/Action1" -## # single action by tuple of name and parameter dict: -## >>> SIM.action.track = ( "Geant4TestTrackAction", {"Property_int": 10} ) -## # single action by dict of name and parameter dict: -## >>> SIM.action.step = { "name": "Geant4TestStepAction", "parameter": {"Property_int": 10} } -## # multiple actions by list of dict of name and parameter dict: -## >>> SIM.action.stack = [ { "name": "Geant4TestStackAction", "parameter": {"Property_int": 10} } ] -## -## On the command line or in python, these actions can be specified as JSON strings: -## $ ddsim --action.stack '{ "name": "Geant4TestStackAction", "parameter": { "Property_int": 10 } }' -## or -## >>> SIM.action.stack = ''' -## { -## "name": "Geant4TestStackAction", -## "parameter": { -## "Property_int": 10, -## "Property_double": "1.0*mm" -## } -## } -## ''' -## -## -################################################################################ - -## set the default calorimeter action -if simulateCalo: - SIM.action.calo = "Geant4ScintillatorCalorimeterAction" - ## List of patterns matching sensitive detectors of type Calorimeter. - SIM.action.calorimeterSDTypes = ["calorimeter", "DRcaloSiPMSD"] - SIM.action.mapActions["DRcalo"] = "DRCaloSDAction" - ## configure regex SD - SIM.geometry.regexSensitiveDetector["DRcalo"] = {"Match": ["(core|clad)"], "OutputLevel": 3} -else: - SIM.action.calo = "Geant4VoidSensitiveAction" - SIM.action.mapActions["DRcalo"] = "Geant4VoidSensitiveAction" - -## set the default event action -SIM.action.event = [] - -## Create a map of patterns and actions to be applied to sensitive detectors. -## -## Example: if the name of the detector matches 'tpc' the TPCSDAction is used. -## -## SIM.action.mapActions['tpc'] = "TPCSDAction" -## - -## Set the drift chamber action -SIM.action.mapActions['DCH_v2'] = "Geant4TrackerAction" - -## set the default run action -SIM.action.run = [] - -## set the default stack action -SIM.action.stack = [] - -## set the default step action -SIM.action.step = [] - -## set the default track action -SIM.action.track = [] - -## set the default tracker action -SIM.action.tracker = ( - "Geant4TrackerWeightedAction", - {"HitPositionCombination": 2, "CollectSingleDeposits": False}, -) - -## List of patterns matching sensitive detectors of type Tracker. -SIM.action.trackerSDTypes = ["tracker"] - - -################################################################################ -## Configuration for the magnetic field (stepper) -################################################################################ -SIM.field.delta_chord = 0.25 -SIM.field.delta_intersection = 0.001 -SIM.field.delta_one_step = 0.01 -SIM.field.eps_max = 0.001 -SIM.field.eps_min = 5e-05 -SIM.field.equation = "Mag_UsualEqRhs" -SIM.field.largest_step = 10000.0 -SIM.field.min_chord_step = 0.01 -SIM.field.stepper = "ClassicalRK4" - - -################################################################################ -## Configuration for sensitive detector filters -## -## Set the default filter for 'tracker' -## >>> SIM.filter.tracker = "edep1kev" -## Use no filter for 'calorimeter' by default -## >>> SIM.filter.calo = "" -## -## Assign a filter to a sensitive detector via pattern matching -## >>> SIM.filter.mapDetFilter['FTD'] = "edep1kev" -## -## Or more than one filter: -## >>> SIM.filter.mapDetFilter['FTD'] = ["edep1kev", "geantino"] -## -## Don't use the default filter or anything else: -## >>> SIM.filter.mapDetFilter['TPC'] = None ## or "" or [] -## -## Create a custom filter. The dictionary is used to instantiate the filter later on -## >>> SIM.filter.filters['edep3kev'] = dict(name="EnergyDepositMinimumCut/3keV", parameter={"Cut": 3.0*keV} ) -## -## -################################################################################ - -## -## default filter for calorimeter sensitive detectors; -## this is applied if no other filter is used for a calorimeter -## -# note: do not turn on the calo filter, otherwise all optical photons will be killed! -SIM.filter.calo = "" - -## list of filter objects: map between name and parameter dictionary -SIM.filter.filters = { - "geantino": {"name": "GeantinoRejectFilter/GeantinoRejector", "parameter": {}}, - "edep1kev": {"name": "EnergyDepositMinimumCut", "parameter": {"Cut": 0.001}}, - "edep0": {"name": "EnergyDepositMinimumCut/Cut0", "parameter": {"Cut": 0.0}}, -} - -## a map between patterns and filter objects, using patterns to attach filters to sensitive detector -SIM.filter.mapDetFilter["DCH_v2"] = "edep0" -SIM.filter.mapDetFilter["Muon-System"] = "edep0" - -## default filter for tracking sensitive detectors; this is applied if no other filter is used for a tracker -SIM.filter.tracker = "edep1kev" - - -################################################################################ -## Configuration for the Detector Construction. -################################################################################ -SIM.geometry.dumpGDML = "" -SIM.geometry.dumpHierarchy = 0 - -## Print Debug information about Elements -SIM.geometry.enableDebugElements = False - -## Print Debug information about Materials -SIM.geometry.enableDebugMaterials = False - -## Print Debug information about Placements -SIM.geometry.enableDebugPlacements = False - -## Print Debug information about Reflections -SIM.geometry.enableDebugReflections = False - -## Print Debug information about Regions -SIM.geometry.enableDebugRegions = False - -## Print Debug information about Shapes -SIM.geometry.enableDebugShapes = False - -## Print Debug information about Surfaces -SIM.geometry.enableDebugSurfaces = False - -## Print Debug information about Volumes -SIM.geometry.enableDebugVolumes = False - -## Print information about placements -SIM.geometry.enablePrintPlacements = False - -## Print information about Sensitives -SIM.geometry.enablePrintSensitives = False - -################################################################################ -## Configuration for the GuineaPig InputFiles -################################################################################ - -## Set the number of pair particles to simulate per event. -## Only used if inputFile ends with ".pairs" -## If "-1" all particles will be simulated in a single event -## -SIM.guineapig.particlesPerEvent = "-1" - - -################################################################################ -## Configuration for the DDG4 ParticleGun -################################################################################ - -## direction of the particle gun, 3 vector -SIM.gun.direction = (1.0, 0.1, 0.1) - -## choose the distribution of the random direction for theta -## -## Options for random distributions: -## -## 'uniform' is the default distribution, flat in theta -## 'cos(theta)' is flat in cos(theta) -## 'eta', or 'pseudorapidity' is flat in pseudorapity -## 'ffbar' is distributed according to 1+cos^2(theta) -## -## Setting a distribution will set isotrop = True -## -SIM.gun.distribution = None - -## Total energy (including mass) for the particle gun. -## -## If not None, it will overwrite the setting of momentumMin and momentumMax -SIM.gun.energy = 10.0 * GeV - -## Maximal pseudorapidity for random distibution (overrides thetaMin) -SIM.gun.etaMax = None - -## Minimal pseudorapidity for random distibution (overrides thetaMax) -SIM.gun.etaMin = None - -## isotropic distribution for the particle gun -## -## use the options phiMin, phiMax, thetaMin, and thetaMax to limit the range of randomly distributed directions -## if one of these options is not None the random distribution will be set to True and cannot be turned off! -## -SIM.gun.isotrop = False - -## Maximal momentum when using distribution (default = 0.0) -# SIM.gun.momentumMax = 10000.0 - -## Minimal momentum when using distribution (default = 0.0) -# SIM.gun.momentumMin = 0.0 -SIM.gun.multiplicity = 1 -SIM.gun.particle = "e-" - -## Maximal azimuthal angle for random distribution -SIM.gun.phiMax = None - -## Minimal azimuthal angle for random distribution -SIM.gun.phiMin = None - -## position of the particle gun, 3 vector -SIM.gun.position = (0.0, 0.0, 0.0) - -## Maximal polar angle for random distribution -SIM.gun.thetaMax = None - -## Minimal polar angle for random distribution -SIM.gun.thetaMin = None - - -################################################################################ -## Configuration for the hepmc3 InputFiles -################################################################################ - -## Set the name of the attribute contraining color flow information index 0. -SIM.hepmc3.Flow1 = "flow1" - -## Set the name of the attribute contraining color flow information index 1. -SIM.hepmc3.Flow2 = "flow2" - -## Set to false if the input should be opened with the hepmc2 ascii reader. -## -## If ``True`` a '.hepmc' file will be opened with the HEPMC3 Reader Factory. -## -## Defaults to true if DD4hep was build with HEPMC3 support. -## -SIM.hepmc3.useHepMC3 = True - - -################################################################################ -## Configuration for Input Files. -################################################################################ - -## Set one or more functions to configure input steps. -## -## The functions must take a ``DD4hepSimulation`` object as their only argument and return the created generatorAction -## ``gen`` (for example). -## -## For example one can add this to the ddsim steering file: -## -## def exampleUserPlugin(dd4hepSimulation): -## '''Example code for user created plugin. -## -## :param DD4hepSimulation dd4hepSimulation: The DD4hepSimulation instance, so all parameters can be accessed -## :return: GeneratorAction -## ''' -## from DDG4 import GeneratorAction, Kernel -## # Geant4InputAction is the type of plugin, Cry1 just an identifier -## gen = GeneratorAction(Kernel(), 'Geant4InputAction/Cry1' , True) -## # CRYEventReader is the actual plugin, steeringFile its constructor parameter -## gen.Input = 'CRYEventReader|' + 'steeringFile' -## # we can give a dictionary of Parameters that has to be interpreted by the setParameters function of the plugin -## gen.Parameters = {'DataFilePath': '/path/to/files/data'} -## gen.enableUI() -## return gen -## -## SIM.inputConfig.userInputPlugin = exampleUserPlugin -## -## Repeat function definition and assignment to add multiple input steps -## -## -SIM.inputConfig.userInputPlugin = [] - - -################################################################################ -## Configuration for the generator-level InputFiles -################################################################################ - -## Set the name of the collection containing the MCParticle input. -## Default is "MCParticle". -## -SIM.lcio.mcParticleCollectionName = "MCParticle" - - -################################################################################ -## Configuration for the LCIO output file settings -################################################################################ - -## The event number offset to write in slcio output file. E.g setting it to 42 will start counting events from 42 instead of 0 -SIM.meta.eventNumberOffset = 0 - -## Event parameters to write in every event. Use C/F/I ids to specify parameter type. E.g parameterName/F=0.42 to set a float parameter -SIM.meta.eventParameters = [] - -## The run number offset to write in slcio output file. E.g setting it to 42 will start counting runs from 42 instead of 0 -SIM.meta.runNumberOffset = 0 - - -################################################################################ -## Configuration for the output levels of DDG4 components -################################################################################ - -## Output level for geometry. -SIM.output.geometry = 2 - -## Output level for input sources -SIM.output.inputStage = 3 - -## Output level for Geant4 kernel -SIM.output.kernel = 3 - -## Output level for ParticleHandler -SIM.output.part = 3 - -## Output level for Random Number Generator setup -SIM.output.random = 6 - - -################################################################################ -## Configuration for Output Files. -################################################################################ - -## Use the DD4HEP output plugin regardless of outputfilename. -SIM.outputConfig.forceDD4HEP = False - -## Use the EDM4HEP output plugin regardless of outputfilename. -SIM.outputConfig.forceEDM4HEP = False - -## Use the LCIO output plugin regardless of outputfilename. -SIM.outputConfig.forceLCIO = False - -## Set a function to configure the outputFile. -## -## The function must take a ``DD4hepSimulation`` object as its only argument and return ``None``. -## -## For example one can add this to the ddsim steering file: -## -## def exampleUserPlugin(dd4hepSimulation): -## '''Example code for user created plugin. -## -## :param DD4hepSimulation dd4hepSimulation: The DD4hepSimulation instance, so all parameters can be accessed -## :return: None -## ''' -## from DDG4 import EventAction, Kernel -## dd = dd4hepSimulation # just shorter variable name -## evt_root = EventAction(Kernel(), 'Geant4Output2ROOT/' + dd.outputFile, True) -## evt_root.HandleMCTruth = True or False -## evt_root.Control = True -## output = dd.outputFile -## if not dd.outputFile.endswith(dd.outputConfig.myExtension): -## output = dd.outputFile + dd.outputConfig.myExtension -## evt_root.Output = output -## evt_root.enableUI() -## Kernel().eventAction().add(evt_root) -## return None -## -## SIM.outputConfig.userOutputPlugin = exampleUserPlugin -## # arbitrary options can be created and set via the steering file or command line -## SIM.outputConfig.myExtension = '.csv' -## - - -def Geant4Output2EDM4hep_DRC_plugin(dd4hepSimulation): - from DDG4 import EventAction, Kernel - - evt_root = EventAction( - Kernel(), "Geant4Output2EDM4hep_DRC/" + dd4hepSimulation.outputFile, True - ) - evt_root.Control = True - output = dd4hepSimulation.outputFile - evt_root.Output = output - evt_root.enableUI() - Kernel().eventAction().add(evt_root) - return None - - -SIM.outputConfig.userOutputPlugin = Geant4Output2EDM4hep_DRC_plugin - -################################################################################ -## Configuration for the Particle Handler/ MCTruth treatment -################################################################################ - -## Enable lots of printout on simulated hits and MC-truth information -SIM.part.enableDetailedHitsAndParticleInfo = False - -## Keep all created particles -SIM.part.keepAllParticles = False - -## Minimal distance between particle vertex and endpoint of parent after -## which the vertexIsNotEndpointOfParent flag is set -## -SIM.part.minDistToParentVertex = 2.2e-14 - -## MinimalKineticEnergy to store particles created in the tracking region -SIM.part.minimalKineticEnergy = 1.0 - -## Printout at End of Tracking -SIM.part.printEndTracking = False - -## Printout at Start of Tracking -SIM.part.printStartTracking = False - -## List of processes to save, on command line give as whitespace separated string in quotation marks -SIM.part.saveProcesses = ["Decay"] - -## Optionally enable an extended Particle Handler -SIM.part.userParticleHandler = "Geant4TCUserParticleHandler" - - -################################################################################ -## Configuration for the PhysicsList and Monte Carlo particle selection. -## -## To load arbitrary plugins, add a function to be executed. -## -## The function must take the DDG4.Kernel() object as the only argument. -## -## For example, add a function definition and the call to a steering file:: -## -## def setupCerenkov(kernel): -## from DDG4 import PhysicsList -## seq = kernel.physicsList() -## cerenkov = PhysicsList(kernel, 'Geant4CerenkovPhysics/CerenkovPhys') -## cerenkov.MaxNumPhotonsPerStep = 10 -## cerenkov.MaxBetaChangePerStep = 10.0 -## cerenkov.TrackSecondariesFirst = True -## cerenkov.VerboseLevel = 2 -## cerenkov.enableUI() -## seq.adopt(cerenkov) -## ph = PhysicsList(kernel, 'Geant4OpticalPhotonPhysics/OpticalGammaPhys') -## ph.addParticleConstructor('G4OpticalPhoton') -## ph.VerboseLevel = 2 -## ph.enableUI() -## seq.adopt(ph) -## return None -## -## SIM.physics.setupUserPhysics(setupCerenkov) -## -## # End of example -## -################################################################################ - -## Set of Generator Statuses that are used to mark unstable particles that should decay inside of Geant4. -## -SIM.physics.alternativeDecayStatuses = set() - -## If true, add decay processes for all particles. -## -## Only enable when creating a physics list not based on an existing Geant4 list! -## -SIM.physics.decays = False - -## The name of the Geant4 Physics list. -SIM.physics.list = "FTFP_BERT" - -## location of particle.tbl file containing extra particles and their lifetime information -## -## For example in $DD4HEP/examples/DDG4/examples/particle.tbl -## -SIM.physics.pdgfile = None - -## The global geant4 rangecut for secondary production -## -## Default is 0.7 mm as is the case in geant4 10 -## -## To disable this plugin and be absolutely sure to use the Geant4 default range cut use "None" -## -## Set printlevel to DEBUG to see a printout of all range cuts, -## but this only works if range cut is not "None" -## -SIM.physics.rangecut = None - -## Set of PDG IDs that will not be passed from the input record to Geant4. -## -## Quarks, gluons and W's Z's etc should not be treated by Geant4 -## -SIM.physics.rejectPDGs = { - 1, - 2, - 3, - 4, - 5, - 6, - 3201, - 3203, - 4101, - 4103, - 21, - 23, - 24, - 5401, - 25, - 2203, - 5403, - 3101, - 3103, - 4403, - 2101, - 5301, - 2103, - 5303, - 4301, - 1103, - 4303, - 5201, - 5203, - 3303, - 4201, - 4203, - 5101, - 5103, - 5503, -} - -## Set of PDG IDs for particles that should not be passed to Geant4 if their properTime is 0. -## -## The properTime of 0 indicates a documentation to add FSR to a lepton for example. -## -SIM.physics.zeroTimePDGs = {17, 11, 13, 15} - - -def setupOpticalPhysics(kernel): - from DDG4 import PhysicsList - - seq = kernel.physicsList() - cerenkov = PhysicsList(kernel, "Geant4CerenkovPhysics/CerenkovPhys") - cerenkov.TrackSecondariesFirst = True - cerenkov.VerboseLevel = 1 - cerenkov.enableUI() - seq.adopt(cerenkov) - - opt = PhysicsList(kernel, "Geant4OpticalPhotonPhysics/OpticalGammaPhys") - opt.addParticleConstructor("G4OpticalPhoton") - opt.VerboseLevel = 1 - # set BoundaryInvokeSD to true when using DRC wafer as the SD - # opt.BoundaryInvokeSD = True - opt.enableUI() - seq.adopt(opt) - - return None - -if simulateCalo: - SIM.physics.setupUserPhysics(setupOpticalPhysics) - -def setupDRCFastSim(kernel): - from DDG4 import DetectorConstruction, Geant4, PhysicsList - - geant4 = Geant4(kernel) - seq = geant4.detectorConstruction() - # Create a model for fast simulation - model = DetectorConstruction(kernel, str("Geant4DRCFiberModel/ShowerModel")) - # Mandatory model parameters - model.RegionName = "FastSimOpFiberRegion" - model.Enable = True - model.ApplicableParticles = ["opticalphoton"] - model.enableUI() - seq.adopt(model) - # Now build the physics list: - phys = kernel.physicsList() - ph = PhysicsList(kernel, str("Geant4FastPhysics/FastPhysicsList")) - ph.EnabledParticles = ["opticalphoton"] - ph.BeVerbose = True - ph.enableUI() - phys.adopt(ph) - phys.dump() - - -# turn-on fastsim if the skipScint option of the SD is set to false -# SIM.physics.setupUserPhysics(setupDRCFastSim) - -################################################################################ -## Properties for the random number generator -################################################################################ - -## If True, calculate random seed for each event basedon eventID and runID -## Allows reproducibility even whenSkippingEvents -SIM.random.enableEventSeed = False -SIM.random.file = None -SIM.random.luxury = 1 -SIM.random.replace_gRandom = True -SIM.random.seed = None -SIM.random.type = None - - -################################################################################ -## Configuration for setting commands to run during different phases. -## -## In this section, one can configure commands that should be run during the different phases of the Geant4 execution. -## -## 1. Configuration -## 2. Initialization -## 3. Pre Run -## 4. Post Run -## 5. Terminate / Finalization -## -## For example, one can add -## -## >>> SIM.ui.commandsConfigure = ['/physics_lists/em/SyncRadiation true'] -## -## Further details should be taken from the Geant4 documentation. -## -################################################################################ - -## List of UI commands to run during the 'Configure' phase. -SIM.ui.commandsConfigure = [] - -## List of UI commands to run during the 'Initialize' phase. -SIM.ui.commandsInitialize = [] - -## List of UI commands to run during the 'PostRun' phase. -SIM.ui.commandsPostRun = [] - -## List of UI commands to run during the 'PreRun' phase. -SIM.ui.commandsPreRun = [] - -## List of UI commands to run during the 'Terminate' phase. -SIM.ui.commandsTerminate = [] diff --git a/TrackingPerformance/test/testTrackingValidation.sh b/TrackingPerformance/test/testTrackingValidation.sh index 9f22310..495f46e 100644 --- a/TrackingPerformance/test/testTrackingValidation.sh +++ b/TrackingPerformance/test/testTrackingValidation.sh @@ -35,8 +35,12 @@ if ! command -v k4run >/dev/null 2>&1; then exit 1 fi +if ! command -v curl >/dev/null 2>&1; then + echo "ERROR: curl not found in PATH" + exit 1 +fi + XML_FILE="${K4GEO}/FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml" -STEERING_FILE="${SCRIPT_DIR}/SteeringFile_IDEA_o1_v03.py" RUN_FILE="${SCRIPT_DIR}/runTrackingValidation.py" VAL_FILE="${SCRIPT_DIR}/validation_output_test.root" @@ -45,11 +49,6 @@ if [ ! -f "${XML_FILE}" ]; then exit 1 fi -if [ ! -f "${STEERING_FILE}" ]; then - echo "ERROR: DDSim steering file not found: ${STEERING_FILE}" - exit 1 -fi - if [ ! -f "${RUN_FILE}" ]; then echo "ERROR: tracking validation run file not found: ${RUN_FILE}" exit 1 @@ -58,6 +57,7 @@ fi TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/tracking_validation.XXXXXX")" trap 'rm -rf "${TMPDIR}"' EXIT +STEERING_FILE="${TMPDIR}/SteeringFile_IDEA_o1_v03.py" SIM_FILE="${TMPDIR}/out_sim_edm4hep.root" RECO_FILE="${TMPDIR}/out_reco.root" @@ -66,6 +66,16 @@ rm -f "${VAL_FILE}" N_EVENTS=5 SEED=42 +echo "=== Downloading DDSim steering file ===" +curl -L \ + -o "${STEERING_FILE}" \ + https://raw.githubusercontent.com/key4hep/k4geo/master/example/SteeringFile_IDEA_o1_v03.py + +if [ ! -f "${STEERING_FILE}" ]; then + echo "ERROR: failed to download DDSim steering file" + exit 1 +fi + echo "=== Test configuration ===" echo "Script dir: ${SCRIPT_DIR}" echo "Temporary dir: ${TMPDIR}" From 180d9e93eddc5be21b220428537aa3ed41203481 Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Fri, 20 Mar 2026 16:15:51 +0100 Subject: [PATCH 13/15] Finalize test workflow and documentation --- TrackingPerformance/README.md | 5 +- .../test/testTrackingValidation.sh | 109 +++++++++++------- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index 38d22c0..52bbf15 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -160,8 +160,11 @@ k4DetectorPerformance/TrackingPerformance/test/validation_output_test.root ``` ### Test configuration and steering options -The current test runs the full reconstruction and validation chain after simulation with the following settings: +The current test runs the full reconstruction and validation chain with the following settings: +- `RUN_SIM = 1` + simulation step in the shell test (`0` = skip simulation and use an existing EDM4hep input file via `INPUT_FILE_OVERRIDE`, `1` = run simulation with `ddsim`); + - `runDigi = 1` digitization step (`0` = skip digitization, `1` = run digitization); diff --git a/TrackingPerformance/test/testTrackingValidation.sh b/TrackingPerformance/test/testTrackingValidation.sh index 495f46e..737d341 100644 --- a/TrackingPerformance/test/testTrackingValidation.sh +++ b/TrackingPerformance/test/testTrackingValidation.sh @@ -4,6 +4,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" MODEL_FILE="${1:-}" +# Optional shell-level control of the simulation step +RUN_SIM="${RUN_SIM:-1}" +INPUT_FILE_OVERRIDE="${INPUT_FILE_OVERRIDE:-}" + if [ -z "${MODEL_FILE}" ]; then echo "ERROR: missing ONNX model path argument" echo "Usage: $0 /full/path/to/SimpleGatrIDEAv3o1.onnx" @@ -25,19 +29,21 @@ if [ ! -d "${K4GEO}" ]; then exit 1 fi -if ! command -v ddsim >/dev/null 2>&1; then - echo "ERROR: ddsim not found in PATH" - exit 1 -fi - if ! command -v k4run >/dev/null 2>&1; then echo "ERROR: k4run not found in PATH" exit 1 fi -if ! command -v curl >/dev/null 2>&1; then - echo "ERROR: curl not found in PATH" - exit 1 +if [ "${RUN_SIM}" -eq 1 ]; then + if ! command -v ddsim >/dev/null 2>&1; then + echo "ERROR: ddsim not found in PATH" + exit 1 + fi + + if ! command -v curl >/dev/null 2>&1; then + echo "ERROR: curl not found in PATH" + exit 1 + fi fi XML_FILE="${K4GEO}/FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml" @@ -66,24 +72,33 @@ rm -f "${VAL_FILE}" N_EVENTS=5 SEED=42 -echo "=== Downloading DDSim steering file ===" -curl -L \ - -o "${STEERING_FILE}" \ - https://raw.githubusercontent.com/key4hep/k4geo/master/example/SteeringFile_IDEA_o1_v03.py - -if [ ! -f "${STEERING_FILE}" ]; then - echo "ERROR: failed to download DDSim steering file" - exit 1 +# Decide which input file to pass to k4run +if [ "${RUN_SIM}" -eq 1 ]; then + INPUT_FILE="${SIM_FILE}" +else + if [ -z "${INPUT_FILE_OVERRIDE}" ]; then + echo "ERROR: RUN_SIM=0 but INPUT_FILE_OVERRIDE is empty" + echo "Please provide an existing EDM4hep file, e.g." + echo " RUN_SIM=0 INPUT_FILE_OVERRIDE=/path/to/input.root $0 /path/to/model.onnx" + exit 1 + fi + + if [ ! -f "${INPUT_FILE_OVERRIDE}" ]; then + echo "ERROR: INPUT_FILE_OVERRIDE does not exist: ${INPUT_FILE_OVERRIDE}" + exit 1 + fi + + INPUT_FILE="${INPUT_FILE_OVERRIDE}" fi echo "=== Test configuration ===" echo "Script dir: ${SCRIPT_DIR}" echo "Temporary dir: ${TMPDIR}" echo "Geometry XML: ${XML_FILE}" -echo "DDSim steering: ${STEERING_FILE}" echo "Run script: ${RUN_FILE}" echo "ONNX model: ${MODEL_FILE}" -echo "Simulation file: ${SIM_FILE}" +echo "RUN_SIM: ${RUN_SIM}" +echo "Input file: ${INPUT_FILE}" echo "Reco file: ${RECO_FILE}" echo "Validation file: ${VAL_FILE}" echo "Events: ${N_EVENTS}" @@ -99,30 +114,44 @@ echo "doPerfectFit: 1" echo "finderEfficiencyDefinition: 1" echo "finderPurityThreshold: 0.75" -echo "=== Step 1: DDSim ===" -ddsim \ - --steeringFile "${STEERING_FILE}" \ - --compactFile "${XML_FILE}" \ - -G \ - --gun.particle mu- \ - --gun.energy "5*GeV" \ - --gun.distribution uniform \ - --gun.thetaMin "89*deg" \ - --gun.thetaMax "89*deg" \ - --gun.phiMin "0*deg" \ - --gun.phiMax "0*deg" \ - --random.seed "${SEED}" \ - --numberOfEvents "${N_EVENTS}" \ - --outputFile "${SIM_FILE}" - -if [ ! -f "${SIM_FILE}" ]; then - echo "ERROR: simulation output was not created: ${SIM_FILE}" - exit 1 +if [ "${RUN_SIM}" -eq 1 ]; then + echo "=== Downloading DDSim steering file ===" + curl -L \ + -o "${STEERING_FILE}" \ + https://raw.githubusercontent.com/key4hep/k4geo/master/example/SteeringFile_IDEA_o1_v03.py + + if [ ! -f "${STEERING_FILE}" ]; then + echo "ERROR: failed to download DDSim steering file" + exit 1 + fi + + echo "=== Step 1: DDSim ===" + ddsim \ + --steeringFile "${STEERING_FILE}" \ + --compactFile "${XML_FILE}" \ + -G \ + --gun.particle mu- \ + --gun.energy "5*GeV" \ + --gun.distribution uniform \ + --gun.thetaMin "89*deg" \ + --gun.thetaMax "89*deg" \ + --gun.phiMin "0*deg" \ + --gun.phiMax "0*deg" \ + --random.seed "${SEED}" \ + --numberOfEvents "${N_EVENTS}" \ + --outputFile "${SIM_FILE}" + + if [ ! -f "${SIM_FILE}" ]; then + echo "ERROR: simulation output was not created: ${SIM_FILE}" + exit 1 + fi +else + echo "=== Step 1: DDSim skipped (RUN_SIM=0) ===" fi echo "=== Step 2: digitization + tracking + validation ===" k4run "${RUN_FILE}" \ - --inputFile "${SIM_FILE}" \ + --inputFile "${INPUT_FILE}" \ --modelPath "${MODEL_FILE}" \ --outputFile "${RECO_FILE}" \ --validationFile "${VAL_FILE}" \ @@ -149,7 +178,9 @@ if [ ! -f "${VAL_FILE}" ]; then fi echo "=== Step 3: check outputs ===" -test -f "${SIM_FILE}" +if [ "${RUN_SIM}" -eq 1 ]; then + test -f "${SIM_FILE}" +fi test -f "${RECO_FILE}" test -f "${VAL_FILE}" From dc3701eb1c40c9c976030b9b46486307cae6748c Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Mon, 23 Mar 2026 16:40:47 +0100 Subject: [PATCH 14/15] Make tracking validation test script executable --- .../test/testTrackingValidation.sh | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) mode change 100644 => 100755 TrackingPerformance/test/testTrackingValidation.sh diff --git a/TrackingPerformance/test/testTrackingValidation.sh b/TrackingPerformance/test/testTrackingValidation.sh old mode 100644 new mode 100755 index 737d341..379b282 --- a/TrackingPerformance/test/testTrackingValidation.sh +++ b/TrackingPerformance/test/testTrackingValidation.sh @@ -5,8 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" MODEL_FILE="${1:-}" # Optional shell-level control of the simulation step -RUN_SIM="${RUN_SIM:-1}" -INPUT_FILE_OVERRIDE="${INPUT_FILE_OVERRIDE:-}" +TRACKINGPERF_RUN_SIM="${TRACKINGPERF_RUN_SIM:-1}" +TRACKINGPERF_INPUT_FILE_OVERRIDE="${TRACKINGPERF_INPUT_FILE_OVERRIDE:-}" if [ -z "${MODEL_FILE}" ]; then echo "ERROR: missing ONNX model path argument" @@ -34,7 +34,7 @@ if ! command -v k4run >/dev/null 2>&1; then exit 1 fi -if [ "${RUN_SIM}" -eq 1 ]; then +if [ "${TRACKINGPERF_RUN_SIM}" -eq 1 ]; then if ! command -v ddsim >/dev/null 2>&1; then echo "ERROR: ddsim not found in PATH" exit 1 @@ -73,22 +73,22 @@ N_EVENTS=5 SEED=42 # Decide which input file to pass to k4run -if [ "${RUN_SIM}" -eq 1 ]; then +if [ "${TRACKINGPERF_RUN_SIM}" -eq 1 ]; then INPUT_FILE="${SIM_FILE}" else - if [ -z "${INPUT_FILE_OVERRIDE}" ]; then - echo "ERROR: RUN_SIM=0 but INPUT_FILE_OVERRIDE is empty" + if [ -z "${TRACKINGPERF_INPUT_FILE_OVERRIDE}" ]; then + echo "ERROR: TRACKINGPERF_RUN_SIM=0 but TRACKINGPERF_INPUT_FILE_OVERRIDE is empty" echo "Please provide an existing EDM4hep file, e.g." - echo " RUN_SIM=0 INPUT_FILE_OVERRIDE=/path/to/input.root $0 /path/to/model.onnx" + echo " TRACKINGPERF_RUN_SIM=0 TRACKINGPERF_INPUT_FILE_OVERRIDE=/path/to/input.root $0 /path/to/model.onnx" exit 1 fi - if [ ! -f "${INPUT_FILE_OVERRIDE}" ]; then - echo "ERROR: INPUT_FILE_OVERRIDE does not exist: ${INPUT_FILE_OVERRIDE}" + if [ ! -f "${TRACKINGPERF_INPUT_FILE_OVERRIDE}" ]; then + echo "ERROR: TRACKINGPERF_INPUT_FILE_OVERRIDE does not exist: ${TRACKINGPERF_INPUT_FILE_OVERRIDE}" exit 1 fi - INPUT_FILE="${INPUT_FILE_OVERRIDE}" + INPUT_FILE="${TRACKINGPERF_INPUT_FILE_OVERRIDE}" fi echo "=== Test configuration ===" @@ -97,7 +97,7 @@ echo "Temporary dir: ${TMPDIR}" echo "Geometry XML: ${XML_FILE}" echo "Run script: ${RUN_FILE}" echo "ONNX model: ${MODEL_FILE}" -echo "RUN_SIM: ${RUN_SIM}" +echo "TRACKINGPERF_RUN_SIM: ${TRACKINGPERF_RUN_SIM}" echo "Input file: ${INPUT_FILE}" echo "Reco file: ${RECO_FILE}" echo "Validation file: ${VAL_FILE}" @@ -114,7 +114,7 @@ echo "doPerfectFit: 1" echo "finderEfficiencyDefinition: 1" echo "finderPurityThreshold: 0.75" -if [ "${RUN_SIM}" -eq 1 ]; then +if [ "${TRACKINGPERF_RUN_SIM}" -eq 1 ]; then echo "=== Downloading DDSim steering file ===" curl -L \ -o "${STEERING_FILE}" \ @@ -146,7 +146,7 @@ if [ "${RUN_SIM}" -eq 1 ]; then exit 1 fi else - echo "=== Step 1: DDSim skipped (RUN_SIM=0) ===" + echo "=== Step 1: DDSim skipped (TRACKINGPERF_RUN_SIM=0) ===" fi echo "=== Step 2: digitization + tracking + validation ===" @@ -178,7 +178,7 @@ if [ ! -f "${VAL_FILE}" ]; then fi echo "=== Step 3: check outputs ===" -if [ "${RUN_SIM}" -eq 1 ]; then +if [ "${TRACKINGPERF_RUN_SIM}" -eq 1 ]; then test -f "${SIM_FILE}" fi test -f "${RECO_FILE}" From 9484462ad72ebafb1781c0fd376ed94175ac18b6 Mon Sep 17 00:00:00 2001 From: Arina Ponomareva Date: Tue, 24 Mar 2026 09:52:19 +0100 Subject: [PATCH 15/15] Address review comments --- TrackingPerformance/README.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/TrackingPerformance/README.md b/TrackingPerformance/README.md index 52bbf15..e20af91 100644 --- a/TrackingPerformance/README.md +++ b/TrackingPerformance/README.md @@ -3,7 +3,7 @@ ## Overview `TrackingValidation` is a validation algorithm for studying the performance (efficiency, purity, residuals, resolutions) of track finding and track fitting in the tracking reconstruction. -It is desighned to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking i.e. tracks fitted using the correct simhits from the particle truth information. The algorithm writes validation information to a ROOT output file containing TTrees and summary plots that can be used later for performance studies and plotting. +It is designed to compare reconstructed and fitted tracks with Monte Carlo truth information and, when enabled, with tracks obtained from perfect tracking, i.e. tracks fitted using the correct simHits from the particle truth information. The algorithm writes validation information to a ROOT output file containing TTrees and summary plots that can be used later for performance studies and plotting. Typical use cases include: - validation of track-finder performance, @@ -126,26 +126,6 @@ In the current setup: - the reconstruction and validation steps are controlled by `runTrackingValidation.py`, - the full test is launched through `ctest`. - -### Build and run the test - -Set up the Key4hep environment: - -```bash -source /cvmfs/sw-nightlies.hsf.org/key4hep/setup.sh -``` - -Build and install the package: - -```bash - -mkdir build install -cd build -cmake .. -DCMAKE_INSTALL_PREFIX=../install -make install -j 8 -``` - - Run the test from the build directory: ```bash @@ -162,8 +142,8 @@ k4DetectorPerformance/TrackingPerformance/test/validation_output_test.root The current test runs the full reconstruction and validation chain with the following settings: -- `RUN_SIM = 1` - simulation step in the shell test (`0` = skip simulation and use an existing EDM4hep input file via `INPUT_FILE_OVERRIDE`, `1` = run simulation with `ddsim`); +- `TRACKINGPERF_RUN_SIM = 1` + simulation step in the shell test (`0` = skip simulation and use an existing EDM4hep input file via `TRACKINGPERF_INPUT_FILE_OVERRIDE`, `1` = run simulation with `ddsim`); - `runDigi = 1` digitization step (`0` = skip digitization, `1` = run digitization);