Skip to content

Commit 6d3e25c

Browse files
committed
[PWGJE] Add ParT b-jet tagger to jetTaggerHF
1 parent b313b31 commit 6d3e25c

3 files changed

Lines changed: 223 additions & 1 deletion

File tree

‎PWGJE/Core/JetTaggingUtilities.h‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,65 @@ void analyzeJetTrackInfo4GNN(AnalysisJet const& analysisJet, AnyTracks const& /*
11551155
}
11561156
}
11571157

1158+
// Looping over the track info and putting them in the input vectors (for ParT b-jet tagging).
1159+
template <typename AnalysisJet, typename AnyTracks>
1160+
void analyzeJetTrackInfo4ParT(AnalysisJet const& analysisJet, AnyTracks const& /*allTracks*/, std::vector<std::vector<float>>& featConstit, std::vector<std::vector<float>>& vecConstit, float trackPtMin = 0.5, float trackDcaXYMax = 10.0, float trackDcaZMax = 10.0)
1161+
{
1162+
for (const auto& constituent : analysisJet.template tracks_as<AnyTracks>()) {
1163+
1164+
if (constituent.pt() < trackPtMin || !trackAcceptanceWithDca(constituent, trackDcaXYMax, trackDcaZMax)) {
1165+
continue;
1166+
}
1167+
1168+
int sign = getGeoSign(analysisJet, constituent);
1169+
1170+
float trackPt = constituent.pt();
1171+
float trackEta = constituent.eta();
1172+
float trackPhi = constituent.phi();
1173+
1174+
float ip2d = std::abs(constituent.dcaXY()) * sign;
1175+
float ipz = std::abs(constituent.dcaZ()) * sign;
1176+
float ip2dSigma = constituent.sigmadcaXY();
1177+
float ipzSigma = constituent.sigmadcaZ();
1178+
1179+
float dPhi = std::atan2(std::sin(trackPhi - analysisJet.phi()), std::cos(trackPhi - analysisJet.phi()));
1180+
1181+
featConstit.emplace_back(std::vector<float>{
1182+
std::log(trackPt),
1183+
std::log(trackPt / analysisJet.pt()),
1184+
jetutilities::deltaR(analysisJet, constituent),
1185+
trackEta - analysisJet.eta(),
1186+
dPhi,
1187+
std::tanh(ip2d),
1188+
std::tanh(ipz),
1189+
(ip2dSigma != 0.f) ? ip2d / ip2dSigma : 0.f,
1190+
(ipzSigma != 0.f) ? ipz / ipzSigma : 0.f,
1191+
static_cast<float>(constituent.sign())});
1192+
1193+
vecConstit.emplace_back(std::vector<float>{
1194+
trackPt * std::cos(trackPhi),
1195+
trackPt * std::sin(trackPhi),
1196+
trackPt * std::sinh(trackEta),
1197+
trackPt * std::cosh(trackEta)});
1198+
}
1199+
}
1200+
1201+
// Discriminant value. Probability order is assumed to be [b, c, lf].
1202+
template <typename T>
1203+
T getDbFromProb(const std::vector<T>& probs, double fC = 0.018)
1204+
{
1205+
// MlResponse::getModelOutput() returns exactly nClassesMl entries, so a mismatched
1206+
// nClassesMl silently truncates the model output and would read out of bounds here.
1207+
if (probs.size() < 3) {
1208+
LOG(fatal) << "getDbFromProb expects 3 class probabilities but received " << probs.size() << ". Set the nClassesMl configurable to 3 (and give cutDirMl 3 entries to match).";
1209+
}
1210+
T denominator = fC * probs[1] + (1. - fC) * probs[2];
1211+
if (probs[0] <= 0. || denominator <= 0.) {
1212+
return std::numeric_limits<T>::quiet_NaN();
1213+
}
1214+
return std::log(probs[0] / denominator);
1215+
}
1216+
11581217
// Discriminant value for GNN b-jet tagging
11591218
template <typename T>
11601219
T getDb(const std::vector<T>& logits, double fC = 0.018)

‎PWGJE/Core/MlResponseHfTagging.h‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ class MlResponseHfTagging : public MlResponse<TypeOutputScore>
119119
/// @return number of output nodes
120120
int getOutputNodes(int imod = 0) const { return this->mModels[imod].getNumOutputNodes(); }
121121

122+
/// @brief Run a model that takes several pre-built input tensors (e.g. ParT: pf_features, pf_vectors, pf_mask inputs).
123+
/// \param input the input tensors, in the order the graph declares them
124+
/// \param nModel index of the model
125+
/// @return the model output, mNClasses entries long
126+
template <typename T>
127+
std::vector<TypeOutputScore> getModelOutputMultiInput(T& input, int nModel = 0)
128+
{
129+
// evalModel returns an owning copy of the (last) output tensor of the model
130+
std::vector<TypeOutputScore> output = this->mModels[nModel].template evalModel<TypeOutputScore>(input);
131+
if (output.size() < this->mNClasses) {
132+
LOG(fatal) << "Model " << this->mPaths[nModel] << " returned " << output.size() << " scores, but " << static_cast<int>(this->mNClasses) << " classes are expected. Please check your configurables.";
133+
}
134+
output.resize(this->mNClasses);
135+
return output;
136+
}
137+
122138
/// Method to fill the inputs of jet, tracks and secondary vertices
123139
/// \param jet is the b-jet candidate
124140
/// \param tracks is the vector of tracks associated to the jet
@@ -483,6 +499,85 @@ class GNNBjetAllocator : public TensorAllocator
483499
}
484500
};
485501

502+
// TensorAllocator for the ParT b-jet tagger.
503+
// Inputs, in graph order: pf_features (1, nFeat, nConstit), pf_vectors (1, nVec, nConstit),
504+
// pf_mask (1, 1, nConstit).
505+
class ParTBjetAllocator : public TensorAllocator
506+
{
507+
private:
508+
int64_t nConstit;
509+
int64_t nFeat;
510+
int64_t nVec;
511+
bool padWrap;
512+
513+
// Replace non-finite values in a vector, which the model cannot consume
514+
template <typename T>
515+
static void replaceNonFinite(std::vector<T>& vec, T value)
516+
{
517+
for (auto& el : vec) { // o2-linter: disable=const-ref-in-for-loop
518+
if (std::isnan(el) || std::isinf(el)) {
519+
el = value;
520+
}
521+
}
522+
}
523+
524+
public:
525+
ParTBjetAllocator() : TensorAllocator(), nConstit(32), nFeat(10), nVec(4), padWrap(true) {}
526+
ParTBjetAllocator(int64_t nConstit, int64_t nFeat, int64_t nVec, bool padWrap = true)
527+
: TensorAllocator(), nConstit(nConstit), nFeat(nFeat), nVec(nVec), padWrap(padWrap) {}
528+
~ParTBjetAllocator() = default;
529+
530+
ParTBjetAllocator& operator=(const ParTBjetAllocator& other)
531+
{
532+
nConstit = other.nConstit;
533+
nFeat = other.nFeat;
534+
nVec = other.nVec;
535+
padWrap = other.padWrap;
536+
return *this;
537+
}
538+
539+
// Allocate and return the ParT input tensors (std::vector<Ort::Value>).
540+
// featData/vecData/maskData back the tensors
541+
void getParTInput(const std::vector<std::vector<float>>& featConstit, const std::vector<std::vector<float>>& vecConstit, std::vector<float>& featData, std::vector<float>& vecData, std::vector<float>& maskData, std::vector<Ort::Value>& partInput)
542+
{
543+
featData.assign(nFeat * nConstit, 0.f);
544+
vecData.assign(nVec * nConstit, 0.f);
545+
maskData.assign(nConstit, 0.f);
546+
547+
int64_t nSelected = static_cast<int64_t>(featConstit.size());
548+
const int64_t nReal = (nSelected < nConstit) ? nSelected : nConstit; // jets longer than nConstit are truncated
549+
550+
for (int64_t iConstit = 0; iConstit < nConstit; ++iConstit) {
551+
int64_t iSource = iConstit;
552+
if (iConstit < nReal) {
553+
maskData[iConstit] = 1.f;
554+
} else {
555+
if (!padWrap || nReal == 0) {
556+
continue; // zero padding, mask stays 0
557+
}
558+
iSource = iConstit % nReal; // weaver 'wrap' padding: cyclic repetition, mask stays 0
559+
}
560+
for (int64_t iFeat = 0; iFeat < nFeat; ++iFeat) {
561+
featData[iFeat * nConstit + iConstit] = featConstit[iSource][iFeat];
562+
}
563+
for (int64_t iVec = 0; iVec < nVec; ++iVec) {
564+
vecData[iVec * nConstit + iConstit] = vecConstit[iSource][iVec];
565+
}
566+
}
567+
568+
replaceNonFinite(featData, 0.f);
569+
replaceNonFinite(vecData, 0.f);
570+
571+
std::vector<int64_t> featShape{1, nFeat, nConstit};
572+
std::vector<int64_t> vecShape{1, nVec, nConstit};
573+
std::vector<int64_t> maskShape{1, 1, nConstit};
574+
575+
partInput.emplace_back(createTensor(featData, featShape));
576+
partInput.emplace_back(createTensor(vecData, vecShape));
577+
partInput.emplace_back(createTensor(maskData, maskShape));
578+
}
579+
};
580+
486581
} // namespace o2::analysis
487582

488583
#undef FILL_MAP_BJET

‎PWGJE/TableProducer/jetTaggerHF.cxx‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,17 @@ struct JetTaggerHFTask {
145145
ConfigurableAxis binTrackProbability{"binTrackProbability", {100, 0.f, 1.f}, ""};
146146
ConfigurableAxis binJetFlavour{"binJetFlavour", {6, -0.5, 5.5}, ""};
147147

148+
// ParT (Particle Transformer) tagger. Grouped in a ConfigurableGroup because
149+
// StructToTuple.h supports at most 99 task members and this task is near the limit.
150+
struct ParTCfg : ConfigurableGroup {
151+
std::string prefix = "part";
152+
Configurable<int64_t> nConstit{"nConstit", 32, "fixed number of constituents fed to the ParT model, must match training"};
153+
Configurable<int64_t> nFeat{"nFeat", 10, "number of pf_features channels"};
154+
Configurable<int64_t> nVec{"nVec", 4, "number of pf_vectors channels"};
155+
Configurable<bool> padWrap{"padWrap", true, "pad short jets by cyclic repetition (weaver 'wrap' mode); set false for zero padding"};
156+
Configurable<int> minTracks{"minTracks", 2, "minimum number of selected constituents; training applied jet_nTracks >= 2"};
157+
} parTCfg;
158+
148159
o2::analysis::MlResponseHfTagging<float> bMlResponse;
149160
o2::ccdb::CcdbApi ccdbApi;
150161

@@ -277,7 +288,7 @@ struct JetTaggerHFTask {
277288
}
278289
}
279290
}
280-
if (doprocessAlgorithmGNN || doprocessAlgorithmGNNwExtra) {
291+
if (doprocessAlgorithmGNN || doprocessAlgorithmGNNwExtra || doprocessAlgorithmParT) {
281292
if (jet.pt() >= jetpTMin) {
282293
float dbRange;
283294
if (scoreML[jet.globalIndex()] < dbMin) {
@@ -525,9 +536,24 @@ struct JetTaggerHFTask {
525536
bMlResponse.init();
526537
}
527538

539+
if (doprocessAlgorithmParT) {
540+
bMlResponse.configure(binsPtMl, cutsMl, cutDirMl, nClassesMl);
541+
if (loadModelsFromCCDB) {
542+
ccdbApi.init(ccdbUrl);
543+
bMlResponse.setModelPathsCCDB(onnxFileNames, ccdbApi, modelPathsCCDB, timestampCCDB);
544+
} else {
545+
bMlResponse.setModelPathsLocal(onnxFileNames);
546+
}
547+
bMlResponse.init();
548+
549+
LOGF(info, "ParT: loaded %s with %d output node(s); %d constituents, %s padding", onnxFileNames.value[0].c_str(), bMlResponse.getOutputNodes(), static_cast<int>(parTCfg.nConstit.value), parTCfg.padWrap.value ? "wrap" : "zero");
550+
}
551+
528552
if (doprocessAlgorithmGNN || doprocessAlgorithmGNNwExtra) {
529553
tensorAlloc = o2::analysis::GNNBjetAllocator(nJetFeat.value, nTrkFeat.value, nClassesMl.value, nTrkOrigin.value, transformFeatureJetMean.value, transformFeatureJetStdev.value, transformFeatureTrkMean.value, transformFeatureTrkStdev.value, nJetConst, tfFuncTypeGNN.value);
554+
}
530555

556+
if (doprocessAlgorithmGNN || doprocessAlgorithmGNNwExtra || doprocessAlgorithmParT) {
531557
registry.add("h2_count_db", "#it{D}_{b} underflow/overflow;Jet flavour;#it{D}_{b} range", {HistType::kTH2F, {{4, 0., 4.}, {3, 0., 3.}}});
532558
auto h2CountDb = registry.get<TH2>(HIST("h2_count_db"));
533559
h2CountDb->GetXaxis()->SetBinLabel(1, "b-jet");
@@ -669,6 +695,42 @@ struct JetTaggerHFTask {
669695
}
670696
}
671697

698+
template <typename AnyJets, typename AnyTracks>
699+
void analyzeJetAlgorithmParT(AnyJets const& jets, AnyTracks const& tracks)
700+
{
701+
o2::analysis::ParTBjetAllocator parTTensorAlloc(parTCfg.nConstit.value, parTCfg.nFeat.value, parTCfg.nVec.value, parTCfg.padWrap.value);
702+
703+
for (const auto& jet : jets) {
704+
std::vector<std::vector<float>> featConstit;
705+
std::vector<std::vector<float>> vecConstit;
706+
jettaggingutilities::analyzeJetTrackInfo4ParT(jet, tracks, featConstit, vecConstit, trackPtMin, trackDcaXYMax, trackDcaZMax);
707+
708+
// Training applied a jet_nTracks >= 2 selection, so jets below it are out of distribution.
709+
if (static_cast<int>(featConstit.size()) < parTCfg.minTracks) {
710+
scoreML[jet.globalIndex()] = -999.;
711+
LOGF(debug, "doprocessAlgorithmParT, too few selected constituents (%d)", jet.globalIndex());
712+
continue;
713+
}
714+
715+
std::vector<float> featData;
716+
std::vector<float> vecData;
717+
std::vector<float> maskData;
718+
std::vector<Ort::Value> parTInput;
719+
parTTensorAlloc.getParTInput(featConstit, vecConstit, featData, vecData, maskData, parTInput);
720+
721+
// The exported graph applies the softmax itself, so the output is already
722+
// [p_b, p_c, p_udsg] and must not be passed through getDb(), which would softmax again.
723+
auto modelOutput = bMlResponse.getModelOutputMultiInput(parTInput, 0);
724+
float db = jettaggingutilities::getDbFromProb(modelOutput, fC);
725+
if (!std::isnan(db)) {
726+
scoreML[jet.globalIndex()] = db;
727+
} else {
728+
scoreML[jet.globalIndex()] = 999.;
729+
LOGF(debug, "doprocessAlgorithmParT, Db is NaN (%d)", jet.globalIndex());
730+
}
731+
}
732+
}
733+
672734
void processDummy(aod::JetCollisions const&)
673735
{
674736
}
@@ -725,6 +787,12 @@ struct JetTaggerHFTask {
725787
}
726788
PROCESS_SWITCH(JetTaggerHFTask, processAlgorithmGNN, "Fill GNN evaluation score (D_b) for charged jets", false);
727789

790+
void processAlgorithmParT(JetTable const& jets, JetTracksExt const& jtracks)
791+
{
792+
analyzeJetAlgorithmParT(jets, jtracks);
793+
}
794+
PROCESS_SWITCH(JetTaggerHFTask, processAlgorithmParT, "Fill ParT evaluation score (D_b) for charged jets", false);
795+
728796
void processFillTables(std::conditional_t<isMCD, soa::Join<JetTable, aod::ChargedMCDetectorLevelJetFlavourDef>, JetTable>::iterator const& jet, JetTracksExt const& tracks)
729797
{
730798
fillTables<isMCD>(jet, tracks);

0 commit comments

Comments
 (0)