1717#ifndef PWGLF_CORE_K1ANALYSISMICROCORE_H_
1818#define PWGLF_CORE_K1ANALYSISMICROCORE_H_
1919
20+ #include " PWGLF/Core/K1MlFeatures.h"
2021#include " PWGLF/DataModel/LFResonanceTables.h"
2122
2223#include < CommonConstants/MathConstants.h>
4041#include < cstdint>
4142#include < limits>
4243#include < set>
44+ #include < type_traits>
4345#include < vector>
4446
4547namespace o2 ::analysis::k1micro
@@ -100,6 +102,16 @@ enum class QAFolder {
100102 MC // QAMC/*: matched K1 truth candidates
101103};
102104
105+ // Cumulative selection bits of an unlike-sign candidate handed to the candidate callback.
106+ enum CandidatePassBit : uint16_t {
107+ kPassLoose = 1 , // valid canonical candidate inside the K1 rapidity window
108+ kPassQuality = 2 , // track quality of all three tracks
109+ kPassPID = 4 , // TOF requirement and PID of all three tracks
110+ kPassPair = 8 , // pion-pair pT and secondary mass window
111+ kPassCandidate = 16 // candidate cuts
112+ };
113+ inline constexpr uint16_t PassBitsSelected = kPassLoose | kPassQuality | kPassPID | kPassPair | kPassCandidate ;
114+
103115// Resolved PID cut of one species at a given pT.
104116struct PIDCut {
105117 double tpcMax = 0 .;
@@ -221,6 +233,13 @@ struct ProcessModes {
221233 bool mcGen = false ; // generated K1 parents in selected reconstructed events
222234};
223235
236+ // / Loose-stage traversal of unlike-sign micro candidates for the candidate callback.
237+ // / With the defaults and without a callback, the candidate loop applies only the conventional selection.
238+ struct LooseStageOptions {
239+ bool audit = false ; // fill ML/looseCutflow and ML/looseMassPtActivity
240+ bool exportSelected = false ; // hand candidates to the callback at the selected stage instead of the loose stage
241+ };
242+
224243// A cut is on unless it carries the disabled value (tolerant to the float parsing of the JSON value).
225244inline bool isCutEnabled (float value)
226245{
@@ -453,7 +472,8 @@ class K1AnalysisMicroCore
453472 EventCuts const & eventCuts, TrackCuts const & trackCuts,
454473 PionPidCuts const & pionPidCuts, KaonPidCuts const & kaonPidCuts,
455474 SecondaryCuts const & secondaryCuts, CandidateCuts const & candidateCuts,
456- HistogramOptions const & histogramOptions, ProcessModes const & modes)
475+ HistogramOptions const & histogramOptions, ProcessModes const & modes,
476+ LooseStageOptions const & looseOptions = {})
457477 {
458478 mEventCuts = eventCuts;
459479 mTrackCuts = trackCuts;
@@ -462,6 +482,7 @@ class K1AnalysisMicroCore
462482 mSecondaryCuts = secondaryCuts;
463483 mCandidateCuts = candidateCuts;
464484 mHistogramOptions = histogramOptions;
485+ mLooseOptions = looseOptions;
465486 mTruthDebugCounts = {};
466487
467488 mSecondaryWindowOn = isCutEnabled (mSecondaryCuts .cSecondaryMasswindow );
@@ -707,13 +728,16 @@ class K1AnalysisMicroCore
707728 }
708729
709730 // Unordered (pion, pion, kaon) candidate loop of one collision (or one mixed pair of collisions).
710- // dTracks1: bachelor kaons, dTracks2: pions.
711- template <bool IsMC, bool IsMix, bool IsResoMicrotrack, typename CollisionType, typename TracksType>
712- void fillHistograms (o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2)
731+ // dTracks1: bachelor kaons, dTracks2: pions. The optional callback receives the unlike-sign micro
732+ // same-event candidates in the canonical roles (collision, kaon, same-sign pion, opposite-sign pion,
733+ // truth channel, pass bits) at the loose or selected stage configured by LooseStageOptions.
734+ template <bool IsMC, bool IsMix, bool IsResoMicrotrack, typename CollisionType, typename TracksType, typename Callback = std::nullptr_t >
735+ void fillHistograms (o2::framework::HistogramRegistry& histos, const CollisionType& collision, const TracksType& dTracks1, const TracksType& dTracks2, Callback callback = nullptr )
713736 {
714737 if (dTracks1.size () == 0 || dTracks2.size () == 0 ) {
715738 return ;
716739 }
740+ constexpr bool HasCallback = !std::is_same_v<Callback, std::nullptr_t >;
717741 // Sets are local to this reconstructed collision: IDs cannot leak across DFs.
718742 // Source-file/DF deduplication across split collisions belongs in the audit.
719743 std::array<std::set<int >, NTruthChannels> matchedMothers;
@@ -726,6 +750,24 @@ class K1AnalysisMicroCore
726750 const auto kaonSelected = buildSelectionCache<IsResoMicrotrack, Species::Kaon, FillCutFlow>(histos, dTracks1, firstKaonIndex);
727751 const auto pionSelected = buildSelectionCache<IsResoMicrotrack, Species::Pion, FillCutFlow>(histos, dTracks2, firstPionIndex);
728752
753+ // Only micro same-event ML work needs traversal before conventional cuts.
754+ // The canonical-candidate validity is also required before a selected-stage callback.
755+ bool visitLoose = false ;
756+ bool checkValidity = false ;
757+ if constexpr (IsResoMicrotrack && !IsMix) {
758+ visitLoose = mLooseOptions .audit || (!mLooseOptions .exportSelected && HasCallback);
759+ checkValidity = visitLoose || (mLooseOptions .exportSelected && HasCallback);
760+ }
761+ std::vector<uint8_t > kaonQuality (dTracks1.size (), 0 ), pionQuality (dTracks2.size (), 0 );
762+ if (visitLoose) {
763+ for (const auto & track : dTracks1) {
764+ kaonQuality[getCacheIndex (track, firstKaonIndex, kaonQuality.size ())] = trackQualityStage<IsResoMicrotrack>(track) == kTrkClusters ;
765+ }
766+ for (const auto & track : dTracks2) {
767+ pionQuality[getCacheIndex (track, firstPionIndex, pionQuality.size ())] = trackQualityStage<IsResoMicrotrack>(track) == kTrkClusters ;
768+ }
769+ }
770+
729771 // Values needed only by switched-on cuts or QA are computed only then
730772 const bool isK892Mode = mSecondaryCuts .cfgModeK892orRho ;
731773 const bool fillQA = !IsMix && mHistogramOptions .additionalQAplots ;
@@ -745,7 +787,7 @@ class K1AnalysisMicroCore
745787 const bool pionsSelected = pionSelected[getCacheIndex (trk1, firstPionIndex, pionSelected.size ())] && pionSelected[getCacheIndex (trk2, firstPionIndex, pionSelected.size ())];
746788 bool pairPt = false ;
747789 bool rhoWindow = true ;
748- if (pionsSelected) {
790+ if (pionsSelected || visitLoose ) {
749791 // Resonance reconstruction
750792 lDecayDaughter1.SetCoordinates (trk1.px (), trk1.py (), trk1.pz (), o2::constants::physics::MassPionCharged);
751793 lDecayDaughter2.SetCoordinates (trk2.px (), trk2.py (), trk2.pz (), o2::constants::physics::MassPionCharged);
@@ -777,27 +819,29 @@ class K1AnalysisMicroCore
777819 }
778820 }
779821 }
780- if (!pionsSelected) {
822+ if (!pionsSelected && !visitLoose ) {
781823 continue ;
782824 }
783825
784- if (fillQA) {
826+ if (fillQA && pionsSelected ) {
785827 fillPionQA<QAFolder::Before>(histos, trk1, true );
786828 fillPionQA<QAFolder::Before>(histos, trk2, false );
787829 }
788830
789- if (!pairPt) {
831+ if (!pairPt && !visitLoose ) {
790832 continue ;
791833 }
792834
793- if (fillQA) {
835+ if (fillQA && pionsSelected && pairPt ) {
794836 histos.fill (HIST (" QA/hInvmassSecon" ), lResonanceSecondary.M ());
795837 }
796838 if constexpr (IsMC) {
797- histos.fill (HIST (" QAMC/hpT_Secondary" ), lResonanceSecondary.Pt ());
839+ if (pionsSelected && pairPt) {
840+ histos.fill (HIST (" QAMC/hpT_Secondary" ), lResonanceSecondary.Pt ());
841+ }
798842 }
799843 // Secondary mass window (rho mode): the bachelor loop is skipped for rejected pairs
800- if (!rhoWindow) {
844+ if (!rhoWindow && !visitLoose ) {
801845 continue ;
802846 }
803847
@@ -819,13 +863,19 @@ class K1AnalysisMicroCore
819863 }
820864 }
821865 };
822- countCandidate (6 );
823- if (!kaonSelected[getCacheIndex (bTrack, firstKaonIndex, kaonSelected.size ())]) {
866+ const bool pairSelected = pionsSelected && pairPt && rhoWindow;
867+ const bool bachelorSelected = kaonSelected[getCacheIndex (bTrack, firstKaonIndex, kaonSelected.size ())];
868+ if (pairSelected) {
869+ countCandidate (6 );
870+ }
871+ if ((!pairSelected || !bachelorSelected) && !visitLoose) {
824872 continue ;
825873 }
826- countCandidate (7 );
874+ if (pairSelected && bachelorSelected) {
875+ countCandidate (7 );
876+ }
827877
828- if (fillQA) {
878+ if (fillQA && pairSelected && bachelorSelected ) {
829879 fillKaonQA<QAFolder::Before>(histos, bTrack);
830880 }
831881
@@ -843,11 +893,37 @@ class K1AnalysisMicroCore
843893 lDecayDaughter_bach.SetCoordinates (bTrack.px (), bTrack.py (), bTrack.pz (), o2::constants::physics::MassKaonCharged);
844894 lResonanceK1 = lResonanceSecondary + lDecayDaughter_bach;
845895
846- // Cuts
896+ auto countMl = [&](int stage) {
897+ if (mLooseOptions .audit && isUnlikeSign) {
898+ histos.fill (HIST (" ML/looseCutflow" ), stage, 0 );
899+ if (flowChannel != K1TruthChannel::None) {
900+ const int stratum = 2 * static_cast <int >(flowChannel) - (bTrack.sign () > 0 ? 1 : 0 );
901+ histos.fill (HIST (" ML/looseCutflow" ), stage, stratum);
902+ }
903+ }
904+ };
905+ bool validLoose = false ;
906+ if constexpr (IsResoMicrotrack && !IsMix) {
907+ if (checkValidity && isUnlikeSign) {
908+ const auto canonical = o2::analysis::k1ml::canonicalizeUS (o2::analysis::k1ml::makeTrackSnapshot (bTrack), o2::analysis::k1ml::makeTrackSnapshot (pion2), o2::analysis::k1ml::makeTrackSnapshot (pion1));
909+ validLoose = canonical.status == o2::analysis::k1ml::BuildStatus::Ok &&
910+ std::isfinite (lResonanceK1.M ()) && std::isfinite (lResonanceK1.Pt ()) &&
911+ std::isfinite (lResonanceK1.Rapidity ()) && std::isfinite (lResonanceK1.Eta ()) &&
912+ std::isfinite (lResonanceK1.Phi ()) &&
913+ o2::analysis::k1ml::buildMasterFeatures (canonical.candidate ).status == o2::analysis::k1ml::BuildStatus::Ok;
914+ if (validLoose) {
915+ countMl (0 );
916+ }
917+ }
918+ }
919+
920+ // Stage L common acceptance uses the existing inclusive rapidity window.
847921 if (lResonanceK1.Rapidity () > mCandidateCuts .cK1MaxRap || lResonanceK1.Rapidity () < mCandidateCuts .cK1MinRap ) {
848922 continue ;
849923 }
850- countCandidate (8 );
924+ if (pairSelected && bachelorSelected) {
925+ countCandidate (8 );
926+ }
851927
852928 double mass13 = 0 .;
853929 double mass23 = 0 .;
@@ -870,6 +946,54 @@ class K1AnalysisMicroCore
870946 : (lResonanceSecondary.E () - lDecayDaughter_bach.E ()) / (lResonanceSecondary.E () + lDecayDaughter_bach.E ());
871947 }
872948
949+ // Candidate cuts (each one is evaluated only if switched on)
950+ const bool candidateCutsPass =
951+ !(isK892Mode && mSecondaryWindowOn && (!isInWindow (mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts .cSecondaryMasswindow ) || pion1.sign () == bTrack.sign ())) &&
952+ !(mAnotherMassCutOn && !isInRange (isK892Mode ? lResonanceSecondary.M () : mass13, mSecondaryCuts .cMinAnotherSecondaryMassCut , mSecondaryCuts .cMaxAnotherSecondaryMassCut )) &&
953+ !(mPiKaMassCutOn && !isInRange (mass23, mSecondaryCuts .cMinPiKaMassCut , mSecondaryCuts .cMaxPiKaMassCut )) &&
954+ !(mAngleCutOn && !isInRange (lK1Angle, mSecondaryCuts .cMinAngle , mSecondaryCuts .cMaxAngle )) &&
955+ !(mPairAsymCutOn && !isInRange (lPairAsym, mSecondaryCuts .cMinPairAsym , mSecondaryCuts .cMaxPairAsym ));
956+ auto emitCandidate = [&](uint16_t passBits) {
957+ if constexpr (IsResoMicrotrack && !IsMix && HasCallback) {
958+ callback (collision, bTrack, pion2, pion1, flowChannel, passBits);
959+ } else {
960+ static_cast <void >(passBits);
961+ }
962+ };
963+ if (visitLoose && validLoose) {
964+ countMl (1 );
965+ if (mLooseOptions .audit ) {
966+ histos.fill (HIST (" ML/looseMassPtActivity" ), lResonanceK1.M (), lResonanceK1.Pt (), collision.cent ());
967+ }
968+ const bool qualityPass = kaonQuality[getCacheIndex (bTrack, firstKaonIndex, kaonQuality.size ())] &&
969+ pionQuality[getCacheIndex (trk1, firstPionIndex, pionQuality.size ())] &&
970+ pionQuality[getCacheIndex (trk2, firstPionIndex, pionQuality.size ())];
971+ uint16_t passBits = kPassLoose ;
972+ if (qualityPass) {
973+ passBits |= kPassQuality ;
974+ countMl (2 );
975+ if (pionsSelected && bachelorSelected) {
976+ passBits |= kPassPID ;
977+ countMl (3 );
978+ if (pairPt && rhoWindow) {
979+ passBits |= kPassPair ;
980+ countMl (4 );
981+ if (candidateCutsPass) {
982+ passBits |= kPassCandidate ;
983+ countMl (5 );
984+ }
985+ }
986+ }
987+ }
988+ if (!mLooseOptions .exportSelected ) {
989+ emitCandidate (passBits);
990+ }
991+ }
992+ // Stage C retains the frozen conventional selections and QA population.
993+ if (!pairSelected || !bachelorSelected) {
994+ continue ;
995+ }
996+
873997 // QA histogram before the candidate cuts
874998 if (fillQA) {
875999 histos.fill (HIST (" QA/K1OA" ), lK1Angle);
@@ -879,20 +1003,7 @@ class K1AnalysisMicroCore
8791003 histos.fill (HIST (" QA/hpT_Secondary" ), lResonanceSecondary.Pt ());
8801004 }
8811005
882- // Candidate cuts (each one is evaluated only if switched on)
883- if (isK892Mode && mSecondaryWindowOn && (!isInWindow (mass13, o2::constants::physics::MassK0Star892, mSecondaryCuts .cSecondaryMasswindow ) || pion1.sign () == bTrack.sign ())) {
884- continue ;
885- }
886- if (mAnotherMassCutOn && !isInRange (isK892Mode ? lResonanceSecondary.M () : mass13, mSecondaryCuts .cMinAnotherSecondaryMassCut , mSecondaryCuts .cMaxAnotherSecondaryMassCut )) {
887- continue ;
888- }
889- if (mPiKaMassCutOn && !isInRange (mass23, mSecondaryCuts .cMinPiKaMassCut , mSecondaryCuts .cMaxPiKaMassCut )) {
890- continue ;
891- }
892- if (mAngleCutOn && !isInRange (lK1Angle, mSecondaryCuts .cMinAngle , mSecondaryCuts .cMaxAngle )) {
893- continue ;
894- }
895- if (mPairAsymCutOn && !isInRange (lPairAsym, mSecondaryCuts .cMinPairAsym , mSecondaryCuts .cMaxPairAsym )) {
1006+ if (!candidateCutsPass) {
8961007 continue ;
8971008 }
8981009 countCandidate (9 );
@@ -911,6 +1022,9 @@ class K1AnalysisMicroCore
9111022 }
9121023
9131024 countCandidate (isUnlikeSign ? 10 : 11 );
1025+ if (isUnlikeSign && mLooseOptions .exportSelected && validLoose) {
1026+ emitCandidate (PassBitsSelected);
1027+ }
9141028 if constexpr (IsMC && IsResoMicrotrack && !IsMix) {
9151029 if (flowChannel != K1TruthChannel::None) {
9161030 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
9791093 }
9801094 } // fillHistograms
9811095
982- // Generated K1 parents of a selected reconstructed MC collision.
1096+ // Generated K1 parents of a selected reconstructed MC collision. The optional callback receives
1097+ // (parent, immediate channel) for the parents inside the K1 rapidity window.
9831098 // Parents belong to selected reconstructed events; split reco collisions
9841099 // repeat parent sets. This is not an unconditional generated denominator.
985- template <typename ParentsType>
986- void fillGenerated (o2::framework::HistogramRegistry& histos, const ParentsType& resoParents)
1100+ template <typename ParentsType, typename Callback = std:: nullptr_t >
1101+ void fillGenerated (o2::framework::HistogramRegistry& histos, const ParentsType& resoParents, Callback callback = nullptr )
9871102 {
9881103 for (const auto & part : resoParents) {
9891104 if (std::abs (part.pdgCode ()) != o2::constants::physics::Pdg::kK1_1270Plus ) {
@@ -999,6 +1114,9 @@ class K1AnalysisMicroCore
9991114 // Keep other/unresolved immediate decays too; never require both pairs.
10001115 histos.fill (HIST (" MCGen/chargeChannel" ), charge, static_cast <int >(channel));
10011116 histos.fill (HIST (" MCGen/ptChannel" ), static_cast <int >(channel), part.pt ());
1117+ if constexpr (!std::is_same_v<Callback, std::nullptr_t >) {
1118+ callback (part, channel);
1119+ }
10021120 }
10031121 }
10041122
@@ -1143,6 +1261,20 @@ class K1AnalysisMicroCore
11431261 AxisSpec axisAnti = {BinAnti::kNAEnd , 0 , BinAnti::kNAEnd , " Type of bin: Normal or Anti" };
11441262 AxisSpec axisType = {BinType::kTYEnd , 0 , BinType::kTYEnd , " Type of bin with charge and mix" };
11451263
1264+ if (mLooseOptions .audit ) {
1265+ auto flow = histos.add <TH2 >(" ML/looseCutflow" , " US triplets;stage;signal stratum" , HistType::kTH2D , {{6 , -0.5 , 5.5 }, {5 , -0.5 , 4.5 }});
1266+ const std::array<const char *, 6 > labels{" structural US" , " loose acceptance" , " track quality" , " TOF + PID" , " pair requirements" , " selected US" };
1267+ const std::array<const char *, 5 > strata{" all US" , " rhoK+" , " rhoK-" , " KstarPi+" , " KstarPi-" };
1268+ for (std::size_t i = 0 ; i < labels.size (); ++i) {
1269+ flow->GetXaxis ()->SetBinLabel (i + 1 , labels[i]);
1270+ }
1271+ for (std::size_t i = 0 ; i < strata.size (); ++i) {
1272+ flow->GetYaxis ()->SetBinLabel (i + 1 , strata[i]);
1273+ }
1274+ histos.add (" ML/looseMassPtActivity" , " Loose US;mass (GeV/c^{2});pT (GeV/c);FT0M percentile" , HistType::kTH3D ,
1275+ {{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" }});
1276+ }
1277+
11461278 // Micro-only instrumentation: category 0 includes all combinations, not just unmatched.
11471279 auto trackFlow = histos.add <TH2 >(" CutFlow/tracks" , " Micro tracks, once per selected collision;stage;species" , HistType::kTH2D , {{static_cast <int >(kTrkNStages ), -0.5 , static_cast <int >(kTrkNStages ) - 0.5 }, {2 , -0.5 , 1.5 }});
11481280 const std::array<const char *, TrackStage::kTrkNStages > trackLabels{" input" , " pT" , " eta" , " DCAxy" , " DCAz" , " track flags" , " clusters / crossed rows" , " TOF required" , " PID" };
@@ -1299,6 +1431,7 @@ class K1AnalysisMicroCore
12991431 SecondaryCuts mSecondaryCuts ;
13001432 CandidateCuts mCandidateCuts ;
13011433 HistogramOptions mHistogramOptions ;
1434+ LooseStageOptions mLooseOptions ;
13021435
13031436 // Derived once in init(): which candidate cuts are switched on.
13041437 bool mSecondaryWindowOn = false ;
0 commit comments