Skip to content

Commit 58087ca

Browse files
committed
[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.
1 parent e0e3992 commit 58087ca

5 files changed

Lines changed: 1449 additions & 34 deletions

File tree

‎PWGLF/Core/K1AnalysisMicroCore.h‎

Lines changed: 167 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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>
@@ -40,6 +41,7 @@
4041
#include <cstdint>
4142
#include <limits>
4243
#include <set>
44+
#include <type_traits>
4345
#include <vector>
4446

4547
namespace 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.
104116
struct 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).
225244
inline 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

Comments
 (0)