From abdac64b9d2c48394e6ee9d6e96e1422fa3d5f96 Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Wed, 30 Sep 2026 06:23:11 +0900 Subject: [PATCH 1/9] [PWGLF] Modernize K1 micro analysis track selection - Use v001 resonance tables (ResoCollisions_001, ResoMicroTracks_001, ResoMCMicroTracks_001, ResoMCParents_001); keep ResoTracks as fallback - Unify full/micro track selection and PID in a single code path, handling quantised v001 DCA/nSigma values and producer pT-dependent DCA bits - Apply pion PID on micro tracks; apply cUseOnlyTOFTrackPi to both pions and cUseOnlyTOFTrackKa to the kaon; add cByPassTOF and optional pT-dependent PID/DCA - Restore secondary-resonance and K1 candidate cuts (mass window, other-pair masses, opening angle, pair asymmetry); -999 disables a cut and skips its computation; fill QAcut and kaon QA - Iterate unordered pion pairs to stop double-filling triplets, with a canonical pion assignment for role-dependent quantities - Cache per-collision selections, record cut-flow from the selection functions, add event cuts and init-time config validation - Replace TLorentzVector with ROOT::Math vectors, use PDG constants, remove unused options --- PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx | 1382 ++++++++++++++------ 1 file changed, 999 insertions(+), 383 deletions(-) diff --git a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx index 0cbab0d427c..4f1d4d58496 100644 --- a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx +++ b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx @@ -28,13 +28,21 @@ #include #include #include +#include #include #include #include -#include -#include // FIXME +#include +#include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) +#include +#include +#include +#include +#include +#include +#include #include using namespace o2; @@ -43,9 +51,17 @@ using namespace o2::framework::expressions; using namespace o2::soa; using namespace o2::constants::physics; using namespace o2::constants::math; -; struct K1AnalysisMicro { + // Module-initializer v001 tables; full tracks keep their unversioned schema as a fallback. + using ResoCollisions = aod::ResoCollisions_001; + using ResoMCCols = soa::Join; + using ResoTracks = aod::ResoTracks; // no v001 exists; K1 does not need ResoTrackTracks (trackId unused) + using ResoMicroTracks = aod::ResoMicroTracks_001; + using ResoMCTracks = soa::Join; + using ResoMCMicroTracks = soa::Join; + using ResoMCParents = aod::ResoMCParents_001; + enum BinAnti : unsigned int { kNormal = 0, kAnti, @@ -68,11 +84,53 @@ struct K1AnalysisMicro { kK1N_Rec, kTYEnd }; + enum class Species : int { + Pion = 0, + Kaon = 1 + }; + // Last stage passed by a track; the cut-flow histogram is filled directly from this value. + enum TrackStage : int { + kTrkInput = 0, + kTrkPt, + kTrkEta, + kTrkDCAxy, + kTrkDCAz, + kTrkFlags, + kTrkClusters, + kTrkTOFRequired, + kTrkPID, + kTrkNStages + }; + enum class QAFolder { + Before, // QA/*: before the candidate cuts + After, // QAcut/*: after the candidate cuts + MC // QAMC/*: matched K1 truth candidates + }; + // Resolved PID cut of one species at a given pT. + struct PIDCut { + double tpcMax = 0.; + double tofMax = 0.; + double combined = 0.; + bool tofRequired = false; + }; + + static constexpr float DisabledCut = -999.f; // an optional cut with this value is off and not evaluated + static constexpr double MassRho770 = 0.77526; // PDG 2024, not available in o2::constants::physics + static constexpr double DCAGridStep = 0.025; // v001 micro DCA encoding, lower-inclusive bins up to DCAGridMax + static constexpr double DCAGridMax = 0.15; + static constexpr double PIDGridStart = 2.0; // v001 micro nSigma encoding: 0.25 bins in [2.0, 3.5] + static constexpr double PIDGridStep = 0.25; + static constexpr double PIDGridMax = 3.5; + static constexpr double GridTolerance = 1e-4; + static constexpr int NCandidateStages = 12; + SliceCache cache; - Preslice perRCol = aod::resodaughter::resoCollisionId; - Preslice perCollision = aod::track::collisionId; + // Registered only to enable the slice cache that SameKindPair (event mixing) needs, as in Xi1820Analysis + Preslice perResoCollisionTrack = aod::resodaughter::resoCollisionId; + Preslice perResoCollisionMicroTrack = aod::resodaughter::resoCollisionId; HistogramRegistry histos{"histos", {}, OutputObjHandlingPolicy::AnalysisObject}; - using ResoMCCols = soa::Join; + Configurable cfgTruthDebug{"cfgTruthDebug", 0, "Maximum logged matched candidates per truth channel"}; + std::array truthDebugCounts{}; //// Configurables Configurable cNbinsDiv{"cNbinsDiv", 1, "Integer to divide the number of bins"}; @@ -80,60 +138,252 @@ struct K1AnalysisMicro { Configurable nEvtMixing{"nEvtMixing", 5, "Number of events to mix"}; ConfigurableAxis cfgVtxBins{"cfgVtxBins", {VARIABLE_WIDTH, -10.0f, -8.f, -6.f, -4.f, -2.f, 0.f, 2.f, 4.f, 6.f, 8.f, 10.f}, "Mixing bins - z-vertex"}; ConfigurableAxis cfgMultBins{"cfgMultBins", {VARIABLE_WIDTH, 0.0f, 20.0f, 40.0f, 60.0f, 80.0f, 100.0f, 200.0f, 99999.f}, "Mixing bins - multiplicity"}; - /// Pre-selection cuts - Configurable cMinPtcut{"cMinPtcut", 0.15, "Track minium pt cut"}; - /// DCA Selections - // DCAr to PV - Configurable cMaxDCArToPVcut{"cMaxDCArToPVcut", 0.1, "Track DCAr cut to PV Maximum"}; - // DCAz to PV - Configurable cMaxDCAzToPVcut{"cMaxDCAzToPVcut", 0.1, "Track DCAz cut to PV Maximum"}; - Configurable cMinDCAzToPVcut{"cMinDCAzToPVcut", 0.0, "Track DCAz cut to PV Minimum"}; + // Event selection (a group without prefix keeps the plain key names) + struct : ConfigurableGroup { + Configurable cRecoINELgt0{"cRecoINELgt0", false, "Apply reconstructed INEL>0 selection"}; + Configurable cMCINELgt0{"cMCINELgt0", false, "Require generator INEL>0 in MC processes"}; + Configurable cMCVtxIn10{"cMCVtxIn10", false, "Require generator |vz| < 10 cm in MC processes"}; + } eventCuts; + + /// Track selections (common for pion and kaon, -999 switches an optional cut off) + struct : ConfigurableGroup { + Configurable cMinPtcut{"cMinPtcut", 0.15, "Track minium pt cut"}; + Configurable cMaxEtacut{"cMaxEtacut", -999.f, "Track maximum |eta| cut (-999: off)"}; + // DCAr to PV + Configurable cMaxDCArToPVcut{"cMaxDCArToPVcut", 0.1, "Track DCAr cut to PV Maximum"}; + // DCAz to PV + Configurable cMaxDCAzToPVcut{"cMaxDCAzToPVcut", 0.1, "Track DCAz cut to PV Maximum"}; + Configurable cMinDCAzToPVcut{"cMinDCAzToPVcut", 0.0, "Track DCAz cut to PV Minimum"}; + Configurable cfgUsePtDepDCA{"cfgUsePtDepDCA", false, "Use pT dependent DCA cut instead of the fixed maximum"}; + Configurable cDCAToPVByPtP0{"cDCAToPVByPtP0", 0.004f, "pT dependent DCA cut = P0 + coefficient / pT^power (cm)"}; + Configurable cDCAToPVByPtCoeff{"cDCAToPVByPtCoeff", 0.013f, "Coefficient in the pT dependent DCA cut"}; + Configurable cDCAToPVByPtPower{"cDCAToPVByPtPower", 1.f, "Power in the pT dependent DCA cut"}; + Configurable cfgPrimaryTrack{"cfgPrimaryTrack", true, "Primary track selection"}; // kGoldenChi2 | kDCAxy | kDCAz + Configurable cfgGlobalWoDCATrack{"cfgGlobalWoDCATrack", true, "Global track selection without DCA"}; // kQualityTracks (kTrackType | kTPCNCls | kTPCCrossedRows | kTPCCrossedRowsOverNCls | kTPCChi2NDF | kTPCRefit | kITSNCls | kITSChi2NDF | kITSRefit | kITSHits) | kInAcceptanceTracks (kPtRange | kEtaRange) + Configurable cfgGlobalTrack{"cfgGlobalTrack", false, "Global track selection"}; // kGoldenChi2 | kDCAxy | kDCAz + Configurable cfgPVContributor{"cfgPVContributor", false, "PV contributor track selection"}; // PV Contriuibutor + Configurable cfgUseTPCRefit{"cfgUseTPCRefit", false, "Require TPC Refit"}; + Configurable cfgUseITSRefit{"cfgUseITSRefit", false, "Require ITS Refit"}; + Configurable cfgTPCcluster{"cfgTPCcluster", 0, "Number of TPC cluster (found clusters, ResoTracks only)"}; + Configurable cfgTPCCrossedRowsMin{"cfgTPCCrossedRowsMin", 0, "Minimum number of TPC crossed rows"}; + Configurable cfgITSNClsMin{"cfgITSNClsMin", 0, "Minimum number of ITS clusters (ResoMicroTracks only)"}; + Configurable cfgHasTOF{"cfgHasTOF", false, "Require TOF"}; + } trackCuts; /// PID Selections - Configurable cMaxTPCnSigmaPion{"cMaxTPCnSigmaPion", 3.0, "TPC nSigma cut for Pion"}; // TPC - Configurable cMaxTOFnSigmaPion{"cMaxTOFnSigmaPion", 3.0, "TOF nSigma cut for Pion"}; // TOF - Configurable nsigmaCutCombinedPion{"nsigmaCutCombinedPion", -999, "Combined nSigma cut for Pion"}; // Combined - Configurable cTOFVeto{"cTOFVeto", true, "TOF Veto, if false, TOF is nessessary for PID selection"}; // TOF Veto - Configurable cUseOnlyTOFTrackPi{"cUseOnlyTOFTrackPi", false, "Use only TOF track for PID selection"}; // Use only TOF track for Pion PID selection - // Kaon - Configurable cMaxTPCnSigmaKaon{"cMaxTPCnSigmaKaon", 3.0, "TPC nSigma cut for Kaon"}; // TPC - Configurable cMaxTOFnSigmaKaon{"cMaxTOFnSigmaKaon", 3.0, "TOF nSigma cut for Kaon"}; // TOF - Configurable nsigmaCutCombinedKaon{"nsigmaCutCombinedKaon", -999, "Combined nSigma cut for Kaon"}; // Combined - Configurable cUseOnlyTOFTrackKa{"cUseOnlyTOFTrackKa", false, "Use only TOF track for PID selection"}; // Use only TOF track for Kaon PID selection - // Track selections - Configurable cfgPrimaryTrack{"cfgPrimaryTrack", true, "Primary track selection"}; // kGoldenChi2 | kDCAxy | kDCAz - Configurable cfgGlobalWoDCATrack{"cfgGlobalWoDCATrack", true, "Global track selection without DCA"}; // kQualityTracks (kTrackType | kTPCNCls | kTPCCrossedRows | kTPCCrossedRowsOverNCls | kTPCChi2NDF | kTPCRefit | kITSNCls | kITSChi2NDF | kITSRefit | kITSHits) | kInAcceptanceTracks (kPtRange | kEtaRange) - Configurable cfgGlobalTrack{"cfgGlobalTrack", false, "Global track selection"}; // kGoldenChi2 | kDCAxy | kDCAz - Configurable cfgPVContributor{"cfgPVContributor", false, "PV contributor track selection"}; // PV Contriuibutor + Configurable cByPassTOF{"cByPassTOF", false, "Bypass the TOF nSigma selection"}; + struct : ConfigurableGroup { + Configurable cMaxTPCnSigmaPion{"cMaxTPCnSigmaPion", 3.0, "TPC nSigma cut for Pion (-999: off)"}; // TPC + Configurable cMaxTOFnSigmaPion{"cMaxTOFnSigmaPion", 3.0, "TOF nSigma cut for Pion (-999: off)"}; // TOF + Configurable nsigmaCutCombinedPion{"nsigmaCutCombinedPion", -999, "Combined nSigma cut for Pion"}; // Combined + Configurable cUseOnlyTOFTrackPi{"cUseOnlyTOFTrackPi", false, "Use only TOF track for PID selection"}; // Use only TOF track for Pion PID selection + Configurable cPionUsePtDepPID{"cPionUsePtDepPID", false, "Use pT-dependent PID cuts for pion"}; + Configurable> cPionPIDPtBins{"cPionPIDPtBins", {0.0f, 0.5f, 0.8f, 2.0f, 999.0f}, "pT bin edges for pion PID cuts"}; + Configurable> cPionTPCNSigmaCuts{"cPionTPCNSigmaCuts", {3.0f, 3.0f, 2.0f, 2.0f}, "TPC NSigma cuts per pT bin (pion)"}; + Configurable> cPionTOFNSigmaCuts{"cPionTOFNSigmaCuts", {3.0f, 3.0f, 3.0f, 3.0f}, "TOF NSigma cuts per pT bin (pion)"}; + Configurable> cPionTOFRequired{"cPionTOFRequired", {0, 0, 1, 1}, "Require TOF per pT bin (pion)"}; + } pionPID; + struct : ConfigurableGroup { + Configurable cMaxTPCnSigmaKaon{"cMaxTPCnSigmaKaon", 3.0, "TPC nSigma cut for Kaon (-999: off)"}; // TPC + Configurable cMaxTOFnSigmaKaon{"cMaxTOFnSigmaKaon", 3.0, "TOF nSigma cut for Kaon (-999: off)"}; // TOF + Configurable nsigmaCutCombinedKaon{"nsigmaCutCombinedKaon", -999, "Combined nSigma cut for Kaon"}; // Combined + Configurable cUseOnlyTOFTrackKa{"cUseOnlyTOFTrackKa", false, "Use only TOF track for PID selection"}; // Use only TOF track for Kaon PID selection + Configurable cKaonUsePtDepPID{"cKaonUsePtDepPID", false, "Use pT-dependent PID cuts for kaon"}; + Configurable> cKaonPIDPtBins{"cKaonPIDPtBins", {0.0f, 0.5f, 0.8f, 2.0f, 999.0f}, "pT bin edges for kaon PID cuts"}; + Configurable> cKaonTPCNSigmaCuts{"cKaonTPCNSigmaCuts", {3.0f, 3.0f, 2.0f, 2.0f}, "TPC NSigma cuts per pT bin (kaon)"}; + Configurable> cKaonTOFNSigmaCuts{"cKaonTOFNSigmaCuts", {3.0f, 3.0f, 3.0f, 3.0f}, "TOF NSigma cuts per pT bin (kaon)"}; + Configurable> cKaonTOFRequired{"cKaonTOFRequired", {0, 0, 1, 1}, "Require TOF per pT bin (kaon)"}; + } kaonPID; + Configurable additionalQAplots{"additionalQAplots", true, "Additional QA plots"}; - Configurable additionalEvsel{"additionalEvsel", true, "Additional event selcection"}; - Configurable cfgTPCcluster{"cfgTPCcluster", 0, "Number of TPC cluster"}; - Configurable cfgUseTPCRefit{"cfgUseTPCRefit", false, "Require TPC Refit"}; - Configurable cfgUseITSRefit{"cfgUseITSRefit", false, "Require ITS Refit"}; - Configurable cfgHasTOF{"cfgHasTOF", false, "Require TOF"}; - - // Secondary selection - Configurable cMinSecondaryPtCut{"cMinSecondaryPtCut", 0.5, "Min pT cut for secondary selection"}; - /* - Configurable cfgModeK892orRho{"cfgModeK892orRho", false, "Secondary scenario for K892 (true) or Rho (false)"}; - Configurable cSecondaryMasswindow{"cSecondaryMasswindow", 0.1, "Secondary inv mass selection window"}; - Configurable cMinAnotherSecondaryMassCut{"cMinAnotherSecondaryMassCut", 0, "Min inv. mass selection of another secondary scenario"}; - Configurable cMaxAnotherSecondaryMassCut{"cMaxAnotherSecondaryMassCut", 999, "MAx inv. mass selection of another secondary scenario"}; - Configurable cMinPiKaMassCut{"cMinPiKaMassCut", 0, "bPion-Kaon pair inv mass selection minimum"}; - Configurable cMaxPiKaMassCut{"cMaxPiKaMassCut", 999, "bPion-Kaon pair inv mass selection maximum"}; - Configurable cMinAngle{"cMinAngle", 0, "Minimum angle between K(892)0 and bachelor pion"}; - Configurable cMaxAngle{"cMaxAngle", 4, "Maximum angle between K(892)0 and bachelor pion"}; - Configurable cMinPairAsym{"cMinPairAsym", -1, "Minimum pair asymmetry"}; - Configurable cMaxPairAsym{"cMaxPairAsym", 1, "Maximum pair asymmetry"}; -*/ + + // Secondary selection (-999 switches a cut off; the values it needs are then not computed) + struct : ConfigurableGroup { + Configurable cMinSecondaryPtCut{"cMinSecondaryPtCut", 0.5, "Min pT cut for secondary selection"}; + Configurable cfgModeK892orRho{"cfgModeK892orRho", false, "Secondary scenario for K892 (true) or Rho (false)"}; + Configurable cSecondaryMasswindow{"cSecondaryMasswindow", -999, "Secondary inv mass selection window"}; + Configurable cMinAnotherSecondaryMassCut{"cMinAnotherSecondaryMassCut", -999, "Min inv. mass selection of another secondary scenario"}; + Configurable cMaxAnotherSecondaryMassCut{"cMaxAnotherSecondaryMassCut", -999, "MAx inv. mass selection of another secondary scenario"}; + Configurable cMinPiKaMassCut{"cMinPiKaMassCut", -999, "bPion-Kaon pair inv mass selection minimum"}; + Configurable cMaxPiKaMassCut{"cMaxPiKaMassCut", -999, "bPion-Kaon pair inv mass selection maximum"}; + Configurable cMinAngle{"cMinAngle", -999, "Minimum angle between the secondary resonance and the bachelor"}; + Configurable cMaxAngle{"cMaxAngle", -999, "Maximum angle between the secondary resonance and the bachelor"}; + Configurable cMinPairAsym{"cMinPairAsym", -999, "Minimum pair asymmetry"}; + Configurable cMaxPairAsym{"cMaxPairAsym", -999, "Maximum pair asymmetry"}; + } secondaryCuts; // K1 selection Configurable cK1MaxRap{"cK1MaxRap", 0.5, "K1 maximum rapidity"}; Configurable cK1MinRap{"cK1MinRap", -0.5, "K1 minimum rapidity"}; + // A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value). + static bool isCutEnabled(float value) + { + return value > DisabledCut + 1.f; + } + + // v001 micro values are lower-inclusive bin edges: a maximum cut on the grid keeps bins below it. + static bool passesBinnedMax(double decoded, double cut) + { + return decoded < cut - Epsilon; + } + + // Minimum cut on the grid keeps the bin starting at the cut. + static bool passesBinnedMin(double decoded, double cut) + { + return decoded >= cut - Epsilon; + } + + template + static bool passesMax(double value, double cut) + { + if constexpr (IsResoMicrotrack) { + return passesBinnedMax(value, cut); + } else { + return value < cut; + } + } + + static bool isInRange(double value, double minimum, double maximum) + { + if (isCutEnabled(minimum) && value < minimum) { + return false; + } + if (isCutEnabled(maximum) && value > maximum) { + return false; + } + return true; + } + + static bool isInWindow(double value, double center, double width) + { + return std::abs(value - center) < width; + } + + // Preserve pT-bin membership [low, high). + static int getPtBinIndex(float pt, const std::vector& ptBins) + { + for (std::size_t i = 1; i < ptBins.size(); ++i) { + if (pt >= ptBins[i - 1] && pt < ptBins[i]) { + return static_cast(i - 1); + } + } + return -1; + } + + // Derived once in init(): which candidate cuts are switched on. + bool secondaryWindowOn = false; + bool anotherMassCutOn = false; + bool piKaMassCutOn = false; + bool angleCutOn = false; + bool pairAsymCutOn = false; + void init(o2::framework::InitContext&) { + const int sameEventModes = static_cast(doprocessResoTracks) + static_cast(doprocessResoMicroTracks) + + static_cast(doprocessMC) + static_cast(doprocessMCMicro); + const int mixedEventModes = static_cast(doprocessME) + static_cast(doprocessMEMicro); + if (sameEventModes > 1 || mixedEventModes > 1) { + LOG(fatal) << "Enable at most one same-event mode and one mixing mode"; + } + + secondaryWindowOn = isCutEnabled(secondaryCuts.cSecondaryMasswindow); + anotherMassCutOn = isCutEnabled(secondaryCuts.cMinAnotherSecondaryMassCut) || isCutEnabled(secondaryCuts.cMaxAnotherSecondaryMassCut); + piKaMassCutOn = isCutEnabled(secondaryCuts.cMinPiKaMassCut) || isCutEnabled(secondaryCuts.cMaxPiKaMassCut); + angleCutOn = isCutEnabled(secondaryCuts.cMinAngle) || isCutEnabled(secondaryCuts.cMaxAngle); + pairAsymCutOn = isCutEnabled(secondaryCuts.cMinPairAsym) || isCutEnabled(secondaryCuts.cMaxPairAsym); + + // Consistency of the pT dependent PID configuration + if (pionPID.cPionUsePtDepPID) { + const auto& bins = pionPID.cPionPIDPtBins.value; + if (bins.size() < 2 || pionPID.cPionTPCNSigmaCuts.value.size() != bins.size() - 1 || + pionPID.cPionTOFNSigmaCuts.value.size() != bins.size() - 1 || pionPID.cPionTOFRequired.value.size() != bins.size() - 1) { + LOG(fatal) << "Pion pT dependent PID vectors must have (number of pT bin edges - 1) entries"; + } + } + if (kaonPID.cKaonUsePtDepPID) { + const auto& bins = kaonPID.cKaonPIDPtBins.value; + if (bins.size() < 2 || kaonPID.cKaonTPCNSigmaCuts.value.size() != bins.size() - 1 || + kaonPID.cKaonTOFNSigmaCuts.value.size() != bins.size() - 1 || kaonPID.cKaonTOFRequired.value.size() != bins.size() - 1) { + LOG(fatal) << "Kaon pT dependent PID vectors must have (number of pT bin edges - 1) entries"; + } + } + if (cByPassTOF && (pionPID.cUseOnlyTOFTrackPi || kaonPID.cUseOnlyTOFTrackKa)) { + LOG(warning) << "cByPassTOF skips the TOF nSigma cut, but cUseOnlyTOFTrack* still requires a TOF signal"; + } + + // Micro tracks store quantised DCA and nSigma: a cut off the grid would silently act as a different cut. + if (doprocessResoMicroTracks || doprocessMCMicro || doprocessMEMicro) { + auto checkDCAGrid = [](const char* name, double cut) { + const double nearest = std::min(std::max(std::round(cut / DCAGridStep) * DCAGridStep, 0.), DCAGridMax); + if (std::abs(cut - nearest) > GridTolerance) { + LOG(fatal) << name << " = " << cut << " is not on the quantised DCA grid (multiples of " << DCAGridStep << " up to " << DCAGridMax << "); nearest value: " << nearest; + } + }; + auto checkPIDGrid = [](const char* name, double cut) { + const double nearest = std::min(std::max(PIDGridStart + std::round((cut - PIDGridStart) / PIDGridStep) * PIDGridStep, PIDGridStart), PIDGridMax); + if (std::abs(cut - nearest) > GridTolerance) { + LOG(fatal) << name << " = " << cut << " is not on the quantised nSigma grid ({2.0, 2.25, ..., 3.5}); nearest value: " << nearest; + } + }; + if (trackCuts.cfgUsePtDepDCA) { + LOG(info) << "Micro tracks use the producer pT dependent DCA flags (0.004 + 0.013 / pT); cDCAToPVByPt* are ignored"; + if (std::abs(trackCuts.cDCAToPVByPtP0 - 0.004f) > 1e-6f || std::abs(trackCuts.cDCAToPVByPtCoeff - 0.013f) > 1e-6f || std::abs(trackCuts.cDCAToPVByPtPower - 1.f) > 1e-6f) { + LOG(warning) << "cDCAToPVByPt* differ from the producer defaults, but micro tracks always use the producer formula"; + } + } else { + if (isCutEnabled(trackCuts.cMaxDCArToPVcut)) { + checkDCAGrid("cMaxDCArToPVcut", trackCuts.cMaxDCArToPVcut); + } + if (isCutEnabled(trackCuts.cMaxDCAzToPVcut)) { + checkDCAGrid("cMaxDCAzToPVcut", trackCuts.cMaxDCAzToPVcut); + } + } + if (isCutEnabled(trackCuts.cMinDCAzToPVcut)) { + checkDCAGrid("cMinDCAzToPVcut", trackCuts.cMinDCAzToPVcut); + } + if (isCutEnabled(pionPID.cMaxTPCnSigmaPion) && !pionPID.cPionUsePtDepPID) { + checkPIDGrid("cMaxTPCnSigmaPion", pionPID.cMaxTPCnSigmaPion); + } + if (isCutEnabled(pionPID.cMaxTOFnSigmaPion) && !pionPID.cPionUsePtDepPID) { + checkPIDGrid("cMaxTOFnSigmaPion", pionPID.cMaxTOFnSigmaPion); + } + if (isCutEnabled(kaonPID.cMaxTPCnSigmaKaon) && !kaonPID.cKaonUsePtDepPID) { + checkPIDGrid("cMaxTPCnSigmaKaon", kaonPID.cMaxTPCnSigmaKaon); + } + if (isCutEnabled(kaonPID.cMaxTOFnSigmaKaon) && !kaonPID.cKaonUsePtDepPID) { + checkPIDGrid("cMaxTOFnSigmaKaon", kaonPID.cMaxTOFnSigmaKaon); + } + if (pionPID.cPionUsePtDepPID) { + for (const auto cut : pionPID.cPionTPCNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cPionTPCNSigmaCuts", cut); + } + } + for (const auto cut : pionPID.cPionTOFNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cPionTOFNSigmaCuts", cut); + } + } + } + if (kaonPID.cKaonUsePtDepPID) { + for (const auto cut : kaonPID.cKaonTPCNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cKaonTPCNSigmaCuts", cut); + } + } + for (const auto cut : kaonPID.cKaonTOFNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cKaonTOFNSigmaCuts", cut); + } + } + } + if (pionPID.nsigmaCutCombinedPion > 0 || kaonPID.nsigmaCutCombinedKaon > 0) { + LOG(warning) << "nsigmaCutCombined* on micro tracks uses quantised nSigma values (approximate)"; + } + } + std::vector centBinning = {0., 1., 5., 10., 15., 20., 25., 30., 35., 40., 45., 50., 55., 60., 65., 70., 80., 90., 100., 200.}; AxisSpec centAxis = {centBinning, "T0M (%)"}; AxisSpec ptAxis = {150, 0, 15, "#it{p}_{T} (GeV/#it{c})"}; @@ -152,6 +402,36 @@ struct K1AnalysisMicro { AxisSpec axisType = {BinType::kTYEnd, 0, BinType::kTYEnd, "Type of bin with charge and mix"}; AxisSpec mcLabelAxis = {5, -0.5, 4.5, "MC Label"}; + // Micro-only instrumentation: category 0 includes all combinations, not just unmatched. + auto trackFlow = histos.add("CutFlow/tracks", "Micro tracks, once per selected collision;stage;species", HistType::kTH2D, {{static_cast(kTrkNStages), -0.5, static_cast(kTrkNStages) - 0.5}, {2, -0.5, 1.5}}); + const std::array trackLabels{"input", "pT", "eta", "DCAxy", "DCAz", "track flags", "clusters / crossed rows", "TOF required", "PID"}; + for (size_t i = 0; i < trackLabels.size(); ++i) { + trackFlow->GetXaxis()->SetBinLabel(i + 1, trackLabels[i]); + } + trackFlow->GetYaxis()->SetBinLabel(1, "pion"); + trackFlow->GetYaxis()->SetBinLabel(2, "kaon"); + auto candidateFlow = histos.add("CutFlow/candidates", "Unordered micro triplets;stage;category", HistType::kTH2D, {{NCandidateStages, -0.5, NCandidateStages - 0.5}, {3, -0.5, 2.5}}); + const std::array candidateLabels{"input unordered triplets", "distinct pion IDs", "pion selection (quality+PID)", "pion pair constructed", "pair pT", "secondary mass window (rho mode)", "three distinct IDs", "kaon selection (quality+PID)", "K1 rapidity", "candidate cuts", "final US", "final LS"}; + for (size_t i = 0; i < candidateLabels.size(); ++i) { + candidateFlow->GetXaxis()->SetBinLabel(i + 1, candidateLabels[i]); + } + candidateFlow->GetYaxis()->SetBinLabel(1, "all"); + candidateFlow->GetYaxis()->SetBinLabel(2, "rho K"); + candidateFlow->GetYaxis()->SetBinLabel(3, "K* pi"); + if (doprocessMCMicro) { + auto mothers = histos.add("CutFlow/uniqueMothersPerCollision", "Final unique K1 IDs summed over reconstructed collisions (not globally deduplicated)", HistType::kTH1D, {{2, 0.5, 2.5}}); + mothers->GetXaxis()->SetBinLabel(1, "rho K"); + mothers->GetXaxis()->SetBinLabel(2, "K* pi"); + } + if (doprocessMCTrue) { + auto generated = histos.add("CutFlow/generated", "K1 parent rows conditional on selected reconstructed events;stage;immediate channel", HistType::kTH2D, {{2, -0.5, 1.5}, {3, -0.5, 2.5}}); + generated->GetXaxis()->SetBinLabel(1, "all K1 parent rows"); + generated->GetXaxis()->SetBinLabel(2, "K1 rapidity window"); + generated->GetYaxis()->SetBinLabel(1, "other / unresolved"); + generated->GetYaxis()->SetBinLabel(2, "rho K"); + generated->GetYaxis()->SetBinLabel(3, "K* pi"); + } + // DCA QA // Primary pion histos.add("QA/trkppionDCAxy", "DCAxy disstribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); @@ -223,7 +503,16 @@ struct K1AnalysisMicro { histos.add("k1invmass_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTH1F, {invMassAxisReso}); // MC - if (doprocessMC) { + if (doprocessMC || doprocessMCMicro) { + AxisSpec channelAxis = {3, -0.5, 2.5, "0: non-K1, 1: rho K, 2: K* pi"}; + histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/channel", "All selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis}); + histos.add("MCReco/mass", "Reconstructed mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisReso}); + histos.add("MCReco/pt", "Reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis}); + histos.add("MCReco/piPiMass", "pi-pi mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisRho}); + histos.add("MCReco/pi1KMass", "First pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); + histos.add("MCReco/pi2KMass", "Second pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); histos.add("k1invmass_MC", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); histos.add("k1invmass_MC_noK1", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); @@ -255,311 +544,637 @@ struct K1AnalysisMicro { histos.add("QAMC/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); histos.add("QAMC/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); } // doprocessMC + if (doprocessMCTrue) { + AxisSpec channelAxis = {3, -0.5, 2.5, "0: other/unresolved, 1: rho K, 2: K* pi"}; + 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}); + histos.add("MCGen/ptChannel", "Generated K1 pT by immediate decay channel", HistType::kTH2D, {channelAxis, ptAxis}); + } // Print output histograms statistics LOG(info) << "Size of the histograms in K1 Analysis Task"; histos.print(); } // init - // PDG code - int kPDGRho770 = 113; - int kK1Plus = 10323; - - template - bool trackCut(const TrackType& track) + // Resolve the PID cut of one species at a given pT; false if the pT is outside all pT-dependent bins. + template + bool getPIDCut(float pt, PIDCut& cut) { - if constexpr (!IsResoMicrotrack) { - // basic track cuts - if (std::abs(track.pt()) < cMinPtcut) - return false; - if (std::abs(track.dcaXY()) > cMaxDCArToPVcut) - return false; - if (std::abs(track.dcaZ()) > cMaxDCAzToPVcut) - return false; - if (track.tpcNClsFound() < cfgTPCcluster) - return false; - if (cfgHasTOF && !track.hasTOF()) - return false; - if (cfgUseITSRefit && !track.passedITSRefit()) - return false; - if (cfgUseTPCRefit && !track.passedTPCRefit()) - return false; - if (cfgPVContributor && !track.isPVContributor()) - return false; - if (cfgPrimaryTrack && !track.isPrimaryTrack()) - return false; - if (cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) - return false; - if (cfgGlobalTrack && !track.isGlobalTrack()) - return false; + if constexpr (S == Species::Pion) { + cut.tpcMax = pionPID.cMaxTPCnSigmaPion; + cut.tofMax = pionPID.cMaxTOFnSigmaPion; + cut.combined = pionPID.nsigmaCutCombinedPion; + cut.tofRequired = false; + if (pionPID.cPionUsePtDepPID) { + const int ptBin = getPtBinIndex(pt, pionPID.cPionPIDPtBins.value); + if (ptBin < 0) { + return false; + } + const auto bin = static_cast(ptBin); + cut.tpcMax = pionPID.cPionTPCNSigmaCuts.value[bin]; + cut.tofMax = pionPID.cPionTOFNSigmaCuts.value[bin]; + cut.tofRequired = pionPID.cPionTOFRequired.value[bin] != 0; + } } else { - if (std::abs(track.pt()) < cMinPtcut) - return false; - if (o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAxy(track.trackSelectionFlags()) > cMaxDCArToPVcut - Epsilon) - return false; - if (o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAz(track.trackSelectionFlags()) > cMaxDCAzToPVcut - Epsilon) - return false; - if (cfgPrimaryTrack && !track.isPrimaryTrack()) - return false; - if (cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) - return false; - if (cfgPVContributor && !track.isPVContributor()) - return false; + cut.tpcMax = kaonPID.cMaxTPCnSigmaKaon; + cut.tofMax = kaonPID.cMaxTOFnSigmaKaon; + cut.combined = kaonPID.nsigmaCutCombinedKaon; + cut.tofRequired = false; + if (kaonPID.cKaonUsePtDepPID) { + const int ptBin = getPtBinIndex(pt, kaonPID.cKaonPIDPtBins.value); + if (ptBin < 0) { + return false; + } + const auto bin = static_cast(ptBin); + cut.tpcMax = kaonPID.cKaonTPCNSigmaCuts.value[bin]; + cut.tofMax = kaonPID.cKaonTOFNSigmaCuts.value[bin]; + cut.tofRequired = kaonPID.cKaonTOFRequired.value[bin] != 0; + } } return true; } - // Pion PID selection tools - template - bool selectionPIDpion(const T& candidate) + // Track quality selection shared by pion and kaon. Returns the last stage that was passed. + // Full tracks store exact values; micro tracks store quantised DCA (see LFResonanceTables.h). + template + int trackQualityStage(const TrackType& track) { - if constexpr (!IsResoMicrotrack) { - bool tpcPIDPassed{false}, tofPIDPassed{false}; - if (std::abs(candidate.tpcNSigmaPi()) < cMaxTPCnSigmaPion) { - tpcPIDPassed = true; - } else { - return false; - } - if (candidate.hasTOF()) { - if (std::abs(candidate.tofNSigmaPi()) < cMaxTOFnSigmaPion) { - tofPIDPassed = true; + const double pt = track.pt(); + const double dcaXY = track.dcaXY(); + const double dcaZ = track.dcaZ(); + // Invalid micro DCA codes decode to NaN + if (!std::isfinite(pt) || !std::isfinite(track.eta()) || !std::isfinite(dcaXY) || !std::isfinite(dcaZ)) { + return kTrkInput; + } + if (std::abs(pt) < trackCuts.cMinPtcut) { + return kTrkInput; + } + if (isCutEnabled(trackCuts.cMaxEtacut) && !(std::abs(track.eta()) < trackCuts.cMaxEtacut)) { + return kTrkPt; + } + + if (trackCuts.cfgUsePtDepDCA) { + if constexpr (IsResoMicrotrack) { + if (!track.passedPtDependentDCAxy()) { + return kTrkEta; } - if ((nsigmaCutCombinedPion > 0) && (candidate.tpcNSigmaPi() * candidate.tpcNSigmaPi() + candidate.tofNSigmaPi() * candidate.tofNSigmaPi() < nsigmaCutCombinedPion * nsigmaCutCombinedPion)) { - tofPIDPassed = true; + if (!track.passedPtDependentDCAz()) { + return kTrkDCAxy; } } else { - if (!cTOFVeto) { - return false; + const double dcaPtCut = trackCuts.cDCAToPVByPtP0 + trackCuts.cDCAToPVByPtCoeff * std::pow(pt, -static_cast(trackCuts.cDCAToPVByPtPower)); + if (!(std::abs(dcaXY) < dcaPtCut)) { + return kTrkEta; + } + if (!(std::abs(dcaZ) < dcaPtCut)) { + return kTrkDCAxy; } - tofPIDPassed = true; - } - if (tpcPIDPassed && tofPIDPassed) { - return true; } } else { - bool tpcPIDPassed{false}, tofPIDPassed{false}; - tpcPIDPassed = std::abs(o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(candidate.pidNSigmaPiFlag())) < cMaxTPCnSigmaPion + Epsilon; - tofPIDPassed = candidate.hasTOF() ? std::abs(o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(candidate.pidNSigmaPiFlag())) < cMaxTOFnSigmaPion + Epsilon : true; - if (tpcPIDPassed && tofPIDPassed) { - return true; - } - } - return false; - } - - // Kaon PID selection tools - template - bool selectionPIDkaon(const T& candidate) - { - if constexpr (!IsResoMicrotrack) { - bool tpcPIDPassed{false}, tofPIDPassed{false}; - if (std::abs(candidate.tpcNSigmaKa()) < cMaxTPCnSigmaKaon) { - tpcPIDPassed = true; - } else { - return false; + if (isCutEnabled(trackCuts.cMaxDCArToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMax(dcaXY, trackCuts.cMaxDCArToPVcut)) { + return kTrkEta; + } + } else { + if (!(std::abs(dcaXY) <= trackCuts.cMaxDCArToPVcut)) { + return kTrkEta; + } + } } - if (candidate.hasTOF()) { - if (std::abs(candidate.tofNSigmaKa()) < cMaxTOFnSigmaKaon) { - tofPIDPassed = true; + if (isCutEnabled(trackCuts.cMaxDCAzToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMax(dcaZ, trackCuts.cMaxDCAzToPVcut)) { + return kTrkDCAxy; + } + } else { + if (!(std::abs(dcaZ) <= trackCuts.cMaxDCAzToPVcut)) { + return kTrkDCAxy; + } } - if ((nsigmaCutCombinedKaon > 0) && (candidate.tpcNSigmaKa() * candidate.tpcNSigmaKa() + candidate.tofNSigmaKa() * candidate.tofNSigmaKa() < nsigmaCutCombinedKaon * nsigmaCutCombinedKaon)) { - tofPIDPassed = true; + } + } + if (isCutEnabled(trackCuts.cMinDCAzToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMin(dcaZ, trackCuts.cMinDCAzToPVcut)) { + return kTrkDCAxy; } } else { - if (!cTOFVeto) { - return false; + if (!(std::abs(dcaZ) >= trackCuts.cMinDCAzToPVcut)) { + return kTrkDCAxy; } - tofPIDPassed = true; } - if (tpcPIDPassed && tofPIDPassed) { - return true; + } + + // Track flags + if ((trackCuts.cfgPrimaryTrack && !track.isPrimaryTrack()) || + (trackCuts.cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) || + (trackCuts.cfgGlobalTrack && !track.isGlobalTrack()) || + (trackCuts.cfgPVContributor && !track.isPVContributor()) || + (trackCuts.cfgUseITSRefit && !track.passedITSRefit()) || + (trackCuts.cfgUseTPCRefit && !track.passedTPCRefit())) { + return kTrkDCAz; + } + + // Clusters: found clusters exist only in ResoTracks, ITS clusters only in ResoMicroTracks + if constexpr (!IsResoMicrotrack) { + if constexpr (requires { track.tpcNClsFound(); }) { + if (track.tpcNClsFound() < trackCuts.cfgTPCcluster) { + return kTrkFlags; + } } - } else { - bool tpcPIDPassed{false}, tofPIDPassed{false}; - tpcPIDPassed = std::abs(o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(candidate.pidNSigmaKaFlag())) < cMaxTPCnSigmaKaon + Epsilon; - tofPIDPassed = candidate.hasTOF() ? std::abs(o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(candidate.pidNSigmaKaFlag())) < cMaxTOFnSigmaKaon + Epsilon : true; - if (tpcPIDPassed && tofPIDPassed) { - return true; + } + if constexpr (requires { track.tpcNClsCrossedRows(); }) { + if (track.tpcNClsCrossedRows() < trackCuts.cfgTPCCrossedRowsMin) { + return kTrkFlags; } } - return false; + if constexpr (IsResoMicrotrack) { + if constexpr (requires { track.itsNCls(); }) { + if (track.itsNCls() < trackCuts.cfgITSNClsMin) { + return kTrkFlags; + } + } + } + return kTrkClusters; } - template - bool isTrueK1(const T& trk1, const T& trk2, const T2& bTrack) + // TOF signal requirement of the track (global, per species, or per pT bin) + template + bool passesTOFRequired(const TrackType& track) { - if (std::abs(trk1.pdgCode()) != kPiPlus || std::abs(trk2.pdgCode()) != kPiPlus) - return false; - if (std::abs(bTrack.pdgCode()) != kKPlus) - return false; - auto mother1 = trk1.motherId(); - auto mother2 = trk2.motherId(); - if (mother1 != mother2) - return false; - if (((std::abs(trk1.motherPDG()) && std::abs(trk2.motherPDG()) != kPDGRho770) && (std::abs(bTrack.motherPDG()) != kK1Plus)) || (std::abs(trk1.motherPDG()) && std::abs(bTrack.motherPDG()) != kK0Star892 && (std::abs(trk2.motherPDG()) != kK1Plus)) || (std::abs(trk2.motherPDG()) && std::abs(bTrack.motherPDG()) != kK0Star892 && (std::abs(trk1.motherPDG()) != kK1Plus))) - return false; - auto siblings = bTrack.siblingIds(); - if (siblings[0] != mother1 && siblings[1] != mother2) - return false; - return true; - } // isTrueK1 + bool required = trackCuts.cfgHasTOF; + if constexpr (S == Species::Pion) { + required = required || pionPID.cUseOnlyTOFTrackPi; + } else { + required = required || kaonPID.cUseOnlyTOFTrackKa; + } + PIDCut cut; + // A pT outside all bins is rejected by passesPID + if (!cByPassTOF && getPIDCut(track.pt(), cut) && cut.tofRequired) { + required = true; + } + return !required || track.hasTOF(); + } - template - bool isTrueK892(const T& trk1, const T& trk2) + // PID selection: the same code for full and micro tracks, only the comparison is quantisation aware + template + bool passesPID(const TrackType& track) { - if (std::abs(trk1.pdgCode()) != kPiPlus || std::abs(trk2.pdgCode()) != kKPlus) - return false; - auto mother1 = trk1.motherId(); - auto mother2 = trk2.motherId(); - if (mother1 != mother2) + PIDCut cut; + if (!getPIDCut(track.pt(), cut)) { return false; - if (std::abs(trk1.motherPDG()) != kK0Star892) + } + const bool hasTOF = track.hasTOF(); + double tpcNSigma = std::numeric_limits::quiet_NaN(); + double tofNSigma = std::numeric_limits::quiet_NaN(); // TOF value is only valid with hasTOF + if constexpr (S == Species::Pion) { + tpcNSigma = track.tpcNSigmaPi(); + if (hasTOF) { + tofNSigma = track.tofNSigmaPi(); + } + } else { + tpcNSigma = track.tpcNSigmaKa(); + if (hasTOF) { + tofNSigma = track.tofNSigmaKa(); + } + } + if (isCutEnabled(cut.tpcMax) && !passesMax(std::abs(tpcNSigma), cut.tpcMax)) { return false; - return true; + } + // Missing TOF is handled by passesTOFRequired; here the TPC alone decides + if (cByPassTOF || !hasTOF) { + return true; + } + bool tofPassed = !isCutEnabled(cut.tofMax) || passesMax(std::abs(tofNSigma), cut.tofMax); + if (!tofPassed && cut.combined > 0 && tpcNSigma * tpcNSigma + tofNSigma * tofNSigma < cut.combined * cut.combined) { + tofPassed = true; + } + return tofPassed; } - template - bool isTrueRho(const T& trk1, const T& trk2) + // Full selection stage of a track (quality, TOF requirement, PID) + template + int trackSelectionStage(const TrackType& track) { - if (std::abs(trk1.pdgCode()) != kPiPlus || std::abs(trk2.pdgCode()) != kPiPlus) - return false; - auto mother1 = trk1.motherId(); - auto mother2 = trk2.motherId(); - if (mother1 != mother2) - return false; - if (std::abs(trk1.motherPDG()) != kPDGRho770) - return false; - return true; + const int qualityStage = trackQualityStage(track); + if (qualityStage < kTrkClusters) { + return qualityStage; + } + if (!passesTOFRequired(track)) { + return kTrkClusters; + } + if (!passesPID(track)) { + return kTrkTOFRequired; + } + return kTrkPID; } - template - void fillHistograms(const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2) + + template + bool selectTrack(const TrackType& track) { - auto multiplicity = collision.cent(); - TLorentzVector lDecayDaughter1, lDecayDaughter2, lResonanceSecondary, lDecayDaughter_bach, lResonanceK1; - for (const auto& [trk1, trk2] : combinations(CombinationsFullIndexPolicy(dTracks2, dTracks2))) { - // Full index policy is needed to consider all possible combinations - if (trk1.index() == trk2.index()) - continue; // We need to run (0,1), (1,0) pairs too. But the same id pairs are not needed. - // trk1: pion, trk2: pion, bTrack: kaon - if (!trackCut(trk1) || !trackCut(trk2)) - continue; + return trackSelectionStage(track) == kTrkPID; + } - auto trk1pt = trk1.pt(); - auto trk2pt = trk2.pt(); - auto isTrk1hasTOF = trk1.hasTOF(); - auto isTrk2hasTOF = trk2.hasTOF(); + // Selection cache of one track slice. The row number of a grouped slice is global, hence the offset. + template + std::size_t getCacheIndex(const TrackType& track, int64_t firstIndex, std::size_t size) + { + const int64_t index = static_cast(track.index()) - firstIndex; + if (index < 0 || index >= static_cast(size)) { + LOG(fatal) << "Track index " << track.index() << " is outside the selection cache [" << firstIndex << ", " << firstIndex + static_cast(size) << ")"; + } + return static_cast(index); + } - if constexpr (!IsResoMicrotrack) { - auto trk1NSigmaPiTPC = trk1.tpcNSigmaPi(); - auto trk1NSigmaPiTOF = (isTrk1hasTOF) ? trk1.tofNSigmaPi() : -999.; - auto trk2NSigmaPiTPC = trk2.tpcNSigmaPi(); - auto trk2NSigmaPiTOF = (isTrk2hasTOF) ? trk2.tofNSigmaPi() : -999.; + template + std::vector buildSelectionCache(const TracksType& tracks, int64_t firstIndex) + { + std::vector selected(tracks.size(), 0); + for (const auto& track : tracks) { + const int stage = trackSelectionStage(track); + selected[getCacheIndex(track, firstIndex, selected.size())] = (stage == kTrkPID) ? 1 : 0; + if constexpr (FillCutFlow) { + for (int i = 0; i <= stage; ++i) { + histos.fill(HIST("CutFlow/tracks"), i, static_cast(S)); + } + } + } + return selected; + } - if (cUseOnlyTOFTrackPi && !isTrk1hasTOF) - continue; - if (!selectionPIDpion(trk1) || !selectionPIDpion(trk2)) - continue; + enum class K1TruthChannel { + None = 0, + RhoK = 1, + KStarPi = 2 + }; - if constexpr (!IsMix) { + template + bool hasSibling(const Track& directDaughter, int resonanceId) + { + if (resonanceId < 0) { + return false; + } + const auto siblings = directDaughter.siblingIds(); + return siblings[0] == resonanceId || siblings[1] == resonanceId; + } - histos.fill(HIST("QA/trkppionTPCPID"), trk1pt, trk1NSigmaPiTPC); - if (isTrk1hasTOF) { - histos.fill(HIST("QA/trkppionTOFPID"), trk1pt, trk1NSigmaPiTOF); - histos.fill(HIST("QA/trkppionTPCTOFPID"), trk1NSigmaPiTPC, trk1NSigmaPiTOF); - } - histos.fill(HIST("QA/trkppionpT"), trk1pt); - histos.fill(HIST("QA/trkppionDCAxy"), trk1.dcaXY()); - histos.fill(HIST("QA/trkppionDCAz"), trk1.dcaZ()); - - histos.fill(HIST("QA/trkspionTPCPID"), trk2pt, trk2NSigmaPiTPC); - if (isTrk2hasTOF) { - histos.fill(HIST("QA/trkspionTOFPID"), trk2pt, trk2NSigmaPiTOF); - histos.fill(HIST("QA/trkspionTPCTOFPID"), trk2NSigmaPiTPC, trk2NSigmaPiTOF); - } - histos.fill(HIST("QA/trkspionpT"), trk2pt); - histos.fill(HIST("QA/trkspionDCAxy"), trk2.dcaXY()); - histos.fill(HIST("QA/trkspionDCAz"), trk2.dcaZ()); + template + bool matchesKStarPi(const Track& resonancePion, const Track& directPion, const Kaon& kaon) + { + const int charge = kaon.pdgCode() > 0 ? 1 : -1; + if (resonancePion.motherId() != kaon.motherId() || + resonancePion.motherId() == directPion.motherId()) { + return false; + } + if (resonancePion.motherPDG() != charge * kK0Star892 || kaon.motherPDG() != charge * kK0Star892) { + return false; + } + if (resonancePion.pdgCode() != -charge * kPiPlus || directPion.pdgCode() != charge * kPiPlus || + directPion.motherPDG() != charge * Pdg::kK1_1270Plus) { + return false; + } + return hasSibling(directPion, kaon.motherId()); + } + + template + K1TruthChannel classifyK1Truth(const Track& pion1, const Track& pion2, const Kaon& kaon) + { + if (std::abs(pion1.pdgCode()) != kPiPlus || std::abs(pion2.pdgCode()) != kPiPlus || + std::abs(kaon.pdgCode()) != kKPlus) { + return K1TruthChannel::None; + } + if (pion1.motherId() < 0 || pion2.motherId() < 0 || kaon.motherId() < 0) { + return K1TruthChannel::None; + } + const int charge = kaon.pdgCode() > 0 ? 1 : -1; + const bool rhoPions = pion1.motherId() == pion2.motherId() && + pion1.motherPDG() == kRho770_0 && pion2.motherPDG() == kRho770_0 && + pion1.pdgCode() == -pion2.pdgCode(); + if (rhoPions && kaon.motherPDG() == charge * Pdg::kK1_1270Plus && + kaon.motherId() != pion1.motherId() && hasSibling(kaon, pion1.motherId())) { + return K1TruthChannel::RhoK; + } + if (matchesKStarPi(pion1, pion2, kaon) || matchesKStarPi(pion2, pion1, kaon)) { + return K1TruthChannel::KStarPi; + } + return K1TruthChannel::None; + } + + // Track QA of a pion; isPrimary selects the trkppion (first) or trkspion (second) histograms + template + void fillPionQA(const TrackType& track, bool isPrimary) + { + const bool hasTOF = track.hasTOF(); + if (isPrimary) { + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QA/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QA/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QA/trkppionpT"), track.pt()); + histos.fill(HIST("QA/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkppionDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAcut/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAcut/trkppionpT"), track.pt()); + histos.fill(HIST("QAcut/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkppionDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAMC/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAMC/trkppionpT"), track.pt()); + histos.fill(HIST("QAMC/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkppionDCAz"), track.dcaZ()); + } + } else { + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QA/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QA/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QA/trkspionpT"), track.pt()); + histos.fill(HIST("QA/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkspionDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAcut/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); } + histos.fill(HIST("QAcut/trkspionpT"), track.pt()); + histos.fill(HIST("QAcut/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkspionDCAz"), track.dcaZ()); } else { - histos.fill(HIST("QA/trkppionTPCPID"), trk1pt, o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk1.pidNSigmaPiFlag())); - if (isTrk1hasTOF) { - histos.fill(HIST("QA/trkppionTOFPID"), trk1pt, o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk1.pidNSigmaPiFlag())); - histos.fill(HIST("QA/trkppionTPCTOFPID"), o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk1.pidNSigmaPiFlag()), o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk1.pidNSigmaPiFlag())); + histos.fill(HIST("QAMC/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAMC/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); } - histos.fill(HIST("QA/trkppionpT"), trk1pt); - histos.fill(HIST("QA/trkppionDCAxy"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAxy(trk1.trackSelectionFlags())); - histos.fill(HIST("QA/trkppionDCAz"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAz(trk1.trackSelectionFlags())); + histos.fill(HIST("QAMC/trkspionpT"), track.pt()); + histos.fill(HIST("QAMC/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkspionDCAz"), track.dcaZ()); + } + } + } + + // Track QA of the bachelor kaon + template + void fillKaonQA(const TrackType& track) + { + const bool hasTOF = track.hasTOF(); + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QA/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QA/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QA/trkkaonpT"), track.pt()); + histos.fill(HIST("QA/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkkaonDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QAcut/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QAcut/trkkaonpT"), track.pt()); + histos.fill(HIST("QAcut/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkkaonDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QAMC/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QAMC/trkkaonpT"), track.pt()); + histos.fill(HIST("QAMC/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkkaonDCAz"), track.dcaZ()); + } + } + + template + void fillHistograms(const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2) + { + if (dTracks1.size() == 0 || dTracks2.size() == 0) { + return; + } + // Sets are local to this reconstructed collision: IDs cannot leak across DFs. + // Source-file/DF deduplication across split collisions belongs in the audit. + std::array, 3> matchedMothers; + + // Selection cache: every track is selected once, not once per pair x bachelor. + // dTracks1: bachelor kaons, dTracks2: pions (different collisions in mixed events). + constexpr bool FillCutFlow = IsResoMicrotrack && !IsMix; + const int64_t firstKaonIndex = dTracks1.begin().index(); + const int64_t firstPionIndex = dTracks2.begin().index(); + const auto kaonSelected = buildSelectionCache(dTracks1, firstKaonIndex); + const auto pionSelected = buildSelectionCache(dTracks2, firstPionIndex); + + // Values needed only by switched-on cuts or QA are computed only then + const bool isK892Mode = secondaryCuts.cfgModeK892orRho; + const bool fillQA = !IsMix && additionalQAplots; + const bool needAngle = IsMC || fillQA || angleCutOn; + const bool needPairAsym = IsMC || fillQA || pairAsymCutOn; + // K892 mode: the K* candidate is (trk1, K), rho mode: the rho is (trk1, trk2) + const bool needMass13 = IsMC || fillQA || (isK892Mode ? secondaryWindowOn : anotherMassCutOn) || (isK892Mode && (needAngle || needPairAsym)); + const bool needMass23 = IsMC || fillQA || piKaMassCutOn; + const bool rhoWindowOn = secondaryWindowOn && !isK892Mode; - histos.fill(HIST("QA/trkspionTPCPID"), trk2pt, o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk2.pidNSigmaPiFlag())); - if (isTrk2hasTOF) { - histos.fill(HIST("QA/trkspionTOFPID"), trk2pt, o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk2.pidNSigmaPiFlag())); - histos.fill(HIST("QA/trkspionTPCTOFPID"), o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk2.pidNSigmaPiFlag()), o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk2.pidNSigmaPiFlag())); + auto multiplicity = collision.cent(); + ROOT::Math::PxPyPzMVector lDecayDaughter1, lDecayDaughter2, lResonanceSecondary, lDecayDaughter_bach, lResonanceK1, lPair13, lPair23; + // Unordered pion pairs: each (pion, pion, kaon) triplet is filled once. + // Here trk1 is the pion with the lower index; the roles are assigned once the bachelor is known. + for (const auto& [trk1, trk2] : combinations(CombinationsStrictlyUpperIndexPolicy(dTracks2, dTracks2))) { + // trk1: pion, trk2: pion, bTrack: kaon + const bool pionsSelected = pionSelected[getCacheIndex(trk1, firstPionIndex, pionSelected.size())] && pionSelected[getCacheIndex(trk2, firstPionIndex, pionSelected.size())]; + bool pairPt = false; + bool rhoWindow = true; + if (pionsSelected) { + // Resonance reconstruction + lDecayDaughter1.SetCoordinates(trk1.px(), trk1.py(), trk1.pz(), MassPionCharged); + lDecayDaughter2.SetCoordinates(trk2.px(), trk2.py(), trk2.pz(), MassPionCharged); + lResonanceSecondary = lDecayDaughter1 + lDecayDaughter2; + pairPt = !(lResonanceSecondary.Pt() < secondaryCuts.cMinSecondaryPtCut); + rhoWindow = !rhoWindowOn || isInWindow(lResonanceSecondary.M(), MassRho770, secondaryCuts.cSecondaryMasswindow); + } + if constexpr (FillCutFlow) { + // Early stages count potential triplets: each pair carries N bachelor trials. + // This preserves the pair-first reconstruction and avoids a new cubic data loop. + // Distinct pion IDs are guaranteed by the strictly upper index policy (stage 1 is always passed). + const int lastStage = !pionsSelected ? 1 : !pairPt ? 3 : !rhoWindow ? 4 : 5; + for (int stage = 0; stage <= lastStage; ++stage) { + histos.fill(HIST("CutFlow/candidates"), stage, 0, static_cast(dTracks1.size())); + } + if constexpr (IsMC) { + // Match before rejecting quality/pT so both channels have an upstream numerator. + if (std::abs(trk1.pdgCode()) == kPiPlus && trk1.pdgCode() == -trk2.pdgCode()) { + for (const auto& bachelor : dTracks1) { + const auto channel = classifyK1Truth(trk1, trk2, bachelor); + if (channel != K1TruthChannel::None) { + for (int stage = 0; stage <= lastStage; ++stage) { + histos.fill(HIST("CutFlow/candidates"), stage, static_cast(channel)); + } + } + } + } } - histos.fill(HIST("QA/trkspionpT"), trk2pt); - histos.fill(HIST("QA/trkspionDCAxy"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAxy(trk2.trackSelectionFlags())); - histos.fill(HIST("QA/trkspionDCAz"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAz(trk2.trackSelectionFlags())); + } + if (!pionsSelected) { + continue; } - // Resonance reconstruction - lDecayDaughter1.SetXYZM(trk1.px(), trk1.py(), trk1.pz(), MassPionCharged); - lDecayDaughter2.SetXYZM(trk2.px(), trk2.py(), trk2.pz(), MassPionCharged); - lResonanceSecondary = lDecayDaughter1 + lDecayDaughter2; + if (fillQA) { + fillPionQA(trk1, true); + fillPionQA(trk2, false); + } - if (lResonanceSecondary.Pt() < cMinSecondaryPtCut) + if (!pairPt) { continue; + } - if constexpr (!IsMix) { + if (fillQA) { histos.fill(HIST("QA/hInvmassSecon"), lResonanceSecondary.M()); } if constexpr (IsMC) { - /* - if (isTrueK892(trk1, trk2)) - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - } else { - if (isTrueRho(trk1, trk2)) - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - } - */ histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); } - // Mass Window cut is removed + // Secondary mass window (rho mode): the bachelor loop is skipped for rejected pairs + if (!rhoWindow) { + continue; + } for (const auto& bTrack : dTracks1) { - if (bTrack.index() == trk1.index() || bTrack.index() == trk2.index()) - continue; - if (!trackCut(bTrack)) + if (bTrack.index() == trk1.index() || bTrack.index() == trk2.index()) { continue; - if (!selectionPIDkaon(bTrack)) + } + K1TruthChannel flowChannel = K1TruthChannel::None; + if constexpr (IsMC && IsResoMicrotrack && !IsMix) { + flowChannel = classifyK1Truth(trk1, trk2, bTrack); + } + auto countCandidate = [&](int stage) { + if constexpr (FillCutFlow) { + histos.fill(HIST("CutFlow/candidates"), stage, 0); + if constexpr (IsMC) { + if (flowChannel != K1TruthChannel::None) { + histos.fill(HIST("CutFlow/candidates"), stage, static_cast(flowChannel)); + } + } + } + }; + countCandidate(6); + if (!kaonSelected[getCacheIndex(bTrack, firstKaonIndex, kaonSelected.size())]) { continue; + } + countCandidate(7); + + if (fillQA) { + fillKaonQA(bTrack); + } + + // Canonical assignment of the pion roles, once the bachelor is known. + // Unlike-sign pair: the pion with the sign opposite to the kaon is pion 1 (K*0 partner), the other is pion 2. + // Like-sign pair (the rule is ambiguous): the pion with the lower index is pion 1. + const bool isUnlikeSign = trk1.sign() * trk2.sign() < 0; + const bool swapPions = isUnlikeSign && trk1.sign() == bTrack.sign(); + const auto& pion1 = swapPions ? trk2 : trk1; + const auto& pion2 = swapPions ? trk1 : trk2; + const auto& lPion1 = swapPions ? lDecayDaughter2 : lDecayDaughter1; + const auto& lPion2 = swapPions ? lDecayDaughter1 : lDecayDaughter2; // K1 reconstruction - lDecayDaughter_bach.SetXYZM(bTrack.px(), bTrack.py(), bTrack.pz(), MassKaonCharged); + lDecayDaughter_bach.SetCoordinates(bTrack.px(), bTrack.py(), bTrack.pz(), MassKaonCharged); lResonanceK1 = lResonanceSecondary + lDecayDaughter_bach; // Cuts - if (lResonanceK1.Rapidity() > cK1MaxRap || lResonanceK1.Rapidity() < cK1MinRap) + if (lResonanceK1.Rapidity() > cK1MaxRap || lResonanceK1.Rapidity() < cK1MinRap) { continue; + } + countCandidate(8); + + double mass13 = 0.; + double mass23 = 0.; + double lK1Angle = 0.; + double lPairAsym = 0.; + if (needMass13) { + lPair13 = lPion1 + lDecayDaughter_bach; + mass13 = lPair13.M(); + } + if (needMass23) { + lPair23 = lPion2 + lDecayDaughter_bach; + mass23 = lPair23.M(); + } + // Rho mode: secondary = (trk1, trk2) against the bachelor. K892 mode: secondary = (trk1, K) against trk2. + if (needAngle) { + lK1Angle = isK892Mode ? ROOT::Math::VectorUtil::Angle(lPair13, lPion2) : ROOT::Math::VectorUtil::Angle(lResonanceSecondary, lDecayDaughter_bach); + } + if (needPairAsym) { + lPairAsym = isK892Mode ? (lPair13.E() - lPion2.E()) / (lPair13.E() + lPion2.E()) + : (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); + } - auto lK1Angle = lResonanceSecondary.Angle(lDecayDaughter_bach.Vect()); - auto lPairAsym = (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); - - TLorentzVector temp13 = lDecayDaughter1 + lDecayDaughter_bach; - TLorentzVector temp23 = lDecayDaughter2 + lDecayDaughter_bach; - - // QA histogram - if constexpr (!IsMix) { + // QA histogram before the candidate cuts + if (fillQA) { histos.fill(HIST("QA/K1OA"), lK1Angle); histos.fill(HIST("QA/K1PairAsym"), lPairAsym); - histos.fill(HIST("QA/hInvmassK892_Rho"), temp13.M(), lResonanceSecondary.M()); - histos.fill(HIST("QA/hInvmassSecon_PiKa"), lResonanceSecondary.M(), temp23.M()); + histos.fill(HIST("QA/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); + histos.fill(HIST("QA/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); histos.fill(HIST("QA/hpT_Secondary"), lResonanceSecondary.Pt()); } - // Selection cuts are removed - // QA histograms after the cuts are removed as no cuts are applied + + // Candidate cuts (each one is evaluated only if switched on) + if (isK892Mode && secondaryWindowOn && (!isInWindow(mass13, MassK0Star892, secondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) { + continue; + } + if (anotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : mass13, secondaryCuts.cMinAnotherSecondaryMassCut, secondaryCuts.cMaxAnotherSecondaryMassCut)) { + continue; + } + if (piKaMassCutOn && !isInRange(mass23, secondaryCuts.cMinPiKaMassCut, secondaryCuts.cMaxPiKaMassCut)) { + continue; + } + if (angleCutOn && !isInRange(lK1Angle, secondaryCuts.cMinAngle, secondaryCuts.cMaxAngle)) { + continue; + } + if (pairAsymCutOn && !isInRange(lPairAsym, secondaryCuts.cMinPairAsym, secondaryCuts.cMaxPairAsym)) { + continue; + } + countCandidate(9); + + // QA histograms after the candidate cuts + if (fillQA) { + fillPionQA(pion1, true); + fillPionQA(pion2, false); + fillKaonQA(bTrack); + histos.fill(HIST("QAcut/K1OA"), lK1Angle); + histos.fill(HIST("QAcut/K1PairAsym"), lPairAsym); + histos.fill(HIST("QAcut/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); + histos.fill(HIST("QAcut/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); + histos.fill(HIST("QAcut/hInvmassSecon"), lResonanceSecondary.M()); + histos.fill(HIST("QAcut/hpT_Secondary"), lResonanceSecondary.Pt()); + } + + countCandidate(isUnlikeSign ? 10 : 11); + if constexpr (IsMC && IsResoMicrotrack && !IsMix) { + if (flowChannel != K1TruthChannel::None) { + const int mother = flowChannel == K1TruthChannel::RhoK ? bTrack.motherId() : + std::abs(pion1.motherPDG()) == Pdg::kK1_1270Plus ? pion1.motherId() : pion2.motherId(); + if (matchedMothers[static_cast(flowChannel)].insert(mother).second) { + histos.fill(HIST("CutFlow/uniqueMothersPerCollision"), static_cast(flowChannel)); + } + } + } if constexpr (!IsMix) { unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P : BinType::kK1N; unsigned int typeNormal = BinAnti::kNormal; - if (trk1.sign() * trk2.sign() < 0) { + if (isUnlikeSign) { histos.fill(HIST("k1invmass"), lResonanceK1.M()); histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); } else { @@ -568,65 +1183,38 @@ struct K1AnalysisMicro { } if constexpr (IsMC) { - if (isTrueK1(trk1, trk2, bTrack)) { + const auto channel = classifyK1Truth(pion1, pion2, bTrack); + const int channelBin = static_cast(channel); + histos.fill(HIST("MCReco/channel"), channelBin); + histos.fill(HIST("MCReco/mass"), channelBin, lResonanceK1.M()); + histos.fill(HIST("MCReco/pt"), channelBin, lResonanceK1.Pt()); + histos.fill(HIST("MCReco/piPiMass"), channelBin, lResonanceSecondary.M()); + histos.fill(HIST("MCReco/pi1KMass"), channelBin, mass13); + histos.fill(HIST("MCReco/pi2KMass"), channelBin, mass23); + if (channel != K1TruthChannel::None) { + if (truthDebugCounts[channelBin] < cfgTruthDebug) { + ++truthDebugCounts[channelBin]; + LOGF(info, "K1Truth channel=%d collision=%lld tracks=(%lld,%lld,%lld) pdg=(%d,%d,%d) mothers=(%d,%d,%d) motherPDG=(%d,%d,%d) siblings=((%d,%d),(%d,%d),(%d,%d))", + channelBin, static_cast(collision.globalIndex()), + static_cast(pion1.globalIndex()), static_cast(pion2.globalIndex()), static_cast(bTrack.globalIndex()), + pion1.pdgCode(), pion2.pdgCode(), bTrack.pdgCode(), pion1.motherId(), pion2.motherId(), bTrack.motherId(), + pion1.motherPDG(), pion2.motherPDG(), bTrack.motherPDG(), + pion1.siblingIds()[0], pion1.siblingIds()[1], pion2.siblingIds()[0], pion2.siblingIds()[1], bTrack.siblingIds()[0], bTrack.siblingIds()[1]); + } typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec; histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); histos.fill(HIST("k1invmass_MC"), lResonanceK1.M()); histos.fill(HIST("QAMC/K1OA"), lK1Angle); histos.fill(HIST("QAMC/K1PairAsym"), lPairAsym); - histos.fill(HIST("QAMC/hInvmassK892_Rho"), temp13.M(), lResonanceSecondary.M()); - histos.fill(HIST("QAMC/hInvmassSecon_PiKa"), lResonanceSecondary.M(), temp23.M()); + histos.fill(HIST("QAMC/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); + histos.fill(HIST("QAMC/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); histos.fill(HIST("QAMC/hInvmassSecon"), lResonanceSecondary.M()); - histos.fill(HIST("QAMC/hpT_Seocondary"), lResonanceSecondary.Pt()); - - if constexpr (!IsResoMicrotrack) { - - auto trk1NSigmaPiTPC = trk1.tpcNSigmaPi(); - auto trk1NSigmaPiTOF = (isTrk1hasTOF) ? trk1.tofNSigmaPi() : -999.; - auto trk2NSigmaPiTPC = trk2.tpcNSigmaPi(); - auto trk2NSigmaPiTOF = (isTrk2hasTOF) ? trk2.tofNSigmaPi() : -999.; + histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - // PID QA primary pion - histos.fill(HIST("QAMC/trkppionTPCPID"), trk1pt, trk1NSigmaPiTPC); - if (isTrk1hasTOF) { - histos.fill(HIST("QAMC/trkppionTOFPID"), trk1pt, trk1NSigmaPiTOF); - histos.fill(HIST("QAMC/trkppionTPCTOFPID"), trk1NSigmaPiTPC, trk1NSigmaPiTOF); - } - histos.fill(HIST("QAMC/trkppionpT"), trk1pt); - histos.fill(HIST("QAMC/trkppionDCAxy"), trk1.dcaXY()); - histos.fill(HIST("QAMC/trkppionDCAz"), trk1.dcaZ()); - - // PID QA secondary pion - histos.fill(HIST("QAMC/trkspionTPCPID"), trk2pt, trk2NSigmaPiTPC); - if (isTrk2hasTOF) { - histos.fill(HIST("QAMC/trkspionTOFPID"), trk2pt, trk2NSigmaPiTOF); - histos.fill(HIST("QAMC/trkspionTPCTOFPID"), trk2NSigmaPiTPC, trk2NSigmaPiTOF); - } - histos.fill(HIST("QAMC/trkspionpT"), trk2pt); - histos.fill(HIST("QAMC/trkspionDCAxy"), trk2.dcaXY()); - histos.fill(HIST("QAMC/trkspionDCAz"), trk2.dcaZ()); - - } else { - - histos.fill(HIST("QAMC/trkppionTPCPID"), trk1pt, o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk1.pidNSigmaSelectionFlags())); - if (isTrk1hasTOF) { - histos.fill(HIST("QAMC/trkppionTOFPID"), trk1pt, o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk1.pidNSigmaSelectionFlags())); - histos.fill(HIST("QAMC/trkppionTPCTOFPID"), o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk1.pidNSigmaSelectionFlags()), o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk1.pidNSigmaSelectionFlags())); - } - histos.fill(HIST("QAMC/trkppionpT"), trk1pt); - histos.fill(HIST("QAMC/trkppionDCAxy"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAxy(trk1.trackSelectionFlags())); - histos.fill(HIST("QAMC/trkppionDCAz"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAz(trk1.trackSelectionFlags())); - - // PID QA secondary pion - histos.fill(HIST("QAMC/trkspionTPCPID"), trk2pt, o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk2.pidNSigmaSelectionFlags())); - if (isTrk2hasTOF) { - histos.fill(HIST("QAMC/trkspionTOFPID"), trk2pt, o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk2.pidNSigmaSelectionFlags())); - histos.fill(HIST("QAMC/trkspionTPCTOFPID"), o2::aod::resomicrodaughter::PidNSigma::getTPCnSigma(trk2.pidNSigmaSelectionFlags()), o2::aod::resomicrodaughter::PidNSigma::getTOFnSigma(trk2.pidNSigmaSelectionFlags())); - } - histos.fill(HIST("QAMC/trkspionpT"), trk2pt); - histos.fill(HIST("QAMC/trkspionDCAxy"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAxy(trk2.trackSelectionFlags())); - histos.fill(HIST("QAMC/trkspionDCAz"), o2::aod::resomicrodaughter::ResoMicroTrackSelFlag::decodeDCAz(trk2.trackSelectionFlags())); - } + // PID QA primary and secondary pion + fillPionQA(pion1, true); + fillPionQA(pion2, false); + fillKaonQA(bTrack); } else { histos.fill(HIST("k1invmass_MC_noK1"), lResonanceK1.M()); } @@ -641,106 +1229,134 @@ struct K1AnalysisMicro { } } // fillHistograms - void processResoTracks(aod::ResoCollision const& collision, - aod::ResoTracks const& resotracks) + template + bool passesEventCuts(const CollisionType& collision) + { + return !(eventCuts.cRecoINELgt0 && !collision.isRecINELgt0()); + } + + template + bool passesMCEventCuts(const CollisionType& collision) { + if (eventCuts.cMCINELgt0 && !collision.isINELgt0()) { + return false; + } + if (eventCuts.cMCVtxIn10 && !collision.isVtxIn10()) { + return false; + } + return true; + } + + void processResoTracks(ResoCollisions::iterator const& collision, + ResoTracks const& resotracks) + { + if (!passesEventCuts(collision)) { + return; + } fillHistograms(collision, resotracks, resotracks); } PROCESS_SWITCH(K1AnalysisMicro, processResoTracks, "Process ResoTracks", false); - void processResoMicroTracks(aod::ResoCollision const& collision, - aod::ResoMicroTracks const& resomicrotracks) + void processResoMicroTracks(ResoCollisions::iterator const& collision, + ResoMicroTracks const& resomicrotracks) { + if (!passesEventCuts(collision)) { + return; + } fillHistograms(collision, resomicrotracks, resomicrotracks); } PROCESS_SWITCH(K1AnalysisMicro, processResoMicroTracks, "Process ResoMicroTracks", true); - void processMC(aod::ResoCollision const& collision, - soa::Join const& resotracks) + void processMC(ResoMCCols::iterator const& collision, + ResoMCTracks const& resotracks) { + if (!passesEventCuts(collision) || !passesMCEventCuts(collision)) { + return; + } + histos.fill(HIST("MCReco/collisions"), 0.5); fillHistograms(collision, resotracks, resotracks); } PROCESS_SWITCH(K1AnalysisMicro, processMC, "Process Event for MC", false); - void processMCTrue(ResoMCCols::iterator const& collision, aod::ResoMCParents const& resoParents) + void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks) { - auto multiplicity = collision.cent(); + // The modular producer already selected these reconstructed collisions. + // Apply precisely the same reconstruction loop as the frozen data baseline. + if (!passesEventCuts(collision) || !passesMCEventCuts(collision)) { + return; + } + histos.fill(HIST("MCReco/collisions"), 0.5); + histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size()); + fillHistograms(collision, tracks, tracks); + } + PROCESS_SWITCH(K1AnalysisMicro, processMCMicro, "Process reconstructed MC with micro v001 tables", false); + + void processMCTrue(ResoMCCols::iterator const& collision, ResoMCParents const& resoParents) + { + if (!passesEventCuts(collision) || !passesMCEventCuts(collision)) { + return; + } + // Parents belong to selected reconstructed events; split reco collisions + // repeat parent sets. This is not an unconditional generated denominator. for (const auto& part : resoParents) { - if (std::abs(part.pdgCode()) != kK1Plus) + if (std::abs(part.pdgCode()) != Pdg::kK1_1270Plus) { continue; - if (std::abs(part.y()) > 0.5) { - continue; - } - bool pass1 = false; - bool pass2 = false; - bool pass3 = false; - bool pass4 = false; - if (std::abs(part.daughterPDG1()) == 313 || std::abs(part.daughterPDG2()) == 313) { // At least one decay into K892 - pass2 = true; } - if (std::abs(part.daughterPDG1()) == kPiPlus || std::abs(part.daughterPDG2()) == kPiPlus) { // At lest one decay into pion - pass1 = true; + const int charge = part.pdgCode() > 0 ? 1 : -1; + const int daughter1 = part.daughterPDG1(); + const int daughter2 = part.daughterPDG2(); + K1TruthChannel channel = K1TruthChannel::None; + if ((daughter1 == kRho770_0 && daughter2 == charge * kKPlus) || + (daughter2 == kRho770_0 && daughter1 == charge * kKPlus)) { + channel = K1TruthChannel::RhoK; + } else if ((daughter1 == charge * kK0Star892 && daughter2 == charge * kPiPlus) || + (daughter2 == charge * kK0Star892 && daughter1 == charge * kPiPlus)) { + channel = K1TruthChannel::KStarPi; } - if (std::abs(part.daughterPDG1()) == kPDGRho770 || std::abs(part.daughterPDG2()) == kPDGRho770) { - pass4 = true; - } - if (std::abs(part.daughterPDG1()) == kKPlus || std::abs(part.daughterPDG2()) == kKPlus) { - pass3 = true; - } - if (!pass1 || !pass2 || !pass3 || !pass4) // If we have both decay products + histos.fill(HIST("CutFlow/generated"), 0, static_cast(channel)); + if (part.y() < cK1MinRap || part.y() > cK1MaxRap) { continue; - auto typeNormal = part.pdgCode() > 0 ? BinAnti::kNormal : BinAnti::kAnti; - if (collision.isVtxIn10()) // INEL>10 - { - auto typeK1 = part.pdgCode() > 0 ? BinType::kK1P_GenINEL10 : BinType::kK1N_GenINEL10; - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, part.pt(), 1); - } - if (collision.isVtxIn10() && collision.isInSel8()) // INEL>10, vtx10 - { - auto typeK1 = part.pdgCode() > 0 ? BinType::kK1P_GenINELgt10 : BinType::kK1N_GenINELgt10; - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, part.pt(), 1); - } - if (collision.isVtxIn10() && collision.isTriggerTVX()) // vtx10, TriggerTVX - { - auto typeK1 = part.pdgCode() > 0 ? BinType::kK1P_GenTrig10 : BinType::kK1N_GenTrig10; - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, part.pt(), 1); - } - if (collision.isInAfterAllCuts()) // after all event selection - { - auto typeK1 = part.pdgCode() > 0 ? BinType::kK1P_GenEvtSel : BinType::kK1N_GenEvtSel; - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, part.pt(), 1); } + histos.fill(HIST("CutFlow/generated"), 1, static_cast(channel)); + // Keep other/unresolved immediate decays too; never require both pairs. + histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast(channel)); + histos.fill(HIST("MCGen/ptChannel"), static_cast(channel), part.pt()); } } - PROCESS_SWITCH(K1AnalysisMicro, processMCTrue, "Process Event for MC", false); + PROCESS_SWITCH(K1AnalysisMicro, processMCTrue, "Process generated K1 in selected events with v001 parents", false); // Processing Event Mixing using BinningTypeVtxZT0M = ColumnBinningPolicy; - void processME(o2::aod::ResoCollisions const& collisions, aod::ResoTracks const& resotracks) + void processME(ResoCollisions const& collisions, ResoTracks const& resotracks) { auto tracksTuple = std::make_tuple(resotracks); BinningTypeVtxZT0M colBinning{{cfgVtxBins, cfgMultBins}, true}; - SameKindPair pairs{colBinning, nEvtMixing, -1, collisions, tracksTuple, &cache}; // -1 is the number of the bin to skip + SameKindPair pairs{colBinning, nEvtMixing, -1, collisions, tracksTuple, &cache}; // -1 is the number of the bin to skip for (const auto& [collision1, tracks1, collision2, tracks2] : pairs) { + if (!passesEventCuts(collision1) || !passesEventCuts(collision2)) { + continue; + } fillHistograms(collision1, tracks1, tracks2); } }; PROCESS_SWITCH(K1AnalysisMicro, processME, "Process EventMixing light without partition", false); // Processing Event Mixing -- Micro - // using BinningTypeVtxZT0M = ColumnBinningPolicy; - void processMEMicro(o2::aod::ResoCollisions const& collisions, aod::ResoMicroTracks const& resomicrotracks) + void processMEMicro(ResoCollisions const& collisions, ResoMicroTracks const& resomicrotracks) { auto tracksTuple = std::make_tuple(resomicrotracks); BinningTypeVtxZT0M colBinning{{cfgVtxBins, cfgMultBins}, true}; - SameKindPair pairs{colBinning, nEvtMixing, -1, collisions, tracksTuple, &cache}; // -1 is the number of the bin to skip + SameKindPair pairs{colBinning, nEvtMixing, -1, collisions, tracksTuple, &cache}; // -1 is the number of the bin to skip for (const auto& [collision1, tracks1, collision2, tracks2] : pairs) { + if (!passesEventCuts(collision1) || !passesEventCuts(collision2)) { + continue; + } fillHistograms(collision1, tracks1, tracks2); } }; - PROCESS_SWITCH(K1AnalysisMicro, processMEMicro, "Process EventMixing light without partition", true); + PROCESS_SWITCH(K1AnalysisMicro, processMEMicro, "Process EventMixing light without partition", false); }; // struct WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) From 2f72f4bb77a4dd1d82a426f05b4d97cd361cdeec Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Wed, 30 Sep 2026 06:34:20 +0900 Subject: [PATCH 2/9] [PWGLF] Fix O2 linter warnings in K1 micro analysis - Use const references in range-based loops over configured PID cuts - Replace magic numbers in init checks with named constants --- PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx index 4f1d4d58496..f8410f25e04 100644 --- a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx +++ b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx @@ -122,6 +122,11 @@ struct K1AnalysisMicro { static constexpr double PIDGridStep = 0.25; static constexpr double PIDGridMax = 3.5; static constexpr double GridTolerance = 1e-4; + static constexpr std::size_t MinPtBinEdges = 2; // a pT dependent PID table needs at least one bin + static constexpr float ProducerDCAPtP0 = 0.004f; // resonanceModuleInitializer cfgTightDCAOffset default + static constexpr float ProducerDCAPtCoeff = 0.013f; // resonanceModuleInitializer cfgTightDCAPtCoefficient default + static constexpr float ProducerDCAPtPower = 1.f; // resonanceModuleInitializer cfgTightDCAPtPower default + static constexpr float ConfigTolerance = 1e-6f; static constexpr int NCandidateStages = 12; SliceCache cache; @@ -297,14 +302,14 @@ struct K1AnalysisMicro { // Consistency of the pT dependent PID configuration if (pionPID.cPionUsePtDepPID) { const auto& bins = pionPID.cPionPIDPtBins.value; - if (bins.size() < 2 || pionPID.cPionTPCNSigmaCuts.value.size() != bins.size() - 1 || + if (bins.size() < MinPtBinEdges || pionPID.cPionTPCNSigmaCuts.value.size() != bins.size() - 1 || pionPID.cPionTOFNSigmaCuts.value.size() != bins.size() - 1 || pionPID.cPionTOFRequired.value.size() != bins.size() - 1) { LOG(fatal) << "Pion pT dependent PID vectors must have (number of pT bin edges - 1) entries"; } } if (kaonPID.cKaonUsePtDepPID) { const auto& bins = kaonPID.cKaonPIDPtBins.value; - if (bins.size() < 2 || kaonPID.cKaonTPCNSigmaCuts.value.size() != bins.size() - 1 || + if (bins.size() < MinPtBinEdges || kaonPID.cKaonTPCNSigmaCuts.value.size() != bins.size() - 1 || kaonPID.cKaonTOFNSigmaCuts.value.size() != bins.size() - 1 || kaonPID.cKaonTOFRequired.value.size() != bins.size() - 1) { LOG(fatal) << "Kaon pT dependent PID vectors must have (number of pT bin edges - 1) entries"; } @@ -329,7 +334,7 @@ struct K1AnalysisMicro { }; if (trackCuts.cfgUsePtDepDCA) { LOG(info) << "Micro tracks use the producer pT dependent DCA flags (0.004 + 0.013 / pT); cDCAToPVByPt* are ignored"; - if (std::abs(trackCuts.cDCAToPVByPtP0 - 0.004f) > 1e-6f || std::abs(trackCuts.cDCAToPVByPtCoeff - 0.013f) > 1e-6f || std::abs(trackCuts.cDCAToPVByPtPower - 1.f) > 1e-6f) { + if (std::abs(trackCuts.cDCAToPVByPtP0 - ProducerDCAPtP0) > ConfigTolerance || std::abs(trackCuts.cDCAToPVByPtCoeff - ProducerDCAPtCoeff) > ConfigTolerance || std::abs(trackCuts.cDCAToPVByPtPower - ProducerDCAPtPower) > ConfigTolerance) { LOG(warning) << "cDCAToPVByPt* differ from the producer defaults, but micro tracks always use the producer formula"; } } else { @@ -356,24 +361,24 @@ struct K1AnalysisMicro { checkPIDGrid("cMaxTOFnSigmaKaon", kaonPID.cMaxTOFnSigmaKaon); } if (pionPID.cPionUsePtDepPID) { - for (const auto cut : pionPID.cPionTPCNSigmaCuts.value) { + for (const auto& cut : pionPID.cPionTPCNSigmaCuts.value) { if (isCutEnabled(cut)) { checkPIDGrid("cPionTPCNSigmaCuts", cut); } } - for (const auto cut : pionPID.cPionTOFNSigmaCuts.value) { + for (const auto& cut : pionPID.cPionTOFNSigmaCuts.value) { if (isCutEnabled(cut)) { checkPIDGrid("cPionTOFNSigmaCuts", cut); } } } if (kaonPID.cKaonUsePtDepPID) { - for (const auto cut : kaonPID.cKaonTPCNSigmaCuts.value) { + for (const auto& cut : kaonPID.cKaonTPCNSigmaCuts.value) { if (isCutEnabled(cut)) { checkPIDGrid("cKaonTPCNSigmaCuts", cut); } } - for (const auto cut : kaonPID.cKaonTOFNSigmaCuts.value) { + for (const auto& cut : kaonPID.cKaonTOFNSigmaCuts.value) { if (isCutEnabled(cut)) { checkPIDGrid("cKaonTOFNSigmaCuts", cut); } From e0e3992d103a1f9c1d681fe23c8df6bf22b7ef4d Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Sun, 4 Oct 2026 09:53:29 +0900 Subject: [PATCH 3/9] [PWGLF] Move the K1 micro analysis selection into PWGLF/Core/K1AnalysisMicroCore.h Split the K1(1270) microtrack histogram task into a reusable core header and a thin workflow, so that other K1 workflows can share one selection and candidate loop implementation instead of copying it. - PWGLF/Core/K1AnalysisMicroCore.h (new): event, track, PID, secondary and candidate configurable groups (no group prefix, so all JSON keys and defaults are unchanged), the track quality/PID stages, the quantised DCA/nSigma grid checks, the sibling-based truth classification, the unordered triplet loop with its selection cache and cut-flow histograms, and the histogram registration. No using-directives, no runDataProcessing.h. - k1AnalysisMicro.cxx: struct K1AnalysisMicro now owns the configurable groups, the histogram registry and the process functions, and calls the core. Process switches, configurable names/defaults, histogram names and the selection are unchanged. --- PWGLF/Core/K1AnalysisMicroCore.h | 1315 ++++++++++++++++++++ PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx | 1258 +------------------ 2 files changed, 1351 insertions(+), 1222 deletions(-) create mode 100644 PWGLF/Core/K1AnalysisMicroCore.h diff --git a/PWGLF/Core/K1AnalysisMicroCore.h b/PWGLF/Core/K1AnalysisMicroCore.h new file mode 100644 index 00000000000..63b24667c37 --- /dev/null +++ b/PWGLF/Core/K1AnalysisMicroCore.h @@ -0,0 +1,1315 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file K1AnalysisMicroCore.h +/// \brief Shared selection, truth classification and candidate loop of the K1(1270) resonance tasks +/// \author Su-Jeong Ji , Bong-Hwi Lim +/// + +#ifndef PWGLF_CORE_K1ANALYSISMICROCORE_H_ +#define PWGLF_CORE_K1ANALYSISMICROCORE_H_ + +#include "PWGLF/DataModel/LFResonanceTables.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace o2::analysis::k1micro +{ + +enum class K1TruthChannel { + None = 0, + RhoK = 1, + KStarPi = 2 +}; + +enum BinAnti : unsigned int { + kNormal = 0, + kAnti, + kNAEnd +}; + +enum BinType : unsigned int { + kK1P = 0, + kK1N, + kK1P_Mix, + kK1N_Mix, + kK1P_GenINEL10, + kK1N_GenINEL10, + kK1P_GenINELgt10, + kK1N_GenINELgt10, + kK1P_GenTrig10, + kK1N_GenTrig10, + kK1P_GenEvtSel, + kK1N_GenEvtSel, + kK1P_Rec, + kK1N_Rec, + kTYEnd +}; + +enum class Species : int { + Pion = 0, + Kaon = 1 +}; + +// Last stage passed by a track; the cut-flow histogram is filled directly from this value. +enum TrackStage : int { + kTrkInput = 0, + kTrkPt, + kTrkEta, + kTrkDCAxy, + kTrkDCAz, + kTrkFlags, + kTrkClusters, + kTrkTOFRequired, + kTrkPID, + kTrkNStages +}; + +enum class QAFolder { + Before, // QA/*: before the candidate cuts + After, // QAcut/*: after the candidate cuts + MC // QAMC/*: matched K1 truth candidates +}; + +// Resolved PID cut of one species at a given pT. +struct PIDCut { + double tpcMax = 0.; + double tofMax = 0.; + double combined = 0.; + bool tofRequired = false; +}; + +inline constexpr float DisabledCut = -999.f; // an optional cut with this value is off and not evaluated +inline constexpr double MassRho770 = 0.77526; // PDG 2024, not available in o2::constants::physics +inline constexpr double DCAGridStep = 0.025; // v001 micro DCA encoding, lower-inclusive bins up to DCAGridMax +inline constexpr double DCAGridMax = 0.15; +inline constexpr double PIDGridStart = 2.0; // v001 micro nSigma encoding: 0.25 bins in [2.0, 3.5] +inline constexpr double PIDGridStep = 0.25; +inline constexpr double PIDGridMax = 3.5; +inline constexpr double GridTolerance = 1e-4; +inline constexpr std::size_t MinPtBinEdges = 2; // a pT dependent PID table needs at least one bin +inline constexpr float ProducerDCAPtP0 = 0.004f; // resonanceModuleInitializer cfgTightDCAOffset default +inline constexpr float ProducerDCAPtCoeff = 0.013f; // resonanceModuleInitializer cfgTightDCAPtCoefficient default +inline constexpr float ProducerDCAPtPower = 1.f; // resonanceModuleInitializer cfgTightDCAPtPower default +inline constexpr float ConfigTolerance = 1e-6f; +inline constexpr int NCandidateStages = 12; +inline constexpr int NTruthChannels = 3; + +// Configurable groups without prefix: the JSON keys are the plain configurable names. + +/// Event selection +struct EventCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cRecoINELgt0{"cRecoINELgt0", false, "Apply reconstructed INEL>0 selection"}; + o2::framework::Configurable cMCINELgt0{"cMCINELgt0", false, "Require generator INEL>0 in MC processes"}; + o2::framework::Configurable cMCVtxIn10{"cMCVtxIn10", false, "Require generator |vz| < 10 cm in MC processes"}; +}; + +/// Track selections (common for pion and kaon, -999 switches an optional cut off) +struct TrackCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cMinPtcut{"cMinPtcut", 0.15, "Track minium pt cut"}; + o2::framework::Configurable cMaxEtacut{"cMaxEtacut", -999.f, "Track maximum |eta| cut (-999: off)"}; + // DCAr to PV + o2::framework::Configurable cMaxDCArToPVcut{"cMaxDCArToPVcut", 0.1, "Track DCAr cut to PV Maximum"}; + // DCAz to PV + o2::framework::Configurable cMaxDCAzToPVcut{"cMaxDCAzToPVcut", 0.1, "Track DCAz cut to PV Maximum"}; + o2::framework::Configurable cMinDCAzToPVcut{"cMinDCAzToPVcut", 0.0, "Track DCAz cut to PV Minimum"}; + o2::framework::Configurable cfgUsePtDepDCA{"cfgUsePtDepDCA", false, "Use pT dependent DCA cut instead of the fixed maximum"}; + o2::framework::Configurable cDCAToPVByPtP0{"cDCAToPVByPtP0", 0.004f, "pT dependent DCA cut = P0 + coefficient / pT^power (cm)"}; + o2::framework::Configurable cDCAToPVByPtCoeff{"cDCAToPVByPtCoeff", 0.013f, "Coefficient in the pT dependent DCA cut"}; + o2::framework::Configurable cDCAToPVByPtPower{"cDCAToPVByPtPower", 1.f, "Power in the pT dependent DCA cut"}; + o2::framework::Configurable cfgPrimaryTrack{"cfgPrimaryTrack", true, "Primary track selection"}; // kGoldenChi2 | kDCAxy | kDCAz + o2::framework::Configurable cfgGlobalWoDCATrack{"cfgGlobalWoDCATrack", true, "Global track selection without DCA"}; // kQualityTracks (kTrackType | kTPCNCls | kTPCCrossedRows | kTPCCrossedRowsOverNCls | kTPCChi2NDF | kTPCRefit | kITSNCls | kITSChi2NDF | kITSRefit | kITSHits) | kInAcceptanceTracks (kPtRange | kEtaRange) + o2::framework::Configurable cfgGlobalTrack{"cfgGlobalTrack", false, "Global track selection"}; // kGoldenChi2 | kDCAxy | kDCAz + o2::framework::Configurable cfgPVContributor{"cfgPVContributor", false, "PV contributor track selection"}; // PV Contriuibutor + o2::framework::Configurable cfgUseTPCRefit{"cfgUseTPCRefit", false, "Require TPC Refit"}; + o2::framework::Configurable cfgUseITSRefit{"cfgUseITSRefit", false, "Require ITS Refit"}; + o2::framework::Configurable cfgTPCcluster{"cfgTPCcluster", 0, "Number of TPC cluster (found clusters, ResoTracks only)"}; + o2::framework::Configurable cfgTPCCrossedRowsMin{"cfgTPCCrossedRowsMin", 0, "Minimum number of TPC crossed rows"}; + o2::framework::Configurable cfgITSNClsMin{"cfgITSNClsMin", 0, "Minimum number of ITS clusters (ResoMicroTracks only)"}; + o2::framework::Configurable cfgHasTOF{"cfgHasTOF", false, "Require TOF"}; +}; + +/// Pion PID selection +struct PionPidCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cMaxTPCnSigmaPion{"cMaxTPCnSigmaPion", 3.0, "TPC nSigma cut for Pion (-999: off)"}; // TPC + o2::framework::Configurable cMaxTOFnSigmaPion{"cMaxTOFnSigmaPion", 3.0, "TOF nSigma cut for Pion (-999: off)"}; // TOF + o2::framework::Configurable nsigmaCutCombinedPion{"nsigmaCutCombinedPion", -999, "Combined nSigma cut for Pion"}; // Combined + o2::framework::Configurable cUseOnlyTOFTrackPi{"cUseOnlyTOFTrackPi", false, "Use only TOF track for PID selection"}; // Use only TOF track for Pion PID selection + o2::framework::Configurable cPionUsePtDepPID{"cPionUsePtDepPID", false, "Use pT-dependent PID cuts for pion"}; + o2::framework::Configurable> cPionPIDPtBins{"cPionPIDPtBins", {0.0f, 0.5f, 0.8f, 2.0f, 999.0f}, "pT bin edges for pion PID cuts"}; + o2::framework::Configurable> cPionTPCNSigmaCuts{"cPionTPCNSigmaCuts", {3.0f, 3.0f, 2.0f, 2.0f}, "TPC NSigma cuts per pT bin (pion)"}; + o2::framework::Configurable> cPionTOFNSigmaCuts{"cPionTOFNSigmaCuts", {3.0f, 3.0f, 3.0f, 3.0f}, "TOF NSigma cuts per pT bin (pion)"}; + o2::framework::Configurable> cPionTOFRequired{"cPionTOFRequired", {0, 0, 1, 1}, "Require TOF per pT bin (pion)"}; +}; + +/// Kaon PID selection +struct KaonPidCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cMaxTPCnSigmaKaon{"cMaxTPCnSigmaKaon", 3.0, "TPC nSigma cut for Kaon (-999: off)"}; // TPC + o2::framework::Configurable cMaxTOFnSigmaKaon{"cMaxTOFnSigmaKaon", 3.0, "TOF nSigma cut for Kaon (-999: off)"}; // TOF + o2::framework::Configurable nsigmaCutCombinedKaon{"nsigmaCutCombinedKaon", -999, "Combined nSigma cut for Kaon"}; // Combined + o2::framework::Configurable cUseOnlyTOFTrackKa{"cUseOnlyTOFTrackKa", false, "Use only TOF track for PID selection"}; // Use only TOF track for Kaon PID selection + o2::framework::Configurable cKaonUsePtDepPID{"cKaonUsePtDepPID", false, "Use pT-dependent PID cuts for kaon"}; + o2::framework::Configurable> cKaonPIDPtBins{"cKaonPIDPtBins", {0.0f, 0.5f, 0.8f, 2.0f, 999.0f}, "pT bin edges for kaon PID cuts"}; + o2::framework::Configurable> cKaonTPCNSigmaCuts{"cKaonTPCNSigmaCuts", {3.0f, 3.0f, 2.0f, 2.0f}, "TPC NSigma cuts per pT bin (kaon)"}; + o2::framework::Configurable> cKaonTOFNSigmaCuts{"cKaonTOFNSigmaCuts", {3.0f, 3.0f, 3.0f, 3.0f}, "TOF NSigma cuts per pT bin (kaon)"}; + o2::framework::Configurable> cKaonTOFRequired{"cKaonTOFRequired", {0, 0, 1, 1}, "Require TOF per pT bin (kaon)"}; +}; + +/// Secondary selection (-999 switches a cut off; the values it needs are then not computed) +struct SecondaryCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cMinSecondaryPtCut{"cMinSecondaryPtCut", 0.5, "Min pT cut for secondary selection"}; + o2::framework::Configurable cfgModeK892orRho{"cfgModeK892orRho", false, "Secondary scenario for K892 (true) or Rho (false)"}; + o2::framework::Configurable cSecondaryMasswindow{"cSecondaryMasswindow", -999, "Secondary inv mass selection window"}; + o2::framework::Configurable cMinAnotherSecondaryMassCut{"cMinAnotherSecondaryMassCut", -999, "Min inv. mass selection of another secondary scenario"}; + o2::framework::Configurable cMaxAnotherSecondaryMassCut{"cMaxAnotherSecondaryMassCut", -999, "MAx inv. mass selection of another secondary scenario"}; + o2::framework::Configurable cMinPiKaMassCut{"cMinPiKaMassCut", -999, "bPion-Kaon pair inv mass selection minimum"}; + o2::framework::Configurable cMaxPiKaMassCut{"cMaxPiKaMassCut", -999, "bPion-Kaon pair inv mass selection maximum"}; + o2::framework::Configurable cMinAngle{"cMinAngle", -999, "Minimum angle between the secondary resonance and the bachelor"}; + o2::framework::Configurable cMaxAngle{"cMaxAngle", -999, "Maximum angle between the secondary resonance and the bachelor"}; + o2::framework::Configurable cMinPairAsym{"cMinPairAsym", -999, "Minimum pair asymmetry"}; + o2::framework::Configurable cMaxPairAsym{"cMaxPairAsym", -999, "Maximum pair asymmetry"}; +}; + +/// Common TOF switch and K1 selection +struct CandidateCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cByPassTOF{"cByPassTOF", false, "Bypass the TOF nSigma selection"}; + o2::framework::Configurable cK1MaxRap{"cK1MaxRap", 0.5, "K1 maximum rapidity"}; + o2::framework::Configurable cK1MinRap{"cK1MinRap", -0.5, "K1 minimum rapidity"}; +}; + +/// Histogram binning, QA and debug output +struct HistogramOptions : o2::framework::ConfigurableGroup { + o2::framework::Configurable cNbinsDiv{"cNbinsDiv", 1, "Integer to divide the number of bins"}; + o2::framework::Configurable additionalQAplots{"additionalQAplots", true, "Additional QA plots"}; + o2::framework::Configurable cfgTruthDebug{"cfgTruthDebug", 0, "Maximum logged matched candidates per truth channel"}; +}; + +/// Process functions enabled in the task; they decide the configuration checks and the registered histograms. +struct ProcessModes { + bool microTracks = false; // any process function reading micro tracks (quantised DCA and nSigma) + bool mcReco = false; // reconstructed MC with full or micro tracks + bool mcRecoMicro = false; // reconstructed MC with micro tracks + bool mcGen = false; // generated K1 parents in selected reconstructed events +}; + +// A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value). +inline bool isCutEnabled(float value) +{ + return value > DisabledCut + 1.f; +} + +// v001 micro values are lower-inclusive bin edges: a maximum cut on the grid keeps bins below it. +inline bool passesBinnedMax(double decoded, double cut) +{ + return decoded < cut - o2::constants::math::Epsilon; +} + +// Minimum cut on the grid keeps the bin starting at the cut. +inline bool passesBinnedMin(double decoded, double cut) +{ + return decoded >= cut - o2::constants::math::Epsilon; +} + +template +bool passesMax(double value, double cut) +{ + if constexpr (IsResoMicrotrack) { + return passesBinnedMax(value, cut); + } else { + return value < cut; + } +} + +inline bool isInRange(double value, double minimum, double maximum) +{ + if (isCutEnabled(minimum) && value < minimum) { + return false; + } + if (isCutEnabled(maximum) && value > maximum) { + return false; + } + return true; +} + +inline bool isInWindow(double value, double center, double width) +{ + return std::abs(value - center) < width; +} + +// Preserve pT-bin membership [low, high). +inline int getPtBinIndex(float pt, const std::vector& ptBins) +{ + for (std::size_t i = 1; i < ptBins.size(); ++i) { + if (pt >= ptBins[i - 1] && pt < ptBins[i]) { + return static_cast(i - 1); + } + } + return -1; +} + +// Truth classification from the immediate mothers and the sibling IDs of the reconstructed daughters. +template +bool hasSibling(const Track& directDaughter, int resonanceId) +{ + if (resonanceId < 0) { + return false; + } + const auto siblings = directDaughter.siblingIds(); + return siblings[0] == resonanceId || siblings[1] == resonanceId; +} + +template +bool matchesKStarPi(const Track& resonancePion, const Track& directPion, const Kaon& kaon) +{ + const int charge = kaon.pdgCode() > 0 ? 1 : -1; + if (resonancePion.motherId() != kaon.motherId() || + resonancePion.motherId() == directPion.motherId()) { + return false; + } + if (resonancePion.motherPDG() != charge * o2::constants::physics::Pdg::kK0Star892 || kaon.motherPDG() != charge * o2::constants::physics::Pdg::kK0Star892) { + return false; + } + if (resonancePion.pdgCode() != -charge * kPiPlus || directPion.pdgCode() != charge * kPiPlus || + directPion.motherPDG() != charge * o2::constants::physics::Pdg::kK1_1270Plus) { + return false; + } + return hasSibling(directPion, kaon.motherId()); +} + +template +K1TruthChannel classifyK1Truth(const Track& pion1, const Track& pion2, const Kaon& kaon) +{ + if (std::abs(pion1.pdgCode()) != kPiPlus || std::abs(pion2.pdgCode()) != kPiPlus || + std::abs(kaon.pdgCode()) != kKPlus) { + return K1TruthChannel::None; + } + if (pion1.motherId() < 0 || pion2.motherId() < 0 || kaon.motherId() < 0) { + return K1TruthChannel::None; + } + const int charge = kaon.pdgCode() > 0 ? 1 : -1; + const bool rhoPions = pion1.motherId() == pion2.motherId() && + pion1.motherPDG() == kRho770_0 && pion2.motherPDG() == kRho770_0 && + pion1.pdgCode() == -pion2.pdgCode(); + if (rhoPions && kaon.motherPDG() == charge * o2::constants::physics::Pdg::kK1_1270Plus && + kaon.motherId() != pion1.motherId() && hasSibling(kaon, pion1.motherId())) { + return K1TruthChannel::RhoK; + } + if (matchesKStarPi(pion1, pion2, kaon) || matchesKStarPi(pion2, pion1, kaon)) { + return K1TruthChannel::KStarPi; + } + return K1TruthChannel::None; +} + +// Immediate decay channel of a generated K1 from the PDG codes of its two daughters. +inline K1TruthChannel classifyGeneratedK1(int charge, int daughter1, int daughter2) +{ + if ((daughter1 == kRho770_0 && daughter2 == charge * kKPlus) || + (daughter2 == kRho770_0 && daughter1 == charge * kKPlus)) { + return K1TruthChannel::RhoK; + } + if ((daughter1 == charge * o2::constants::physics::Pdg::kK0Star892 && daughter2 == charge * kPiPlus) || + (daughter2 == charge * o2::constants::physics::Pdg::kK0Star892 && daughter1 == charge * kPiPlus)) { + return K1TruthChannel::KStarPi; + } + return K1TruthChannel::None; +} + +// Track QA of a pion; isPrimary selects the trkppion (first) or trkspion (second) histograms +template +void fillPionQA(o2::framework::HistogramRegistry& histos, const TrackType& track, bool isPrimary) +{ + const bool hasTOF = track.hasTOF(); + if (isPrimary) { + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QA/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QA/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QA/trkppionpT"), track.pt()); + histos.fill(HIST("QA/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkppionDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAcut/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAcut/trkppionpT"), track.pt()); + histos.fill(HIST("QAcut/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkppionDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAMC/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAMC/trkppionpT"), track.pt()); + histos.fill(HIST("QAMC/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkppionDCAz"), track.dcaZ()); + } + } else { + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QA/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QA/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QA/trkspionpT"), track.pt()); + histos.fill(HIST("QA/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkspionDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAcut/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAcut/trkspionpT"), track.pt()); + histos.fill(HIST("QAcut/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkspionDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAMC/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAMC/trkspionpT"), track.pt()); + histos.fill(HIST("QAMC/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkspionDCAz"), track.dcaZ()); + } + } +} + +// Track QA of the bachelor kaon +template +void fillKaonQA(o2::framework::HistogramRegistry& histos, const TrackType& track) +{ + const bool hasTOF = track.hasTOF(); + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QA/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QA/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QA/trkkaonpT"), track.pt()); + histos.fill(HIST("QA/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkkaonDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QAcut/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QAcut/trkkaonpT"), track.pt()); + histos.fill(HIST("QAcut/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkkaonDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QAMC/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QAMC/trkkaonpT"), track.pt()); + histos.fill(HIST("QAMC/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkkaonDCAz"), track.dcaZ()); + } +} + +/// Selection and candidate loop shared by the K1 tasks. +/// The task owns the configurable groups and the histogram registry and passes them in init(). +class K1AnalysisMicroCore +{ + public: + void init(o2::framework::HistogramRegistry& histos, + EventCuts const& eventCuts, TrackCuts const& trackCuts, + PionPidCuts const& pionPidCuts, KaonPidCuts const& kaonPidCuts, + SecondaryCuts const& secondaryCuts, CandidateCuts const& candidateCuts, + HistogramOptions const& histogramOptions, ProcessModes const& modes) + { + mEventCuts = eventCuts; + mTrackCuts = trackCuts; + mPionPid = pionPidCuts; + mKaonPid = kaonPidCuts; + mSecondaryCuts = secondaryCuts; + mCandidateCuts = candidateCuts; + mHistogramOptions = histogramOptions; + mTruthDebugCounts = {}; + + mSecondaryWindowOn = isCutEnabled(mSecondaryCuts.cSecondaryMasswindow); + mAnotherMassCutOn = isCutEnabled(mSecondaryCuts.cMinAnotherSecondaryMassCut) || isCutEnabled(mSecondaryCuts.cMaxAnotherSecondaryMassCut); + mPiKaMassCutOn = isCutEnabled(mSecondaryCuts.cMinPiKaMassCut) || isCutEnabled(mSecondaryCuts.cMaxPiKaMassCut); + mAngleCutOn = isCutEnabled(mSecondaryCuts.cMinAngle) || isCutEnabled(mSecondaryCuts.cMaxAngle); + mPairAsymCutOn = isCutEnabled(mSecondaryCuts.cMinPairAsym) || isCutEnabled(mSecondaryCuts.cMaxPairAsym); + + checkConfiguration(modes); + registerHistograms(histos, modes); + } + + template + bool passesEventCuts(const CollisionType& collision) + { + return !(mEventCuts.cRecoINELgt0 && !collision.isRecINELgt0()); + } + + template + bool passesMCEventCuts(const CollisionType& collision) + { + if (mEventCuts.cMCINELgt0 && !collision.isINELgt0()) { + return false; + } + if (mEventCuts.cMCVtxIn10 && !collision.isVtxIn10()) { + return false; + } + return true; + } + + // Resolve the PID cut of one species at a given pT; false if the pT is outside all pT-dependent bins. + template + bool getPIDCut(float pt, PIDCut& cut) + { + if constexpr (S == Species::Pion) { + cut.tpcMax = mPionPid.cMaxTPCnSigmaPion; + cut.tofMax = mPionPid.cMaxTOFnSigmaPion; + cut.combined = mPionPid.nsigmaCutCombinedPion; + cut.tofRequired = false; + if (mPionPid.cPionUsePtDepPID) { + const int ptBin = getPtBinIndex(pt, mPionPid.cPionPIDPtBins.value); + if (ptBin < 0) { + return false; + } + const auto bin = static_cast(ptBin); + cut.tpcMax = mPionPid.cPionTPCNSigmaCuts.value[bin]; + cut.tofMax = mPionPid.cPionTOFNSigmaCuts.value[bin]; + cut.tofRequired = mPionPid.cPionTOFRequired.value[bin] != 0; + } + } else { + cut.tpcMax = mKaonPid.cMaxTPCnSigmaKaon; + cut.tofMax = mKaonPid.cMaxTOFnSigmaKaon; + cut.combined = mKaonPid.nsigmaCutCombinedKaon; + cut.tofRequired = false; + if (mKaonPid.cKaonUsePtDepPID) { + const int ptBin = getPtBinIndex(pt, mKaonPid.cKaonPIDPtBins.value); + if (ptBin < 0) { + return false; + } + const auto bin = static_cast(ptBin); + cut.tpcMax = mKaonPid.cKaonTPCNSigmaCuts.value[bin]; + cut.tofMax = mKaonPid.cKaonTOFNSigmaCuts.value[bin]; + cut.tofRequired = mKaonPid.cKaonTOFRequired.value[bin] != 0; + } + } + return true; + } + + // Track quality selection shared by pion and kaon. Returns the last stage that was passed. + // Full tracks store exact values; micro tracks store quantised DCA (see LFResonanceTables.h). + template + int trackQualityStage(const TrackType& track) + { + const double pt = track.pt(); + const double dcaXY = track.dcaXY(); + const double dcaZ = track.dcaZ(); + // Invalid micro DCA codes decode to NaN + if (!std::isfinite(pt) || !std::isfinite(track.eta()) || !std::isfinite(dcaXY) || !std::isfinite(dcaZ)) { + return kTrkInput; + } + if (std::abs(pt) < mTrackCuts.cMinPtcut) { + return kTrkInput; + } + if (isCutEnabled(mTrackCuts.cMaxEtacut) && !(std::abs(track.eta()) < mTrackCuts.cMaxEtacut)) { + return kTrkPt; + } + + if (mTrackCuts.cfgUsePtDepDCA) { + if constexpr (IsResoMicrotrack) { + if (!track.passedPtDependentDCAxy()) { + return kTrkEta; + } + if (!track.passedPtDependentDCAz()) { + return kTrkDCAxy; + } + } else { + const double dcaPtCut = mTrackCuts.cDCAToPVByPtP0 + mTrackCuts.cDCAToPVByPtCoeff * std::pow(pt, -static_cast(mTrackCuts.cDCAToPVByPtPower)); + if (!(std::abs(dcaXY) < dcaPtCut)) { + return kTrkEta; + } + if (!(std::abs(dcaZ) < dcaPtCut)) { + return kTrkDCAxy; + } + } + } else { + if (isCutEnabled(mTrackCuts.cMaxDCArToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMax(dcaXY, mTrackCuts.cMaxDCArToPVcut)) { + return kTrkEta; + } + } else { + if (!(std::abs(dcaXY) <= mTrackCuts.cMaxDCArToPVcut)) { + return kTrkEta; + } + } + } + if (isCutEnabled(mTrackCuts.cMaxDCAzToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMax(dcaZ, mTrackCuts.cMaxDCAzToPVcut)) { + return kTrkDCAxy; + } + } else { + if (!(std::abs(dcaZ) <= mTrackCuts.cMaxDCAzToPVcut)) { + return kTrkDCAxy; + } + } + } + } + if (isCutEnabled(mTrackCuts.cMinDCAzToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMin(dcaZ, mTrackCuts.cMinDCAzToPVcut)) { + return kTrkDCAxy; + } + } else { + if (!(std::abs(dcaZ) >= mTrackCuts.cMinDCAzToPVcut)) { + return kTrkDCAxy; + } + } + } + + // Track flags + if ((mTrackCuts.cfgPrimaryTrack && !track.isPrimaryTrack()) || + (mTrackCuts.cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) || + (mTrackCuts.cfgGlobalTrack && !track.isGlobalTrack()) || + (mTrackCuts.cfgPVContributor && !track.isPVContributor()) || + (mTrackCuts.cfgUseITSRefit && !track.passedITSRefit()) || + (mTrackCuts.cfgUseTPCRefit && !track.passedTPCRefit())) { + return kTrkDCAz; + } + + // Clusters: found clusters exist only in ResoTracks, ITS clusters only in ResoMicroTracks + if constexpr (!IsResoMicrotrack) { + if constexpr (requires { track.tpcNClsFound(); }) { + if (track.tpcNClsFound() < mTrackCuts.cfgTPCcluster) { + return kTrkFlags; + } + } + } + if constexpr (requires { track.tpcNClsCrossedRows(); }) { + if (track.tpcNClsCrossedRows() < mTrackCuts.cfgTPCCrossedRowsMin) { + return kTrkFlags; + } + } + if constexpr (IsResoMicrotrack) { + if constexpr (requires { track.itsNCls(); }) { + if (track.itsNCls() < mTrackCuts.cfgITSNClsMin) { + return kTrkFlags; + } + } + } + return kTrkClusters; + } + + // TOF signal requirement of the track (global, per species, or per pT bin) + template + bool passesTOFRequired(const TrackType& track) + { + bool required = mTrackCuts.cfgHasTOF; + if constexpr (S == Species::Pion) { + required = required || mPionPid.cUseOnlyTOFTrackPi; + } else { + required = required || mKaonPid.cUseOnlyTOFTrackKa; + } + PIDCut cut; + // A pT outside all bins is rejected by passesPID + if (!mCandidateCuts.cByPassTOF && getPIDCut(track.pt(), cut) && cut.tofRequired) { + required = true; + } + return !required || track.hasTOF(); + } + + // PID selection: the same code for full and micro tracks, only the comparison is quantisation aware + template + bool passesPID(const TrackType& track) + { + PIDCut cut; + if (!getPIDCut(track.pt(), cut)) { + return false; + } + const bool hasTOF = track.hasTOF(); + double tpcNSigma = std::numeric_limits::quiet_NaN(); + double tofNSigma = std::numeric_limits::quiet_NaN(); // TOF value is only valid with hasTOF + if constexpr (S == Species::Pion) { + tpcNSigma = track.tpcNSigmaPi(); + if (hasTOF) { + tofNSigma = track.tofNSigmaPi(); + } + } else { + tpcNSigma = track.tpcNSigmaKa(); + if (hasTOF) { + tofNSigma = track.tofNSigmaKa(); + } + } + if (isCutEnabled(cut.tpcMax) && !passesMax(std::abs(tpcNSigma), cut.tpcMax)) { + return false; + } + // Missing TOF is handled by passesTOFRequired; here the TPC alone decides + if (mCandidateCuts.cByPassTOF || !hasTOF) { + return true; + } + bool tofPassed = !isCutEnabled(cut.tofMax) || passesMax(std::abs(tofNSigma), cut.tofMax); + if (!tofPassed && cut.combined > 0 && tpcNSigma * tpcNSigma + tofNSigma * tofNSigma < cut.combined * cut.combined) { + tofPassed = true; + } + return tofPassed; + } + + // Full selection stage of a track (quality, TOF requirement, PID) + template + int trackSelectionStage(const TrackType& track) + { + const int qualityStage = trackQualityStage(track); + if (qualityStage < kTrkClusters) { + return qualityStage; + } + if (!passesTOFRequired(track)) { + return kTrkClusters; + } + if (!passesPID(track)) { + return kTrkTOFRequired; + } + return kTrkPID; + } + + // Unordered (pion, pion, kaon) candidate loop of one collision (or one mixed pair of collisions). + // dTracks1: bachelor kaons, dTracks2: pions. + template + void fillHistograms(o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2) + { + if (dTracks1.size() == 0 || dTracks2.size() == 0) { + return; + } + // Sets are local to this reconstructed collision: IDs cannot leak across DFs. + // Source-file/DF deduplication across split collisions belongs in the audit. + std::array, NTruthChannels> matchedMothers; + + // Selection cache: every track is selected once, not once per pair x bachelor. + // dTracks1: bachelor kaons, dTracks2: pions (different collisions in mixed events). + constexpr bool FillCutFlow = IsResoMicrotrack && !IsMix; + const int64_t firstKaonIndex = dTracks1.begin().index(); + const int64_t firstPionIndex = dTracks2.begin().index(); + const auto kaonSelected = buildSelectionCache(histos, dTracks1, firstKaonIndex); + const auto pionSelected = buildSelectionCache(histos, dTracks2, firstPionIndex); + + // Values needed only by switched-on cuts or QA are computed only then + const bool isK892Mode = mSecondaryCuts.cfgModeK892orRho; + const bool fillQA = !IsMix && mHistogramOptions.additionalQAplots; + const bool needAngle = IsMC || fillQA || mAngleCutOn; + const bool needPairAsym = IsMC || fillQA || mPairAsymCutOn; + // K892 mode: the K* candidate is (trk1, K), rho mode: the rho is (trk1, trk2) + const bool needMass13 = IsMC || fillQA || (isK892Mode ? mSecondaryWindowOn : mAnotherMassCutOn) || (isK892Mode && (needAngle || needPairAsym)); + const bool needMass23 = IsMC || fillQA || mPiKaMassCutOn; + const bool rhoWindowOn = mSecondaryWindowOn && !isK892Mode; + + auto multiplicity = collision.cent(); + ROOT::Math::PxPyPzMVector lDecayDaughter1, lDecayDaughter2, lResonanceSecondary, lDecayDaughter_bach, lResonanceK1, lPair13, lPair23; + // Unordered pion pairs: each (pion, pion, kaon) triplet is filled once. + // Here trk1 is the pion with the lower index; the roles are assigned once the bachelor is known. + for (const auto& [trk1, trk2] : o2::soa::combinations(o2::soa::CombinationsStrictlyUpperIndexPolicy(dTracks2, dTracks2))) { + // trk1: pion, trk2: pion, bTrack: kaon + const bool pionsSelected = pionSelected[getCacheIndex(trk1, firstPionIndex, pionSelected.size())] && pionSelected[getCacheIndex(trk2, firstPionIndex, pionSelected.size())]; + bool pairPt = false; + bool rhoWindow = true; + if (pionsSelected) { + // Resonance reconstruction + lDecayDaughter1.SetCoordinates(trk1.px(), trk1.py(), trk1.pz(), o2::constants::physics::MassPionCharged); + lDecayDaughter2.SetCoordinates(trk2.px(), trk2.py(), trk2.pz(), o2::constants::physics::MassPionCharged); + lResonanceSecondary = lDecayDaughter1 + lDecayDaughter2; + pairPt = !(lResonanceSecondary.Pt() < mSecondaryCuts.cMinSecondaryPtCut); + rhoWindow = !rhoWindowOn || isInWindow(lResonanceSecondary.M(), MassRho770, mSecondaryCuts.cSecondaryMasswindow); + } + if constexpr (FillCutFlow) { + // Early stages count potential triplets: each pair carries N bachelor trials. + // This preserves the pair-first reconstruction and avoids a new cubic data loop. + // Distinct pion IDs are guaranteed by the strictly upper index policy (stage 1 is always passed). + const int lastStage = !pionsSelected ? 1 : !pairPt ? 3 + : !rhoWindow ? 4 + : 5; + for (int stage = 0; stage <= lastStage; ++stage) { + histos.fill(HIST("CutFlow/candidates"), stage, 0, static_cast(dTracks1.size())); + } + if constexpr (IsMC) { + // Match before rejecting quality/pT so both channels have an upstream numerator. + if (std::abs(trk1.pdgCode()) == kPiPlus && trk1.pdgCode() == -trk2.pdgCode()) { + for (const auto& bachelor : dTracks1) { + const auto channel = classifyK1Truth(trk1, trk2, bachelor); + if (channel != K1TruthChannel::None) { + for (int stage = 0; stage <= lastStage; ++stage) { + histos.fill(HIST("CutFlow/candidates"), stage, static_cast(channel)); + } + } + } + } + } + } + if (!pionsSelected) { + continue; + } + + if (fillQA) { + fillPionQA(histos, trk1, true); + fillPionQA(histos, trk2, false); + } + + if (!pairPt) { + continue; + } + + if (fillQA) { + histos.fill(HIST("QA/hInvmassSecon"), lResonanceSecondary.M()); + } + if constexpr (IsMC) { + histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); + } + // Secondary mass window (rho mode): the bachelor loop is skipped for rejected pairs + if (!rhoWindow) { + continue; + } + + for (const auto& bTrack : dTracks1) { + if (bTrack.index() == trk1.index() || bTrack.index() == trk2.index()) { + continue; + } + K1TruthChannel flowChannel = K1TruthChannel::None; + if constexpr (IsMC && IsResoMicrotrack && !IsMix) { + flowChannel = classifyK1Truth(trk1, trk2, bTrack); + } + auto countCandidate = [&](int stage) { + if constexpr (FillCutFlow) { + histos.fill(HIST("CutFlow/candidates"), stage, 0); + if constexpr (IsMC) { + if (flowChannel != K1TruthChannel::None) { + histos.fill(HIST("CutFlow/candidates"), stage, static_cast(flowChannel)); + } + } + } + }; + countCandidate(6); + if (!kaonSelected[getCacheIndex(bTrack, firstKaonIndex, kaonSelected.size())]) { + continue; + } + countCandidate(7); + + if (fillQA) { + fillKaonQA(histos, bTrack); + } + + // Canonical assignment of the pion roles, once the bachelor is known. + // Unlike-sign pair: the pion with the sign opposite to the kaon is pion 1 (K*0 partner), the other is pion 2. + // Like-sign pair (the rule is ambiguous): the pion with the lower index is pion 1. + const bool isUnlikeSign = trk1.sign() * trk2.sign() < 0; + const bool swapPions = isUnlikeSign && trk1.sign() == bTrack.sign(); + const auto& pion1 = swapPions ? trk2 : trk1; + const auto& pion2 = swapPions ? trk1 : trk2; + const auto& lPion1 = swapPions ? lDecayDaughter2 : lDecayDaughter1; + const auto& lPion2 = swapPions ? lDecayDaughter1 : lDecayDaughter2; + + // K1 reconstruction + lDecayDaughter_bach.SetCoordinates(bTrack.px(), bTrack.py(), bTrack.pz(), o2::constants::physics::MassKaonCharged); + lResonanceK1 = lResonanceSecondary + lDecayDaughter_bach; + + // Cuts + if (lResonanceK1.Rapidity() > mCandidateCuts.cK1MaxRap || lResonanceK1.Rapidity() < mCandidateCuts.cK1MinRap) { + continue; + } + countCandidate(8); + + double mass13 = 0.; + double mass23 = 0.; + double lK1Angle = 0.; + double lPairAsym = 0.; + if (needMass13) { + lPair13 = lPion1 + lDecayDaughter_bach; + mass13 = lPair13.M(); + } + if (needMass23) { + lPair23 = lPion2 + lDecayDaughter_bach; + mass23 = lPair23.M(); + } + // Rho mode: secondary = (trk1, trk2) against the bachelor. K892 mode: secondary = (trk1, K) against trk2. + if (needAngle) { + lK1Angle = isK892Mode ? ROOT::Math::VectorUtil::Angle(lPair13, lPion2) : ROOT::Math::VectorUtil::Angle(lResonanceSecondary, lDecayDaughter_bach); + } + if (needPairAsym) { + lPairAsym = isK892Mode ? (lPair13.E() - lPion2.E()) / (lPair13.E() + lPion2.E()) + : (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); + } + + // QA histogram before the candidate cuts + if (fillQA) { + histos.fill(HIST("QA/K1OA"), lK1Angle); + histos.fill(HIST("QA/K1PairAsym"), lPairAsym); + histos.fill(HIST("QA/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); + histos.fill(HIST("QA/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); + histos.fill(HIST("QA/hpT_Secondary"), lResonanceSecondary.Pt()); + } + + // Candidate cuts (each one is evaluated only if switched on) + if (isK892Mode && mSecondaryWindowOn && (!isInWindow(mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) { + continue; + } + if (mAnotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) { + continue; + } + if (mPiKaMassCutOn && !isInRange(mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) { + continue; + } + if (mAngleCutOn && !isInRange(lK1Angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) { + continue; + } + if (mPairAsymCutOn && !isInRange(lPairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)) { + continue; + } + countCandidate(9); + + // QA histograms after the candidate cuts + if (fillQA) { + fillPionQA(histos, pion1, true); + fillPionQA(histos, pion2, false); + fillKaonQA(histos, bTrack); + histos.fill(HIST("QAcut/K1OA"), lK1Angle); + histos.fill(HIST("QAcut/K1PairAsym"), lPairAsym); + histos.fill(HIST("QAcut/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); + histos.fill(HIST("QAcut/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); + histos.fill(HIST("QAcut/hInvmassSecon"), lResonanceSecondary.M()); + histos.fill(HIST("QAcut/hpT_Secondary"), lResonanceSecondary.Pt()); + } + + countCandidate(isUnlikeSign ? 10 : 11); + if constexpr (IsMC && IsResoMicrotrack && !IsMix) { + if (flowChannel != K1TruthChannel::None) { + const int mother = flowChannel == K1TruthChannel::RhoK ? bTrack.motherId() : std::abs(pion1.motherPDG()) == o2::constants::physics::Pdg::kK1_1270Plus ? pion1.motherId() + : pion2.motherId(); + if (matchedMothers[static_cast(flowChannel)].insert(mother).second) { + histos.fill(HIST("CutFlow/uniqueMothersPerCollision"), static_cast(flowChannel)); + } + } + } + + if constexpr (!IsMix) { + unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P : BinType::kK1N; + unsigned int typeNormal = BinAnti::kNormal; + if (isUnlikeSign) { + histos.fill(HIST("k1invmass"), lResonanceK1.M()); + histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); + } else { + histos.fill(HIST("k1invmass_LS"), lResonanceK1.M()); + histos.fill(HIST("hInvmass_K1_LS"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); + } + + if constexpr (IsMC) { + const auto channel = classifyK1Truth(pion1, pion2, bTrack); + const int channelBin = static_cast(channel); + histos.fill(HIST("MCReco/channel"), channelBin); + histos.fill(HIST("MCReco/mass"), channelBin, lResonanceK1.M()); + histos.fill(HIST("MCReco/pt"), channelBin, lResonanceK1.Pt()); + histos.fill(HIST("MCReco/piPiMass"), channelBin, lResonanceSecondary.M()); + histos.fill(HIST("MCReco/pi1KMass"), channelBin, mass13); + histos.fill(HIST("MCReco/pi2KMass"), channelBin, mass23); + if (channel != K1TruthChannel::None) { + if (mTruthDebugCounts[channelBin] < mHistogramOptions.cfgTruthDebug) { + ++mTruthDebugCounts[channelBin]; + LOGP(info, "K1Truth channel={} collision={} tracks=({},{},{}) pdg=({},{},{}) mothers=({},{},{}) motherPDG=({},{},{}) siblings=(({},{}),({},{}),({},{}))", + channelBin, collision.globalIndex(), + pion1.globalIndex(), pion2.globalIndex(), bTrack.globalIndex(), + pion1.pdgCode(), pion2.pdgCode(), bTrack.pdgCode(), pion1.motherId(), pion2.motherId(), bTrack.motherId(), + pion1.motherPDG(), pion2.motherPDG(), bTrack.motherPDG(), + pion1.siblingIds()[0], pion1.siblingIds()[1], pion2.siblingIds()[0], pion2.siblingIds()[1], bTrack.siblingIds()[0], bTrack.siblingIds()[1]); + } + typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec; + histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); + histos.fill(HIST("k1invmass_MC"), lResonanceK1.M()); + histos.fill(HIST("QAMC/K1OA"), lK1Angle); + histos.fill(HIST("QAMC/K1PairAsym"), lPairAsym); + histos.fill(HIST("QAMC/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); + histos.fill(HIST("QAMC/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); + histos.fill(HIST("QAMC/hInvmassSecon"), lResonanceSecondary.M()); + histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); + + // PID QA primary and secondary pion + fillPionQA(histos, pion1, true); + fillPionQA(histos, pion2, false); + fillKaonQA(histos, bTrack); + } else { + histos.fill(HIST("k1invmass_MC_noK1"), lResonanceK1.M()); + } + } // IsMC + } else { + unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Mix : BinType::kK1N_Mix; + unsigned int typeNormal = BinAnti::kNormal; + histos.fill(HIST("hInvmass_K1_Mix"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); + histos.fill(HIST("k1invmass_Mix"), lResonanceK1.M()); + } + } // bTrack + } + } // fillHistograms + + // Generated K1 parents of a selected reconstructed MC collision. + // Parents belong to selected reconstructed events; split reco collisions + // repeat parent sets. This is not an unconditional generated denominator. + template + void fillGenerated(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents) + { + for (const auto& part : resoParents) { + if (std::abs(part.pdgCode()) != o2::constants::physics::Pdg::kK1_1270Plus) { + continue; + } + const int charge = part.pdgCode() > 0 ? 1 : -1; + const K1TruthChannel channel = classifyGeneratedK1(charge, part.daughterPDG1(), part.daughterPDG2()); + histos.fill(HIST("CutFlow/generated"), 0, static_cast(channel)); + if (part.y() < mCandidateCuts.cK1MinRap || part.y() > mCandidateCuts.cK1MaxRap) { + continue; + } + histos.fill(HIST("CutFlow/generated"), 1, static_cast(channel)); + // Keep other/unresolved immediate decays too; never require both pairs. + histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast(channel)); + histos.fill(HIST("MCGen/ptChannel"), static_cast(channel), part.pt()); + } + } + + private: + // Selection cache of one track slice. The row number of a grouped slice is global, hence the offset. + template + static std::size_t getCacheIndex(const TrackType& track, int64_t firstIndex, std::size_t size) + { + const int64_t index = static_cast(track.index()) - firstIndex; + if (index < 0 || index >= static_cast(size)) { + LOG(fatal) << "Track index " << track.index() << " is outside the selection cache [" << firstIndex << ", " << firstIndex + static_cast(size) << ")"; + } + return static_cast(index); + } + + template + std::vector buildSelectionCache(o2::framework::HistogramRegistry& histos, const TracksType& tracks, int64_t firstIndex) + { + std::vector selected(tracks.size(), 0); + for (const auto& track : tracks) { + const int stage = trackSelectionStage(track); + selected[getCacheIndex(track, firstIndex, selected.size())] = (stage == kTrkPID) ? 1 : 0; + if constexpr (FillCutFlow) { + for (int i = 0; i <= stage; ++i) { + histos.fill(HIST("CutFlow/tracks"), i, static_cast(S)); + } + } + } + return selected; + } + + void checkConfiguration(ProcessModes const& modes) + { + // Consistency of the pT dependent PID configuration + if (mPionPid.cPionUsePtDepPID) { + const auto& bins = mPionPid.cPionPIDPtBins.value; + if (bins.size() < MinPtBinEdges || mPionPid.cPionTPCNSigmaCuts.value.size() != bins.size() - 1 || + mPionPid.cPionTOFNSigmaCuts.value.size() != bins.size() - 1 || mPionPid.cPionTOFRequired.value.size() != bins.size() - 1) { + LOG(fatal) << "Pion pT dependent PID vectors must have (number of pT bin edges - 1) entries"; + } + } + if (mKaonPid.cKaonUsePtDepPID) { + const auto& bins = mKaonPid.cKaonPIDPtBins.value; + if (bins.size() < MinPtBinEdges || mKaonPid.cKaonTPCNSigmaCuts.value.size() != bins.size() - 1 || + mKaonPid.cKaonTOFNSigmaCuts.value.size() != bins.size() - 1 || mKaonPid.cKaonTOFRequired.value.size() != bins.size() - 1) { + LOG(fatal) << "Kaon pT dependent PID vectors must have (number of pT bin edges - 1) entries"; + } + } + if (mCandidateCuts.cByPassTOF && (mPionPid.cUseOnlyTOFTrackPi || mKaonPid.cUseOnlyTOFTrackKa)) { + LOG(warning) << "cByPassTOF skips the TOF nSigma cut, but cUseOnlyTOFTrack* still requires a TOF signal"; + } + + // Micro tracks store quantised DCA and nSigma: a cut off the grid would silently act as a different cut. + if (!modes.microTracks) { + return; + } + auto checkDCAGrid = [](const char* name, double cut) { + const double nearest = std::min(std::max(std::round(cut / DCAGridStep) * DCAGridStep, 0.), DCAGridMax); + if (std::abs(cut - nearest) > GridTolerance) { + LOG(fatal) << name << " = " << cut << " is not on the quantised DCA grid (multiples of " << DCAGridStep << " up to " << DCAGridMax << "); nearest value: " << nearest; + } + }; + auto checkPIDGrid = [](const char* name, double cut) { + const double nearest = std::min(std::max(PIDGridStart + std::round((cut - PIDGridStart) / PIDGridStep) * PIDGridStep, PIDGridStart), PIDGridMax); + if (std::abs(cut - nearest) > GridTolerance) { + LOG(fatal) << name << " = " << cut << " is not on the quantised nSigma grid ({2.0, 2.25, ..., 3.5}); nearest value: " << nearest; + } + }; + if (mTrackCuts.cfgUsePtDepDCA) { + LOG(info) << "Micro tracks use the producer pT dependent DCA flags (0.004 + 0.013 / pT); cDCAToPVByPt* are ignored"; + if (std::abs(mTrackCuts.cDCAToPVByPtP0 - ProducerDCAPtP0) > ConfigTolerance || std::abs(mTrackCuts.cDCAToPVByPtCoeff - ProducerDCAPtCoeff) > ConfigTolerance || std::abs(mTrackCuts.cDCAToPVByPtPower - ProducerDCAPtPower) > ConfigTolerance) { + LOG(warning) << "cDCAToPVByPt* differ from the producer defaults, but micro tracks always use the producer formula"; + } + } else { + if (isCutEnabled(mTrackCuts.cMaxDCArToPVcut)) { + checkDCAGrid("cMaxDCArToPVcut", mTrackCuts.cMaxDCArToPVcut); + } + if (isCutEnabled(mTrackCuts.cMaxDCAzToPVcut)) { + checkDCAGrid("cMaxDCAzToPVcut", mTrackCuts.cMaxDCAzToPVcut); + } + } + if (isCutEnabled(mTrackCuts.cMinDCAzToPVcut)) { + checkDCAGrid("cMinDCAzToPVcut", mTrackCuts.cMinDCAzToPVcut); + } + if (isCutEnabled(mPionPid.cMaxTPCnSigmaPion) && !mPionPid.cPionUsePtDepPID) { + checkPIDGrid("cMaxTPCnSigmaPion", mPionPid.cMaxTPCnSigmaPion); + } + if (isCutEnabled(mPionPid.cMaxTOFnSigmaPion) && !mPionPid.cPionUsePtDepPID) { + checkPIDGrid("cMaxTOFnSigmaPion", mPionPid.cMaxTOFnSigmaPion); + } + if (isCutEnabled(mKaonPid.cMaxTPCnSigmaKaon) && !mKaonPid.cKaonUsePtDepPID) { + checkPIDGrid("cMaxTPCnSigmaKaon", mKaonPid.cMaxTPCnSigmaKaon); + } + if (isCutEnabled(mKaonPid.cMaxTOFnSigmaKaon) && !mKaonPid.cKaonUsePtDepPID) { + checkPIDGrid("cMaxTOFnSigmaKaon", mKaonPid.cMaxTOFnSigmaKaon); + } + if (mPionPid.cPionUsePtDepPID) { + for (const auto& cut : mPionPid.cPionTPCNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cPionTPCNSigmaCuts", cut); + } + } + for (const auto& cut : mPionPid.cPionTOFNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cPionTOFNSigmaCuts", cut); + } + } + } + if (mKaonPid.cKaonUsePtDepPID) { + for (const auto& cut : mKaonPid.cKaonTPCNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cKaonTPCNSigmaCuts", cut); + } + } + for (const auto& cut : mKaonPid.cKaonTOFNSigmaCuts.value) { + if (isCutEnabled(cut)) { + checkPIDGrid("cKaonTOFNSigmaCuts", cut); + } + } + } + if (mPionPid.nsigmaCutCombinedPion > 0 || mKaonPid.nsigmaCutCombinedKaon > 0) { + LOG(warning) << "nsigmaCutCombined* on micro tracks uses quantised nSigma values (approximate)"; + } + } + + void registerHistograms(o2::framework::HistogramRegistry& histos, ProcessModes const& modes) + { + using o2::framework::AxisSpec; + using o2::framework::HistType; + const int nBinsDiv = mHistogramOptions.cNbinsDiv; + std::vector centBinning = {0., 1., 5., 10., 15., 20., 25., 30., 35., 40., 45., 50., 55., 60., 65., 70., 80., 90., 100., 200.}; + AxisSpec centAxis = {centBinning, "T0M (%)"}; + AxisSpec ptAxis = {150, 0, 15, "#it{p}_{T} (GeV/#it{c})"}; + AxisSpec dcaxyAxis = {300, 0, 3, "DCA_{#it{xy}} (cm)"}; + AxisSpec dcazAxis = {500, 0, 5, "DCA_{#it{z}} (cm)"}; + AxisSpec invMassAxisK892 = {1400 / nBinsDiv, 0.6, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // K(892)0 + AxisSpec invMassAxisRho = {2000 / nBinsDiv, 0.0, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // rho + AxisSpec invMassAxisReso = {1600 / nBinsDiv, 0.9f, 2.5f, "Invariant Mass (GeV/#it{c}^2)"}; // K1 + AxisSpec pidQAAxis = {130, -6.5, 6.5}; + + // THnSparse + AxisSpec axisAnti = {BinAnti::kNAEnd, 0, BinAnti::kNAEnd, "Type of bin: Normal or Anti"}; + AxisSpec axisType = {BinType::kTYEnd, 0, BinType::kTYEnd, "Type of bin with charge and mix"}; + + // Micro-only instrumentation: category 0 includes all combinations, not just unmatched. + auto trackFlow = histos.add("CutFlow/tracks", "Micro tracks, once per selected collision;stage;species", HistType::kTH2D, {{static_cast(kTrkNStages), -0.5, static_cast(kTrkNStages) - 0.5}, {2, -0.5, 1.5}}); + const std::array trackLabels{"input", "pT", "eta", "DCAxy", "DCAz", "track flags", "clusters / crossed rows", "TOF required", "PID"}; + for (std::size_t i = 0; i < trackLabels.size(); ++i) { + trackFlow->GetXaxis()->SetBinLabel(i + 1, trackLabels[i]); + } + trackFlow->GetYaxis()->SetBinLabel(1, "pion"); + trackFlow->GetYaxis()->SetBinLabel(2, "kaon"); + auto candidateFlow = histos.add("CutFlow/candidates", "Unordered micro triplets;stage;category", HistType::kTH2D, {{NCandidateStages, -0.5, NCandidateStages - 0.5}, {3, -0.5, 2.5}}); + const std::array candidateLabels{"input unordered triplets", "distinct pion IDs", "pion selection (quality+PID)", "pion pair constructed", "pair pT", "secondary mass window (rho mode)", "three distinct IDs", "kaon selection (quality+PID)", "K1 rapidity", "candidate cuts", "final US", "final LS"}; + for (std::size_t i = 0; i < candidateLabels.size(); ++i) { + candidateFlow->GetXaxis()->SetBinLabel(i + 1, candidateLabels[i]); + } + candidateFlow->GetYaxis()->SetBinLabel(1, "all"); + candidateFlow->GetYaxis()->SetBinLabel(2, "rho K"); + candidateFlow->GetYaxis()->SetBinLabel(3, "K* pi"); + if (modes.mcRecoMicro) { + auto mothers = histos.add("CutFlow/uniqueMothersPerCollision", "Final unique K1 IDs summed over reconstructed collisions (not globally deduplicated)", HistType::kTH1D, {{2, 0.5, 2.5}}); + mothers->GetXaxis()->SetBinLabel(1, "rho K"); + mothers->GetXaxis()->SetBinLabel(2, "K* pi"); + } + if (modes.mcGen) { + auto generated = histos.add("CutFlow/generated", "K1 parent rows conditional on selected reconstructed events;stage;immediate channel", HistType::kTH2D, {{2, -0.5, 1.5}, {3, -0.5, 2.5}}); + generated->GetXaxis()->SetBinLabel(1, "all K1 parent rows"); + generated->GetXaxis()->SetBinLabel(2, "K1 rapidity window"); + generated->GetYaxis()->SetBinLabel(1, "other / unresolved"); + generated->GetYaxis()->SetBinLabel(2, "rho K"); + generated->GetYaxis()->SetBinLabel(3, "K* pi"); + } + + // DCA QA + // Primary pion + histos.add("QA/trkppionDCAxy", "DCAxy disstribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QA/trkppionDCAz", "DCAz disstribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QA/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QA/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAcut/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAcut/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAcut/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAcut/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + // Secondary pion + histos.add("QA/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QA/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QA/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QA/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAcut/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAcut/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAcut/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAcut/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + // Kaon + histos.add("QA/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QA/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QA/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); + histos.add("QA/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAcut/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAcut/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAcut/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAcut/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + // K1 + histos.add("QA/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); + histos.add("QA/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); + histos.add("QA/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); + histos.add("QA/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); + histos.add("QA/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); + histos.add("QA/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); + + histos.add("QAcut/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); + histos.add("QAcut/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); + histos.add("QAcut/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); + histos.add("QAcut/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); + histos.add("QAcut/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); + histos.add("QAcut/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); + + // Invariant mass + histos.add("hInvmass_K1", "Invariant mass of K1(1270) (US)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); + histos.add("hInvmass_K1_LS", "Invariant mass of K1(1270) (LS)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); + histos.add("hInvmass_K1_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); + // Mass QA (quick check) + histos.add("k1invmass", "Invariant mass of K1(1270) (US)", HistType::kTH1F, {invMassAxisReso}); + histos.add("k1invmass_LS", "Invariant mass of K1(1270) (LS)", HistType::kTH1F, {invMassAxisReso}); + histos.add("k1invmass_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTH1F, {invMassAxisReso}); + + // MC + if (modes.mcReco) { + AxisSpec channelAxis = {3, -0.5, 2.5, "0: non-K1, 1: rho K, 2: K* pi"}; + histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/channel", "All selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis}); + histos.add("MCReco/mass", "Reconstructed mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisReso}); + histos.add("MCReco/pt", "Reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis}); + histos.add("MCReco/piPiMass", "pi-pi mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisRho}); + histos.add("MCReco/pi1KMass", "First pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); + histos.add("MCReco/pi2KMass", "Second pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); + histos.add("k1invmass_MC", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); + histos.add("k1invmass_MC_noK1", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); + + histos.add("QAMC/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAMC/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAMC/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAMC/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAMC/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAMC/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAMC/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAMC/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAMC/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAMC/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAMC/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAMC/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAMC/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); + histos.add("QAMC/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); + histos.add("QAMC/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); + histos.add("QAMC/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); + histos.add("QAMC/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); + histos.add("QAMC/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); + } // mcReco + if (modes.mcGen) { + AxisSpec channelAxis = {3, -0.5, 2.5, "0: other/unresolved, 1: rho K, 2: K* pi"}; + 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}); + histos.add("MCGen/ptChannel", "Generated K1 pT by immediate decay channel", HistType::kTH2D, {channelAxis, ptAxis}); + } + } + + EventCuts mEventCuts; + TrackCuts mTrackCuts; + PionPidCuts mPionPid; + KaonPidCuts mKaonPid; + SecondaryCuts mSecondaryCuts; + CandidateCuts mCandidateCuts; + HistogramOptions mHistogramOptions; + + // Derived once in init(): which candidate cuts are switched on. + bool mSecondaryWindowOn = false; + bool mAnotherMassCutOn = false; + bool mPiKaMassCutOn = false; + bool mAngleCutOn = false; + bool mPairAsymCutOn = false; + + std::array mTruthDebugCounts{}; +}; + +} // namespace o2::analysis::k1micro + +#endif // PWGLF_CORE_K1ANALYSISMICROCORE_H_ diff --git a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx index f8410f25e04..0546857f486 100644 --- a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx +++ b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx @@ -14,43 +14,29 @@ /// \author Su-Jeong Ji , Bong-Hwi Lim /// +#include "PWGLF/Core/K1AnalysisMicroCore.h" #include "PWGLF/DataModel/LFResonanceTables.h" -#include -#include #include -#include #include #include #include #include #include #include -#include #include #include #include #include #include -#include -#include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) -#include - -#include -#include -#include -#include -#include -#include -#include +#include using namespace o2; using namespace o2::framework; using namespace o2::framework::expressions; using namespace o2::soa; -using namespace o2::constants::physics; -using namespace o2::constants::math; +using namespace o2::analysis::k1micro; struct K1AnalysisMicro { // Module-initializer v001 tables; full tracks keep their unversioned schema as a fallback. @@ -62,229 +48,29 @@ struct K1AnalysisMicro { using ResoMCMicroTracks = soa::Join; using ResoMCParents = aod::ResoMCParents_001; - enum BinAnti : unsigned int { - kNormal = 0, - kAnti, - kNAEnd - }; - enum BinType : unsigned int { - kK1P = 0, - kK1N, - kK1P_Mix, - kK1N_Mix, - kK1P_GenINEL10, - kK1N_GenINEL10, - kK1P_GenINELgt10, - kK1N_GenINELgt10, - kK1P_GenTrig10, - kK1N_GenTrig10, - kK1P_GenEvtSel, - kK1N_GenEvtSel, - kK1P_Rec, - kK1N_Rec, - kTYEnd - }; - enum class Species : int { - Pion = 0, - Kaon = 1 - }; - // Last stage passed by a track; the cut-flow histogram is filled directly from this value. - enum TrackStage : int { - kTrkInput = 0, - kTrkPt, - kTrkEta, - kTrkDCAxy, - kTrkDCAz, - kTrkFlags, - kTrkClusters, - kTrkTOFRequired, - kTrkPID, - kTrkNStages - }; - enum class QAFolder { - Before, // QA/*: before the candidate cuts - After, // QAcut/*: after the candidate cuts - MC // QAMC/*: matched K1 truth candidates - }; - // Resolved PID cut of one species at a given pT. - struct PIDCut { - double tpcMax = 0.; - double tofMax = 0.; - double combined = 0.; - bool tofRequired = false; - }; - - static constexpr float DisabledCut = -999.f; // an optional cut with this value is off and not evaluated - static constexpr double MassRho770 = 0.77526; // PDG 2024, not available in o2::constants::physics - static constexpr double DCAGridStep = 0.025; // v001 micro DCA encoding, lower-inclusive bins up to DCAGridMax - static constexpr double DCAGridMax = 0.15; - static constexpr double PIDGridStart = 2.0; // v001 micro nSigma encoding: 0.25 bins in [2.0, 3.5] - static constexpr double PIDGridStep = 0.25; - static constexpr double PIDGridMax = 3.5; - static constexpr double GridTolerance = 1e-4; - static constexpr std::size_t MinPtBinEdges = 2; // a pT dependent PID table needs at least one bin - static constexpr float ProducerDCAPtP0 = 0.004f; // resonanceModuleInitializer cfgTightDCAOffset default - static constexpr float ProducerDCAPtCoeff = 0.013f; // resonanceModuleInitializer cfgTightDCAPtCoefficient default - static constexpr float ProducerDCAPtPower = 1.f; // resonanceModuleInitializer cfgTightDCAPtPower default - static constexpr float ConfigTolerance = 1e-6f; - static constexpr int NCandidateStages = 12; - SliceCache cache; // Registered only to enable the slice cache that SameKindPair (event mixing) needs, as in Xi1820Analysis Preslice perResoCollisionTrack = aod::resodaughter::resoCollisionId; Preslice perResoCollisionMicroTrack = aod::resodaughter::resoCollisionId; HistogramRegistry histos{"histos", {}, OutputObjHandlingPolicy::AnalysisObject}; - Configurable cfgTruthDebug{"cfgTruthDebug", 0, "Maximum logged matched candidates per truth channel"}; - std::array truthDebugCounts{}; - //// Configurables - Configurable cNbinsDiv{"cNbinsDiv", 1, "Integer to divide the number of bins"}; + // Selection shared with the K1 training-table task (plain JSON keys, no group prefix) + EventCuts eventCuts; + TrackCuts trackCuts; + PionPidCuts pionPID; + KaonPidCuts kaonPID; + SecondaryCuts secondaryCuts; + CandidateCuts candidateCuts; + HistogramOptions histogramOptions; + /// Event Mixing Configurable nEvtMixing{"nEvtMixing", 5, "Number of events to mix"}; ConfigurableAxis cfgVtxBins{"cfgVtxBins", {VARIABLE_WIDTH, -10.0f, -8.f, -6.f, -4.f, -2.f, 0.f, 2.f, 4.f, 6.f, 8.f, 10.f}, "Mixing bins - z-vertex"}; ConfigurableAxis cfgMultBins{"cfgMultBins", {VARIABLE_WIDTH, 0.0f, 20.0f, 40.0f, 60.0f, 80.0f, 100.0f, 200.0f, 99999.f}, "Mixing bins - multiplicity"}; - // Event selection (a group without prefix keeps the plain key names) - struct : ConfigurableGroup { - Configurable cRecoINELgt0{"cRecoINELgt0", false, "Apply reconstructed INEL>0 selection"}; - Configurable cMCINELgt0{"cMCINELgt0", false, "Require generator INEL>0 in MC processes"}; - Configurable cMCVtxIn10{"cMCVtxIn10", false, "Require generator |vz| < 10 cm in MC processes"}; - } eventCuts; - - /// Track selections (common for pion and kaon, -999 switches an optional cut off) - struct : ConfigurableGroup { - Configurable cMinPtcut{"cMinPtcut", 0.15, "Track minium pt cut"}; - Configurable cMaxEtacut{"cMaxEtacut", -999.f, "Track maximum |eta| cut (-999: off)"}; - // DCAr to PV - Configurable cMaxDCArToPVcut{"cMaxDCArToPVcut", 0.1, "Track DCAr cut to PV Maximum"}; - // DCAz to PV - Configurable cMaxDCAzToPVcut{"cMaxDCAzToPVcut", 0.1, "Track DCAz cut to PV Maximum"}; - Configurable cMinDCAzToPVcut{"cMinDCAzToPVcut", 0.0, "Track DCAz cut to PV Minimum"}; - Configurable cfgUsePtDepDCA{"cfgUsePtDepDCA", false, "Use pT dependent DCA cut instead of the fixed maximum"}; - Configurable cDCAToPVByPtP0{"cDCAToPVByPtP0", 0.004f, "pT dependent DCA cut = P0 + coefficient / pT^power (cm)"}; - Configurable cDCAToPVByPtCoeff{"cDCAToPVByPtCoeff", 0.013f, "Coefficient in the pT dependent DCA cut"}; - Configurable cDCAToPVByPtPower{"cDCAToPVByPtPower", 1.f, "Power in the pT dependent DCA cut"}; - Configurable cfgPrimaryTrack{"cfgPrimaryTrack", true, "Primary track selection"}; // kGoldenChi2 | kDCAxy | kDCAz - Configurable cfgGlobalWoDCATrack{"cfgGlobalWoDCATrack", true, "Global track selection without DCA"}; // kQualityTracks (kTrackType | kTPCNCls | kTPCCrossedRows | kTPCCrossedRowsOverNCls | kTPCChi2NDF | kTPCRefit | kITSNCls | kITSChi2NDF | kITSRefit | kITSHits) | kInAcceptanceTracks (kPtRange | kEtaRange) - Configurable cfgGlobalTrack{"cfgGlobalTrack", false, "Global track selection"}; // kGoldenChi2 | kDCAxy | kDCAz - Configurable cfgPVContributor{"cfgPVContributor", false, "PV contributor track selection"}; // PV Contriuibutor - Configurable cfgUseTPCRefit{"cfgUseTPCRefit", false, "Require TPC Refit"}; - Configurable cfgUseITSRefit{"cfgUseITSRefit", false, "Require ITS Refit"}; - Configurable cfgTPCcluster{"cfgTPCcluster", 0, "Number of TPC cluster (found clusters, ResoTracks only)"}; - Configurable cfgTPCCrossedRowsMin{"cfgTPCCrossedRowsMin", 0, "Minimum number of TPC crossed rows"}; - Configurable cfgITSNClsMin{"cfgITSNClsMin", 0, "Minimum number of ITS clusters (ResoMicroTracks only)"}; - Configurable cfgHasTOF{"cfgHasTOF", false, "Require TOF"}; - } trackCuts; - - /// PID Selections - Configurable cByPassTOF{"cByPassTOF", false, "Bypass the TOF nSigma selection"}; - struct : ConfigurableGroup { - Configurable cMaxTPCnSigmaPion{"cMaxTPCnSigmaPion", 3.0, "TPC nSigma cut for Pion (-999: off)"}; // TPC - Configurable cMaxTOFnSigmaPion{"cMaxTOFnSigmaPion", 3.0, "TOF nSigma cut for Pion (-999: off)"}; // TOF - Configurable nsigmaCutCombinedPion{"nsigmaCutCombinedPion", -999, "Combined nSigma cut for Pion"}; // Combined - Configurable cUseOnlyTOFTrackPi{"cUseOnlyTOFTrackPi", false, "Use only TOF track for PID selection"}; // Use only TOF track for Pion PID selection - Configurable cPionUsePtDepPID{"cPionUsePtDepPID", false, "Use pT-dependent PID cuts for pion"}; - Configurable> cPionPIDPtBins{"cPionPIDPtBins", {0.0f, 0.5f, 0.8f, 2.0f, 999.0f}, "pT bin edges for pion PID cuts"}; - Configurable> cPionTPCNSigmaCuts{"cPionTPCNSigmaCuts", {3.0f, 3.0f, 2.0f, 2.0f}, "TPC NSigma cuts per pT bin (pion)"}; - Configurable> cPionTOFNSigmaCuts{"cPionTOFNSigmaCuts", {3.0f, 3.0f, 3.0f, 3.0f}, "TOF NSigma cuts per pT bin (pion)"}; - Configurable> cPionTOFRequired{"cPionTOFRequired", {0, 0, 1, 1}, "Require TOF per pT bin (pion)"}; - } pionPID; - struct : ConfigurableGroup { - Configurable cMaxTPCnSigmaKaon{"cMaxTPCnSigmaKaon", 3.0, "TPC nSigma cut for Kaon (-999: off)"}; // TPC - Configurable cMaxTOFnSigmaKaon{"cMaxTOFnSigmaKaon", 3.0, "TOF nSigma cut for Kaon (-999: off)"}; // TOF - Configurable nsigmaCutCombinedKaon{"nsigmaCutCombinedKaon", -999, "Combined nSigma cut for Kaon"}; // Combined - Configurable cUseOnlyTOFTrackKa{"cUseOnlyTOFTrackKa", false, "Use only TOF track for PID selection"}; // Use only TOF track for Kaon PID selection - Configurable cKaonUsePtDepPID{"cKaonUsePtDepPID", false, "Use pT-dependent PID cuts for kaon"}; - Configurable> cKaonPIDPtBins{"cKaonPIDPtBins", {0.0f, 0.5f, 0.8f, 2.0f, 999.0f}, "pT bin edges for kaon PID cuts"}; - Configurable> cKaonTPCNSigmaCuts{"cKaonTPCNSigmaCuts", {3.0f, 3.0f, 2.0f, 2.0f}, "TPC NSigma cuts per pT bin (kaon)"}; - Configurable> cKaonTOFNSigmaCuts{"cKaonTOFNSigmaCuts", {3.0f, 3.0f, 3.0f, 3.0f}, "TOF NSigma cuts per pT bin (kaon)"}; - Configurable> cKaonTOFRequired{"cKaonTOFRequired", {0, 0, 1, 1}, "Require TOF per pT bin (kaon)"}; - } kaonPID; - - Configurable additionalQAplots{"additionalQAplots", true, "Additional QA plots"}; - - // Secondary selection (-999 switches a cut off; the values it needs are then not computed) - struct : ConfigurableGroup { - Configurable cMinSecondaryPtCut{"cMinSecondaryPtCut", 0.5, "Min pT cut for secondary selection"}; - Configurable cfgModeK892orRho{"cfgModeK892orRho", false, "Secondary scenario for K892 (true) or Rho (false)"}; - Configurable cSecondaryMasswindow{"cSecondaryMasswindow", -999, "Secondary inv mass selection window"}; - Configurable cMinAnotherSecondaryMassCut{"cMinAnotherSecondaryMassCut", -999, "Min inv. mass selection of another secondary scenario"}; - Configurable cMaxAnotherSecondaryMassCut{"cMaxAnotherSecondaryMassCut", -999, "MAx inv. mass selection of another secondary scenario"}; - Configurable cMinPiKaMassCut{"cMinPiKaMassCut", -999, "bPion-Kaon pair inv mass selection minimum"}; - Configurable cMaxPiKaMassCut{"cMaxPiKaMassCut", -999, "bPion-Kaon pair inv mass selection maximum"}; - Configurable cMinAngle{"cMinAngle", -999, "Minimum angle between the secondary resonance and the bachelor"}; - Configurable cMaxAngle{"cMaxAngle", -999, "Maximum angle between the secondary resonance and the bachelor"}; - Configurable cMinPairAsym{"cMinPairAsym", -999, "Minimum pair asymmetry"}; - Configurable cMaxPairAsym{"cMaxPairAsym", -999, "Maximum pair asymmetry"}; - } secondaryCuts; - - // K1 selection - Configurable cK1MaxRap{"cK1MaxRap", 0.5, "K1 maximum rapidity"}; - Configurable cK1MinRap{"cK1MinRap", -0.5, "K1 minimum rapidity"}; - - // A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value). - static bool isCutEnabled(float value) - { - return value > DisabledCut + 1.f; - } - - // v001 micro values are lower-inclusive bin edges: a maximum cut on the grid keeps bins below it. - static bool passesBinnedMax(double decoded, double cut) - { - return decoded < cut - Epsilon; - } - - // Minimum cut on the grid keeps the bin starting at the cut. - static bool passesBinnedMin(double decoded, double cut) - { - return decoded >= cut - Epsilon; - } - - template - static bool passesMax(double value, double cut) - { - if constexpr (IsResoMicrotrack) { - return passesBinnedMax(value, cut); - } else { - return value < cut; - } - } - - static bool isInRange(double value, double minimum, double maximum) - { - if (isCutEnabled(minimum) && value < minimum) { - return false; - } - if (isCutEnabled(maximum) && value > maximum) { - return false; - } - return true; - } - - static bool isInWindow(double value, double center, double width) - { - return std::abs(value - center) < width; - } - - // Preserve pT-bin membership [low, high). - static int getPtBinIndex(float pt, const std::vector& ptBins) - { - for (std::size_t i = 1; i < ptBins.size(); ++i) { - if (pt >= ptBins[i - 1] && pt < ptBins[i]) { - return static_cast(i - 1); - } - } - return -1; - } - - // Derived once in init(): which candidate cuts are switched on. - bool secondaryWindowOn = false; - bool anotherMassCutOn = false; - bool piKaMassCutOn = false; - bool angleCutOn = false; - bool pairAsymCutOn = false; + K1AnalysisMicroCore core; - void init(o2::framework::InitContext&) + void init(InitContext&) { const int sameEventModes = static_cast(doprocessResoTracks) + static_cast(doprocessResoMicroTracks) + static_cast(doprocessMC) + static_cast(doprocessMCMicro); @@ -293,1040 +79,68 @@ struct K1AnalysisMicro { LOG(fatal) << "Enable at most one same-event mode and one mixing mode"; } - secondaryWindowOn = isCutEnabled(secondaryCuts.cSecondaryMasswindow); - anotherMassCutOn = isCutEnabled(secondaryCuts.cMinAnotherSecondaryMassCut) || isCutEnabled(secondaryCuts.cMaxAnotherSecondaryMassCut); - piKaMassCutOn = isCutEnabled(secondaryCuts.cMinPiKaMassCut) || isCutEnabled(secondaryCuts.cMaxPiKaMassCut); - angleCutOn = isCutEnabled(secondaryCuts.cMinAngle) || isCutEnabled(secondaryCuts.cMaxAngle); - pairAsymCutOn = isCutEnabled(secondaryCuts.cMinPairAsym) || isCutEnabled(secondaryCuts.cMaxPairAsym); + ProcessModes modes; + modes.microTracks = doprocessResoMicroTracks || doprocessMCMicro || doprocessMEMicro; + modes.mcReco = doprocessMC || doprocessMCMicro; + modes.mcRecoMicro = doprocessMCMicro; + modes.mcGen = doprocessMCTrue; + core.init(histos, eventCuts, trackCuts, pionPID, kaonPID, secondaryCuts, candidateCuts, histogramOptions, modes); - // Consistency of the pT dependent PID configuration - if (pionPID.cPionUsePtDepPID) { - const auto& bins = pionPID.cPionPIDPtBins.value; - if (bins.size() < MinPtBinEdges || pionPID.cPionTPCNSigmaCuts.value.size() != bins.size() - 1 || - pionPID.cPionTOFNSigmaCuts.value.size() != bins.size() - 1 || pionPID.cPionTOFRequired.value.size() != bins.size() - 1) { - LOG(fatal) << "Pion pT dependent PID vectors must have (number of pT bin edges - 1) entries"; - } - } - if (kaonPID.cKaonUsePtDepPID) { - const auto& bins = kaonPID.cKaonPIDPtBins.value; - if (bins.size() < MinPtBinEdges || kaonPID.cKaonTPCNSigmaCuts.value.size() != bins.size() - 1 || - kaonPID.cKaonTOFNSigmaCuts.value.size() != bins.size() - 1 || kaonPID.cKaonTOFRequired.value.size() != bins.size() - 1) { - LOG(fatal) << "Kaon pT dependent PID vectors must have (number of pT bin edges - 1) entries"; - } - } - if (cByPassTOF && (pionPID.cUseOnlyTOFTrackPi || kaonPID.cUseOnlyTOFTrackKa)) { - LOG(warning) << "cByPassTOF skips the TOF nSigma cut, but cUseOnlyTOFTrack* still requires a TOF signal"; - } - - // Micro tracks store quantised DCA and nSigma: a cut off the grid would silently act as a different cut. - if (doprocessResoMicroTracks || doprocessMCMicro || doprocessMEMicro) { - auto checkDCAGrid = [](const char* name, double cut) { - const double nearest = std::min(std::max(std::round(cut / DCAGridStep) * DCAGridStep, 0.), DCAGridMax); - if (std::abs(cut - nearest) > GridTolerance) { - LOG(fatal) << name << " = " << cut << " is not on the quantised DCA grid (multiples of " << DCAGridStep << " up to " << DCAGridMax << "); nearest value: " << nearest; - } - }; - auto checkPIDGrid = [](const char* name, double cut) { - const double nearest = std::min(std::max(PIDGridStart + std::round((cut - PIDGridStart) / PIDGridStep) * PIDGridStep, PIDGridStart), PIDGridMax); - if (std::abs(cut - nearest) > GridTolerance) { - LOG(fatal) << name << " = " << cut << " is not on the quantised nSigma grid ({2.0, 2.25, ..., 3.5}); nearest value: " << nearest; - } - }; - if (trackCuts.cfgUsePtDepDCA) { - LOG(info) << "Micro tracks use the producer pT dependent DCA flags (0.004 + 0.013 / pT); cDCAToPVByPt* are ignored"; - if (std::abs(trackCuts.cDCAToPVByPtP0 - ProducerDCAPtP0) > ConfigTolerance || std::abs(trackCuts.cDCAToPVByPtCoeff - ProducerDCAPtCoeff) > ConfigTolerance || std::abs(trackCuts.cDCAToPVByPtPower - ProducerDCAPtPower) > ConfigTolerance) { - LOG(warning) << "cDCAToPVByPt* differ from the producer defaults, but micro tracks always use the producer formula"; - } - } else { - if (isCutEnabled(trackCuts.cMaxDCArToPVcut)) { - checkDCAGrid("cMaxDCArToPVcut", trackCuts.cMaxDCArToPVcut); - } - if (isCutEnabled(trackCuts.cMaxDCAzToPVcut)) { - checkDCAGrid("cMaxDCAzToPVcut", trackCuts.cMaxDCAzToPVcut); - } - } - if (isCutEnabled(trackCuts.cMinDCAzToPVcut)) { - checkDCAGrid("cMinDCAzToPVcut", trackCuts.cMinDCAzToPVcut); - } - if (isCutEnabled(pionPID.cMaxTPCnSigmaPion) && !pionPID.cPionUsePtDepPID) { - checkPIDGrid("cMaxTPCnSigmaPion", pionPID.cMaxTPCnSigmaPion); - } - if (isCutEnabled(pionPID.cMaxTOFnSigmaPion) && !pionPID.cPionUsePtDepPID) { - checkPIDGrid("cMaxTOFnSigmaPion", pionPID.cMaxTOFnSigmaPion); - } - if (isCutEnabled(kaonPID.cMaxTPCnSigmaKaon) && !kaonPID.cKaonUsePtDepPID) { - checkPIDGrid("cMaxTPCnSigmaKaon", kaonPID.cMaxTPCnSigmaKaon); - } - if (isCutEnabled(kaonPID.cMaxTOFnSigmaKaon) && !kaonPID.cKaonUsePtDepPID) { - checkPIDGrid("cMaxTOFnSigmaKaon", kaonPID.cMaxTOFnSigmaKaon); - } - if (pionPID.cPionUsePtDepPID) { - for (const auto& cut : pionPID.cPionTPCNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cPionTPCNSigmaCuts", cut); - } - } - for (const auto& cut : pionPID.cPionTOFNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cPionTOFNSigmaCuts", cut); - } - } - } - if (kaonPID.cKaonUsePtDepPID) { - for (const auto& cut : kaonPID.cKaonTPCNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cKaonTPCNSigmaCuts", cut); - } - } - for (const auto& cut : kaonPID.cKaonTOFNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cKaonTOFNSigmaCuts", cut); - } - } - } - if (pionPID.nsigmaCutCombinedPion > 0 || kaonPID.nsigmaCutCombinedKaon > 0) { - LOG(warning) << "nsigmaCutCombined* on micro tracks uses quantised nSigma values (approximate)"; - } - } - - std::vector centBinning = {0., 1., 5., 10., 15., 20., 25., 30., 35., 40., 45., 50., 55., 60., 65., 70., 80., 90., 100., 200.}; - AxisSpec centAxis = {centBinning, "T0M (%)"}; - AxisSpec ptAxis = {150, 0, 15, "#it{p}_{T} (GeV/#it{c})"}; - AxisSpec dcaxyAxis = {300, 0, 3, "DCA_{#it{xy}} (cm)"}; - AxisSpec dcazAxis = {500, 0, 5, "DCA_{#it{z}} (cm)"}; - AxisSpec invMassAxisK892 = {1400 / cNbinsDiv, 0.6, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // K(892)0 - AxisSpec invMassAxisRho = {2000 / cNbinsDiv, 0.0, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // rho - AxisSpec invMassAxisReso = {1600 / cNbinsDiv, 0.9f, 2.5f, "Invariant Mass (GeV/#it{c}^2)"}; // K1 - AxisSpec invMassAxisScan = {250, 0, 2.5, "Invariant Mass (GeV/#it{c}^2)"}; // For selection - AxisSpec pidQAAxis = {130, -6.5, 6.5}; - AxisSpec dataTypeAxis = {9, 0, 9, "Histogram types"}; - AxisSpec mcTypeAxis = {4, 0, 4, "Histogram types"}; - - // THnSparse - AxisSpec axisAnti = {BinAnti::kNAEnd, 0, BinAnti::kNAEnd, "Type of bin: Normal or Anti"}; - AxisSpec axisType = {BinType::kTYEnd, 0, BinType::kTYEnd, "Type of bin with charge and mix"}; - AxisSpec mcLabelAxis = {5, -0.5, 4.5, "MC Label"}; - - // Micro-only instrumentation: category 0 includes all combinations, not just unmatched. - auto trackFlow = histos.add("CutFlow/tracks", "Micro tracks, once per selected collision;stage;species", HistType::kTH2D, {{static_cast(kTrkNStages), -0.5, static_cast(kTrkNStages) - 0.5}, {2, -0.5, 1.5}}); - const std::array trackLabels{"input", "pT", "eta", "DCAxy", "DCAz", "track flags", "clusters / crossed rows", "TOF required", "PID"}; - for (size_t i = 0; i < trackLabels.size(); ++i) { - trackFlow->GetXaxis()->SetBinLabel(i + 1, trackLabels[i]); - } - trackFlow->GetYaxis()->SetBinLabel(1, "pion"); - trackFlow->GetYaxis()->SetBinLabel(2, "kaon"); - auto candidateFlow = histos.add("CutFlow/candidates", "Unordered micro triplets;stage;category", HistType::kTH2D, {{NCandidateStages, -0.5, NCandidateStages - 0.5}, {3, -0.5, 2.5}}); - const std::array candidateLabels{"input unordered triplets", "distinct pion IDs", "pion selection (quality+PID)", "pion pair constructed", "pair pT", "secondary mass window (rho mode)", "three distinct IDs", "kaon selection (quality+PID)", "K1 rapidity", "candidate cuts", "final US", "final LS"}; - for (size_t i = 0; i < candidateLabels.size(); ++i) { - candidateFlow->GetXaxis()->SetBinLabel(i + 1, candidateLabels[i]); - } - candidateFlow->GetYaxis()->SetBinLabel(1, "all"); - candidateFlow->GetYaxis()->SetBinLabel(2, "rho K"); - candidateFlow->GetYaxis()->SetBinLabel(3, "K* pi"); - if (doprocessMCMicro) { - auto mothers = histos.add("CutFlow/uniqueMothersPerCollision", "Final unique K1 IDs summed over reconstructed collisions (not globally deduplicated)", HistType::kTH1D, {{2, 0.5, 2.5}}); - mothers->GetXaxis()->SetBinLabel(1, "rho K"); - mothers->GetXaxis()->SetBinLabel(2, "K* pi"); - } - if (doprocessMCTrue) { - auto generated = histos.add("CutFlow/generated", "K1 parent rows conditional on selected reconstructed events;stage;immediate channel", HistType::kTH2D, {{2, -0.5, 1.5}, {3, -0.5, 2.5}}); - generated->GetXaxis()->SetBinLabel(1, "all K1 parent rows"); - generated->GetXaxis()->SetBinLabel(2, "K1 rapidity window"); - generated->GetYaxis()->SetBinLabel(1, "other / unresolved"); - generated->GetYaxis()->SetBinLabel(2, "rho K"); - generated->GetYaxis()->SetBinLabel(3, "K* pi"); - } - - // DCA QA - // Primary pion - histos.add("QA/trkppionDCAxy", "DCAxy disstribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QA/trkppionDCAz", "DCAz disstribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QA/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QA/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAcut/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAcut/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAcut/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAcut/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - // Secondary pion - histos.add("QA/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QA/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QA/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QA/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAcut/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAcut/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAcut/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAcut/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - // Kaon - histos.add("QA/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QA/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QA/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); - histos.add("QA/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAcut/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAcut/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAcut/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAcut/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - // K1 - histos.add("QA/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); - histos.add("QA/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); - histos.add("QA/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); - histos.add("QA/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); - histos.add("QA/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); - histos.add("QA/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); - - histos.add("QAcut/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); - histos.add("QAcut/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); - histos.add("QAcut/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); - histos.add("QAcut/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); - histos.add("QAcut/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); - histos.add("QAcut/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); - - // Invariant mass - histos.add("hInvmass_K1", "Invariant mass of K1(1270) (US)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); - histos.add("hInvmass_K1_LS", "Invariant mass of K1(1270) (LS)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); - histos.add("hInvmass_K1_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); - // Mass QA (quick check) - histos.add("k1invmass", "Invariant mass of K1(1270) (US)", HistType::kTH1F, {invMassAxisReso}); - histos.add("k1invmass_LS", "Invariant mass of K1(1270) (LS)", HistType::kTH1F, {invMassAxisReso}); - histos.add("k1invmass_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTH1F, {invMassAxisReso}); - - // MC - if (doprocessMC || doprocessMCMicro) { - AxisSpec channelAxis = {3, -0.5, 2.5, "0: non-K1, 1: rho K, 2: K* pi"}; - histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}}); - histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}}); - histos.add("MCReco/channel", "All selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis}); - histos.add("MCReco/mass", "Reconstructed mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisReso}); - histos.add("MCReco/pt", "Reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis}); - histos.add("MCReco/piPiMass", "pi-pi mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisRho}); - histos.add("MCReco/pi1KMass", "First pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); - histos.add("MCReco/pi2KMass", "Second pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); - histos.add("k1invmass_MC", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); - histos.add("k1invmass_MC_noK1", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); - - histos.add("QAMC/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAMC/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAMC/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAMC/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAMC/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAMC/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAMC/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAMC/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAMC/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAMC/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAMC/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAMC/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAMC/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); - histos.add("QAMC/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); - histos.add("QAMC/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); - histos.add("QAMC/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); - histos.add("QAMC/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); - histos.add("QAMC/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); - } // doprocessMC - if (doprocessMCTrue) { - AxisSpec channelAxis = {3, -0.5, 2.5, "0: other/unresolved, 1: rho K, 2: K* pi"}; - 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}); - histos.add("MCGen/ptChannel", "Generated K1 pT by immediate decay channel", HistType::kTH2D, {channelAxis, ptAxis}); - } // Print output histograms statistics LOG(info) << "Size of the histograms in K1 Analysis Task"; histos.print(); - } // init - - // Resolve the PID cut of one species at a given pT; false if the pT is outside all pT-dependent bins. - template - bool getPIDCut(float pt, PIDCut& cut) - { - if constexpr (S == Species::Pion) { - cut.tpcMax = pionPID.cMaxTPCnSigmaPion; - cut.tofMax = pionPID.cMaxTOFnSigmaPion; - cut.combined = pionPID.nsigmaCutCombinedPion; - cut.tofRequired = false; - if (pionPID.cPionUsePtDepPID) { - const int ptBin = getPtBinIndex(pt, pionPID.cPionPIDPtBins.value); - if (ptBin < 0) { - return false; - } - const auto bin = static_cast(ptBin); - cut.tpcMax = pionPID.cPionTPCNSigmaCuts.value[bin]; - cut.tofMax = pionPID.cPionTOFNSigmaCuts.value[bin]; - cut.tofRequired = pionPID.cPionTOFRequired.value[bin] != 0; - } - } else { - cut.tpcMax = kaonPID.cMaxTPCnSigmaKaon; - cut.tofMax = kaonPID.cMaxTOFnSigmaKaon; - cut.combined = kaonPID.nsigmaCutCombinedKaon; - cut.tofRequired = false; - if (kaonPID.cKaonUsePtDepPID) { - const int ptBin = getPtBinIndex(pt, kaonPID.cKaonPIDPtBins.value); - if (ptBin < 0) { - return false; - } - const auto bin = static_cast(ptBin); - cut.tpcMax = kaonPID.cKaonTPCNSigmaCuts.value[bin]; - cut.tofMax = kaonPID.cKaonTOFNSigmaCuts.value[bin]; - cut.tofRequired = kaonPID.cKaonTOFRequired.value[bin] != 0; - } - } - return true; - } - - // Track quality selection shared by pion and kaon. Returns the last stage that was passed. - // Full tracks store exact values; micro tracks store quantised DCA (see LFResonanceTables.h). - template - int trackQualityStage(const TrackType& track) - { - const double pt = track.pt(); - const double dcaXY = track.dcaXY(); - const double dcaZ = track.dcaZ(); - // Invalid micro DCA codes decode to NaN - if (!std::isfinite(pt) || !std::isfinite(track.eta()) || !std::isfinite(dcaXY) || !std::isfinite(dcaZ)) { - return kTrkInput; - } - if (std::abs(pt) < trackCuts.cMinPtcut) { - return kTrkInput; - } - if (isCutEnabled(trackCuts.cMaxEtacut) && !(std::abs(track.eta()) < trackCuts.cMaxEtacut)) { - return kTrkPt; - } - - if (trackCuts.cfgUsePtDepDCA) { - if constexpr (IsResoMicrotrack) { - if (!track.passedPtDependentDCAxy()) { - return kTrkEta; - } - if (!track.passedPtDependentDCAz()) { - return kTrkDCAxy; - } - } else { - const double dcaPtCut = trackCuts.cDCAToPVByPtP0 + trackCuts.cDCAToPVByPtCoeff * std::pow(pt, -static_cast(trackCuts.cDCAToPVByPtPower)); - if (!(std::abs(dcaXY) < dcaPtCut)) { - return kTrkEta; - } - if (!(std::abs(dcaZ) < dcaPtCut)) { - return kTrkDCAxy; - } - } - } else { - if (isCutEnabled(trackCuts.cMaxDCArToPVcut)) { - if constexpr (IsResoMicrotrack) { - if (!passesBinnedMax(dcaXY, trackCuts.cMaxDCArToPVcut)) { - return kTrkEta; - } - } else { - if (!(std::abs(dcaXY) <= trackCuts.cMaxDCArToPVcut)) { - return kTrkEta; - } - } - } - if (isCutEnabled(trackCuts.cMaxDCAzToPVcut)) { - if constexpr (IsResoMicrotrack) { - if (!passesBinnedMax(dcaZ, trackCuts.cMaxDCAzToPVcut)) { - return kTrkDCAxy; - } - } else { - if (!(std::abs(dcaZ) <= trackCuts.cMaxDCAzToPVcut)) { - return kTrkDCAxy; - } - } - } - } - if (isCutEnabled(trackCuts.cMinDCAzToPVcut)) { - if constexpr (IsResoMicrotrack) { - if (!passesBinnedMin(dcaZ, trackCuts.cMinDCAzToPVcut)) { - return kTrkDCAxy; - } - } else { - if (!(std::abs(dcaZ) >= trackCuts.cMinDCAzToPVcut)) { - return kTrkDCAxy; - } - } - } - - // Track flags - if ((trackCuts.cfgPrimaryTrack && !track.isPrimaryTrack()) || - (trackCuts.cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) || - (trackCuts.cfgGlobalTrack && !track.isGlobalTrack()) || - (trackCuts.cfgPVContributor && !track.isPVContributor()) || - (trackCuts.cfgUseITSRefit && !track.passedITSRefit()) || - (trackCuts.cfgUseTPCRefit && !track.passedTPCRefit())) { - return kTrkDCAz; - } - - // Clusters: found clusters exist only in ResoTracks, ITS clusters only in ResoMicroTracks - if constexpr (!IsResoMicrotrack) { - if constexpr (requires { track.tpcNClsFound(); }) { - if (track.tpcNClsFound() < trackCuts.cfgTPCcluster) { - return kTrkFlags; - } - } - } - if constexpr (requires { track.tpcNClsCrossedRows(); }) { - if (track.tpcNClsCrossedRows() < trackCuts.cfgTPCCrossedRowsMin) { - return kTrkFlags; - } - } - if constexpr (IsResoMicrotrack) { - if constexpr (requires { track.itsNCls(); }) { - if (track.itsNCls() < trackCuts.cfgITSNClsMin) { - return kTrkFlags; - } - } - } - return kTrkClusters; - } - - // TOF signal requirement of the track (global, per species, or per pT bin) - template - bool passesTOFRequired(const TrackType& track) - { - bool required = trackCuts.cfgHasTOF; - if constexpr (S == Species::Pion) { - required = required || pionPID.cUseOnlyTOFTrackPi; - } else { - required = required || kaonPID.cUseOnlyTOFTrackKa; - } - PIDCut cut; - // A pT outside all bins is rejected by passesPID - if (!cByPassTOF && getPIDCut(track.pt(), cut) && cut.tofRequired) { - required = true; - } - return !required || track.hasTOF(); - } - - // PID selection: the same code for full and micro tracks, only the comparison is quantisation aware - template - bool passesPID(const TrackType& track) - { - PIDCut cut; - if (!getPIDCut(track.pt(), cut)) { - return false; - } - const bool hasTOF = track.hasTOF(); - double tpcNSigma = std::numeric_limits::quiet_NaN(); - double tofNSigma = std::numeric_limits::quiet_NaN(); // TOF value is only valid with hasTOF - if constexpr (S == Species::Pion) { - tpcNSigma = track.tpcNSigmaPi(); - if (hasTOF) { - tofNSigma = track.tofNSigmaPi(); - } - } else { - tpcNSigma = track.tpcNSigmaKa(); - if (hasTOF) { - tofNSigma = track.tofNSigmaKa(); - } - } - if (isCutEnabled(cut.tpcMax) && !passesMax(std::abs(tpcNSigma), cut.tpcMax)) { - return false; - } - // Missing TOF is handled by passesTOFRequired; here the TPC alone decides - if (cByPassTOF || !hasTOF) { - return true; - } - bool tofPassed = !isCutEnabled(cut.tofMax) || passesMax(std::abs(tofNSigma), cut.tofMax); - if (!tofPassed && cut.combined > 0 && tpcNSigma * tpcNSigma + tofNSigma * tofNSigma < cut.combined * cut.combined) { - tofPassed = true; - } - return tofPassed; - } - - // Full selection stage of a track (quality, TOF requirement, PID) - template - int trackSelectionStage(const TrackType& track) - { - const int qualityStage = trackQualityStage(track); - if (qualityStage < kTrkClusters) { - return qualityStage; - } - if (!passesTOFRequired(track)) { - return kTrkClusters; - } - if (!passesPID(track)) { - return kTrkTOFRequired; - } - return kTrkPID; - } - - template - bool selectTrack(const TrackType& track) - { - return trackSelectionStage(track) == kTrkPID; - } - - // Selection cache of one track slice. The row number of a grouped slice is global, hence the offset. - template - std::size_t getCacheIndex(const TrackType& track, int64_t firstIndex, std::size_t size) - { - const int64_t index = static_cast(track.index()) - firstIndex; - if (index < 0 || index >= static_cast(size)) { - LOG(fatal) << "Track index " << track.index() << " is outside the selection cache [" << firstIndex << ", " << firstIndex + static_cast(size) << ")"; - } - return static_cast(index); - } - - template - std::vector buildSelectionCache(const TracksType& tracks, int64_t firstIndex) - { - std::vector selected(tracks.size(), 0); - for (const auto& track : tracks) { - const int stage = trackSelectionStage(track); - selected[getCacheIndex(track, firstIndex, selected.size())] = (stage == kTrkPID) ? 1 : 0; - if constexpr (FillCutFlow) { - for (int i = 0; i <= stage; ++i) { - histos.fill(HIST("CutFlow/tracks"), i, static_cast(S)); - } - } - } - return selected; - } - - enum class K1TruthChannel { - None = 0, - RhoK = 1, - KStarPi = 2 - }; - - template - bool hasSibling(const Track& directDaughter, int resonanceId) - { - if (resonanceId < 0) { - return false; - } - const auto siblings = directDaughter.siblingIds(); - return siblings[0] == resonanceId || siblings[1] == resonanceId; - } - - template - bool matchesKStarPi(const Track& resonancePion, const Track& directPion, const Kaon& kaon) - { - const int charge = kaon.pdgCode() > 0 ? 1 : -1; - if (resonancePion.motherId() != kaon.motherId() || - resonancePion.motherId() == directPion.motherId()) { - return false; - } - if (resonancePion.motherPDG() != charge * kK0Star892 || kaon.motherPDG() != charge * kK0Star892) { - return false; - } - if (resonancePion.pdgCode() != -charge * kPiPlus || directPion.pdgCode() != charge * kPiPlus || - directPion.motherPDG() != charge * Pdg::kK1_1270Plus) { - return false; - } - return hasSibling(directPion, kaon.motherId()); - } - - template - K1TruthChannel classifyK1Truth(const Track& pion1, const Track& pion2, const Kaon& kaon) - { - if (std::abs(pion1.pdgCode()) != kPiPlus || std::abs(pion2.pdgCode()) != kPiPlus || - std::abs(kaon.pdgCode()) != kKPlus) { - return K1TruthChannel::None; - } - if (pion1.motherId() < 0 || pion2.motherId() < 0 || kaon.motherId() < 0) { - return K1TruthChannel::None; - } - const int charge = kaon.pdgCode() > 0 ? 1 : -1; - const bool rhoPions = pion1.motherId() == pion2.motherId() && - pion1.motherPDG() == kRho770_0 && pion2.motherPDG() == kRho770_0 && - pion1.pdgCode() == -pion2.pdgCode(); - if (rhoPions && kaon.motherPDG() == charge * Pdg::kK1_1270Plus && - kaon.motherId() != pion1.motherId() && hasSibling(kaon, pion1.motherId())) { - return K1TruthChannel::RhoK; - } - if (matchesKStarPi(pion1, pion2, kaon) || matchesKStarPi(pion2, pion1, kaon)) { - return K1TruthChannel::KStarPi; - } - return K1TruthChannel::None; - } - - // Track QA of a pion; isPrimary selects the trkppion (first) or trkspion (second) histograms - template - void fillPionQA(const TrackType& track, bool isPrimary) - { - const bool hasTOF = track.hasTOF(); - if (isPrimary) { - if constexpr (Folder == QAFolder::Before) { - histos.fill(HIST("QA/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QA/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QA/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QA/trkppionpT"), track.pt()); - histos.fill(HIST("QA/trkppionDCAxy"), track.dcaXY()); - histos.fill(HIST("QA/trkppionDCAz"), track.dcaZ()); - } else if constexpr (Folder == QAFolder::After) { - histos.fill(HIST("QAcut/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAcut/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAcut/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAcut/trkppionpT"), track.pt()); - histos.fill(HIST("QAcut/trkppionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAcut/trkppionDCAz"), track.dcaZ()); - } else { - histos.fill(HIST("QAMC/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAMC/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAMC/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAMC/trkppionpT"), track.pt()); - histos.fill(HIST("QAMC/trkppionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAMC/trkppionDCAz"), track.dcaZ()); - } - } else { - if constexpr (Folder == QAFolder::Before) { - histos.fill(HIST("QA/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QA/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QA/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QA/trkspionpT"), track.pt()); - histos.fill(HIST("QA/trkspionDCAxy"), track.dcaXY()); - histos.fill(HIST("QA/trkspionDCAz"), track.dcaZ()); - } else if constexpr (Folder == QAFolder::After) { - histos.fill(HIST("QAcut/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAcut/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAcut/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAcut/trkspionpT"), track.pt()); - histos.fill(HIST("QAcut/trkspionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAcut/trkspionDCAz"), track.dcaZ()); - } else { - histos.fill(HIST("QAMC/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAMC/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAMC/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAMC/trkspionpT"), track.pt()); - histos.fill(HIST("QAMC/trkspionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAMC/trkspionDCAz"), track.dcaZ()); - } - } - } - - // Track QA of the bachelor kaon - template - void fillKaonQA(const TrackType& track) - { - const bool hasTOF = track.hasTOF(); - if constexpr (Folder == QAFolder::Before) { - histos.fill(HIST("QA/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); - if (hasTOF) { - histos.fill(HIST("QA/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); - histos.fill(HIST("QA/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); - } - histos.fill(HIST("QA/trkkaonpT"), track.pt()); - histos.fill(HIST("QA/trkkaonDCAxy"), track.dcaXY()); - histos.fill(HIST("QA/trkkaonDCAz"), track.dcaZ()); - } else if constexpr (Folder == QAFolder::After) { - histos.fill(HIST("QAcut/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); - if (hasTOF) { - histos.fill(HIST("QAcut/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); - histos.fill(HIST("QAcut/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); - } - histos.fill(HIST("QAcut/trkkaonpT"), track.pt()); - histos.fill(HIST("QAcut/trkkaonDCAxy"), track.dcaXY()); - histos.fill(HIST("QAcut/trkkaonDCAz"), track.dcaZ()); - } else { - histos.fill(HIST("QAMC/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); - if (hasTOF) { - histos.fill(HIST("QAMC/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); - histos.fill(HIST("QAMC/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); - } - histos.fill(HIST("QAMC/trkkaonpT"), track.pt()); - histos.fill(HIST("QAMC/trkkaonDCAxy"), track.dcaXY()); - histos.fill(HIST("QAMC/trkkaonDCAz"), track.dcaZ()); - } - } - - template - void fillHistograms(const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2) - { - if (dTracks1.size() == 0 || dTracks2.size() == 0) { - return; - } - // Sets are local to this reconstructed collision: IDs cannot leak across DFs. - // Source-file/DF deduplication across split collisions belongs in the audit. - std::array, 3> matchedMothers; - - // Selection cache: every track is selected once, not once per pair x bachelor. - // dTracks1: bachelor kaons, dTracks2: pions (different collisions in mixed events). - constexpr bool FillCutFlow = IsResoMicrotrack && !IsMix; - const int64_t firstKaonIndex = dTracks1.begin().index(); - const int64_t firstPionIndex = dTracks2.begin().index(); - const auto kaonSelected = buildSelectionCache(dTracks1, firstKaonIndex); - const auto pionSelected = buildSelectionCache(dTracks2, firstPionIndex); - - // Values needed only by switched-on cuts or QA are computed only then - const bool isK892Mode = secondaryCuts.cfgModeK892orRho; - const bool fillQA = !IsMix && additionalQAplots; - const bool needAngle = IsMC || fillQA || angleCutOn; - const bool needPairAsym = IsMC || fillQA || pairAsymCutOn; - // K892 mode: the K* candidate is (trk1, K), rho mode: the rho is (trk1, trk2) - const bool needMass13 = IsMC || fillQA || (isK892Mode ? secondaryWindowOn : anotherMassCutOn) || (isK892Mode && (needAngle || needPairAsym)); - const bool needMass23 = IsMC || fillQA || piKaMassCutOn; - const bool rhoWindowOn = secondaryWindowOn && !isK892Mode; - - auto multiplicity = collision.cent(); - ROOT::Math::PxPyPzMVector lDecayDaughter1, lDecayDaughter2, lResonanceSecondary, lDecayDaughter_bach, lResonanceK1, lPair13, lPair23; - // Unordered pion pairs: each (pion, pion, kaon) triplet is filled once. - // Here trk1 is the pion with the lower index; the roles are assigned once the bachelor is known. - for (const auto& [trk1, trk2] : combinations(CombinationsStrictlyUpperIndexPolicy(dTracks2, dTracks2))) { - // trk1: pion, trk2: pion, bTrack: kaon - const bool pionsSelected = pionSelected[getCacheIndex(trk1, firstPionIndex, pionSelected.size())] && pionSelected[getCacheIndex(trk2, firstPionIndex, pionSelected.size())]; - bool pairPt = false; - bool rhoWindow = true; - if (pionsSelected) { - // Resonance reconstruction - lDecayDaughter1.SetCoordinates(trk1.px(), trk1.py(), trk1.pz(), MassPionCharged); - lDecayDaughter2.SetCoordinates(trk2.px(), trk2.py(), trk2.pz(), MassPionCharged); - lResonanceSecondary = lDecayDaughter1 + lDecayDaughter2; - pairPt = !(lResonanceSecondary.Pt() < secondaryCuts.cMinSecondaryPtCut); - rhoWindow = !rhoWindowOn || isInWindow(lResonanceSecondary.M(), MassRho770, secondaryCuts.cSecondaryMasswindow); - } - if constexpr (FillCutFlow) { - // Early stages count potential triplets: each pair carries N bachelor trials. - // This preserves the pair-first reconstruction and avoids a new cubic data loop. - // Distinct pion IDs are guaranteed by the strictly upper index policy (stage 1 is always passed). - const int lastStage = !pionsSelected ? 1 : !pairPt ? 3 : !rhoWindow ? 4 : 5; - for (int stage = 0; stage <= lastStage; ++stage) { - histos.fill(HIST("CutFlow/candidates"), stage, 0, static_cast(dTracks1.size())); - } - if constexpr (IsMC) { - // Match before rejecting quality/pT so both channels have an upstream numerator. - if (std::abs(trk1.pdgCode()) == kPiPlus && trk1.pdgCode() == -trk2.pdgCode()) { - for (const auto& bachelor : dTracks1) { - const auto channel = classifyK1Truth(trk1, trk2, bachelor); - if (channel != K1TruthChannel::None) { - for (int stage = 0; stage <= lastStage; ++stage) { - histos.fill(HIST("CutFlow/candidates"), stage, static_cast(channel)); - } - } - } - } - } - } - if (!pionsSelected) { - continue; - } - - if (fillQA) { - fillPionQA(trk1, true); - fillPionQA(trk2, false); - } - - if (!pairPt) { - continue; - } - - if (fillQA) { - histos.fill(HIST("QA/hInvmassSecon"), lResonanceSecondary.M()); - } - if constexpr (IsMC) { - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - } - // Secondary mass window (rho mode): the bachelor loop is skipped for rejected pairs - if (!rhoWindow) { - continue; - } - - for (const auto& bTrack : dTracks1) { - if (bTrack.index() == trk1.index() || bTrack.index() == trk2.index()) { - continue; - } - K1TruthChannel flowChannel = K1TruthChannel::None; - if constexpr (IsMC && IsResoMicrotrack && !IsMix) { - flowChannel = classifyK1Truth(trk1, trk2, bTrack); - } - auto countCandidate = [&](int stage) { - if constexpr (FillCutFlow) { - histos.fill(HIST("CutFlow/candidates"), stage, 0); - if constexpr (IsMC) { - if (flowChannel != K1TruthChannel::None) { - histos.fill(HIST("CutFlow/candidates"), stage, static_cast(flowChannel)); - } - } - } - }; - countCandidate(6); - if (!kaonSelected[getCacheIndex(bTrack, firstKaonIndex, kaonSelected.size())]) { - continue; - } - countCandidate(7); - - if (fillQA) { - fillKaonQA(bTrack); - } - - // Canonical assignment of the pion roles, once the bachelor is known. - // Unlike-sign pair: the pion with the sign opposite to the kaon is pion 1 (K*0 partner), the other is pion 2. - // Like-sign pair (the rule is ambiguous): the pion with the lower index is pion 1. - const bool isUnlikeSign = trk1.sign() * trk2.sign() < 0; - const bool swapPions = isUnlikeSign && trk1.sign() == bTrack.sign(); - const auto& pion1 = swapPions ? trk2 : trk1; - const auto& pion2 = swapPions ? trk1 : trk2; - const auto& lPion1 = swapPions ? lDecayDaughter2 : lDecayDaughter1; - const auto& lPion2 = swapPions ? lDecayDaughter1 : lDecayDaughter2; - - // K1 reconstruction - lDecayDaughter_bach.SetCoordinates(bTrack.px(), bTrack.py(), bTrack.pz(), MassKaonCharged); - lResonanceK1 = lResonanceSecondary + lDecayDaughter_bach; - - // Cuts - if (lResonanceK1.Rapidity() > cK1MaxRap || lResonanceK1.Rapidity() < cK1MinRap) { - continue; - } - countCandidate(8); - - double mass13 = 0.; - double mass23 = 0.; - double lK1Angle = 0.; - double lPairAsym = 0.; - if (needMass13) { - lPair13 = lPion1 + lDecayDaughter_bach; - mass13 = lPair13.M(); - } - if (needMass23) { - lPair23 = lPion2 + lDecayDaughter_bach; - mass23 = lPair23.M(); - } - // Rho mode: secondary = (trk1, trk2) against the bachelor. K892 mode: secondary = (trk1, K) against trk2. - if (needAngle) { - lK1Angle = isK892Mode ? ROOT::Math::VectorUtil::Angle(lPair13, lPion2) : ROOT::Math::VectorUtil::Angle(lResonanceSecondary, lDecayDaughter_bach); - } - if (needPairAsym) { - lPairAsym = isK892Mode ? (lPair13.E() - lPion2.E()) / (lPair13.E() + lPion2.E()) - : (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); - } - - // QA histogram before the candidate cuts - if (fillQA) { - histos.fill(HIST("QA/K1OA"), lK1Angle); - histos.fill(HIST("QA/K1PairAsym"), lPairAsym); - histos.fill(HIST("QA/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); - histos.fill(HIST("QA/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); - histos.fill(HIST("QA/hpT_Secondary"), lResonanceSecondary.Pt()); - } - - // Candidate cuts (each one is evaluated only if switched on) - if (isK892Mode && secondaryWindowOn && (!isInWindow(mass13, MassK0Star892, secondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) { - continue; - } - if (anotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : mass13, secondaryCuts.cMinAnotherSecondaryMassCut, secondaryCuts.cMaxAnotherSecondaryMassCut)) { - continue; - } - if (piKaMassCutOn && !isInRange(mass23, secondaryCuts.cMinPiKaMassCut, secondaryCuts.cMaxPiKaMassCut)) { - continue; - } - if (angleCutOn && !isInRange(lK1Angle, secondaryCuts.cMinAngle, secondaryCuts.cMaxAngle)) { - continue; - } - if (pairAsymCutOn && !isInRange(lPairAsym, secondaryCuts.cMinPairAsym, secondaryCuts.cMaxPairAsym)) { - continue; - } - countCandidate(9); - - // QA histograms after the candidate cuts - if (fillQA) { - fillPionQA(pion1, true); - fillPionQA(pion2, false); - fillKaonQA(bTrack); - histos.fill(HIST("QAcut/K1OA"), lK1Angle); - histos.fill(HIST("QAcut/K1PairAsym"), lPairAsym); - histos.fill(HIST("QAcut/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); - histos.fill(HIST("QAcut/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); - histos.fill(HIST("QAcut/hInvmassSecon"), lResonanceSecondary.M()); - histos.fill(HIST("QAcut/hpT_Secondary"), lResonanceSecondary.Pt()); - } - - countCandidate(isUnlikeSign ? 10 : 11); - if constexpr (IsMC && IsResoMicrotrack && !IsMix) { - if (flowChannel != K1TruthChannel::None) { - const int mother = flowChannel == K1TruthChannel::RhoK ? bTrack.motherId() : - std::abs(pion1.motherPDG()) == Pdg::kK1_1270Plus ? pion1.motherId() : pion2.motherId(); - if (matchedMothers[static_cast(flowChannel)].insert(mother).second) { - histos.fill(HIST("CutFlow/uniqueMothersPerCollision"), static_cast(flowChannel)); - } - } - } - - if constexpr (!IsMix) { - unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P : BinType::kK1N; - unsigned int typeNormal = BinAnti::kNormal; - if (isUnlikeSign) { - histos.fill(HIST("k1invmass"), lResonanceK1.M()); - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - } else { - histos.fill(HIST("k1invmass_LS"), lResonanceK1.M()); - histos.fill(HIST("hInvmass_K1_LS"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - } - - if constexpr (IsMC) { - const auto channel = classifyK1Truth(pion1, pion2, bTrack); - const int channelBin = static_cast(channel); - histos.fill(HIST("MCReco/channel"), channelBin); - histos.fill(HIST("MCReco/mass"), channelBin, lResonanceK1.M()); - histos.fill(HIST("MCReco/pt"), channelBin, lResonanceK1.Pt()); - histos.fill(HIST("MCReco/piPiMass"), channelBin, lResonanceSecondary.M()); - histos.fill(HIST("MCReco/pi1KMass"), channelBin, mass13); - histos.fill(HIST("MCReco/pi2KMass"), channelBin, mass23); - if (channel != K1TruthChannel::None) { - if (truthDebugCounts[channelBin] < cfgTruthDebug) { - ++truthDebugCounts[channelBin]; - LOGF(info, "K1Truth channel=%d collision=%lld tracks=(%lld,%lld,%lld) pdg=(%d,%d,%d) mothers=(%d,%d,%d) motherPDG=(%d,%d,%d) siblings=((%d,%d),(%d,%d),(%d,%d))", - channelBin, static_cast(collision.globalIndex()), - static_cast(pion1.globalIndex()), static_cast(pion2.globalIndex()), static_cast(bTrack.globalIndex()), - pion1.pdgCode(), pion2.pdgCode(), bTrack.pdgCode(), pion1.motherId(), pion2.motherId(), bTrack.motherId(), - pion1.motherPDG(), pion2.motherPDG(), bTrack.motherPDG(), - pion1.siblingIds()[0], pion1.siblingIds()[1], pion2.siblingIds()[0], pion2.siblingIds()[1], bTrack.siblingIds()[0], bTrack.siblingIds()[1]); - } - typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec; - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - histos.fill(HIST("k1invmass_MC"), lResonanceK1.M()); - histos.fill(HIST("QAMC/K1OA"), lK1Angle); - histos.fill(HIST("QAMC/K1PairAsym"), lPairAsym); - histos.fill(HIST("QAMC/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); - histos.fill(HIST("QAMC/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); - histos.fill(HIST("QAMC/hInvmassSecon"), lResonanceSecondary.M()); - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - - // PID QA primary and secondary pion - fillPionQA(pion1, true); - fillPionQA(pion2, false); - fillKaonQA(bTrack); - } else { - histos.fill(HIST("k1invmass_MC_noK1"), lResonanceK1.M()); - } - } // IsMC - } else { - unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Mix : BinType::kK1N_Mix; - unsigned int typeNormal = BinAnti::kNormal; - histos.fill(HIST("hInvmass_K1_Mix"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - histos.fill(HIST("k1invmass_Mix"), lResonanceK1.M()); - } - } // bTrack - } - } // fillHistograms - - template - bool passesEventCuts(const CollisionType& collision) - { - return !(eventCuts.cRecoINELgt0 && !collision.isRecINELgt0()); - } - - template - bool passesMCEventCuts(const CollisionType& collision) - { - if (eventCuts.cMCINELgt0 && !collision.isINELgt0()) { - return false; - } - if (eventCuts.cMCVtxIn10 && !collision.isVtxIn10()) { - return false; - } - return true; } void processResoTracks(ResoCollisions::iterator const& collision, ResoTracks const& resotracks) { - if (!passesEventCuts(collision)) { + if (!core.passesEventCuts(collision)) { return; } - fillHistograms(collision, resotracks, resotracks); + core.fillHistograms(histos, collision, resotracks, resotracks); } PROCESS_SWITCH(K1AnalysisMicro, processResoTracks, "Process ResoTracks", false); void processResoMicroTracks(ResoCollisions::iterator const& collision, ResoMicroTracks const& resomicrotracks) { - if (!passesEventCuts(collision)) { + if (!core.passesEventCuts(collision)) { return; } - fillHistograms(collision, resomicrotracks, resomicrotracks); + core.fillHistograms(histos, collision, resomicrotracks, resomicrotracks); } PROCESS_SWITCH(K1AnalysisMicro, processResoMicroTracks, "Process ResoMicroTracks", true); void processMC(ResoMCCols::iterator const& collision, ResoMCTracks const& resotracks) { - if (!passesEventCuts(collision) || !passesMCEventCuts(collision)) { + if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { return; } histos.fill(HIST("MCReco/collisions"), 0.5); - fillHistograms(collision, resotracks, resotracks); + core.fillHistograms(histos, collision, resotracks, resotracks); } PROCESS_SWITCH(K1AnalysisMicro, processMC, "Process Event for MC", false); void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks) { // The modular producer already selected these reconstructed collisions. - // Apply precisely the same reconstruction loop as the frozen data baseline. - if (!passesEventCuts(collision) || !passesMCEventCuts(collision)) { + // Apply precisely the same reconstruction loop as the data baseline. + if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { return; } histos.fill(HIST("MCReco/collisions"), 0.5); histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size()); - fillHistograms(collision, tracks, tracks); + core.fillHistograms(histos, collision, tracks, tracks); } PROCESS_SWITCH(K1AnalysisMicro, processMCMicro, "Process reconstructed MC with micro v001 tables", false); void processMCTrue(ResoMCCols::iterator const& collision, ResoMCParents const& resoParents) { - if (!passesEventCuts(collision) || !passesMCEventCuts(collision)) { + if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { return; } - // Parents belong to selected reconstructed events; split reco collisions - // repeat parent sets. This is not an unconditional generated denominator. - for (const auto& part : resoParents) { - if (std::abs(part.pdgCode()) != Pdg::kK1_1270Plus) { - continue; - } - const int charge = part.pdgCode() > 0 ? 1 : -1; - const int daughter1 = part.daughterPDG1(); - const int daughter2 = part.daughterPDG2(); - K1TruthChannel channel = K1TruthChannel::None; - if ((daughter1 == kRho770_0 && daughter2 == charge * kKPlus) || - (daughter2 == kRho770_0 && daughter1 == charge * kKPlus)) { - channel = K1TruthChannel::RhoK; - } else if ((daughter1 == charge * kK0Star892 && daughter2 == charge * kPiPlus) || - (daughter2 == charge * kK0Star892 && daughter1 == charge * kPiPlus)) { - channel = K1TruthChannel::KStarPi; - } - histos.fill(HIST("CutFlow/generated"), 0, static_cast(channel)); - if (part.y() < cK1MinRap || part.y() > cK1MaxRap) { - continue; - } - histos.fill(HIST("CutFlow/generated"), 1, static_cast(channel)); - // Keep other/unresolved immediate decays too; never require both pairs. - histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast(channel)); - histos.fill(HIST("MCGen/ptChannel"), static_cast(channel), part.pt()); - } + core.fillGenerated(histos, resoParents); } PROCESS_SWITCH(K1AnalysisMicro, processMCTrue, "Process generated K1 in selected events with v001 parents", false); @@ -1339,10 +153,10 @@ struct K1AnalysisMicro { SameKindPair pairs{colBinning, nEvtMixing, -1, collisions, tracksTuple, &cache}; // -1 is the number of the bin to skip for (const auto& [collision1, tracks1, collision2, tracks2] : pairs) { - if (!passesEventCuts(collision1) || !passesEventCuts(collision2)) { + if (!core.passesEventCuts(collision1) || !core.passesEventCuts(collision2)) { continue; } - fillHistograms(collision1, tracks1, tracks2); + core.fillHistograms(histos, collision1, tracks1, tracks2); } }; PROCESS_SWITCH(K1AnalysisMicro, processME, "Process EventMixing light without partition", false); @@ -1355,14 +169,14 @@ struct K1AnalysisMicro { SameKindPair pairs{colBinning, nEvtMixing, -1, collisions, tracksTuple, &cache}; // -1 is the number of the bin to skip for (const auto& [collision1, tracks1, collision2, tracks2] : pairs) { - if (!passesEventCuts(collision1) || !passesEventCuts(collision2)) { + if (!core.passesEventCuts(collision1) || !core.passesEventCuts(collision2)) { continue; } - fillHistograms(collision1, tracks1, tracks2); + core.fillHistograms(histos, collision1, tracks1, tracks2); } }; PROCESS_SWITCH(K1AnalysisMicro, processMEMicro, "Process EventMixing light without partition", false); -}; // struct +}; WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) { From 58087ca6d88741f15552b739c6e0256b275332ba Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Sun, 4 Oct 2026 09:53:49 +0900 Subject: [PATCH 4/9] [PWGLF] Add K1 ML training-table exporter K1TrainingTable Add a derived-table workflow that writes the unlike-sign K1(1270) micro candidates selected by the shared K1 core, together with the canonical tracks and a 125-entry feature vector for ML training. - PWGLF/Core/K1MlFeatures.h (new): canonical unlike-sign role assignment (kaon, same-sign pion, opposite-sign pion) and the master feature builder of the frozen feature contract (FeatureContractSha256), with compile-time checks of the feature-name count and of the projection indices. The pion mass keeps the contract value 0.13957039 GeV (O2 MassPionCharged is 0.1395704 and would change the features). - PWGLF/DataModel/LFK1MlTables.h (new): K1MlEvents, K1MlTracks, K1MlCandidates, K1MlInputs (float[125] features), K1MlTruth and K1MlGenAudit. Relations point only to the derived tables. - K1AnalysisMicroCore.h: optional candidate callback and loose-stage traversal (pass bits 1/2/4/8/16; selected = 31), loose cut-flow histograms. The histogram task does not use them and is unchanged. Candidates at the selected stage are emitted only if they satisfy the canonical/feature contract, as at the loose stage. - k1TrainingTable.cxx (new, workflow k1-training-table): struct K1TrainingTable reuses the core loop; invalid candidates are skipped and counted in ML/exportSkipped by build status instead of aborting. --- PWGLF/Core/K1AnalysisMicroCore.h | 201 ++++- PWGLF/Core/K1MlFeatures.h | 878 +++++++++++++++++++++ PWGLF/DataModel/LFK1MlTables.h | 125 +++ PWGLF/Tasks/Resonances/CMakeLists.txt | 5 + PWGLF/Tasks/Resonances/k1TrainingTable.cxx | 274 +++++++ 5 files changed, 1449 insertions(+), 34 deletions(-) create mode 100644 PWGLF/Core/K1MlFeatures.h create mode 100644 PWGLF/DataModel/LFK1MlTables.h create mode 100644 PWGLF/Tasks/Resonances/k1TrainingTable.cxx diff --git a/PWGLF/Core/K1AnalysisMicroCore.h b/PWGLF/Core/K1AnalysisMicroCore.h index 63b24667c37..e254b485add 100644 --- a/PWGLF/Core/K1AnalysisMicroCore.h +++ b/PWGLF/Core/K1AnalysisMicroCore.h @@ -17,6 +17,7 @@ #ifndef PWGLF_CORE_K1ANALYSISMICROCORE_H_ #define PWGLF_CORE_K1ANALYSISMICROCORE_H_ +#include "PWGLF/Core/K1MlFeatures.h" #include "PWGLF/DataModel/LFResonanceTables.h" #include @@ -40,6 +41,7 @@ #include #include #include +#include #include namespace o2::analysis::k1micro @@ -100,6 +102,16 @@ enum class QAFolder { MC // QAMC/*: matched K1 truth candidates }; +// Cumulative selection bits of an unlike-sign candidate handed to the candidate callback. +enum CandidatePassBit : uint16_t { + kPassLoose = 1, // valid canonical candidate inside the K1 rapidity window + kPassQuality = 2, // track quality of all three tracks + kPassPID = 4, // TOF requirement and PID of all three tracks + kPassPair = 8, // pion-pair pT and secondary mass window + kPassCandidate = 16 // candidate cuts +}; +inline constexpr uint16_t PassBitsSelected = kPassLoose | kPassQuality | kPassPID | kPassPair | kPassCandidate; + // Resolved PID cut of one species at a given pT. struct PIDCut { double tpcMax = 0.; @@ -221,6 +233,13 @@ struct ProcessModes { bool mcGen = false; // generated K1 parents in selected reconstructed events }; +/// Loose-stage traversal of unlike-sign micro candidates for the candidate callback. +/// With the defaults and without a callback, the candidate loop applies only the conventional selection. +struct LooseStageOptions { + bool audit = false; // fill ML/looseCutflow and ML/looseMassPtActivity + bool exportSelected = false; // hand candidates to the callback at the selected stage instead of the loose stage +}; + // A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value). inline bool isCutEnabled(float value) { @@ -453,7 +472,8 @@ class K1AnalysisMicroCore EventCuts const& eventCuts, TrackCuts const& trackCuts, PionPidCuts const& pionPidCuts, KaonPidCuts const& kaonPidCuts, SecondaryCuts const& secondaryCuts, CandidateCuts const& candidateCuts, - HistogramOptions const& histogramOptions, ProcessModes const& modes) + HistogramOptions const& histogramOptions, ProcessModes const& modes, + LooseStageOptions const& looseOptions = {}) { mEventCuts = eventCuts; mTrackCuts = trackCuts; @@ -462,6 +482,7 @@ class K1AnalysisMicroCore mSecondaryCuts = secondaryCuts; mCandidateCuts = candidateCuts; mHistogramOptions = histogramOptions; + mLooseOptions = looseOptions; mTruthDebugCounts = {}; mSecondaryWindowOn = isCutEnabled(mSecondaryCuts.cSecondaryMasswindow); @@ -707,13 +728,16 @@ class K1AnalysisMicroCore } // Unordered (pion, pion, kaon) candidate loop of one collision (or one mixed pair of collisions). - // dTracks1: bachelor kaons, dTracks2: pions. - template - void fillHistograms(o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2) + // dTracks1: bachelor kaons, dTracks2: pions. The optional callback receives the unlike-sign micro + // same-event candidates in the canonical roles (collision, kaon, same-sign pion, opposite-sign pion, + // truth channel, pass bits) at the loose or selected stage configured by LooseStageOptions. + template + void fillHistograms(o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2, Callback callback = nullptr) { if (dTracks1.size() == 0 || dTracks2.size() == 0) { return; } + constexpr bool HasCallback = !std::is_same_v; // Sets are local to this reconstructed collision: IDs cannot leak across DFs. // Source-file/DF deduplication across split collisions belongs in the audit. std::array, NTruthChannels> matchedMothers; @@ -726,6 +750,24 @@ class K1AnalysisMicroCore const auto kaonSelected = buildSelectionCache(histos, dTracks1, firstKaonIndex); const auto pionSelected = buildSelectionCache(histos, dTracks2, firstPionIndex); + // Only micro same-event ML work needs traversal before conventional cuts. + // The canonical-candidate validity is also required before a selected-stage callback. + bool visitLoose = false; + bool checkValidity = false; + if constexpr (IsResoMicrotrack && !IsMix) { + visitLoose = mLooseOptions.audit || (!mLooseOptions.exportSelected && HasCallback); + checkValidity = visitLoose || (mLooseOptions.exportSelected && HasCallback); + } + std::vector kaonQuality(dTracks1.size(), 0), pionQuality(dTracks2.size(), 0); + if (visitLoose) { + for (const auto& track : dTracks1) { + kaonQuality[getCacheIndex(track, firstKaonIndex, kaonQuality.size())] = trackQualityStage(track) == kTrkClusters; + } + for (const auto& track : dTracks2) { + pionQuality[getCacheIndex(track, firstPionIndex, pionQuality.size())] = trackQualityStage(track) == kTrkClusters; + } + } + // Values needed only by switched-on cuts or QA are computed only then const bool isK892Mode = mSecondaryCuts.cfgModeK892orRho; const bool fillQA = !IsMix && mHistogramOptions.additionalQAplots; @@ -745,7 +787,7 @@ class K1AnalysisMicroCore const bool pionsSelected = pionSelected[getCacheIndex(trk1, firstPionIndex, pionSelected.size())] && pionSelected[getCacheIndex(trk2, firstPionIndex, pionSelected.size())]; bool pairPt = false; bool rhoWindow = true; - if (pionsSelected) { + if (pionsSelected || visitLoose) { // Resonance reconstruction lDecayDaughter1.SetCoordinates(trk1.px(), trk1.py(), trk1.pz(), o2::constants::physics::MassPionCharged); lDecayDaughter2.SetCoordinates(trk2.px(), trk2.py(), trk2.pz(), o2::constants::physics::MassPionCharged); @@ -777,27 +819,29 @@ class K1AnalysisMicroCore } } } - if (!pionsSelected) { + if (!pionsSelected && !visitLoose) { continue; } - if (fillQA) { + if (fillQA && pionsSelected) { fillPionQA(histos, trk1, true); fillPionQA(histos, trk2, false); } - if (!pairPt) { + if (!pairPt && !visitLoose) { continue; } - if (fillQA) { + if (fillQA && pionsSelected && pairPt) { histos.fill(HIST("QA/hInvmassSecon"), lResonanceSecondary.M()); } if constexpr (IsMC) { - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); + if (pionsSelected && pairPt) { + histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); + } } // Secondary mass window (rho mode): the bachelor loop is skipped for rejected pairs - if (!rhoWindow) { + if (!rhoWindow && !visitLoose) { continue; } @@ -819,13 +863,19 @@ class K1AnalysisMicroCore } } }; - countCandidate(6); - if (!kaonSelected[getCacheIndex(bTrack, firstKaonIndex, kaonSelected.size())]) { + const bool pairSelected = pionsSelected && pairPt && rhoWindow; + const bool bachelorSelected = kaonSelected[getCacheIndex(bTrack, firstKaonIndex, kaonSelected.size())]; + if (pairSelected) { + countCandidate(6); + } + if ((!pairSelected || !bachelorSelected) && !visitLoose) { continue; } - countCandidate(7); + if (pairSelected && bachelorSelected) { + countCandidate(7); + } - if (fillQA) { + if (fillQA && pairSelected && bachelorSelected) { fillKaonQA(histos, bTrack); } @@ -843,11 +893,37 @@ class K1AnalysisMicroCore lDecayDaughter_bach.SetCoordinates(bTrack.px(), bTrack.py(), bTrack.pz(), o2::constants::physics::MassKaonCharged); lResonanceK1 = lResonanceSecondary + lDecayDaughter_bach; - // Cuts + auto countMl = [&](int stage) { + if (mLooseOptions.audit && isUnlikeSign) { + histos.fill(HIST("ML/looseCutflow"), stage, 0); + if (flowChannel != K1TruthChannel::None) { + const int stratum = 2 * static_cast(flowChannel) - (bTrack.sign() > 0 ? 1 : 0); + histos.fill(HIST("ML/looseCutflow"), stage, stratum); + } + } + }; + bool validLoose = false; + if constexpr (IsResoMicrotrack && !IsMix) { + if (checkValidity && isUnlikeSign) { + const auto canonical = o2::analysis::k1ml::canonicalizeUS(o2::analysis::k1ml::makeTrackSnapshot(bTrack), o2::analysis::k1ml::makeTrackSnapshot(pion2), o2::analysis::k1ml::makeTrackSnapshot(pion1)); + validLoose = canonical.status == o2::analysis::k1ml::BuildStatus::Ok && + std::isfinite(lResonanceK1.M()) && std::isfinite(lResonanceK1.Pt()) && + std::isfinite(lResonanceK1.Rapidity()) && std::isfinite(lResonanceK1.Eta()) && + std::isfinite(lResonanceK1.Phi()) && + o2::analysis::k1ml::buildMasterFeatures(canonical.candidate).status == o2::analysis::k1ml::BuildStatus::Ok; + if (validLoose) { + countMl(0); + } + } + } + + // Stage L common acceptance uses the existing inclusive rapidity window. if (lResonanceK1.Rapidity() > mCandidateCuts.cK1MaxRap || lResonanceK1.Rapidity() < mCandidateCuts.cK1MinRap) { continue; } - countCandidate(8); + if (pairSelected && bachelorSelected) { + countCandidate(8); + } double mass13 = 0.; double mass23 = 0.; @@ -870,6 +946,54 @@ class K1AnalysisMicroCore : (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); } + // Candidate cuts (each one is evaluated only if switched on) + const bool candidateCutsPass = + !(isK892Mode && mSecondaryWindowOn && (!isInWindow(mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) && + !(mAnotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) && + !(mPiKaMassCutOn && !isInRange(mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) && + !(mAngleCutOn && !isInRange(lK1Angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) && + !(mPairAsymCutOn && !isInRange(lPairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)); + auto emitCandidate = [&](uint16_t passBits) { + if constexpr (IsResoMicrotrack && !IsMix && HasCallback) { + callback(collision, bTrack, pion2, pion1, flowChannel, passBits); + } else { + static_cast(passBits); + } + }; + if (visitLoose && validLoose) { + countMl(1); + if (mLooseOptions.audit) { + histos.fill(HIST("ML/looseMassPtActivity"), lResonanceK1.M(), lResonanceK1.Pt(), collision.cent()); + } + const bool qualityPass = kaonQuality[getCacheIndex(bTrack, firstKaonIndex, kaonQuality.size())] && + pionQuality[getCacheIndex(trk1, firstPionIndex, pionQuality.size())] && + pionQuality[getCacheIndex(trk2, firstPionIndex, pionQuality.size())]; + uint16_t passBits = kPassLoose; + if (qualityPass) { + passBits |= kPassQuality; + countMl(2); + if (pionsSelected && bachelorSelected) { + passBits |= kPassPID; + countMl(3); + if (pairPt && rhoWindow) { + passBits |= kPassPair; + countMl(4); + if (candidateCutsPass) { + passBits |= kPassCandidate; + countMl(5); + } + } + } + } + if (!mLooseOptions.exportSelected) { + emitCandidate(passBits); + } + } + // Stage C retains the frozen conventional selections and QA population. + if (!pairSelected || !bachelorSelected) { + continue; + } + // QA histogram before the candidate cuts if (fillQA) { histos.fill(HIST("QA/K1OA"), lK1Angle); @@ -879,20 +1003,7 @@ class K1AnalysisMicroCore histos.fill(HIST("QA/hpT_Secondary"), lResonanceSecondary.Pt()); } - // Candidate cuts (each one is evaluated only if switched on) - if (isK892Mode && mSecondaryWindowOn && (!isInWindow(mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) { - continue; - } - if (mAnotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) { - continue; - } - if (mPiKaMassCutOn && !isInRange(mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) { - continue; - } - if (mAngleCutOn && !isInRange(lK1Angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) { - continue; - } - if (mPairAsymCutOn && !isInRange(lPairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)) { + if (!candidateCutsPass) { continue; } countCandidate(9); @@ -911,6 +1022,9 @@ class K1AnalysisMicroCore } countCandidate(isUnlikeSign ? 10 : 11); + if (isUnlikeSign && mLooseOptions.exportSelected && validLoose) { + emitCandidate(PassBitsSelected); + } if constexpr (IsMC && IsResoMicrotrack && !IsMix) { if (flowChannel != K1TruthChannel::None) { const int mother = flowChannel == K1TruthChannel::RhoK ? bTrack.motherId() : std::abs(pion1.motherPDG()) == o2::constants::physics::Pdg::kK1_1270Plus ? pion1.motherId() @@ -979,11 +1093,12 @@ class K1AnalysisMicroCore } } // fillHistograms - // Generated K1 parents of a selected reconstructed MC collision. + // Generated K1 parents of a selected reconstructed MC collision. The optional callback receives + // (parent, immediate channel) for the parents inside the K1 rapidity window. // Parents belong to selected reconstructed events; split reco collisions // repeat parent sets. This is not an unconditional generated denominator. - template - void fillGenerated(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents) + template + void fillGenerated(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents, Callback callback = nullptr) { for (const auto& part : resoParents) { if (std::abs(part.pdgCode()) != o2::constants::physics::Pdg::kK1_1270Plus) { @@ -999,6 +1114,9 @@ class K1AnalysisMicroCore // Keep other/unresolved immediate decays too; never require both pairs. histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast(channel)); histos.fill(HIST("MCGen/ptChannel"), static_cast(channel), part.pt()); + if constexpr (!std::is_same_v) { + callback(part, channel); + } } } @@ -1143,6 +1261,20 @@ class K1AnalysisMicroCore AxisSpec axisAnti = {BinAnti::kNAEnd, 0, BinAnti::kNAEnd, "Type of bin: Normal or Anti"}; AxisSpec axisType = {BinType::kTYEnd, 0, BinType::kTYEnd, "Type of bin with charge and mix"}; + if (mLooseOptions.audit) { + auto flow = histos.add("ML/looseCutflow", "US triplets;stage;signal stratum", HistType::kTH2D, {{6, -0.5, 5.5}, {5, -0.5, 4.5}}); + const std::array labels{"structural US", "loose acceptance", "track quality", "TOF + PID", "pair requirements", "selected US"}; + const std::array strata{"all US", "rhoK+", "rhoK-", "KstarPi+", "KstarPi-"}; + for (std::size_t i = 0; i < labels.size(); ++i) { + flow->GetXaxis()->SetBinLabel(i + 1, labels[i]); + } + for (std::size_t i = 0; i < strata.size(); ++i) { + flow->GetYaxis()->SetBinLabel(i + 1, strata[i]); + } + histos.add("ML/looseMassPtActivity", "Loose US;mass (GeV/c^{2});pT (GeV/c);FT0M percentile", HistType::kTH3D, + {{300, 0.7, 3.7}, {{0., 0.5, 1., 2., 3., 5., 8., 15., 30., 100.}, "pT"}, {{0., 10., 30., 50., 70., 100., 110.}, "FT0M percentile"}}); + } + // Micro-only instrumentation: category 0 includes all combinations, not just unmatched. auto trackFlow = histos.add("CutFlow/tracks", "Micro tracks, once per selected collision;stage;species", HistType::kTH2D, {{static_cast(kTrkNStages), -0.5, static_cast(kTrkNStages) - 0.5}, {2, -0.5, 1.5}}); const std::array trackLabels{"input", "pT", "eta", "DCAxy", "DCAz", "track flags", "clusters / crossed rows", "TOF required", "PID"}; @@ -1299,6 +1431,7 @@ class K1AnalysisMicroCore SecondaryCuts mSecondaryCuts; CandidateCuts mCandidateCuts; HistogramOptions mHistogramOptions; + LooseStageOptions mLooseOptions; // Derived once in init(): which candidate cuts are switched on. bool mSecondaryWindowOn = false; diff --git a/PWGLF/Core/K1MlFeatures.h b/PWGLF/Core/K1MlFeatures.h new file mode 100644 index 00000000000..6ebfc742a62 --- /dev/null +++ b/PWGLF/Core/K1MlFeatures.h @@ -0,0 +1,878 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License version 3, copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file K1MlFeatures.h +/// \brief Canonical micro001 K1 candidate and feature contract helpers +/// \author Bong-Hwi Lim +/// + +#ifndef PWGLF_CORE_K1MLFEATURES_H_ +#define PWGLF_CORE_K1MLFEATURES_H_ + +#include "PWGLF/DataModel/LFResonanceTables.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace o2::analysis::k1ml +{ +inline constexpr std::size_t NItsLayers = 7; +inline constexpr std::size_t NCandidateTracks = 3; +inline constexpr std::size_t NMasterFeatures = 125; + +enum class Role : uint8_t { Kaon, + PionSame, + PionOpp }; +enum class Profile : uint8_t { DetectorV1, + RelationalV1, + SubstructureV1 }; +enum class BuildStatus : uint8_t { + Ok, + InvalidChargePattern, + ReusedTrack, + InvalidMomentum, + InvalidKinematics, + InvalidContract +}; + +struct TrackSnapshot { + int64_t sourceTrackId = -1; + int8_t charge = 0; + float px = 0.f, py = 0.f, pz = 0.f; + uint8_t pidNSigmaPiFlag = 0, pidNSigmaKaFlag = 0, pidNSigmaPrFlag = 0; + uint8_t trackSelectionFlags = 0, trackFlags = 0, tpcNClsCrossedRows = 0, itsClusterMap = 0; + std::array tpcNSigma{}; // pi, ka, pr + std::array tofNSigma{}; // pi, ka, pr + float dcaXY = std::numeric_limits::quiet_NaN(); + float dcaZ = std::numeric_limits::quiet_NaN(); + bool passedPtDependentDCAxy = false, passedPtDependentDCAz = false; + bool hasTOF = false, isPVContributor = false; + std::array itsHit{}; +}; + +struct CandidateSnapshot { + std::array tracks{}; // K, same-charge pion, opposite-charge pion +}; + +struct CanonicalizationResult { + CandidateSnapshot candidate{}; + BuildStatus status = BuildStatus::InvalidChargePattern; + explicit operator bool() const { return status == BuildStatus::Ok; } +}; + +struct KinematicAudit { + float massKPiPi = 0.f; + float massPiPi = 0.f; + float massKaonPiSame = 0.f; + float massKaonPiOpp = 0.f; + float candidatePt = 0.f; + float candidateEta = 0.f; + float candidatePhi = 0.f; + float scalarSumPt = 0.f; +}; + +struct FeaturePack { + std::array master{}; + BuildStatus status = BuildStatus::InvalidContract; + KinematicAudit kinematics{}; +}; + +// Adapt the real ResoMicroTracks_001 public columns and dynamic accessors. +// Keeping the raw bytes alongside decoded accessors makes the encoding auditable. +template +TrackSnapshot makeTrackSnapshot(MicroTrack const& row) +{ + TrackSnapshot out; + out.sourceTrackId = static_cast(row.trackId()); + out.charge = static_cast(row.sign()); + out.px = static_cast(row.px()); + out.py = static_cast(row.py()); + out.pz = static_cast(row.pz()); + out.pidNSigmaPiFlag = static_cast(row.pidNSigmaPiFlag()); + out.pidNSigmaKaFlag = static_cast(row.pidNSigmaKaFlag()); + out.pidNSigmaPrFlag = static_cast(row.pidNSigmaPrFlag()); + out.trackSelectionFlags = static_cast(row.trackSelectionFlags()); + out.trackFlags = static_cast(row.trackFlags()); + out.tpcNClsCrossedRows = static_cast(row.tpcNClsCrossedRows()); + out.itsClusterMap = static_cast(row.itsClusterMap()); + out.tpcNSigma = {static_cast(row.tpcNSigmaPi()), static_cast(row.tpcNSigmaKa()), static_cast(row.tpcNSigmaPr())}; + out.tofNSigma = {static_cast(row.tofNSigmaPi()), static_cast(row.tofNSigmaKa()), static_cast(row.tofNSigmaPr())}; + out.dcaXY = static_cast(row.dcaXY()); + out.dcaZ = static_cast(row.dcaZ()); + out.passedPtDependentDCAxy = row.passedPtDependentDCAxy(); + out.passedPtDependentDCAz = row.passedPtDependentDCAz(); + out.hasTOF = row.hasTOF(); + out.isPVContributor = row.isPVContributor(); + for (std::size_t layer = 0; layer < NItsLayers; ++layer) { + out.itsHit[layer] = row.hasITSHitInLayer(static_cast(layer)); + } + return out; +} + +inline bool hasFiniteMomentum(TrackSnapshot const& track) +{ + return std::isfinite(track.px) && std::isfinite(track.py) && std::isfinite(track.pz) && + std::hypot(track.px, track.py) > 0.f; +} + +inline CanonicalizationResult canonicalizeUS(TrackSnapshot const& kaon, + TrackSnapshot const& pionA, + TrackSnapshot const& pionB) +{ + CanonicalizationResult result; + if (kaon.sourceTrackId == pionA.sourceTrackId || kaon.sourceTrackId == pionB.sourceTrackId || + pionA.sourceTrackId == pionB.sourceTrackId) { + result.status = BuildStatus::ReusedTrack; + return result; + } + if ((kaon.charge != 1 && kaon.charge != -1) || (pionA.charge != 1 && pionA.charge != -1) || + (pionB.charge != 1 && pionB.charge != -1) || pionA.charge == pionB.charge || + (pionA.charge != kaon.charge && pionB.charge != kaon.charge)) { + result.status = BuildStatus::InvalidChargePattern; + return result; + } + if (!hasFiniteMomentum(kaon) || !hasFiniteMomentum(pionA) || !hasFiniteMomentum(pionB)) { + result.status = BuildStatus::InvalidMomentum; + return result; + } + const auto& same = pionA.charge == kaon.charge ? pionA : pionB; + const auto& opposite = pionA.charge == kaon.charge ? pionB : pionA; + result.candidate.tracks = {kaon, same, opposite}; + result.status = BuildStatus::Ok; + return result; +} + +namespace detail +{ +struct EncodedValue { + float value; + float valid; + float overflow; +}; + +inline EncodedValue encodePID(float decoded) +{ + if (std::isnan(decoded)) { + return {0.f, 0.f, 0.f}; + } + if (std::isinf(decoded)) { + return {std::signbit(decoded) ? -3.5f : 3.5f, 1.f, 1.f}; + } + return {decoded, 1.f, 0.f}; +} + +inline EncodedValue encodeDCA(float decoded) +{ + if (!std::isfinite(decoded)) { + return {0.f, 0.f, 0.f}; + } + const bool overflow = decoded == o2::aod::resomicrodaughter001::DCAEncoding::MaxDCA; + return {decoded, 1.f, overflow ? 1.f : 0.f}; +} + +struct Kinematics { + double pt; + double eta; + double phi; + double energy; +}; + +inline bool getKinematics(TrackSnapshot const& track, double mass, Kinematics& out) +{ + if (!hasFiniteMomentum(track)) { + return false; + } + const double px = track.px, py = track.py, pz = track.pz; + out.pt = std::hypot(px, py); + const double p = std::hypot(out.pt, pz); + out.eta = std::asinh(pz / out.pt); + out.phi = std::atan2(py, px); + out.energy = std::sqrt(p * p + mass * mass); + return std::isfinite(out.pt) && std::isfinite(out.eta) && std::isfinite(out.phi) && std::isfinite(out.energy); +} + +inline float invariantMass(Kinematics const& a, Kinematics const& b) +{ + const auto pxa = a.pt * std::cos(a.phi), pya = a.pt * std::sin(a.phi); + const auto pxb = b.pt * std::cos(b.phi), pyb = b.pt * std::sin(b.phi); + const double e = a.energy + b.energy; + const double px = pxa + pxb, py = pya + pyb; + // pz is recovered from pt*sinh(eta), matching the stored three-momentum. + const double pz = a.pt * std::sinh(a.eta) + b.pt * std::sinh(b.eta); + const double m2 = e * e - px * px - py * py - pz * pz; + return static_cast(std::sqrt(std::max(0.0, m2))); +} + +inline float invariantMass(std::array const& tracks, + std::array const& indices) +{ + double e = 0., px = 0., py = 0., pz = 0.; + for (const auto& i : indices) { + const auto& track = tracks[i]; + e += track.energy; + px += track.pt * std::cos(track.phi); + py += track.pt * std::sin(track.phi); + pz += track.pt * std::sinh(track.eta); + } + return static_cast(std::sqrt(std::max(0.0, e * e - px * px - py * py - pz * pz))); +} + +inline void append(std::array& out, std::size_t& index, EncodedValue value) +{ + out[index++] = value.value; + out[index++] = value.valid; + out[index++] = value.overflow; +} + +template +constexpr bool isStrictlyIncreasingBelow(std::array const& indices, std::size_t bound) +{ + for (std::size_t i = 0; i < N; ++i) { + if (indices[i] >= bound || (i > 0 && indices[i - 1] >= indices[i])) { + return false; + } + } + return true; +} +} // namespace detail + +// Names and projection indices are generated from feature_contract_v1.json. +inline constexpr std::array MasterFeatureNames{ + "kaon.tpc_nsigma_pi", + "kaon.tpc_nsigma_pi_valid", + "kaon.tpc_nsigma_pi_overflow", + "kaon.tpc_nsigma_ka", + "kaon.tpc_nsigma_ka_valid", + "kaon.tpc_nsigma_ka_overflow", + "kaon.tpc_nsigma_pr", + "kaon.tpc_nsigma_pr_valid", + "kaon.tpc_nsigma_pr_overflow", + "kaon.tof_nsigma_pi", + "kaon.tof_nsigma_pi_valid", + "kaon.tof_nsigma_pi_overflow", + "kaon.tof_nsigma_ka", + "kaon.tof_nsigma_ka_valid", + "kaon.tof_nsigma_ka_overflow", + "kaon.tof_nsigma_pr", + "kaon.tof_nsigma_pr_valid", + "kaon.tof_nsigma_pr_overflow", + "kaon.abs_dca_xy", + "kaon.abs_dca_xy_valid", + "kaon.abs_dca_xy_overflow", + "kaon.abs_dca_z", + "kaon.abs_dca_z_valid", + "kaon.abs_dca_z_overflow", + "kaon.passed_ptdep_dca_xy", + "kaon.passed_ptdep_dca_z", + "kaon.has_tof", + "kaon.tpc_crossed_rows", + "kaon.its_hit_l0", + "kaon.its_hit_l1", + "kaon.its_hit_l2", + "kaon.its_hit_l3", + "kaon.its_hit_l4", + "kaon.its_hit_l5", + "kaon.its_hit_l6", + "kaon.is_pv_contributor", + "kaon.pt_fraction", + "pion_same.tpc_nsigma_pi", + "pion_same.tpc_nsigma_pi_valid", + "pion_same.tpc_nsigma_pi_overflow", + "pion_same.tpc_nsigma_ka", + "pion_same.tpc_nsigma_ka_valid", + "pion_same.tpc_nsigma_ka_overflow", + "pion_same.tpc_nsigma_pr", + "pion_same.tpc_nsigma_pr_valid", + "pion_same.tpc_nsigma_pr_overflow", + "pion_same.tof_nsigma_pi", + "pion_same.tof_nsigma_pi_valid", + "pion_same.tof_nsigma_pi_overflow", + "pion_same.tof_nsigma_ka", + "pion_same.tof_nsigma_ka_valid", + "pion_same.tof_nsigma_ka_overflow", + "pion_same.tof_nsigma_pr", + "pion_same.tof_nsigma_pr_valid", + "pion_same.tof_nsigma_pr_overflow", + "pion_same.abs_dca_xy", + "pion_same.abs_dca_xy_valid", + "pion_same.abs_dca_xy_overflow", + "pion_same.abs_dca_z", + "pion_same.abs_dca_z_valid", + "pion_same.abs_dca_z_overflow", + "pion_same.passed_ptdep_dca_xy", + "pion_same.passed_ptdep_dca_z", + "pion_same.has_tof", + "pion_same.tpc_crossed_rows", + "pion_same.its_hit_l0", + "pion_same.its_hit_l1", + "pion_same.its_hit_l2", + "pion_same.its_hit_l3", + "pion_same.its_hit_l4", + "pion_same.its_hit_l5", + "pion_same.its_hit_l6", + "pion_same.is_pv_contributor", + "pion_same.pt_fraction", + "pion_opp.tpc_nsigma_pi", + "pion_opp.tpc_nsigma_pi_valid", + "pion_opp.tpc_nsigma_pi_overflow", + "pion_opp.tpc_nsigma_ka", + "pion_opp.tpc_nsigma_ka_valid", + "pion_opp.tpc_nsigma_ka_overflow", + "pion_opp.tpc_nsigma_pr", + "pion_opp.tpc_nsigma_pr_valid", + "pion_opp.tpc_nsigma_pr_overflow", + "pion_opp.tof_nsigma_pi", + "pion_opp.tof_nsigma_pi_valid", + "pion_opp.tof_nsigma_pi_overflow", + "pion_opp.tof_nsigma_ka", + "pion_opp.tof_nsigma_ka_valid", + "pion_opp.tof_nsigma_ka_overflow", + "pion_opp.tof_nsigma_pr", + "pion_opp.tof_nsigma_pr_valid", + "pion_opp.tof_nsigma_pr_overflow", + "pion_opp.abs_dca_xy", + "pion_opp.abs_dca_xy_valid", + "pion_opp.abs_dca_xy_overflow", + "pion_opp.abs_dca_z", + "pion_opp.abs_dca_z_valid", + "pion_opp.abs_dca_z_overflow", + "pion_opp.passed_ptdep_dca_xy", + "pion_opp.passed_ptdep_dca_z", + "pion_opp.has_tof", + "pion_opp.tpc_crossed_rows", + "pion_opp.its_hit_l0", + "pion_opp.its_hit_l1", + "pion_opp.its_hit_l2", + "pion_opp.its_hit_l3", + "pion_opp.its_hit_l4", + "pion_opp.its_hit_l5", + "pion_opp.its_hit_l6", + "pion_opp.is_pv_contributor", + "pion_opp.pt_fraction", + "kaon__pion_same.delta_eta", + "kaon__pion_same.sin_delta_phi", + "kaon__pion_same.cos_delta_phi", + "kaon__pion_same.z_pt", + "kaon__pion_opp.delta_eta", + "kaon__pion_opp.sin_delta_phi", + "kaon__pion_opp.cos_delta_phi", + "kaon__pion_opp.z_pt", + "pion_same__pion_opp.delta_eta", + "pion_same__pion_opp.sin_delta_phi", + "pion_same__pion_opp.cos_delta_phi", + "pion_same__pion_opp.z_pt", + "mass_pi_pi", + "mass_kaon_pion_opp"}; +inline constexpr std::array DetectorV1Projection{ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109}; +inline constexpr std::array RelationalV1Projection{ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122}; +inline constexpr std::array SubstructureV1Projection{ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124}; +static_assert(MasterFeatureNames.size() == NMasterFeatures, "master feature name count must match NMasterFeatures"); +static_assert(detail::isStrictlyIncreasingBelow(DetectorV1Projection, NMasterFeatures), "DetectorV1 projection indices must be strictly increasing and below NMasterFeatures"); +static_assert(detail::isStrictlyIncreasingBelow(RelationalV1Projection, NMasterFeatures), "RelationalV1 projection indices must be strictly increasing and below NMasterFeatures"); +static_assert(detail::isStrictlyIncreasingBelow(SubstructureV1Projection, NMasterFeatures), "SubstructureV1 projection indices must be strictly increasing and below NMasterFeatures"); +inline constexpr std::string_view FeatureContractSha256 = "39f38ece001581d8ebf57392fad045759a63ba49f57c411b9b566d7c1cc58b8a"; + +// The kaon mass is the O2 constant (exactly 0.493677). The pion mass keeps the +// frozen feature-contract literal; O2 MassPionCharged differs (0.1395704). +inline constexpr double K1ChargedKaonMassGeV = o2::constants::physics::MassKaonCharged; +inline constexpr double ChargedPionMassGeV = 0.13957039; // o2-linter: disable=pdg/explicit-mass (frozen feature contract value; O2 MassPionCharged is 0.1395704) + +inline FeaturePack buildMasterFeatures(CandidateSnapshot const& candidate) +{ + FeaturePack pack; + for (auto const& track : candidate.tracks) { + if ((track.charge != 1 && track.charge != -1) || !hasFiniteMomentum(track)) { + pack.status = BuildStatus::InvalidMomentum; + return pack; + } + } + if (candidate.tracks[0].sourceTrackId == candidate.tracks[1].sourceTrackId || + candidate.tracks[0].sourceTrackId == candidate.tracks[2].sourceTrackId || + candidate.tracks[1].sourceTrackId == candidate.tracks[2].sourceTrackId || + candidate.tracks[0].charge != candidate.tracks[1].charge || + candidate.tracks[0].charge == candidate.tracks[2].charge) { + pack.status = BuildStatus::InvalidChargePattern; + return pack; + } + + std::array kin{}; + for (std::size_t i = 0; i < NCandidateTracks; ++i) { + if (!detail::getKinematics(candidate.tracks[i], i == 0 ? K1ChargedKaonMassGeV : ChargedPionMassGeV, kin[i])) { + pack.status = BuildStatus::InvalidKinematics; + return pack; + } + } + const double sumPt = kin[0].pt + kin[1].pt + kin[2].pt; + if (!std::isfinite(sumPt) || sumPt <= 0.) { + pack.status = BuildStatus::InvalidKinematics; + return pack; + } + const double totalPx = static_cast(candidate.tracks[0].px) + candidate.tracks[1].px + candidate.tracks[2].px; + const double totalPy = static_cast(candidate.tracks[0].py) + candidate.tracks[1].py + candidate.tracks[2].py; + const double totalPz = static_cast(candidate.tracks[0].pz) + candidate.tracks[1].pz + candidate.tracks[2].pz; + const double candidatePt = std::hypot(totalPx, totalPy); + if (!std::isfinite(candidatePt)) { + pack.status = BuildStatus::InvalidKinematics; + return pack; + } + pack.kinematics.scalarSumPt = static_cast(sumPt); + pack.kinematics.candidatePt = static_cast(candidatePt); + pack.kinematics.candidateEta = candidatePt > 0. ? static_cast(std::asinh(totalPz / candidatePt)) : 0.f; + pack.kinematics.candidatePhi = static_cast(std::atan2(totalPy, totalPx)); + pack.kinematics.massKPiPi = detail::invariantMass(kin, std::array{0, 1, 2}); + pack.kinematics.massPiPi = detail::invariantMass(kin[1], kin[2]); + pack.kinematics.massKaonPiSame = detail::invariantMass(kin[0], kin[1]); + pack.kinematics.massKaonPiOpp = detail::invariantMass(kin[0], kin[2]); + + std::size_t index = 0; + for (std::size_t i = 0; i < NCandidateTracks; ++i) { + const auto& track = candidate.tracks[i]; + for (const float& decoded : track.tpcNSigma) { + detail::append(pack.master, index, detail::encodePID(decoded)); + } + for (const float& decoded : track.tofNSigma) { + detail::append(pack.master, index, track.hasTOF ? detail::encodePID(decoded) : detail::EncodedValue{0.f, 0.f, 0.f}); + } + detail::append(pack.master, index, detail::encodeDCA(track.dcaXY)); + detail::append(pack.master, index, detail::encodeDCA(track.dcaZ)); + pack.master[index++] = track.passedPtDependentDCAxy ? 1.f : 0.f; + pack.master[index++] = track.passedPtDependentDCAz ? 1.f : 0.f; + pack.master[index++] = track.hasTOF ? 1.f : 0.f; + pack.master[index++] = static_cast(track.tpcNClsCrossedRows); + for (const bool& hit : track.itsHit) { + pack.master[index++] = hit ? 1.f : 0.f; + } + pack.master[index++] = track.isPVContributor ? 1.f : 0.f; + pack.master[index++] = static_cast(kin[i].pt / sumPt); + } + + constexpr std::array, 3> PairIndices{{{{0, 1}}, {{0, 2}}, {{1, 2}}}}; + for (auto const& pair : PairIndices) { + const auto i = pair[0], j = pair[1]; + const double dphi = kin[i].phi - kin[j].phi; + const double zDenominator = kin[i].pt + kin[j].pt; + if (!std::isfinite(dphi) || zDenominator <= 0.) { + pack.status = BuildStatus::InvalidKinematics; + return pack; + } + pack.master[index++] = static_cast(kin[i].eta - kin[j].eta); + pack.master[index++] = static_cast(std::sin(dphi)); + pack.master[index++] = static_cast(std::cos(dphi)); + pack.master[index++] = static_cast(std::min(kin[i].pt, kin[j].pt) / zDenominator); + } + pack.master[index++] = pack.kinematics.massPiPi; + pack.master[index++] = pack.kinematics.massKaonPiOpp; + const bool allFinite = std::all_of(pack.master.begin(), pack.master.end(), [](float x) { return std::isfinite(x); }); + if (index != pack.master.size() || !allFinite) { + pack.status = BuildStatus::InvalidContract; + return pack; + } + pack.status = BuildStatus::Ok; + return pack; +} + +inline std::vector projectFeatures(FeaturePack const& pack, Profile profile) +{ + if (pack.status != BuildStatus::Ok) { + return {}; + } + std::vector projected; + switch (profile) { + case Profile::DetectorV1: + projected.reserve(DetectorV1Projection.size()); + for (const auto& i : DetectorV1Projection) { + projected.push_back(pack.master[i]); + } + break; + case Profile::RelationalV1: + projected.reserve(RelationalV1Projection.size()); + for (const auto& i : RelationalV1Projection) { + projected.push_back(pack.master[i]); + } + break; + case Profile::SubstructureV1: + projected.reserve(SubstructureV1Projection.size()); + for (const auto& i : SubstructureV1Projection) { + projected.push_back(pack.master[i]); + } + break; + default: + return {}; + } + return projected; +} +} // namespace o2::analysis::k1ml + +#endif // PWGLF_CORE_K1MLFEATURES_H_ diff --git a/PWGLF/DataModel/LFK1MlTables.h b/PWGLF/DataModel/LFK1MlTables.h new file mode 100644 index 00000000000..44708c47dbf --- /dev/null +++ b/PWGLF/DataModel/LFK1MlTables.h @@ -0,0 +1,125 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License version 3, copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file LFK1MlTables.h +/// \brief Derived K1 microtrack training and audit tables +/// \author Bong-Hwi Lim +/// +#ifndef PWGLF_DATAMODEL_LFK1MLTABLES_H_ +#define PWGLF_DATAMODEL_LFK1MLTABLES_H_ + +#include +#include + +#include + +namespace o2::aod +{ +namespace k1ml +{ +// Persisted relations target only these derived tables, so AO2D merge tools +// can relocate them. Source AO2D row numbers remain scalar audit values. +DECLARE_SOA_COLUMN(K1RecoCollisionId, k1RecoCollisionId, int64_t); //! row index of the reduced ResoCollisions_001 collision in the input DF +DECLARE_SOA_COLUMN(K1PosZ, k1PosZ, float); +DECLARE_SOA_COLUMN(K1BField, k1BField, float); +DECLARE_SOA_COLUMN(K1Centrality, k1Centrality, float); +DECLARE_SOA_COLUMN(K1Multiplicity, k1Multiplicity, float); +DECLARE_SOA_COLUMN(K1RecINELgt0, k1RecINELgt0, bool); +DECLARE_SOA_COLUMN(K1SourceTrackId, k1SourceTrackId, int64_t); //! trackId of the reduced micro track +DECLARE_SOA_COLUMN(K1Px, k1Px, float); +DECLARE_SOA_COLUMN(K1Py, k1Py, float); +DECLARE_SOA_COLUMN(K1Pz, k1Pz, float); +DECLARE_SOA_COLUMN(K1PidPi, k1PidPi, uint8_t); +DECLARE_SOA_COLUMN(K1PidKa, k1PidKa, uint8_t); +DECLARE_SOA_COLUMN(K1PidPr, k1PidPr, uint8_t); +DECLARE_SOA_COLUMN(K1SelectionFlags, k1SelectionFlags, uint8_t); +DECLARE_SOA_COLUMN(K1TrackFlags, k1TrackFlags, uint8_t); +DECLARE_SOA_COLUMN(K1CrossedRows, k1CrossedRows, uint8_t); +DECLARE_SOA_COLUMN(K1ItsClusterMap, k1ItsClusterMap, uint8_t); +DECLARE_SOA_COLUMN(K1Mass, k1Mass, float); +DECLARE_SOA_COLUMN(K1MassPiPi, k1MassPiPi, float); +DECLARE_SOA_COLUMN(K1MassKaPiSame, k1MassKaPiSame, float); +DECLARE_SOA_COLUMN(K1MassKaPiOpp, k1MassKaPiOpp, float); +DECLARE_SOA_COLUMN(K1ScalarSumPt, k1ScalarSumPt, float); +DECLARE_SOA_COLUMN(K1PiPiPt, k1PiPiPt, float); +DECLARE_SOA_COLUMN(K1Pt, k1Pt, float); +DECLARE_SOA_COLUMN(K1Y, k1Y, float); +DECLARE_SOA_COLUMN(K1Eta, k1Eta, float); +DECLARE_SOA_COLUMN(K1Phi, k1Phi, float); +DECLARE_SOA_COLUMN(K1Charge, k1Charge, int8_t); +// Cumulative selection bits of the unlike-sign candidate: +// 1 = valid canonical candidate inside the K1 rapidity window (loose stage), +// 2 = track quality of all three tracks, +// 4 = TOF requirement and PID of all three tracks, +// 8 = pion-pair pT and secondary mass window, +// 16 = candidate cuts. +// Candidates written at the "selected" export stage carry 31. +DECLARE_SOA_COLUMN(K1BaselinePassBits, k1BaselinePassBits, uint16_t); +DECLARE_SOA_COLUMN(K1MasterFeatures, k1MasterFeatures, float[125]); //! 125 master features of the K1 ML feature contract (PWGLF/Core/K1MlFeatures.h) +DECLARE_SOA_COLUMN(K1FeatureStatus, k1FeatureStatus, uint8_t); //! o2::analysis::k1ml::BuildStatus; only Ok (0) rows are written +DECLARE_SOA_COLUMN(K1TruthStatus, k1TruthStatus, uint8_t); //! 0 data, 1 matched, 2 unmatched +DECLARE_SOA_COLUMN(K1TruthChannel, k1TruthChannel, uint8_t); //! 0 none, 1 rho K, 2 K* pi +DECLARE_SOA_COLUMN(K1MotherPdg, k1MotherPdg, int32_t); +DECLARE_SOA_COLUMN(K1MotherId, k1MotherId, int64_t); +DECLARE_SOA_COLUMN(K1GeneratedPt, k1GeneratedPt, float); //! NaN in K1MlTruth: not filled for reconstructed candidates +DECLARE_SOA_COLUMN(K1GeneratedY, k1GeneratedY, float); //! NaN in K1MlTruth: not filled for reconstructed candidates +DECLARE_SOA_COLUMN(K1OriginalMcParticleId, k1OriginalMcParticleId, int64_t); +DECLARE_SOA_COLUMN(K1DaughterPdg1, k1DaughterPdg1, int32_t); +DECLARE_SOA_COLUMN(K1DaughterPdg2, k1DaughterPdg2, int32_t); +DECLARE_SOA_COLUMN(K1GenSelectedRecoEvent, k1GenSelectedRecoEvent, bool); //! true: parents are taken from selected reconstructed events +} // namespace k1ml + +DECLARE_SOA_TABLE(K1MlEvents, "AOD", "K1MLEVENT", + o2::soa::Index<>, k1ml::K1RecoCollisionId, k1ml::K1PosZ, k1ml::K1BField, + k1ml::K1Centrality, k1ml::K1Multiplicity, k1ml::K1RecINELgt0); +namespace k1ml +{ +DECLARE_SOA_INDEX_COLUMN_FULL(K1MlEvent, k1MlEvent, int, K1MlEvents, ""); +} // namespace k1ml +DECLARE_SOA_TABLE(K1MlTracks, "AOD", "K1MLTRACK", + o2::soa::Index<>, k1ml::K1MlEventId, k1ml::K1SourceTrackId, + k1ml::K1Px, k1ml::K1Py, k1ml::K1Pz, + k1ml::K1PidPi, k1ml::K1PidKa, k1ml::K1PidPr, + k1ml::K1SelectionFlags, k1ml::K1TrackFlags, + k1ml::K1CrossedRows, k1ml::K1ItsClusterMap); +namespace k1ml +{ +DECLARE_SOA_INDEX_COLUMN_FULL(K1MlKaonTrack, k1MlKaonTrack, int, K1MlTracks, "_Kaon"); +DECLARE_SOA_INDEX_COLUMN_FULL(K1MlSamePionTrack, k1MlSamePionTrack, int, K1MlTracks, "_Same"); +DECLARE_SOA_INDEX_COLUMN_FULL(K1MlOppPionTrack, k1MlOppPionTrack, int, K1MlTracks, "_Opp"); +} // namespace k1ml +DECLARE_SOA_TABLE(K1MlCandidates, "AOD", "K1MLCANDIDATE", + o2::soa::Index<>, k1ml::K1MlEventId, + k1ml::K1MlKaonTrackId, k1ml::K1MlSamePionTrackId, k1ml::K1MlOppPionTrackId, + k1ml::K1Mass, k1ml::K1MassPiPi, k1ml::K1MassKaPiSame, k1ml::K1MassKaPiOpp, + k1ml::K1ScalarSumPt, k1ml::K1PiPiPt, + k1ml::K1Pt, k1ml::K1Y, k1ml::K1Eta, k1ml::K1Phi, k1ml::K1Charge, + k1ml::K1BaselinePassBits); +namespace k1ml +{ +DECLARE_SOA_INDEX_COLUMN_FULL(K1MlCandidate, k1MlCandidate, int, K1MlCandidates, ""); +} // namespace k1ml +DECLARE_SOA_TABLE(K1MlInputs, "AOD", "K1MLINPUT", + o2::soa::Index<>, k1ml::K1MlCandidateId, + k1ml::K1MasterFeatures, k1ml::K1FeatureStatus); +DECLARE_SOA_TABLE(K1MlTruth, "AOD", "K1MLTRUTH", + o2::soa::Index<>, k1ml::K1MlCandidateId, + k1ml::K1TruthStatus, k1ml::K1TruthChannel, + k1ml::K1MotherPdg, k1ml::K1MotherId, + k1ml::K1GeneratedPt, k1ml::K1GeneratedY); +DECLARE_SOA_TABLE(K1MlGenAudit, "AOD", "K1MLGENAUDIT", + o2::soa::Index<>, k1ml::K1RecoCollisionId, k1ml::K1OriginalMcParticleId, + k1ml::K1MotherPdg, k1ml::K1DaughterPdg1, k1ml::K1DaughterPdg2, + k1ml::K1TruthChannel, k1ml::K1GeneratedPt, k1ml::K1GeneratedY, + k1ml::K1GenSelectedRecoEvent); +} // namespace o2::aod + +#endif // PWGLF_DATAMODEL_LFK1MLTABLES_H_ diff --git a/PWGLF/Tasks/Resonances/CMakeLists.txt b/PWGLF/Tasks/Resonances/CMakeLists.txt index f8a6669861e..b1f09db8d9a 100644 --- a/PWGLF/Tasks/Resonances/CMakeLists.txt +++ b/PWGLF/Tasks/Resonances/CMakeLists.txt @@ -79,6 +79,11 @@ o2physics_add_dpl_workflow(k1analysismicro PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore COMPONENT_NAME Analysis) +o2physics_add_dpl_workflow(k1-training-table + SOURCES k1TrainingTable.cxx + PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore + COMPONENT_NAME Analysis) + o2physics_add_dpl_workflow(phianalysisrun3 SOURCES phianalysisrun3.cxx PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore diff --git a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx new file mode 100644 index 00000000000..9fa1c2834f2 --- /dev/null +++ b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx @@ -0,0 +1,274 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License version 3, copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file k1TrainingTable.cxx +/// \brief Derived K1 training table workflow +/// \author Bong-Hwi Lim +/// + +#include "PWGLF/Core/K1AnalysisMicroCore.h" +#include "PWGLF/Core/K1MlFeatures.h" +#include "PWGLF/DataModel/LFK1MlTables.h" +#include "PWGLF/DataModel/LFResonanceTables.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace o2; +using namespace o2::framework; +using namespace o2::constants::physics; +using namespace o2::analysis::k1micro; + +static_assert(std::extent_v == o2::analysis::k1ml::NMasterFeatures, + "K1MasterFeatures column size must match the K1 ML feature contract"); + +/// Writes the unlike-sign K1 micro candidates of the shared K1 selection, with the +/// canonical (kaon, same-sign pion, opposite-sign pion) tracks and the master features. +struct K1TrainingTable { + using ResoCollisions = aod::ResoCollisions_001; + using ResoMCCols = soa::Join; + using ResoMicroTracks = aod::ResoMicroTracks_001; + using ResoMCMicroTracks = soa::Join; + using ResoMCParents = aod::ResoMCParents_001; + + // FNV-1a over the bit patterns of the master features, for parity logs + static constexpr uint64_t FnvOffsetBasis = 14695981039346656037ULL; + static constexpr uint64_t FnvPrime = 1099511628211ULL; + static constexpr unsigned int BitsPerByte = 8; + static constexpr unsigned int BitsPerFloat = 32; + static constexpr uint32_t ByteMask = 0xffU; + + Produces k1MlEvents; + Produces k1MlTracks; + Produces k1MlCandidates; + Produces k1MlInputs; + Produces k1MlTruth; + Produces k1MlGenAudit; + + HistogramRegistry histos{"histos", {}, OutputObjHandlingPolicy::AnalysisObject}; + + // Selection shared with the K1 histogram task (plain JSON keys, no group prefix) + EventCuts eventCuts; + TrackCuts trackCuts; + PionPidCuts pionPID; + KaonPidCuts kaonPID; + SecondaryCuts secondaryCuts; + CandidateCuts candidateCuts; + HistogramOptions histogramOptions; + + Configurable k1MlExportStage{"k1MlExportStage", "loose", "Candidate export stage: loose (pass bits >= 1) or selected (pass bits = 31)"}; + Configurable k1MlLooseAudit{"k1MlLooseAudit", true, "Record loose US cutflow and mass/pT/activity spectrum"}; + Configurable k1MlParityRows{"k1MlParityRows", 0, "Log feature hashes of the first N exported candidates"}; + + K1AnalysisMicroCore core; + int64_t k1MlEventRow = -1; + std::unordered_map k1MlTrackRows; + int k1MlParityLogged = 0; + + void init(InitContext&) + { + if (static_cast(doprocessResoMicroTracks) + static_cast(doprocessMCMicro) != 1 || + (doprocessMCTrue && !doprocessMCMicro)) { + LOG(fatal) << "K1 training table requires exactly one of processResoMicroTracks and processMCMicro; processMCTrue requires processMCMicro"; + } + if (k1MlParityRows < 0) { + LOG(fatal) << "k1MlParityRows must not be negative"; + } + if (k1MlExportStage.value != "loose" && k1MlExportStage.value != "selected") { + LOG(fatal) << "k1MlExportStage must be loose or selected"; + } + + ProcessModes modes; + modes.microTracks = true; + modes.mcReco = doprocessMCMicro; + modes.mcRecoMicro = doprocessMCMicro; + modes.mcGen = doprocessMCTrue; + LooseStageOptions looseOptions; + looseOptions.audit = k1MlLooseAudit; + looseOptions.exportSelected = k1MlExportStage.value == "selected"; + core.init(histos, eventCuts, trackCuts, pionPID, kaonPID, secondaryCuts, candidateCuts, histogramOptions, modes, looseOptions); + + // Candidates that violate the canonical or feature contract are skipped, never written. + auto skipped = histos.add("ML/exportSkipped", "Skipped candidates;K1 ML build status;candidates", HistType::kTH1D, {{6, -0.5, 5.5}}); + const std::array statusLabels{"Ok", "InvalidChargePattern", "ReusedTrack", "InvalidMomentum", "InvalidKinematics", "InvalidContract"}; + for (std::size_t i = 0; i < statusLabels.size(); ++i) { + skipped->GetXaxis()->SetBinLabel(i + 1, statusLabels[i]); + } + + LOG(info) << "Size of the histograms in K1 training table task"; + histos.print(); + } + + template + int64_t writeK1MlTrack(Track const& track) + { + const auto id = static_cast(track.globalIndex()); + if (auto it = k1MlTrackRows.find(id); it != k1MlTrackRows.end()) { + return it->second; + } + k1MlTracks(k1MlEventRow, static_cast(track.trackId()), track.px(), track.py(), track.pz(), + track.pidNSigmaPiFlag(), track.pidNSigmaKaFlag(), track.pidNSigmaPrFlag(), + track.trackSelectionFlags(), track.trackFlags(), track.tpcNClsCrossedRows(), track.itsClusterMap()); + const auto row = static_cast(k1MlTracks.lastIndex()); + k1MlTrackRows.emplace(id, row); + return row; + } + + template + void writeK1MlEvent(Collision const& collision) + { + k1MlTrackRows.clear(); + // ResoCollisions_001 carries no run number or BC; the reduced collision row identifies the event within its DF. + k1MlEvents(static_cast(collision.globalIndex()), + collision.posZ(), collision.bMagField(), collision.cent(), collision.multiplicity(), collision.isRecINELgt0()); + k1MlEventRow = static_cast(k1MlEvents.lastIndex()); + } + + template + void logParity(Collision const& collision, o2::analysis::k1ml::CandidateSnapshot const& candidate, o2::analysis::k1ml::FeaturePack const& pack) + { + if (k1MlParityLogged >= k1MlParityRows) { + return; + } + uint64_t hash = FnvOffsetBasis; + for (const auto& value : pack.master) { + const uint32_t bits = std::bit_cast(value); + for (unsigned int shift = 0; shift < BitsPerFloat; shift += BitsPerByte) { + hash = (hash ^ ((bits >> shift) & ByteMask)) * FnvPrime; + } + } + LOGP(info, "K1MLPARITY collision={} tracks={},{},{} featureHash={}", + collision.globalIndex(), candidate.tracks[0].sourceTrackId, candidate.tracks[1].sourceTrackId, + candidate.tracks[2].sourceTrackId, hash); + ++k1MlParityLogged; + } + + template + void writeK1MlCandidate(Collision const& collision, Kaon const& kaon, Pion const& samePion, Pion const& oppPion, + K1TruthChannel channel, uint16_t passBits) + { + using o2::analysis::k1ml::BuildStatus; + const auto canonical = o2::analysis::k1ml::canonicalizeUS(o2::analysis::k1ml::makeTrackSnapshot(kaon), o2::analysis::k1ml::makeTrackSnapshot(samePion), o2::analysis::k1ml::makeTrackSnapshot(oppPion)); + if (canonical.status != BuildStatus::Ok) { + histos.fill(HIST("ML/exportSkipped"), static_cast(canonical.status)); + return; + } + const auto pack = o2::analysis::k1ml::buildMasterFeatures(canonical.candidate); + if (pack.status != BuildStatus::Ok) { + histos.fill(HIST("ML/exportSkipped"), static_cast(pack.status)); + return; + } + logParity(collision, canonical.candidate, pack); + const auto kaonRow = writeK1MlTrack(kaon); + const auto sameRow = writeK1MlTrack(samePion); + const auto oppRow = writeK1MlTrack(oppPion); + ROOT::Math::PxPyPzMVector k{kaon.px(), kaon.py(), kaon.pz(), MassKaonCharged}; + ROOT::Math::PxPyPzMVector s{samePion.px(), samePion.py(), samePion.pz(), MassPionCharged}; + ROOT::Math::PxPyPzMVector o{oppPion.px(), oppPion.py(), oppPion.pz(), MassPionCharged}; + const auto mother = k + s + o; + k1MlCandidates(k1MlEventRow, kaonRow, sameRow, oppRow, + static_cast(mother.M()), static_cast((s + o).M()), + static_cast((k + s).M()), static_cast((k + o).M()), + pack.kinematics.scalarSumPt, static_cast((s + o).Pt()), + static_cast(mother.Pt()), static_cast(mother.Rapidity()), + static_cast(mother.Eta()), static_cast(mother.Phi()), + static_cast(kaon.sign()), passBits); + const auto row = static_cast(k1MlCandidates.lastIndex()); + k1MlInputs(row, pack.master.data(), static_cast(pack.status)); + if constexpr (IsMC) { + const bool matched = channel != K1TruthChannel::None; + int motherId = -1; + if (matched) { + motherId = channel == K1TruthChannel::RhoK ? kaon.motherId() : std::abs(samePion.motherPDG()) == Pdg::kK1_1270Plus ? samePion.motherId() + : oppPion.motherId(); + } + // Immediate-mother information alone does not prove a physical UID. + k1MlTruth(row, matched ? uint8_t{1} : uint8_t{2}, static_cast(channel), + matched ? kaon.sign() * Pdg::kK1_1270Plus : 0, static_cast(motherId), + std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); + } else { + k1MlTruth(row, uint8_t{0}, uint8_t{0}, 0, int64_t{-1}, + std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); + } + } + + void processResoMicroTracks(ResoCollisions::iterator const& collision, ResoMicroTracks const& tracks) + { + if (!core.passesEventCuts(collision)) { + return; + } + writeK1MlEvent(collision); + core.fillHistograms(histos, collision, tracks, tracks, + [this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion, + K1TruthChannel channel, uint16_t passBits) { + writeK1MlCandidate(coll, kaon, samePion, oppPion, channel, passBits); + }); + } + PROCESS_SWITCH(K1TrainingTable, processResoMicroTracks, "Write K1 candidates from data micro v001 tables", true); + + void processMCMicro(ResoMCCols::iterator const& collision, ResoMCMicroTracks const& tracks) + { + // The modular producer already selected these reconstructed collisions. + if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { + return; + } + histos.fill(HIST("MCReco/collisions"), 0.5); + histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size()); + writeK1MlEvent(collision); + core.fillHistograms(histos, collision, tracks, tracks, + [this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion, + K1TruthChannel channel, uint16_t passBits) { + writeK1MlCandidate(coll, kaon, samePion, oppPion, channel, passBits); + }); + } + PROCESS_SWITCH(K1TrainingTable, processMCMicro, "Write K1 candidates with truth from reconstructed MC micro v001 tables", false); + + void processMCTrue(ResoMCCols::iterator const& collision, ResoMCParents const& resoParents) + { + if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { + return; + } + core.fillGenerated(histos, resoParents, [&](auto const& part, K1TruthChannel channel) { + k1MlGenAudit(static_cast(collision.globalIndex()), static_cast(part.originalMcParticleId()), + part.pdgCode(), part.daughterPDG1(), part.daughterPDG2(), static_cast(channel), + part.pt(), part.y(), true); + }); + } + PROCESS_SWITCH(K1TrainingTable, processMCTrue, "Write generated K1 parents of selected reconstructed MC events", false); +}; + +WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) +{ + return WorkflowSpec{adaptAnalysisTask(cfgc)}; +} From 0f084983b1bb02186822cbde5049b581407bc527 Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Sun, 4 Oct 2026 10:57:09 +0900 Subject: [PATCH 5/9] [PWGLF] Use the O2 charged pion mass constant in the K1 ML feature builder The K1 ML feature contract SHA covers the 125 feature names and the three projection index lists only, so switching the pion mass from the local literal to o2::constants::physics::MassPionCharged does not change the contract identity. Feature values change at the 1e-8 relative level for the pair masses; exports written with the previous literal are therefore not byte-identical to new exports. --- PWGLF/Core/K1MlFeatures.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/PWGLF/Core/K1MlFeatures.h b/PWGLF/Core/K1MlFeatures.h index 6ebfc742a62..2ca0b7c3770 100644 --- a/PWGLF/Core/K1MlFeatures.h +++ b/PWGLF/Core/K1MlFeatures.h @@ -744,11 +744,6 @@ static_assert(detail::isStrictlyIncreasingBelow(RelationalV1Projection, NMasterF static_assert(detail::isStrictlyIncreasingBelow(SubstructureV1Projection, NMasterFeatures), "SubstructureV1 projection indices must be strictly increasing and below NMasterFeatures"); inline constexpr std::string_view FeatureContractSha256 = "39f38ece001581d8ebf57392fad045759a63ba49f57c411b9b566d7c1cc58b8a"; -// The kaon mass is the O2 constant (exactly 0.493677). The pion mass keeps the -// frozen feature-contract literal; O2 MassPionCharged differs (0.1395704). -inline constexpr double K1ChargedKaonMassGeV = o2::constants::physics::MassKaonCharged; -inline constexpr double ChargedPionMassGeV = 0.13957039; // o2-linter: disable=pdg/explicit-mass (frozen feature contract value; O2 MassPionCharged is 0.1395704) - inline FeaturePack buildMasterFeatures(CandidateSnapshot const& candidate) { FeaturePack pack; @@ -769,7 +764,7 @@ inline FeaturePack buildMasterFeatures(CandidateSnapshot const& candidate) std::array kin{}; for (std::size_t i = 0; i < NCandidateTracks; ++i) { - if (!detail::getKinematics(candidate.tracks[i], i == 0 ? K1ChargedKaonMassGeV : ChargedPionMassGeV, kin[i])) { + if (!detail::getKinematics(candidate.tracks[i], i == 0 ? o2::constants::physics::MassKaonCharged : o2::constants::physics::MassPionCharged, kin[i])) { pack.status = BuildStatus::InvalidKinematics; return pack; } From c5ba4c58ab3781c0a9b5fcd32a249bde3248c1b8 Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Sun, 4 Oct 2026 15:02:27 +0900 Subject: [PATCH 6/9] [PWGLF] Fix cppcheck redundantInitialization in K1 micro PID selection Initialize the TPC n-sigma once from the species instead of overwriting a NaN default in both branches. The selection is unchanged. --- PWGLF/Core/K1AnalysisMicroCore.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PWGLF/Core/K1AnalysisMicroCore.h b/PWGLF/Core/K1AnalysisMicroCore.h index e254b485add..a48805350a4 100644 --- a/PWGLF/Core/K1AnalysisMicroCore.h +++ b/PWGLF/Core/K1AnalysisMicroCore.h @@ -683,15 +683,13 @@ class K1AnalysisMicroCore return false; } const bool hasTOF = track.hasTOF(); - double tpcNSigma = std::numeric_limits::quiet_NaN(); + const double tpcNSigma = (S == Species::Pion) ? track.tpcNSigmaPi() : track.tpcNSigmaKa(); double tofNSigma = std::numeric_limits::quiet_NaN(); // TOF value is only valid with hasTOF if constexpr (S == Species::Pion) { - tpcNSigma = track.tpcNSigmaPi(); if (hasTOF) { tofNSigma = track.tofNSigmaPi(); } } else { - tpcNSigma = track.tpcNSigmaKa(); if (hasTOF) { tofNSigma = track.tofNSigmaKa(); } From f2916360da56338ba5d72d33c039657e29aebdda Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Sun, 4 Oct 2026 23:20:52 +0900 Subject: [PATCH 7/9] [PWGLF] Split the generic resonance selection out of the K1 core Move the event, track-quality, TOF-requirement and PID selection of resonance daughters into PWGLF/Core/ResoAnalysisSelectionCore.h. The K1 core keeps the K1 selection, truth classification, candidate loop and its cut-flow/ML audit histograms, and hands the selected pion pairs and candidates to the task through hooks. The K1 analysis histograms are now registered and filled in k1AnalysisMicro.cxx; the training-table task writes only the ML tables, CutFlow/*, ML/* and MCReco event counters. The selection, all configurable names and the ML table output are unchanged. --- PWGLF/Core/K1AnalysisMicroCore.h | 1045 ++++---------------- PWGLF/Core/ResoAnalysisSelectionCore.h | 462 +++++++++ PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx | 424 +++++++- PWGLF/Tasks/Resonances/k1TrainingTable.cxx | 32 +- 4 files changed, 1067 insertions(+), 896 deletions(-) create mode 100644 PWGLF/Core/ResoAnalysisSelectionCore.h diff --git a/PWGLF/Core/K1AnalysisMicroCore.h b/PWGLF/Core/K1AnalysisMicroCore.h index a48805350a4..e1ec22624e7 100644 --- a/PWGLF/Core/K1AnalysisMicroCore.h +++ b/PWGLF/Core/K1AnalysisMicroCore.h @@ -10,17 +10,19 @@ // or submit itself to any jurisdiction. /// /// \file K1AnalysisMicroCore.h -/// \brief Shared selection, truth classification and candidate loop of the K1(1270) resonance tasks +/// \brief Shared K1(1270) selection, truth classification and candidate enumeration of the K1 resonance tasks /// \author Su-Jeong Ji , Bong-Hwi Lim /// +/// The core owns the selection and its cut-flow instrumentation (CutFlow/*, ML/*). The tasks own their +/// output histograms and fill them from the pair and candidate hooks of forEachCandidate(). #ifndef PWGLF_CORE_K1ANALYSISMICROCORE_H_ #define PWGLF_CORE_K1ANALYSISMICROCORE_H_ #include "PWGLF/Core/K1MlFeatures.h" +#include "PWGLF/Core/ResoAnalysisSelectionCore.h" #include "PWGLF/DataModel/LFResonanceTables.h" -#include #include #include #include @@ -34,7 +36,6 @@ #include #include -#include #include #include #include @@ -42,67 +43,34 @@ #include #include #include +#include #include namespace o2::analysis::k1micro { +using o2::analysis::resonance::EventCuts; +using o2::analysis::resonance::isCutEnabled; +using o2::analysis::resonance::isInRange; +using o2::analysis::resonance::isInWindow; +using o2::analysis::resonance::PIDCutConfig; +using o2::analysis::resonance::ResoAnalysisSelectionCore; +using o2::analysis::resonance::TrackCuts; +using o2::analysis::resonance::TrackStage; + enum class K1TruthChannel { None = 0, RhoK = 1, KStarPi = 2 }; -enum BinAnti : unsigned int { - kNormal = 0, - kAnti, - kNAEnd -}; - -enum BinType : unsigned int { - kK1P = 0, - kK1N, - kK1P_Mix, - kK1N_Mix, - kK1P_GenINEL10, - kK1N_GenINEL10, - kK1P_GenINELgt10, - kK1N_GenINELgt10, - kK1P_GenTrig10, - kK1N_GenTrig10, - kK1P_GenEvtSel, - kK1N_GenEvtSel, - kK1P_Rec, - kK1N_Rec, - kTYEnd -}; - +// The value is the species index of the PID configuration in ResoAnalysisSelectionCore. enum class Species : int { Pion = 0, Kaon = 1 }; -// Last stage passed by a track; the cut-flow histogram is filled directly from this value. -enum TrackStage : int { - kTrkInput = 0, - kTrkPt, - kTrkEta, - kTrkDCAxy, - kTrkDCAz, - kTrkFlags, - kTrkClusters, - kTrkTOFRequired, - kTrkPID, - kTrkNStages -}; - -enum class QAFolder { - Before, // QA/*: before the candidate cuts - After, // QAcut/*: after the candidate cuts - MC // QAMC/*: matched K1 truth candidates -}; - -// Cumulative selection bits of an unlike-sign candidate handed to the candidate callback. +// Cumulative selection bits of an unlike-sign candidate handed to the export hook. enum CandidatePassBit : uint16_t { kPassLoose = 1, // valid canonical candidate inside the K1 rapidity window kPassQuality = 2, // track quality of all three tracks @@ -112,64 +80,12 @@ enum CandidatePassBit : uint16_t { }; inline constexpr uint16_t PassBitsSelected = kPassLoose | kPassQuality | kPassPID | kPassPair | kPassCandidate; -// Resolved PID cut of one species at a given pT. -struct PIDCut { - double tpcMax = 0.; - double tofMax = 0.; - double combined = 0.; - bool tofRequired = false; -}; - -inline constexpr float DisabledCut = -999.f; // an optional cut with this value is off and not evaluated inline constexpr double MassRho770 = 0.77526; // PDG 2024, not available in o2::constants::physics -inline constexpr double DCAGridStep = 0.025; // v001 micro DCA encoding, lower-inclusive bins up to DCAGridMax -inline constexpr double DCAGridMax = 0.15; -inline constexpr double PIDGridStart = 2.0; // v001 micro nSigma encoding: 0.25 bins in [2.0, 3.5] -inline constexpr double PIDGridStep = 0.25; -inline constexpr double PIDGridMax = 3.5; -inline constexpr double GridTolerance = 1e-4; -inline constexpr std::size_t MinPtBinEdges = 2; // a pT dependent PID table needs at least one bin -inline constexpr float ProducerDCAPtP0 = 0.004f; // resonanceModuleInitializer cfgTightDCAOffset default -inline constexpr float ProducerDCAPtCoeff = 0.013f; // resonanceModuleInitializer cfgTightDCAPtCoefficient default -inline constexpr float ProducerDCAPtPower = 1.f; // resonanceModuleInitializer cfgTightDCAPtPower default -inline constexpr float ConfigTolerance = 1e-6f; inline constexpr int NCandidateStages = 12; inline constexpr int NTruthChannels = 3; // Configurable groups without prefix: the JSON keys are the plain configurable names. -/// Event selection -struct EventCuts : o2::framework::ConfigurableGroup { - o2::framework::Configurable cRecoINELgt0{"cRecoINELgt0", false, "Apply reconstructed INEL>0 selection"}; - o2::framework::Configurable cMCINELgt0{"cMCINELgt0", false, "Require generator INEL>0 in MC processes"}; - o2::framework::Configurable cMCVtxIn10{"cMCVtxIn10", false, "Require generator |vz| < 10 cm in MC processes"}; -}; - -/// Track selections (common for pion and kaon, -999 switches an optional cut off) -struct TrackCuts : o2::framework::ConfigurableGroup { - o2::framework::Configurable cMinPtcut{"cMinPtcut", 0.15, "Track minium pt cut"}; - o2::framework::Configurable cMaxEtacut{"cMaxEtacut", -999.f, "Track maximum |eta| cut (-999: off)"}; - // DCAr to PV - o2::framework::Configurable cMaxDCArToPVcut{"cMaxDCArToPVcut", 0.1, "Track DCAr cut to PV Maximum"}; - // DCAz to PV - o2::framework::Configurable cMaxDCAzToPVcut{"cMaxDCAzToPVcut", 0.1, "Track DCAz cut to PV Maximum"}; - o2::framework::Configurable cMinDCAzToPVcut{"cMinDCAzToPVcut", 0.0, "Track DCAz cut to PV Minimum"}; - o2::framework::Configurable cfgUsePtDepDCA{"cfgUsePtDepDCA", false, "Use pT dependent DCA cut instead of the fixed maximum"}; - o2::framework::Configurable cDCAToPVByPtP0{"cDCAToPVByPtP0", 0.004f, "pT dependent DCA cut = P0 + coefficient / pT^power (cm)"}; - o2::framework::Configurable cDCAToPVByPtCoeff{"cDCAToPVByPtCoeff", 0.013f, "Coefficient in the pT dependent DCA cut"}; - o2::framework::Configurable cDCAToPVByPtPower{"cDCAToPVByPtPower", 1.f, "Power in the pT dependent DCA cut"}; - o2::framework::Configurable cfgPrimaryTrack{"cfgPrimaryTrack", true, "Primary track selection"}; // kGoldenChi2 | kDCAxy | kDCAz - o2::framework::Configurable cfgGlobalWoDCATrack{"cfgGlobalWoDCATrack", true, "Global track selection without DCA"}; // kQualityTracks (kTrackType | kTPCNCls | kTPCCrossedRows | kTPCCrossedRowsOverNCls | kTPCChi2NDF | kTPCRefit | kITSNCls | kITSChi2NDF | kITSRefit | kITSHits) | kInAcceptanceTracks (kPtRange | kEtaRange) - o2::framework::Configurable cfgGlobalTrack{"cfgGlobalTrack", false, "Global track selection"}; // kGoldenChi2 | kDCAxy | kDCAz - o2::framework::Configurable cfgPVContributor{"cfgPVContributor", false, "PV contributor track selection"}; // PV Contriuibutor - o2::framework::Configurable cfgUseTPCRefit{"cfgUseTPCRefit", false, "Require TPC Refit"}; - o2::framework::Configurable cfgUseITSRefit{"cfgUseITSRefit", false, "Require ITS Refit"}; - o2::framework::Configurable cfgTPCcluster{"cfgTPCcluster", 0, "Number of TPC cluster (found clusters, ResoTracks only)"}; - o2::framework::Configurable cfgTPCCrossedRowsMin{"cfgTPCCrossedRowsMin", 0, "Minimum number of TPC crossed rows"}; - o2::framework::Configurable cfgITSNClsMin{"cfgITSNClsMin", 0, "Minimum number of ITS clusters (ResoMicroTracks only)"}; - o2::framework::Configurable cfgHasTOF{"cfgHasTOF", false, "Require TOF"}; -}; - /// Pion PID selection struct PionPidCuts : o2::framework::ConfigurableGroup { o2::framework::Configurable cMaxTPCnSigmaPion{"cMaxTPCnSigmaPion", 3.0, "TPC nSigma cut for Pion (-999: off)"}; // TPC @@ -218,13 +134,6 @@ struct CandidateCuts : o2::framework::ConfigurableGroup { o2::framework::Configurable cK1MinRap{"cK1MinRap", -0.5, "K1 minimum rapidity"}; }; -/// Histogram binning, QA and debug output -struct HistogramOptions : o2::framework::ConfigurableGroup { - o2::framework::Configurable cNbinsDiv{"cNbinsDiv", 1, "Integer to divide the number of bins"}; - o2::framework::Configurable additionalQAplots{"additionalQAplots", true, "Additional QA plots"}; - o2::framework::Configurable cfgTruthDebug{"cfgTruthDebug", 0, "Maximum logged matched candidates per truth channel"}; -}; - /// Process functions enabled in the task; they decide the configuration checks and the registered histograms. struct ProcessModes { bool microTracks = false; // any process function reading micro tracks (quantised DCA and nSigma) @@ -233,67 +142,27 @@ struct ProcessModes { bool mcGen = false; // generated K1 parents in selected reconstructed events }; -/// Loose-stage traversal of unlike-sign micro candidates for the candidate callback. -/// With the defaults and without a callback, the candidate loop applies only the conventional selection. +/// Loose-stage traversal of unlike-sign micro candidates for the export hook. +/// With the defaults and without an export hook, the candidate loop applies only the conventional selection. struct LooseStageOptions { bool audit = false; // fill ML/looseCutflow and ML/looseMassPtActivity - bool exportSelected = false; // hand candidates to the callback at the selected stage instead of the loose stage + bool exportSelected = false; // hand candidates to the export hook at the selected stage instead of the loose stage }; -// A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value). -inline bool isCutEnabled(float value) -{ - return value > DisabledCut + 1.f; -} - -// v001 micro values are lower-inclusive bin edges: a maximum cut on the grid keeps bins below it. -inline bool passesBinnedMax(double decoded, double cut) -{ - return decoded < cut - o2::constants::math::Epsilon; -} - -// Minimum cut on the grid keeps the bin starting at the cut. -inline bool passesBinnedMin(double decoded, double cut) -{ - return decoded >= cut - o2::constants::math::Epsilon; -} - -template -bool passesMax(double value, double cut) -{ - if constexpr (IsResoMicrotrack) { - return passesBinnedMax(value, cut); - } else { - return value < cut; - } -} - -inline bool isInRange(double value, double minimum, double maximum) -{ - if (isCutEnabled(minimum) && value < minimum) { - return false; - } - if (isCutEnabled(maximum) && value > maximum) { - return false; - } - return true; -} - -inline bool isInWindow(double value, double center, double width) -{ - return std::abs(value - center) < width; -} - -// Preserve pT-bin membership [low, high). -inline int getPtBinIndex(float pt, const std::vector& ptBins) -{ - for (std::size_t i = 1; i < ptBins.size(); ++i) { - if (pt >= ptBins[i - 1] && pt < ptBins[i]) { - return static_cast(i - 1); - } - } - return -1; -} +/// Selected (pion, pion, kaon) triplet handed to the candidate hook. +/// mass13, mass23, angle and pairAsym are computed only inside the K1 rapidity window, and there only +/// when a candidate cut needs them or forEachCandidate() was asked for them; otherwise they are 0. +struct K1CandidateValues { + ROOT::Math::PxPyPzMVector k1; // pion1 + pion2 + kaon + ROOT::Math::PxPyPzMVector secondary; // pion1 + pion2 + double mass13 = 0.; // pion1 + kaon (K*0 candidate in the unlike-sign case) + double mass23 = 0.; // pion2 + kaon + double angle = 0.; // opening angle between the secondary resonance and the bachelor + double pairAsym = 0.; // energy asymmetry between the secondary resonance and the bachelor + bool isUnlikeSign = false; // opposite-sign pion pair + bool inRapidity = false; // K1 rapidity window + bool passesCandidateCuts = false; // secondary mass, pi-K mass, angle and asymmetry cuts (inside the rapidity window) +}; // Truth classification from the immediate mothers and the sibling IDs of the reconstructed daughters. template @@ -362,108 +231,7 @@ inline K1TruthChannel classifyGeneratedK1(int charge, int daughter1, int daughte return K1TruthChannel::None; } -// Track QA of a pion; isPrimary selects the trkppion (first) or trkspion (second) histograms -template -void fillPionQA(o2::framework::HistogramRegistry& histos, const TrackType& track, bool isPrimary) -{ - const bool hasTOF = track.hasTOF(); - if (isPrimary) { - if constexpr (Folder == QAFolder::Before) { - histos.fill(HIST("QA/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QA/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QA/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QA/trkppionpT"), track.pt()); - histos.fill(HIST("QA/trkppionDCAxy"), track.dcaXY()); - histos.fill(HIST("QA/trkppionDCAz"), track.dcaZ()); - } else if constexpr (Folder == QAFolder::After) { - histos.fill(HIST("QAcut/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAcut/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAcut/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAcut/trkppionpT"), track.pt()); - histos.fill(HIST("QAcut/trkppionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAcut/trkppionDCAz"), track.dcaZ()); - } else { - histos.fill(HIST("QAMC/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAMC/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAMC/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAMC/trkppionpT"), track.pt()); - histos.fill(HIST("QAMC/trkppionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAMC/trkppionDCAz"), track.dcaZ()); - } - } else { - if constexpr (Folder == QAFolder::Before) { - histos.fill(HIST("QA/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QA/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QA/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QA/trkspionpT"), track.pt()); - histos.fill(HIST("QA/trkspionDCAxy"), track.dcaXY()); - histos.fill(HIST("QA/trkspionDCAz"), track.dcaZ()); - } else if constexpr (Folder == QAFolder::After) { - histos.fill(HIST("QAcut/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAcut/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAcut/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAcut/trkspionpT"), track.pt()); - histos.fill(HIST("QAcut/trkspionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAcut/trkspionDCAz"), track.dcaZ()); - } else { - histos.fill(HIST("QAMC/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); - if (hasTOF) { - histos.fill(HIST("QAMC/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); - histos.fill(HIST("QAMC/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); - } - histos.fill(HIST("QAMC/trkspionpT"), track.pt()); - histos.fill(HIST("QAMC/trkspionDCAxy"), track.dcaXY()); - histos.fill(HIST("QAMC/trkspionDCAz"), track.dcaZ()); - } - } -} - -// Track QA of the bachelor kaon -template -void fillKaonQA(o2::framework::HistogramRegistry& histos, const TrackType& track) -{ - const bool hasTOF = track.hasTOF(); - if constexpr (Folder == QAFolder::Before) { - histos.fill(HIST("QA/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); - if (hasTOF) { - histos.fill(HIST("QA/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); - histos.fill(HIST("QA/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); - } - histos.fill(HIST("QA/trkkaonpT"), track.pt()); - histos.fill(HIST("QA/trkkaonDCAxy"), track.dcaXY()); - histos.fill(HIST("QA/trkkaonDCAz"), track.dcaZ()); - } else if constexpr (Folder == QAFolder::After) { - histos.fill(HIST("QAcut/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); - if (hasTOF) { - histos.fill(HIST("QAcut/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); - histos.fill(HIST("QAcut/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); - } - histos.fill(HIST("QAcut/trkkaonpT"), track.pt()); - histos.fill(HIST("QAcut/trkkaonDCAxy"), track.dcaXY()); - histos.fill(HIST("QAcut/trkkaonDCAz"), track.dcaZ()); - } else { - histos.fill(HIST("QAMC/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); - if (hasTOF) { - histos.fill(HIST("QAMC/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); - histos.fill(HIST("QAMC/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); - } - histos.fill(HIST("QAMC/trkkaonpT"), track.pt()); - histos.fill(HIST("QAMC/trkkaonDCAxy"), track.dcaXY()); - histos.fill(HIST("QAMC/trkkaonDCAz"), track.dcaZ()); - } -} - -/// Selection and candidate loop shared by the K1 tasks. +/// K1 selection and candidate enumeration shared by the K1 tasks. /// The task owns the configurable groups and the histogram registry and passes them in init(). class K1AnalysisMicroCore { @@ -472,18 +240,45 @@ class K1AnalysisMicroCore EventCuts const& eventCuts, TrackCuts const& trackCuts, PionPidCuts const& pionPidCuts, KaonPidCuts const& kaonPidCuts, SecondaryCuts const& secondaryCuts, CandidateCuts const& candidateCuts, - HistogramOptions const& histogramOptions, ProcessModes const& modes, - LooseStageOptions const& looseOptions = {}) + ProcessModes const& modes, LooseStageOptions const& looseOptions = {}) { - mEventCuts = eventCuts; - mTrackCuts = trackCuts; - mPionPid = pionPidCuts; - mKaonPid = kaonPidCuts; mSecondaryCuts = secondaryCuts; mCandidateCuts = candidateCuts; - mHistogramOptions = histogramOptions; mLooseOptions = looseOptions; - mTruthDebugCounts = {}; + + // The order follows Species + std::vector pid(2); + auto& pion = pid[static_cast(Species::Pion)]; + pion.species = "Pion"; + pion.maxTPCnSigma = pionPidCuts.cMaxTPCnSigmaPion.value; + pion.maxTOFnSigma = pionPidCuts.cMaxTOFnSigmaPion.value; + pion.combinedNSigma = pionPidCuts.nsigmaCutCombinedPion.value; + pion.onlyTOFTracks = pionPidCuts.cUseOnlyTOFTrackPi.value; + pion.usePtDependent = pionPidCuts.cPionUsePtDepPID.value; + pion.ptBins = pionPidCuts.cPionPIDPtBins.value; + pion.tpcNSigmaCuts = pionPidCuts.cPionTPCNSigmaCuts.value; + pion.tofNSigmaCuts = pionPidCuts.cPionTOFNSigmaCuts.value; + pion.tofRequired = pionPidCuts.cPionTOFRequired.value; + pion.maxTPCName = "cMaxTPCnSigmaPion"; + pion.maxTOFName = "cMaxTOFnSigmaPion"; + pion.tpcCutsName = "cPionTPCNSigmaCuts"; + pion.tofCutsName = "cPionTOFNSigmaCuts"; + auto& kaon = pid[static_cast(Species::Kaon)]; + kaon.species = "Kaon"; + kaon.maxTPCnSigma = kaonPidCuts.cMaxTPCnSigmaKaon.value; + kaon.maxTOFnSigma = kaonPidCuts.cMaxTOFnSigmaKaon.value; + kaon.combinedNSigma = kaonPidCuts.nsigmaCutCombinedKaon.value; + kaon.onlyTOFTracks = kaonPidCuts.cUseOnlyTOFTrackKa.value; + kaon.usePtDependent = kaonPidCuts.cKaonUsePtDepPID.value; + kaon.ptBins = kaonPidCuts.cKaonPIDPtBins.value; + kaon.tpcNSigmaCuts = kaonPidCuts.cKaonTPCNSigmaCuts.value; + kaon.tofNSigmaCuts = kaonPidCuts.cKaonTOFNSigmaCuts.value; + kaon.tofRequired = kaonPidCuts.cKaonTOFRequired.value; + kaon.maxTPCName = "cMaxTPCnSigmaKaon"; + kaon.maxTOFName = "cMaxTOFnSigmaKaon"; + kaon.tpcCutsName = "cKaonTPCNSigmaCuts"; + kaon.tofCutsName = "cKaonTOFNSigmaCuts"; + mSelection.init(eventCuts, trackCuts, std::move(pid), mCandidateCuts.cByPassTOF, modes.microTracks); mSecondaryWindowOn = isCutEnabled(mSecondaryCuts.cSecondaryMasswindow); mAnotherMassCutOn = isCutEnabled(mSecondaryCuts.cMinAnotherSecondaryMassCut) || isCutEnabled(mSecondaryCuts.cMaxAnotherSecondaryMassCut); @@ -491,196 +286,32 @@ class K1AnalysisMicroCore mAngleCutOn = isCutEnabled(mSecondaryCuts.cMinAngle) || isCutEnabled(mSecondaryCuts.cMaxAngle); mPairAsymCutOn = isCutEnabled(mSecondaryCuts.cMinPairAsym) || isCutEnabled(mSecondaryCuts.cMaxPairAsym); - checkConfiguration(modes); registerHistograms(histos, modes); } template bool passesEventCuts(const CollisionType& collision) { - return !(mEventCuts.cRecoINELgt0 && !collision.isRecINELgt0()); + return mSelection.passesEventCuts(collision); } template bool passesMCEventCuts(const CollisionType& collision) { - if (mEventCuts.cMCINELgt0 && !collision.isINELgt0()) { - return false; - } - if (mEventCuts.cMCVtxIn10 && !collision.isVtxIn10()) { - return false; - } - return true; - } - - // Resolve the PID cut of one species at a given pT; false if the pT is outside all pT-dependent bins. - template - bool getPIDCut(float pt, PIDCut& cut) - { - if constexpr (S == Species::Pion) { - cut.tpcMax = mPionPid.cMaxTPCnSigmaPion; - cut.tofMax = mPionPid.cMaxTOFnSigmaPion; - cut.combined = mPionPid.nsigmaCutCombinedPion; - cut.tofRequired = false; - if (mPionPid.cPionUsePtDepPID) { - const int ptBin = getPtBinIndex(pt, mPionPid.cPionPIDPtBins.value); - if (ptBin < 0) { - return false; - } - const auto bin = static_cast(ptBin); - cut.tpcMax = mPionPid.cPionTPCNSigmaCuts.value[bin]; - cut.tofMax = mPionPid.cPionTOFNSigmaCuts.value[bin]; - cut.tofRequired = mPionPid.cPionTOFRequired.value[bin] != 0; - } - } else { - cut.tpcMax = mKaonPid.cMaxTPCnSigmaKaon; - cut.tofMax = mKaonPid.cMaxTOFnSigmaKaon; - cut.combined = mKaonPid.nsigmaCutCombinedKaon; - cut.tofRequired = false; - if (mKaonPid.cKaonUsePtDepPID) { - const int ptBin = getPtBinIndex(pt, mKaonPid.cKaonPIDPtBins.value); - if (ptBin < 0) { - return false; - } - const auto bin = static_cast(ptBin); - cut.tpcMax = mKaonPid.cKaonTPCNSigmaCuts.value[bin]; - cut.tofMax = mKaonPid.cKaonTOFNSigmaCuts.value[bin]; - cut.tofRequired = mKaonPid.cKaonTOFRequired.value[bin] != 0; - } - } - return true; + return mSelection.passesMCEventCuts(collision); } - // Track quality selection shared by pion and kaon. Returns the last stage that was passed. - // Full tracks store exact values; micro tracks store quantised DCA (see LFResonanceTables.h). - template - int trackQualityStage(const TrackType& track) - { - const double pt = track.pt(); - const double dcaXY = track.dcaXY(); - const double dcaZ = track.dcaZ(); - // Invalid micro DCA codes decode to NaN - if (!std::isfinite(pt) || !std::isfinite(track.eta()) || !std::isfinite(dcaXY) || !std::isfinite(dcaZ)) { - return kTrkInput; - } - if (std::abs(pt) < mTrackCuts.cMinPtcut) { - return kTrkInput; - } - if (isCutEnabled(mTrackCuts.cMaxEtacut) && !(std::abs(track.eta()) < mTrackCuts.cMaxEtacut)) { - return kTrkPt; - } - - if (mTrackCuts.cfgUsePtDepDCA) { - if constexpr (IsResoMicrotrack) { - if (!track.passedPtDependentDCAxy()) { - return kTrkEta; - } - if (!track.passedPtDependentDCAz()) { - return kTrkDCAxy; - } - } else { - const double dcaPtCut = mTrackCuts.cDCAToPVByPtP0 + mTrackCuts.cDCAToPVByPtCoeff * std::pow(pt, -static_cast(mTrackCuts.cDCAToPVByPtPower)); - if (!(std::abs(dcaXY) < dcaPtCut)) { - return kTrkEta; - } - if (!(std::abs(dcaZ) < dcaPtCut)) { - return kTrkDCAxy; - } - } - } else { - if (isCutEnabled(mTrackCuts.cMaxDCArToPVcut)) { - if constexpr (IsResoMicrotrack) { - if (!passesBinnedMax(dcaXY, mTrackCuts.cMaxDCArToPVcut)) { - return kTrkEta; - } - } else { - if (!(std::abs(dcaXY) <= mTrackCuts.cMaxDCArToPVcut)) { - return kTrkEta; - } - } - } - if (isCutEnabled(mTrackCuts.cMaxDCAzToPVcut)) { - if constexpr (IsResoMicrotrack) { - if (!passesBinnedMax(dcaZ, mTrackCuts.cMaxDCAzToPVcut)) { - return kTrkDCAxy; - } - } else { - if (!(std::abs(dcaZ) <= mTrackCuts.cMaxDCAzToPVcut)) { - return kTrkDCAxy; - } - } - } - } - if (isCutEnabled(mTrackCuts.cMinDCAzToPVcut)) { - if constexpr (IsResoMicrotrack) { - if (!passesBinnedMin(dcaZ, mTrackCuts.cMinDCAzToPVcut)) { - return kTrkDCAxy; - } - } else { - if (!(std::abs(dcaZ) >= mTrackCuts.cMinDCAzToPVcut)) { - return kTrkDCAxy; - } - } - } - - // Track flags - if ((mTrackCuts.cfgPrimaryTrack && !track.isPrimaryTrack()) || - (mTrackCuts.cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) || - (mTrackCuts.cfgGlobalTrack && !track.isGlobalTrack()) || - (mTrackCuts.cfgPVContributor && !track.isPVContributor()) || - (mTrackCuts.cfgUseITSRefit && !track.passedITSRefit()) || - (mTrackCuts.cfgUseTPCRefit && !track.passedTPCRefit())) { - return kTrkDCAz; - } - - // Clusters: found clusters exist only in ResoTracks, ITS clusters only in ResoMicroTracks - if constexpr (!IsResoMicrotrack) { - if constexpr (requires { track.tpcNClsFound(); }) { - if (track.tpcNClsFound() < mTrackCuts.cfgTPCcluster) { - return kTrkFlags; - } - } - } - if constexpr (requires { track.tpcNClsCrossedRows(); }) { - if (track.tpcNClsCrossedRows() < mTrackCuts.cfgTPCCrossedRowsMin) { - return kTrkFlags; - } - } - if constexpr (IsResoMicrotrack) { - if constexpr (requires { track.itsNCls(); }) { - if (track.itsNCls() < mTrackCuts.cfgITSNClsMin) { - return kTrkFlags; - } - } - } - return kTrkClusters; - } - - // TOF signal requirement of the track (global, per species, or per pT bin) - template - bool passesTOFRequired(const TrackType& track) - { - bool required = mTrackCuts.cfgHasTOF; - if constexpr (S == Species::Pion) { - required = required || mPionPid.cUseOnlyTOFTrackPi; - } else { - required = required || mKaonPid.cUseOnlyTOFTrackKa; - } - PIDCut cut; - // A pT outside all bins is rejected by passesPID - if (!mCandidateCuts.cByPassTOF && getPIDCut(track.pt(), cut) && cut.tofRequired) { - required = true; - } - return !required || track.hasTOF(); - } - - // PID selection: the same code for full and micro tracks, only the comparison is quantisation aware + // Full selection stage of a track (quality, TOF requirement, PID) template - bool passesPID(const TrackType& track) + int trackSelectionStage(const TrackType& track) { - PIDCut cut; - if (!getPIDCut(track.pt(), cut)) { - return false; + const int qualityStage = mSelection.trackQualityStage(track); + if (qualityStage < TrackStage::kTrkClusters) { + return qualityStage; + } + constexpr int SpeciesIndex = static_cast(S); + if (!mSelection.passesTOFRequired(SpeciesIndex, track)) { + return TrackStage::kTrkClusters; } const bool hasTOF = track.hasTOF(); const double tpcNSigma = (S == Species::Pion) ? track.tpcNSigmaPi() : track.tpcNSigmaKa(); @@ -694,48 +325,33 @@ class K1AnalysisMicroCore tofNSigma = track.tofNSigmaKa(); } } - if (isCutEnabled(cut.tpcMax) && !passesMax(std::abs(tpcNSigma), cut.tpcMax)) { - return false; - } - // Missing TOF is handled by passesTOFRequired; here the TPC alone decides - if (mCandidateCuts.cByPassTOF || !hasTOF) { - return true; + if (!mSelection.passesPID(SpeciesIndex, track.pt(), hasTOF, tpcNSigma, tofNSigma)) { + return TrackStage::kTrkTOFRequired; } - bool tofPassed = !isCutEnabled(cut.tofMax) || passesMax(std::abs(tofNSigma), cut.tofMax); - if (!tofPassed && cut.combined > 0 && tpcNSigma * tpcNSigma + tofNSigma * tofNSigma < cut.combined * cut.combined) { - tofPassed = true; - } - return tofPassed; + return TrackStage::kTrkPID; } - // Full selection stage of a track (quality, TOF requirement, PID) - template - int trackSelectionStage(const TrackType& track) - { - const int qualityStage = trackQualityStage(track); - if (qualityStage < kTrkClusters) { - return qualityStage; - } - if (!passesTOFRequired(track)) { - return kTrkClusters; - } - if (!passesPID(track)) { - return kTrkTOFRequired; - } - return kTrkPID; - } - - // Unordered (pion, pion, kaon) candidate loop of one collision (or one mixed pair of collisions). - // dTracks1: bachelor kaons, dTracks2: pions. The optional callback receives the unlike-sign micro - // same-event candidates in the canonical roles (collision, kaon, same-sign pion, opposite-sign pion, - // truth channel, pass bits) at the loose or selected stage configured by LooseStageOptions. - template - void fillHistograms(o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2, Callback callback = nullptr) + // Unordered (pion, pion, kaon) candidate enumeration of one collision (or one mixed pair of collisions). + // dTracks1: bachelor kaons, dTracks2: pions. The core fills the cut-flow instrumentation; the hooks + // (nullptr to skip) receive + // - onPair(trk1, trk2, secondary, passesPairPt): every pion pair passing the pion selection, + // trk1 being the pion with the lower index, + // - onCandidate(kaon, pion1, pion2, K1CandidateValues): every triplet passing the pion, pair and kaon + // selection, with the canonical pion roles (pion1: opposite sign to the kaon in the unlike-sign case), + // - onExport(collision, kaon, same-sign pion, opposite-sign pion, truth channel, pass bits): the unlike-sign + // micro same-event candidates at the loose or selected stage configured by LooseStageOptions. + // computeValues requests mass13, mass23, angle and pairAsym for every candidate in the rapidity window. + template + void forEachCandidate(o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2, + bool computeValues, PairHook onPair = nullptr, CandidateHook onCandidate = nullptr, ExportHook onExport = nullptr) { if (dTracks1.size() == 0 || dTracks2.size() == 0) { return; } - constexpr bool HasCallback = !std::is_same_v; + constexpr bool HasPairHook = !std::is_same_v; + constexpr bool HasCandidateHook = !std::is_same_v; + constexpr bool HasExportHook = !std::is_same_v; // Sets are local to this reconstructed collision: IDs cannot leak across DFs. // Source-file/DF deduplication across split collisions belongs in the audit. std::array, NTruthChannels> matchedMothers; @@ -749,36 +365,34 @@ class K1AnalysisMicroCore const auto pionSelected = buildSelectionCache(histos, dTracks2, firstPionIndex); // Only micro same-event ML work needs traversal before conventional cuts. - // The canonical-candidate validity is also required before a selected-stage callback. + // The canonical-candidate validity is also required before a selected-stage export. bool visitLoose = false; bool checkValidity = false; if constexpr (IsResoMicrotrack && !IsMix) { - visitLoose = mLooseOptions.audit || (!mLooseOptions.exportSelected && HasCallback); - checkValidity = visitLoose || (mLooseOptions.exportSelected && HasCallback); + visitLoose = mLooseOptions.audit || (!mLooseOptions.exportSelected && HasExportHook); + checkValidity = visitLoose || (mLooseOptions.exportSelected && HasExportHook); } std::vector kaonQuality(dTracks1.size(), 0), pionQuality(dTracks2.size(), 0); if (visitLoose) { for (const auto& track : dTracks1) { - kaonQuality[getCacheIndex(track, firstKaonIndex, kaonQuality.size())] = trackQualityStage(track) == kTrkClusters; + kaonQuality[getCacheIndex(track, firstKaonIndex, kaonQuality.size())] = mSelection.trackQualityStage(track) == TrackStage::kTrkClusters; } for (const auto& track : dTracks2) { - pionQuality[getCacheIndex(track, firstPionIndex, pionQuality.size())] = trackQualityStage(track) == kTrkClusters; + pionQuality[getCacheIndex(track, firstPionIndex, pionQuality.size())] = mSelection.trackQualityStage(track) == TrackStage::kTrkClusters; } } - // Values needed only by switched-on cuts or QA are computed only then + // Values needed only by switched-on cuts or by the task are computed only then const bool isK892Mode = mSecondaryCuts.cfgModeK892orRho; - const bool fillQA = !IsMix && mHistogramOptions.additionalQAplots; - const bool needAngle = IsMC || fillQA || mAngleCutOn; - const bool needPairAsym = IsMC || fillQA || mPairAsymCutOn; + const bool needAngle = computeValues || mAngleCutOn; + const bool needPairAsym = computeValues || mPairAsymCutOn; // K892 mode: the K* candidate is (trk1, K), rho mode: the rho is (trk1, trk2) - const bool needMass13 = IsMC || fillQA || (isK892Mode ? mSecondaryWindowOn : mAnotherMassCutOn) || (isK892Mode && (needAngle || needPairAsym)); - const bool needMass23 = IsMC || fillQA || mPiKaMassCutOn; + const bool needMass13 = computeValues || (isK892Mode ? mSecondaryWindowOn : mAnotherMassCutOn) || (isK892Mode && (needAngle || needPairAsym)); + const bool needMass23 = computeValues || mPiKaMassCutOn; const bool rhoWindowOn = mSecondaryWindowOn && !isK892Mode; - auto multiplicity = collision.cent(); ROOT::Math::PxPyPzMVector lDecayDaughter1, lDecayDaughter2, lResonanceSecondary, lDecayDaughter_bach, lResonanceK1, lPair13, lPair23; - // Unordered pion pairs: each (pion, pion, kaon) triplet is filled once. + // Unordered pion pairs: each (pion, pion, kaon) triplet is visited once. // Here trk1 is the pion with the lower index; the roles are assigned once the bachelor is known. for (const auto& [trk1, trk2] : o2::soa::combinations(o2::soa::CombinationsStrictlyUpperIndexPolicy(dTracks2, dTracks2))) { // trk1: pion, trk2: pion, bTrack: kaon @@ -820,24 +434,14 @@ class K1AnalysisMicroCore if (!pionsSelected && !visitLoose) { continue; } - - if (fillQA && pionsSelected) { - fillPionQA(histos, trk1, true); - fillPionQA(histos, trk2, false); + if constexpr (HasPairHook) { + if (pionsSelected) { + onPair(trk1, trk2, lResonanceSecondary, pairPt); + } } - if (!pairPt && !visitLoose) { continue; } - - if (fillQA && pionsSelected && pairPt) { - histos.fill(HIST("QA/hInvmassSecon"), lResonanceSecondary.M()); - } - if constexpr (IsMC) { - if (pionsSelected && pairPt) { - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - } - } // Secondary mass window (rho mode): the bachelor loop is skipped for rejected pairs if (!rhoWindow && !visitLoose) { continue; @@ -869,14 +473,11 @@ class K1AnalysisMicroCore if ((!pairSelected || !bachelorSelected) && !visitLoose) { continue; } - if (pairSelected && bachelorSelected) { + const bool tripletSelected = pairSelected && bachelorSelected; + if (tripletSelected) { countCandidate(7); } - if (fillQA && pairSelected && bachelorSelected) { - fillKaonQA(histos, bTrack); - } - // Canonical assignment of the pion roles, once the bachelor is known. // Unlike-sign pair: the pion with the sign opposite to the kaon is pion 1 (K*0 partner), the other is pion 2. // Like-sign pair (the rule is ambiguous): the pion with the lower index is pion 1. @@ -890,6 +491,10 @@ class K1AnalysisMicroCore // K1 reconstruction lDecayDaughter_bach.SetCoordinates(bTrack.px(), bTrack.py(), bTrack.pz(), o2::constants::physics::MassKaonCharged); lResonanceK1 = lResonanceSecondary + lDecayDaughter_bach; + K1CandidateValues values; + values.k1 = lResonanceK1; + values.secondary = lResonanceSecondary; + values.isUnlikeSign = isUnlikeSign; auto countMl = [&](int stage) { if (mLooseOptions.audit && isUnlikeSign) { @@ -916,44 +521,46 @@ class K1AnalysisMicroCore } // Stage L common acceptance uses the existing inclusive rapidity window. - if (lResonanceK1.Rapidity() > mCandidateCuts.cK1MaxRap || lResonanceK1.Rapidity() < mCandidateCuts.cK1MinRap) { + values.inRapidity = !(lResonanceK1.Rapidity() > mCandidateCuts.cK1MaxRap || lResonanceK1.Rapidity() < mCandidateCuts.cK1MinRap); + if (!values.inRapidity) { + if constexpr (HasCandidateHook) { + if (tripletSelected) { + onCandidate(bTrack, pion1, pion2, values); + } + } continue; } - if (pairSelected && bachelorSelected) { + if (tripletSelected) { countCandidate(8); } - double mass13 = 0.; - double mass23 = 0.; - double lK1Angle = 0.; - double lPairAsym = 0.; if (needMass13) { lPair13 = lPion1 + lDecayDaughter_bach; - mass13 = lPair13.M(); + values.mass13 = lPair13.M(); } if (needMass23) { lPair23 = lPion2 + lDecayDaughter_bach; - mass23 = lPair23.M(); + values.mass23 = lPair23.M(); } // Rho mode: secondary = (trk1, trk2) against the bachelor. K892 mode: secondary = (trk1, K) against trk2. if (needAngle) { - lK1Angle = isK892Mode ? ROOT::Math::VectorUtil::Angle(lPair13, lPion2) : ROOT::Math::VectorUtil::Angle(lResonanceSecondary, lDecayDaughter_bach); + values.angle = isK892Mode ? ROOT::Math::VectorUtil::Angle(lPair13, lPion2) : ROOT::Math::VectorUtil::Angle(lResonanceSecondary, lDecayDaughter_bach); } if (needPairAsym) { - lPairAsym = isK892Mode ? (lPair13.E() - lPion2.E()) / (lPair13.E() + lPion2.E()) - : (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); + values.pairAsym = isK892Mode ? (lPair13.E() - lPion2.E()) / (lPair13.E() + lPion2.E()) + : (lResonanceSecondary.E() - lDecayDaughter_bach.E()) / (lResonanceSecondary.E() + lDecayDaughter_bach.E()); } // Candidate cuts (each one is evaluated only if switched on) - const bool candidateCutsPass = - !(isK892Mode && mSecondaryWindowOn && (!isInWindow(mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) && - !(mAnotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) && - !(mPiKaMassCutOn && !isInRange(mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) && - !(mAngleCutOn && !isInRange(lK1Angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) && - !(mPairAsymCutOn && !isInRange(lPairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)); - auto emitCandidate = [&](uint16_t passBits) { - if constexpr (IsResoMicrotrack && !IsMix && HasCallback) { - callback(collision, bTrack, pion2, pion1, flowChannel, passBits); + values.passesCandidateCuts = + !(isK892Mode && mSecondaryWindowOn && (!isInWindow(values.mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) && + !(mAnotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : values.mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) && + !(mPiKaMassCutOn && !isInRange(values.mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) && + !(mAngleCutOn && !isInRange(values.angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) && + !(mPairAsymCutOn && !isInRange(values.pairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)); + auto exportCandidate = [&](uint16_t passBits) { + if constexpr (IsResoMicrotrack && !IsMix && HasExportHook) { + onExport(collision, bTrack, pion2, pion1, flowChannel, passBits); } else { static_cast(passBits); } @@ -976,7 +583,7 @@ class K1AnalysisMicroCore if (pairPt && rhoWindow) { passBits |= kPassPair; countMl(4); - if (candidateCutsPass) { + if (values.passesCandidateCuts) { passBits |= kPassCandidate; countMl(5); } @@ -984,119 +591,42 @@ class K1AnalysisMicroCore } } if (!mLooseOptions.exportSelected) { - emitCandidate(passBits); + exportCandidate(passBits); } } - // Stage C retains the frozen conventional selections and QA population. - if (!pairSelected || !bachelorSelected) { - continue; - } - - // QA histogram before the candidate cuts - if (fillQA) { - histos.fill(HIST("QA/K1OA"), lK1Angle); - histos.fill(HIST("QA/K1PairAsym"), lPairAsym); - histos.fill(HIST("QA/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); - histos.fill(HIST("QA/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); - histos.fill(HIST("QA/hpT_Secondary"), lResonanceSecondary.Pt()); - } - - if (!candidateCutsPass) { + // Stage C retains the frozen conventional selections. + if (!tripletSelected) { continue; } - countCandidate(9); - - // QA histograms after the candidate cuts - if (fillQA) { - fillPionQA(histos, pion1, true); - fillPionQA(histos, pion2, false); - fillKaonQA(histos, bTrack); - histos.fill(HIST("QAcut/K1OA"), lK1Angle); - histos.fill(HIST("QAcut/K1PairAsym"), lPairAsym); - histos.fill(HIST("QAcut/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); - histos.fill(HIST("QAcut/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); - histos.fill(HIST("QAcut/hInvmassSecon"), lResonanceSecondary.M()); - histos.fill(HIST("QAcut/hpT_Secondary"), lResonanceSecondary.Pt()); - } - - countCandidate(isUnlikeSign ? 10 : 11); - if (isUnlikeSign && mLooseOptions.exportSelected && validLoose) { - emitCandidate(PassBitsSelected); - } - if constexpr (IsMC && IsResoMicrotrack && !IsMix) { - if (flowChannel != K1TruthChannel::None) { - const int mother = flowChannel == K1TruthChannel::RhoK ? bTrack.motherId() : std::abs(pion1.motherPDG()) == o2::constants::physics::Pdg::kK1_1270Plus ? pion1.motherId() - : pion2.motherId(); - if (matchedMothers[static_cast(flowChannel)].insert(mother).second) { - histos.fill(HIST("CutFlow/uniqueMothersPerCollision"), static_cast(flowChannel)); - } + if (values.passesCandidateCuts) { + countCandidate(9); + countCandidate(isUnlikeSign ? 10 : 11); + if (isUnlikeSign && mLooseOptions.exportSelected && validLoose) { + exportCandidate(PassBitsSelected); } - } - - if constexpr (!IsMix) { - unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P : BinType::kK1N; - unsigned int typeNormal = BinAnti::kNormal; - if (isUnlikeSign) { - histos.fill(HIST("k1invmass"), lResonanceK1.M()); - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - } else { - histos.fill(HIST("k1invmass_LS"), lResonanceK1.M()); - histos.fill(HIST("hInvmass_K1_LS"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - } - - if constexpr (IsMC) { - const auto channel = classifyK1Truth(pion1, pion2, bTrack); - const int channelBin = static_cast(channel); - histos.fill(HIST("MCReco/channel"), channelBin); - histos.fill(HIST("MCReco/mass"), channelBin, lResonanceK1.M()); - histos.fill(HIST("MCReco/pt"), channelBin, lResonanceK1.Pt()); - histos.fill(HIST("MCReco/piPiMass"), channelBin, lResonanceSecondary.M()); - histos.fill(HIST("MCReco/pi1KMass"), channelBin, mass13); - histos.fill(HIST("MCReco/pi2KMass"), channelBin, mass23); - if (channel != K1TruthChannel::None) { - if (mTruthDebugCounts[channelBin] < mHistogramOptions.cfgTruthDebug) { - ++mTruthDebugCounts[channelBin]; - LOGP(info, "K1Truth channel={} collision={} tracks=({},{},{}) pdg=({},{},{}) mothers=({},{},{}) motherPDG=({},{},{}) siblings=(({},{}),({},{}),({},{}))", - channelBin, collision.globalIndex(), - pion1.globalIndex(), pion2.globalIndex(), bTrack.globalIndex(), - pion1.pdgCode(), pion2.pdgCode(), bTrack.pdgCode(), pion1.motherId(), pion2.motherId(), bTrack.motherId(), - pion1.motherPDG(), pion2.motherPDG(), bTrack.motherPDG(), - pion1.siblingIds()[0], pion1.siblingIds()[1], pion2.siblingIds()[0], pion2.siblingIds()[1], bTrack.siblingIds()[0], bTrack.siblingIds()[1]); + if constexpr (IsMC && IsResoMicrotrack && !IsMix) { + if (flowChannel != K1TruthChannel::None) { + const int mother = flowChannel == K1TruthChannel::RhoK ? bTrack.motherId() : std::abs(pion1.motherPDG()) == o2::constants::physics::Pdg::kK1_1270Plus ? pion1.motherId() + : pion2.motherId(); + if (matchedMothers[static_cast(flowChannel)].insert(mother).second) { + histos.fill(HIST("CutFlow/uniqueMothersPerCollision"), static_cast(flowChannel)); } - typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec; - histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - histos.fill(HIST("k1invmass_MC"), lResonanceK1.M()); - histos.fill(HIST("QAMC/K1OA"), lK1Angle); - histos.fill(HIST("QAMC/K1PairAsym"), lPairAsym); - histos.fill(HIST("QAMC/hInvmassK892_Rho"), mass13, lResonanceSecondary.M()); - histos.fill(HIST("QAMC/hInvmassSecon_PiKa"), lResonanceSecondary.M(), mass23); - histos.fill(HIST("QAMC/hInvmassSecon"), lResonanceSecondary.M()); - histos.fill(HIST("QAMC/hpT_Secondary"), lResonanceSecondary.Pt()); - - // PID QA primary and secondary pion - fillPionQA(histos, pion1, true); - fillPionQA(histos, pion2, false); - fillKaonQA(histos, bTrack); - } else { - histos.fill(HIST("k1invmass_MC_noK1"), lResonanceK1.M()); } - } // IsMC - } else { - unsigned int typeK1 = bTrack.sign() > 0 ? BinType::kK1P_Mix : BinType::kK1N_Mix; - unsigned int typeNormal = BinAnti::kNormal; - histos.fill(HIST("hInvmass_K1_Mix"), typeNormal, typeK1, multiplicity, lResonanceK1.Pt(), lResonanceK1.M()); - histos.fill(HIST("k1invmass_Mix"), lResonanceK1.M()); + } + } + if constexpr (HasCandidateHook) { + onCandidate(bTrack, pion1, pion2, values); } } // bTrack } - } // fillHistograms + } // forEachCandidate // Generated K1 parents of a selected reconstructed MC collision. The optional callback receives // (parent, immediate channel) for the parents inside the K1 rapidity window. // Parents belong to selected reconstructed events; split reco collisions // repeat parent sets. This is not an unconditional generated denominator. template - void fillGenerated(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents, Callback callback = nullptr) + void forEachGeneratedK1(o2::framework::HistogramRegistry& histos, const ParentsType& resoParents, Callback callback = nullptr) { for (const auto& part : resoParents) { if (std::abs(part.pdgCode()) != o2::constants::physics::Pdg::kK1_1270Plus) { @@ -1109,9 +639,6 @@ class K1AnalysisMicroCore continue; } histos.fill(HIST("CutFlow/generated"), 1, static_cast(channel)); - // Keep other/unresolved immediate decays too; never require both pairs. - histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast(channel)); - histos.fill(HIST("MCGen/ptChannel"), static_cast(channel), part.pt()); if constexpr (!std::is_same_v) { callback(part, channel); } @@ -1136,7 +663,7 @@ class K1AnalysisMicroCore std::vector selected(tracks.size(), 0); for (const auto& track : tracks) { const int stage = trackSelectionStage(track); - selected[getCacheIndex(track, firstIndex, selected.size())] = (stage == kTrkPID) ? 1 : 0; + selected[getCacheIndex(track, firstIndex, selected.size())] = (stage == TrackStage::kTrkPID) ? 1 : 0; if constexpr (FillCutFlow) { for (int i = 0; i <= stage; ++i) { histos.fill(HIST("CutFlow/tracks"), i, static_cast(S)); @@ -1146,119 +673,10 @@ class K1AnalysisMicroCore return selected; } - void checkConfiguration(ProcessModes const& modes) - { - // Consistency of the pT dependent PID configuration - if (mPionPid.cPionUsePtDepPID) { - const auto& bins = mPionPid.cPionPIDPtBins.value; - if (bins.size() < MinPtBinEdges || mPionPid.cPionTPCNSigmaCuts.value.size() != bins.size() - 1 || - mPionPid.cPionTOFNSigmaCuts.value.size() != bins.size() - 1 || mPionPid.cPionTOFRequired.value.size() != bins.size() - 1) { - LOG(fatal) << "Pion pT dependent PID vectors must have (number of pT bin edges - 1) entries"; - } - } - if (mKaonPid.cKaonUsePtDepPID) { - const auto& bins = mKaonPid.cKaonPIDPtBins.value; - if (bins.size() < MinPtBinEdges || mKaonPid.cKaonTPCNSigmaCuts.value.size() != bins.size() - 1 || - mKaonPid.cKaonTOFNSigmaCuts.value.size() != bins.size() - 1 || mKaonPid.cKaonTOFRequired.value.size() != bins.size() - 1) { - LOG(fatal) << "Kaon pT dependent PID vectors must have (number of pT bin edges - 1) entries"; - } - } - if (mCandidateCuts.cByPassTOF && (mPionPid.cUseOnlyTOFTrackPi || mKaonPid.cUseOnlyTOFTrackKa)) { - LOG(warning) << "cByPassTOF skips the TOF nSigma cut, but cUseOnlyTOFTrack* still requires a TOF signal"; - } - - // Micro tracks store quantised DCA and nSigma: a cut off the grid would silently act as a different cut. - if (!modes.microTracks) { - return; - } - auto checkDCAGrid = [](const char* name, double cut) { - const double nearest = std::min(std::max(std::round(cut / DCAGridStep) * DCAGridStep, 0.), DCAGridMax); - if (std::abs(cut - nearest) > GridTolerance) { - LOG(fatal) << name << " = " << cut << " is not on the quantised DCA grid (multiples of " << DCAGridStep << " up to " << DCAGridMax << "); nearest value: " << nearest; - } - }; - auto checkPIDGrid = [](const char* name, double cut) { - const double nearest = std::min(std::max(PIDGridStart + std::round((cut - PIDGridStart) / PIDGridStep) * PIDGridStep, PIDGridStart), PIDGridMax); - if (std::abs(cut - nearest) > GridTolerance) { - LOG(fatal) << name << " = " << cut << " is not on the quantised nSigma grid ({2.0, 2.25, ..., 3.5}); nearest value: " << nearest; - } - }; - if (mTrackCuts.cfgUsePtDepDCA) { - LOG(info) << "Micro tracks use the producer pT dependent DCA flags (0.004 + 0.013 / pT); cDCAToPVByPt* are ignored"; - if (std::abs(mTrackCuts.cDCAToPVByPtP0 - ProducerDCAPtP0) > ConfigTolerance || std::abs(mTrackCuts.cDCAToPVByPtCoeff - ProducerDCAPtCoeff) > ConfigTolerance || std::abs(mTrackCuts.cDCAToPVByPtPower - ProducerDCAPtPower) > ConfigTolerance) { - LOG(warning) << "cDCAToPVByPt* differ from the producer defaults, but micro tracks always use the producer formula"; - } - } else { - if (isCutEnabled(mTrackCuts.cMaxDCArToPVcut)) { - checkDCAGrid("cMaxDCArToPVcut", mTrackCuts.cMaxDCArToPVcut); - } - if (isCutEnabled(mTrackCuts.cMaxDCAzToPVcut)) { - checkDCAGrid("cMaxDCAzToPVcut", mTrackCuts.cMaxDCAzToPVcut); - } - } - if (isCutEnabled(mTrackCuts.cMinDCAzToPVcut)) { - checkDCAGrid("cMinDCAzToPVcut", mTrackCuts.cMinDCAzToPVcut); - } - if (isCutEnabled(mPionPid.cMaxTPCnSigmaPion) && !mPionPid.cPionUsePtDepPID) { - checkPIDGrid("cMaxTPCnSigmaPion", mPionPid.cMaxTPCnSigmaPion); - } - if (isCutEnabled(mPionPid.cMaxTOFnSigmaPion) && !mPionPid.cPionUsePtDepPID) { - checkPIDGrid("cMaxTOFnSigmaPion", mPionPid.cMaxTOFnSigmaPion); - } - if (isCutEnabled(mKaonPid.cMaxTPCnSigmaKaon) && !mKaonPid.cKaonUsePtDepPID) { - checkPIDGrid("cMaxTPCnSigmaKaon", mKaonPid.cMaxTPCnSigmaKaon); - } - if (isCutEnabled(mKaonPid.cMaxTOFnSigmaKaon) && !mKaonPid.cKaonUsePtDepPID) { - checkPIDGrid("cMaxTOFnSigmaKaon", mKaonPid.cMaxTOFnSigmaKaon); - } - if (mPionPid.cPionUsePtDepPID) { - for (const auto& cut : mPionPid.cPionTPCNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cPionTPCNSigmaCuts", cut); - } - } - for (const auto& cut : mPionPid.cPionTOFNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cPionTOFNSigmaCuts", cut); - } - } - } - if (mKaonPid.cKaonUsePtDepPID) { - for (const auto& cut : mKaonPid.cKaonTPCNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cKaonTPCNSigmaCuts", cut); - } - } - for (const auto& cut : mKaonPid.cKaonTOFNSigmaCuts.value) { - if (isCutEnabled(cut)) { - checkPIDGrid("cKaonTOFNSigmaCuts", cut); - } - } - } - if (mPionPid.nsigmaCutCombinedPion > 0 || mKaonPid.nsigmaCutCombinedKaon > 0) { - LOG(warning) << "nsigmaCutCombined* on micro tracks uses quantised nSigma values (approximate)"; - } - } - + // Cut-flow instrumentation of the selection; the output histograms belong to the tasks. void registerHistograms(o2::framework::HistogramRegistry& histos, ProcessModes const& modes) { - using o2::framework::AxisSpec; using o2::framework::HistType; - const int nBinsDiv = mHistogramOptions.cNbinsDiv; - std::vector centBinning = {0., 1., 5., 10., 15., 20., 25., 30., 35., 40., 45., 50., 55., 60., 65., 70., 80., 90., 100., 200.}; - AxisSpec centAxis = {centBinning, "T0M (%)"}; - AxisSpec ptAxis = {150, 0, 15, "#it{p}_{T} (GeV/#it{c})"}; - AxisSpec dcaxyAxis = {300, 0, 3, "DCA_{#it{xy}} (cm)"}; - AxisSpec dcazAxis = {500, 0, 5, "DCA_{#it{z}} (cm)"}; - AxisSpec invMassAxisK892 = {1400 / nBinsDiv, 0.6, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // K(892)0 - AxisSpec invMassAxisRho = {2000 / nBinsDiv, 0.0, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // rho - AxisSpec invMassAxisReso = {1600 / nBinsDiv, 0.9f, 2.5f, "Invariant Mass (GeV/#it{c}^2)"}; // K1 - AxisSpec pidQAAxis = {130, -6.5, 6.5}; - - // THnSparse - AxisSpec axisAnti = {BinAnti::kNAEnd, 0, BinAnti::kNAEnd, "Type of bin: Normal or Anti"}; - AxisSpec axisType = {BinType::kTYEnd, 0, BinType::kTYEnd, "Type of bin with charge and mix"}; - if (mLooseOptions.audit) { auto flow = histos.add("ML/looseCutflow", "US triplets;stage;signal stratum", HistType::kTH2D, {{6, -0.5, 5.5}, {5, -0.5, 4.5}}); const std::array labels{"structural US", "loose acceptance", "track quality", "TOF + PID", "pair requirements", "selected US"}; @@ -1274,8 +692,9 @@ class K1AnalysisMicroCore } // Micro-only instrumentation: category 0 includes all combinations, not just unmatched. - auto trackFlow = histos.add("CutFlow/tracks", "Micro tracks, once per selected collision;stage;species", HistType::kTH2D, {{static_cast(kTrkNStages), -0.5, static_cast(kTrkNStages) - 0.5}, {2, -0.5, 1.5}}); - const std::array trackLabels{"input", "pT", "eta", "DCAxy", "DCAz", "track flags", "clusters / crossed rows", "TOF required", "PID"}; + constexpr int NTrackStages = TrackStage::kTrkNStages; + auto trackFlow = histos.add("CutFlow/tracks", "Micro tracks, once per selected collision;stage;species", HistType::kTH2D, {{NTrackStages, -0.5, NTrackStages - 0.5}, {2, -0.5, 1.5}}); + const std::array trackLabels{"input", "pT", "eta", "DCAxy", "DCAz", "track flags", "clusters / crossed rows", "TOF required", "PID"}; for (std::size_t i = 0; i < trackLabels.size(); ++i) { trackFlow->GetXaxis()->SetBinLabel(i + 1, trackLabels[i]); } @@ -1302,133 +721,11 @@ class K1AnalysisMicroCore generated->GetYaxis()->SetBinLabel(2, "rho K"); generated->GetYaxis()->SetBinLabel(3, "K* pi"); } - - // DCA QA - // Primary pion - histos.add("QA/trkppionDCAxy", "DCAxy disstribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QA/trkppionDCAz", "DCAz disstribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QA/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QA/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAcut/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAcut/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAcut/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAcut/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - // Secondary pion - histos.add("QA/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QA/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QA/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QA/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAcut/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAcut/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAcut/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAcut/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - // Kaon - histos.add("QA/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QA/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QA/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); - histos.add("QA/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QA/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAcut/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAcut/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAcut/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAcut/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAcut/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - // K1 - histos.add("QA/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); - histos.add("QA/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); - histos.add("QA/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); - histos.add("QA/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); - histos.add("QA/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); - histos.add("QA/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); - - histos.add("QAcut/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); - histos.add("QAcut/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); - histos.add("QAcut/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); - histos.add("QAcut/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); - histos.add("QAcut/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); - histos.add("QAcut/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); - - // Invariant mass - histos.add("hInvmass_K1", "Invariant mass of K1(1270) (US)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); - histos.add("hInvmass_K1_LS", "Invariant mass of K1(1270) (LS)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); - histos.add("hInvmass_K1_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); - // Mass QA (quick check) - histos.add("k1invmass", "Invariant mass of K1(1270) (US)", HistType::kTH1F, {invMassAxisReso}); - histos.add("k1invmass_LS", "Invariant mass of K1(1270) (LS)", HistType::kTH1F, {invMassAxisReso}); - histos.add("k1invmass_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTH1F, {invMassAxisReso}); - - // MC - if (modes.mcReco) { - AxisSpec channelAxis = {3, -0.5, 2.5, "0: non-K1, 1: rho K, 2: K* pi"}; - histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}}); - histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}}); - histos.add("MCReco/channel", "All selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis}); - histos.add("MCReco/mass", "Reconstructed mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisReso}); - histos.add("MCReco/pt", "Reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis}); - histos.add("MCReco/piPiMass", "pi-pi mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisRho}); - histos.add("MCReco/pi1KMass", "First pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); - histos.add("MCReco/pi2KMass", "Second pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); - histos.add("k1invmass_MC", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); - histos.add("k1invmass_MC_noK1", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); - - histos.add("QAMC/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAMC/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAMC/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAMC/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAMC/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAMC/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAMC/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAMC/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAMC/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); - histos.add("QAMC/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); - histos.add("QAMC/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); - histos.add("QAMC/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); - histos.add("QAMC/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); - - histos.add("QAMC/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); - histos.add("QAMC/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); - histos.add("QAMC/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); - histos.add("QAMC/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); - histos.add("QAMC/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); - histos.add("QAMC/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); - } // mcReco - if (modes.mcGen) { - AxisSpec channelAxis = {3, -0.5, 2.5, "0: other/unresolved, 1: rho K, 2: K* pi"}; - 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}); - histos.add("MCGen/ptChannel", "Generated K1 pT by immediate decay channel", HistType::kTH2D, {channelAxis, ptAxis}); - } } - EventCuts mEventCuts; - TrackCuts mTrackCuts; - PionPidCuts mPionPid; - KaonPidCuts mKaonPid; + ResoAnalysisSelectionCore mSelection; SecondaryCuts mSecondaryCuts; CandidateCuts mCandidateCuts; - HistogramOptions mHistogramOptions; LooseStageOptions mLooseOptions; // Derived once in init(): which candidate cuts are switched on. @@ -1437,8 +734,6 @@ class K1AnalysisMicroCore bool mPiKaMassCutOn = false; bool mAngleCutOn = false; bool mPairAsymCutOn = false; - - std::array mTruthDebugCounts{}; }; } // namespace o2::analysis::k1micro diff --git a/PWGLF/Core/ResoAnalysisSelectionCore.h b/PWGLF/Core/ResoAnalysisSelectionCore.h new file mode 100644 index 00000000000..225f65124b7 --- /dev/null +++ b/PWGLF/Core/ResoAnalysisSelectionCore.h @@ -0,0 +1,462 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file ResoAnalysisSelectionCore.h +/// \brief Event, track-quality and PID selection of resonance daughters from the reduced v001 resonance tables +/// \author Bong-Hwi Lim +/// +/// The same selection code serves full ResoTracks (exact values) and ResoMicroTracks (quantised DCA and nSigma, +/// see LFResonanceTables.h); only the comparisons are quantisation aware. A cut set to DisabledCut is off. + +#ifndef PWGLF_CORE_RESOANALYSISSELECTIONCORE_H_ +#define PWGLF_CORE_RESOANALYSISSELECTIONCORE_H_ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace o2::analysis::resonance +{ + +inline constexpr float DisabledCut = -999.f; // an optional cut with this value is off and not evaluated +inline constexpr double DCAGridStep = 0.025; // v001 micro DCA encoding, lower-inclusive bins up to DCAGridMax +inline constexpr double DCAGridMax = 0.15; +inline constexpr double PIDGridStart = 2.0; // v001 micro nSigma encoding: 0.25 bins in [2.0, 3.5] +inline constexpr double PIDGridStep = 0.25; +inline constexpr double PIDGridMax = 3.5; +inline constexpr double GridTolerance = 1e-4; +inline constexpr std::size_t MinPtBinEdges = 2; // a pT dependent PID table needs at least one bin +inline constexpr float ProducerDCAPtP0 = 0.004f; // resonanceModuleInitializer cfgTightDCAOffset default +inline constexpr float ProducerDCAPtCoeff = 0.013f; // resonanceModuleInitializer cfgTightDCAPtCoefficient default +inline constexpr float ProducerDCAPtPower = 1.f; // resonanceModuleInitializer cfgTightDCAPtPower default +inline constexpr float ConfigTolerance = 1e-6f; + +// Last stage passed by a track; a cut-flow histogram can be filled directly from this value. +enum TrackStage : int { + kTrkInput = 0, + kTrkPt, + kTrkEta, + kTrkDCAxy, + kTrkDCAz, + kTrkFlags, + kTrkClusters, + kTrkTOFRequired, + kTrkPID, + kTrkNStages +}; + +// Resolved PID cut of one species at a given pT. +struct PIDCut { + double tpcMax = 0.; + double tofMax = 0.; + double combined = 0.; + bool tofRequired = false; +}; + +// A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value). +inline bool isCutEnabled(float value) +{ + return value > DisabledCut + 1.f; +} + +// v001 micro values are lower-inclusive bin edges: a maximum cut on the grid keeps bins below it. +inline bool passesBinnedMax(double decoded, double cut) +{ + return decoded < cut - o2::constants::math::Epsilon; +} + +// Minimum cut on the grid keeps the bin starting at the cut. +inline bool passesBinnedMin(double decoded, double cut) +{ + return decoded >= cut - o2::constants::math::Epsilon; +} + +template +bool passesMax(double value, double cut) +{ + if constexpr (IsResoMicrotrack) { + return passesBinnedMax(value, cut); + } else { + return value < cut; + } +} + +inline bool isInRange(double value, double minimum, double maximum) +{ + if (isCutEnabled(minimum) && value < minimum) { + return false; + } + if (isCutEnabled(maximum) && value > maximum) { + return false; + } + return true; +} + +inline bool isInWindow(double value, double center, double width) +{ + return std::abs(value - center) < width; +} + +// Preserve pT-bin membership [low, high). +inline int getPtBinIndex(float pt, const std::vector& ptBins) +{ + for (std::size_t i = 1; i < ptBins.size(); ++i) { + if (pt >= ptBins[i - 1] && pt < ptBins[i]) { + return static_cast(i - 1); + } + } + return -1; +} + +// Configurable groups without prefix: the JSON keys are the plain configurable names. + +/// Event selection +struct EventCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cRecoINELgt0{"cRecoINELgt0", false, "Apply reconstructed INEL>0 selection"}; + o2::framework::Configurable cMCINELgt0{"cMCINELgt0", false, "Require generator INEL>0 in MC processes"}; + o2::framework::Configurable cMCVtxIn10{"cMCVtxIn10", false, "Require generator |vz| < 10 cm in MC processes"}; +}; + +/// Track selections (common for all daughter species, -999 switches an optional cut off) +struct TrackCuts : o2::framework::ConfigurableGroup { + o2::framework::Configurable cMinPtcut{"cMinPtcut", 0.15, "Track minium pt cut"}; + o2::framework::Configurable cMaxEtacut{"cMaxEtacut", -999.f, "Track maximum |eta| cut (-999: off)"}; + // DCAr to PV + o2::framework::Configurable cMaxDCArToPVcut{"cMaxDCArToPVcut", 0.1, "Track DCAr cut to PV Maximum"}; + // DCAz to PV + o2::framework::Configurable cMaxDCAzToPVcut{"cMaxDCAzToPVcut", 0.1, "Track DCAz cut to PV Maximum"}; + o2::framework::Configurable cMinDCAzToPVcut{"cMinDCAzToPVcut", 0.0, "Track DCAz cut to PV Minimum"}; + o2::framework::Configurable cfgUsePtDepDCA{"cfgUsePtDepDCA", false, "Use pT dependent DCA cut instead of the fixed maximum"}; + o2::framework::Configurable cDCAToPVByPtP0{"cDCAToPVByPtP0", 0.004f, "pT dependent DCA cut = P0 + coefficient / pT^power (cm)"}; + o2::framework::Configurable cDCAToPVByPtCoeff{"cDCAToPVByPtCoeff", 0.013f, "Coefficient in the pT dependent DCA cut"}; + o2::framework::Configurable cDCAToPVByPtPower{"cDCAToPVByPtPower", 1.f, "Power in the pT dependent DCA cut"}; + o2::framework::Configurable cfgPrimaryTrack{"cfgPrimaryTrack", true, "Primary track selection"}; // kGoldenChi2 | kDCAxy | kDCAz + o2::framework::Configurable cfgGlobalWoDCATrack{"cfgGlobalWoDCATrack", true, "Global track selection without DCA"}; // kQualityTracks (kTrackType | kTPCNCls | kTPCCrossedRows | kTPCCrossedRowsOverNCls | kTPCChi2NDF | kTPCRefit | kITSNCls | kITSChi2NDF | kITSRefit | kITSHits) | kInAcceptanceTracks (kPtRange | kEtaRange) + o2::framework::Configurable cfgGlobalTrack{"cfgGlobalTrack", false, "Global track selection"}; // kGoldenChi2 | kDCAxy | kDCAz + o2::framework::Configurable cfgPVContributor{"cfgPVContributor", false, "PV contributor track selection"}; // PV Contriuibutor + o2::framework::Configurable cfgUseTPCRefit{"cfgUseTPCRefit", false, "Require TPC Refit"}; + o2::framework::Configurable cfgUseITSRefit{"cfgUseITSRefit", false, "Require ITS Refit"}; + o2::framework::Configurable cfgTPCcluster{"cfgTPCcluster", 0, "Number of TPC cluster (found clusters, ResoTracks only)"}; + o2::framework::Configurable cfgTPCCrossedRowsMin{"cfgTPCCrossedRowsMin", 0, "Minimum number of TPC crossed rows"}; + o2::framework::Configurable cfgITSNClsMin{"cfgITSNClsMin", 0, "Minimum number of ITS clusters (ResoMicroTracks only)"}; + o2::framework::Configurable cfgHasTOF{"cfgHasTOF", false, "Require TOF"}; +}; + +/// PID selection of one daughter species, filled by the task from its own (species-named) configurables. +/// The configurable names are used only in the configuration messages. +struct PIDCutConfig { + std::string species; // e.g. "Pion", used in messages + double maxTPCnSigma = DisabledCut; + double maxTOFnSigma = DisabledCut; + double combinedNSigma = DisabledCut; // combined TPC-TOF cut, on when > 0 + bool onlyTOFTracks = false; // require a TOF signal + bool usePtDependent = false; // use the pT binned cuts below instead of the fixed maxima + std::vector ptBins; // bin edges; the other vectors have one entry per bin + std::vector tpcNSigmaCuts; + std::vector tofNSigmaCuts; + std::vector tofRequired; + std::string maxTPCName; // configurable names for the messages + std::string maxTOFName; + std::string tpcCutsName; + std::string tofCutsName; +}; + +/// Event, track-quality, TOF-requirement and PID selection of resonance daughters. +/// The species index is the position of its PIDCutConfig in init(). +class ResoAnalysisSelectionCore +{ + public: + // byPassTOF skips the TOF nSigma cut and the pT binned TOF requirement; microTracks enables the grid checks. + void init(EventCuts const& eventCuts, TrackCuts const& trackCuts, std::vector pidCuts, bool byPassTOF, bool microTracks) + { + mEventCuts = eventCuts; + mTrackCuts = trackCuts; + mPID = std::move(pidCuts); + mByPassTOF = byPassTOF; + checkConfiguration(microTracks); + } + + template + bool passesEventCuts(const CollisionType& collision) + { + return !(mEventCuts.cRecoINELgt0 && !collision.isRecINELgt0()); + } + + template + bool passesMCEventCuts(const CollisionType& collision) + { + if (mEventCuts.cMCINELgt0 && !collision.isINELgt0()) { + return false; + } + if (mEventCuts.cMCVtxIn10 && !collision.isVtxIn10()) { + return false; + } + return true; + } + + // Resolve the PID cut of one species at a given pT; false if the pT is outside all pT-dependent bins. + bool getPIDCut(int species, float pt, PIDCut& cut) + { + const auto& config = mPID[species]; + cut.tpcMax = config.maxTPCnSigma; + cut.tofMax = config.maxTOFnSigma; + cut.combined = config.combinedNSigma; + cut.tofRequired = false; + if (config.usePtDependent) { + const int ptBin = getPtBinIndex(pt, config.ptBins); + if (ptBin < 0) { + return false; + } + const auto bin = static_cast(ptBin); + cut.tpcMax = config.tpcNSigmaCuts[bin]; + cut.tofMax = config.tofNSigmaCuts[bin]; + cut.tofRequired = config.tofRequired[bin] != 0; + } + return true; + } + + // Track quality selection shared by all species. Returns the last stage that was passed. + // Full tracks store exact values; micro tracks store quantised DCA (see LFResonanceTables.h). + template + int trackQualityStage(const TrackType& track) + { + const double pt = track.pt(); + const double dcaXY = track.dcaXY(); + const double dcaZ = track.dcaZ(); + // Invalid micro DCA codes decode to NaN + if (!std::isfinite(pt) || !std::isfinite(track.eta()) || !std::isfinite(dcaXY) || !std::isfinite(dcaZ)) { + return kTrkInput; + } + if (std::abs(pt) < mTrackCuts.cMinPtcut) { + return kTrkInput; + } + if (isCutEnabled(mTrackCuts.cMaxEtacut) && !(std::abs(track.eta()) < mTrackCuts.cMaxEtacut)) { + return kTrkPt; + } + + if (mTrackCuts.cfgUsePtDepDCA) { + if constexpr (IsResoMicrotrack) { + if (!track.passedPtDependentDCAxy()) { + return kTrkEta; + } + if (!track.passedPtDependentDCAz()) { + return kTrkDCAxy; + } + } else { + const double dcaPtCut = mTrackCuts.cDCAToPVByPtP0 + mTrackCuts.cDCAToPVByPtCoeff * std::pow(pt, -static_cast(mTrackCuts.cDCAToPVByPtPower)); + if (!(std::abs(dcaXY) < dcaPtCut)) { + return kTrkEta; + } + if (!(std::abs(dcaZ) < dcaPtCut)) { + return kTrkDCAxy; + } + } + } else { + if (isCutEnabled(mTrackCuts.cMaxDCArToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMax(dcaXY, mTrackCuts.cMaxDCArToPVcut)) { + return kTrkEta; + } + } else { + if (!(std::abs(dcaXY) <= mTrackCuts.cMaxDCArToPVcut)) { + return kTrkEta; + } + } + } + if (isCutEnabled(mTrackCuts.cMaxDCAzToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMax(dcaZ, mTrackCuts.cMaxDCAzToPVcut)) { + return kTrkDCAxy; + } + } else { + if (!(std::abs(dcaZ) <= mTrackCuts.cMaxDCAzToPVcut)) { + return kTrkDCAxy; + } + } + } + } + if (isCutEnabled(mTrackCuts.cMinDCAzToPVcut)) { + if constexpr (IsResoMicrotrack) { + if (!passesBinnedMin(dcaZ, mTrackCuts.cMinDCAzToPVcut)) { + return kTrkDCAxy; + } + } else { + if (!(std::abs(dcaZ) >= mTrackCuts.cMinDCAzToPVcut)) { + return kTrkDCAxy; + } + } + } + + // Track flags + if ((mTrackCuts.cfgPrimaryTrack && !track.isPrimaryTrack()) || + (mTrackCuts.cfgGlobalWoDCATrack && !track.isGlobalTrackWoDCA()) || + (mTrackCuts.cfgGlobalTrack && !track.isGlobalTrack()) || + (mTrackCuts.cfgPVContributor && !track.isPVContributor()) || + (mTrackCuts.cfgUseITSRefit && !track.passedITSRefit()) || + (mTrackCuts.cfgUseTPCRefit && !track.passedTPCRefit())) { + return kTrkDCAz; + } + + // Clusters: found clusters exist only in ResoTracks, ITS clusters only in ResoMicroTracks + if constexpr (!IsResoMicrotrack) { + if constexpr (requires { track.tpcNClsFound(); }) { + if (track.tpcNClsFound() < mTrackCuts.cfgTPCcluster) { + return kTrkFlags; + } + } + } + if constexpr (requires { track.tpcNClsCrossedRows(); }) { + if (track.tpcNClsCrossedRows() < mTrackCuts.cfgTPCCrossedRowsMin) { + return kTrkFlags; + } + } + if constexpr (IsResoMicrotrack) { + if constexpr (requires { track.itsNCls(); }) { + if (track.itsNCls() < mTrackCuts.cfgITSNClsMin) { + return kTrkFlags; + } + } + } + return kTrkClusters; + } + + // TOF signal requirement of the track (global, per species, or per pT bin) + template + bool passesTOFRequired(int species, const TrackType& track) + { + bool required = mTrackCuts.cfgHasTOF || mPID[species].onlyTOFTracks; + PIDCut cut; + // A pT outside all bins is rejected by passesPID + if (!mByPassTOF && getPIDCut(species, track.pt(), cut) && cut.tofRequired) { + required = true; + } + return !required || track.hasTOF(); + } + + // PID selection from the nSigma values of the species; tofNSigma is used only with hasTOF. + template + bool passesPID(int species, float pt, bool hasTOF, double tpcNSigma, double tofNSigma) + { + PIDCut cut; + if (!getPIDCut(species, pt, cut)) { + return false; + } + if (isCutEnabled(cut.tpcMax) && !passesMax(std::abs(tpcNSigma), cut.tpcMax)) { + return false; + } + // Missing TOF is handled by passesTOFRequired; here the TPC alone decides + if (mByPassTOF || !hasTOF) { + return true; + } + bool tofPassed = !isCutEnabled(cut.tofMax) || passesMax(std::abs(tofNSigma), cut.tofMax); + if (!tofPassed && cut.combined > 0 && tpcNSigma * tpcNSigma + tofNSigma * tofNSigma < cut.combined * cut.combined) { + tofPassed = true; + } + return tofPassed; + } + + private: + void checkConfiguration(bool microTracks) + { + // Consistency of the pT dependent PID configuration + bool anyOnlyTOF = false; + for (const auto& config : mPID) { + if (config.usePtDependent) { + const auto& bins = config.ptBins; + if (bins.size() < MinPtBinEdges || config.tpcNSigmaCuts.size() != bins.size() - 1 || + config.tofNSigmaCuts.size() != bins.size() - 1 || config.tofRequired.size() != bins.size() - 1) { + LOG(fatal) << config.species << " pT dependent PID vectors must have (number of pT bin edges - 1) entries"; + } + } + anyOnlyTOF = anyOnlyTOF || config.onlyTOFTracks; + } + if (mByPassTOF && anyOnlyTOF) { + LOG(warning) << "cByPassTOF skips the TOF nSigma selection, but cUseOnlyTOFTrack* still requires a TOF signal"; + } + + // Micro tracks store quantised DCA and nSigma: a cut off the grid would silently act as a different cut. + if (!microTracks) { + return; + } + auto checkDCAGrid = [](const char* name, double cut) { + const double nearest = std::min(std::max(std::round(cut / DCAGridStep) * DCAGridStep, 0.), DCAGridMax); + if (std::abs(cut - nearest) > GridTolerance) { + LOG(fatal) << name << " = " << cut << " is not on the quantised DCA grid (multiples of " << DCAGridStep << " up to " << DCAGridMax << "); nearest value: " << nearest; + } + }; + auto checkPIDGrid = [](const std::string& name, double cut) { + const double nearest = std::min(std::max(PIDGridStart + std::round((cut - PIDGridStart) / PIDGridStep) * PIDGridStep, PIDGridStart), PIDGridMax); + if (std::abs(cut - nearest) > GridTolerance) { + LOG(fatal) << name << " = " << cut << " is not on the quantised nSigma grid ({2.0, 2.25, ..., 3.5}); nearest value: " << nearest; + } + }; + if (mTrackCuts.cfgUsePtDepDCA) { + LOG(info) << "Micro tracks use the producer pT dependent DCA flags (0.004 + 0.013 / pT); cDCAToPVByPt* are ignored"; + if (std::abs(mTrackCuts.cDCAToPVByPtP0 - ProducerDCAPtP0) > ConfigTolerance || std::abs(mTrackCuts.cDCAToPVByPtCoeff - ProducerDCAPtCoeff) > ConfigTolerance || std::abs(mTrackCuts.cDCAToPVByPtPower - ProducerDCAPtPower) > ConfigTolerance) { + LOG(warning) << "cDCAToPVByPt* differ from the producer defaults, but micro tracks always use the producer formula"; + } + } else { + if (isCutEnabled(mTrackCuts.cMaxDCArToPVcut)) { + checkDCAGrid("cMaxDCArToPVcut", mTrackCuts.cMaxDCArToPVcut); + } + if (isCutEnabled(mTrackCuts.cMaxDCAzToPVcut)) { + checkDCAGrid("cMaxDCAzToPVcut", mTrackCuts.cMaxDCAzToPVcut); + } + } + if (isCutEnabled(mTrackCuts.cMinDCAzToPVcut)) { + checkDCAGrid("cMinDCAzToPVcut", mTrackCuts.cMinDCAzToPVcut); + } + bool anyCombined = false; + for (const auto& config : mPID) { + if (isCutEnabled(config.maxTPCnSigma) && !config.usePtDependent) { + checkPIDGrid(config.maxTPCName, config.maxTPCnSigma); + } + if (isCutEnabled(config.maxTOFnSigma) && !config.usePtDependent) { + checkPIDGrid(config.maxTOFName, config.maxTOFnSigma); + } + anyCombined = anyCombined || config.combinedNSigma > 0; + } + for (const auto& config : mPID) { + if (!config.usePtDependent) { + continue; + } + for (const auto& cut : config.tpcNSigmaCuts) { + if (isCutEnabled(cut)) { + checkPIDGrid(config.tpcCutsName, cut); + } + } + for (const auto& cut : config.tofNSigmaCuts) { + if (isCutEnabled(cut)) { + checkPIDGrid(config.tofCutsName, cut); + } + } + } + if (anyCombined) { + LOG(warning) << "nsigmaCutCombined* on micro tracks uses quantised nSigma values (approximate)"; + } + } + + EventCuts mEventCuts; + TrackCuts mTrackCuts; + std::vector mPID; + bool mByPassTOF = false; +}; + +} // namespace o2::analysis::resonance + +#endif // PWGLF_CORE_RESOANALYSISSELECTIONCORE_H_ diff --git a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx index 0546857f486..c108e5df193 100644 --- a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx +++ b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx @@ -15,8 +15,10 @@ /// #include "PWGLF/Core/K1AnalysisMicroCore.h" +#include "PWGLF/Core/ResoAnalysisSelectionCore.h" #include "PWGLF/DataModel/LFResonanceTables.h" +#include #include #include #include @@ -24,20 +26,64 @@ #include #include #include +#include #include #include #include #include #include +#include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) + +#include +#include #include +#include using namespace o2; using namespace o2::framework; using namespace o2::framework::expressions; using namespace o2::soa; +using namespace o2::analysis::resonance; using namespace o2::analysis::k1micro; +/// Histogram binning, QA and debug output +struct HistogramOptions : ConfigurableGroup { + Configurable cNbinsDiv{"cNbinsDiv", 1, "Integer to divide the number of bins"}; + Configurable additionalQAplots{"additionalQAplots", true, "Additional QA plots"}; + Configurable cfgTruthDebug{"cfgTruthDebug", 0, "Maximum logged matched candidates per truth channel"}; +}; + +enum BinAnti : unsigned int { + kNormal = 0, + kAnti, + kNAEnd +}; + +enum BinType : unsigned int { + kK1P = 0, + kK1N, + kK1P_Mix, + kK1N_Mix, + kK1P_GenINEL10, + kK1N_GenINEL10, + kK1P_GenINELgt10, + kK1N_GenINELgt10, + kK1P_GenTrig10, + kK1N_GenTrig10, + kK1P_GenEvtSel, + kK1N_GenEvtSel, + kK1P_Rec, + kK1N_Rec, + kTYEnd +}; + +enum class QAFolder { + Before, // QA/*: before the candidate cuts + After, // QAcut/*: after the candidate cuts + MC // QAMC/*: matched K1 truth candidates +}; + struct K1AnalysisMicro { // Module-initializer v001 tables; full tracks keep their unversioned schema as a fallback. using ResoCollisions = aod::ResoCollisions_001; @@ -69,6 +115,7 @@ struct K1AnalysisMicro { ConfigurableAxis cfgMultBins{"cfgMultBins", {VARIABLE_WIDTH, 0.0f, 20.0f, 40.0f, 60.0f, 80.0f, 100.0f, 200.0f, 99999.f}, "Mixing bins - multiplicity"}; K1AnalysisMicroCore core; + std::array truthDebugCounts{}; void init(InitContext&) { @@ -84,20 +131,376 @@ struct K1AnalysisMicro { modes.mcReco = doprocessMC || doprocessMCMicro; modes.mcRecoMicro = doprocessMCMicro; modes.mcGen = doprocessMCTrue; - core.init(histos, eventCuts, trackCuts, pionPID, kaonPID, secondaryCuts, candidateCuts, histogramOptions, modes); + core.init(histos, eventCuts, trackCuts, pionPID, kaonPID, secondaryCuts, candidateCuts, modes); + registerHistograms(modes); // Print output histograms statistics LOG(info) << "Size of the histograms in K1 Analysis Task"; histos.print(); } + void registerHistograms(ProcessModes const& modes) + { + const int nBinsDiv = histogramOptions.cNbinsDiv; + std::vector centBinning = {0., 1., 5., 10., 15., 20., 25., 30., 35., 40., 45., 50., 55., 60., 65., 70., 80., 90., 100., 200.}; + AxisSpec centAxis = {centBinning, "T0M (%)"}; + AxisSpec ptAxis = {150, 0, 15, "#it{p}_{T} (GeV/#it{c})"}; + AxisSpec dcaxyAxis = {300, 0, 3, "DCA_{#it{xy}} (cm)"}; + AxisSpec dcazAxis = {500, 0, 5, "DCA_{#it{z}} (cm)"}; + AxisSpec invMassAxisK892 = {1400 / nBinsDiv, 0.6, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // K(892)0 + AxisSpec invMassAxisRho = {2000 / nBinsDiv, 0.0, 2.0, "Invariant Mass (GeV/#it{c}^2)"}; // rho + AxisSpec invMassAxisReso = {1600 / nBinsDiv, 0.9f, 2.5f, "Invariant Mass (GeV/#it{c}^2)"}; // K1 + AxisSpec pidQAAxis = {130, -6.5, 6.5}; + + // THnSparse + AxisSpec axisAnti = {BinAnti::kNAEnd, 0, BinAnti::kNAEnd, "Type of bin: Normal or Anti"}; + AxisSpec axisType = {BinType::kTYEnd, 0, BinType::kTYEnd, "Type of bin with charge and mix"}; + + // DCA QA + // Primary pion + histos.add("QA/trkppionDCAxy", "DCAxy disstribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QA/trkppionDCAz", "DCAz disstribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QA/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QA/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAcut/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAcut/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAcut/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAcut/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + // Secondary pion + histos.add("QA/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QA/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QA/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QA/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAcut/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAcut/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAcut/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAcut/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + // Kaon + histos.add("QA/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QA/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QA/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); + histos.add("QA/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QA/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAcut/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAcut/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAcut/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAcut/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAcut/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + // K1 + histos.add("QA/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); + histos.add("QA/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); + histos.add("QA/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); + histos.add("QA/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); + histos.add("QA/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); + histos.add("QA/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); + + histos.add("QAcut/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); + histos.add("QAcut/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); + histos.add("QAcut/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); + histos.add("QAcut/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); + histos.add("QAcut/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); + histos.add("QAcut/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); + + // Invariant mass + histos.add("hInvmass_K1", "Invariant mass of K1(1270) (US)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); + histos.add("hInvmass_K1_LS", "Invariant mass of K1(1270) (LS)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); + histos.add("hInvmass_K1_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTHnSparseD, {axisAnti, axisType, centAxis, ptAxis, invMassAxisReso}); + // Mass QA (quick check) + histos.add("k1invmass", "Invariant mass of K1(1270) (US)", HistType::kTH1F, {invMassAxisReso}); + histos.add("k1invmass_LS", "Invariant mass of K1(1270) (LS)", HistType::kTH1F, {invMassAxisReso}); + histos.add("k1invmass_Mix", "Invariant mass of K1(1270) (ME)", HistType::kTH1F, {invMassAxisReso}); + + // MC + if (modes.mcReco) { + AxisSpec channelAxis = {3, -0.5, 2.5, "0: non-K1, 1: rho K, 2: K* pi"}; + histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/channel", "All selected pi-pi-K combinations by truth channel", HistType::kTH1D, {channelAxis}); + histos.add("MCReco/mass", "Reconstructed mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisReso}); + histos.add("MCReco/pt", "Reconstructed pT by truth channel", HistType::kTH2D, {channelAxis, ptAxis}); + histos.add("MCReco/piPiMass", "pi-pi mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisRho}); + histos.add("MCReco/pi1KMass", "First pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); + histos.add("MCReco/pi2KMass", "Second pion-kaon mass by truth channel", HistType::kTH2D, {channelAxis, invMassAxisK892}); + histos.add("k1invmass_MC", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); + histos.add("k1invmass_MC_noK1", "Invariant mass of K1(1270)", HistType::kTH1F, {invMassAxisReso}); + + histos.add("QAMC/trkppionDCAxy", "DCAxy distribution of primary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAMC/trkppionDCAz", "DCAz distribution of primary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAMC/trkppionpT", "pT distribution of primary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAMC/trkppionTPCPID", "TPC PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkppionTOFPID", "TOF PID of primary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkppionTPCTOFPID", "TPC-TOF PID map of primary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAMC/trkspionDCAxy", "DCAxy distribution of secondary pion candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAMC/trkspionDCAz", "DCAz distribution of secondary pion candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAMC/trkspionpT", "pT distribution of secondary pion candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAMC/trkspionTPCPID", "TPC PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkspionTOFPID", "TOF PID of secondary pion candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkspionTPCTOFPID", "TPC-TOF PID map of secondary pion candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAMC/trkkaonDCAxy", "DCAxy distribution of kaon candidates", HistType::kTH1F, {dcaxyAxis}); + histos.add("QAMC/trkkaonDCAz", "DCAz distribution of kaon candidates", HistType::kTH1F, {dcazAxis}); + histos.add("QAMC/trkkaonpT", "pT distribution of kaon candidates", HistType::kTH1F, {ptAxis}); + histos.add("QAMC/trkkaonTPCPID", "TPC PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkkaonTOFPID", "TOF PID of kaon candidates", HistType::kTH2F, {ptAxis, pidQAAxis}); + histos.add("QAMC/trkkaonTPCTOFPID", "TPC-TOF PID map of kaon candidates", HistType::kTH2F, {pidQAAxis, pidQAAxis}); + + histos.add("QAMC/K1OA", "Opening angle of K1(1270)", HistType::kTH1F, {AxisSpec{100, 0, 3.14, "Opening angle"}}); + histos.add("QAMC/K1PairAsym", "Pair asymmetry of K1(1270)", HistType::kTH1F, {AxisSpec{100, -1, 1, "Pair asymmetry"}}); + histos.add("QAMC/hInvmassK892_Rho", "Invariant mass of K(892)0 vs Rho(770)", HistType::kTH2F, {invMassAxisK892, invMassAxisRho}); + histos.add("QAMC/hInvmassSecon_PiKa", "Invariant mass of secondary resonance vs pion-kaon", HistType::kTH2F, {invMassAxisRho, invMassAxisK892}); + histos.add("QAMC/hInvmassSecon", "Invariant mass of secondary resonance", HistType::kTH1F, {invMassAxisRho}); + histos.add("QAMC/hpT_Secondary", "pT distribution of secondary resonance", HistType::kTH1F, {ptAxis}); + } // mcReco + if (modes.mcGen) { + AxisSpec channelAxis = {3, -0.5, 2.5, "0: other/unresolved, 1: rho K, 2: K* pi"}; + 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}); + histos.add("MCGen/ptChannel", "Generated K1 pT by immediate decay channel", HistType::kTH2D, {channelAxis, ptAxis}); + } + } + + // Track QA of a pion; isPrimary selects the trkppion (first) or trkspion (second) histograms + template + void fillPionQA(const TrackType& track, bool isPrimary) + { + const bool hasTOF = track.hasTOF(); + if (isPrimary) { + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QA/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QA/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QA/trkppionpT"), track.pt()); + histos.fill(HIST("QA/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkppionDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAcut/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAcut/trkppionpT"), track.pt()); + histos.fill(HIST("QAcut/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkppionDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkppionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkppionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAMC/trkppionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAMC/trkppionpT"), track.pt()); + histos.fill(HIST("QAMC/trkppionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkppionDCAz"), track.dcaZ()); + } + } else { + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QA/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QA/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QA/trkspionpT"), track.pt()); + histos.fill(HIST("QA/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkspionDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAcut/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAcut/trkspionpT"), track.pt()); + histos.fill(HIST("QAcut/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkspionDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkspionTPCPID"), track.pt(), track.tpcNSigmaPi()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkspionTOFPID"), track.pt(), track.tofNSigmaPi()); + histos.fill(HIST("QAMC/trkspionTPCTOFPID"), track.tpcNSigmaPi(), track.tofNSigmaPi()); + } + histos.fill(HIST("QAMC/trkspionpT"), track.pt()); + histos.fill(HIST("QAMC/trkspionDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkspionDCAz"), track.dcaZ()); + } + } + } + + // Track QA of the bachelor kaon + template + void fillKaonQA(const TrackType& track) + { + const bool hasTOF = track.hasTOF(); + if constexpr (Folder == QAFolder::Before) { + histos.fill(HIST("QA/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QA/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QA/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QA/trkkaonpT"), track.pt()); + histos.fill(HIST("QA/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QA/trkkaonDCAz"), track.dcaZ()); + } else if constexpr (Folder == QAFolder::After) { + histos.fill(HIST("QAcut/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QAcut/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QAcut/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QAcut/trkkaonpT"), track.pt()); + histos.fill(HIST("QAcut/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QAcut/trkkaonDCAz"), track.dcaZ()); + } else { + histos.fill(HIST("QAMC/trkkaonTPCPID"), track.pt(), track.tpcNSigmaKa()); + if (hasTOF) { + histos.fill(HIST("QAMC/trkkaonTOFPID"), track.pt(), track.tofNSigmaKa()); + histos.fill(HIST("QAMC/trkkaonTPCTOFPID"), track.tpcNSigmaKa(), track.tofNSigmaKa()); + } + histos.fill(HIST("QAMC/trkkaonpT"), track.pt()); + histos.fill(HIST("QAMC/trkkaonDCAxy"), track.dcaXY()); + histos.fill(HIST("QAMC/trkkaonDCAz"), track.dcaZ()); + } + } + + // Histograms of the selected pion pairs and (pion, pion, kaon) candidates of one collision + // (or one mixed pair of collisions). The selection itself is the shared K1 core. + // dTracks1: bachelor kaons, dTracks2: pions. + template + void fillHistograms(const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2) + { + const bool fillQA = !IsMix && histogramOptions.additionalQAplots; + const auto multiplicity = collision.cent(); + + // Pion pair passing the pion selection; trk1 is the pion with the lower index + auto onPair = [&](auto const& trk1, auto const& trk2, ROOT::Math::PxPyPzMVector const& secondary, bool passesPairPt) { + if (fillQA) { + fillPionQA(trk1, true); + fillPionQA(trk2, false); + } + if (!passesPairPt) { + return; + } + if (fillQA) { + histos.fill(HIST("QA/hInvmassSecon"), secondary.M()); + } + if constexpr (IsMC) { + histos.fill(HIST("QAMC/hpT_Secondary"), secondary.Pt()); + } + }; + + // Candidate passing the pion, pair and kaon selection; pion1 is the K*0 partner in the unlike-sign case + auto onCandidate = [&](auto const& kaon, auto const& pion1, auto const& pion2, K1CandidateValues const& c) { + if (fillQA) { + fillKaonQA(kaon); + } + if (!c.inRapidity) { + return; + } + + // QA histogram before the candidate cuts + if (fillQA) { + histos.fill(HIST("QA/K1OA"), c.angle); + histos.fill(HIST("QA/K1PairAsym"), c.pairAsym); + histos.fill(HIST("QA/hInvmassK892_Rho"), c.mass13, c.secondary.M()); + histos.fill(HIST("QA/hInvmassSecon_PiKa"), c.secondary.M(), c.mass23); + histos.fill(HIST("QA/hpT_Secondary"), c.secondary.Pt()); + } + if (!c.passesCandidateCuts) { + return; + } + + // QA histograms after the candidate cuts + if (fillQA) { + fillPionQA(pion1, true); + fillPionQA(pion2, false); + fillKaonQA(kaon); + histos.fill(HIST("QAcut/K1OA"), c.angle); + histos.fill(HIST("QAcut/K1PairAsym"), c.pairAsym); + histos.fill(HIST("QAcut/hInvmassK892_Rho"), c.mass13, c.secondary.M()); + histos.fill(HIST("QAcut/hInvmassSecon_PiKa"), c.secondary.M(), c.mass23); + histos.fill(HIST("QAcut/hInvmassSecon"), c.secondary.M()); + histos.fill(HIST("QAcut/hpT_Secondary"), c.secondary.Pt()); + } + + const unsigned int typeNormal = BinAnti::kNormal; + if constexpr (IsMix) { + const unsigned int typeK1 = kaon.sign() > 0 ? BinType::kK1P_Mix : BinType::kK1N_Mix; + histos.fill(HIST("hInvmass_K1_Mix"), typeNormal, typeK1, multiplicity, c.k1.Pt(), c.k1.M()); + histos.fill(HIST("k1invmass_Mix"), c.k1.M()); + return; + } + unsigned int typeK1 = kaon.sign() > 0 ? BinType::kK1P : BinType::kK1N; + if (c.isUnlikeSign) { + histos.fill(HIST("k1invmass"), c.k1.M()); + histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, c.k1.Pt(), c.k1.M()); + } else { + histos.fill(HIST("k1invmass_LS"), c.k1.M()); + histos.fill(HIST("hInvmass_K1_LS"), typeNormal, typeK1, multiplicity, c.k1.Pt(), c.k1.M()); + } + + if constexpr (IsMC) { + const auto channel = classifyK1Truth(pion1, pion2, kaon); + const int channelBin = static_cast(channel); + histos.fill(HIST("MCReco/channel"), channelBin); + histos.fill(HIST("MCReco/mass"), channelBin, c.k1.M()); + histos.fill(HIST("MCReco/pt"), channelBin, c.k1.Pt()); + histos.fill(HIST("MCReco/piPiMass"), channelBin, c.secondary.M()); + histos.fill(HIST("MCReco/pi1KMass"), channelBin, c.mass13); + histos.fill(HIST("MCReco/pi2KMass"), channelBin, c.mass23); + if (channel == K1TruthChannel::None) { + histos.fill(HIST("k1invmass_MC_noK1"), c.k1.M()); + return; + } + if (truthDebugCounts[channelBin] < histogramOptions.cfgTruthDebug) { + ++truthDebugCounts[channelBin]; + LOGP(info, "K1Truth channel={} collision={} tracks=({},{},{}) pdg=({},{},{}) mothers=({},{},{}) motherPDG=({},{},{}) siblings=(({},{}),({},{}),({},{}))", + channelBin, collision.globalIndex(), + pion1.globalIndex(), pion2.globalIndex(), kaon.globalIndex(), + pion1.pdgCode(), pion2.pdgCode(), kaon.pdgCode(), pion1.motherId(), pion2.motherId(), kaon.motherId(), + pion1.motherPDG(), pion2.motherPDG(), kaon.motherPDG(), + pion1.siblingIds()[0], pion1.siblingIds()[1], pion2.siblingIds()[0], pion2.siblingIds()[1], kaon.siblingIds()[0], kaon.siblingIds()[1]); + } + typeK1 = kaon.sign() > 0 ? BinType::kK1P_Rec : BinType::kK1N_Rec; + histos.fill(HIST("hInvmass_K1"), typeNormal, typeK1, multiplicity, c.k1.Pt(), c.k1.M()); + histos.fill(HIST("k1invmass_MC"), c.k1.M()); + histos.fill(HIST("QAMC/K1OA"), c.angle); + histos.fill(HIST("QAMC/K1PairAsym"), c.pairAsym); + histos.fill(HIST("QAMC/hInvmassK892_Rho"), c.mass13, c.secondary.M()); + histos.fill(HIST("QAMC/hInvmassSecon_PiKa"), c.secondary.M(), c.mass23); + histos.fill(HIST("QAMC/hInvmassSecon"), c.secondary.M()); + histos.fill(HIST("QAMC/hpT_Secondary"), c.secondary.Pt()); + + // PID QA primary and secondary pion + fillPionQA(pion1, true); + fillPionQA(pion2, false); + fillKaonQA(kaon); + } + }; + + core.forEachCandidate(histos, collision, dTracks1, dTracks2, IsMC || fillQA, onPair, onCandidate); + } + void processResoTracks(ResoCollisions::iterator const& collision, ResoTracks const& resotracks) { if (!core.passesEventCuts(collision)) { return; } - core.fillHistograms(histos, collision, resotracks, resotracks); + fillHistograms(collision, resotracks, resotracks); } PROCESS_SWITCH(K1AnalysisMicro, processResoTracks, "Process ResoTracks", false); @@ -107,7 +510,7 @@ struct K1AnalysisMicro { if (!core.passesEventCuts(collision)) { return; } - core.fillHistograms(histos, collision, resomicrotracks, resomicrotracks); + fillHistograms(collision, resomicrotracks, resomicrotracks); } PROCESS_SWITCH(K1AnalysisMicro, processResoMicroTracks, "Process ResoMicroTracks", true); @@ -118,7 +521,7 @@ struct K1AnalysisMicro { return; } histos.fill(HIST("MCReco/collisions"), 0.5); - core.fillHistograms(histos, collision, resotracks, resotracks); + fillHistograms(collision, resotracks, resotracks); } PROCESS_SWITCH(K1AnalysisMicro, processMC, "Process Event for MC", false); @@ -131,7 +534,7 @@ struct K1AnalysisMicro { } histos.fill(HIST("MCReco/collisions"), 0.5); histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size()); - core.fillHistograms(histos, collision, tracks, tracks); + fillHistograms(collision, tracks, tracks); } PROCESS_SWITCH(K1AnalysisMicro, processMCMicro, "Process reconstructed MC with micro v001 tables", false); @@ -140,7 +543,12 @@ struct K1AnalysisMicro { if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { return; } - core.fillGenerated(histos, resoParents); + // Keep other/unresolved immediate decays too; never require both pairs. + core.forEachGeneratedK1(histos, resoParents, [&](auto const& part, K1TruthChannel channel) { + const int charge = part.pdgCode() > 0 ? 1 : -1; + histos.fill(HIST("MCGen/chargeChannel"), charge, static_cast(channel)); + histos.fill(HIST("MCGen/ptChannel"), static_cast(channel), part.pt()); + }); } PROCESS_SWITCH(K1AnalysisMicro, processMCTrue, "Process generated K1 in selected events with v001 parents", false); @@ -156,7 +564,7 @@ struct K1AnalysisMicro { if (!core.passesEventCuts(collision1) || !core.passesEventCuts(collision2)) { continue; } - core.fillHistograms(histos, collision1, tracks1, tracks2); + fillHistograms(collision1, tracks1, tracks2); } }; PROCESS_SWITCH(K1AnalysisMicro, processME, "Process EventMixing light without partition", false); @@ -172,7 +580,7 @@ struct K1AnalysisMicro { if (!core.passesEventCuts(collision1) || !core.passesEventCuts(collision2)) { continue; } - core.fillHistograms(histos, collision1, tracks1, tracks2); + fillHistograms(collision1, tracks1, tracks2); } }; PROCESS_SWITCH(K1AnalysisMicro, processMEMicro, "Process EventMixing light without partition", false); diff --git a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx index 9fa1c2834f2..ef5878dc81f 100644 --- a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx +++ b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx @@ -16,6 +16,7 @@ #include "PWGLF/Core/K1AnalysisMicroCore.h" #include "PWGLF/Core/K1MlFeatures.h" +#include "PWGLF/Core/ResoAnalysisSelectionCore.h" #include "PWGLF/DataModel/LFK1MlTables.h" #include "PWGLF/DataModel/LFResonanceTables.h" @@ -48,6 +49,7 @@ using namespace o2; using namespace o2::framework; using namespace o2::constants::physics; +using namespace o2::analysis::resonance; using namespace o2::analysis::k1micro; static_assert(std::extent_v == o2::analysis::k1ml::NMasterFeatures, @@ -85,7 +87,6 @@ struct K1TrainingTable { KaonPidCuts kaonPID; SecondaryCuts secondaryCuts; CandidateCuts candidateCuts; - HistogramOptions histogramOptions; Configurable k1MlExportStage{"k1MlExportStage", "loose", "Candidate export stage: loose (pass bits >= 1) or selected (pass bits = 31)"}; Configurable k1MlLooseAudit{"k1MlLooseAudit", true, "Record loose US cutflow and mass/pT/activity spectrum"}; @@ -117,7 +118,11 @@ struct K1TrainingTable { LooseStageOptions looseOptions; looseOptions.audit = k1MlLooseAudit; looseOptions.exportSelected = k1MlExportStage.value == "selected"; - core.init(histos, eventCuts, trackCuts, pionPID, kaonPID, secondaryCuts, candidateCuts, histogramOptions, modes, looseOptions); + core.init(histos, eventCuts, trackCuts, pionPID, kaonPID, secondaryCuts, candidateCuts, modes, looseOptions); + if (doprocessMCMicro) { + histos.add("MCReco/collisions", "Selected reconstructed MC collisions", HistType::kTH1D, {{1, 0, 1}}); + histos.add("MCReco/microTracks", "Input micro tracks in selected MC collisions", HistType::kTH1D, {{1, 0, 1}}); + } // Candidates that violate the canonical or feature contract are skipped, never written. auto skipped = histos.add("ML/exportSkipped", "Skipped candidates;K1 ML build status;candidates", HistType::kTH1D, {{6, -0.5, 5.5}}); @@ -229,11 +234,12 @@ struct K1TrainingTable { return; } writeK1MlEvent(collision); - core.fillHistograms(histos, collision, tracks, tracks, - [this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion, - K1TruthChannel channel, uint16_t passBits) { - writeK1MlCandidate(coll, kaon, samePion, oppPion, channel, passBits); - }); + // Selection only: the K1 analysis histograms belong to the K1 analysis task + core.forEachCandidate(histos, collision, tracks, tracks, false, nullptr, nullptr, + [this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion, + K1TruthChannel channel, uint16_t passBits) { + writeK1MlCandidate(coll, kaon, samePion, oppPion, channel, passBits); + }); } PROCESS_SWITCH(K1TrainingTable, processResoMicroTracks, "Write K1 candidates from data micro v001 tables", true); @@ -246,11 +252,11 @@ struct K1TrainingTable { histos.fill(HIST("MCReco/collisions"), 0.5); histos.fill(HIST("MCReco/microTracks"), 0.5, tracks.size()); writeK1MlEvent(collision); - core.fillHistograms(histos, collision, tracks, tracks, - [this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion, - K1TruthChannel channel, uint16_t passBits) { - writeK1MlCandidate(coll, kaon, samePion, oppPion, channel, passBits); - }); + core.forEachCandidate(histos, collision, tracks, tracks, false, nullptr, nullptr, + [this](auto const& coll, auto const& kaon, auto const& samePion, auto const& oppPion, + K1TruthChannel channel, uint16_t passBits) { + writeK1MlCandidate(coll, kaon, samePion, oppPion, channel, passBits); + }); } PROCESS_SWITCH(K1TrainingTable, processMCMicro, "Write K1 candidates with truth from reconstructed MC micro v001 tables", false); @@ -259,7 +265,7 @@ struct K1TrainingTable { if (!core.passesEventCuts(collision) || !core.passesMCEventCuts(collision)) { return; } - core.fillGenerated(histos, resoParents, [&](auto const& part, K1TruthChannel channel) { + core.forEachGeneratedK1(histos, resoParents, [&](auto const& part, K1TruthChannel channel) { k1MlGenAudit(static_cast(collision.globalIndex()), static_cast(part.originalMcParticleId()), part.pdgCode(), part.daughterPDG1(), part.daughterPDG2(), static_cast(channel), part.pt(), part.y(), true); From 0ac4b3f14592cd5c429bb7e87b52a476cda276b8 Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Sun, 4 Oct 2026 23:29:05 +0900 Subject: [PATCH 8/9] [PWGLF] Fix the copyright notice of the K1 ML files --- PWGLF/Core/K1MlFeatures.h | 2 +- PWGLF/DataModel/LFK1MlTables.h | 2 +- PWGLF/Tasks/Resonances/k1TrainingTable.cxx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PWGLF/Core/K1MlFeatures.h b/PWGLF/Core/K1MlFeatures.h index 2ca0b7c3770..93436e39249 100644 --- a/PWGLF/Core/K1MlFeatures.h +++ b/PWGLF/Core/K1MlFeatures.h @@ -3,7 +3,7 @@ // All rights not expressly granted are reserved. // // This software is distributed under the terms of the GNU General Public -// License version 3, copied verbatim in the file "COPYING". +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". // // In applying this license CERN does not waive the privileges and immunities // granted to it by virtue of its status as an Intergovernmental Organization diff --git a/PWGLF/DataModel/LFK1MlTables.h b/PWGLF/DataModel/LFK1MlTables.h index 44708c47dbf..892d2adefe0 100644 --- a/PWGLF/DataModel/LFK1MlTables.h +++ b/PWGLF/DataModel/LFK1MlTables.h @@ -3,7 +3,7 @@ // All rights not expressly granted are reserved. // // This software is distributed under the terms of the GNU General Public -// License version 3, copied verbatim in the file "COPYING". +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". // // In applying this license CERN does not waive the privileges and immunities // granted to it by virtue of its status as an Intergovernmental Organization diff --git a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx index ef5878dc81f..11c6a8bf5d7 100644 --- a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx +++ b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx @@ -3,7 +3,7 @@ // All rights not expressly granted are reserved. // // This software is distributed under the terms of the GNU General Public -// License version 3, copied verbatim in the file "COPYING". +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". // // In applying this license CERN does not waive the privileges and immunities // granted to it by virtue of its status as an Intergovernmental Organization From f3564ff82b6675e028ca77c371dbb68e189c5f25 Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Mon, 5 Oct 2026 00:54:49 +0900 Subject: [PATCH 9/9] [PWGLF] Fix clang-tidy findings in the K1 micro analysis and ML exporter Include Math/Vector4Dfwd.h where ROOT::Math::PxPyPzMVector is used, drop the unused LFResonanceTables.h and PhysicsConstants.h includes, simplify the candidate-cut boolean expressions, use designated initializers for EncodedValue and remove redundant int64_t casts. No change in behaviour. --- PWGLF/Core/K1AnalysisMicroCore.h | 14 +++++++------- PWGLF/Core/K1MlFeatures.h | 12 ++++++------ PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx | 2 +- PWGLF/Tasks/Resonances/k1TrainingTable.cxx | 13 +++++++------ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/PWGLF/Core/K1AnalysisMicroCore.h b/PWGLF/Core/K1AnalysisMicroCore.h index e1ec22624e7..4e2cd0e720c 100644 --- a/PWGLF/Core/K1AnalysisMicroCore.h +++ b/PWGLF/Core/K1AnalysisMicroCore.h @@ -21,7 +21,6 @@ #include "PWGLF/Core/K1MlFeatures.h" #include "PWGLF/Core/ResoAnalysisSelectionCore.h" -#include "PWGLF/DataModel/LFResonanceTables.h" #include #include @@ -32,6 +31,7 @@ #include #include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) +#include #include #include #include @@ -521,7 +521,7 @@ class K1AnalysisMicroCore } // Stage L common acceptance uses the existing inclusive rapidity window. - values.inRapidity = !(lResonanceK1.Rapidity() > mCandidateCuts.cK1MaxRap || lResonanceK1.Rapidity() < mCandidateCuts.cK1MinRap); + values.inRapidity = lResonanceK1.Rapidity() >= mCandidateCuts.cK1MinRap && lResonanceK1.Rapidity() <= mCandidateCuts.cK1MaxRap; if (!values.inRapidity) { if constexpr (HasCandidateHook) { if (tripletSelected) { @@ -553,11 +553,11 @@ class K1AnalysisMicroCore // Candidate cuts (each one is evaluated only if switched on) values.passesCandidateCuts = - !(isK892Mode && mSecondaryWindowOn && (!isInWindow(values.mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) || pion1.sign() == bTrack.sign())) && - !(mAnotherMassCutOn && !isInRange(isK892Mode ? lResonanceSecondary.M() : values.mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) && - !(mPiKaMassCutOn && !isInRange(values.mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) && - !(mAngleCutOn && !isInRange(values.angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) && - !(mPairAsymCutOn && !isInRange(values.pairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)); + (!isK892Mode || !mSecondaryWindowOn || (isInWindow(values.mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts.cSecondaryMasswindow) && pion1.sign() != bTrack.sign())) && + (!mAnotherMassCutOn || isInRange(isK892Mode ? lResonanceSecondary.M() : values.mass13, mSecondaryCuts.cMinAnotherSecondaryMassCut, mSecondaryCuts.cMaxAnotherSecondaryMassCut)) && + (!mPiKaMassCutOn || isInRange(values.mass23, mSecondaryCuts.cMinPiKaMassCut, mSecondaryCuts.cMaxPiKaMassCut)) && + (!mAngleCutOn || isInRange(values.angle, mSecondaryCuts.cMinAngle, mSecondaryCuts.cMaxAngle)) && + (!mPairAsymCutOn || isInRange(values.pairAsym, mSecondaryCuts.cMinPairAsym, mSecondaryCuts.cMaxPairAsym)); auto exportCandidate = [&](uint16_t passBits) { if constexpr (IsResoMicrotrack && !IsMix && HasExportHook) { onExport(collision, bTrack, pion2, pion1, flowChannel, passBits); diff --git a/PWGLF/Core/K1MlFeatures.h b/PWGLF/Core/K1MlFeatures.h index 93436e39249..262fb0feff7 100644 --- a/PWGLF/Core/K1MlFeatures.h +++ b/PWGLF/Core/K1MlFeatures.h @@ -169,21 +169,21 @@ struct EncodedValue { inline EncodedValue encodePID(float decoded) { if (std::isnan(decoded)) { - return {0.f, 0.f, 0.f}; + return {.value = 0.f, .valid = 0.f, .overflow = 0.f}; } if (std::isinf(decoded)) { - return {std::signbit(decoded) ? -3.5f : 3.5f, 1.f, 1.f}; + return {.value = std::signbit(decoded) ? -3.5f : 3.5f, .valid = 1.f, .overflow = 1.f}; } - return {decoded, 1.f, 0.f}; + return {.value = decoded, .valid = 1.f, .overflow = 0.f}; } inline EncodedValue encodeDCA(float decoded) { if (!std::isfinite(decoded)) { - return {0.f, 0.f, 0.f}; + return {.value = 0.f, .valid = 0.f, .overflow = 0.f}; } const bool overflow = decoded == o2::aod::resomicrodaughter001::DCAEncoding::MaxDCA; - return {decoded, 1.f, overflow ? 1.f : 0.f}; + return {.value = decoded, .valid = 1.f, .overflow = overflow ? 1.f : 0.f}; } struct Kinematics { @@ -798,7 +798,7 @@ inline FeaturePack buildMasterFeatures(CandidateSnapshot const& candidate) detail::append(pack.master, index, detail::encodePID(decoded)); } for (const float& decoded : track.tofNSigma) { - detail::append(pack.master, index, track.hasTOF ? detail::encodePID(decoded) : detail::EncodedValue{0.f, 0.f, 0.f}); + detail::append(pack.master, index, track.hasTOF ? detail::encodePID(decoded) : detail::EncodedValue{.value = 0.f, .valid = 0.f, .overflow = 0.f}); } detail::append(pack.master, index, detail::encodeDCA(track.dcaXY)); detail::append(pack.master, index, detail::encodeDCA(track.dcaZ)); diff --git a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx index c108e5df193..9d6877ca7e5 100644 --- a/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx +++ b/PWGLF/Tasks/Resonances/k1AnalysisMicro.cxx @@ -18,7 +18,6 @@ #include "PWGLF/Core/ResoAnalysisSelectionCore.h" #include "PWGLF/DataModel/LFResonanceTables.h" -#include #include #include #include @@ -34,6 +33,7 @@ #include #include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) +#include #include #include diff --git a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx index 11c6a8bf5d7..8b070a97be1 100644 --- a/PWGLF/Tasks/Resonances/k1TrainingTable.cxx +++ b/PWGLF/Tasks/Resonances/k1TrainingTable.cxx @@ -34,6 +34,7 @@ #include #include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) +#include #include #include @@ -145,7 +146,7 @@ struct K1TrainingTable { k1MlTracks(k1MlEventRow, static_cast(track.trackId()), track.px(), track.py(), track.pz(), track.pidNSigmaPiFlag(), track.pidNSigmaKaFlag(), track.pidNSigmaPrFlag(), track.trackSelectionFlags(), track.trackFlags(), track.tpcNClsCrossedRows(), track.itsClusterMap()); - const auto row = static_cast(k1MlTracks.lastIndex()); + const int64_t row = k1MlTracks.lastIndex(); k1MlTrackRows.emplace(id, row); return row; } @@ -155,9 +156,9 @@ struct K1TrainingTable { { k1MlTrackRows.clear(); // ResoCollisions_001 carries no run number or BC; the reduced collision row identifies the event within its DF. - k1MlEvents(static_cast(collision.globalIndex()), + k1MlEvents(collision.globalIndex(), collision.posZ(), collision.bMagField(), collision.cent(), collision.multiplicity(), collision.isRecINELgt0()); - k1MlEventRow = static_cast(k1MlEvents.lastIndex()); + k1MlEventRow = k1MlEvents.lastIndex(); } template @@ -168,7 +169,7 @@ struct K1TrainingTable { } uint64_t hash = FnvOffsetBasis; for (const auto& value : pack.master) { - const uint32_t bits = std::bit_cast(value); + const auto bits = std::bit_cast(value); for (unsigned int shift = 0; shift < BitsPerFloat; shift += BitsPerByte) { hash = (hash ^ ((bits >> shift) & ByteMask)) * FnvPrime; } @@ -209,7 +210,7 @@ struct K1TrainingTable { static_cast(mother.Pt()), static_cast(mother.Rapidity()), static_cast(mother.Eta()), static_cast(mother.Phi()), static_cast(kaon.sign()), passBits); - const auto row = static_cast(k1MlCandidates.lastIndex()); + const int64_t row = k1MlCandidates.lastIndex(); k1MlInputs(row, pack.master.data(), static_cast(pack.status)); if constexpr (IsMC) { const bool matched = channel != K1TruthChannel::None; @@ -266,7 +267,7 @@ struct K1TrainingTable { return; } core.forEachGeneratedK1(histos, resoParents, [&](auto const& part, K1TruthChannel channel) { - k1MlGenAudit(static_cast(collision.globalIndex()), static_cast(part.originalMcParticleId()), + k1MlGenAudit(collision.globalIndex(), static_cast(part.originalMcParticleId()), part.pdgCode(), part.daughterPDG1(), part.daughterPDG2(), static_cast(channel), part.pt(), part.y(), true); });