Skip to content

Commit a150df9

Browse files
committed
[PWGEM,PWGJE] EMCal TM monitor: MC truth of the track matching chain
The EMCal track-matching veto misses many clusters made by conversion electrons (PCM photons counted twice in PCM-EMC pairs). The derived data cannot tell where the matching chain fails, so add an MC truth process that follows the leading MC particle of each cluster through it. emcTmMonitor: - New processMCTruth (off by default). For every cluster, take the leading MC contributor (largest energy fraction) and classify it as conversion e+-, primary e+-, other e+-, other charged, photon, neutral or other. - For charged leading particles, follow the particle's best reconstructed track and record one status per cluster: no reco track, not extrapolated to the EMCal, no matching candidate (neither a global track nor a V0 leg), candidate not matched, matched as primary, matched as secondary. - Histograms under MCTruth/: - status vs truth class, track type, V0-leg flag and MC pT - track-cluster residuals up to +-0.4 in the origin frame of the extrapolation, so tails beyond the matching windows are visible - own residual minus stored residual, to validate the frame conversion - status vs conversion radius - E/p of conversion-e+- clusters - Needs emcal-correction-task with processMCWithSecondaries and the photon conversion builder (aod::V0Legs). - The event selection moves into a helper shared by both process functions; the event histograms are filled only once. - Charges come from the O2DatabasePDG service. - Histograms with labelled axes are TH2 or THn, avoiding the TH3 label merge bug in ROOT 6.36. - Add missing BC table to process functions for event selecton which needs it to get the runnumber via collision -> bc -> runnumber twoPhotonMcTask: - hOrigin: disable axis extension on the labelled z axis. TH1Merger::LabelMerge in ROOT 6.36 reads the z labels from the y axis, so merged content ended up in a new, unlabelled bin. - Leg matching diagnostics: evaluate the primary-match status only for legs that pass the global track selection (rebuilt from the V0Legs columns); other legs get a new "not a global track" status instead of inflating "no match stored". Co-Authored-By: Claude Opus 5.5 Fix missing BC table in process functions
1 parent 9c5b19b commit a150df9

2 files changed

Lines changed: 390 additions & 30 deletions

File tree

‎PWGEM/PhotonMeson/Tasks/twoPhotonMcTask.cxx‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ enum class LegMatchStatus : uint8_t {
119119
kOutsideWindow, // leg found, but outside the track matching window of the analysis
120120
kInsideWindowEoverP, // leg found inside the window, but E/p above the cut (primaries only)
121121
kInsideWindow, // leg found inside the window: the cluster should have been vetoed
122+
kNotGlobalTrack, // primaries only: the leg is not a global track, so it is no candidate of the primary matching
122123
kNStatus
123124
};
124125

@@ -478,12 +479,15 @@ struct TwoPhotonMcTask {
478479
const AxisSpec axisDeltaPhi{100, -0.1, 0.1, "#Delta#varphi (cluster - track)"};
479480
const AxisSpec axisDp{200, -1., 1., "(#it{p}_{match} - #it{p}_{leg}) / #it{p}_{leg}"};
480481
static constexpr std::array<std::string_view, static_cast<int>(SameParticleOrigin::kNOrigins)> OriginLabels = {"leg e^{#pm}", "brems #gamma of leg", "other e^{#pm}", "other"};
481-
static constexpr std::array<std::string_view, static_cast<int>(LegMatchStatus::kNStatus)> StatusLabels = {"no match stored", "no match with leg #it{p}", "outside window", "in window, #it{E}/#it{p} #geq cut", "in window"};
482+
static constexpr std::array<std::string_view, static_cast<int>(LegMatchStatus::kNStatus)> StatusLabels = {"no match stored", "no match with leg #it{p}", "outside window", "in window, #it{E}/#it{p} #geq cut", "in window", "not a global track"};
482483

483484
auto hOrigin = registry.add<TH3>("PCMEMC/SameParticle/hOrigin", "same particle pairs: leading contributor of the cluster", HistType::kTH3F, {thnAxisInvMass, thnAxisPtRec, axisOrigin});
484485
for (std::size_t i = 0; i < OriginLabels.size(); ++i) {
485486
hOrigin->GetZaxis()->SetBinLabel(static_cast<int>(i) + 1, OriginLabels[i].data());
486487
}
488+
// labels make the axis extendable, and TH1Merger::LabelMerge in ROOT 6.36 reads the z labels from the y axis:
489+
// merged TH3 content would end up in a new, unlabelled z bin. With a fixed axis the merge goes bin by bin.
490+
hOrigin->GetZaxis()->SetCanExtend(false);
487491
registry.add("PCMEMC/SameParticle/hLegEoverP", "cluster from a V0 leg: #it{E}_{cls}/#it{p}_{leg}", HistType::kTH2F, {{200, 0., 4., "#it{E}_{cls}/#it{p}_{leg}"}, axisPtLeg});
488492
for (const auto& [type, title] : {std::pair<std::string_view, std::string_view>{"Prim", "primary"}, {"Sec", "secondary"}}) {
489493
auto hStatus = registry.add<TH2>(std::format("PCMEMC/SameParticle/hLeg{}Status", type.data()).data(), std::format("cluster from a V0 leg: leg among the matched {} tracks", title.data()).data(), HistType::kTH2F, {axisStatus, axisPtLeg});
@@ -762,6 +766,30 @@ struct TwoPhotonMcTask {
762766
return PairBucket::kCrossOtherHadron;
763767
}
764768

769+
/// \brief Whether the track of a V0 leg passes the global track selection used by the primary EMCal track matching.
770+
/// Rebuilt from the leg columns of the derived data, following getGlobalTrackSelectionRun3ITSMatch(Run3ITSibAny):
771+
/// the golden chi2 requirement cannot be checked here, and pT/eta are the ones of the leg at the V0 vertex.
772+
/// \param leg V0 leg
773+
template <o2::soa::is_iterator TLeg>
774+
static bool isLikelyGlobalTrack(TLeg const& leg)
775+
{
776+
constexpr float MinPt = 0.1f, MaxEta = 0.8f;
777+
constexpr int MinCrossedRows = 70;
778+
constexpr float MinCrossedRowsOverFindable = 0.8f, MaxChi2TPC = 4.f, MaxChi2ITS = 36.f, MaxDcaZ = 2.f;
779+
const float pt = leg.pt();
780+
if (!leg.hasITS() || !leg.hasTPC() || pt < MinPt || std::fabs(leg.eta()) > MaxEta) {
781+
return false;
782+
}
783+
if (leg.itsNClsInnerBarrel() < 1 || leg.itsChi2NCl() > MaxChi2ITS) { // Run3ITSibAny: at least one hit in ITS layers 0-2
784+
return false;
785+
}
786+
if (leg.tpcNClsCrossedRows() < MinCrossedRows || leg.tpcCrossedRowsOverFindableCls() < MinCrossedRowsOverFindable || leg.tpcChi2NCl() > MaxChi2TPC) {
787+
return false;
788+
}
789+
const float maxDcaXY = 0.0105f + 0.035f / std::pow(pt, 1.1f); // o2-linter: disable=magic-number (global track selection)
790+
return std::fabs(leg.dcaXY()) <= maxDcaXY && std::fabs(leg.dcaZ()) <= MaxDcaZ;
791+
}
792+
765793
/// \brief Look for the V0 leg among the tracks matched to an EMC cluster and classify why it did not veto the cluster.
766794
/// The matched track tables store no track index, so the leg is identified by its momentum.
767795
/// \tparam isSecondary matches are secondaries (V0 legs, no E/p condition) instead of primaries
@@ -865,7 +893,12 @@ struct TwoPhotonMcTask {
865893
return;
866894
}
867895
registry.fill(HIST("PCMEMC/SameParticle/hLegEoverP"), emc.e() / pLeg, ptLeg);
868-
fillLegMatch<false>(matchedPrims.sliceBy(perEMCClusterMT, emc.globalIndex()), emc.e(), pLeg, ptLeg);
896+
// the primary matching only uses global tracks: other legs are no candidate there and get their own status
897+
if (isLikelyGlobalTrack(leg)) {
898+
fillLegMatch<false>(matchedPrims.sliceBy(perEMCClusterMT, emc.globalIndex()), emc.e(), pLeg, ptLeg);
899+
} else {
900+
registry.fill(HIST("PCMEMC/SameParticle/hLegPrimStatus"), static_cast<int>(LegMatchStatus::kNotGlobalTrack), ptLeg);
901+
}
869902
fillLegMatch<true>(matchedSeconds.sliceBy(perEMCClusterMS, emc.globalIndex()), emc.e(), pLeg, ptLeg);
870903
}
871904

0 commit comments

Comments
 (0)