Skip to content

Commit 6ee9184

Browse files
BongHwiBong-Hwi Lim
andauthored
[PWGLF] Fix Omega2012 and K1 MC efficiency denominators and truth matching (#18431)
Co-authored-by: Bong-Hwi Lim <lim@cns.s.u-tokyo.ac.jp>
1 parent 9410f19 commit 6ee9184

7 files changed

Lines changed: 288 additions & 49 deletions

File tree

‎PWGLF/Core/K1AnalysisMicroCore.h‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include <limits>
4444
#include <set>
4545
#include <type_traits>
46+
#include <unordered_map>
4647
#include <utility>
4748
#include <vector>
4849

@@ -64,6 +65,16 @@ enum class K1TruthChannel {
6465
KStarPi = 2
6566
};
6667

68+
struct K1ParentTruth {
69+
double pt = std::numeric_limits<double>::quiet_NaN();
70+
double y = std::numeric_limits<double>::quiet_NaN();
71+
int pdgCode = 0;
72+
K1TruthChannel channel = K1TruthChannel::None;
73+
};
74+
using K1ParentTruthMap = std::unordered_map<int64_t, K1ParentTruth>;
75+
76+
inline K1TruthChannel classifyGeneratedK1(int charge, int daughter1, int daughter2);
77+
6778
// The value is the species index of the PID configuration in ResoAnalysisSelectionCore.
6879
enum class Species : int {
6980
Pion = 0,
@@ -217,6 +228,49 @@ K1TruthChannel classifyK1Truth(const Track& pion1, const Track& pion2, const Kao
217228
return K1TruthChannel::None;
218229
}
219230

231+
template <typename Kaon, typename Pion>
232+
int k1MotherId(K1TruthChannel channel, const Kaon& kaon, const Pion& pion1, const Pion& pion2)
233+
{
234+
if (channel == K1TruthChannel::RhoK) {
235+
return kaon.motherId();
236+
}
237+
if (channel == K1TruthChannel::KStarPi) {
238+
return std::abs(pion1.motherPDG()) == o2::constants::physics::Pdg::kK1_1270Plus ? pion1.motherId() : pion2.motherId();
239+
}
240+
return -1;
241+
}
242+
243+
template <typename ParentsType>
244+
K1ParentTruthMap makeK1ParentTruthMap(const ParentsType& parents)
245+
{
246+
K1ParentTruthMap result;
247+
for (const auto& part : parents) {
248+
if (std::abs(part.pdgCode()) != o2::constants::physics::Pdg::kK1_1270Plus || part.originalMcParticleId() < 0) {
249+
continue;
250+
}
251+
const auto id = static_cast<int64_t>(part.originalMcParticleId());
252+
const int charge = part.pdgCode() > 0 ? 1 : -1;
253+
result.emplace(id, K1ParentTruth{part.pt(), part.y(), part.pdgCode(), classifyGeneratedK1(charge, part.daughterPDG1(), part.daughterPDG2())});
254+
}
255+
return result;
256+
}
257+
258+
inline const K1ParentTruth* findK1ParentTruth(const K1ParentTruthMap& parents, int64_t motherId,
259+
K1TruthChannel channel, int charge)
260+
{
261+
const auto it = parents.find(motherId);
262+
if (it == parents.end()) {
263+
return nullptr;
264+
}
265+
const auto& parent = it->second;
266+
const int expectedPdg = charge * o2::constants::physics::Pdg::kK1_1270Plus;
267+
if (channel == K1TruthChannel::None || parent.channel != channel || parent.pdgCode != expectedPdg ||
268+
!std::isfinite(parent.pt) || parent.pt < 0. || !std::isfinite(parent.y)) {
269+
return nullptr;
270+
}
271+
return &parent;
272+
}
273+
220274
// Immediate decay channel of a generated K1 from the PDG codes of its two daughters.
221275
inline K1TruthChannel classifyGeneratedK1(int charge, int daughter1, int daughter2)
222276
{
@@ -635,7 +689,8 @@ class K1AnalysisMicroCore
635689
const int charge = part.pdgCode() > 0 ? 1 : -1;
636690
const K1TruthChannel channel = classifyGeneratedK1(charge, part.daughterPDG1(), part.daughterPDG2());
637691
histos.fill(HIST("CutFlow/generated"), 0, static_cast<int>(channel));
638-
if (part.y() < mCandidateCuts.cK1MinRap || part.y() > mCandidateCuts.cK1MaxRap) {
692+
if (!std::isfinite(part.pt()) || part.pt() < 0. || !std::isfinite(part.y()) ||
693+
part.y() < mCandidateCuts.cK1MinRap || part.y() > mCandidateCuts.cK1MaxRap) {
639694
continue;
640695
}
641696
histos.fill(HIST("CutFlow/generated"), 1, static_cast<int>(channel));

‎PWGLF/Core/Omega2012AnalysisCore.h‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ using o2::analysis::resonance::ResoAnalysisSelectionCore;
5858
using o2::analysis::resonance::TrackCuts;
5959
using o2::analysis::resonance::TrackStage;
6060

61+
// Omega(2012) analysis and ML export share these event-level selection defaults.
62+
// Keep the generic EventCuts defaults unchanged for other resonance tasks.
63+
inline EventCuts makeOmega2012EventCuts()
64+
{
65+
EventCuts cuts;
66+
cuts.cRecoINELgt0.value = true;
67+
cuts.cMCINELgt0.value = true;
68+
cuts.cMCVtxIn10.value = true;
69+
return cuts;
70+
}
71+
6172
enum class DecayMode : uint8_t {
6273
XiK0s = 0, // Omega(2012)- -> Xi- K0S
6374
Xi1530K = 1 // Omega(2012)- -> Xi(1530)0 K- -> Xi- pi+ K-
@@ -932,19 +943,20 @@ class Omega2012AnalysisCore
932943
}
933944
}
934945

935-
// Generated Omega(2012) parents of a selected reconstructed MC collision (ResoMCParents_001). The optional callback
936-
// receives (parent, immediate channel) for the parents inside the rapidity window. Split reconstructed collisions
937-
// repeat parent sets: this is not an unconditional generated denominator.
946+
// Generated Omega(2012) parents of a selected reconstructed MC collision (ResoMCParents_001). The caller supplies
947+
// the denominator rapidity limit so it can remain independent of a wider candidate-export window. The optional
948+
// callback receives (parent, immediate channel) for the parents inside that rapidity window. Split reconstructed
949+
// collisions repeat parent sets: this is not an unconditional generated denominator.
938950
template <typename ParentsType, typename Callback = std::nullptr_t>
939-
void forEachGeneratedOmega2012(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents, Callback callback = nullptr)
951+
void forEachGeneratedOmega2012(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents, float rapidityMax, Callback callback = nullptr)
940952
{
941953
for (const auto& part : resoParents) {
942954
if (std::abs(part.pdgCode()) != o2::constants::physics::Pdg::kOmega2012Minus) {
943955
continue;
944956
}
945957
const GeneratedChannel channel = classifyGeneratedOmega2012(part.pdgCode(), part.daughterPDG1(), part.daughterPDG2());
946958
histos.fill(HIST("CutFlow/generated"), 0, static_cast<int>(channel));
947-
if (!(std::abs(part.y()) < mCandidateCuts.cfgRapidityCut.value)) {
959+
if (!(std::abs(part.y()) < rapidityMax)) {
948960
continue;
949961
}
950962
histos.fill(HIST("CutFlow/generated"), 1, static_cast<int>(channel));

‎PWGLF/DataModel/LFK1MlTables.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ DECLARE_SOA_COLUMN(K1Charge, k1Charge, int8_t);
6363
// 16 = candidate cuts.
6464
// Candidates written at the "selected" export stage carry 31.
6565
DECLARE_SOA_COLUMN(K1BaselinePassBits, k1BaselinePassBits, uint16_t);
66+
// NOLINTNEXTLINE(modernize-avoid-c-arrays) -- persistent fixed-size AOD column
6667
DECLARE_SOA_COLUMN(K1MasterFeatures, k1MasterFeatures, float[125]); //! 125 master features of the K1 ML feature contract (PWGLF/Core/K1MlFeatures.h)
6768
DECLARE_SOA_COLUMN(K1FeatureStatus, k1FeatureStatus, uint8_t); //! o2::analysis::k1ml::BuildStatus; only Ok (0) rows are written
6869
DECLARE_SOA_COLUMN(K1TruthStatus, k1TruthStatus, uint8_t); //! 0 data, 1 matched, 2 unmatched
6970
DECLARE_SOA_COLUMN(K1TruthChannel, k1TruthChannel, uint8_t); //! 0 none, 1 rho K, 2 K* pi
7071
DECLARE_SOA_COLUMN(K1MotherPdg, k1MotherPdg, int32_t);
7172
DECLARE_SOA_COLUMN(K1MotherId, k1MotherId, int64_t);
72-
DECLARE_SOA_COLUMN(K1GeneratedPt, k1GeneratedPt, float); //! NaN in K1MlTruth: not filled for reconstructed candidates
73-
DECLARE_SOA_COLUMN(K1GeneratedY, k1GeneratedY, float); //! NaN in K1MlTruth: not filled for reconstructed candidates
73+
DECLARE_SOA_COLUMN(K1GeneratedPt, k1GeneratedPt, float); //! Parent-table pT for a validated reconstructed MC match; NaN otherwise
74+
DECLARE_SOA_COLUMN(K1GeneratedY, k1GeneratedY, float); //! Parent-table y for a validated reconstructed MC match; NaN otherwise
7475
DECLARE_SOA_COLUMN(K1OriginalMcParticleId, k1OriginalMcParticleId, int64_t);
7576
DECLARE_SOA_COLUMN(K1DaughterPdg1, k1DaughterPdg1, int32_t);
7677
DECLARE_SOA_COLUMN(K1DaughterPdg2, k1DaughterPdg2, int32_t);

‎PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ struct HistogramOptions : ConfigurableGroup {
5252
Configurable<int> cNbinsDiv{"cNbinsDiv", 1, "Integer to divide the number of bins"};
5353
Configurable<bool> additionalQAplots{"additionalQAplots", true, "Additional QA plots"};
5454
Configurable<int> cfgTruthDebug{"cfgTruthDebug", 0, "Maximum logged matched candidates per truth channel"};
55+
Configurable<bool> cUseTruthRapidity{"cUseTruthRapidity", true, "Require generated K1 rapidity in the configured window for MC efficiency numerators"};
5556
};
5657

5758
enum BinAnti : unsigned int {
@@ -231,12 +232,15 @@ struct K1AnalysisMicro {
231232
AxisSpec channelAxis = {3, -0.5, 2.5, "0: non-K1, 1: rho K, 2: K* pi"};
232233
histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}});
233234
histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}});
234-
histos.add("MCReco/channel", "All selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis});
235+
// These channel and pT histograms are pre-truth-fiducial QA; MCReco/response is the validated numerator.
236+
histos.add("MCReco/channel", "Pre-truth-fiducial QA: selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis});
235237
histos.add("MCReco/mass", "Reconstructed mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisReso});
236-
histos.add("MCReco/pt", "Reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis});
238+
histos.add("MCReco/pt", "Pre-truth-fiducial QA: reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis});
237239
histos.add("MCReco/piPiMass", "pi-pi mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisRho});
238240
histos.add("MCReco/pi1KMass", "First pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892});
239241
histos.add("MCReco/pi2KMass", "Second pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892});
242+
histos.add("MCReco/response", "Selected MC candidates;reconstructed pT;generated pT;channel;charge;centrality", HistType::kTHnSparseD,
243+
{ptAxis, ptAxis, channelAxis, AxisSpec{2, -1.5, 1.5, "K1 charge"}, centAxis});
240244
histos.add("k1invmass_MC", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso});
241245
histos.add("k1invmass_MC_noK1", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso});
242246

@@ -272,6 +276,8 @@ struct K1AnalysisMicro {
272276
AxisSpec channelAxis = {3, -0.5, 2.5, "0: other/unresolved, 1: rho K, 2: K* pi"};
273277
histos.add("MCGen/chargeChannel", "K1 parents in selected reconstructed events, inside the K1 rapidity window", HistType::kTH2D, {{2, -1.5, 1.5, "K1 charge"}, channelAxis});
274278
histos.add("MCGen/ptChannel", "Generated K1 pT by immediate decay channel", HistType::kTH2D, {channelAxis, ptAxis});
279+
histos.add("MCGen/response", "Generated K1 parents;generated pT;channel;charge;centrality", HistType::kTHnSparseD,
280+
{ptAxis, channelAxis, AxisSpec{2, -1.5, 1.5, "K1 charge"}, centAxis});
275281
}
276282
}
277283

@@ -380,7 +386,8 @@ struct K1AnalysisMicro {
380386
// (or one mixed pair of collisions). The selection itself is the shared K1 core.
381387
// dTracks1: bachelor kaons, dTracks2: pions.
382388
template <bool IsMC, bool IsMix, bool IsResoMicrotrack, typename CollisionType, typename TracksType>
383-
void fillHistograms(const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2)
389+
void fillHistograms(const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2,
390+
const K1ParentTruthMap* parentTruth = nullptr)
384391
{
385392
const bool fillQA = !IsMix && histogramOptions.additionalQAplots;
386393
const auto multiplicity = collision.cent();
@@ -474,9 +481,17 @@ struct K1AnalysisMicro {
474481
pion1.motherPDG(), pion2.motherPDG(), kaon.motherPDG(),
475482
pion1.siblingIds()[0], pion1.siblingIds()[1], pion2.siblingIds()[0], pion2.siblingIds()[1], kaon.siblingIds()[0], kaon.siblingIds()[1]);
476483
}
477-
typeK1 = kaon.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec;
478-
histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, c.k1.Pt(), c.k1.M());
479-
histos.fill(HIST("k1invmass_MC"), c.k1.M());
484+
const int motherId = k1MotherId(channel, kaon, pion1, pion2);
485+
const int charge = kaon.pdgCode() > 0 ? 1 : -1;
486+
const auto* parent = parentTruth ? findK1ParentTruth(*parentTruth, motherId, channel, charge) : nullptr;
487+
const bool truthRapidity = c.isUnlikeSign && parent && (!histogramOptions.cUseTruthRapidity || (parent->y >= candidateCuts.cK1MinRap && parent->y <= candidateCuts.cK1MaxRap));
488+
if (truthRapidity) {
489+
typeK1 = kaon.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec;
490+
histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, c.k1.Pt(), c.k1.M());
491+
histos.fill(HIST("k1invmass_MC"), c.k1.M());
492+
const int parentCharge = parent->pdgCode > 0 ? 1 : -1;
493+
histos.fill(HIST("MCReco/response"), c.k1.Pt(), parent->pt, channelBin, parentCharge, multiplicity);
494+
}
480495
histos.fill(HIST("QAMC/K1OA"), c.angle);
481496
histos.fill(HIST("QAMC/K1PairAsym"), c.pairAsym);
482497
histos.fill(HIST("QAMC/hInvmassK892_Rho"), c.mass13, c.secondary.M());
@@ -515,17 +530,18 @@ struct K1AnalysisMicro {
515530
PROCESS_SWITCH(K1AnalysisMicro, processResoMicroTracks, "Process ResoMicroTracks", true);
516531

517532
void processMC(ResoMCCols::iterator const& collision,
518-
ResoMCTracks const& resotracks)
533+
ResoMCTracks const& resotracks, ResoMCParents const& resoParents)
519534
{
520535
if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) {
521536
return;
522537
}
523538
histos.fill(HIST("MCReco/collisions"), 0.5);
524-
fillHistograms<true, false, false>(collision, resotracks, resotracks);
539+
const auto parentTruth = makeK1ParentTruthMap(resoParents);
540+
fillHistograms<true, false, false>(collision, resotracks, resotracks, &parentTruth);
525541
}
526542
PROCESS_SWITCH(K1AnalysisMicro, processMC, "Process Event for MC", false);
527543

528-
void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks)
544+
void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks, ResoMCParents const& resoParents)
529545
{
530546
// The modular producer already selected these reconstructed collisions.
531547
// Apply precisely the same reconstruction loop as the data baseline.
@@ -534,7 +550,8 @@ struct K1AnalysisMicro {
534550
}
535551
histos.fill(HIST("MCReco/collisions"), 0.5);
536552
histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size());
537-
fillHistograms<true, false, true>(collision, tracks, tracks);
553+
const auto parentTruth = makeK1ParentTruthMap(resoParents);
554+
fillHistograms<true, false, true>(collision, tracks, tracks, &parentTruth);
538555
}
539556
PROCESS_SWITCH(K1AnalysisMicro, processMCMicro, "Process reconstructed MC with micro v001 tables", false);
540557

@@ -548,6 +565,9 @@ struct K1AnalysisMicro {
548565
const int charge = part.pdgCode() > 0 ? 1 : -1;
549566
histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast<int>(channel));
550567
histos.fill(HIST("MCGen/ptChannel"), static_cast<int>(channel), part.pt());
568+
if (channel != K1TruthChannel::None) {
569+
histos.fill(HIST("MCGen/response"), part.pt(), static_cast<int>(channel), charge, collision.cent());
570+
}
551571
});
552572
}
553573
PROCESS_SWITCH(K1AnalysisMicro, processMCTrue, "Process generated K1 in selected events with v001 parents", false);

‎PWGLF/Tasks/Resonances/k1TrainingTable.cxx‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ struct K1TrainingTable {
182182

183183
template <bool IsMC, typename Collision, typename Kaon, typename Pion>
184184
void writeK1MlCandidate(Collision const& collision, Kaon const& kaon, Pion const& samePion, Pion const& oppPion,
185-
K1TruthChannel channel, uint16_t passBits)
185+
K1TruthChannel channel, uint16_t passBits, const K1ParentTruthMap* parentTruth = nullptr)
186186
{
187187
using o2::analysis::k1ml::BuildStatus;
188188
const auto canonical = o2::analysis::k1ml::canonicalizeUS(o2::analysis::k1ml::makeTrackSnapshot(kaon), o2::analysis::k1ml::makeTrackSnapshot(samePion), o2::analysis::k1ml::makeTrackSnapshot(oppPion));
@@ -215,14 +215,17 @@ struct K1TrainingTable {
215215
if constexpr (IsMC) {
216216
const bool matched = channel != K1TruthChannel::None;
217217
int motherId = -1;
218+
const K1ParentTruth* parent = nullptr;
218219
if (matched) {
219-
motherId = channel == K1TruthChannel::RhoK ? kaon.motherId() : std::abs(samePion.motherPDG()) == Pdg::kK1_1270Plus ? samePion.motherId()
220-
: oppPion.motherId();
220+
motherId = k1MotherId(channel, kaon, samePion, oppPion);
221+
const int charge = kaon.pdgCode() > 0 ? 1 : -1;
222+
parent = parentTruth ? findK1ParentTruth(*parentTruth, motherId, channel, charge) : nullptr;
221223
}
222224
// Immediate-mother information alone does not prove a physical UID.
223225
k1MlTruth(row, matched ? uint8_t{1} : uint8_t{2}, static_cast<uint8_t>(channel),
224-
matched ? kaon.sign() * Pdg::kK1_1270Plus : 0, static_cast<int64_t>(motherId),
225-
std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::quiet_NaN());
226+
parent ? parent->pdgCode : (matched ? kaon.sign() * Pdg::kK1_1270Plus : 0), static_cast<int64_t>(motherId),
227+
parent ? static_cast<float>(parent->pt) : std::numeric_limits<float>::quiet_NaN(),
228+
parent ? static_cast<float>(parent->y) : std::numeric_limits<float>::quiet_NaN());
226229
} else {
227230
k1MlTruth(row, uint8_t{0}, uint8_t{0}, 0, int64_t{-1},
228231
std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::quiet_NaN());
@@ -244,7 +247,7 @@ struct K1TrainingTable {
244247
}
245248
PROCESS_SWITCH(K1TrainingTable, processResoMicroTracks, "Write K1 candidates from data micro v001 tables", true);
246249

247-
void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks)
250+
void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks, ResoMCParents const& resoParents)
248251
{
249252
// The modular producer already selected these reconstructed collisions.
250253
if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) {
@@ -253,10 +256,11 @@ struct K1TrainingTable {
253256
histos.fill(HIST("MCReco/collisions"), 0.5);
254257
histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size());
255258
writeK1MlEvent(collision);
259+
const auto parentTruth = makeK1ParentTruthMap(resoParents);
256260
core.forEachCandidate<true, false, true>(histos, collision, tracks, tracks, false, nullptr, nullptr,
257-
[this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion,
258-
K1TruthChannel channel, uint16_t passBits) {
259-
writeK1MlCandidate<true>(coll, kaon, samePion, oppPion, channel, passBits);
261+
[this, &parentTruth](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion,
262+
K1TruthChannel channel, uint16_t passBits) {
263+
writeK1MlCandidate<true>(coll, kaon, samePion, oppPion, channel, passBits, &parentTruth);
260264
});
261265
}
262266
PROCESS_SWITCH(K1TrainingTable, processMCMicro, "Write K1 candidates with truth from reconstructed MC micro v001 tables", false);

0 commit comments

Comments
 (0)