Skip to content

Commit 1689bad

Browse files
author
Henrik Fribert
committed
Feat: Task for evaluating combined PID
1 parent 39ed52a commit 1689bad

File tree

2 files changed

+379
-1
lines changed

2 files changed

+379
-1
lines changed

ALICE3/Tasks/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,7 @@ o2physics_add_dpl_workflow(alice3-tracking-performance
7474
PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore
7575
COMPONENT_NAME Analysis)
7676

77-
77+
o2physics_add_dpl_workflow(alice3-pid-evaluation
78+
SOURCES alice3PidEvaluation.cxx
79+
PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore
80+
COMPONENT_NAME Analysis)
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
///
12+
/// \file alice3PidEvaluation.cxx
13+
///
14+
/// \brief This task computes purity and efficiency from the OTF PID tables for multiple detectors.
15+
/// Analyzes individual detectors (Tracker, TOF Inner, TOF Outer, RICH),
16+
/// as well as combined detector performance using quadrature combination
17+
/// of nSigma values.
18+
///
19+
/// \author Henrik Fribert TUM
20+
/// \since August 14, 2025
21+
///
22+
23+
#include "ALICE3/DataModel/OTFPIDTrk.h"
24+
#include "ALICE3/DataModel/OTFRICH.h"
25+
#include "ALICE3/DataModel/OTFTOF.h"
26+
#include "Common/DataModel/TrackSelectionTables.h"
27+
28+
#include "CommonUtils/NameConf.h"
29+
#include "Framework/ASoAHelpers.h"
30+
#include "Framework/AnalysisDataModel.h"
31+
#include "Framework/AnalysisTask.h"
32+
#include "Framework/HistogramRegistry.h"
33+
#include "Framework/RunningWorkflowInfo.h"
34+
#include "Framework/runDataProcessing.h"
35+
#include "ReconstructionDataFormats/DCA.h"
36+
37+
#include "TH1F.h"
38+
#include "TH2F.h"
39+
#include "TProfile.h"
40+
#include "TVector3.h"
41+
42+
#include <array>
43+
#include <vector>
44+
45+
using namespace o2;
46+
using namespace o2::framework;
47+
48+
struct Alice3PidEvaluation {
49+
50+
HistogramRegistry histos{"histos", {}, OutputObjHandlingPolicy::AnalysisObject};
51+
52+
static constexpr float kInvalidNSigmaValue = 999.0f;
53+
54+
Configurable<float> maxNSigmaForIdentification{"maxNSigmaForIdentification", 3.0f, "Maximum |nSigma| allowed for particle identification (closest-hypothesis rule)"};
55+
Configurable<int> numLogBins{"numLogBins", 200, "Number of logarithmic momentum bins"};
56+
Configurable<bool> useClosestHypothesisRule{"useClosestHypothesisRule", true, "Use closest-hypothesis rule: assign track to hypothesis with smallest |nSigma|"};
57+
Configurable<bool> useMinimalIdentification{"useMinimalIdentification", false, "Require that only one hypothesis is within the cutoff"};
58+
Configurable<bool> includeTrackerInCombined{"includeTrackerInCombined", true, "Include Tracker in combined analysis"};
59+
Configurable<bool> includeTofInnerInCombined{"includeTofInnerInCombined", true, "Include TOF Inner in combined analysis"};
60+
Configurable<bool> includeTofOuterInCombined{"includeTofOuterInCombined", true, "Include TOF Outer in combined analysis"};
61+
Configurable<bool> includeRichInCombined{"includeRichInCombined", false, "Include RICH in combined analysis"};
62+
63+
std::vector<double> mLogBins;
64+
65+
enum PidHypothesis { kElectron,
66+
kMuon,
67+
kPion,
68+
kKaon,
69+
kProton,
70+
kDeuteron,
71+
kTriton,
72+
kHelium3,
73+
kAlpha,
74+
kCount };
75+
static constexpr std::array<int, PidHypothesis::kCount> kHypothesisPdg = {11, 13, 211, 321, 2212, 1000010020, 1000010030, 1000020030, 1000020040};
76+
static constexpr std::array<const char*, PidHypothesis::kCount> kHypothesisNames = {"Electron", "Muon", "Pion", "Kaon", "Proton", "Deuteron", "Triton", "Helium3", "Alpha"};
77+
78+
struct DetectorHistograms {
79+
std::array<std::shared_ptr<TH1>, PidHypothesis::kCount> hTotalTrue;
80+
std::array<std::shared_ptr<TProfile>, PidHypothesis::kCount> hEfficiency;
81+
std::array<std::shared_ptr<TProfile>, PidHypothesis::kCount> hPurityAsHypothesis;
82+
};
83+
84+
std::shared_ptr<TH2> hDetectorParticipation2D;
85+
86+
DetectorHistograms trackerHists;
87+
DetectorHistograms tofInnerHists;
88+
DetectorHistograms tofOuterHists;
89+
DetectorHistograms richHists;
90+
DetectorHistograms combinedHists;
91+
DetectorHistograms combinedNoTrackerHists;
92+
void init(o2::framework::InitContext&)
93+
{
94+
LOG(info) << "Initializing multi-detector PID evaluation using closest-hypothesis rule";
95+
LOG(info) << "Maximum |nSigma| for identification: " << maxNSigmaForIdentification.value;
96+
LOG(info) << "Closest-hypothesis rule: " << (useClosestHypothesisRule.value ? "ENABLED" : "DISABLED");
97+
LOG(info) << "Require unique identification: " << (useMinimalIdentification.value ? "YES" : "NO");
98+
LOG(info) << "Combined analysis includes: "
99+
<< (includeTrackerInCombined.value ? "Tracker " : "")
100+
<< (includeTofInnerInCombined.value ? "TOF_Inner " : "")
101+
<< (includeTofOuterInCombined.value ? "TOF_Outer " : "")
102+
<< (includeRichInCombined.value ? "RICH " : "");
103+
104+
mLogBins.clear();
105+
double pMin = 0.05;
106+
double pMax = 10;
107+
double logMin = std::log10(pMin);
108+
double logMax = std::log10(pMax);
109+
double dLog = (logMax - logMin) / numLogBins.value;
110+
for (int i = 0; i <= numLogBins.value; ++i) {
111+
mLogBins.push_back(std::pow(10, logMin + i * dLog));
112+
}
113+
const AxisSpec axisMomentum{mLogBins, "#it{p} (GeV/#it{c})"};
114+
115+
auto createDetectorHistograms = [&](DetectorHistograms& detHists, const std::string& detectorName) {
116+
for (int trueIdx = 0; trueIdx < PidHypothesis::kCount; ++trueIdx) {
117+
const auto& trueName = kHypothesisNames[trueIdx];
118+
detHists.hTotalTrue[trueIdx] = histos.add<TH1>(Form("%s/hTotalTrue%s", detectorName.c_str(), trueName),
119+
Form("%s: Total True %s; #it{p} (GeV/#it{c})", detectorName.c_str(), trueName),
120+
kTH1F, {axisMomentum});
121+
detHists.hEfficiency[trueIdx] = histos.add<TProfile>(Form("%s/hEfficiency%s", detectorName.c_str(), trueName),
122+
Form("%s: PID Efficiency for %s; #it{p} (GeV/#it{c}); Efficiency", detectorName.c_str(), trueName),
123+
kTProfile, {axisMomentum});
124+
}
125+
126+
for (int hypIdx = 0; hypIdx < PidHypothesis::kCount; ++hypIdx) {
127+
const auto& hypName = kHypothesisNames[hypIdx];
128+
detHists.hPurityAsHypothesis[hypIdx] = histos.add<TProfile>(Form("%s/hPurityAs%s", detectorName.c_str(), hypName),
129+
Form("%s: Purity when selecting as %s; #it{p} (GeV/#it{c}); Purity", detectorName.c_str(), hypName),
130+
kTProfile, {axisMomentum});
131+
}
132+
};
133+
134+
createDetectorHistograms(trackerHists, "Tracker");
135+
createDetectorHistograms(tofInnerHists, "TOF_Inner");
136+
createDetectorHistograms(tofOuterHists, "TOF_Outer");
137+
createDetectorHistograms(richHists, "RICH");
138+
createDetectorHistograms(combinedHists, "Combined");
139+
createDetectorHistograms(combinedNoTrackerHists, "Combined_NoTracker");
140+
141+
const AxisSpec axisDetectorCount{5, -0.5, 4.5, "Number of detectors"};
142+
hDetectorParticipation2D = histos.add<TH2>("Combined/hDetectorParticipation2D",
143+
"Detector participation vs momentum; #it{p} (GeV/#it{c}); Number of detectors",
144+
kTH2F, {axisMomentum, axisDetectorCount});
145+
}
146+
147+
void process(soa::Join<aod::Tracks, aod::TracksCov, aod::McTrackLabels, aod::UpgradeTrkPids, aod::UpgradeTrkPidSignals, aod::UpgradeTofs, aod::UpgradeRichs> const& tracks,
148+
aod::McParticles const& /*mcParticles*/)
149+
{
150+
int totalTracks = 0;
151+
int analyzedTracks = 0;
152+
153+
auto isValidNSigma = [](float nSigma) -> bool {
154+
return (nSigma < kInvalidNSigmaValue && nSigma > -kInvalidNSigmaValue);
155+
};
156+
157+
auto computeCombinedNSigma = [&](const std::vector<std::array<float, PidHypothesis::kCount>>& detectorNSigmas, float p) -> std::array<float, PidHypothesis::kCount> {
158+
std::array<float, PidHypothesis::kCount> combinedNSigma;
159+
int totalValidDetectors = 0;
160+
for (const auto& detNSigma : detectorNSigmas) {
161+
bool detectorHasValidMeasurement = false;
162+
for (int hypIdx = 0; hypIdx < PidHypothesis::kCount; ++hypIdx) {
163+
if (isValidNSigma(detNSigma[hypIdx])) {
164+
detectorHasValidMeasurement = true;
165+
break;
166+
}
167+
}
168+
if (detectorHasValidMeasurement) {
169+
totalValidDetectors++;
170+
}
171+
}
172+
173+
for (int hypIdx = 0; hypIdx < PidHypothesis::kCount; ++hypIdx) {
174+
float sumSquares = 0.0f;
175+
int validDetectors = 0;
176+
177+
for (const auto& detNSigma : detectorNSigmas) {
178+
if (isValidNSigma(detNSigma[hypIdx])) {
179+
sumSquares += detNSigma[hypIdx] * detNSigma[hypIdx];
180+
validDetectors++;
181+
}
182+
}
183+
184+
if (validDetectors > 0) {
185+
combinedNSigma[hypIdx] = std::sqrt(sumSquares);
186+
} else {
187+
combinedNSigma[hypIdx] = kInvalidNSigmaValue;
188+
}
189+
}
190+
if (totalValidDetectors > 0) {
191+
hDetectorParticipation2D->Fill(p, totalValidDetectors);
192+
}
193+
194+
return combinedNSigma;
195+
};
196+
197+
auto analyzeDetector = [&](DetectorHistograms& detHists, const std::array<float, PidHypothesis::kCount>& nSigmaValues,
198+
int trueParticleIndex, float p) {
199+
detHists.hTotalTrue[trueParticleIndex]->Fill(p);
200+
bool hasValidNSigma = false;
201+
for (int i = 0; i < PidHypothesis::kCount; ++i) {
202+
if (isValidNSigma(nSigmaValues[i])) {
203+
hasValidNSigma = true;
204+
break;
205+
}
206+
}
207+
if (!hasValidNSigma) {
208+
return;
209+
}
210+
211+
bool correctlyIdentified = false;
212+
int selectedHypothesis = -1;
213+
214+
if (useClosestHypothesisRule.value) {
215+
float minAbsNSigma = kInvalidNSigmaValue;
216+
int bestHypothesis = -1;
217+
int validHypothesesCount = 0;
218+
219+
for (int hypIdx = 0; hypIdx < PidHypothesis::kCount; ++hypIdx) {
220+
if (isValidNSigma(nSigmaValues[hypIdx])) {
221+
float absNSigma = std::fabs(nSigmaValues[hypIdx]);
222+
if (absNSigma < minAbsNSigma) {
223+
minAbsNSigma = absNSigma;
224+
bestHypothesis = hypIdx;
225+
}
226+
if (absNSigma < maxNSigmaForIdentification.value) {
227+
validHypothesesCount++;
228+
}
229+
}
230+
}
231+
232+
if (bestHypothesis >= 0 && minAbsNSigma < maxNSigmaForIdentification.value) {
233+
if (useMinimalIdentification.value && validHypothesesCount > 1) {
234+
selectedHypothesis = -1;
235+
} else {
236+
selectedHypothesis = bestHypothesis;
237+
}
238+
}
239+
correctlyIdentified = (selectedHypothesis == trueParticleIndex);
240+
} else {
241+
correctlyIdentified = (std::fabs(nSigmaValues[trueParticleIndex]) < maxNSigmaForIdentification.value);
242+
for (int hypIdx = 0; hypIdx < PidHypothesis::kCount; ++hypIdx) {
243+
if (std::fabs(nSigmaValues[hypIdx]) < maxNSigmaForIdentification.value) {
244+
bool isCorrect = (hypIdx == trueParticleIndex);
245+
detHists.hPurityAsHypothesis[hypIdx]->Fill(p, isCorrect ? 1.0 : 0.0);
246+
}
247+
}
248+
}
249+
250+
detHists.hEfficiency[trueParticleIndex]->Fill(p, correctlyIdentified ? 1.0 : 0.0);
251+
252+
if (useClosestHypothesisRule.value && selectedHypothesis >= 0) {
253+
bool isCorrect = (selectedHypothesis == trueParticleIndex);
254+
detHists.hPurityAsHypothesis[selectedHypothesis]->Fill(p, isCorrect ? 1.0 : 0.0);
255+
}
256+
};
257+
258+
for (const auto& track : tracks) {
259+
totalTracks++;
260+
261+
if (!track.has_mcParticle()) {
262+
continue;
263+
}
264+
265+
const auto& mcParticle = track.mcParticle();
266+
const float p = mcParticle.p();
267+
const int truePdg = std::abs(mcParticle.pdgCode());
268+
269+
int trueParticleIndex = -1;
270+
for (int i = 0; i < PidHypothesis::kCount; ++i) {
271+
if (kHypothesisPdg[i] == truePdg) {
272+
trueParticleIndex = i;
273+
break;
274+
}
275+
}
276+
if (trueParticleIndex == -1) {
277+
continue;
278+
}
279+
280+
std::array<float, PidHypothesis::kCount> trackerNSigma;
281+
trackerNSigma[kElectron] = track.nSigmaTrkEl();
282+
trackerNSigma[kMuon] = track.nSigmaTrkMu();
283+
trackerNSigma[kPion] = track.nSigmaTrkPi();
284+
trackerNSigma[kKaon] = track.nSigmaTrkKa();
285+
trackerNSigma[kProton] = track.nSigmaTrkPr();
286+
trackerNSigma[kDeuteron] = track.nSigmaTrkDe();
287+
trackerNSigma[kTriton] = track.nSigmaTrkTr();
288+
trackerNSigma[kHelium3] = track.nSigmaTrkHe();
289+
trackerNSigma[kAlpha] = track.nSigmaTrkAl();
290+
291+
analyzedTracks++;
292+
293+
analyzeDetector(trackerHists, trackerNSigma, trueParticleIndex, p);
294+
295+
std::array<float, PidHypothesis::kCount> tofInnerNSigma;
296+
tofInnerNSigma[kElectron] = track.nSigmaElectronInnerTOF();
297+
tofInnerNSigma[kMuon] = track.nSigmaMuonInnerTOF();
298+
tofInnerNSigma[kPion] = track.nSigmaPionInnerTOF();
299+
tofInnerNSigma[kKaon] = track.nSigmaKaonInnerTOF();
300+
tofInnerNSigma[kProton] = track.nSigmaProtonInnerTOF();
301+
tofInnerNSigma[kDeuteron] = track.nSigmaDeuteronInnerTOF();
302+
tofInnerNSigma[kTriton] = track.nSigmaTritonInnerTOF();
303+
tofInnerNSigma[kHelium3] = track.nSigmaHelium3InnerTOF();
304+
tofInnerNSigma[kAlpha] = track.nSigmaAlphaInnerTOF();
305+
306+
analyzeDetector(tofInnerHists, tofInnerNSigma, trueParticleIndex, p);
307+
308+
std::array<float, PidHypothesis::kCount> tofOuterNSigma;
309+
tofOuterNSigma[kElectron] = track.nSigmaElectronOuterTOF();
310+
tofOuterNSigma[kMuon] = track.nSigmaMuonOuterTOF();
311+
tofOuterNSigma[kPion] = track.nSigmaPionOuterTOF();
312+
tofOuterNSigma[kKaon] = track.nSigmaKaonOuterTOF();
313+
tofOuterNSigma[kProton] = track.nSigmaProtonOuterTOF();
314+
tofOuterNSigma[kDeuteron] = track.nSigmaDeuteronOuterTOF();
315+
tofOuterNSigma[kTriton] = track.nSigmaTritonOuterTOF();
316+
tofOuterNSigma[kHelium3] = track.nSigmaHelium3OuterTOF();
317+
tofOuterNSigma[kAlpha] = track.nSigmaAlphaOuterTOF();
318+
319+
analyzeDetector(tofOuterHists, tofOuterNSigma, trueParticleIndex, p);
320+
321+
std::array<float, PidHypothesis::kCount> richNSigma;
322+
richNSigma[kElectron] = track.nSigmaElectronRich();
323+
richNSigma[kMuon] = track.nSigmaMuonRich();
324+
richNSigma[kPion] = track.nSigmaPionRich();
325+
richNSigma[kKaon] = track.nSigmaKaonRich();
326+
richNSigma[kProton] = track.nSigmaProtonRich();
327+
richNSigma[kDeuteron] = track.nSigmaDeuteronRich();
328+
richNSigma[kTriton] = track.nSigmaTritonRich();
329+
richNSigma[kHelium3] = track.nSigmaHelium3Rich();
330+
richNSigma[kAlpha] = track.nSigmaAlphaRich();
331+
332+
analyzeDetector(richHists, richNSigma, trueParticleIndex, p);
333+
334+
std::vector<std::array<float, PidHypothesis::kCount>> allDetectorNSigmas;
335+
336+
if (includeTrackerInCombined.value) {
337+
allDetectorNSigmas.push_back(trackerNSigma);
338+
}
339+
if (includeTofInnerInCombined.value) {
340+
allDetectorNSigmas.push_back(tofInnerNSigma);
341+
}
342+
if (includeTofOuterInCombined.value) {
343+
allDetectorNSigmas.push_back(tofOuterNSigma);
344+
}
345+
if (includeRichInCombined.value) {
346+
allDetectorNSigmas.push_back(richNSigma);
347+
}
348+
if (!allDetectorNSigmas.empty()) {
349+
std::array<float, PidHypothesis::kCount> combinedNSigma = computeCombinedNSigma(allDetectorNSigmas, p);
350+
analyzeDetector(combinedHists, combinedNSigma, trueParticleIndex, p);
351+
}
352+
353+
std::vector<std::array<float, PidHypothesis::kCount>> noTrackerDetectorNSigmas;
354+
355+
if (includeTofInnerInCombined.value) {
356+
noTrackerDetectorNSigmas.push_back(tofInnerNSigma);
357+
}
358+
if (includeTofOuterInCombined.value) {
359+
noTrackerDetectorNSigmas.push_back(tofOuterNSigma);
360+
}
361+
if (includeRichInCombined.value) {
362+
noTrackerDetectorNSigmas.push_back(richNSigma);
363+
}
364+
if (!noTrackerDetectorNSigmas.empty()) {
365+
std::array<float, PidHypothesis::kCount> combinedNoTrackerNSigma = computeCombinedNSigma(noTrackerDetectorNSigmas, p);
366+
analyzeDetector(combinedNoTrackerHists, combinedNoTrackerNSigma, trueParticleIndex, p);
367+
}
368+
}
369+
}
370+
};
371+
372+
WorkflowSpec defineDataProcessing(ConfigContext const& cfgc)
373+
{
374+
return WorkflowSpec{adaptAnalysisTask<Alice3PidEvaluation>(cfgc)};
375+
}

0 commit comments

Comments
 (0)